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