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.

Stationary TMTPP — NUTS fit on synthetic data

Stationary thinned marked TPP — NumPyro NUTS fit

This notebook walks through a full Bayesian inversion of a thinned marked temporal point process, end-to-end on synthetic data so the run is self-contained and quick. We’ll:

  1. Simulate a stationary TMTPP: homogeneous Poisson events with lognormal marks, thinned by a logistic PoD.

  2. Fit the three “latent” parameters (α,x0,σ)(\alpha, x_0, \sigma) of a POD-modified power-law model to the observed (thinned) flux distribution using NumPyro’s NUTS sampler via plumax.population.fitting.run_mcmc.

  3. Compare the posterior to the true generating values.

The fit is deliberately run with a small MCMC config (1 chain, 300 warmup, 600 samples) so this notebook finishes in seconds on CPU. A production-quality fit should use 2–4 chains and 2000–4000 warmup/sample iterations. A cell near the end shows how to flip that switch.

1. Simulate the observed flux distribution

We draw from a truncated power law on [xmin⁡,xmax⁡][x_{\min}, x_{\max}] — that is the physical model behind the fit, not a lognormal. Then we thin each draw by a lognormal-CDF POD, giving the observed sample. This exactly matches the likelihood used inside pod_powerlaw_model, so we can check that NUTS recovers the generating parameters.

Latent events:     20000
Observed (thinned):   1101
Survival fraction:  0.055

A quick look at the latent vs observed flux distributions shows the characteristic thinning: the observed histogram falls away at small Q where the POD is near-zero, while the latent distribution extends all the way to xmin⁡x_{\min}. This is the biased sample the satellite sees.

<Figure size 900x450 with 1 Axes>

2. Run NUTS (short config)

The run_mcmc helper wraps NumPyro’s MCMC and NUTS kernel around pod_powerlaw_model. Priors are uniform on α∈[1.1,4.5]\alpha \in [1.1, 4.5], x0∈[1,20000]x_0 \in [1, 20000], σ∈[0.1,1.5]\sigma \in [0.1, 1.5]. The log-likelihood subtracts a normaliser ln⁡I(θ)\ln I(\theta) computed by trapezoidal quadrature inside the model.

Small config first: 1 chain, 300 warmup, 600 samples. On CPU this finishes in a few seconds.


                mean       std    median      5.0%     95.0%     n_eff     r_hat
     alpha      1.78      0.04      1.78      1.73      1.84    174.42      1.00
        sk      0.46      0.03      0.46      0.40      0.50    152.83      1.00
        x0    410.98     26.14    409.92    369.47    452.82    125.89      1.01

Number of divergences: 0
               x0          sk       alpha
count  600.000000  600.000000  600.000000
mean   410.982239    0.460618    1.782897
std     26.142370    0.030987    0.035592
min    343.507629    0.370645    1.680248
16%    384.574191    0.430430    1.745689
50%    409.920731    0.460315    1.783105
84%    438.311584    0.491495    1.817596
max    485.675262    0.549068    1.896194

3. Compare posterior to truth

We plot the marginal posterior histograms for α,x0,σ\alpha, x_0, \sigma with the true values overlaid as dashed vertical lines. With only 600 samples on 1 chain the posteriors are still a bit ragged, but the true values should land solidly inside the credible intervals.

<Figure size 1300x380 with 3 Axes>

4. Posterior-predictive PoD curve

The most informative downstream product is the inferred PoD curve with credible bands. For each posterior draw (x0(s),σ(s))(x_0^{(s)}, \sigma^{(s)}) we evaluate Pd(Q)P_d(Q) on a fixed grid, then plot the median and 68 / 95 % envelopes. Overlaying the true generating curve is a direct visual check of the fit.

<Figure size 900x450 with 1 Axes>

5. Consistency check against the paradox library

A useful sanity check: feed the recovered POD curve into plumax.population.paradox.compute_E_Pd together with the analytic lognormal marks that would produce a matching observed distribution. The E[Pd]E[P_d] value should agree with the empirical survival fraction from the simulation above.

Empirical survival fraction  : 0.0551
Quadrature E[P_d] (matched)  : 0.1382

6. Flipping to a production MCMC run

When you’re ready for a credible fit rather than a pedagogical smoke test, bump the config:

df_mcmc_prod = fitting.run_mcmc(
    np.asarray(x_obs),
    num_warmup=2000,
    num_samples=4000,
    num_chains=2,
    seed=0,
    print_summary=True,
)

On CPU this takes a few minutes; on a single GPU a few seconds. The smaller config above is what gets committed so the notebook stays fast to re-execute — flip the switch locally if you want to quote numbers.