The generative log-normal Poisson model first starts with a normally
distributed random field \(s(x)\) drawn from a normal distribution
\(\mathcal{G}(s,S)\). To ensure positivity and allow for variations over several
orders of magnitude, \(s(x)\) will not directly be used as a model for the rate
function, but rather as the logarithm of the rate function scales. Consequently,
the functional relation between \(\rho (x)\) and \(s^x\) reads as follows
\[ s(x) = \text{ln}\bigg( \frac{\rho(x)}{\rho_0} \bigg) \iff \rho(x) = \rho_0 \, \text{e}^{s(x)}. \]
Similar to the definition of expected counts for the Poisson process, applying
a response \(R(x)\) to the rate function \(\rho^x\) gives the expected count
\[ \mathcal{P}(d|\lambda) = \prod_i \mathcal{P}(d_i|\lambda_i) = \prod_i \frac{(\lambda_i)^{d_i} \, e^{-\lambda_i}}{d_i!} \]
is just a product of all single bin likelihoods.
Log-normal Poisson model with the correlated field
This section should be an example of how to model Poisson count data in time.
Naturally, we can extend the procedure to multiple dimensions, similar to this
example.
First, let us read the data contained in data_poisson.txt.
events = np.loadtxt("data_poisson.txt")
for k in range(15):
print(events[k])
17.025680541992188
23.017667770385742
26.773649215698242
28.523881912231445
29.105871200561523
30.136960983276367
31.293071746826172
31.614225387573242
32.483299255371094
32.93983840942383
33.88088607788086
33.984371185302734
34.11470031738281
34.33364486694336
34.449947357177734
events now contains all recorded events’ time stamps, but
there is still no count data. Therefore, we take all events and bin them over
the viewed interval \([0,T]\) with a resolution of t_res. In this analysis, we
choose the last timestamp as our \(T\). Using jnp.histogram, we get the counts
per bin and its bin bounds. Moreover, by only setting the number of desired
bins, we get uniform bins with bin width \(\Delta t\).
Now let us visualize the binned count data.
t_res = 2**8
T = events.max()
data, bins = np.histogram(events, t_res, (0, T))
bins = bins[:-1]
plt.xlabel("Time [s]")
plt.ylabel("Counts")
plt.plot(bins, data, ".", label="Data", color="tab:blue")
plt.legend(loc="upper right")
plt.show()

The plot shows us that roughly between 30 and 70 seconds, we
have more counts than at the beginning and end of the interval. Therefore, it
is plausible that the underlying rate function \(\rho(t)\) must be higher in
this region, meaning \(\rho(t)\) allows for a greater number of events.
Now let us model the rate function \(\rho(t)\) itself.
The exact form of \(\rho(t)\) is unavailable in a real-world scenario.
Therefore, we model the rate function \(\rho(t)\) via the log-normal Poisson
model. We choose a correlated field with a non-parametric power spectrum for
the underlying Gaussian process. Since the correlated field uses the Hartley
transformation, it follows periodic boundary conditions.
To reconstruct also non-periodic signals, we make use of zero padding.
For that, we define our correlated field for twice the time interval
and then cut out the zero-padded region in the forward model.
Furthermore, we choose the distances of the correlated
field to coincide with the bin width of the data.
This saves us transformations between (differently binned) data and Poisson rates,
simplifying operators or fields of the model for the expected counts.
cf_offset_dct = dict(
offset_mean=1,
offset_std=(0.5, 0.1),
)
cf_ps_dct = dict(
shape=2 * t_res,
distances=T / t_res,
non_parametric_kind="power",
fluctuations=(0.5, 0.1),
loglogavgslope=(-4, 0.5),
flexibility=(1, 0.5),
asperity=(10, 5),
)
cf_maker = jft.CorrelatedFieldMaker("signal_")
cf_maker.set_amplitude_total_offset(**cf_offset_dct)
cf_maker.add_fluctuations(**cf_ps_dct)
s = cf_maker.finalize()
The forward model mainly consists of the log-normal Poisson model described at the beginning.
We choose the correlated field’s timeline to coincide with the data’s
timeline in the first half.
The second half is the artificial zero padding region
and must be cut out. The method signal in the
forward model applies this slicing operation.
Next, we ensure positivity by exponentiating the resulting signal in the exp_signal method.
By making it a separate method, we can easily extract \(\rho(t)\) later
if needed.
Lastly, we compute the expected counts from the Poisson rate.
Recall that the log-normal Poisson model computes the expected counts per
bin \(i\) within the time interval \(\left[t_i,t_{i+1} \right]\) as
\[\lambda_i = \int_{t_i}^{t_{i+1}} d\tau \, R_i(\tau) \, e^{s(\tau)}.\]
In our case, the response operator \(R\) is the identity operator.
As one entry of the signal field \(s\) corresponds to exactly one data
bin, we can assume that \({s(t)} = s(t_i) = s_i\). The integral simplifies to
\[\lambda_i = e^{s_i} \, \int_{t_i}^{t_{i+1}} d\tau = e^{s_i} \, (t_{i+1}-t_i) = e^{s_i} \, \Delta t.\]
Thus, the __call__ method of the forward model takes the expontiated signal
and multiplies all array entries by the bin width.
class LogNormalPoisson(jft.Model):
def __init__(self, signal_field, res, T):
self.s = signal_field
self.res = res
self.dt = T / res
super().__init__(init=signal_field.init)
def __call__(self, x):
return self.dt * self.exp_signal(x)
def exp_signal(self, x):
return jnp.exp(self.signal(x))
def signal(self, x):
return self.s(x)[: self.res]
lamb = LogNormalPoisson(s, t_res, T)
To see if the chosen hyperparameters of our correlated field
prior are reasonable, let us look at some prior samples of our forward model.
plt.plot(bins, data, ".")
key, k1 = random.split(rng_key, 2)
for k in range(20):
k0, k1 = random.split(k1, 2)
plt.plot(bins, lamb(lamb.init(k0)))

