diffct 1.3.2__tar.gz → 1.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {diffct-1.3.2 → diffct-1.3.4}/CHANGELOG.md +465 -363
  2. {diffct-1.3.2 → diffct-1.3.4}/PKG-INFO +1 -1
  3. {diffct-1.3.2 → diffct-1.3.4}/diffct/differentiable.py +514 -663
  4. {diffct-1.3.2 → diffct-1.3.4}/examples/data/NOTICE +20 -15
  5. {diffct-1.3.2 → diffct-1.3.4}/examples/data/preprocess_walnut.py +32 -7
  6. {diffct-1.3.2 → diffct-1.3.4}/examples/data/walnut_cone.npz +0 -0
  7. diffct-1.3.4/examples/data/walnut_reco.png +0 -0
  8. {diffct-1.3.2 → diffct-1.3.4}/examples/fbp_fan.py +3 -1
  9. {diffct-1.3.2 → diffct-1.3.4}/examples/fbp_parallel.py +3 -1
  10. {diffct-1.3.2 → diffct-1.3.4}/examples/fdk_cone.py +3 -1
  11. {diffct-1.3.2 → diffct-1.3.4}/examples/iterative_reco_cone.py +13 -13
  12. {diffct-1.3.2 → diffct-1.3.4}/examples/iterative_reco_fan.py +8 -7
  13. {diffct-1.3.2 → diffct-1.3.4}/examples/iterative_reco_parallel.py +8 -7
  14. {diffct-1.3.2 → diffct-1.3.4}/examples/realdata_fbp_fan.py +10 -7
  15. {diffct-1.3.2 → diffct-1.3.4}/examples/realdata_fbp_parallel.py +3 -1
  16. {diffct-1.3.2 → diffct-1.3.4}/examples/realdata_fdk_cone.py +10 -8
  17. {diffct-1.3.2 → diffct-1.3.4}/examples/realdata_walnut_fdk.py +35 -22
  18. {diffct-1.3.2 → diffct-1.3.4}/pyproject.toml +2 -2
  19. {diffct-1.3.2 → diffct-1.3.4}/tests/test_fdk_cone_accuracy.py +6 -4
  20. diffct-1.3.2/examples/data/walnut_reco.png +0 -0
  21. {diffct-1.3.2 → diffct-1.3.4}/.git +0 -0
  22. {diffct-1.3.2 → diffct-1.3.4}/.github/workflows/docs.yml +0 -0
  23. {diffct-1.3.2 → diffct-1.3.4}/.github/workflows/release.yml +0 -0
  24. {diffct-1.3.2 → diffct-1.3.4}/.gitignore +0 -0
  25. {diffct-1.3.2 → diffct-1.3.4}/LICENSE +0 -0
  26. {diffct-1.3.2 → diffct-1.3.4}/README.md +0 -0
  27. {diffct-1.3.2 → diffct-1.3.4}/README.zh.md +0 -0
  28. {diffct-1.3.2 → diffct-1.3.4}/diffct/__init__.py +0 -0
  29. {diffct-1.3.2 → diffct-1.3.4}/docs/Makefile +0 -0
  30. {diffct-1.3.2 → diffct-1.3.4}/docs/source/_static/.gitkeep +0 -0
  31. {diffct-1.3.2 → diffct-1.3.4}/docs/source/api.rst +0 -0
  32. {diffct-1.3.2 → diffct-1.3.4}/docs/source/conf.py +0 -0
  33. {diffct-1.3.2 → diffct-1.3.4}/docs/source/examples.rst +0 -0
  34. {diffct-1.3.2 → diffct-1.3.4}/docs/source/fbp_fan_example.rst +0 -0
  35. {diffct-1.3.2 → diffct-1.3.4}/docs/source/fbp_parallel_example.rst +0 -0
  36. {diffct-1.3.2 → diffct-1.3.4}/docs/source/fdk_cone_example.rst +0 -0
  37. {diffct-1.3.2 → diffct-1.3.4}/docs/source/getting_started.rst +0 -0
  38. {diffct-1.3.2 → diffct-1.3.4}/docs/source/index.rst +0 -0
  39. {diffct-1.3.2 → diffct-1.3.4}/docs/source/iterative_reco_cone_example.rst +0 -0
  40. {diffct-1.3.2 → diffct-1.3.4}/docs/source/iterative_reco_fan_example.rst +0 -0
  41. {diffct-1.3.2 → diffct-1.3.4}/docs/source/iterative_reco_parallel_example.rst +0 -0
  42. {diffct-1.3.2 → diffct-1.3.4}/pytest.ini +0 -0
  43. {diffct-1.3.2 → diffct-1.3.4}/requirements.txt +0 -0
  44. {diffct-1.3.2 → diffct-1.3.4}/tests/benchmarks/README.md +0 -0
  45. {diffct-1.3.2 → diffct-1.3.4}/tests/benchmarks/__init__.py +0 -0
  46. {diffct-1.3.2 → diffct-1.3.4}/tests/benchmarks/_common.py +0 -0
  47. {diffct-1.3.2 → diffct-1.3.4}/tests/benchmarks/test_bench_cone.py +0 -0
  48. {diffct-1.3.2 → diffct-1.3.4}/tests/benchmarks/test_bench_fan.py +0 -0
  49. {diffct-1.3.2 → diffct-1.3.4}/tests/benchmarks/test_bench_parallel.py +0 -0
  50. {diffct-1.3.2 → diffct-1.3.4}/tests/test_adjoint_inner_product.py +0 -0
  51. {diffct-1.3.2 → diffct-1.3.4}/tests/test_cone_projector_autograd.py +0 -0
  52. {diffct-1.3.2 → diffct-1.3.4}/tests/test_cuda_smoke.py +0 -0
  53. {diffct-1.3.2 → diffct-1.3.4}/tests/test_fbp_fan_accuracy.py +0 -0
  54. {diffct-1.3.2 → diffct-1.3.4}/tests/test_fbp_fan_offsets.py +0 -0
  55. {diffct-1.3.2 → diffct-1.3.4}/tests/test_fbp_parallel_accuracy.py +0 -0
  56. {diffct-1.3.2 → diffct-1.3.4}/tests/test_fdk_cone_offsets.py +0 -0
  57. {diffct-1.3.2 → diffct-1.3.4}/tests/test_gradcheck.py +0 -0
  58. {diffct-1.3.2 → diffct-1.3.4}/tests/test_ramp_filter_windows.py +0 -0
  59. {diffct-1.3.2 → diffct-1.3.4}/tests/test_weights.py +0 -0
