Skip to content

Commit 258fb4a

Browse files
committed
Merge remote-tracking branch 'origin' into PETSIRD
2 parents 137804a + 2c0192b commit 258fb4a

36 files changed

+1415
-569
lines changed

.appveyor.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ build_script:
5151
- "set PATH=%MINICONDA%;%MINICONDA%\\Scripts;%MINICONDA%\\Library\\bin;%PATH%"
5252
# install parallelproj and Python stuff
5353
# don't do numpy here due to https://github.com/conda-forge/numpy-feedstock/issues/350
54-
- conda create --name stirbuild -c conda-forge -yq libparallelproj swig pytest ccache ninja cmake
54+
- conda create --name stirbuild -c conda-forge -yq libparallelproj=2 swig pytest ccache ninja cmake
5555
- CALL conda.bat activate stirbuild
5656
- python --version
5757
- pip install numpy matplotlib
@@ -89,4 +89,4 @@ test_script:
8989

9090
on_finish:
9191
# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
92-
92+

.github/workflows/build-test.yml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ jobs:
149149
compiler: clang
150150
compiler_version: 21
151151
cuda_version: "0"
152-
BUILD_FLAGS: "-DSTIR_OPENMP=ON"
152+
# openMP off as problems again with brew
153+
BUILD_FLAGS: "-DSTIR_OPENMP=OFF"
154+
# parallelproj off as it needs openMP
153155
parallelproj: "OFF"
154156
BUILD_TYPE: "Release"
155157
ROOT: "OFF"
@@ -313,16 +315,20 @@ jobs:
313315
esac
314316
315317
if test "${{matrix.parallelproj}}XX" == "ONXX"; then
316-
git clone --depth 1 --branch v1.7.3 https://github.com/gschramm/parallelproj
317-
mkdir parallelproj/build
318-
cd parallelproj/build
318+
git clone --depth 20 https://github.com/KUL-recon-lab/libparallelproj/
319+
cd libparallelproj
320+
git checkout v2.0.3
321+
mkdir build
322+
cd build
319323
if test "${{matrix.cuda_version}}" == "0"; then
320-
extra_args="-DSKIP_CUDA_LIB:BOOL=ON"
324+
extra_args="-DUSE_CUDA:BOOL=OFF"
325+
else
326+
extra_args="-DUSE_CUDA:BOOL=ON"
321327
fi
322328
cmake .. -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX} ${extra_args}
323329
cmake --build . --target install --config Release
324330
cd ../..
325-
rm -rf parallelproj
331+
rm -rf libparallelproj
326332
fi
327333
328334
# Install ROOT (warning: brittle due to OS versions etc)

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[submodule "external_helpers/fmt"]
22
path = external_helpers/fmt
33
url = https://github.com/fmtlib/fmt.git
4+
[submodule "cuvec"]
5+
path = external_helpers/CuVec
6+
url = https://github.com/AMYPAD/CuVec

CMakeLists.txt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -262,17 +262,18 @@ endif()
262262

263263
# Parallelproj
264264
if(NOT DISABLE_Parallelproj_PROJECTOR)
265-
find_package(parallelproj 1.3.4 CONFIG)
265+
find_package(parallelproj CONFIG)
266266
if (parallelproj_FOUND)
267267
set(STIR_WITH_Parallelproj_PROJECTOR ON)
268-
if (parallelproj_built_with_CUDA)
268+
if (PARALLELPROJ_VERSION VERSION_LESS 2.0)
269+
message(WARNING "Found parallelproj ${parallelproj_VERSION}. This will not be supported anymore in the near future.")
270+
endif()
271+
if (PARALLELPROJ_CUDA)
269272
message(STATUS "Found parallelproj ${parallelproj_VERSION} (will use its CUDA support)")
273+
set(parallelproj_built_with_CUDA ON)
270274
else()
271275
message(STATUS "Found parallelproj ${parallelproj_VERSION} (but using its OpenMP version as it wasn't built with CUDA)")
272276
endif()
273-
if (parallelproj_VERSION VERSION_LESS 1.0.1)
274-
message(STATUS "If the above parallelproj info looks incorrect, upgrade it to at least 1.0.1 (but 1.2.13 or later is recommended)")
275-
endif()
276277
endif()
277278
endif()
278279
if (STIR_WITH_Parallelproj_PROJECTOR)

