Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Latent ODE for irregularly-observed Inhomogeneous Poisson Processes

Open In Colab

This notebook combines two ideas from the literature into one xtremax-flavoured example:

The generative model is

z0∼N(0,I),dzdt=flat(z,t),log⁡λ(t)=gθ(z(t)),z_0 \sim \mathcal{N}(0, I), \qquad \frac{dz}{dt} = f_\text{lat}(z, t), \qquad \log\lambda(t) = g_\theta(z(t)),

observed as event times {ti}\{t_i\} from IPP(λ)\mathrm{IPP}(\lambda) on [0,T][0, T].

Inference is amortised with a pure neural-ODE encoder in the spirit of Kidger et al. (2020), “Neural CDEs”: the irregular event sequence is summarised by its counting process N(t)=#{ti≤t}N(t) = \#\{t_i \le t\}, linearly interpolated to a continuous control signal, and fed into an encoder ODE

dhdt=fenc(h(t), N^(t), t),h(0)=0.\frac{dh}{dt} = f_\text{enc}\big(h(t),\, \hat N(t),\, t\big),\qquad h(0) = 0.

The final state h(T)h(T) is mapped to (μ,log⁡σ2)(\mu, \log\sigma^2) of q(z0)q(z_0). No RNN cells anywhere — observations enter the encoder only through the counting-process path.

Likelihood. xtremax’s ipp_log_prob evaluates the standard IPP log-likelihood

log⁡L=∑ilog⁡λ(ti)−Λ(T),Λ(T)=∫0Tλ(s) ds,\log L = \sum_i \log\lambda(t_i) - \Lambda(T), \qquad \Lambda(T) = \int_0^T \lambda(s)\,ds,

given a callable for log⁡λ\log\lambda at requested times and a pre-computed compensator. We build Λ(T)\Lambda(T) with integrate_log_intensity.

Python implementation: CPython
Python version       : 3.13.5
IPython version      : 9.10.0

numpy     : 2.4.4
matplotlib: 3.10.8
jax       : 0.6.2
diffrax   : 0.7.2
interpax  : 0.3.14
optax     : 0.2.8
equinox   : 0.13.7
xtremax   : 0.0.0

Compiler    : GCC 11.2.0
OS          : Linux
Release     : 6.8.0-1044-azure
Machine     : x86_64
Processor   : x86_64
CPU cores   : 16
Architecture: 64bit

1. Synthetic IPP data

We draw 100 sequences from a sinusoidally-modulated rate

λi(t)=ai⋅(1+ampisin⁡(ωt+ϕi)),t∈[0,T].\lambda_i(t) = a_i \cdot \big(1 + \mathrm{amp}_i \sin(\omega t + \phi_i)\big), \qquad t \in [0, T].

Each sequence has its own random amplitude ampi∈[0,0.85]\mathrm{amp}_i \in [0, 0.85], baseline ai∈[3,7]a_i \in [3, 7], and phase ϕi∈[0,2π)\phi_i \in [0, 2\pi) — but a shared frequency ω\omega. The model has access only to event times and must recover the per-sequence intensity from those alone.

sequences           : 100
max-events buffer   : 80
mean events / seq   : 47.0
min/max events      : 22 / 65
<Figure size 1300x750 with 4 Axes>

2. Encoder via the counting process

For each sequence we precompute N(t)=#{ti≤t}N(t) = \#\{t_i \le t\} on a dense reference grid, then linearly interpolate it to a continuous control signal N^(t)\hat N(t) for the encoder ODE. Two notes:

  • The grid is shared across sequences (same shape under vmap), but each sequence has its own count values.

  • Linear interpolation of a step function adds a small smoothing artifact; for a Latent ODE this is fine because the encoder integrates against N^(t)\hat N(t) rather than reading point values. To get the original Kidger et al. natural-cubic-spline behaviour, switch method="linear" to method="cubic2" below.

count_paths shape       : (100, 60)
count_paths[0, ::10]    : [ 0.  7. 14. 28. 30. 34.]

3. Model

Two MLP vector fields and a small log-rate decoder. The encoder vector field takes a concatenation of (t,h,N^(t))(t, h, \hat N(t)); the dynamics field takes (t,z)(t, z); the decoder maps z→log⁡λz \to \log\lambda.

latent dim                : 4
encoder hidden            : 24
decoder hidden            : 16
trainable parameters      : 4661

4. Training

Per gradient step: encode by integrating the encoder ODE forward against the count path; sample z0z_0 via reparameterisation; integrate the latent ODE on both event times and a quadrature grid; assemble the IPP log-likelihood and KL.

epoch    1  β 0.00  loss     5.405  log_lik    -5.405  KL  16.110
epoch   20  β 0.24  loss   -26.225  log_lik    26.625  KL   1.683
epoch   40  β 0.49  loss   -26.171  log_lik    26.691  KL   1.067
epoch   60  β 0.74  loss   -25.568  log_lik    26.092  KL   0.711
epoch   80  β 0.99  loss   -25.840  log_lik    26.325  KL   0.491
epoch  100  β 1.00  loss   -26.088  log_lik    26.543  KL   0.455
epoch  120  β 1.00  loss   -25.935  log_lik    26.242  KL   0.307
epoch  140  β 1.00  loss   -25.959  log_lik    26.330  KL   0.371
epoch  160  β 1.00  loss   -25.904  log_lik    26.266  KL   0.362
epoch  180  β 1.00  loss   -25.999  log_lik    26.459  KL   0.460
epoch  200  β 1.00  loss   -25.814  log_lik    26.197  KL   0.383
<Figure size 700x450 with 1 Axes>

5. Recovered intensities

For each held-out sequence we encode the events, use the mean of q(z0)q(z_0), decode on a dense grid, and overlay the recovered λ^(t)\hat\lambda(t) on the ground-truth λ(t)\lambda(t) along with the event raster.

<Figure size 1300x750 with 4 Axes>

6. Compensator residuals (time-rescaling diagnostic)

For an IPP, Λ(ti)\Lambda(t_i) on the recovered intensity should look like the event times of a homogeneous Poisson process with rate 1. Plotting cumulative event counts vs. Λ(t)\Lambda(t) should yield a straight diagonal — a classical goodness-of-fit check for point-process intensities.

<Figure size 700x650 with 1 Axes>

7. Notes

  • Why a count-path encoder? The original Latent ODE encoder is an ODE-RNN run backwards over the observation times. Replacing it with a pure neural ODE on N^(t)\hat N(t) (Kidger-style) keeps the model uniformly continuous-time — and for a point process, the cumulative count is the natural sufficient statistic the encoder should ingest.

  • Two ODE solves per sequence per gradient step. Encoder forward (drives hh) plus latent ODE forward (drives zz, evaluated at both event times and a quadrature grid). The dominant cost is the latent solve because we save at ∣events∣+Nquad|\text{events}| + N_\text{quad} time points.

  • Why xtremax primitives? ipp_log_prob is the canonical IPP likelihood with mask-aware padding, and integrate_log_intensity is the matching trapezoidal compensator. Plugging neural intensities into them keeps the math identical to the analytic IPP examples in the rest of the docs.

  • Time-rescaling. When the model fits well, Λ^(ti)\hat\Lambda(t_i) behaves like the arrival times of a unit-rate HPP, so the cumulative count is a near-diagonal staircase. Persistent deviation from the diagonal signals model misspecification (e.g., latent dynamics too rigid to capture per-sequence intensity shapes).