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,562 @@
1
+ """CUDA kernels for gradients of the Siddon projector with respect to geometry.
2
+
3
+ For a cell-constant image the Siddon projection of one ray is
4
+
5
+ p = s * |D| * sum_k f_k (tau_{k+1} - tau_k), D = B - A,
6
+
7
+ where A is the source (or, for parallel beams, the detector pixel), B the
8
+ detector pixel, ``s`` the voxel spacing and ``tau_k`` the fractions along
9
+ ``D`` at which the ray enters and leaves cell ``k``. Collecting terms gives
10
+ ``sum_j c_j tau_j`` with ``c_j = f_before - f_after`` at each crossed cell
11
+ face (``-f_first`` at an entry face, ``+f_last`` at an exit face). A face
12
+ ``x_a = b`` gives ``tau = (b - A_a) / D_a``, so
13
+
14
+ d tau / d A_a = (tau - 1) / D_a, d tau / d B_a = -tau / D_a.
15
+
16
+ Segment ends at the source or the detector do not move a face crossing and
17
+ contribute nothing. The derivative is exact for the discrete model wherever
18
+ the ray does not pass through a cell edge or corner. On an edge, where two
19
+ crossings coincide, the kernels return the derivative of one adjacent side.
20
+
21
+ Each thread traces one ray with the same setup and traversal as the
22
+ projector kernels and adds ``grad_sino * dp/dgeometry`` to per-view
23
+ gradients with atomic additions.
24
+ """
25
+
26
+ import math
27
+ import numpy as np
28
+ from numba import cuda
29
+
30
+ from ..constants import (
31
+ _FASTMATH_DECORATOR,
32
+ _BIG,
33
+ _TINY,
34
+ _ZERO,
35
+ _ONE,
36
+ _HALF,
37
+ _EPSILON,
38
+ )
39
+
40
+
41
+ @cuda.jit(device=True)
42
+ def _face_terms(c, t_org, t_a0, t_b0, length, d):
43
+ """Return (d/dA_a, d/dB_a) of ``c * tau`` for a face crossed at ``t_org``.
44
+
45
+ ``t_org`` is the ray parameter of the crossing; ``t_a0`` and ``t_b0`` are
46
+ the parameters of A and B on the same unit-speed ray, ``d`` is the unit
47
+ direction component normal to the face.
48
+ """
49
+ tau_minus_one = (t_org - t_b0) / length
50
+ tau = (t_org - t_a0) / length
51
+ return c * tau_minus_one / d, -c * tau / d
52
+
53
+
54
+ @_FASTMATH_DECORATOR
55
+ def _fan_2d_geometry_vjp_kernel(
56
+ d_image, Nx, Ny,
57
+ d_grad_sino, n_ang, n_det,
58
+ det_spacing, d_src_pos, d_det_center, d_det_u_vec,
59
+ cx, cy, voxel_spacing,
60
+ d_grad_src, d_grad_det_center, d_grad_det_u,
61
+ ):
62
+ """Accumulate fan-beam geometry gradients of ``<grad_sino, A x>`` per view."""
63
+ iang, idet = cuda.grid(2)
64
+ if iang >= n_ang or idet >= n_det:
65
+ return
66
+ g = d_grad_sino[iang, idet]
67
+ if g == _ZERO:
68
+ return
69
+
70
+ # Ray setup: identical to _fan_2d_forward_kernel, plus the face that sets
71
+ # the entry and the exit (-1 = segment end at the source or detector).
72
+ src_x = d_src_pos[iang, 0] / voxel_spacing
73
+ src_y = d_src_pos[iang, 1] / voxel_spacing
74
+ det_cx = d_det_center[iang, 0] / voxel_spacing
75
+ det_cy = d_det_center[iang, 1] / voxel_spacing
76
+ u_vec_x = d_det_u_vec[iang, 0]
77
+ u_vec_y = d_det_u_vec[iang, 1]
78
+ u_phys = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing
79
+ u_offset = u_phys / voxel_spacing
80
+ det_x = det_cx + u_offset * u_vec_x
81
+ det_y = det_cy + u_offset * u_vec_y
82
+
83
+ dir_x, dir_y = det_x - src_x, det_y - src_y
84
+ length = math.sqrt(dir_x * dir_x + dir_y * dir_y)
85
+ if length < _EPSILON:
86
+ return
87
+ inv_len = _ONE / length
88
+ dir_x, dir_y = dir_x * inv_len, dir_y * inv_len
89
+
90
+ if src_x * src_x + src_y * src_y <= det_x * det_x + det_y * det_y:
91
+ org_x = src_x
92
+ org_y = src_y
93
+ t_min, t_max = _ZERO, length
94
+ t_a0, t_b0 = _ZERO, length
95
+ else:
96
+ org_x = det_x
97
+ org_y = det_y
98
+ t_min, t_max = -length, _ZERO
99
+ t_a0, t_b0 = -length, _ZERO
100
+ ent_axis = -1
101
+ ex_axis = -1
102
+ if abs(dir_x) > _TINY:
103
+ tx1, tx2 = (-cx - org_x) / dir_x, (cx - org_x) / dir_x
104
+ lo, hi = min(tx1, tx2), max(tx1, tx2)
105
+ if lo > t_min:
106
+ t_min = lo
107
+ ent_axis = 0
108
+ if hi < t_max:
109
+ t_max = hi
110
+ ex_axis = 0
111
+ elif org_x < -cx or org_x > cx:
112
+ return
113
+ if abs(dir_y) > _TINY:
114
+ ty1, ty2 = (-cy - org_y) / dir_y, (cy - org_y) / dir_y
115
+ lo, hi = min(ty1, ty2), max(ty1, ty2)
116
+ if lo > t_min:
117
+ t_min = lo
118
+ ent_axis = 1
119
+ if hi < t_max:
120
+ t_max = hi
121
+ ex_axis = 1
122
+ elif org_y < -cy or org_y > cy:
123
+ return
124
+ if t_min >= t_max:
125
+ return
126
+
127
+ ent_x = org_x + t_min * dir_x
128
+ ent_y = org_y + t_min * dir_y
129
+ ray_x = dir_x
130
+ ray_y = dir_y
131
+ t_end = t_max - t_min
132
+
133
+ accum = _ZERO
134
+ t = _ZERO
135
+ ix = int(math.floor(ent_x + cx))
136
+ iy = int(math.floor(ent_y + cy))
137
+ step_x, step_y = (1 if ray_x >= 0 else -1), (1 if ray_y >= 0 else -1)
138
+ inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
139
+ inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
140
+ dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG
141
+ dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG
142
+ next_ix = ix + (1 if step_x > 0 else 0)
143
+ next_iy = iy + (1 if step_y > 0 else 0)
144
+ tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
145
+ ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
146
+
147
+ ga_x = _ZERO
148
+ ga_y = _ZERO
149
+ gb_x = _ZERO
150
+ gb_y = _ZERO
151
+ f_prev = _ZERO
152
+ pend_axis = ent_axis
153
+ pend_t = _ZERO
154
+ first = True
155
+ while t < t_end:
156
+ f = _ZERO
157
+ if 0 <= ix < Nx and 0 <= iy < Ny:
158
+ f = d_image[iy, ix]
159
+ t_next = min(tx, ty, t_end)
160
+ seg_len = t_next - t
161
+ # The face crossed just before this cell: entry face or interior face.
162
+ if pend_axis >= 0:
163
+ c = (-f) if first else (f_prev - f)
164
+ if pend_axis == 0:
165
+ da, db = _face_terms(c, pend_t + t_min, t_a0, t_b0, length, ray_x)
166
+ ga_x += da
167
+ gb_x += db
168
+ else:
169
+ da, db = _face_terms(c, pend_t + t_min, t_a0, t_b0, length, ray_y)
170
+ ga_y += da
171
+ gb_y += db
172
+ first = False
173
+ if seg_len > _ZERO:
174
+ accum += f * seg_len
175
+ f_prev = f
176
+ if tx <= ty:
177
+ t = tx
178
+ pend_t = tx
179
+ pend_axis = 0
180
+ ix += step_x
181
+ tx += dt_x
182
+ else:
183
+ t = ty
184
+ pend_t = ty
185
+ pend_axis = 1
186
+ iy += step_y
187
+ ty += dt_y
188
+ if ex_axis == 0:
189
+ da, db = _face_terms(f_prev, t_max, t_a0, t_b0, length, ray_x)
190
+ ga_x += da
191
+ gb_x += db
192
+ elif ex_axis == 1:
193
+ da, db = _face_terms(f_prev, t_max, t_a0, t_b0, length, ray_y)
194
+ ga_y += da
195
+ gb_y += db
196
+
197
+ # Length term: p = s * |D| * S with S = accum / |D| (voxel units).
198
+ S = accum / length
199
+ ga_x -= S * ray_x
200
+ ga_y -= S * ray_y
201
+ gb_x += S * ray_x
202
+ gb_y += S * ray_y
203
+
204
+ cuda.atomic.add(d_grad_src, (iang, 0), g * ga_x)
205
+ cuda.atomic.add(d_grad_src, (iang, 1), g * ga_y)
206
+ cuda.atomic.add(d_grad_det_center, (iang, 0), g * gb_x)
207
+ cuda.atomic.add(d_grad_det_center, (iang, 1), g * gb_y)
208
+ cuda.atomic.add(d_grad_det_u, (iang, 0), g * u_phys * gb_x)
209
+ cuda.atomic.add(d_grad_det_u, (iang, 1), g * u_phys * gb_y)
210
+
211
+
212
+ @_FASTMATH_DECORATOR
213
+ def _cone_3d_geometry_vjp_kernel(
214
+ d_vol, Nx, Ny, Nz,
215
+ d_grad_sino, n_views, n_u, n_v,
216
+ du, dv, d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
217
+ cx, cy, cz, voxel_spacing,
218
+ d_grad_src, d_grad_det_center, d_grad_det_u, d_grad_det_v,
219
+ ):
220
+ """Accumulate cone-beam geometry gradients of ``<grad_sino, A x>`` per view."""
221
+ iv, iu, iview = cuda.grid(3)
222
+ if iview >= n_views or iu >= n_u or iv >= n_v:
223
+ return
224
+ g = d_grad_sino[iview, iu, iv]
225
+ if g == _ZERO:
226
+ return
227
+
228
+ src_x = d_src_pos[iview, 0] / voxel_spacing
229
+ src_y = d_src_pos[iview, 1] / voxel_spacing
230
+ src_z = d_src_pos[iview, 2] / voxel_spacing
231
+ det_cx = d_det_center[iview, 0] / voxel_spacing
232
+ det_cy = d_det_center[iview, 1] / voxel_spacing
233
+ det_cz = d_det_center[iview, 2] / voxel_spacing
234
+ u_vec_x = d_det_u_vec[iview, 0]
235
+ u_vec_y = d_det_u_vec[iview, 1]
236
+ u_vec_z = d_det_u_vec[iview, 2]
237
+ v_vec_x = d_det_v_vec[iview, 0]
238
+ v_vec_y = d_det_v_vec[iview, 1]
239
+ v_vec_z = d_det_v_vec[iview, 2]
240
+ u_phys = (np.float32(iu) + _HALF - np.float32(n_u) * _HALF) * du
241
+ v_phys = (np.float32(iv) + _HALF - np.float32(n_v) * _HALF) * dv
242
+ u_offset = u_phys / voxel_spacing
243
+ v_offset = v_phys / voxel_spacing
244
+ det_x = det_cx + u_offset * u_vec_x + v_offset * v_vec_x
245
+ det_y = det_cy + u_offset * u_vec_y + v_offset * v_vec_y
246
+ det_z = det_cz + u_offset * u_vec_z + v_offset * v_vec_z
247
+
248
+ dir_x, dir_y, dir_z = det_x - src_x, det_y - src_y, det_z - src_z
249
+ length = math.sqrt(dir_x * dir_x + dir_y * dir_y + dir_z * dir_z)
250
+ if length < _EPSILON:
251
+ return
252
+ inv_len = _ONE / length
253
+ dir_x, dir_y, dir_z = dir_x * inv_len, dir_y * inv_len, dir_z * inv_len
254
+
255
+ if (src_x * src_x + src_y * src_y + src_z * src_z
256
+ <= det_x * det_x + det_y * det_y + det_z * det_z):
257
+ org_x = src_x
258
+ org_y = src_y
259
+ org_z = src_z
260
+ t_min, t_max = _ZERO, length
261
+ t_a0, t_b0 = _ZERO, length
262
+ else:
263
+ org_x = det_x
264
+ org_y = det_y
265
+ org_z = det_z
266
+ t_min, t_max = -length, _ZERO
267
+ t_a0, t_b0 = -length, _ZERO
268
+ ent_axis = -1
269
+ ex_axis = -1
270
+ if abs(dir_x) > _TINY:
271
+ tx1, tx2 = (-cx - org_x) / dir_x, (cx - org_x) / dir_x
272
+ lo, hi = min(tx1, tx2), max(tx1, tx2)
273
+ if lo > t_min:
274
+ t_min = lo
275
+ ent_axis = 0
276
+ if hi < t_max:
277
+ t_max = hi
278
+ ex_axis = 0
279
+ elif org_x < -cx or org_x > cx:
280
+ return
281
+ if abs(dir_y) > _TINY:
282
+ ty1, ty2 = (-cy - org_y) / dir_y, (cy - org_y) / dir_y
283
+ lo, hi = min(ty1, ty2), max(ty1, ty2)
284
+ if lo > t_min:
285
+ t_min = lo
286
+ ent_axis = 1
287
+ if hi < t_max:
288
+ t_max = hi
289
+ ex_axis = 1
290
+ elif org_y < -cy or org_y > cy:
291
+ return
292
+ if abs(dir_z) > _TINY:
293
+ tz1, tz2 = (-cz - org_z) / dir_z, (cz - org_z) / dir_z
294
+ lo, hi = min(tz1, tz2), max(tz1, tz2)
295
+ if lo > t_min:
296
+ t_min = lo
297
+ ent_axis = 2
298
+ if hi < t_max:
299
+ t_max = hi
300
+ ex_axis = 2
301
+ elif org_z < -cz or org_z > cz:
302
+ return
303
+ if t_min >= t_max:
304
+ return
305
+
306
+ ent_x = org_x + t_min * dir_x
307
+ ent_y = org_y + t_min * dir_y
308
+ ent_z = org_z + t_min * dir_z
309
+ ray_x = dir_x
310
+ ray_y = dir_y
311
+ ray_z = dir_z
312
+ t_end = t_max - t_min
313
+
314
+ accum = _ZERO
315
+ t = _ZERO
316
+ ix = int(math.floor(ent_x + cx))
317
+ iy = int(math.floor(ent_y + cy))
318
+ iz = int(math.floor(ent_z + cz))
319
+ 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)
320
+ inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
321
+ inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
322
+ inv_dir_z = (_ONE / ray_z) if abs(ray_z) > _TINY else _ZERO
323
+ dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG
324
+ dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG
325
+ dt_z = abs(inv_dir_z) if abs(ray_z) > _TINY else _BIG
326
+ next_ix = ix + (1 if step_x > 0 else 0)
327
+ next_iy = iy + (1 if step_y > 0 else 0)
328
+ next_iz = iz + (1 if step_z > 0 else 0)
329
+ tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
330
+ ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
331
+ tz = (np.float32(next_iz) - cz - ent_z) * inv_dir_z if abs(ray_z) > _TINY else _BIG
332
+
333
+ ga_x = _ZERO
334
+ ga_y = _ZERO
335
+ ga_z = _ZERO
336
+ gb_x = _ZERO
337
+ gb_y = _ZERO
338
+ gb_z = _ZERO
339
+ f_prev = _ZERO
340
+ pend_axis = ent_axis
341
+ pend_t = _ZERO
342
+ first = True
343
+ while t < t_end:
344
+ f = _ZERO
345
+ if 0 <= ix < Nx and 0 <= iy < Ny and 0 <= iz < Nz:
346
+ f = d_vol[ix, iy, iz]
347
+ t_next = min(tx, ty, tz, t_end)
348
+ seg_len = t_next - t
349
+ if pend_axis >= 0:
350
+ c = (-f) if first else (f_prev - f)
351
+ if pend_axis == 0:
352
+ da, db = _face_terms(c, pend_t + t_min, t_a0, t_b0, length, ray_x)
353
+ ga_x += da
354
+ gb_x += db
355
+ elif pend_axis == 1:
356
+ da, db = _face_terms(c, pend_t + t_min, t_a0, t_b0, length, ray_y)
357
+ ga_y += da
358
+ gb_y += db
359
+ else:
360
+ da, db = _face_terms(c, pend_t + t_min, t_a0, t_b0, length, ray_z)
361
+ ga_z += da
362
+ gb_z += db
363
+ first = False
364
+ if seg_len > _ZERO:
365
+ accum += f * seg_len
366
+ f_prev = f
367
+ if tx <= ty and tx <= tz:
368
+ t = tx
369
+ pend_t = tx
370
+ pend_axis = 0
371
+ ix += step_x
372
+ tx += dt_x
373
+ elif ty <= tx and ty <= tz:
374
+ t = ty
375
+ pend_t = ty
376
+ pend_axis = 1
377
+ iy += step_y
378
+ ty += dt_y
379
+ else:
380
+ t = tz
381
+ pend_t = tz
382
+ pend_axis = 2
383
+ iz += step_z
384
+ tz += dt_z
385
+ if ex_axis == 0:
386
+ da, db = _face_terms(f_prev, t_max, t_a0, t_b0, length, ray_x)
387
+ ga_x += da
388
+ gb_x += db
389
+ elif ex_axis == 1:
390
+ da, db = _face_terms(f_prev, t_max, t_a0, t_b0, length, ray_y)
391
+ ga_y += da
392
+ gb_y += db
393
+ elif ex_axis == 2:
394
+ da, db = _face_terms(f_prev, t_max, t_a0, t_b0, length, ray_z)
395
+ ga_z += da
396
+ gb_z += db
397
+
398
+ S = accum / length
399
+ ga_x -= S * ray_x
400
+ ga_y -= S * ray_y
401
+ ga_z -= S * ray_z
402
+ gb_x += S * ray_x
403
+ gb_y += S * ray_y
404
+ gb_z += S * ray_z
405
+
406
+ cuda.atomic.add(d_grad_src, (iview, 0), g * ga_x)
407
+ cuda.atomic.add(d_grad_src, (iview, 1), g * ga_y)
408
+ cuda.atomic.add(d_grad_src, (iview, 2), g * ga_z)
409
+ cuda.atomic.add(d_grad_det_center, (iview, 0), g * gb_x)
410
+ cuda.atomic.add(d_grad_det_center, (iview, 1), g * gb_y)
411
+ cuda.atomic.add(d_grad_det_center, (iview, 2), g * gb_z)
412
+ cuda.atomic.add(d_grad_det_u, (iview, 0), g * u_phys * gb_x)
413
+ cuda.atomic.add(d_grad_det_u, (iview, 1), g * u_phys * gb_y)
414
+ cuda.atomic.add(d_grad_det_u, (iview, 2), g * u_phys * gb_z)
415
+ cuda.atomic.add(d_grad_det_v, (iview, 0), g * v_phys * gb_x)
416
+ cuda.atomic.add(d_grad_det_v, (iview, 1), g * v_phys * gb_y)
417
+ cuda.atomic.add(d_grad_det_v, (iview, 2), g * v_phys * gb_z)
418
+
419
+
420
+ @_FASTMATH_DECORATOR
421
+ def _parallel_2d_geometry_vjp_kernel(
422
+ d_image, Nx, Ny,
423
+ d_grad_sino, n_ang, n_det,
424
+ det_spacing, d_ray_dir, d_det_origin, d_det_u_vec,
425
+ cx, cy, voxel_spacing,
426
+ d_grad_ray_dir, d_grad_det_origin, d_grad_det_u,
427
+ ):
428
+ """Accumulate parallel-beam geometry gradients of ``<grad_sino, A x>`` per view.
429
+
430
+ The ray is ``P + t r`` over the whole line, with P the detector pixel and
431
+ r the ray direction as given. A face crossing ``t = (b - P_a) / r_a`` gives
432
+ ``dt/dP_a = -1 / r_a`` and ``dt/dr_a = -t / r_a``.
433
+ """
434
+ iang, idet = cuda.grid(2)
435
+ if iang >= n_ang or idet >= n_det:
436
+ return
437
+ g = d_grad_sino[iang, idet]
438
+ if g == _ZERO:
439
+ return
440
+
441
+ # Ray setup: identical to _parallel_2d_forward_kernel (float64), plus the
442
+ # entry and exit faces.
443
+ dir_x = np.float64(d_ray_dir[iang, 0])
444
+ dir_y = np.float64(d_ray_dir[iang, 1])
445
+ det_ox = np.float64(d_det_origin[iang, 0]) / voxel_spacing
446
+ det_oy = np.float64(d_det_origin[iang, 1]) / voxel_spacing
447
+ u_vec_x = np.float64(d_det_u_vec[iang, 0])
448
+ u_vec_y = np.float64(d_det_u_vec[iang, 1])
449
+ u_phys = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing
450
+ u_offset = (np.float64(idet) + _HALF - np.float64(n_det) * _HALF) * det_spacing / voxel_spacing
451
+ pnt_x = det_ox + u_offset * u_vec_x
452
+ pnt_y = det_oy + u_offset * u_vec_y
453
+
454
+ t_min, t_max = -_BIG, _BIG
455
+ ent_axis = -1
456
+ ex_axis = -1
457
+ if dir_x != 0.0:
458
+ tx1, tx2 = (-cx - pnt_x) / dir_x, (cx - pnt_x) / dir_x
459
+ lo, hi = min(tx1, tx2), max(tx1, tx2)
460
+ if lo > t_min:
461
+ t_min = lo
462
+ ent_axis = 0
463
+ if hi < t_max:
464
+ t_max = hi
465
+ ex_axis = 0
466
+ elif pnt_x < -cx or pnt_x > cx:
467
+ return
468
+ if dir_y != 0.0:
469
+ ty1, ty2 = (-cy - pnt_y) / dir_y, (cy - pnt_y) / dir_y
470
+ lo, hi = min(ty1, ty2), max(ty1, ty2)
471
+ if lo > t_min:
472
+ t_min = lo
473
+ ent_axis = 1
474
+ if hi < t_max:
475
+ t_max = hi
476
+ ex_axis = 1
477
+ elif pnt_y < -cy or pnt_y > cy:
478
+ return
479
+ if t_min >= t_max:
480
+ return
481
+
482
+ ent_x = np.float32(pnt_x + t_min * dir_x)
483
+ ent_y = np.float32(pnt_y + t_min * dir_y)
484
+ ray_x = np.float32(dir_x)
485
+ ray_y = np.float32(dir_y)
486
+ t_end = np.float32(t_max - t_min)
487
+
488
+ t = _ZERO
489
+ ix = int(math.floor(ent_x + cx))
490
+ iy = int(math.floor(ent_y + cy))
491
+ step_x, step_y = (1 if ray_x >= 0 else -1), (1 if ray_y >= 0 else -1)
492
+ inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
493
+ inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
494
+ dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG
495
+ dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG
496
+ next_ix = ix + (1 if step_x > 0 else 0)
497
+ next_iy = iy + (1 if step_y > 0 else 0)
498
+ tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
499
+ ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
500
+
501
+ # Per axis: C = sum of face coefficients, T = sum of coefficient * t,
502
+ # with t measured from the entry point (t_min is added afterwards).
503
+ c_x = _ZERO
504
+ c_y = _ZERO
505
+ ct_x = _ZERO
506
+ ct_y = _ZERO
507
+ f_prev = _ZERO
508
+ pend_axis = ent_axis
509
+ pend_t = _ZERO
510
+ first = True
511
+ while t < t_end:
512
+ f = _ZERO
513
+ if 0 <= ix < Nx and 0 <= iy < Ny:
514
+ f = d_image[iy, ix]
515
+ if pend_axis >= 0:
516
+ c = (-f) if first else (f_prev - f)
517
+ if pend_axis == 0:
518
+ c_x += c
519
+ ct_x += c * pend_t
520
+ else:
521
+ c_y += c
522
+ ct_y += c * pend_t
523
+ first = False
524
+ f_prev = f
525
+ if tx <= ty:
526
+ t = tx
527
+ pend_t = tx
528
+ pend_axis = 0
529
+ ix += step_x
530
+ tx += dt_x
531
+ else:
532
+ t = ty
533
+ pend_t = ty
534
+ pend_axis = 1
535
+ iy += step_y
536
+ ty += dt_y
537
+ if ex_axis == 0:
538
+ c_x += f_prev
539
+ ct_x += f_prev * t_end
540
+ elif ex_axis == 1:
541
+ c_y += f_prev
542
+ ct_y += f_prev * t_end
543
+
544
+ # p = s * sum_j c_j t_j; dt/dP_a = -1/r_a (P in voxels) and dt/dr_a = -t/r_a.
545
+ # t_min stays float64: a distant detector origin makes it large.
546
+ gp_x = _ZERO
547
+ gp_y = _ZERO
548
+ gr_x = _ZERO
549
+ gr_y = _ZERO
550
+ if abs(ray_x) > _TINY:
551
+ gp_x = -c_x / ray_x
552
+ gr_x = np.float32(-voxel_spacing * (t_min * c_x + ct_x) / ray_x)
553
+ if abs(ray_y) > _TINY:
554
+ gp_y = -c_y / ray_y
555
+ gr_y = np.float32(-voxel_spacing * (t_min * c_y + ct_y) / ray_y)
556
+
557
+ cuda.atomic.add(d_grad_ray_dir, (iang, 0), g * gr_x)
558
+ cuda.atomic.add(d_grad_ray_dir, (iang, 1), g * gr_y)
559
+ cuda.atomic.add(d_grad_det_origin, (iang, 0), g * gp_x)
560
+ cuda.atomic.add(d_grad_det_origin, (iang, 1), g * gp_y)
561
+ cuda.atomic.add(d_grad_det_u, (iang, 0), g * u_phys * gp_x)
562
+ cuda.atomic.add(d_grad_det_u, (iang, 1), g * u_phys * gp_y)