The plot above displays the data count by the points, and 20
prior samples as graphs. We can see that the hyperparameters are set to
reasonable values, as the different prior samples show enough variance in
amplitude and offset to cover almost every possible data point. Furthermore,
the different realizations show that the variance on the hyperparameters
allows for a wide range of possible realizations, thus enabling the search for
the correct values of the hyperparameters.
Next, we have to define the likelihood of our inference problem.
For count data, as we are using the log-normal Poisson model, we choose a
Poissonian likelihood. NIFTy.re implements a Poissonian likelihood as the
class jft.Poissonian. The class itself needs the binned count data and
computes the Information Hamiltonian of a multivariate Poisson distribution
\[ \mathcal{H}(d|\lambda) = \sum_i \mathcal{H}(d_i|\lambda_i) = \sum_i \lambda_i - d_i \, \text{ln}(\lambda_i) + \text{ln}(d_i!) \]
up to constant terms in \(\lambda_i\).
As the likelihood does not know about the model, we must amend an instance of
the model class.
lh = jft.Poissonian(data).amend(lamb)
The last part is again the inference algorithm. For an introduction on using the
optimize_kl function, please look at the inference notebook.
For visualization, we evaluate the mean and standard deviation of the
exponentiated signal \(\text{e}^{s(t)}\) after each VI iteration and plot the
mean, together with a \(1 \sigma\) -band, against the exact rate function
\(\rho(t)\). Furthermore, the reconstructed power spectrum of the correlated
field is shown next to it.
def callback(samples, state):
exp_mean, exp_std = jft.mean_and_std(tuple(lamb.exp_signal(s) for s in samples))
ps_mean, ps_std = jft.mean_and_std(
tuple(cf_maker.power_spectrum(s) for s in samples)
)
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 5), constrained_layout=True)
axs[0].plot(bins, exp_mean, label="Posterior mean", color="tab:blue")
axs[0].fill_between(
bins, exp_mean - exp_std, exp_mean + exp_std, color="tab:blue", alpha=0.3
)
axs[0].legend()
axs[1].set_xscale("log")
axs[1].set_yscale("log")
axs[1].plot(
s.target_grids[0].harmonic_grid.mode_lengths,
ps_mean,
label="Posterior power spectrum",
color="tab:blue",
)
plt.show()
We will now use the geometric Variational Inference (geoVI) algorithm that is better
in approximating non-Gaussian posteriors than Metric Gaussian Variational
Inference (MGVI),
as the posterior will generally be non-Gaussian. Let’s motivate this by
looking at the joint distribution of data \(d\) and the expectation value
\(\lambda\) in one bin. Considering only one bin, we have a one-dimensional
Gaussian and Poisson distribution for the prior and likelihood. Additionally,
we assume a unit response in the log-normal model, meaning that
\(\lambda(s)=\text{e}^s\).
\[\begin{split} \mathcal{P}(s) &= \mathcal{G}(s,\sigma^2) = \frac{1}{\sqrt{2\pi}\sigma} \, \text{e}^{-\frac{s^2}{2\sigma^2}}\\ \mathcal{P}(d|\lambda(s)) &= \frac{\lambda^d \, \text{e}^{-\lambda}}{d!} \end{split}\]
Since \(\lambda\) is defined by an exponentiated normally distributed \(s\), it
will follow the log-normal distribution
\[ \mathcal{P}(\lambda) = \frac{1}{\sqrt{2\pi}\sigma \, \lambda} \, \text{exp}\bigg[ -\frac{\text{ln}^2(\lambda)}{2\sigma^2} \bigg]. \]
The joint distribution of \(d\) and \(\lambda\) will therefore reads
\[ \mathcal{P}(d,\lambda) = \mathcal{P}(d|\lambda) \, \mathcal{P}(\lambda) \propto \lambda^{d-1} \, \text{exp}\bigg[ -\lambda -\frac{\text{ln}^2(\lambda)}{2\sigma^2} \bigg]. \]
Now, let us visualize the unnormalized posterior distribution.
def joint_PDF_curve(x, d, sigma):
return x ** (d - 1) * jnp.exp(-x - jnp.log(x) ** 2 / (2 * sigma**2))
x = jnp.linspace(0, 20, 1000)
y = joint_PDF_curve(x, 5, 1)
plt.plot(x, y)
plt.show()