documentation/release_6.4.htm

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,14 @@ <h1>Summary of changes in STIR release 6.4</h1>
1212
<h2>Overall summary</h2>
1313

1414
<p>
15-
This version is 100% backwards compatible with STIR 6.3, aside from three important bug fixes,
16-
which will change results (see below).
15+
This version is 100% backwards compatible with STIR 6.3.
1716
</p>
1817
<p>
1918
This is a release with many new features, as listed below. Highlights are
2019
<ul>
2120

2221
</ul>
23-
But there are many other changes from other contibutors as well, see below.
22+
But there are many other changes from other contributors as well, see below.
2423
Of course, there is also the usual code-cleanup and some improvements to the documentation.
2524
Overall overview and release management was by Kris Thielemans (UCL) with help
2625
from Daniel Deidda (NPL).
@@ -29,8 +28,8 @@ <h2>Overall summary</h2>
2928
<h2>Patch release info</h2>
3029
<ul>
3130
<li>
32-
6.4.0 released x/2025<br>
33-
<a href="https://github.com/UCL/STIR/milestone/12">GitHub Milestone 6.3</a>
31+
6.4.0 released XXX/2026<br>
32+
<a href="https://github.com/UCL/STIR/milestone/13">GitHub Milestone 6.4</a>
3433
</li>
3534
</ul>
3635

@@ -51,13 +50,52 @@ <h4>Utilities</h4>
5150

5251
<h3>Changed functionality</h3>
5352
<ul>
53+
<li>When using a (ray tracing) matrix as projector, previously the matrix stored elements for the whole LOR,
54+
while many elements would be zero. We now only store the non-zero ones, resulting in a speed-up for the Siemens Vision Quadra of about a factor 1.7. Memory usage is also reduced.<br>
55+
<a href=https://github.com/UCL/STIR/pull/1674># 1674</a>.
56+
</li>
57+
<li>
58+
OpenMP parallelisation was added to the function to create a 2d histogram of hits per crystal
59+
from a proj data object, <code>make_fan_sum_data</code>.
60+
<br>
61+
<a href=https://github.com/UCL/STIR/pull/1667>PR #1667</a>.
62+
</li>
63+
<li>
64+
Several optimisations were contributed as part of the <a href="https://www.ccpsynerbi.ac.uk/events/airbi-hackathon/">SyneRBI AI-RBI hackathon</a>:
65+
<ul>
66+
<li>
67+
Compatibility with <a href="https://github.com/KUL-recon-lab/libparallelproj/">libparallelproj 2.0</a> and usage of CUDA managed pointers via
68+
<a href="https://github.com/AMYPAD/cuvec">CuVec</a> for internal variables in our parallelproj interface and <code>CudaGibbsPenalty</code>.
69+
This results in a ~20% speed-up, but also code simplification.
70+
<br>
71+
<a href=https://github.com/UCL/STIR/pull/1689>PR #1689</a>.
72+
</li>
73+
<li>
74+
Extra constructors for array, image and projdata classes that allow <code>std::move</code> for input arrays.
75+
<br>
76+
<a href=https://github.com/UCL/STIR/pull/1693>PR #1693</a> and <a href=https://github.com/UCL/STIR/pull/1694>PR #1694</a>.
77+
</li>
78+
</ul>
79+
</li>
5480
</ul>
81+
5582
<h4>Changes to examples</h4>
5683
<ul>
5784
</ul>
5885

