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,400 @@
1
+ """CUDA kernels for 2D parallel beam projections.
2
+
3
+ This module contains CUDA kernels implementing the Siddon ray-tracing method
4
+ for 2D parallel beam forward projection and backprojection, plus a
5
+ dedicated voxel-driven FBP gather kernel for analytical reconstruction
6
+ pipelines.
7
+ """
8
+
9
+ import math
10
+ import numpy as np
11
+ from numba import cuda
12
+
13
+ from ..constants import (
14
+ _FASTMATH_DECORATOR,
15
+ _FDK_ACCURACY_DECORATOR,
16
+ _BIG,
17
+ _TINY,
18
+ _ZERO,
19
+ _ONE,
20
+ _HALF,
21
+ _EPSILON,
22
+ )
23
+
24
+
25
+ # ============================================================================
26
+ # 2D Parallel Beam Forward Projection Kernel
27
+ # ============================================================================
28
+
29
+ @_FASTMATH_DECORATOR
30
+ def _parallel_2d_forward_kernel(
31
+ d_image, Nx, Ny,
32
+ d_sino, n_ang, n_det,
33
+ det_spacing, d_ray_dir, d_det_origin, d_det_u_vec, cx, cy, voxel_spacing
34
+ ):
35
+ """Compute the 2D parallel beam forward projection with arbitrary ray trajectories.
36
+
37
+ This CUDA kernel implements cell-constant Siddon ray tracing for
38
+ 2D parallel beam forward projection. Supports arbitrary ray directions and detector
39
+ positions 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 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_ray_dir : numba.cuda.cudadrv.devicearray.DeviceNDArray
58
+ Ray direction unit vectors for each view, shape (n_ang, 2).
59
+ d_det_origin : numba.cuda.cudadrv.devicearray.DeviceNDArray
60
+ Detector origin 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 parallel beam geometries by specifying ray direction,
73
+ detector origin, 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 PARALLEL BEAM GEOMETRY SETUP (ARBITRARY TRAJECTORY) ===
81
+ # Read ray direction (parallel for all rays in this view)
82
+ dir_x = np.float64(d_ray_dir[iang, 0])
83
+ dir_y = np.float64(d_ray_dir[iang, 1])
84
+
85
+ # Read detector origin and orientation vector
86
+ det_ox = np.float64(d_det_origin[iang, 0]) / voxel_spacing
87
+ det_oy = np.float64(d_det_origin[iang, 1]) / voxel_spacing
88
+
89
+ u_vec_x = np.float64(d_det_u_vec[iang, 0])
90
+ u_vec_y = np.float64(d_det_u_vec[iang, 1])
91
+
92
+ # Calculate detector element offset from origin
93
+ u_offset = (np.float64(idet) + _HALF - np.float64(n_det) * _HALF) * det_spacing / voxel_spacing
94
+
95
+ # Ray starting point: detector origin + offset along u-direction
96
+ pnt_x = det_ox + u_offset * u_vec_x
97
+ pnt_y = det_oy + u_offset * u_vec_y
98
+
99
+ # === RAY-VOLUME INTERSECTION CALCULATION ===
100
+ # Compute parametric intersection points with volume boundaries using ray equation r(t) = pnt + t*dir
101
+ # Volume extends from [-cx, cx] x [-cy, cy] in voxel coordinate system
102
+ # Mathematical basis: For ray r(t) = origin + t*direction, solve r(t) = boundary for parameter t
103
+ t_min, t_max = -_BIG, _BIG # Initialize ray parameter range to unbounded
104
+
105
+ # X-direction boundary intersections
106
+ # Handle non-parallel rays: compute intersection parameters with left (-cx) and right (+cx) boundaries
107
+ if dir_x != 0.0: # Ray not parallel to x-axis (avoid division by zero)
108
+ tx1, tx2 = (-cx - pnt_x) / dir_x, (cx - pnt_x) / dir_x # Left and right boundary intersections
109
+ # Update valid parameter range: intersection of current range with x-boundary constraints
110
+ # min/max operations ensure we get the entry/exit points correctly regardless of ray direction
111
+ t_min, t_max = max(t_min, min(tx1, tx2)), min(t_max, max(tx1, tx2)) # Update valid parameter range
112
+ elif pnt_x < -cx or pnt_x > cx: # Ray parallel to x-axis but outside volume bounds
113
+ # Edge case: ray never intersects volume if parallel and outside boundaries
114
+ d_sino[iang, idet] = _ZERO; return
115
+
116
+ # Y-direction boundary intersections (identical logic to x-direction)
117
+ # Handle non-parallel rays: compute intersection parameters with bottom (-cy) and top (+cy) boundaries
118
+ if dir_y != 0.0: # Ray not parallel to y-axis (avoid division by zero)
119
+ ty1, ty2 = (-cy - pnt_y) / dir_y, (cy - pnt_y) / dir_y # Bottom and top boundary intersections
120
+ # Intersect y-boundary constraints with existing parameter range from x-boundaries
121
+ t_min, t_max = max(t_min, min(ty1, ty2)), min(t_max, max(ty1, ty2)) # Intersect with x-range
122
+ elif pnt_y < -cy or pnt_y > cy: # Ray parallel to y-axis but outside volume bounds
123
+ # Edge case: ray never intersects volume if parallel and outside boundaries
124
+ d_sino[iang, idet] = _ZERO; return
125
+
126
+ # Boundary intersection validation: check if ray actually intersects the volume
127
+ # If t_min >= t_max, the ray misses the volume entirely (no valid intersection interval)
128
+ if t_min >= t_max:
129
+ d_sino[iang, idet] = _ZERO; return
130
+
131
+ # The setup above runs in float64 because a distant source makes float32
132
+ # ray parameters too coarse, and only an exactly zero direction component
133
+ # is treated as axis-parallel there. The float32 traversal restarts at the
134
+ # entry point, so its parameters stay within the volume diameter.
135
+ ent_x = np.float32(pnt_x + t_min * dir_x)
136
+ ent_y = np.float32(pnt_y + t_min * dir_y)
137
+ ray_x = np.float32(dir_x)
138
+ ray_y = np.float32(dir_y)
139
+ t_end = np.float32(t_max - t_min)
140
+
141
+ # === SIDDON METHOD VOXEL TRAVERSAL INITIALIZATION ===
142
+ accum = _ZERO # Accumulated projection value along ray
143
+ t = _ZERO
144
+
145
+ # Convert ray entry point to voxel indices (image coordinate system)
146
+ ix = int(math.floor(ent_x + t * ray_x + cx)) # Current voxel x-index
147
+ iy = int(math.floor(ent_y + t * ray_y + cy)) # Current voxel y-index
148
+
149
+ # Determine traversal direction and step sizes for each axis
150
+ step_x, step_y = (1 if ray_x >= 0 else -1), (1 if ray_y >= 0 else -1) # Voxel stepping direction
151
+ # Hoist inverse directions to reduce divisions and branches
152
+ inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
153
+ inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
154
+ dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG
155
+ dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG
156
+
157
+ # Calculate parameter values for next voxel boundary crossings using inv_dir_*
158
+ next_ix = ix + (1 if step_x > 0 else 0)
159
+ next_iy = iy + (1 if step_y > 0 else 0)
160
+ tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
161
+ ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
162
+
163
+ # === MAIN RAY TRAVERSAL LOOP ===
164
+ # Step through voxels along ray path, accumulating cell-constant contributions.
165
+ while t < t_end:
166
+ if 0 <= ix < Nx and 0 <= iy < Ny:
167
+ # Determine next voxel boundary crossing (minimum of x, y boundaries or ray exit)
168
+ t_next = min(tx, ty, t_end)
169
+ seg_len = t_next - t # Length of ray segment within current voxel region
170
+ if seg_len > _ZERO:
171
+ accum += d_image[iy, ix] * seg_len
172
+
173
+ # === VOXEL BOUNDARY CROSSING LOGIC ===
174
+ # Advance to next voxel based on which boundary is crossed first
175
+ if tx <= ty: # X-boundary crossed first
176
+ t = tx
177
+ ix += step_x # Move to next voxel in x-direction
178
+ tx += dt_x # Update next x-boundary crossing parameter
179
+ else: # Y-boundary crossed first
180
+ t = ty
181
+ iy += step_y # Move to next voxel in y-direction
182
+ ty += dt_y # Update next y-boundary crossing parameter
183
+
184
+ # Siddon segment lengths are in voxel units; voxel_spacing makes them physical lengths.
185
+ d_sino[iang, idet] = accum * voxel_spacing
186
+
187
+
188
+ # ============================================================================
189
+ # 2D Parallel Beam Backprojection Kernel
190
+ # ============================================================================
191
+
192
+ @_FASTMATH_DECORATOR
193
+ def _parallel_2d_backward_kernel(
194
+ d_sino, n_ang, n_det,
195
+ d_image, Nx, Ny,
196
+ det_spacing, d_ray_dir, d_det_origin, d_det_u_vec, cx, cy, voxel_spacing
197
+ ):
198
+ """Compute the 2D parallel beam backprojection with arbitrary ray trajectories.
199
+
200
+ This CUDA kernel implements the adjoint of cell-constant Siddon ray tracing for
201
+ 2D parallel beam backprojection. Supports arbitrary ray directions and detector
202
+ positions for each view, enabling non-circular trajectories.
203
+
204
+ Parameters
205
+ ----------
206
+ d_sino : numba.cuda.cudadrv.devicearray.DeviceNDArray
207
+ Input sinogram array on CUDA.
208
+ n_ang : int
209
+ Number of projection angles.
210
+ n_det : int
211
+ Number of detector elements.
212
+ d_image : numba.cuda.cudadrv.devicearray.DeviceNDArray
213
+ Output image gradient array on CUDA.
214
+ Nx : int
215
+ Number of voxels along the x-axis.
216
+ Ny : int
217
+ Number of voxels along the y-axis.
218
+ det_spacing : float
219
+ Physical spacing between detector elements.
220
+ d_ray_dir : numba.cuda.cudadrv.devicearray.DeviceNDArray
221
+ Ray direction unit vectors for each view, shape (n_ang, 2).
222
+ d_det_origin : numba.cuda.cudadrv.devicearray.DeviceNDArray
223
+ Detector origin positions for each view, shape (n_ang, 2), in physical units.
224
+ d_det_u_vec : numba.cuda.cudadrv.devicearray.DeviceNDArray
225
+ Detector u-direction unit vectors for each view, shape (n_ang, 2).
226
+ cx : float
227
+ Half of image width in voxels.
228
+ cy : float
229
+ Half of image height in voxels.
230
+ voxel_spacing : float
231
+ Physical size of one voxel (in same units as det_spacing).
232
+
233
+ Notes
234
+ -----
235
+ This operation is the adjoint of the forward projection. Sinogram values
236
+ are distributed back into the volume along identical ray paths using
237
+ atomic operations to ensure thread-safe accumulation.
238
+ Supports arbitrary parallel beam geometries.
239
+ """
240
+ iang, idet = cuda.grid(2)
241
+ if iang >= n_ang or idet >= n_det:
242
+ return
243
+
244
+ # === 2D BACKPROJECTION VALUE AND GEOMETRY SETUP (ARBITRARY TRAJECTORY) ===
245
+ # Siddon segment lengths are in voxel units; voxel_spacing makes them physical lengths.
246
+ val = d_sino[iang, idet] * voxel_spacing
247
+
248
+ # Read ray direction (parallel for all rays in this view)
249
+ dir_x = np.float64(d_ray_dir[iang, 0])
250
+ dir_y = np.float64(d_ray_dir[iang, 1])
251
+
252
+ # Read detector origin and orientation vector
253
+ det_ox = np.float64(d_det_origin[iang, 0]) / voxel_spacing
254
+ det_oy = np.float64(d_det_origin[iang, 1]) / voxel_spacing
255
+
256
+ u_vec_x = np.float64(d_det_u_vec[iang, 0])
257
+ u_vec_y = np.float64(d_det_u_vec[iang, 1])
258
+
259
+ # Calculate detector element offset from origin
260
+ u_offset = (np.float64(idet) + _HALF - np.float64(n_det) * _HALF) * det_spacing / voxel_spacing
261
+
262
+ # Ray starting point: detector origin + offset along u-direction
263
+ pnt_x = det_ox + u_offset * u_vec_x
264
+ pnt_y = det_oy + u_offset * u_vec_y
265
+
266
+ # === RAY-VOLUME INTERSECTION CALCULATION (identical to forward) ===
267
+ t_min, t_max = -_BIG, _BIG
268
+ if dir_x != 0.0:
269
+ tx1, tx2 = (-cx - pnt_x) / dir_x, (cx - pnt_x) / dir_x
270
+ t_min, t_max = max(t_min, min(tx1, tx2)), min(t_max, max(tx1, tx2))
271
+ elif pnt_x < -cx or pnt_x > cx: return
272
+
273
+ if dir_y != 0.0:
274
+ ty1, ty2 = (-cy - pnt_y) / dir_y, (cy - pnt_y) / dir_y
275
+ t_min, t_max = max(t_min, min(ty1, ty2)), min(t_max, max(ty1, ty2))
276
+ elif pnt_y < -cy or pnt_y > cy: return
277
+
278
+ if t_min >= t_max: return
279
+
280
+ # The setup above runs in float64 because a distant source makes float32
281
+ # ray parameters too coarse, and only an exactly zero direction component
282
+ # is treated as axis-parallel there. The float32 traversal restarts at the
283
+ # entry point, so its parameters stay within the volume diameter.
284
+ ent_x = np.float32(pnt_x + t_min * dir_x)
285
+ ent_y = np.float32(pnt_y + t_min * dir_y)
286
+ ray_x = np.float32(dir_x)
287
+ ray_y = np.float32(dir_y)
288
+ t_end = np.float32(t_max - t_min)
289
+
290
+ # === SIDDON METHOD TRAVERSAL INITIALIZATION ===
291
+ t = _ZERO
292
+ ix = int(math.floor(ent_x + t * ray_x + cx))
293
+ iy = int(math.floor(ent_y + t * ray_y + cy))
294
+
295
+ step_x, step_y = (1 if ray_x >= 0 else -1), (1 if ray_y >= 0 else -1)
296
+ inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
297
+ inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
298
+ dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG
299
+ dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG
300
+ next_ix = ix + (1 if step_x > 0 else 0)
301
+ next_iy = iy + (1 if step_y > 0 else 0)
302
+ tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
303
+ ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
304
+
305
+ # === BACKPROJECTION TRAVERSAL LOOP ===
306
+ # Adjoint of the cell-constant Siddon forward projection.
307
+ while t < t_end:
308
+ if 0 <= ix < Nx and 0 <= iy < Ny:
309
+ t_next = min(tx, ty, t_end)
310
+ seg_len = t_next - t
311
+ if seg_len > _ZERO:
312
+ cuda.atomic.add(d_image, (iy, ix), val * seg_len)
313
+
314
+ # Advance to next voxel (identical logic to forward projection)
315
+ if tx <= ty:
316
+ t = tx
317
+ ix += step_x
318
+ tx += dt_x
319
+ else:
320
+ t = ty
321
+ iy += step_y
322
+ ty += dt_y
323
+
324
+
325
+
326
+ # ============================================================================
327
+ # 2D Parallel Beam Analytical FBP Backprojection Gather (voxel-driven)
328
+ # ============================================================================
329
+ #
330
+ # Distinct from ``_parallel_2d_backward_kernel`` above, which is the
331
+ # pure Siddon adjoint ``P^T`` used by autograd. This kernel is the
332
+ # classical voxel-driven FBP gather: for each output pixel it projects
333
+ # the pixel centre onto the detector for each view, linearly samples
334
+ # the filtered sinogram, and accumulates. Parallel beam has no source,
335
+ # so there is no ``(sid/U)^2`` distance weighting.
336
+
337
+
338
+ @_FDK_ACCURACY_DECORATOR
339
+ def _parallel_2d_fbp_backproject_kernel(
340
+ d_sino, n_views, n_det,
341
+ d_image, Nx, Ny,
342
+ det_spacing, d_ray_dir, d_det_origin, d_det_u_vec,
343
+ cx, cy, voxel_spacing
344
+ ):
345
+ """Voxel-driven parallel-beam FBP backprojection gather (arbitrary trajectory).
346
+
347
+ For each output pixel ``(ix, iy)`` loops over views and accumulates
348
+ a linearly interpolated filtered-sinogram sample. The pixel's
349
+ detector coordinate is ``u = (P - det_origin) . det_u_vec`` (no
350
+ magnification because the rays are parallel).
351
+ """
352
+ ix, iy = cuda.grid(2)
353
+ if ix >= Nx or iy >= Ny:
354
+ return
355
+ if n_det < 2:
356
+ d_image[iy, ix] = _ZERO
357
+ return
358
+
359
+ # Sample at voxel centres, matching the cell-constant Siddon projector.
360
+ x_v = np.float32(ix) + _HALF - cx
361
+ y_v = np.float32(iy) + _HALF - cy
362
+
363
+ det_spacing_v = det_spacing / voxel_spacing
364
+ half_u = (np.float32(n_det) - _ONE) * _HALF
365
+
366
+ accum = _ZERO
367
+ for iview in range(n_views):
368
+ dox = d_det_origin[iview, 0] / voxel_spacing
369
+ doy = d_det_origin[iview, 1] / voxel_spacing
370
+
371
+ ux = d_det_u_vec[iview, 0]
372
+ uy = d_det_u_vec[iview, 1]
373
+
374
+ rx = x_v - dox
375
+ ry = y_v - doy
376
+
377
+ u_det = rx * ux + ry * uy
378
+
379
+ fu = u_det / det_spacing_v + half_u
380
+ if fu < _ZERO or fu > (np.float32(n_det) - _ONE):
381
+ continue
382
+
383
+ iu0 = int(math.floor(fu))
384
+ if iu0 >= n_det - 1:
385
+ iu0 = n_det - 2
386
+ if iu0 < 0:
387
+ iu0 = 0
388
+ tu = fu - np.float32(iu0)
389
+ if tu < _ZERO:
390
+ tu = _ZERO
391
+ elif tu > _ONE:
392
+ tu = _ONE
393
+
394
+ s0 = d_sino[iview, iu0]
395
+ s1 = d_sino[iview, iu0 + 1]
396
+ sample = s0 * (_ONE - tu) + s1 * tu
397
+
398
+ accum += sample
399
+
400
+ d_image[iy, ix] = accum