As we can see, the posterior
\[ \mathcal{P}(\lambda|d) = \frac{\mathcal{P}(d,\lambda)}{\mathcal{P}(d)} \propto \lambda^{d-1} \, \text{exp}\bigg[ -\lambda -\frac{\text{ln}^2(\lambda)}{2\sigma^2} \bigg] \]
deviates from a Gaussian posterior. Therefore, using MGVI in the inference
scheme may lead to a suboptimal reconstruction. Now, let us use the geoVI
(see https://arxiv.org/abs/2105.10470) algorithm, which uses the Fisher
Information metric to construct non-Gaussian posterior distributions.
Now let us specify the optimize_kl arguments.
delta = 1e-6
key, k_i, k_o = random.split(key, 3)
optimize_kl_args = dict(
likelihood=lh,
position_or_samples=jft.Vector(lh.init(k_i)),
n_total_iterations=3,
n_samples=5,
key=k_o,
draw_linear_kwargs=dict(
cg_name=None,
cg_kwargs=dict(absdelta=delta * jft.size(lh.domain), maxiter=300),
),
nonlinearly_update_kwargs=dict(
minimize_kwargs=dict(
name=None,
xtol=delta,
cg_kwargs=dict(name=None),
maxiter=5,
)
),
kl_kwargs=dict(
minimize_kwargs=dict(
name=None, xtol=delta, cg_kwargs=dict(name=None), maxiter=35
)
),
sample_mode="nonlinear_resample",
callback=callback,
)
By setting the sample_mode of the optimize_kl to
nonlinear_resample, we will use the geoVI algorithm in the inference
process. Furthermore, we must now set the nonlinearly_update_kwargs in the
optimize_kl. Similar to the minimizers introduced in the notebook
1_inference, these set the parameters for a Newton conjugate gradient scheme
to calculate geoVI samples.
Furthermore, we can make a function call after each VI iteration by specifying
the’ callback’ argument. As an example, like we did here, we can repeatedly
compute plots of specific quantities during the inference.
samples, state = jft.optimize_kl(**optimize_kl_args)
OPTIMIZE_KL: Starting 0001
N: Iteration Limit Reached
N: Iteration Limit Reached
OPTIMIZE_KL: Iteration 0001 E:+6.2671e+02
OPTIMIZE_KL: #(Nonlinear sampling steps) (5, 3, 4, 5, 3, 4, 5, 3, 4, 5)
OPTIMIZE_KL: #(KL minimization steps) 35
OPTIMIZE_KL: Likelihood residual(s):
'reduced Chi²: 0.79± 0.17, avg: -0.082± 0.14, #dof: 256'
OPTIMIZE_KL: Prior residual(s):
signal_asperity :: 'reduced Chi²: 0.57± 0.62, avg: +0.49± 0.58, #dof: 1'
signal_flexibility :: 'reduced Chi²: 1.5± 1.6, avg: +0.73± 0.96, #dof: 1'
signal_fluctuations :: 'reduced Chi²: 4.9± 4.4, avg: +1.9± 1.1, #dof: 1'
signal_loglogavgslope :: 'reduced Chi²: 1.2± 1.3, avg: +0.87± 0.7, #dof: 1'
signal_spectrum :: 'reduced Chi²: 1.0± 0.083, avg: -0.0025± 0.065, #dof: 510'
signal_xi :: 'reduced Chi²: 1.1± 0.059, avg: -0.014± 0.035, #dof: 512'
signal_zeromode :: 'reduced Chi²: 1.7± 2.0, avg: +0.67± 1.1, #dof: 1'

OPTIMIZE_KL: Starting 0002
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
OPTIMIZE_KL: Iteration 0002 E:+5.8663e+02
OPTIMIZE_KL: #(Nonlinear sampling steps) (5, 5, 5, 5, 5, 5, 5, 5, 5, 5)
OPTIMIZE_KL: #(KL minimization steps) 19
OPTIMIZE_KL: Likelihood residual(s):
'reduced Chi²: 0.66± 0.043, avg: -0.08± 0.043, #dof: 256'
OPTIMIZE_KL: Prior residual(s):
signal_asperity :: 'reduced Chi²: 1.4± 1.2, avg: +0.35± 1.1, #dof: 1'
signal_flexibility :: 'reduced Chi²: 1.7± 1.8, avg: +0.73± 1.1, #dof: 1'
signal_fluctuations :: 'reduced Chi²: 6.3± 3.2, avg: +2.4± 0.65, #dof: 1'
signal_loglogavgslope :: 'reduced Chi²: 1.8± 1.4, avg: +0.26± 1.3, #dof: 1'
signal_spectrum :: 'reduced Chi²: 0.95± 0.024, avg: +0.0086± 0.025, #dof: 510'
signal_xi :: 'reduced Chi²: 1.0± 0.029, avg: -0.015± 0.037, #dof: 512'
signal_zeromode :: 'reduced Chi²: 0.96± 1.2, avg: +0.62± 0.76, #dof: 1'

OPTIMIZE_KL: Starting 0003
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
N: Iteration Limit Reached
OPTIMIZE_KL: Iteration 0003 E:+6.0082e+02
OPTIMIZE_KL: #(Nonlinear sampling steps) (5, 5, 5, 5, 5, 5, 5, 5, 5, 5)
OPTIMIZE_KL: #(KL minimization steps) 19
OPTIMIZE_KL: Likelihood residual(s):
'reduced Chi²: 0.66± 0.042, avg: -0.081± 0.056, #dof: 256'
OPTIMIZE_KL: Prior residual(s):
signal_asperity :: 'reduced Chi²: 0.49± 0.55, avg: +0.29± 0.64, #dof: 1'
signal_flexibility :: 'reduced Chi²: 1.4± 1.7, avg: +0.78± 0.89, #dof: 1'
signal_fluctuations :: 'reduced Chi²: 7.1± 5.0, avg: +2.5± 0.97, #dof: 1'
signal_loglogavgslope :: 'reduced Chi²: 0.76± 0.96, avg: +0.38± 0.79, #dof: 1'
signal_spectrum :: 'reduced Chi²: 1.0± 0.093, avg: -0.00049± 0.052, #dof: 510'
signal_xi :: 'reduced Chi²: 1.0± 0.047, avg: -0.014± 0.048, #dof: 512'
signal_zeromode :: 'reduced Chi²: 1.6± 1.5, avg: +0.74± 1.0, #dof: 1'
