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,451 @@
1
+ """CUDA kernels for 2D fan beam projections.
2
+
3
+ This module contains CUDA kernels implementing the Siddon ray-tracing method
4
+ for 2D fan beam forward projection and backprojection, plus a dedicated
5
+ voxel-driven FBP 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
+ # 2D Fan Beam Forward Projection Kernel
26
+ # ============================================================================
27
+
28
+ @_FASTMATH_DECORATOR
29
+ def _fan_2d_forward_kernel(
30
+ d_image, Nx, Ny,
31
+ d_sino, n_ang, n_det,
32
+ det_spacing, d_src_pos, d_det_center, d_det_u_vec,
33
+ cx, cy, voxel_spacing
34
+ ):
35
+ """Compute the 2D fan beam forward projection with arbitrary source-detector trajectories.
36
+
37
+ This CUDA kernel implements cell-constant Siddon ray tracing for
38
+ 2D fan beam forward projection. Supports arbitrary source and detector positions
39
+ for each view, enabling non-circular trajectories.
40
+
41
+ Parameters
42
+ ----------
43
+ d_image : numba.cuda.cudadrv.devicearray.DeviceNDArray
44
+ Input 2D image 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
+ d_sino : numba.cuda.cudadrv.devicearray.DeviceNDArray
50
+ Output fan beam sinogram array on CUDA.
51
+ n_ang : int
52
+ Number of projection angles.
53
+ n_det : int
54
+ Number of detector elements.
55
+ det_spacing : float
56
+ Physical spacing between detector elements.
57
+ d_src_pos : numba.cuda.cudadrv.devicearray.DeviceNDArray
58
+ Source positions for each view, shape (n_ang, 2), in physical units.
59
+ d_det_center : numba.cuda.cudadrv.devicearray.DeviceNDArray
60
+ Detector center positions for each view, shape (n_ang, 2), in physical units.
61
+ d_det_u_vec : numba.cuda.cudadrv.devicearray.DeviceNDArray
62
+ Detector u-direction unit vectors for each view, shape (n_ang, 2).
63
+ cx : float
64
+ Half of image width in voxels.
65
+ cy : float
66
+ Half of image height in voxels.
67
+ voxel_spacing : float
68
+ Physical size of one voxel (in same units as det_spacing).
69
+
70
+ Notes
71
+ -----
72
+ Supports arbitrary fan beam geometries by specifying source position,
73
+ detector center, and detector orientation vectors for each view.
74
+ Integrates each ray through piecewise-constant pixel cells.
75
+ """
76
+ iang, idet = cuda.grid(2)
77
+ if iang >= n_ang or idet >= n_det:
78
+ return
79
+
80
+ # === 2D FAN BEAM GEOMETRY SETUP (ARBITRARY TRAJECTORY) ===
81
+ # Read source position from position matrix (in physical units)
82
+ src_x = d_src_pos[iang, 0] / voxel_spacing
83
+ src_y = d_src_pos[iang, 1] / voxel_spacing
84
+
85
+ # Read detector center and orientation vector
86
+ det_cx = d_det_center[iang, 0] / voxel_spacing
87
+ det_cy = d_det_center[iang, 1] / voxel_spacing
88
+
89
+ u_vec_x = d_det_u_vec[iang, 0]
90
+ u_vec_y = d_det_u_vec[iang, 1]
91
+
92
+ # Calculate detector element offset from center
93
+ u_offset = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing / voxel_spacing
94
+
95
+ # Calculate 2D detector element position using center + u*u_vec
96
+ det_x = det_cx + u_offset * u_vec_x
97
+ det_y = det_cy + u_offset * u_vec_y
98
+
99
+ # === RAY DIRECTION CALCULATION ===
100
+ # Ray direction vector from source to detector element
101
+ dir_x, dir_y = det_x - src_x, det_y - src_y
102
+ length = math.sqrt(dir_x * dir_x + dir_y * dir_y) # Ray length
103
+ if length < _EPSILON: # Degenerate ray case
104
+ d_sino[iang, idet] = _ZERO; return
105
+
106
+ # Normalize ray direction vector for parametric traversal
107
+ inv_len = _ONE / length
108
+ dir_x, dir_y = dir_x * inv_len, dir_y * inv_len
109
+
110
+ # === RAY-VOLUME INTERSECTION CALCULATION ===
111
+ # Measure the ray parameter from the endpoint nearer the volume centre, so a
112
+ # distant source does not make the float32 parameters coarse. Only the
113
+ # source-to-detector segment is integrated.
114
+ if src_x * src_x + src_y * src_y <= det_x * det_x + det_y * det_y:
115
+ org_x = src_x
116
+ org_y = src_y
117
+ t_min, t_max = _ZERO, length
118
+ else:
119
+ org_x = det_x
120
+ org_y = det_y
121
+ t_min, t_max = -length, _ZERO
122
+ if abs(dir_x) > _TINY:
123
+ tx1, tx2 = (-cx - org_x) / dir_x, (cx - org_x) / dir_x # Volume boundary intersections
124
+ t_min, t_max = max(t_min, min(tx1, tx2)), min(t_max, max(tx1, tx2))
125
+ elif org_x < -cx or org_x > cx: # Ray origin outside volume bounds
126
+ d_sino[iang, idet] = _ZERO; return
127
+
128
+ if abs(dir_y) > _TINY:
129
+ ty1, ty2 = (-cy - org_y) / dir_y, (cy - org_y) / dir_y
130
+ t_min, t_max = max(t_min, min(ty1, ty2)), min(t_max, max(ty1, ty2))
131
+ elif org_y < -cy or org_y > cy:
132
+ d_sino[iang, idet] = _ZERO; return
133
+
134
+ if t_min >= t_max: # No valid intersection
135
+ d_sino[iang, idet] = _ZERO; return
136
+
137
+ # The traversal restarts at the entry point, so its parameters stay
138
+ # within the volume diameter.
139
+ ent_x = org_x + t_min * dir_x
140
+ ent_y = org_y + t_min * dir_y
141
+ ray_x = dir_x
142
+ ray_y = dir_y
143
+ t_end = t_max - t_min
144
+
145
+ # === SIDDON METHOD TRAVERSAL (same algorithm as parallel beam) ===
146
+ accum = _ZERO # Accumulated projection value
147
+ t = _ZERO
148
+
149
+ # Convert ray entry point to voxel indices
150
+ ix = int(math.floor(ent_x + t * ray_x + cx))
151
+ iy = int(math.floor(ent_y + t * ray_y + cy))
152
+
153
+ # Traversal parameters (identical to parallel beam implementation)
154
+ step_x, step_y = (1 if ray_x >= 0 else -1), (1 if ray_y >= 0 else -1)
155
+ inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
156
+ inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
157
+ dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG
158
+ dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG
159
+ next_ix = ix + (1 if step_x > 0 else 0)
160
+ next_iy = iy + (1 if step_y > 0 else 0)
161
+ tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
162
+ ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
163
+
164
+ # Main traversal loop with cell-constant Siddon integration.
165
+ while t < t_end:
166
+ if 0 <= ix < Nx and 0 <= iy < Ny:
167
+ t_next = min(tx, ty, t_end)
168
+ seg_len = t_next - t
169
+ if seg_len > _ZERO:
170
+ accum += d_image[iy, ix] * seg_len
171
+
172
+ # Voxel boundary crossing logic (identical to parallel beam)
173
+ if tx <= ty:
174
+ t = tx
175
+ ix += step_x
176
+ tx += dt_x
177
+ else:
178
+ t = ty
179
+ iy += step_y
180
+ ty += dt_y
181
+
182
+ # Siddon segment lengths are in voxel units; voxel_spacing makes them physical lengths.
183
+ d_sino[iang, idet] = accum * voxel_spacing
184
+
185
+ @_FASTMATH_DECORATOR
186
+
187
+ # ============================================================================
188
+ # 2D Fan Beam Backprojection Kernel
189
+ # ============================================================================
190
+
191
+ def _fan_2d_backward_kernel(
192
+ d_sino, n_ang, n_det,
193
+ d_image, Nx, Ny,
194
+ det_spacing, d_src_pos, d_det_center, d_det_u_vec,
195
+ cx, cy, voxel_spacing
196
+ ):
197
+ """Compute the 2D fan beam backprojection with arbitrary source-detector trajectories.
198
+
199
+ This CUDA kernel implements the adjoint of cell-constant Siddon ray tracing for
200
+ 2D fan beam backprojection. Supports arbitrary source and detector positions
201
+ for each view, enabling non-circular trajectories.
202
+
203
+ Parameters
204
+ ----------
205
+ d_sino : numba.cuda.cudadrv.devicearray.DeviceNDArray
206
+ Input fan beam sinogram array on CUDA.
207
+ n_ang : int
208
+ Number of projection angles.
209
+ n_det : int
210
+ Number of detector elements.
211
+ d_image : numba.cuda.cudadrv.devicearray.DeviceNDArray
212
+ Output image gradient array on CUDA.
213
+ Nx : int
214
+ Number of voxels along the x-axis.
215
+ Ny : int
216
+ Number of voxels along the y-axis.
217
+ det_spacing : float
218
+ Physical spacing between detector elements.
219
+ d_src_pos : numba.cuda.cudadrv.devicearray.DeviceNDArray
220
+ Source positions for each view, shape (n_ang, 2), in physical units.
221
+ d_det_center : numba.cuda.cudadrv.devicearray.DeviceNDArray
222
+ Detector center positions for each view, shape (n_ang, 2), in physical units.
223
+ d_det_u_vec : numba.cuda.cudadrv.devicearray.DeviceNDArray
224
+ Detector u-direction unit vectors for each view, shape (n_ang, 2).
225
+ cx : float
226
+ Half of image width in voxels.
227
+ cy : float
228
+ Half of image height in voxels.
229
+ voxel_spacing : float
230
+ Physical size of one voxel (in same units as det_spacing).
231
+
232
+ Notes
233
+ -----
234
+ As the adjoint to the fan beam forward projection, this operation
235
+ distributes sinogram values back into the volume along divergent ray
236
+ paths using atomic operations for thread-safe accumulation.
237
+ Supports arbitrary fan beam geometries.
238
+ """
239
+ iang, idet = cuda.grid(2)
240
+ if iang >= n_ang or idet >= n_det:
241
+ return
242
+
243
+ # === 2D BACKPROJECTION VALUE AND GEOMETRY SETUP (ARBITRARY TRAJECTORY) ===
244
+ # Siddon segment lengths are in voxel units; voxel_spacing makes them physical lengths.
245
+ val = d_sino[iang, idet] * voxel_spacing
246
+
247
+ # Read source position from position matrix (in physical units)
248
+ src_x = d_src_pos[iang, 0] / voxel_spacing
249
+ src_y = d_src_pos[iang, 1] / voxel_spacing
250
+
251
+ # Read detector center and orientation vector
252
+ det_cx = d_det_center[iang, 0] / voxel_spacing
253
+ det_cy = d_det_center[iang, 1] / voxel_spacing
254
+
255
+ u_vec_x = d_det_u_vec[iang, 0]
256
+ u_vec_y = d_det_u_vec[iang, 1]
257
+
258
+ # Calculate detector element offset from center
259
+ u_offset = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing / voxel_spacing
260
+
261
+ # Calculate 2D detector element position using center + u*u_vec
262
+ det_x = det_cx + u_offset * u_vec_x
263
+ det_y = det_cy + u_offset * u_vec_y
264
+
265
+ # === RAY DIRECTION CALCULATION ===
266
+ # Ray direction vector from source to detector element
267
+ dir_x, dir_y = det_x - src_x, det_y - src_y
268
+ length = math.sqrt(dir_x * dir_x + dir_y * dir_y) # Ray length
269
+ if length < _EPSILON: return # Skip degenerate rays
270
+ inv_len = _ONE / length # Normalization factor for ray direction
271
+ dir_x, dir_y = dir_x * inv_len, dir_y * inv_len # Normalized ray direction vector
272
+
273
+ # === RAY-VOLUME INTERSECTION CALCULATION ===
274
+ # Measure the ray parameter from the endpoint nearer the volume centre, so a
275
+ # distant source does not make the float32 parameters coarse. Only the
276
+ # source-to-detector segment is integrated.
277
+ if src_x * src_x + src_y * src_y <= det_x * det_x + det_y * det_y:
278
+ org_x = src_x
279
+ org_y = src_y
280
+ t_min, t_max = _ZERO, length
281
+ else:
282
+ org_x = det_x
283
+ org_y = det_y
284
+ t_min, t_max = -length, _ZERO
285
+ if abs(dir_x) > _TINY:
286
+ tx1, tx2 = (-cx - org_x) / dir_x, (cx - org_x) / dir_x
287
+ t_min, t_max = max(t_min, min(tx1, tx2)), min(t_max, max(tx1, tx2))
288
+ elif org_x < -cx or org_x > cx: return
289
+
290
+ if abs(dir_y) > _TINY:
291
+ ty1, ty2 = (-cy - org_y) / dir_y, (cy - org_y) / dir_y
292
+ t_min, t_max = max(t_min, min(ty1, ty2)), min(t_max, max(ty1, ty2))
293
+ elif org_y < -cy or org_y > cy: return
294
+
295
+ if t_min >= t_max: return
296
+
297
+ # The traversal restarts at the entry point, so its parameters stay
298
+ # within the volume diameter.
299
+ ent_x = org_x + t_min * dir_x
300
+ ent_y = org_y + t_min * dir_y
301
+ ray_x = dir_x
302
+ ray_y = dir_y
303
+ t_end = t_max - t_min
304
+
305
+ # === SIDDON METHOD TRAVERSAL INITIALIZATION ===
306
+ t = _ZERO
307
+ ix = int(math.floor(ent_x + t * ray_x + cx))
308
+ iy = int(math.floor(ent_y + t * ray_y + cy))
309
+
310
+ step_x, step_y = (1 if ray_x >= 0 else -1), (1 if ray_y >= 0 else -1)
311
+ inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
312
+ inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
313
+ dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG
314
+ dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG
315
+ next_ix = ix + (1 if step_x > 0 else 0)
316
+ next_iy = iy + (1 if step_y > 0 else 0)
317
+ tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
318
+ ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
319
+
320
+ # === FAN BEAM BACKPROJECTION TRAVERSAL LOOP ===
321
+ # Adjoint of the cell-constant Siddon forward projection.
322
+ while t < t_end:
323
+ if 0 <= ix < Nx and 0 <= iy < Ny:
324
+ t_next = min(tx, ty, t_end)
325
+ seg_len = t_next - t
326
+ if seg_len > _ZERO:
327
+ cuda.atomic.add(d_image, (iy, ix), val * seg_len)
328
+
329
+ # === VOXEL BOUNDARY CROSSING LOGIC ===
330
+ # Advance to next voxel based on which boundary is crossed first
331
+ if tx <= ty:
332
+ t = tx
333
+ ix += step_x
334
+ tx += dt_x
335
+ else:
336
+ t = ty
337
+ iy += step_y
338
+ ty += dt_y
339
+
340
+ # ============================================================================
341
+ # 2D Fan Beam Analytical FBP Backprojection Gather (voxel-driven)
342
+ # ============================================================================
343
+ #
344
+ # Distinct from ``_fan_2d_backward_kernel`` above, which is the pure
345
+ # Siddon adjoint ``P^T`` used by autograd. This kernel is the classical
346
+ # voxel-driven FBP gather: for each output pixel it projects the pixel
347
+ # centre onto the detector for each view, linearly samples the filtered
348
+ # sinogram, and accumulates ``(sid_v / U_n)^2 * sample``.
349
+
350
+
351
+ @_FDK_ACCURACY_DECORATOR
352
+ def _fan_2d_fbp_backproject_kernel(
353
+ d_sino, n_views, n_det,
354
+ d_image, Nx, Ny,
355
+ det_spacing, d_src_pos, d_det_center, d_det_u_vec,
356
+ cx, cy, voxel_spacing, iso_x, iso_y
357
+ ):
358
+ """Voxel-driven fan-beam FBP backprojection gather (arbitrary trajectory).
359
+
360
+ For each pixel ``(ix, iy)`` loops over views and accumulates a linearly
361
+ interpolated filtered-sinogram sample weighted by ``(sid_n/U_n)^2``,
362
+ where ``U_n`` is the signed distance from the source ``S_v`` to the
363
+ pixel along the detector normal.
364
+ """
365
+ ix, iy = cuda.grid(2)
366
+ if ix >= Nx or iy >= Ny:
367
+ return
368
+ if n_det < 2:
369
+ d_image[iy, ix] = _ZERO
370
+ return
371
+
372
+ # Sample at voxel centres, matching the cell-constant Siddon projector.
373
+ x_v = np.float32(ix) + _HALF - cx
374
+ y_v = np.float32(iy) + _HALF - cy
375
+
376
+ det_spacing_v = det_spacing / voxel_spacing
377
+ half_u = (np.float32(n_det) - _ONE) * _HALF
378
+
379
+ accum = _ZERO
380
+ for iview in range(n_views):
381
+ sx = d_src_pos[iview, 0] / voxel_spacing
382
+ sy = d_src_pos[iview, 1] / voxel_spacing
383
+
384
+ dcx = d_det_center[iview, 0] / voxel_spacing
385
+ dcy = d_det_center[iview, 1] / voxel_spacing
386
+
387
+ ux = d_det_u_vec[iview, 0]
388
+ uy = d_det_u_vec[iview, 1]
389
+
390
+ # Detector normal in 2D: rotate u_vec 90 degrees. Choose the
391
+ # orientation that points from the source toward the detector,
392
+ # so that ``U_n`` is positive for voxels on the detector side.
393
+ nx = -uy
394
+ ny = ux
395
+ # Align normal so that (det_center - src) . n > 0.
396
+ align = (dcx - sx) * nx + (dcy - sy) * ny
397
+ if align < _ZERO:
398
+ nx = -nx
399
+ ny = -ny
400
+
401
+ # Perpendicular distance from source to voxel along normal.
402
+ px = x_v - sx
403
+ py = y_v - sy
404
+ U_n = px * nx + py * ny
405
+ if U_n <= _EPSILON:
406
+ continue
407
+
408
+ sdd_n = (dcx - sx) * nx + (dcy - sy) * ny
409
+ if sdd_n <= _EPSILON:
410
+ continue
411
+ mag = sdd_n / U_n
412
+
413
+ # Source-to-isocenter distance, independent of coordinate origin.
414
+ sid_n = (iso_x - sx) * nx + (iso_y - sy) * ny
415
+ if sid_n <= _EPSILON:
416
+ continue
417
+
418
+ hx = sx + mag * px
419
+ hy = sy + mag * py
420
+
421
+ rx = hx - dcx
422
+ ry = hy - dcy
423
+ u_det = rx * ux + ry * uy
424
+
425
+ fu = u_det / det_spacing_v + half_u
426
+ if fu < _ZERO or fu > (np.float32(n_det) - _ONE):
427
+ continue
428
+
429
+ iu0 = int(math.floor(fu))
430
+ if iu0 >= n_det - 1:
431
+ iu0 = n_det - 2
432
+ if iu0 < 0:
433
+ iu0 = 0
434
+ tu = fu - np.float32(iu0)
435
+ if tu < _ZERO:
436
+ tu = _ZERO
437
+ elif tu > _ONE:
438
+ tu = _ONE
439
+
440
+ s0 = d_sino[iview, iu0]
441
+ s1 = d_sino[iview, iu0 + 1]
442
+ sample = s0 * (_ONE - tu) + s1 * tu
443
+
444
+ w_ratio = sid_n / U_n
445
+ w = w_ratio * w_ratio
446
+
447
+ accum += w * sample
448
+
449
+ d_image[iy, ix] = accum
450
+
451
+