DePierro’s algorithm to optimize the Poisson logL with quadratic intensity prior

This example demonstrates the use of DePierro’s algorithm to minimize the negative Poisson log-likelihood function combined with a quadratic intensity prior:

\[f(x) = \sum_{i=1}^m \bar{y}_i - y_i \log(\bar{y}_i (x)) + \frac{\beta}{2} \|x - z \|^2\]

subject to

\[x \geq 0\]

using the linear forward model

\[\bar{y}(x) = A x + s\]

Tip

parallelproj is python array API compatible meaning it supports different array backends (e.g. numpy, cupy, torch, …) and devices (CPU or GPU). Choose your preferred array API xp and device dev below.

https://mybinder.org/badge_logo.svg
31 import array_api_compat.numpy as xp
32
33 # import array_api_compat.cupy as xp
34 # import array_api_compat.torch as xp
35
36 import parallelproj
37 from array_api_compat import to_device
38 import array_api_compat.numpy as np
39 import matplotlib.pyplot as plt
40
41 # choose a device (CPU or CUDA GPU)
42 if "numpy" in xp.__name__:
43     # using numpy, device must be cpu
44     dev = "cpu"
45 elif "cupy" in xp.__name__:
46     # using cupy, only cuda devices are possible
47     dev = xp.cuda.Device(0)
48 elif "torch" in xp.__name__:
49     # using torch valid choices are 'cpu' or 'cuda'
50     dev = "cuda"

Setup of the forward model \(\bar{y}(x) = A x + s\)

We setup a minimal linear forward operator \(A\) respresented by a 4x4 matrix and an arbritrary contamination vector \(s\) of length 4.

Note

The OSEM implementation below works with all linear operators that subclass LinearOperator (e.g. the high-level projectors).

64 # setup an arbitrary 4x4 matrix
65 mat = xp.asarray(
66     [
67         [2.5, 1.2, 0.3, 0.1],
68         [0.4, 3.1, 0.7, 0.2],
69         [0.1, 0.3, 4.1, 2.5],
70         [0.2, 0.5, 0.2, 0.9],
71         [0.3, 0.1, 0.7, 0.2],
72     ],
73     dtype=xp.float64,
74     device=dev,
75 )
76
77 op_A = parallelproj.MatrixOperator(mat)
78 # setup an arbitrary contamination vector that has shape op_A.out_shape
79 contamination = xp.asarray([0.3, 0.2, 0.1, 0.4, 0.1], dtype=xp.float64, device=dev)

Setup of ground truth and data simulation

We setup an arbitrary ground truth \(x_{true}\) and simulate noise-free and noisy data \(y\) by adding Poisson noise.

 88 # ground truth
 89 x_true = xp.asarray([5.5, 10.7, 8.2, 7.9], dtype=xp.float64, device=dev)
 90
 91 # simulated noise-free data
 92 noise_free_data = op_A(x_true) + contamination
 93
 94 # add Poisson noise
 95 np.random.seed(1)
 96 y = xp.asarray(
 97     np.random.poisson(parallelproj.to_numpy_array(noise_free_data)),
 98     device=dev,
 99     dtype=xp.float64,
100 )
104 x_prior = xp.full(op_A.in_shape, xp.min(x_true), dtype=xp.float64, device=dev)
105 beta = 0.3
106 num_iter = 50
107
108 # initialize x
109 x = xp.ones(op_A.in_shape, dtype=xp.float64, device=dev)
113 def cost_function(x):
114     exp = op_A(x) + contamination
115     if (xp.min(exp) < 0) or (xp.min(x) < 0):
116         res = xp.finfo(xp.float64).max
117     else:
118         res = (xp.sum(exp - y * xp.log(exp))) + 0.5 * beta * xp.sum((x - x_prior) ** 2)
119     return res

DePierro update to minimize \(f(x)\)

We apply multiple DePierro updates

\[b = A^H 1 - \beta z\]
\[t = x A^H \frac{y}{A x + s}\]
\[x^+ = \frac{2 t}{\sqrt{b^2 + 4 \beta t} + b}\]

to calculate the minimizer of \(f(x)\) iteratively.

See [DP95] for more details.

139 cost = xp.zeros(num_iter, dtype=xp.float64, device=dev)
140
141 # "b" - modified sensitivity image
142 mod_adjoint_ones = (
143     op_A.adjoint(xp.ones(op_A.out_shape, dtype=xp.float64, device=dev)) - beta * x_prior
144 )
145
146 for i in range(num_iter):
147     # evaluate the forward model
148     exp = op_A(x) + contamination
149     ratio = y / exp
150     t = x * op_A.adjoint(ratio)
151     x = 2 * t / (xp.sqrt(mod_adjoint_ones**2 + 4 * beta * t) + mod_adjoint_ones)
152     cost[i] = cost_function(x)
153
154 print(f"Solution after {num_iter} DePierro iterations:")
155 print(x)
156 print(f"cost: {cost[-1]:.6e}")
Solution after 50 DePierro iterations:
[6.28680501 7.26148792 6.72092821 6.43913514]
cost: -3.321656e+02
159 if xp.__name__.endswith("numpy"):
160     from scipy.optimize import fmin_powell
161
162     x_ref = fmin_powell(cost_function, x, xtol=1e-6, ftol=1e-6)
163     rel_dist = float(xp.sum((x - x_ref) ** 2)) / float(xp.sum(x_ref**2))
164
165     print(f"\nReference solution using Powell optimizer:")
166     print(x_ref)
167     print(f"rel. distance to DePierro solution: {rel_dist:.2e}")
168     print(f"cost: {cost_function(x_ref):.6e}")
Optimization terminated successfully.
         Current function value: -332.165557
         Iterations: 1
         Function evaluations: 113

Reference solution using Powell optimizer:
[6.28680485 7.26148793 6.72092821 6.43913496]
rel. distance to DePierro solution: 3.19e-16
cost: -3.321656e+02
171 fig, ax = plt.subplots(1, 1, tight_layout=True)
172 if xp.__name__.endswith("numpy"):
173     ax.axhline(cost_function(x_ref), color="k", linestyle="--")
174 ax.plot(parallelproj.to_numpy_array(cost))
175 ax.set_xlabel("iteration")
176 ax.set_ylabel("cost function")
177 ax.grid(ls=":")
178 fig.show()
06 run depierro

Total running time of the script: (0 minutes 0.154 seconds)

Related examples

Basic MLEM

Basic MLEM

Basic OSEM

Basic OSEM

PDHG to optimize the Poisson logL and directional TV (structural prior)

PDHG to optimize the Poisson logL and directional TV (structural prior)

TOF-MLEM with projection data

TOF-MLEM with projection data

Gallery generated by Sphinx-Gallery