5986
<h3>Bug fixes</h3>
6087
<ul>
88+
<li>
89+
An important bug fix for the "ray tracing matrix" (or actually any "matrix") projector for TOF PET. Previously, oblique
90+
segments had problems, see <a href=https://github.com/UCL/STIR/issues/1537># 1537</a>.<br>
91+
<a href=https://github.com/UCL/STIR/pull/1675># 1675</a>. Extra tests were introduced in <a href=https://github.com/UCL/STIR/pull/1674># 1674</a>.
92+
</li>
93+
<li>
94+
Fixed a small memory leak in scatter estimation caused by multiple calls to <code>ProjDataInfo::clone()</code> combined with <code>dynamic_cast</code>.
95+
The object is now cloned once and ownership is transferred safely after type checking.<br>
96+
<a href=https://github.com/UCL/STIR/pull/1673>PR #1673</a>
97+
</li>
98+
<br>
6199
</ul>
62100

63101
<h3>Build system</h3>
@@ -87,16 +125,18 @@ <h3>Build system</h3>
87125
</ul>
88126

89127
<h3>Known problems</h3>
90-
<p>See <a href=https://github.com/UCL/STIR/labels/bug>our issue tracker</a>.<br>
91-
An important bug is for the "ray tracing matrix" projector for TOF PET. Oblique
92-
segments currently have problems, see <a href=https://github.com/UCL/STIR/issues/1537># 1537</a>.
128+
<p>See <a href=https://github.com/UCL/STIR/labels/bug>our issue tracker</a>.
93129
</p>
94130

95131

96132
<H2>What is new for developers (aside from what should be obvious from the above):</H2>
97133

98134
<h3>New functionality</h3>
99135
<ul>
136+
<li>
137+
Added a Python script to convert e7tools generated Siemens Biograph Vision 600 sinograms to STIR compatible format.
138+
<a href=https://github.com/UCL/STIR/pull/1675>PR #1675</a>
139+
</li>
100140
</ul>
101141

102142
<h3>Changed functionality</h3>

