diffct 2.0.0__py3-none-any.whl

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.
@@ -0,0 +1,43 @@
1
+ """CUDA kernels for CT projections.
2
+
3
+ This subpackage contains CUDA kernels for 2D and 3D CT forward projection
4
+ and backprojection operations.
5
+ """
6
+
7
+ from .parallel_beam import (
8
+ _parallel_2d_forward_kernel,
9
+ _parallel_2d_backward_kernel,
10
+ _parallel_2d_fbp_backproject_kernel,
11
+ )
12
+
13
+ from .fan_beam import (
14
+ _fan_2d_forward_kernel,
15
+ _fan_2d_backward_kernel,
16
+ _fan_2d_fbp_backproject_kernel,
17
+ )
18
+
19
+ from .cone_beam import (
20
+ _cone_3d_forward_kernel,
21
+ _cone_3d_backward_kernel,
22
+ _cone_3d_fdk_backproject_kernel,
23
+ )
24
+ from .geometry_vjp import (
25
+ _parallel_2d_geometry_vjp_kernel,
26
+ _fan_2d_geometry_vjp_kernel,
27
+ _cone_3d_geometry_vjp_kernel,
28
+ )
29
+
30
+ __all__ = [
31
+ '_parallel_2d_forward_kernel',
32
+ '_parallel_2d_backward_kernel',
33
+ '_parallel_2d_fbp_backproject_kernel',
34
+ '_fan_2d_forward_kernel',
35
+ '_fan_2d_backward_kernel',
36
+ '_fan_2d_fbp_backproject_kernel',
37
+ '_cone_3d_forward_kernel',
38
+ '_cone_3d_backward_kernel',
39
+ '_cone_3d_fdk_backproject_kernel',
40
+ '_parallel_2d_geometry_vjp_kernel',
41
+ '_fan_2d_geometry_vjp_kernel',
42
+ '_cone_3d_geometry_vjp_kernel',
43
+ ]
@@ -0,0 +1,617 @@
1
+ """CUDA kernels for 3D cone beam projections.
2
+
3
+ This module contains CUDA kernels implementing the Siddon ray-tracing method
4
+ for 3D cone beam forward projection and backprojection, plus a dedicated
5
+ voxel-driven FDK gather kernel for analytical reconstruction pipelines.
6
+ """
7
+
8
+ import math
9
+ import numpy as np
10
+ from numba import cuda
11
+
12
+ from ..constants import (
13
+ _FASTMATH_DECORATOR,
14
+ _FDK_ACCURACY_DECORATOR,
15
+ _BIG,
16
+ _TINY,
17
+ _ZERO,
18
+ _ONE,
19
+ _HALF,
20
+ _EPSILON,
21
+ )
22
+
23
+
24
+ # ============================================================================
25
+ # 3D Cone Beam Forward Projection Kernel
26
+ # ============================================================================
27
+
28
+ @_FASTMATH_DECORATOR
29
+ def _cone_3d_forward_kernel(
30
+ d_vol, Nx, Ny, Nz,
31
+ d_sino, n_views, n_u, n_v,
32
+ du, dv, d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
33
+ cx, cy, cz, voxel_spacing
34
+ ):
35
+ """Compute the 3D cone-beam forward projection with arbitrary source-detector trajectories.
36
+
37
+ This CUDA kernel implements cell-constant Siddon ray tracing for
38
+ 3D cone-beam forward projection. Supports arbitrary source and detector positions
39
+ for each view, enabling non-circular trajectories.
40
+
41
+ Parameters
42
+ ----------
43
+ d_vol : numba.cuda.cudadrv.devicearray.DeviceNDArray
44
+ Input 3D volume array on CUDA.
45
+ Nx : int
46
+ Number of voxels along the x-axis.
47
+ Ny : int
48
+ Number of voxels along the y-axis.
49
+ Nz : int
50
+ Number of voxels along the z-axis.
51
+ d_sino : numba.cuda.cudadrv.devicearray.DeviceNDArray
52
+ Output cone-beam sinogram array on CUDA.
53
+ n_views : int
54
+ Number of projection views.
55
+ n_u : int
56
+ Number of detector elements along the u-axis.
57
+ n_v : int
58
+ Number of detector elements along the v-axis.
59
+ du : float
60
+ Physical spacing between detector elements along the u-axis.
61
+ dv : float
62
+ Physical spacing between detector elements along the v-axis.
63
+ d_src_pos : numba.cuda.cudadrv.devicearray.DeviceNDArray
64
+ Source positions for each view, shape (n_views, 3), in physical units.
65
+ d_det_center : numba.cuda.cudadrv.devicearray.DeviceNDArray
66
+ Detector center positions for each view, shape (n_views, 3), in physical units.
67
+ d_det_u_vec : numba.cuda.cudadrv.devicearray.DeviceNDArray
68
+ Detector u-direction unit vectors for each view, shape (n_views, 3).
69
+ d_det_v_vec : numba.cuda.cudadrv.devicearray.DeviceNDArray
70
+ Detector v-direction unit vectors for each view, shape (n_views, 3).
71
+ cx : float
72
+ Half of volume width along x-axis (in voxels).
73
+ cy : float
74
+ Half of volume height along y-axis (in voxels).
75
+ cz : float
76
+ Half of volume depth along z-axis (in voxels).
77
+ voxel_spacing : float
78
+ Physical size of one voxel (in same units as source/detector positions).
79
+
80
+ Notes
81
+ -----
82
+ Supports arbitrary cone-beam geometries by specifying source position,
83
+ detector center, and detector orientation vectors for each view.
84
+ Integrates each ray through piecewise-constant voxel cells.
85
+ """
86
+ iv, iu, iview = cuda.grid(3)
87
+ if iview >= n_views or iu >= n_u or iv >= n_v:
88
+ return
89
+
90
+ # === 3D CONE BEAM GEOMETRY SETUP (ARBITRARY TRAJECTORY) ===
91
+ # Read source position from position matrix (in physical units)
92
+ src_x = d_src_pos[iview, 0] / voxel_spacing
93
+ src_y = d_src_pos[iview, 1] / voxel_spacing
94
+ src_z = d_src_pos[iview, 2] / voxel_spacing
95
+
96
+ # Read detector center and orientation vectors
97
+ det_cx = d_det_center[iview, 0] / voxel_spacing
98
+ det_cy = d_det_center[iview, 1] / voxel_spacing
99
+ det_cz = d_det_center[iview, 2] / voxel_spacing
100
+
101
+ u_vec_x = d_det_u_vec[iview, 0]
102
+ u_vec_y = d_det_u_vec[iview, 1]
103
+ u_vec_z = d_det_u_vec[iview, 2]
104
+
105
+ v_vec_x = d_det_v_vec[iview, 0]
106
+ v_vec_y = d_det_v_vec[iview, 1]
107
+ v_vec_z = d_det_v_vec[iview, 2]
108
+
109
+ # Calculate detector element offset from center
110
+ u_offset = (np.float32(iu) + _HALF - np.float32(n_u) * _HALF) * du / voxel_spacing
111
+ v_offset = (np.float32(iv) + _HALF - np.float32(n_v) * _HALF) * dv / voxel_spacing
112
+
113
+ # Calculate 3D detector element position using center + u*u_vec + v*v_vec
114
+ det_x = det_cx + u_offset * u_vec_x + v_offset * v_vec_x
115
+ det_y = det_cy + u_offset * u_vec_y + v_offset * v_vec_y
116
+ det_z = det_cz + u_offset * u_vec_z + v_offset * v_vec_z
117
+
118
+ # === 3D RAY DIRECTION CALCULATION ===
119
+ # Ray direction vector from source to detector element in 3D space
120
+ dir_x, dir_y, dir_z = det_x - src_x, det_y - src_y, det_z - src_z
121
+ length = math.sqrt(dir_x*dir_x + dir_y*dir_y + dir_z*dir_z) # 3D ray length
122
+ if length < _EPSILON: # Degenerate ray case
123
+ d_sino[iview, iu, iv] = _ZERO; return
124
+
125
+ # Normalize 3D ray direction vector for parametric traversal
126
+ inv_len = _ONE / length
127
+ dir_x, dir_y, dir_z = dir_x*inv_len, dir_y*inv_len, dir_z*inv_len
128
+
129
+ # === 3D RAY-VOLUME INTERSECTION CALCULATION ===
130
+ # Measure the ray parameter from the endpoint nearer the volume centre, so a
131
+ # distant source does not make the float32 parameters coarse. Only the
132
+ # source-to-detector segment is integrated.
133
+ if src_x * src_x + src_y * src_y + src_z * src_z <= det_x * det_x + det_y * det_y + det_z * det_z:
134
+ org_x = src_x
135
+ org_y = src_y
136
+ org_z = src_z
137
+ t_min, t_max = _ZERO, length
138
+ else:
139
+ org_x = det_x
140
+ org_y = det_y
141
+ org_z = det_z
142
+ t_min, t_max = -length, _ZERO
143
+
144
+ # X-direction boundary intersections
145
+ if abs(dir_x) > _TINY:
146
+ tx1, tx2 = (-cx - org_x) / dir_x, (cx - org_x) / dir_x
147
+ t_min, t_max = max(t_min, min(tx1, tx2)), min(t_max, max(tx1, tx2))
148
+ elif org_x < -cx or org_x > cx: # Ray origin outside x-bounds
149
+ d_sino[iview, iu, iv] = _ZERO; return
150
+
151
+ # Y-direction boundary intersections
152
+ if abs(dir_y) > _TINY:
153
+ ty1, ty2 = (-cy - org_y) / dir_y, (cy - org_y) / dir_y
154
+ t_min, t_max = max(t_min, min(ty1, ty2)), min(t_max, max(ty1, ty2))
155
+ elif org_y < -cy or org_y > cy: # Ray origin outside y-bounds
156
+ d_sino[iview, iu, iv] = _ZERO; return
157
+
158
+ # Z-direction boundary intersections (extends 2D algorithm to 3D)
159
+ if abs(dir_z) > _TINY:
160
+ tz1, tz2 = (-cz - org_z) / dir_z, (cz - org_z) / dir_z
161
+ t_min, t_max = max(t_min, min(tz1, tz2)), min(t_max, max(tz1, tz2))
162
+ elif org_z < -cz or org_z > cz: # Ray origin outside z-bounds
163
+ d_sino[iview, iu, iv] = _ZERO; return
164
+
165
+ if t_min >= t_max: # No valid 3D intersection
166
+ d_sino[iview, iu, iv] = _ZERO; return
167
+
168
+ # The traversal restarts at the entry point, so its parameters stay
169
+ # within the volume diameter.
170
+ ent_x = org_x + t_min * dir_x
171
+ ent_y = org_y + t_min * dir_y
172
+ ent_z = org_z + t_min * dir_z
173
+ ray_x = dir_x
174
+ ray_y = dir_y
175
+ ray_z = dir_z
176
+ t_end = t_max - t_min
177
+
178
+ # === 3D SIDDON METHOD TRAVERSAL INITIALIZATION ===
179
+ accum = _ZERO # Accumulated projection value
180
+ t = _ZERO
181
+
182
+ # Convert 3D ray entry point to voxel indices
183
+ ix = int(math.floor(ent_x + t * ray_x + cx)) # Current voxel x-index
184
+ iy = int(math.floor(ent_y + t * ray_y + cy)) # Current voxel y-index
185
+ iz = int(math.floor(ent_z + t * ray_z + cz)) # Current voxel z-index
186
+
187
+ # 3D traversal parameters (extends 2D algorithm)
188
+ step_x, step_y, step_z = (1 if ray_x >= 0 else -1), (1 if ray_y >= 0 else -1), (1 if ray_z >= 0 else -1)
189
+ inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
190
+ inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
191
+ inv_dir_z = (_ONE / ray_z) if abs(ray_z) > _TINY else _ZERO
192
+ dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG # Parameter increment per x-voxel
193
+ dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG # Parameter increment per y-voxel
194
+ dt_z = abs(inv_dir_z) if abs(ray_z) > _TINY else _BIG # Parameter increment per z-voxel
195
+
196
+ # Calculate parameter values for next 3D voxel boundary crossings
197
+ next_ix = ix + (1 if step_x > 0 else 0)
198
+ next_iy = iy + (1 if step_y > 0 else 0)
199
+ next_iz = iz + (1 if step_z > 0 else 0)
200
+ tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
201
+ ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
202
+ tz = (np.float32(next_iz) - cz - ent_z) * inv_dir_z if abs(ray_z) > _TINY else _BIG
203
+
204
+ # === 3D TRAVERSAL LOOP WITH CELL-CONSTANT SIDDON INTEGRATION ===
205
+ while t < t_end:
206
+ if 0 <= ix < Nx and 0 <= iy < Ny and 0 <= iz < Nz:
207
+ # Determine next 3D voxel boundary crossing (minimum of x, y, z boundaries or ray exit)
208
+ t_next = min(tx, ty, tz, t_end)
209
+ seg_len = t_next - t
210
+ if seg_len > _ZERO:
211
+ accum += d_vol[ix, iy, iz] * seg_len
212
+
213
+ # === 3D VOXEL BOUNDARY CROSSING LOGIC ===
214
+ # Advance to next voxel based on which boundary is crossed first in 3D
215
+ if tx <= ty and tx <= tz: # X-boundary crossed first
216
+ t = tx
217
+ ix += step_x
218
+ tx += dt_x
219
+ elif ty <= tx and ty <= tz: # Y-boundary crossed first
220
+ t = ty
221
+ iy += step_y
222
+ ty += dt_y
223
+ else: # Z-boundary crossed first
224
+ t = tz
225
+ iz += step_z
226
+ tz += dt_z
227
+
228
+ # Siddon segment lengths are in voxel units; voxel_spacing makes them physical lengths.
229
+ d_sino[iview, iu, iv] = accum * voxel_spacing
230
+
231
+ @_FASTMATH_DECORATOR
232
+
233
+ # ============================================================================
234
+ # 3D Cone Beam Backprojection Kernel
235
+ # ============================================================================
236
+
237
+ def _cone_3d_backward_kernel(
238
+ d_sino, n_views, n_u, n_v,
239
+ d_vol, Nx, Ny, Nz,
240
+ du, dv, d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
241
+ cx, cy, cz, voxel_spacing
242
+ ):
243
+ """Compute the 3D cone-beam backprojection with arbitrary source-detector trajectories.
244
+
245
+ This CUDA kernel implements the adjoint of cell-constant Siddon ray tracing for
246
+ 3D cone-beam backprojection. Supports arbitrary source and detector positions
247
+ for each view, enabling non-circular trajectories.
248
+
249
+ Parameters
250
+ ----------
251
+ d_sino : numba.cuda.cudadrv.devicearray.DeviceNDArray
252
+ Input cone-beam sinogram array on CUDA.
253
+ n_views : int
254
+ Number of projection views.
255
+ n_u : int
256
+ Number of detector elements along the u-axis.
257
+ n_v : int
258
+ Number of detector elements along the v-axis.
259
+ d_vol : numba.cuda.cudadrv.devicearray.DeviceNDArray
260
+ Output 3D volume gradient array on CUDA.
261
+ Nx : int
262
+ Number of voxels along the x-axis.
263
+ Ny : int
264
+ Number of voxels along the y-axis.
265
+ Nz : int
266
+ Number of voxels along the z-axis.
267
+ du : float
268
+ Physical spacing between detector elements along the u-axis.
269
+ dv : float
270
+ Physical spacing between detector elements along the v-axis.
271
+ d_src_pos : numba.cuda.cudadrv.devicearray.DeviceNDArray
272
+ Source positions for each view, shape (n_views, 3), in physical units.
273
+ d_det_center : numba.cuda.cudadrv.devicearray.DeviceNDArray
274
+ Detector center positions for each view, shape (n_views, 3), in physical units.
275
+ d_det_u_vec : numba.cuda.cudadrv.devicearray.DeviceNDArray
276
+ Detector u-direction unit vectors for each view, shape (n_views, 3).
277
+ d_det_v_vec : numba.cuda.cudadrv.devicearray.DeviceNDArray
278
+ Detector v-direction unit vectors for each view, shape (n_views, 3).
279
+ cx : float
280
+ Half of volume width along x-axis (in voxels).
281
+ cy : float
282
+ Half of volume height along y-axis (in voxels).
283
+ cz : float
284
+ Half of volume depth along z-axis (in voxels).
285
+ voxel_spacing : float
286
+ Physical size of one voxel (in same units as source/detector positions).
287
+
288
+ Notes
289
+ -----
290
+ As the adjoint to the cone-beam forward projection, this operation
291
+ distributes sinogram values back into the 3D volume along ray paths using
292
+ atomic operations for thread-safe accumulation.
293
+ Supports arbitrary cone-beam geometries.
294
+ """
295
+ iv, iu, iview = cuda.grid(3)
296
+ if iview >= n_views or iu >= n_u or iv >= n_v:
297
+ return
298
+
299
+ # === 3D BACKPROJECTION VALUE AND GEOMETRY SETUP (ARBITRARY TRAJECTORY) ===
300
+ # Siddon segment lengths are in voxel units; voxel_spacing makes them physical lengths.
301
+ g = d_sino[iview, iu, iv] * voxel_spacing
302
+
303
+ # Read source position from position matrix (in physical units)
304
+ src_x = d_src_pos[iview, 0] / voxel_spacing
305
+ src_y = d_src_pos[iview, 1] / voxel_spacing
306
+ src_z = d_src_pos[iview, 2] / voxel_spacing
307
+
308
+ # Read detector center and orientation vectors
309
+ det_cx = d_det_center[iview, 0] / voxel_spacing
310
+ det_cy = d_det_center[iview, 1] / voxel_spacing
311
+ det_cz = d_det_center[iview, 2] / voxel_spacing
312
+
313
+ u_vec_x = d_det_u_vec[iview, 0]
314
+ u_vec_y = d_det_u_vec[iview, 1]
315
+ u_vec_z = d_det_u_vec[iview, 2]
316
+
317
+ v_vec_x = d_det_v_vec[iview, 0]
318
+ v_vec_y = d_det_v_vec[iview, 1]
319
+ v_vec_z = d_det_v_vec[iview, 2]
320
+
321
+ # Calculate detector element offset from center
322
+ u_offset = (np.float32(iu) + _HALF - np.float32(n_u) * _HALF) * du / voxel_spacing
323
+ v_offset = (np.float32(iv) + _HALF - np.float32(n_v) * _HALF) * dv / voxel_spacing
324
+
325
+ # Calculate 3D detector element position using center + u*u_vec + v*v_vec
326
+ det_x = det_cx + u_offset * u_vec_x + v_offset * v_vec_x
327
+ det_y = det_cy + u_offset * u_vec_y + v_offset * v_vec_y
328
+ det_z = det_cz + u_offset * u_vec_z + v_offset * v_vec_z
329
+
330
+ # === 3D RAY DIRECTION CALCULATION ===
331
+ # Ray direction vector from source to detector element in 3D space
332
+ dir_x, dir_y, dir_z = det_x - src_x, det_y - src_y, det_z - src_z
333
+ length = math.sqrt(dir_x*dir_x + dir_y*dir_y + dir_z*dir_z) # 3D ray length
334
+ if length < _EPSILON: return # Skip degenerate rays
335
+ inv_len = _ONE / length # Normalization factor for ray direction
336
+ dir_x, dir_y, dir_z = dir_x*inv_len, dir_y*inv_len, dir_z*inv_len # Normalized 3D ray direction vector
337
+
338
+ # === 3D RAY-VOLUME INTERSECTION CALCULATION ===
339
+ # Measure the ray parameter from the endpoint nearer the volume centre, so a
340
+ # distant source does not make the float32 parameters coarse. Only the
341
+ # source-to-detector segment is integrated.
342
+ if src_x * src_x + src_y * src_y + src_z * src_z <= det_x * det_x + det_y * det_y + det_z * det_z:
343
+ org_x = src_x
344
+ org_y = src_y
345
+ org_z = src_z
346
+ t_min, t_max = _ZERO, length
347
+ else:
348
+ org_x = det_x
349
+ org_y = det_y
350
+ org_z = det_z
351
+ t_min, t_max = -length, _ZERO
352
+
353
+ # X-direction boundary intersections
354
+ if abs(dir_x) > _TINY:
355
+ tx1, tx2 = (-cx - org_x) / dir_x, (cx - org_x) / dir_x
356
+ t_min, t_max = max(t_min, min(tx1, tx2)), min(t_max, max(tx1, tx2))
357
+ elif org_x < -cx or org_x > cx: return
358
+
359
+ # Y-direction boundary intersections
360
+ if abs(dir_y) > _TINY:
361
+ ty1, ty2 = (-cy - org_y) / dir_y, (cy - org_y) / dir_y
362
+ t_min, t_max = max(t_min, min(ty1, ty2)), min(t_max, max(ty1, ty2))
363
+ elif org_y < -cy or org_y > cy: return
364
+
365
+ # Z-direction boundary intersections (extends 2D algorithm to 3D)
366
+ if abs(dir_z) > _TINY:
367
+ tz1, tz2 = (-cz - org_z) / dir_z, (cz - org_z) / dir_z
368
+ t_min, t_max = max(t_min, min(tz1, tz2)), min(t_max, max(tz1, tz2))
369
+ elif org_z < -cz or org_z > cz: return
370
+
371
+ if t_min >= t_max: return
372
+
373
+ # The traversal restarts at the entry point, so its parameters stay
374
+ # within the volume diameter.
375
+ ent_x = org_x + t_min * dir_x
376
+ ent_y = org_y + t_min * dir_y
377
+ ent_z = org_z + t_min * dir_z
378
+ ray_x = dir_x
379
+ ray_y = dir_y
380
+ ray_z = dir_z
381
+ t_end = t_max - t_min
382
+
383
+ # === 3D SIDDON METHOD TRAVERSAL INITIALIZATION ===
384
+ t = _ZERO
385
+ ix = int(math.floor(ent_x + t * ray_x + cx)) # Current voxel x-index
386
+ iy = int(math.floor(ent_y + t * ray_y + cy)) # Current voxel y-index
387
+ iz = int(math.floor(ent_z + t * ray_z + cz)) # Current voxel z-index
388
+
389
+ # 3D traversal parameters (extends 2D algorithm)
390
+ step_x, step_y, step_z = (1 if ray_x >= 0 else -1), (1 if ray_y >= 0 else -1), (1 if ray_z >= 0 else -1)
391
+ inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
392
+ inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
393
+ inv_dir_z = (_ONE / ray_z) if abs(ray_z) > _TINY else _ZERO
394
+ dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG # Parameter increment per x-voxel
395
+ dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG # Parameter increment per y-voxel
396
+ dt_z = abs(inv_dir_z) if abs(ray_z) > _TINY else _BIG # Parameter increment per z-voxel
397
+
398
+ # Calculate parameter values for next 3D voxel boundary crossings
399
+ next_ix = ix + (1 if step_x > 0 else 0)
400
+ next_iy = iy + (1 if step_y > 0 else 0)
401
+ next_iz = iz + (1 if step_z > 0 else 0)
402
+ tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
403
+ ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
404
+ tz = (np.float32(next_iz) - cz - ent_z) * inv_dir_z if abs(ray_z) > _TINY else _BIG
405
+
406
+ # === 3D CONE BEAM BACKPROJECTION TRAVERSAL LOOP ===
407
+ # Adjoint of the cell-constant Siddon forward projection.
408
+ while t < t_end:
409
+ if 0 <= ix < Nx and 0 <= iy < Ny and 0 <= iz < Nz:
410
+ # Determine next 3D voxel boundary crossing (minimum of x, y, z boundaries or ray exit)
411
+ t_next = min(tx, ty, tz, t_end)
412
+ seg_len = t_next - t
413
+ if seg_len > _ZERO:
414
+ cuda.atomic.add(d_vol, (ix, iy, iz), g * seg_len)
415
+
416
+ # === 3D VOXEL BOUNDARY CROSSING LOGIC ===
417
+ # Advance to next voxel based on which boundary is crossed first in 3D
418
+ if tx <= ty and tx <= tz: # X-boundary crossed first
419
+ t = tx
420
+ ix += step_x
421
+ tx += dt_x
422
+ elif ty <= tx and ty <= tz: # Y-boundary crossed first
423
+ t = ty
424
+ iy += step_y
425
+ ty += dt_y
426
+ else: # Z-boundary crossed first
427
+ t = tz
428
+ iz += step_z
429
+ tz += dt_z
430
+
431
+
432
+ # ============================================================================
433
+ # 3D Cone Beam Analytical FDK Backprojection Gather (voxel-driven)
434
+ # ============================================================================
435
+ #
436
+ # Distinct from ``_cone_3d_backward_kernel`` above, which is the pure
437
+ # Siddon adjoint ``P^T`` used by autograd. This kernel is the classical
438
+ # voxel-driven FDK gather: for each output voxel it projects the voxel
439
+ # centre onto the detector for each view, bilinearly samples the
440
+ # already-filtered sinogram, and accumulates ``(sid/U_n)^2 * sample``.
441
+ # Both forward and adjoint autograd paths deliberately stay on the
442
+ # Siddon pair; only ``cone_weighted_backproject`` dispatches here.
443
+
444
+
445
+ @_FDK_ACCURACY_DECORATOR
446
+ def _cone_3d_fdk_backproject_kernel(
447
+ d_sino, n_views, n_u, n_v,
448
+ d_vol, Nx, Ny, Nz,
449
+ du, dv, d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
450
+ cx, cy, cz, voxel_spacing, iso_x, iso_y, iso_z
451
+ ):
452
+ """Voxel-driven FDK backprojection gather (arbitrary trajectory).
453
+
454
+ For each volume voxel, loops over all views and accumulates a
455
+ bilinearly-interpolated sinogram sample weighted by ``(sid_v/U_n)^2``,
456
+ where:
457
+
458
+ * ``sid_v = (isocenter - S_v) . n_v`` is the source-to-isocenter
459
+ distance along the detector normal,
460
+ * ``U_n = (P - S_v) . n_v`` is the signed distance from the source
461
+ to the voxel along the detector normal ``n_v = u_v x v_v``.
462
+
463
+ For a canonical circular orbit this reduces to the classical
464
+ ``(sid/U)^2`` FDK weight with ``U = sid + x*sin(b) - y*cos(b)``.
465
+
466
+ Coalescing layout: ``d_vol`` has WHD layout ``(Nx, Ny, Nz)`` so its
467
+ innermost stride-1 axis is ``Nz``. We therefore unpack
468
+ ``(iz, iy, ix) = cuda.grid(3)`` and have the Python wrapper launch
469
+ the grid as ``_grid_3d(Nz, Ny, Nx)`` so ``iz`` is warp-adjacent and
470
+ the per-voxel writes are coalesced.
471
+ """
472
+ iz, iy, ix = cuda.grid(3)
473
+ if ix >= Nx or iy >= Ny or iz >= Nz:
474
+ return
475
+ if n_u < 2 or n_v < 2:
476
+ d_vol[ix, iy, iz] = _ZERO
477
+ return
478
+
479
+ # Voxel position in voxel-unit, origin-centred coordinates.
480
+ # Sample at voxel centres, matching the cell-constant Siddon projector.
481
+ x_v = np.float32(ix) + _HALF - cx
482
+ y_v = np.float32(iy) + _HALF - cy
483
+ z_v = np.float32(iz) + _HALF - cz
484
+
485
+ du_v = du / voxel_spacing
486
+ dv_v = dv / voxel_spacing
487
+
488
+ half_u = (np.float32(n_u) - _ONE) * _HALF
489
+ half_v = (np.float32(n_v) - _ONE) * _HALF
490
+
491
+ accum = _ZERO
492
+ for iview in range(n_views):
493
+ # Source position in voxel units.
494
+ sx = d_src_pos[iview, 0] / voxel_spacing
495
+ sy = d_src_pos[iview, 1] / voxel_spacing
496
+ sz = d_src_pos[iview, 2] / voxel_spacing
497
+
498
+ # Detector centre in voxel units.
499
+ dcx = d_det_center[iview, 0] / voxel_spacing
500
+ dcy = d_det_center[iview, 1] / voxel_spacing
501
+ dcz = d_det_center[iview, 2] / voxel_spacing
502
+
503
+ # Detector orientation (unit vectors, ratio-free).
504
+ ux = d_det_u_vec[iview, 0]
505
+ uy = d_det_u_vec[iview, 1]
506
+ uz = d_det_u_vec[iview, 2]
507
+ vx = d_det_v_vec[iview, 0]
508
+ vy = d_det_v_vec[iview, 1]
509
+ vz = d_det_v_vec[iview, 2]
510
+
511
+ # Detector normal n = u x v. Normalized defensively in case the
512
+ # caller supplied non-orthonormal u/v axes.
513
+ nx = uy * vz - uz * vy
514
+ ny = uz * vx - ux * vz
515
+ nz = ux * vy - uy * vx
516
+ n_len = math.sqrt(nx * nx + ny * ny + nz * nz)
517
+ if n_len < _EPSILON:
518
+ continue
519
+ nx /= n_len
520
+ ny /= n_len
521
+ nz /= n_len
522
+
523
+ # Orient the normal so it points from the source toward the
524
+ # detector. Without this flip, user-supplied (u_vec, v_vec) pairs
525
+ # whose cross product happens to point the other way would make
526
+ # every voxel's U_n negative and get silently skipped, producing
527
+ # a zero reconstruction. The circular_trajectory_3d helper already
528
+ # produces the right-handed convention so this is a no-op there.
529
+ align = (dcx - sx) * nx + (dcy - sy) * ny + (dcz - sz) * nz
530
+ if align < _ZERO:
531
+ nx = -nx
532
+ ny = -ny
533
+ nz = -nz
534
+
535
+ # Signed distance from source to voxel along the detector normal
536
+ # (this is the U in the (sid/U)^2 FDK weight).
537
+ px = x_v - sx
538
+ py = y_v - sy
539
+ pz = z_v - sz
540
+ U_n = px * nx + py * ny + pz * nz
541
+ if U_n <= _EPSILON:
542
+ continue
543
+
544
+ # Source-to-detector distance along the normal.
545
+ sdd_n = (dcx - sx) * nx + (dcy - sy) * ny + (dcz - sz) * nz
546
+ if sdd_n <= _EPSILON:
547
+ continue
548
+
549
+ # Source-to-isocenter distance, independent of coordinate origin.
550
+ sid_n = (iso_x - sx) * nx + (iso_y - sy) * ny + (iso_z - sz) * nz
551
+ if sid_n <= _EPSILON:
552
+ continue
553
+
554
+ mag = sdd_n / U_n
555
+
556
+ # Hit point on the detector plane.
557
+ hx = sx + mag * px
558
+ hy = sy + mag * py
559
+ hz = sz + mag * pz
560
+
561
+ # Detector coordinates relative to detector centre.
562
+ rx = hx - dcx
563
+ ry = hy - dcy
564
+ rz = hz - dcz
565
+
566
+ u_det = rx * ux + ry * uy + rz * uz
567
+ v_det = rx * vx + ry * vy + rz * vz
568
+
569
+ # Convert (u, v) to fractional bin index.
570
+ fu = u_det / du_v + half_u
571
+ fv = v_det / dv_v + half_v
572
+
573
+ if fu < _ZERO or fu > (np.float32(n_u) - _ONE) or fv < _ZERO or fv > (np.float32(n_v) - _ONE):
574
+ continue
575
+
576
+ iu0 = int(math.floor(fu))
577
+ iv0 = int(math.floor(fv))
578
+ if iu0 >= n_u - 1:
579
+ iu0 = n_u - 2
580
+ if iv0 >= n_v - 1:
581
+ iv0 = n_v - 2
582
+ if iu0 < 0:
583
+ iu0 = 0
584
+ if iv0 < 0:
585
+ iv0 = 0
586
+ tu = fu - np.float32(iu0)
587
+ tv = fv - np.float32(iv0)
588
+ if tu < _ZERO:
589
+ tu = _ZERO
590
+ elif tu > _ONE:
591
+ tu = _ONE
592
+ if tv < _ZERO:
593
+ tv = _ZERO
594
+ elif tv > _ONE:
595
+ tv = _ONE
596
+
597
+ s00 = d_sino[iview, iu0, iv0 ]
598
+ s10 = d_sino[iview, iu0 + 1, iv0 ]
599
+ s01 = d_sino[iview, iu0, iv0 + 1]
600
+ s11 = d_sino[iview, iu0 + 1, iv0 + 1]
601
+
602
+ omtu = _ONE - tu
603
+ omtv = _ONE - tv
604
+ sample = (
605
+ s00 * omtu * omtv +
606
+ s10 * tu * omtv +
607
+ s01 * omtu * tv +
608
+ s11 * tu * tv
609
+ )
610
+
611
+ # FDK weight (sid_n / U_n)^2.
612
+ w_ratio = sid_n / U_n
613
+ w = w_ratio * w_ratio
614
+
615
+ accum += w * sample
616
+
617
+ d_vol[ix, iy, iz] = accum