Note
Go to the end to download the full example code.
Basic OSEM
This example demonstrates the use of the MLEM algorithm with ordered subsets (OS-MLEM or OSEM) to minimize the negative Poisson log-likelihood function.
subject to
using the linear forward model
The idea is to split the complete forward operator into a sequence \(n\) disjoint subset operators
which is can be used to evaluate a subset of the forward model
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.
42 import array_api_compat.numpy as xp
43
44 # import array_api_compat.cupy as xp
45 # import array_api_compat.torch as xp
46
47 import parallelproj
48 from array_api_compat import to_device
49 import array_api_compat.numpy as np
50 import matplotlib.pyplot as plt
51
52 # choose a device (CPU or CUDA GPU)
53 if "numpy" in xp.__name__:
54 # using numpy, device must be cpu
55 dev = "cpu"
56 elif "cupy" in xp.__name__:
57 # using cupy, only cuda devices are possible
58 dev = xp.cuda.Device(0)
59 elif "torch" in xp.__name__:
60 # using torch valid choices are 'cpu' or 'cuda'
61 dev = "cuda"
Setup of the complete 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.
71 # setup an arbitrary 4x4 matrix
72 mat = xp.asarray(
73 [
74 [2.5, 1.2, 0.3, 0.1],
75 [0.4, 3.1, 0.7, 0.2],
76 [0.1, 0.3, 4.1, 2.5],
77 [0.2, 0.5, 0.2, 0.9],
78 ],
79 dtype=xp.float64,
80 device=dev,
81 )
82
83 op_A = parallelproj.MatrixOperator(mat)
84 # setup an arbitrary contamination vector that has shape op_A.out_shape
85 contamination = xp.asarray([0.3, 0.2, 0.1, 0.4], dtype=xp.float64, device=dev)
86
87 print(op_A.A)
[[2.5 1.2 0.3 0.1]
[0.4 3.1 0.7 0.2]
[0.1 0.3 4.1 2.5]
[0.2 0.5 0.2 0.9]]
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.
96 # ground truth
97 x_true = xp.asarray([5.5, 10.7, 8.2, 7.9], dtype=xp.float64, device=dev)
98
99 # simulated noise-free data
100 noise_free_data = op_A(x_true) + contamination
101
102 # add Poisson noise
103 np.random.seed(1)
104 y = xp.asarray(
105 np.random.poisson(parallelproj.to_numpy_array(noise_free_data)),
106 device=dev,
107 dtype=xp.float64,
108 )
Analytic calculation of the optimal point (as reference)
Since our linear forward operator \(A\) is invertible (which is usually not the case in practice), we can calculate the optimal point \(x^* = A^{-1} (y - s)\) and the corresponding optimal value of \(f(x^*)\).
119 # calculate the reference solution by inverting A
120 mat_inv = xp.linalg.inv(mat)
121 x_ref = mat_inv @ (y - contamination)
122
123 # also calculate the cost of the reference solution
124 exp_ref = op_A(x_ref) + contamination
125 cost_ref = float(xp.sum(exp_ref - y * xp.log(exp_ref)))
Splitting of the forward model into subsets \(A^k\)
We split the matrix operator into two disjoint subsets using slicing along the first dimension of the matrix.
Note
The OSEM implementation below works with all linear operators that
subclass LinearOperatorSequence
138 # define two subsets (they don't need to have equal size)
139 subset_slices = (slice(0, 2), slice(2, None))
140
141 # setup two subsets operators each containing 1 and 3 rows of the matrix A
142 op_seq = parallelproj.LinearOperatorSequence(
143 [parallelproj.MatrixOperator(mat[sl, :]) for sl in subset_slices]
144 )
145
146 for op in op_seq:
147 print(op.A)
[[2.5 1.2 0.3 0.1]
[0.4 3.1 0.7 0.2]]
[[0.1 0.3 4.1 2.5]
[0.2 0.5 0.2 0.9]]
OSEM iterations to minimize \(f(x)\)
We apply multiple OSEM updates [HL94]
to calculate the minimizer of \(f(x)\) iteratively.
To monitor the convergence we calculate the relative cost
and the distance to the optimal point
170 # number MLEM iterations
171 num_iter = 1000 // len(op_seq)
172
173 # initialize x
174 x = xp.ones(op_A.in_shape, dtype=xp.float64, device=dev)
175
176 # calculate A_k^H 1 for all subsets k
177 subset_adjoint_ones = [
178 x.adjoint(xp.ones(x.out_shape, dtype=xp.float64, device=dev)) for x in op_seq
179 ]
180
181 # allocate arrays for the relative cost and the relative distance to the
182 # optimal point
183 rel_cost = xp.zeros(num_iter, dtype=xp.float64, device=dev)
184 rel_dist = xp.zeros(num_iter, dtype=xp.float64, device=dev)
185
186 for i in range(num_iter):
187 for k, sl in enumerate(subset_slices):
188 # evaluate the forward model
189 subset_exp = op_seq[k](x) + contamination[sl]
190 # calculate the relative cost and distance to the optimal point
191 # to do this, we need the full expectation
192 exp = op_A(x) + contamination
193 rel_cost[i] = (xp.sum(exp - y * xp.log(exp)) - cost_ref) / abs(cost_ref)
194 rel_dist[i] = xp.linalg.vector_norm(x - x_ref) / xp.linalg.vector_norm(x_ref)
195 # OSEM update
196 ratio = y[sl] / subset_exp
197 x *= op_seq[k].adjoint(ratio) / subset_adjoint_ones[k]
Convergences plots
- ..note::
The basic OSEM does not converge to the optimal point (but can come close using a few number of fast updates).
208 fig, ax = plt.subplots(1, 2, figsize=(8, 4), sharex=True)
209 ax[0].semilogx(parallelproj.to_numpy_array(rel_cost))
210 ax[1].loglog(parallelproj.to_numpy_array(rel_dist))
211 ax[0].set_ylim(-rel_cost[2], rel_cost[2])
212 ax[0].set_ylabel(r"( f($x$) - f($x^*$) ) / | f($x^*$) |")
213 ax[1].set_ylabel(r"rel. distance to optimum $\|x - x^*\| / \|x^*\|$")
214 ax[0].set_xlabel("iteration")
215 ax[1].set_xlabel("iteration")
216 ax[0].grid(ls=":")
217 ax[1].grid(ls=":")
218 fig.tight_layout()
219 fig.show()

Total running time of the script: (0 minutes 0.205 seconds)
Related examples
DePierro’s algorithm to optimize the Poisson logL with quadratic intensity prior