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.

Inhomogeneous Spatial Poisson Process — From Scratch and With NumPyro

Open In Colab

An inhomogeneous spatial Poisson process (IPP) on a domain D⊂RdD \subset \mathbb{R}^d has a non-constant intensity λ(s)≥0\lambda(s) \geq 0. The total expected count is the integrated intensity

Λ(D)=∫Dλ(s) ds,\Lambda(D) = \int_D \lambda(s)\, ds,

the count is N∼Poisson(Λ)N \sim \mathrm{Poisson}(\Lambda), and the joint log-likelihood factorises as

log⁡L=∑i=1nlog⁡λ(si)−Λ(D).\log L = \sum_{i=1}^n \log \lambda(s_i) - \Lambda(D).

Inhomogeneity is the bread and butter of applied spatial statistics: disease incidence around risk factors, earthquake epicentres along fault lines, methane plumes near industrial facilities, galaxies in the cosmic web. This notebook walks through the same two faces of xtremax as the homogeneous case.

  1. From scratch — primitives for sampling (Lewis–Shedler thinning), evaluating the joint log-likelihood, and integrating the intensity over the box (trapezoid grid + Halton QMC).

  2. With NumPyro — wrap the IPP as a Distribution and run NUTS to recover hyperparameters of a Gaussian-bump intensity field.

Setup

/home/azureuser/localfiles/xtremax/.venv/bin/python: No module named pip
Note: you may need to restart the kernel to use updated packages.
/home/azureuser/localfiles/xtremax/.venv/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

1. From scratch — the primitives layer

Start with a single Gaussian bump centred at the middle of a [0,10]2[0, 10]^2 domain. The log-intensity is a quadratic in space, so it has a tidy closed form for the integrated intensity (which we’ll use to sanity-check the quadrature primitive).

<Figure size 500x450 with 2 Axes>

Integrated intensity

integrate_log_intensity_spatial computes Λ(D)\Lambda(D) on the rectangular domain. Two methods: tensor-product trapezoid (best for d≤2d \leq 2 and smooth integrands) and Halton QMC (better for d≥3d \geq 3, robust to peaks). We compare against the analytical answer.

Analytical Λ(D)  = 31.4159
Trapezoid (4096) = 31.4159
QMC       (4096) = 31.4339

Sampling — Lewis–Shedler thinning

Sampling an IPP requires a thinning bound λmax⁡≥max⁡sλ(s)\lambda_{\max} \geq \max_s \lambda(s) over the box. Procedure: draw an HPP at rate λmax⁡\lambda_{\max} over DD, then accept each candidate with probability λ(s)/λmax⁡\lambda(s)/\lambda_{\max}. Tighter bounds give higher acceptance.

Drew 467 candidates, 31 accepted (acceptance ≈ 6.64%)
<Figure size 500x500 with 1 Axes>

Joint log-likelihood

The decomposition log⁡L=∑log⁡λ(si)−Λ(D)\log L = \sum \log \lambda(s_i) - \Lambda(D) is implemented directly. A useful diagnostic: drag the candidate parameters away from the truth and watch the log-likelihood drop.

log L at truth = -10.37
log L at shifted peak = -303.70

2. With NumPyro — Bayesian inference for the intensity peak

Treat the centre and amplitude of the Gaussian bump as latent and recover them from the observed pattern. The wrapper class InhomogeneousSpatialPP integrates Λ(D) on every log_prob call so gradients flow into both the sum-of-log-intensities and the −Λ-\Lambda term.

Observed 38 points

                mean       std    median      5.0%     95.0%     n_eff     r_hat
 centre[0]      3.82      0.16      3.81      3.59      4.10   1157.76      1.00
 centre[1]      5.80      0.17      5.80      5.55      6.08    899.85      1.00
   log_amp      1.78      0.17      1.78      1.51      2.05    748.53      1.00

Number of divergences: 0

Posterior should concentrate around the true peak location (4,6)(4, 6) and amplitude log⁡5≈1.61\log 5 \approx 1.61.

<Figure size 1200x350 with 3 Axes>

3. Higher dimensions — same API

The whole pipeline scales to 3-D without code changes — only domain_size grows by one entry.

3-D Λ(D) ≈ 27.393
3-D realisation: 23 points in 4³ box

Where to go from here

  • Marked spatial patterns. Pair each event with a magnitude — see the Marked Spatial PP notebook.

  • Tighter quadrature. For sharply peaked intensities, increase n_integration_points or pass an analytical Λ\Lambda via the operator’s integrated_intensity= argument.

  • Neural intensities. Replace the analytical bump with an eqx.Module (e.g. a small MLP). Its parameters become PyTree leaves and gradients flow through log_prob.