examples/python/LBFGSBPC.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Class implementing the LBFGSB-PC algorithm in stir
5+
#
6+
# Authors: Kris Thielemans
7+
#
8+
# Based on Georg Schramm's
9+
# https://github.com/SyneRBI/PETRIC-MaGeZ/blob/a690205b2e3ec874e621ed2a32a802cd0bed4c1d/simulation_src/sim_stochastic_grad.py
10+
# but with using diag(H.1) as preconditioner at the moment, as per Tsai's paper (see ref in the class doc)
11+
#
12+
# Copyright 2025 University College London
13+
14+
import numpy as np
15+
import numpy.typing as npt
16+
import stir
17+
from scipy.optimize import fmin_l_bfgs_b
18+
from typing import Callable, Optional, List
19+
20+
# import matplotlib.pyplot as plt
21+
22+
23+
class LBFGSBPC:
24+
"""Implementation of the LBFGSB-PC Algorithm
25+
26+
See
27+
Tsai et al,
28+
Fast Quasi-Newton Algorithms for Penalized Reconstruction in Emission Tomography and Further Improvements via Preconditioning
29+
IEEE TRANSACTIONS ON MEDICAL IMAGING, VOL. 37, NO. 4, APRIL 2018
30+
DOI: 10.1109/TMI.2017.2786865
31+
32+
WARNING: it maximises the objective function (as required by sirf.STIR.ObjectiveFunction).
33+
WARNING: the implementation uses asarray(), which means you need SIRF 3.9. You should be able to just replace it with as_array() otherwise.
34+
35+
This implementation is NOT a CIL.Algorithm, but it behaves somewhat as one.
36+
"""
37+
38+
def __init__(
39+
self,
40+
objfun: stir.GeneralisedObjectiveFunction3DFloat,
41+
initial: stir.FloatVoxelsOnCartesianGrid,
42+
update_objective_interval: int = 0,
43+
):
44+
self.trunc_filter = stir.TruncateToCylindricalFOVImageProcessor3DFloat()
45+
self.objfun = objfun
46+
self.initial = initial.clone()
47+
self.trunc_filter.apply(self.initial)
48+
self.shape = initial.shape()
49+
self.output = None
50+
self.update_objective_interval = update_objective_interval
51+
52+
precon = initial.get_empty_copy()
53+
objfun.accumulate_Hessian_times_input(precon, initial, initial * 0 + 1)
54+
precon *= -1
55+
# self.Dinv_STIR = precon.maximum(1).power(-0.5)
56+
self.Dinv = np.power(np.maximum(precon.as_array(), 1), -0.5)
57+
self.Dinv_STIR = precon
58+
self.Dinv_STIR.fill(self.Dinv)
59+
self.trunc_filter.apply(self.Dinv_STIR)
60+
# plt.figure()
61+
# plt.imshow(self.Dinv_STIR.as_array()[self.shape[0] // 2, :, :])
62+
self.Dinv = self.Dinv_STIR.as_array().ravel()
63+
self.tmp_for_value = initial.get_empty_copy()
64+
self.tmp1_for_gradient = initial.get_empty_copy()
65+
self.tmp2_for_gradient = initial.get_empty_copy()
66+
67+
def precond_objfun_value(self, z: npt.ArrayLike) -> float:
68+
self.tmp_for_value.fill(
69+
np.reshape(z.astype(np.float32) * self.Dinv, self.shape)
70+
)
71+
return -self.objfun.compute_value(self.tmp_for_value)
72+
73+
def precond_objfun_gradient(self, z: npt.ArrayLike) -> np.ndarray:
74+
self.tmp1_for_gradient.fill(
75+
np.reshape(z.astype(np.float32) * self.Dinv, self.shape)
76+
)
77+
self.objfun.compute_gradient(self.tmp2_for_gradient, self.tmp1_for_gradient)
78+
return self.tmp2_for_gradient.as_array().ravel() * self.Dinv * -1
79+
80+
def callback(self, x):
81+
if (
82+
self.update_objective_interval > 0
83+
and self.iter % self.update_objective_interval == 0
84+
):
85+
self.loss.append(-self.precond_objfun_value(x))
86+
self.iterations.append(self.iter)
87+
self.iter += 1
88+
89+
def process(
90+
self, iterations=None, callbacks: Optional[List[Callable]] = None, verbose=0
91+
) -> None:
92+
r"""run upto :code:`iterations` with callbacks.
93+
94+
Parameters
95+
-----------
96+
iterations: int, default is None
97+
Number of iterations to run.
98+
callbacks: list of callables, default is Defaults to self.callback
99+
List of callables which are passed the current Algorithm object each iteration. Defaults to :code:`[ProgressCallback(verbose)]`.
100+
verbose: 0=quiet, 1=info, 2=debug
101+
Passed to the default callback to determine the verbosity of the printed output.
102+
"""
103+
if iterations is None:
104+
raise ValueError("`missing argument `iterations`")
105+
precond_init = self.initial / self.Dinv_STIR
106+
self.trunc_filter.apply(precond_init)
107+
precond_init = precond_init.as_array().ravel()
108+
bounds = precond_init.size * [(0, None)]
109+
self.iter = 0
110+
self.loss = []
111+
self.iterations = []
112+
# TODO not really required, but it differs from the first value reported by fmin_l_bfgs_b. Not sure why...
113+
self.callback(precond_init)
114+
self.iter = 0 # set back again
115+
res = fmin_l_bfgs_b(
116+
self.precond_objfun_value,
117+
precond_init,
118+
self.precond_objfun_gradient,
119+
maxiter=iterations,
120+
bounds=bounds,
121+
m=20,
122+
callback=self.callback,
123+
factr=0,
124+
pgtol=0,
125+
)
126+
# store result (use name "x" for CIL compatibility)
127+
self.x = self.tmp_for_value.get_empty_copy()
128+
self.x.fill(np.reshape(res[0].astype(np.float32) * self.Dinv, self.shape))
129+
130+
def run(
131+
self, **kwargs
132+
) -> None: # CIL alias, would need to callback and verbose keywords etc
133+
self.process(**kwargs)
134+
135+
def get_output(self) -> stir.FloatVoxelsOnCartesianGrid:
136+
return self.x

0 commit comments

Comments
 (0)