@@ -1,363 +1,465 @@
1
- # Changelog
2
-
3
- All notable changes to this project are documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ## [1.3.1] - 2026-04-15
11
-
12
- ### Added
13
- - **Real-data walnut FDK example** at `examples/realdata_walnut_fdk.py`,
14
- running the full analytical FDK pipeline on a preprocessed subset of
15
- the Helsinki walnut cone-beam CT dataset ([Meaney 2022, Zenodo
16
- 10.5281/zenodo.6986012](https://doi.org/10.5281/zenodo.6986012),
17
- CC-BY 4.0). The 241-view, 256×256-per-view sinogram is shipped at
18
- `examples/data/walnut_cone.npz` (~25 MB, float16, 8× binned, 256²
19
- center crop) together with a `preprocess_walnut.py` regenerator
20
- script, an attribution `NOTICE`, and a `walnut_reco.png` sample
21
- montage. Reconstructs at half the nominal voxel size on a 512³ grid
22
- using `backend="sf_tr"` + Hamming ramp window.
23
- - **Synthetic "real-data" pipeline examples** for all three geometries
24
- (`examples/realdata_fbp_parallel.py`, `realdata_fbp_fan.py`,
25
- `realdata_fdk_cone.py`) that run a Beer-Lambert + Poisson + `-log`
26
- forward model on Shepp-Logan and reconstruct with the analytical
27
- pipeline, showing the proper place for `-log` preprocessing.
28
- - **Chinese README** at `README.zh.md`, mirroring `README.md` with
29
- bidirectional language links in both files.
30
- - **Core Algorithm section** in the READMEs (EN + ZH) explaining the
31
- Siddon + bilinear/trilinear interpolation design choice, the
32
- trade-off (slight blur at nominal voxel size), and when to pick
33
- SF vs VD honestly (see "Changed" below).
34
-
35
- ### Changed
36
- - **SF FBP / FDK backprojection kernels rewritten in LEAP's
37
- chord-weighted matched-adjoint form** (from `projectors_SF.cu`).
38
- The three analytical SF gather kernels
39
- (`_fan_2d_sf_fbp_backproject_kernel`,
40
- `_cone_3d_sf_tr_fdk_backproject_kernel`,
41
- `_cone_3d_sf_tt_fdk_backproject_kernel`) now use the per-voxel
42
- chord through the unit voxel instead of a `(sid/U)²`-plus-footprint-
43
- area form, with a matching `sqrt(1+(v/sdd)²)` axial chord correction
44
- on the cone side. Amplitude is calibrated against Siddon VD at
45
- every voxel / detector-pitch combination tested (nominal, 0.64×
46
- sub-nominal, 0.40× sub-nominal, supra-nominal) via the new
47
- `det_spacing/(voxel·2π)` fan scale and
48
- `(du·dv·sid)/(2π·sdd·voxel²)` cone scale constants.
49
- - **Honest README Core Algorithm section on SF vs VD.** The earlier
50
- "SF lowers MSE by ~17 %" and "SF is measurably sharper at sub-
51
- nominal" claims in 1.3.0 are walked back: on Shepp-Logan and the
52
- walnut dataset, LEAP-form SF and Siddon VD produce visually
53
- indistinguishable edge profiles at typical CBCT magnifications,
54
- and MSE differences are fractions of a percent. SF's real value
55
- is its **mass-conserving cell-integrated forward model**, which
56
- matters for iterative reconstruction, learned priors, and
57
- sinogram-level losses — not as a drop-in sharpness knob on
58
- analytical FBP / FDK. The shipped `fbp_fan.py`, `fdk_cone.py`,
59
- and `realdata_walnut_fdk.py` example comments are updated to
60
- match.
61
-
62
- ### Fixed
63
- - **SF FBP / FDK weight normalization** when the voxel's trapezoidal
64
- footprint straddles the default 4-corner sort. The 1.3.0 SF
65
- backprojection kernels used `fdk_w / (span · v_span)` (bounding-box
66
- area) which under-amplified oblique views by up to 2× per axis and
67
- produced a dim, angularly-modulated reconstruction. Diagnosed by
68
- inspecting a Shepp-Logan SF reconstruction, corrected first to
69
- `fdk_w / ((span+plateau)/2 · v_span)` (true trapezoid area) and
70
- then fully replaced by the LEAP chord-weighted form above.
71
-
72
- ### Acknowledgements
73
- - [LEAP](https://github.com/LLNL/LEAP) (LLNL / Hyojin Kim et al.)
74
- added to `README.md` / `README.zh.md` Acknowledgements.
75
- `projectors_SF.cu` is the reference implementation the new
76
- analytical SF backprojection kernels are ported from.
77
-
78
- ## [1.3.0] - 2026-04-14
79
-
80
- ### Added
81
- - **Separable-footprint (SF) projector backends** for 2D fan and 3D cone
82
- beam geometries, following Long-Fessler-Balter (IEEE TMI 2010).
83
- `FanProjectorFunction` / `FanBackprojectorFunction` now accept
84
- `backend="siddon"` (default, unchanged) or `backend="sf"`.
85
- `ConeProjectorFunction` / `ConeBackprojectorFunction` accept
86
- `backend="siddon"` (default), `"sf_tr"` (trapezoid in transaxial,
87
- rectangle in axial) or `"sf_tt"` (trapezoid in both directions with
88
- axial rise/fall from `U_near`/`U_far` across the voxel). Each SF
89
- backend is implemented as a matched scatter/gather pair of CUDA
90
- kernels so both forward and adjoint are available — autograd works
91
- end-to-end for every backend, and the standalone Backprojector
92
- Function can also be driven in SF mode for iterative reconstruction.
93
- - **Adjoint inner-product tests** for every new SF backend in
94
- `tests/test_adjoint_inner_product.py`, plus `torch.autograd.gradcheck`
95
- coverage in `tests/test_gradcheck.py`. Test count grew from 57 to 66.
96
-
97
- ### Changed
98
- - **Internal grid-point convention** is now uniformly `x_v = ix - cx`
99
- (matching `_cone_3d_fdk_backproject_kernel`). The SF kernels use the
100
- same convention so voxel centers align with where the Siddon/FDK
101
- paths interpret `d_vol[ix, iy, iz]`.
102
- - **`_TPB_SF_3D = (4, 4, 4)`** introduced for the 3D SF kernels. SF-TT
103
- in particular exceeds the 128-register-per-thread budget allowed by
104
- the default `(8, 8, 8)` launch; the smaller block gives each thread
105
- ~1024 registers and avoids `CUDA_ERROR_LAUNCH_OUT_OF_RESOURCES`.
106
-
107
- ### Notes
108
- - **Raw forward accuracy vs analytical Radon**: on both 2D fan
109
- (512² Shepp-Logan) and 3D cone (96³ and 128³ 3D Shepp-Logan) phantoms,
110
- measured against the analytical ellipse / ellipsoid Radon transform,
111
- the existing Siddon projector with bilinear / trilinear interpolation
112
- is slightly more accurate (~3% better RMSE, ~15% better max error)
113
- than SF-TR / SF-TT at the thin-ray sampling level. The classic SF
114
- advantage reported in Long et al. is over the piecewise-constant DD
115
- projector; diffct's Siddon uses smooth interpolation, which covers
116
- most of the gap SF was designed to close.
117
- - **Full analytical reconstruction (forward + FBP/FDK)**: despite the
118
- raw-forward result above, in a complete analytical pipeline
119
- (`FanProjectorFunction` / `ConeProjectorFunction` -> `ramp_filter_1d`
120
- -> `fan_weighted_backproject` / `cone_weighted_backproject`) SF
121
- **measurably improves reconstruction quality**. On the 256²
122
- Shepp-Logan `fbp_fan.py` example the raw MSE drops from
123
- `0.00216` (Siddon) to `0.00178` (SF-TR), a ~17% improvement.
124
- On the 128³ `fdk_cone.py` example it drops from `0.00325` (Siddon)
125
- to `0.00271` (SF-TR), again ~17%. The mechanism is that SF's
126
- mass-conserving cell-integrated forward pairs more naturally with
127
- the voxel-driven FBP/FDK gather backprojector than Siddon's
128
- thin-ray sampling, which feeds extra high-frequency ringing into
129
- the ramp filter.
130
- - **Performance**: SF-TR is ~2× and SF-TT ~2.5–4× slower than Siddon for
131
- cone beam, and ~3× slower than Siddon for fan beam. The extra cost
132
- comes from voxel-driven scatter with atomic adds (forward) versus
133
- ray-driven parallelism in Siddon.
134
- - **Recommendation**: `backend="siddon"` remains the default for
135
- backward compatibility and raw speed. For **analytical FBP/FDK
136
- reconstruction** where reconstruction MSE matters more than forward
137
- runtime, `"sf"` / `"sf_tr"` is a quantifiably better choice — at
138
- 2–3× forward cost you get ~17% lower reconstruction MSE on standard
139
- phantoms. For **iterative reconstruction with autograd**, either
140
- backend is valid; SF has the theoretical advantage of exact voxel-
141
- scale mass conservation on both forward and adjoint, but in practice
142
- Siddon's smoothing usually helps optimizer convergence. SF-TT's
143
- axial-trapezoid refinement over SF-TR improves RMSE only by ~0.001
144
- and is not worth the extra cost for typical cone angles; use SF-TR
145
- by default and reserve SF-TT for extreme cone-angle research.
146
-
147
- ## [1.2.11] - 2026-04-14
148
-
149
- ### Added
150
- - **Parker short-scan demos** in `examples/fdk_cone.py` and `examples/fbp_fan.py`.
151
- Both examples now expose an `apply_parker` switch that not only multiplies
152
- by `parker_weights` but also samples angles on the minimal short-scan range
153
- `pi + 2*gamma_max`, where `gamma_max` is computed from the detector half-width
154
- and `sdd`. (Before this change the `apply_parker` flag in `fbp_fan.py` was
155
- silently a no-op because the angular range stayed at `2*pi`.)
156
- - **`tests/test_gradcheck.py`** — `torch.autograd.gradcheck` coverage for
157
- `ParallelProjectorFunction`, `FanProjectorFunction`, and `ConeProjectorFunction`.
158
- Compares the analytical backward Jacobian to a finite-difference numerical
159
- Jacobian over small inputs with float32-calibrated tolerances. Strongest
160
- autograd-correctness guard beyond the existing inner-product adjoint tests.
161
- - **`tests/test_ramp_filter_windows.py`** — 27 parametrised tests covering every
162
- `ramp_filter_1d` window option (`None`/`"ram-lak"`, `"hann"`, `"hanning"`,
163
- `"hamming"`, `"cosine"`, `"shepp-logan"`) at three layers: direct `_ramp_window`
164
- helper (DC gain, Nyquist value, monotonicity), full `ramp_filter_1d` end-to-end
165
- (shape, DC annihilation, pad correctness, rfft vs complex-fft parity,
166
- `sample_spacing` scaling), and a high-frequency attenuation sanity check on a
167
- step input.
168
- - **`CHANGELOG.md`** — new Keep-a-Changelog file at the repo root.
169
- - **`tests/benchmarks/`** — opt-in `pytest-benchmark` suite covering every CUDA
170
- kernel in the library (forward projector, pure-adjoint backprojector, and
171
- analytical FBP/FDK pipeline) across three sizes for each of the three
172
- geometries. 27 benchmarks total. Excluded from the default `pytest tests/`
173
- run via `--ignore=tests/benchmarks` in `pytest.ini` so the normal test suite
174
- stays fast; run explicitly with `pytest tests/benchmarks/ --benchmark-only`.
175
- Enables before/after perf comparisons via `--benchmark-save=` /
176
- `--benchmark-compare=`.
177
-
178
- ### Changed
179
- - **`_cone_3d_fdk_backproject_kernel`** memory-access layout. The kernel now
180
- unpacks `cuda.grid(3)` as `(iz, iy, ix)` instead of `(ix, iy, iz)`, and
181
- `cone_weighted_backproject` launches the grid as `_grid_3d(D, H, W)` instead
182
- of `_grid_3d(W, H, D)`. This makes `iz` warp-adjacent, which matches the
183
- innermost stride-1 axis of the WHD output buffer, so the final `d_vol` write
184
- becomes coalesced. Measured speedup on a 64^3–160^3 sweep: 1.0x–1.14x,
185
- within run-to-run noise (the kernel is read-bound on the sinogram bilinear
186
- samples, not write-bound, so L2 absorbed the old uncoalesced writes). The
187
- fix is kept as the right default and future-proofs the kernel against
188
- edits that shift work toward the write path. Matching coalescing comments
189
- were added to the 2D `_parallel_2d_fbp_backproject_kernel` and
190
- `_fan_2d_fbp_backproject_kernel` kernels, which were already coalesced
191
- (their first grid index is also warp-adjacent and matches the `Nx` innermost
192
- axis of their `(Ny, Nx)` output buffers).
193
-
194
- ### Notes
195
- - No public-API changes. Pure-adjoint kernels and autograd Function classes
196
- are untouched, so the autograd and iterative-reconstruction paths are
197
- byte-for-byte equivalent to 1.2.10.
198
-
199
- ## [1.2.10] - 2026-04-14
200
-
201
- This release is a correctness overhaul of every analytical reconstruction path
202
- (parallel / fan / cone) **and** of the autograd adjoint path shared by the
203
- iterative examples. It also ships a unified architecture where each geometry
204
- has its own dedicated voxel-driven FBP/FDK gather kernel, separate from the
205
- pure Siddon adjoint used by autograd.
206
-
207
- ### Fixed
208
-
209
- #### Analytical FBP / FDK amplitude bugs
210
-
211
- Prior to this release, all three analytical reconstruction examples produced
212
- results that were off by large constant factors:
213
-
214
- | Example | Pre-fix reco range | Post-fix reco range | MSE improvement |
215
- |---|---|---|---|
216
- | `fbp_parallel.py` | [0, 6.52] | [−0.02, 1.01] | ~580× |
217
- | `fbp_fan.py` | [0, 6.33] | [−0.08, 1.01] | ~900× |
218
- | `fdk_cone.py` | [0, 10.08] | [−0.08, 1.00] | ~600× |
219
-
220
- Root causes:
221
- - Cone and fan used `(sdd/U)^2` with `U = sdd + x·sin − y·cos` instead of
222
- the correct FDK weight `(sid/U)^2` with `U = sid + x·sin − y·cos`.
223
- - All three geometries were missing the `1/(2π)` Fourier-convention
224
- constant from the FBP/FDK reconstruction formula.
225
- - Cone and fan used a Siddon ray-driven scatter for the analytical path,
226
- which is neither a true adjoint nor a classical FBP/FDK gather.
227
-
228
- #### Autograd adjoint bug (cone + fan)
229
-
230
- `ConeProjectorFunction.backward`, `ConeBackprojectorFunction.forward`,
231
- `FanProjectorFunction.backward` and `FanBackprojectorFunction.forward`
232
- were all passing `distance_weight=1.0` to the shared Siddon backward
233
- kernel. This meant the autograd backward was **not the true adjoint** `P^T`
234
- of the forward projector — it had a `(sdd/U)^2` per-voxel factor baked in,
235
- biasing the gradient by ~2–3× depending on voxel position. Any iterative
236
- reconstruction that relied on autograd (including `iterative_reco_cone.py`
237
- and `iterative_reco_fan.py`) was running on a biased gradient flow.
238
-
239
- Parallel beam was unaffected (its backward kernel had no `distance_weight`
240
- parameter to begin with).
241
-
242
- This release removes the `distance_weight` parameter from both
243
- `_cone_3d_backward_kernel` and `_fan_2d_backward_kernel` entirely, so the
244
- dead code path cannot be accidentally reintroduced, and fixes the four
245
- autograd call sites to use the pure adjoint.
246
-
247
- ### Added
248
-
249
- #### Voxel-driven gather kernels for every geometry
250
-
251
- Three new CUDA kernels, all under a dedicated `fastmath=False` decorator
252
- for FDK-grade accuracy:
253
-
254
- - `_parallel_2d_fbp_backproject_kernel` — no distance weighting (no source).
255
- - `_fan_2d_fbp_backproject_kernel` — `(sid/U)^2` + linear detector interp.
256
- - `_cone_3d_fdk_backproject_kernel` — `(sid/U)^2` + bilinear detector interp.
257
-
258
- Each kernel is voxel-driven: one thread per output pixel/voxel, loops over
259
- views, computes the projected detector coordinate, interpolates the filtered
260
- sinogram, weights and accumulates.
261
-
262
- #### `parallel_weighted_backproject` (new public helper)
263
-
264
- Mirrors `fan_weighted_backproject` and `cone_weighted_backproject`. Applies
265
- the `1/(2π)` Fourier-convention constant so a unit-density disk reconstructs
266
- to amplitude 1. Exported from `diffct`.
267
-
268
- #### `ramp_filter_1d` — new backward-compatible kwargs
269
-
270
- - `sample_spacing` (default `1.0`): physical detector cell pitch. Output is
271
- rescaled by `1/sample_spacing` for physical-unit correctness.
272
- - `pad_factor` (default `1`): zero-pad to `pad_factor * N` before the FFT to
273
- suppress circular-convolution wrap-around. `2` is recommended for FBP/FDK.
274
- - `window` (default `None`): frequency-domain apodization. Options: `None` /
275
- `"ram-lak"`, `"hann"`, `"hamming"`, `"cosine"`, `"shepp-logan"`.
276
- - `use_rfft` (default `True`): faster real-FFT path for real-valued inputs.
277
-
278
- Existing `ramp_filter_1d(sino, dim=1)` calls keep working unchanged.
279
-
280
- #### Analytical FBP / FDK scale factors
281
-
282
- Each analytical helper now applies the correct analytical constant
283
- automatically so reconstructions are already amplitude-calibrated:
284
-
285
- - `parallel_weighted_backproject`: `1 / (2π)`.
286
- - `fan_weighted_backproject`: `sdd / (2π · sid)`.
287
- - `cone_weighted_backproject`: `sdd / (2π · sid)`.
288
-
289
- ### Tests
290
-
291
- Test suite expanded from 6 to **27 tests**:
292
-
293
- - `tests/test_adjoint_inner_product.py` — the definitive check. For random `x`
294
- and `y`, asserts `⟨A x, y⟩ = ⟨x, A^T y⟩` for parallel, fan and cone autograd
295
- pairs. Permanently guards against any regression that would reintroduce the
296
- `distance_weight=1.0` bug.
297
- - `tests/test_fdk_cone_accuracy.py` — 128³ Shepp-Logan RMSE / amplitude bounds.
298
- - `tests/test_fdk_cone_offsets.py` — detector, center and combined offsets with
299
- amplitude assertions.
300
- - `tests/test_cone_projector_autograd.py` — gradient finiteness and non-zero
301
- sanity.
302
- - `tests/test_fbp_fan_accuracy.py` — 256×256 Shepp-Logan RMSE.
303
- - `tests/test_fbp_fan_offsets.py` — three offset configurations.
304
- - `tests/test_fbp_parallel_accuracy.py` — parallel FBP RMSE + offset.
305
-
306
- ### Examples
307
-
308
- `examples/fdk_cone.py`, `examples/fbp_fan.py` and `examples/fbp_parallel.py`
309
- have been rewritten with a consistent 8-step layout and detailed inline
310
- comments documenting every geometry variable (what it is, units, typical
311
- values, available options). Each example prints raw MSE, clamped MSE, and the
312
- reconstruction / phantom data ranges.
313
-
314
- The iterative reconstruction examples (`iterative_reco_parallel.py`,
315
- `iterative_reco_fan.py`, `iterative_reco_cone.py`) are unchanged — they
316
- silently benefit from the fixed autograd adjoint.
317
-
318
- ### Documentation
319
-
320
- - `docs/source/api.rst`: adds autofunction entries for
321
- `parallel_weighted_backproject`, documents the new `ramp_filter_1d`
322
- options, and adds an "Analytical FBP / FDK architecture" section
323
- explaining the scale-factor derivation.
324
- - `docs/source/fdk_cone_example.rst`: formula updated to reflect the
325
- voxel-gather kernel and `(sid/U)^2` weight.
326
- - `docs/source/fbp_fan_example.rst` and `fbp_parallel_example.rst`: completely
327
- rewritten to reference the new analytical helpers and fix a sign-convention
328
- inconsistency in the math section.
329
-
330
- ### Compatibility
331
-
332
- This release is source-compatible with 1.2.9 for every public entry point. The
333
- `distance_weight` parameter was internal and is removed from the private
334
- backward kernels; no public API touched that argument.
335
-
336
- ## [1.2.9] - 2026-02-14
337
-
338
- Restored `main` to the state of 1.2.9 by reverting an accidental merge.
339
- See [GitHub release v1.2.9](https://github.com/sypsyp97/diffct/releases/tag/v1.2.9).
340
-
341
- ## [1.2.8] - 2026-02-14
342
-
343
- See [GitHub release v1.2.8](https://github.com/sypsyp97/diffct/releases/tag/v1.2.8)
344
- and the commit history for details.
345
-
346
- ## [1.2.7] - 2025-08-05
347
-
348
- See [GitHub release v1.2.7](https://github.com/sypsyp97/diffct/releases/tag/v1.2.7).
349
-
350
- ## Earlier versions
351
-
352
- Releases 1.2.0 through 1.2.6 and the 1.1.x / 1.0.x lines are tracked on
353
- [GitHub releases](https://github.com/sypsyp97/diffct/releases). This CHANGELOG
354
- was introduced in 1.2.10 and does not back-fill detailed notes for earlier
355
- versions beyond pointers to the GitHub release pages.
356
-
357
- [Unreleased]: https://github.com/sypsyp97/diffct/compare/v1.3.0...HEAD
358
- [1.3.0]: https://github.com/sypsyp97/diffct/releases/tag/v1.3.0
359
- [1.2.11]: https://github.com/sypsyp97/diffct/releases/tag/v1.2.11
360
- [1.2.10]: https://github.com/sypsyp97/diffct/releases/tag/v1.2.10
361
- [1.2.9]: https://github.com/sypsyp97/diffct/releases/tag/v1.2.9
362
- [1.2.8]: https://github.com/sypsyp97/diffct/releases/tag/v1.2.8
363
- [1.2.7]: https://github.com/sypsyp97/diffct/releases/tag/v1.2.7
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.3.4] - 2026-04-23
9
+
10
+ ### Changed
11
+ - **Siddon projectors switched to cell-constant integration.** The 2D
12
+ parallel, 2D fan, and 3D cone Siddon forward/backward kernels in
13
+ `diffct/differentiable.py` now integrate each ray by accumulating
14
+ `d_image[iy, ix] * seg_len` on the traversed pixel/voxel instead of
15
+ sampling the bi-/tri-linear interpolant at the segment midpoint.
16
+ This simplifies the inner loop, removes the interpolation weight
17
+ bookkeeping, and keeps the forward / adjoint pair matched by
18
+ construction (still verified by
19
+ `tests/test_adjoint_inner_product.py`). Kernel docstrings and the
20
+ `"siddon"` backend blurbs in `examples/iterative_reco_*.py` have
21
+ been updated to say *cell-constant Siddon*.
22
+ - **Iterative examples clamp the reconstruction in-place** instead of
23
+ wrapping the forward pass in `nn.ReLU`. `examples/iterative_reco_parallel.py`,
24
+ `iterative_reco_fan.py`, and `iterative_reco_cone.py` now call
25
+ `self.model.reco.clamp_(min=0.0)` under `torch.no_grad()` after each
26
+ optimizer step, so gradients flow through the unclamped volume while
27
+ the stored state stays non-negative.
28
+ - **`examples/iterative_reco_fan.py` default backend** is now
29
+ `"siddon"` (was `"sf"`), matching the parallel and cone iterative
30
+ examples.
31
+
32
+ ### Fixed
33
+ - **FDK cone accuracy test tolerance.** `tests/test_fdk_cone_accuracy.py`
34
+ raises the maximum central z-profile error bound from `0.6` to `0.65`
35
+ to reflect the slightly sharper one-sample transition error at
36
+ ellipsoid boundaries under cell-constant forward projection; global
37
+ FDK RMSE is unchanged.
38
+
39
+ ## [1.3.3] - 2026-04-23
40
+
41
+ ### Fixed
42
+ - **Kernel numerical consistency and speed.** The CUDA kernels in
43
+ `diffct/differentiable.py` are now consistently `float32` end to end —
44
+ literal constants (`0.0`, `0.5`, `1.0`, `-inf`, ...) and integer-to-float
45
+ casts inside the Siddon and SF kernels are routed through typed `_DTYPE`
46
+ helpers so numba no longer implicitly promotes intermediates to `float64`.
47
+ This removes a silent precision mismatch that was slowing down every
48
+ projector/backprojector kernel; the library runs substantially faster
49
+ with no change in public-API behavior.
50
+ - **Thread indexing and grid configuration** in the 3D cone Siddon
51
+ forward / backward kernels and the 2D fan SF backward kernel. The
52
+ `cuda.grid(...)` unpack order and the matching `_grid_2d` / `_grid_3d`
53
+ launch arguments are aligned so the warp-adjacent axis matches the
54
+ stride-1 axis of the output buffer: `(iv, iu, iview)` for 3D cone
55
+ (`d_sino[view, u, v]` / `d_vol[ix, iy, iz]`) and `(ix, iy)` for 2D fan
56
+ SF. Fixes mismatched launches between `ConeProjectorFunction`,
57
+ `ConeBackprojectorFunction`, `FanProjectorFunction`, and
58
+ `FanBackprojectorFunction` and their kernels.
59
+ - **Walnut rotation-axis correction** in the shipped preprocessing and
60
+ sample data. `examples/data/preprocess_walnut.py` now applies the
61
+ Zenodo record's recommended 5-raw-pixel left shift before binning and
62
+ cropping, records the correction in the `.npz` metadata, and the
63
+ shipped `examples/data/walnut_cone.npz` / `walnut_reco.png` are
64
+ regenerated accordingly. `examples/realdata_walnut_fdk.py` and
65
+ `examples/data/NOTICE` are updated to surface the correction and the
66
+ richer walnut metadata.
67
+
68
+ ### Changed
69
+ - **Examples now require CUDA explicitly.** `examples/fbp_parallel.py`,
70
+ `fbp_fan.py`, `fdk_cone.py`, `iterative_reco_*.py`, `realdata_fbp_*.py`,
71
+ `realdata_fdk_cone.py`, and `realdata_walnut_fdk.py` assert
72
+ `torch.cuda.is_available()` and initialize the CUDA device up front
73
+ instead of silently falling back to CPU (which the numba-cuda kernels
74
+ never supported anyway).
75
+
76
+ ## [1.3.2] - 2026-04-23
77
+
78
+ ### Added
79
+ - **Expanded angular-weight regression tests** in
80
+ `tests/test_weights.py`, covering periodic `pi` half scans,
81
+ downsampled full scans, endpoint-included 721-view full scans, and
82
+ open short scans.
83
+
84
+ ### Changed
85
+ - **`angular_integration_weights` is now coverage-aware and order-stable.**
86
+ The helper sorts angles internally, detects `pi`- and `2*pi`-periodic
87
+ sampling more robustly, handles endpoint-included trajectories, and
88
+ maps the computed weights back to the caller's original angle order.
89
+ - **Walnut tooling is now configurable from the command line.**
90
+ `examples/data/preprocess_walnut.py` gained CLI switches for binning,
91
+ cropping, view stride, angle handedness, storage dtype, and archive
92
+ discovery; `examples/realdata_walnut_fdk.py` gained CLI controls for
93
+ backend, output volume size, voxel scale, filter window, output file
94
+ writing, and display suppression.
95
+ - **README polish for bilingual navigation and repo layout.**
96
+ `README.md` now exposes the Chinese README link near the top and
97
+ clarifies the file tree entries for `README.md` / `README.zh.md`.
98
+
99
+ ### Fixed
100
+ - **Angular quadrature artifacts** in analytical reconstructions when
101
+ full scans were endpoint-included or angularly downsampled. These
102
+ trajectories now receive periodic integration weights instead of being
103
+ misclassified as open scans.
104
+ - **Walnut example angle metadata and scan handedness.**
105
+ The walnut preprocessing and FDK example now use the dataset's
106
+ scanner-to-diffct angle convention explicitly and preserve the full
107
+ 721-view `0 .. 360 deg` acquisition description in the stored
108
+ metadata.
109
+
110
+ ## [1.3.1] - 2026-04-15
111
+
112
+ ### Added
113
+ - **Real-data walnut FDK example** at `examples/realdata_walnut_fdk.py`,
114
+ running the full analytical FDK pipeline on a preprocessed subset of
115
+ the Helsinki walnut cone-beam CT dataset ([Meaney 2022, Zenodo
116
+ 10.5281/zenodo.6986012](https://doi.org/10.5281/zenodo.6986012),
117
+ CC-BY 4.0). The 241-view, 256×256-per-view sinogram is shipped at
118
+ `examples/data/walnut_cone.npz` (~25 MB, float16, 8× binned, 256²
119
+ center crop) together with a `preprocess_walnut.py` regenerator
120
+ script, an attribution `NOTICE`, and a `walnut_reco.png` sample
121
+ montage. Reconstructs at half the nominal voxel size on a 512³ grid
122
+ using `backend="sf_tr"` + Hamming ramp window.
123
+ - **Synthetic "real-data" pipeline examples** for all three geometries
124
+ (`examples/realdata_fbp_parallel.py`, `realdata_fbp_fan.py`,
125
+ `realdata_fdk_cone.py`) that run a Beer-Lambert + Poisson + `-log`
126
+ forward model on Shepp-Logan and reconstruct with the analytical
127
+ pipeline, showing the proper place for `-log` preprocessing.
128
+ - **Chinese README** at `README.zh.md`, mirroring `README.md` with
129
+ bidirectional language links in both files.
130
+ - **Core Algorithm section** in the READMEs (EN + ZH) explaining the
131
+ Siddon + bilinear/trilinear interpolation design choice, the
132
+ trade-off (slight blur at nominal voxel size), and when to pick
133
+ SF vs VD honestly (see "Changed" below).
134
+
135
+ ### Changed
136
+ - **SF FBP / FDK backprojection kernels rewritten in LEAP's
137
+ chord-weighted matched-adjoint form** (from `projectors_SF.cu`).
138
+ The three analytical SF gather kernels
139
+ (`_fan_2d_sf_fbp_backproject_kernel`,
140
+ `_cone_3d_sf_tr_fdk_backproject_kernel`,
141
+ `_cone_3d_sf_tt_fdk_backproject_kernel`) now use the per-voxel
142
+ chord through the unit voxel instead of a `(sid/U)²`-plus-footprint-
143
+ area form, with a matching `sqrt(1+(v/sdd)²)` axial chord correction
144
+ on the cone side. Amplitude is calibrated against Siddon VD at
145
+ every voxel / detector-pitch combination tested (nominal, 0.64×
146
+ sub-nominal, 0.40× sub-nominal, supra-nominal) via the new
147
+ `det_spacing/(voxel·2π)` fan scale and
148
+ `(du·dv·sid)/(2π·sdd·voxel²)` cone scale constants.
149
+ - **Honest README Core Algorithm section on SF vs VD.** The earlier
150
+ "SF lowers MSE by ~17 %" and "SF is measurably sharper at sub-
151
+ nominal" claims in 1.3.0 are walked back: on Shepp-Logan and the
152
+ walnut dataset, LEAP-form SF and Siddon VD produce visually
153
+ indistinguishable edge profiles at typical CBCT magnifications,
154
+ and MSE differences are fractions of a percent. SF's real value
155
+ is its **mass-conserving cell-integrated forward model**, which
156
+ matters for iterative reconstruction, learned priors, and
157
+ sinogram-level losses — not as a drop-in sharpness knob on
158
+ analytical FBP / FDK. The shipped `fbp_fan.py`, `fdk_cone.py`,
159
+ and `realdata_walnut_fdk.py` example comments are updated to
160
+ match.
161
+
162
+ ### Fixed
163
+ - **SF FBP / FDK weight normalization** when the voxel's trapezoidal
164
+ footprint straddles the default 4-corner sort. The 1.3.0 SF
165
+ backprojection kernels used `fdk_w / (span · v_span)` (bounding-box
166
+ area) which under-amplified oblique views by up to 2× per axis and
167
+ produced a dim, angularly-modulated reconstruction. Diagnosed by
168
+ inspecting a Shepp-Logan SF reconstruction, corrected first to
169
+ `fdk_w / ((span+plateau)/2 · v_span)` (true trapezoid area) and
170
+ then fully replaced by the LEAP chord-weighted form above.
171
+
172
+ ### Acknowledgements
173
+ - [LEAP](https://github.com/LLNL/LEAP) (LLNL / Hyojin Kim et al.)
174
+ added to `README.md` / `README.zh.md` Acknowledgements.
175
+ `projectors_SF.cu` is the reference implementation the new
176
+ analytical SF backprojection kernels are ported from.
177
+
178
+ ## [1.3.0] - 2026-04-14
179
+
180
+ ### Added
181
+ - **Separable-footprint (SF) projector backends** for 2D fan and 3D cone
182
+ beam geometries, following Long-Fessler-Balter (IEEE TMI 2010).
183
+ `FanProjectorFunction` / `FanBackprojectorFunction` now accept
184
+ `backend="siddon"` (default, unchanged) or `backend="sf"`.
185
+ `ConeProjectorFunction` / `ConeBackprojectorFunction` accept
186
+ `backend="siddon"` (default), `"sf_tr"` (trapezoid in transaxial,
187
+ rectangle in axial) or `"sf_tt"` (trapezoid in both directions with
188
+ axial rise/fall from `U_near`/`U_far` across the voxel). Each SF
189
+ backend is implemented as a matched scatter/gather pair of CUDA
190
+ kernels so both forward and adjoint are available — autograd works
191
+ end-to-end for every backend, and the standalone Backprojector
192
+ Function can also be driven in SF mode for iterative reconstruction.
193
+ - **Adjoint inner-product tests** for every new SF backend in
194
+ `tests/test_adjoint_inner_product.py`, plus `torch.autograd.gradcheck`
195
+ coverage in `tests/test_gradcheck.py`. Test count grew from 57 to 66.
196
+
197
+ ### Changed
198
+ - **Internal grid-point convention** is now uniformly `x_v = ix - cx`
199
+ (matching `_cone_3d_fdk_backproject_kernel`). The SF kernels use the
200
+ same convention so voxel centers align with where the Siddon/FDK
201
+ paths interpret `d_vol[ix, iy, iz]`.
202
+ - **`_TPB_SF_3D = (4, 4, 4)`** introduced for the 3D SF kernels. SF-TT
203
+ in particular exceeds the 128-register-per-thread budget allowed by
204
+ the default `(8, 8, 8)` launch; the smaller block gives each thread
205
+ ~1024 registers and avoids `CUDA_ERROR_LAUNCH_OUT_OF_RESOURCES`.
206
+
207
+ ### Notes
208
+ - **Raw forward accuracy vs analytical Radon**: on both 2D fan
209
+ (512² Shepp-Logan) and 3D cone (96³ and 128³ 3D Shepp-Logan) phantoms,
210
+ measured against the analytical ellipse / ellipsoid Radon transform,
211
+ the existing Siddon projector with bilinear / trilinear interpolation
212
+ is slightly more accurate (~3% better RMSE, ~15% better max error)
213
+ than SF-TR / SF-TT at the thin-ray sampling level. The classic SF
214
+ advantage reported in Long et al. is over the piecewise-constant DD
215
+ projector; diffct's Siddon uses smooth interpolation, which covers
216
+ most of the gap SF was designed to close.
217
+ - **Full analytical reconstruction (forward + FBP/FDK)**: despite the
218
+ raw-forward result above, in a complete analytical pipeline
219
+ (`FanProjectorFunction` / `ConeProjectorFunction` -> `ramp_filter_1d`
220
+ -> `fan_weighted_backproject` / `cone_weighted_backproject`) SF
221
+ **measurably improves reconstruction quality**. On the 256²
222
+ Shepp-Logan `fbp_fan.py` example the raw MSE drops from
223
+ `0.00216` (Siddon) to `0.00178` (SF-TR), a ~17% improvement.
224
+ On the 128³ `fdk_cone.py` example it drops from `0.00325` (Siddon)
225
+ to `0.00271` (SF-TR), again ~17%. The mechanism is that SF's
226
+ mass-conserving cell-integrated forward pairs more naturally with
227
+ the voxel-driven FBP/FDK gather backprojector than Siddon's
228
+ thin-ray sampling, which feeds extra high-frequency ringing into
229
+ the ramp filter.
230
+ - **Performance**: SF-TR is ~2× and SF-TT ~2.5–4× slower than Siddon for
231
+ cone beam, and ~3× slower than Siddon for fan beam. The extra cost
232
+ comes from voxel-driven scatter with atomic adds (forward) versus
233
+ ray-driven parallelism in Siddon.
234
+ - **Recommendation**: `backend="siddon"` remains the default for
235
+ backward compatibility and raw speed. For **analytical FBP/FDK
236
+ reconstruction** where reconstruction MSE matters more than forward
237
+ runtime, `"sf"` / `"sf_tr"` is a quantifiably better choice — at
238
+ 2–3× forward cost you get ~17% lower reconstruction MSE on standard
239
+ phantoms. For **iterative reconstruction with autograd**, either
240
+ backend is valid; SF has the theoretical advantage of exact voxel-
241
+ scale mass conservation on both forward and adjoint, but in practice
242
+ Siddon's smoothing usually helps optimizer convergence. SF-TT's
243
+ axial-trapezoid refinement over SF-TR improves RMSE only by ~0.001
244
+ and is not worth the extra cost for typical cone angles; use SF-TR
245
+ by default and reserve SF-TT for extreme cone-angle research.
246
+
247
+ ## [1.2.11] - 2026-04-14
248
+
249
+ ### Added
250
+ - **Parker short-scan demos** in `examples/fdk_cone.py` and `examples/fbp_fan.py`.
251
+ Both examples now expose an `apply_parker` switch that not only multiplies
252
+ by `parker_weights` but also samples angles on the minimal short-scan range
253
+ `pi + 2*gamma_max`, where `gamma_max` is computed from the detector half-width
254
+ and `sdd`. (Before this change the `apply_parker` flag in `fbp_fan.py` was
255
+ silently a no-op because the angular range stayed at `2*pi`.)
256
+ - **`tests/test_gradcheck.py`** — `torch.autograd.gradcheck` coverage for
257
+ `ParallelProjectorFunction`, `FanProjectorFunction`, and `ConeProjectorFunction`.
258
+ Compares the analytical backward Jacobian to a finite-difference numerical
259
+ Jacobian over small inputs with float32-calibrated tolerances. Strongest
260
+ autograd-correctness guard beyond the existing inner-product adjoint tests.
261
+ - **`tests/test_ramp_filter_windows.py`** — 27 parametrised tests covering every
262
+ `ramp_filter_1d` window option (`None`/`"ram-lak"`, `"hann"`, `"hanning"`,
263
+ `"hamming"`, `"cosine"`, `"shepp-logan"`) at three layers: direct `_ramp_window`
264
+ helper (DC gain, Nyquist value, monotonicity), full `ramp_filter_1d` end-to-end
265
+ (shape, DC annihilation, pad correctness, rfft vs complex-fft parity,
266
+ `sample_spacing` scaling), and a high-frequency attenuation sanity check on a
267
+ step input.
268
+ - **`CHANGELOG.md`** — new Keep-a-Changelog file at the repo root.
269
+ - **`tests/benchmarks/`** — opt-in `pytest-benchmark` suite covering every CUDA
270
+ kernel in the library (forward projector, pure-adjoint backprojector, and
271
+ analytical FBP/FDK pipeline) across three sizes for each of the three
272
+ geometries. 27 benchmarks total. Excluded from the default `pytest tests/`
273
+ run via `--ignore=tests/benchmarks` in `pytest.ini` so the normal test suite
274
+ stays fast; run explicitly with `pytest tests/benchmarks/ --benchmark-only`.
275
+ Enables before/after perf comparisons via `--benchmark-save=` /
276
+ `--benchmark-compare=`.
277
+
278
+ ### Changed
279
+ - **`_cone_3d_fdk_backproject_kernel`** memory-access layout. The kernel now
280
+ unpacks `cuda.grid(3)` as `(iz, iy, ix)` instead of `(ix, iy, iz)`, and
281
+ `cone_weighted_backproject` launches the grid as `_grid_3d(D, H, W)` instead
282
+ of `_grid_3d(W, H, D)`. This makes `iz` warp-adjacent, which matches the
283
+ innermost stride-1 axis of the WHD output buffer, so the final `d_vol` write
284
+ becomes coalesced. Measured speedup on a 64^3–160^3 sweep: 1.0x–1.14x,
285
+ within run-to-run noise (the kernel is read-bound on the sinogram bilinear
286
+ samples, not write-bound, so L2 absorbed the old uncoalesced writes). The
287
+ fix is kept as the right default and future-proofs the kernel against
288
+ edits that shift work toward the write path. Matching coalescing comments
289
+ were added to the 2D `_parallel_2d_fbp_backproject_kernel` and
290
+ `_fan_2d_fbp_backproject_kernel` kernels, which were already coalesced
291
+ (their first grid index is also warp-adjacent and matches the `Nx` innermost
292
+ axis of their `(Ny, Nx)` output buffers).
293
+
294
+ ### Notes
295
+ - No public-API changes. Pure-adjoint kernels and autograd Function classes
296
+ are untouched, so the autograd and iterative-reconstruction paths are
297
+ byte-for-byte equivalent to 1.2.10.
298
+
299
+ ## [1.2.10] - 2026-04-14
300
+
301
+ This release is a correctness overhaul of every analytical reconstruction path
302
+ (parallel / fan / cone) **and** of the autograd adjoint path shared by the
303
+ iterative examples. It also ships a unified architecture where each geometry
304
+ has its own dedicated voxel-driven FBP/FDK gather kernel, separate from the
305
+ pure Siddon adjoint used by autograd.
306
+
307
+ ### Fixed
308
+
309
+ #### Analytical FBP / FDK amplitude bugs
310
+
311
+ Prior to this release, all three analytical reconstruction examples produced
312
+ results that were off by large constant factors:
313
+
314
+ | Example | Pre-fix reco range | Post-fix reco range | MSE improvement |
315
+ |---|---|---|---|
316
+ | `fbp_parallel.py` | [0, 6.52] | [−0.02, 1.01] | ~580× |
317
+ | `fbp_fan.py` | [0, 6.33] | [−0.08, 1.01] | ~900× |
318
+ | `fdk_cone.py` | [0, 10.08] | [−0.08, 1.00] | ~600× |
319
+
320
+ Root causes:
321
+ - Cone and fan used `(sdd/U)^2` with `U = sdd + x·sin − y·cos` instead of
322
+ the correct FDK weight `(sid/U)^2` with `U = sid + x·sin − y·cos`.
323
+ - All three geometries were missing the `1/(2π)` Fourier-convention
324
+ constant from the FBP/FDK reconstruction formula.
325
+ - Cone and fan used a Siddon ray-driven scatter for the analytical path,
326
+ which is neither a true adjoint nor a classical FBP/FDK gather.
327
+
328
+ #### Autograd adjoint bug (cone + fan)
329
+
330
+ `ConeProjectorFunction.backward`, `ConeBackprojectorFunction.forward`,
331
+ `FanProjectorFunction.backward` and `FanBackprojectorFunction.forward`
332
+ were all passing `distance_weight=1.0` to the shared Siddon backward
333
+ kernel. This meant the autograd backward was **not the true adjoint** `P^T`
334
+ of the forward projector — it had a `(sdd/U)^2` per-voxel factor baked in,
335
+ biasing the gradient by ~2–3× depending on voxel position. Any iterative
336
+ reconstruction that relied on autograd (including `iterative_reco_cone.py`
337
+ and `iterative_reco_fan.py`) was running on a biased gradient flow.
338
+
339
+ Parallel beam was unaffected (its backward kernel had no `distance_weight`
340
+ parameter to begin with).
341
+
342
+ This release removes the `distance_weight` parameter from both
343
+ `_cone_3d_backward_kernel` and `_fan_2d_backward_kernel` entirely, so the
344
+ dead code path cannot be accidentally reintroduced, and fixes the four
345
+ autograd call sites to use the pure adjoint.
346
+
347
+ ### Added
348
+
349
+ #### Voxel-driven gather kernels for every geometry
350
+
351
+ Three new CUDA kernels, all under a dedicated `fastmath=False` decorator
352
+ for FDK-grade accuracy:
353
+
354
+ - `_parallel_2d_fbp_backproject_kernel` — no distance weighting (no source).
355
+ - `_fan_2d_fbp_backproject_kernel` — `(sid/U)^2` + linear detector interp.
356
+ - `_cone_3d_fdk_backproject_kernel` — `(sid/U)^2` + bilinear detector interp.
357
+
358
+ Each kernel is voxel-driven: one thread per output pixel/voxel, loops over
359
+ views, computes the projected detector coordinate, interpolates the filtered
360
+ sinogram, weights and accumulates.
361
+
362
+ #### `parallel_weighted_backproject` (new public helper)
363
+
364
+ Mirrors `fan_weighted_backproject` and `cone_weighted_backproject`. Applies
365
+ the `1/(2π)` Fourier-convention constant so a unit-density disk reconstructs
366
+ to amplitude 1. Exported from `diffct`.
367
+
368
+ #### `ramp_filter_1d` — new backward-compatible kwargs
369
+
370
+ - `sample_spacing` (default `1.0`): physical detector cell pitch. Output is
371
+ rescaled by `1/sample_spacing` for physical-unit correctness.
372
+ - `pad_factor` (default `1`): zero-pad to `pad_factor * N` before the FFT to
373
+ suppress circular-convolution wrap-around. `2` is recommended for FBP/FDK.
374
+ - `window` (default `None`): frequency-domain apodization. Options: `None` /
375
+ `"ram-lak"`, `"hann"`, `"hamming"`, `"cosine"`, `"shepp-logan"`.
376
+ - `use_rfft` (default `True`): faster real-FFT path for real-valued inputs.
377
+
378
+ Existing `ramp_filter_1d(sino, dim=1)` calls keep working unchanged.
379
+
380
+ #### Analytical FBP / FDK scale factors
381
+
382
+ Each analytical helper now applies the correct analytical constant
383
+ automatically so reconstructions are already amplitude-calibrated:
384
+
385
+ - `parallel_weighted_backproject`: `1 / (2π)`.
386
+ - `fan_weighted_backproject`: `sdd / (2π · sid)`.
387
+ - `cone_weighted_backproject`: `sdd / (2π · sid)`.
388
+
389
+ ### Tests
390
+
391
+ Test suite expanded from 6 to **27 tests**:
392
+
393
+ - `tests/test_adjoint_inner_product.py` — the definitive check. For random `x`
394
+ and `y`, asserts `⟨A x, y⟩ = ⟨x, A^T y⟩` for parallel, fan and cone autograd
395
+ pairs. Permanently guards against any regression that would reintroduce the
396
+ `distance_weight=1.0` bug.
397
+ - `tests/test_fdk_cone_accuracy.py` — 128³ Shepp-Logan RMSE / amplitude bounds.
398
+ - `tests/test_fdk_cone_offsets.py` — detector, center and combined offsets with
399
+ amplitude assertions.
400
+ - `tests/test_cone_projector_autograd.py` — gradient finiteness and non-zero
401
+ sanity.
402
+ - `tests/test_fbp_fan_accuracy.py` — 256×256 Shepp-Logan RMSE.
403
+ - `tests/test_fbp_fan_offsets.py` — three offset configurations.
404
+ - `tests/test_fbp_parallel_accuracy.py` — parallel FBP RMSE + offset.
405
+
406
+ ### Examples
407
+
408
+ `examples/fdk_cone.py`, `examples/fbp_fan.py` and `examples/fbp_parallel.py`
409
+ have been rewritten with a consistent 8-step layout and detailed inline
410
+ comments documenting every geometry variable (what it is, units, typical
411
+ values, available options). Each example prints raw MSE, clamped MSE, and the
412
+ reconstruction / phantom data ranges.
413
+
414
+ The iterative reconstruction examples (`iterative_reco_parallel.py`,
415
+ `iterative_reco_fan.py`, `iterative_reco_cone.py`) are unchanged — they
416
+ silently benefit from the fixed autograd adjoint.
417
+
418
+ ### Documentation
419
+
420
+ - `docs/source/api.rst`: adds autofunction entries for
421
+ `parallel_weighted_backproject`, documents the new `ramp_filter_1d`
422
+ options, and adds an "Analytical FBP / FDK architecture" section
423
+ explaining the scale-factor derivation.
424
+ - `docs/source/fdk_cone_example.rst`: formula updated to reflect the
425
+ voxel-gather kernel and `(sid/U)^2` weight.
426
+ - `docs/source/fbp_fan_example.rst` and `fbp_parallel_example.rst`: completely
427
+ rewritten to reference the new analytical helpers and fix a sign-convention
428
+ inconsistency in the math section.
429
+
430
+ ### Compatibility
431
+
432
+ This release is source-compatible with 1.2.9 for every public entry point. The
433
+ `distance_weight` parameter was internal and is removed from the private
434
+ backward kernels; no public API touched that argument.
435
+
436
+ ## [1.2.9] - 2026-02-14
437
+
438
+ Restored `main` to the state of 1.2.9 by reverting an accidental merge.
439
+ See [GitHub release v1.2.9](https://github.com/sypsyp97/diffct/releases/tag/v1.2.9).
440
+
441
+ ## [1.2.8] - 2026-02-14
442
+
443
+ See [GitHub release v1.2.8](https://github.com/sypsyp97/diffct/releases/tag/v1.2.8)
444
+ and the commit history for details.
445
+
446
+ ## [1.2.7] - 2025-08-05
447
+
448
+ See [GitHub release v1.2.7](https://github.com/sypsyp97/diffct/releases/tag/v1.2.7).
449
+
450
+ ## Earlier versions
451
+
452
+ Releases 1.2.0 through 1.2.6 and the 1.1.x / 1.0.x lines are tracked on
453
+ [GitHub releases](https://github.com/sypsyp97/diffct/releases). This CHANGELOG
454
+ was introduced in 1.2.10 and does not back-fill detailed notes for earlier
455
+ versions beyond pointers to the GitHub release pages.
456
+
457
+ [1.3.3]: https://github.com/sypsyp97/diffct/releases/tag/v1.3.3
458
+ [1.3.2]: https://github.com/sypsyp97/diffct/releases/tag/v1.3.2
459
+ [1.3.1]: https://github.com/sypsyp97/diffct/releases/tag/v1.3.1
460
+ [1.3.0]: https://github.com/sypsyp97/diffct/releases/tag/v1.3.0
461
+ [1.2.11]: https://github.com/sypsyp97/diffct/releases/tag/v1.2.11
462
+ [1.2.10]: https://github.com/sypsyp97/diffct/releases/tag/v1.2.10
463
+ [1.2.9]: https://github.com/sypsyp97/diffct/releases/tag/v1.2.9
464
+ [1.2.8]: https://github.com/sypsyp97/diffct/releases/tag/v1.2.8
465
+ [1.2.7]: https://github.com/sypsyp97/diffct/releases/tag/v1.2.7