diffct 2.0.2__py3-none-any.whl → 2.2.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.
diffct/__init__.py CHANGED
@@ -15,6 +15,8 @@ from .projectors import (
15
15
  )
16
16
 
17
17
  from .operators import Projector
18
+ from .storage import TensorStore, NpyStore
19
+ from .surfaces import ParameterizedSurface
18
20
 
19
21
  from .geometry import (
20
22
  circular_trajectory_3d,
@@ -49,10 +51,13 @@ try:
49
51
  except ImportError:
50
52
  pass
51
53
 
52
- __version__ = '2.0.2'
54
+ __version__ = '2.2.0'
53
55
 
54
56
  __all__ = [
55
57
  'Projector',
58
+ 'TensorStore',
59
+ 'NpyStore',
60
+ 'ParameterizedSurface',
56
61
  'ParallelProjectorFunction',
57
62
  'ParallelBackprojectorFunction',
58
63
  'FanProjectorFunction',
diffct/chunking.py ADDED
@@ -0,0 +1,201 @@
1
+ """Spatial slices and immutable execution limits for streamed projectors."""
2
+
3
+ from dataclasses import dataclass
4
+ from itertools import product
5
+ import math
6
+
7
+ import torch
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class _ChunkPlan:
12
+ chunk_shape: tuple
13
+ view_chunk_size: int
14
+ devices: tuple
15
+ detector_chunk_shape: tuple = None
16
+ gpu_budget_bytes: int = 0
17
+ allocation_limit: int = (1 << 63) - 1
18
+ schedule: str = "spatial"
19
+ window_size: int = 1
20
+ slots: int = 1
21
+ estimated_gpu_bytes: int = 0
22
+ fallback_reason: str = ""
23
+ foreign_output: bool = False
24
+ reservation_budget_bytes: int = (1 << 63) - 1
25
+ estimated_reserved_bytes: int = 0
26
+
27
+
28
+ def _pixel_tiles(shape, chunk_shape):
29
+ for starts in product(*(range(0, size, part) for size, part in zip(shape, chunk_shape))):
30
+ clipped = tuple(min(part, size - start) for size, start, part in zip(shape, starts, chunk_shape))
31
+ yield tuple(slice(start, start + size) for start, size in zip(starts, clipped)), clipped
32
+
33
+
34
+ def _memory_budget(device):
35
+ """Usable allocator bytes, retaining 25% headroom for allocation overhead.
36
+
37
+ Reusable reserved blocks count as available, but never bypass the process
38
+ fraction ceiling. Older PyTorch without its public fraction getter can
39
+ only observe driver/allocator memory; explicit limits remain available.
40
+ """
41
+ free, total = torch.cuda.mem_get_info(device)
42
+ allocated = torch.cuda.memory_allocated(device)
43
+ reusable = max(0, torch.cuda.memory_reserved(device) - allocated)
44
+ getter = getattr(torch.cuda, "get_per_process_memory_fraction", None)
45
+ fraction = getter(device) if getter is not None else 1.0
46
+ allocator_room = max(0, int(total * fraction) - allocated)
47
+ backend = getattr(torch.cuda, "get_allocator_backend", lambda: "native")()
48
+ foreign_splits = 0
49
+ if backend == "native":
50
+ caller = torch.cuda.current_stream(device).cuda_stream
51
+ for segment in _native_segments(device):
52
+ blocks = segment["blocks"]
53
+ if segment["stream"] != caller and any(block["state"] != "inactive" for block in blocks):
54
+ foreign_splits += sum(block["size"] for block in blocks if block["state"] == "inactive")
55
+ available = max(0, min(free + reusable, allocator_room) - foreign_splits)
56
+ budget = available * 3 // 4
57
+ allocation_limit = (1 << 63) - 1 # Unbounded limit in scalar int64 agreement.
58
+ # Native requests >1 MiB and <10 MiB reserve a 20 MiB segment. A smaller
59
+ # current available room must use the <=1 MiB small pool, whose segments are 2 MiB.
60
+ # Source: pytorch/c10/cuda/CUDACachingAllocator.cpp (Native allocator notes).
61
+ if backend == "native" and available < 20 * 1024 ** 2:
62
+ allocation_limit = 1024 ** 2
63
+ return budget, allocation_limit
64
+
65
+
66
+ def _native_segments(device):
67
+ index = torch.device(device).index if not isinstance(device, int) else device
68
+ index = torch.cuda.current_device() if index is None else index
69
+ return [segment for segment in torch.cuda.memory_snapshot() if segment["device"] == index]
70
+
71
+
72
+ def _reservation_budget(device):
73
+ """Room for fresh segments, conservatively excluding every pinned segment.
74
+
75
+ Free splits in a live segment may serve the caller, but cannot guarantee
76
+ placement of the complete new inventory. Wholly inactive segments can be
77
+ released by the native allocator, including those owned by other streams.
78
+ """
79
+ if torch.cuda.get_allocator_backend() != "native":
80
+ return (1 << 63) - 1
81
+ free, total = torch.cuda.mem_get_info(device)
82
+ segments = _native_segments(device)
83
+ pinned = sum(segment["total_size"] for segment in segments
84
+ if any(block["state"] != "inactive" for block in segment["blocks"]))
85
+ reclaimable = sum(segment["total_size"] for segment in segments
86
+ if all(block["state"] == "inactive" for block in segment["blocks"]))
87
+ getter = getattr(torch.cuda, "get_per_process_memory_fraction", None)
88
+ fraction = getter(device) if getter is not None else 1.0
89
+ return max(0, min(free + reclaimable, int(total * fraction) - pinned))
90
+
91
+
92
+ def _working_set_bytes(beam, shape, views, detector_shape, surface):
93
+ """Conservative native staging/layout, cotangent, geometry and VJP scratch."""
94
+ rank = len(shape)
95
+ voxels = math.prod(shape)
96
+ rays = views * math.prod(detector_shape)
97
+ geometry = (rank * (views + rays) if surface
98
+ else rank * (4 if beam == "cone" else 3) * views)
99
+ # A resident native tile, staging and completed cone layout can coexist.
100
+ # Two ray buffers cover reusable output/cotangent and transfer scratch; two
101
+ # geometry buffers cover staged values and their first-order VJP/reduction.
102
+ tile_buffers = 3 if beam == "cone" else 2
103
+ return 4 * (tile_buffers * voxels + 2 * rays + 2 * geometry)
104
+
105
+
106
+ def _largest_buffer_bytes(beam, shape, views, detector_shape, surface):
107
+ rank = len(shape)
108
+ rays = views * math.prod(detector_shape)
109
+ geometry = (rank * (views + rays) if surface
110
+ else rank * (4 if beam == "cone" else 3) * views)
111
+ return 4 * max(math.prod(shape), rays, geometry)
112
+
113
+
114
+ def _allocation_bytes(elements):
115
+ size = 4 * elements
116
+ if torch.cuda.get_allocator_backend() == "native":
117
+ return (size + 511) // 512 * 512
118
+ return size
119
+
120
+
121
+ def _reservation_bytes(buffer_sizes):
122
+ """Fresh, monotone fixed-buffer inventory bound for standard native pools.
123
+
124
+ Arbitrary free holes and allocator overrides are outside this model. Large
125
+ and middle requests share one pool, so mixed inventories use separate fresh
126
+ segments per request instead of assuming their placement is independent.
127
+ """
128
+ sizes = [size for size in buffer_sizes if size > 0]
129
+ if torch.cuda.get_allocator_backend() != "native":
130
+ return sum(sizes)
131
+ mib = 1024 ** 2
132
+ sizes = [(size + 511) // 512 * 512 for size in sizes]
133
+ large = [size for size in sizes if size >= 10 * mib]
134
+ reservation = sum((size + 2 * mib - 1) // (2 * mib) * (2 * mib) for size in large)
135
+ middle = [size for size in sizes if mib < size < 10 * mib]
136
+ if large:
137
+ reservation += len(middle) * 20 * mib
138
+ middle = []
139
+ for segment_size, requests in (
140
+ (2 * mib, [size for size in sizes if size <= mib]),
141
+ (20 * mib, middle),
142
+ ):
143
+ if not requests:
144
+ continue
145
+ total = sum(requests)
146
+ # At a new segment's birth every older segment holds more than
147
+ # segment_size - max(requests); the new one holds at least min(requests).
148
+ # This bounds online best-fit placement in any allocation order.
149
+ count = (1 if total <= segment_size else
150
+ (total - min(requests)) // (segment_size - max(requests)) + 1)
151
+ reservation += count * segment_size
152
+ return reservation
153
+
154
+
155
+ def _pipeline_buffers(beam, shape, views, pixels, points, *, slots=1, retained=1,
156
+ geometry_grad=False, peers=False, ray_accumulators=0):
157
+ """Live typed allocation inventory, with native rounding per buffer."""
158
+ rank, rays = len(shape), views * math.prod(pixels)
159
+ native = _allocation_bytes(math.prod(shape))
160
+ # Two producer epochs permit the preceding native/layout/output storage
161
+ # to stay in flight while the next retained tile window queues work.
162
+ tiles = slots * (retained + int(beam == "cone")) + int(peers)
163
+ sizes = ((views * rank, rays * rank) if points
164
+ else (views * rank,) * (4 if beam == "cone" else 3))
165
+ geometry = tuple(_allocation_bytes(size) for size in sizes)
166
+ return ((native,) * tiles + (_allocation_bytes(rays),) * (slots + ray_accumulators)
167
+ + geometry * (slots * (1 + int(geometry_grad))))
168
+
169
+
170
+ def _pipeline_bytes(beam, shape, views, pixels, points, *, slots=1, retained=1,
171
+ geometry_grad=False, peers=False, ray_accumulators=0):
172
+ """Live typed bytes; segment reservation is admitted separately."""
173
+ rank, rays = len(shape), views * math.prod(pixels)
174
+ native = _allocation_bytes(math.prod(shape))
175
+ tile = native * (slots * (retained + int(beam == "cone")) + int(peers))
176
+ sizes = ((views * rank, rays * rank) if points
177
+ else (views * rank,) * (4 if beam == "cone" else 3))
178
+ geometry = sum(_allocation_bytes(size) for size in sizes)
179
+ return (tile + slots * (_allocation_bytes(rays) + geometry)
180
+ + ray_accumulators * _allocation_bytes(rays) + (slots * geometry if geometry_grad else 0))
181
+
182
+
183
+ def _host_geometry_bytes(beam, views, pixels, points):
184
+ rank = 3 if beam == "cone" else 2
185
+ if not points:
186
+ return 8 * 2 * rank * views * (4 if beam == "cone" else 3)
187
+ # Grids/offsets, world expressions and first-order cotangent/graph work.
188
+ # Arbitrary sampler internals and explicit parameter snapshots are separate.
189
+ return 8 * rank * (math.prod(pixels) * (1 + 6 * views) + 4 * views)
190
+
191
+
192
+ def _tiles(shape, chunk_shape, spacing):
193
+ """Yield tensor slices, clipped shapes and physical centres in xyz order."""
194
+ starts = (range(0, size, chunk) for size, chunk in zip(shape, chunk_shape))
195
+ for start in product(*starts):
196
+ tile_shape = tuple(min(chunk, size - first)
197
+ for size, chunk, first in zip(shape, chunk_shape, start))
198
+ slices = tuple(slice(first, first + size) for first, size in zip(start, tile_shape))
199
+ centre = tuple((first + size / 2 - full / 2) * spacing
200
+ for first, size, full in zip(start, tile_shape, shape))
201
+ yield slices, tile_shape, centre[::-1]
@@ -30,7 +30,7 @@ def _cone_3d_forward_kernel(
30
30
  d_vol, Nx, Ny, Nz,
31
31
  d_sino, n_views, n_u, n_v,
32
32
  du, dv, d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
33
- cx, cy, cz, voxel_spacing
33
+ cx, cy, cz, voxel_spacing, d_detector_positions=None,
34
34
  ):
35
35
  """Compute the 3D cone-beam forward projection with arbitrary source-detector trajectories.
36
36
 
@@ -93,27 +93,32 @@ def _cone_3d_forward_kernel(
93
93
  src_y = d_src_pos[iview, 1] / voxel_spacing
94
94
  src_z = d_src_pos[iview, 2] / voxel_spacing
95
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
96
+ if d_detector_positions is None or d_detector_positions.size == 0:
97
+ # Read detector center and orientation vectors
98
+ det_cx = d_det_center[iview, 0] / voxel_spacing
99
+ det_cy = d_det_center[iview, 1] / voxel_spacing
100
+ det_cz = d_det_center[iview, 2] / voxel_spacing
100
101
 
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]
102
+ u_vec_x = d_det_u_vec[iview, 0]
103
+ u_vec_y = d_det_u_vec[iview, 1]
104
+ u_vec_z = d_det_u_vec[iview, 2]
104
105
 
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]
106
+ v_vec_x = d_det_v_vec[iview, 0]
107
+ v_vec_y = d_det_v_vec[iview, 1]
108
+ v_vec_z = d_det_v_vec[iview, 2]
108
109
 
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
110
+ # Calculate detector element offset from center
111
+ u_offset = (np.float32(iu) + _HALF - np.float32(n_u) * _HALF) * du / voxel_spacing
112
+ v_offset = (np.float32(iv) + _HALF - np.float32(n_v) * _HALF) * dv / voxel_spacing
112
113
 
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
114
+ # Calculate 3D detector element position using center + u*u_vec + v*v_vec
115
+ det_x = det_cx + u_offset * u_vec_x + v_offset * v_vec_x
116
+ det_y = det_cy + u_offset * u_vec_y + v_offset * v_vec_y
117
+ det_z = det_cz + u_offset * u_vec_z + v_offset * v_vec_z
118
+ else:
119
+ det_x = d_detector_positions[iview, iu, iv, 0] / voxel_spacing
120
+ det_y = d_detector_positions[iview, iu, iv, 1] / voxel_spacing
121
+ det_z = d_detector_positions[iview, iu, iv, 2] / voxel_spacing
117
122
 
118
123
  # === 3D RAY DIRECTION CALCULATION ===
119
124
  # Ray direction vector from source to detector element in 3D space
@@ -238,7 +243,7 @@ def _cone_3d_backward_kernel(
238
243
  d_sino, n_views, n_u, n_v,
239
244
  d_vol, Nx, Ny, Nz,
240
245
  du, dv, d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
241
- cx, cy, cz, voxel_spacing
246
+ cx, cy, cz, voxel_spacing, d_detector_positions=None,
242
247
  ):
243
248
  """Compute the 3D cone-beam backprojection with arbitrary source-detector trajectories.
244
249
 
@@ -305,27 +310,32 @@ def _cone_3d_backward_kernel(
305
310
  src_y = d_src_pos[iview, 1] / voxel_spacing
306
311
  src_z = d_src_pos[iview, 2] / voxel_spacing
307
312
 
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
313
+ if d_detector_positions is None or d_detector_positions.size == 0:
314
+ # Read detector center and orientation vectors
315
+ det_cx = d_det_center[iview, 0] / voxel_spacing
316
+ det_cy = d_det_center[iview, 1] / voxel_spacing
317
+ det_cz = d_det_center[iview, 2] / voxel_spacing
312
318
 
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]
319
+ u_vec_x = d_det_u_vec[iview, 0]
320
+ u_vec_y = d_det_u_vec[iview, 1]
321
+ u_vec_z = d_det_u_vec[iview, 2]
316
322
 
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]
323
+ v_vec_x = d_det_v_vec[iview, 0]
324
+ v_vec_y = d_det_v_vec[iview, 1]
325
+ v_vec_z = d_det_v_vec[iview, 2]
320
326
 
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
327
+ # Calculate detector element offset from center
328
+ u_offset = (np.float32(iu) + _HALF - np.float32(n_u) * _HALF) * du / voxel_spacing
329
+ v_offset = (np.float32(iv) + _HALF - np.float32(n_v) * _HALF) * dv / voxel_spacing
324
330
 
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
331
+ # Calculate 3D detector element position using center + u*u_vec + v*v_vec
332
+ det_x = det_cx + u_offset * u_vec_x + v_offset * v_vec_x
333
+ det_y = det_cy + u_offset * u_vec_y + v_offset * v_vec_y
334
+ det_z = det_cz + u_offset * u_vec_z + v_offset * v_vec_z
335
+ else:
336
+ det_x = d_detector_positions[iview, iu, iv, 0] / voxel_spacing
337
+ det_y = d_detector_positions[iview, iu, iv, 1] / voxel_spacing
338
+ det_z = d_detector_positions[iview, iu, iv, 2] / voxel_spacing
329
339
 
330
340
  # === 3D RAY DIRECTION CALCULATION ===
331
341
  # Ray direction vector from source to detector element in 3D space
@@ -30,7 +30,7 @@ def _fan_2d_forward_kernel(
30
30
  d_image, Nx, Ny,
31
31
  d_sino, n_ang, n_det,
32
32
  det_spacing, d_src_pos, d_det_center, d_det_u_vec,
33
- cx, cy, voxel_spacing
33
+ cx, cy, voxel_spacing, d_detector_positions=None,
34
34
  ):
35
35
  """Compute the 2D fan beam forward projection with arbitrary source-detector trajectories.
36
36
 
@@ -82,19 +82,23 @@ def _fan_2d_forward_kernel(
82
82
  src_x = d_src_pos[iang, 0] / voxel_spacing
83
83
  src_y = d_src_pos[iang, 1] / voxel_spacing
84
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
85
+ if d_detector_positions is None or d_detector_positions.size == 0:
86
+ # Read detector center and orientation vector
87
+ det_cx = d_det_center[iang, 0] / voxel_spacing
88
+ det_cy = d_det_center[iang, 1] / voxel_spacing
88
89
 
89
- u_vec_x = d_det_u_vec[iang, 0]
90
- u_vec_y = d_det_u_vec[iang, 1]
90
+ u_vec_x = d_det_u_vec[iang, 0]
91
+ u_vec_y = d_det_u_vec[iang, 1]
91
92
 
92
- # Calculate detector element offset from center
93
- u_offset = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing / voxel_spacing
93
+ # Calculate detector element offset from center
94
+ u_offset = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing / voxel_spacing
94
95
 
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
96
+ # Calculate 2D detector element position using center + u*u_vec
97
+ det_x = det_cx + u_offset * u_vec_x
98
+ det_y = det_cy + u_offset * u_vec_y
99
+ else:
100
+ det_x = d_detector_positions[iang, idet, 0] / voxel_spacing
101
+ det_y = d_detector_positions[iang, idet, 1] / voxel_spacing
98
102
 
99
103
  # === RAY DIRECTION CALCULATION ===
100
104
  # Ray direction vector from source to detector element
@@ -192,7 +196,7 @@ def _fan_2d_backward_kernel(
192
196
  d_sino, n_ang, n_det,
193
197
  d_image, Nx, Ny,
194
198
  det_spacing, d_src_pos, d_det_center, d_det_u_vec,
195
- cx, cy, voxel_spacing
199
+ cx, cy, voxel_spacing, d_detector_positions=None,
196
200
  ):
197
201
  """Compute the 2D fan beam backprojection with arbitrary source-detector trajectories.
198
202
 
@@ -248,19 +252,23 @@ def _fan_2d_backward_kernel(
248
252
  src_x = d_src_pos[iang, 0] / voxel_spacing
249
253
  src_y = d_src_pos[iang, 1] / voxel_spacing
250
254
 
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
255
+ if d_detector_positions is None or d_detector_positions.size == 0:
256
+ # Read detector center and orientation vector
257
+ det_cx = d_det_center[iang, 0] / voxel_spacing
258
+ det_cy = d_det_center[iang, 1] / voxel_spacing
254
259
 
255
- u_vec_x = d_det_u_vec[iang, 0]
256
- u_vec_y = d_det_u_vec[iang, 1]
260
+ u_vec_x = d_det_u_vec[iang, 0]
261
+ u_vec_y = d_det_u_vec[iang, 1]
257
262
 
258
- # Calculate detector element offset from center
259
- u_offset = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing / voxel_spacing
263
+ # Calculate detector element offset from center
264
+ u_offset = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing / voxel_spacing
260
265
 
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
266
+ # Calculate 2D detector element position using center + u*u_vec
267
+ det_x = det_cx + u_offset * u_vec_x
268
+ det_y = det_cy + u_offset * u_vec_y
269
+ else:
270
+ det_x = d_detector_positions[iang, idet, 0] / voxel_spacing
271
+ det_y = d_detector_positions[iang, idet, 1] / voxel_spacing
264
272
 
265
273
  # === RAY DIRECTION CALCULATION ===
266
274
  # Ray direction vector from source to detector element
@@ -58,6 +58,7 @@ def _fan_2d_geometry_vjp_kernel(
58
58
  det_spacing, d_src_pos, d_det_center, d_det_u_vec,
59
59
  cx, cy, voxel_spacing,
60
60
  d_grad_src, d_grad_det_center, d_grad_det_u,
61
+ d_detector_positions=None, d_grad_detector_positions=None,
61
62
  ):
62
63
  """Accumulate fan-beam geometry gradients of ``<grad_sino, A x>`` per view."""
63
64
  iang, idet = cuda.grid(2)
@@ -71,14 +72,18 @@ def _fan_2d_geometry_vjp_kernel(
71
72
  # the entry and the exit (-1 = segment end at the source or detector).
72
73
  src_x = d_src_pos[iang, 0] / voxel_spacing
73
74
  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
75
+ if d_detector_positions is None or d_detector_positions.size == 0:
76
+ det_cx = d_det_center[iang, 0] / voxel_spacing
77
+ det_cy = d_det_center[iang, 1] / voxel_spacing
78
+ u_vec_x = d_det_u_vec[iang, 0]
79
+ u_vec_y = d_det_u_vec[iang, 1]
80
+ u_phys = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing
81
+ u_offset = u_phys / voxel_spacing
82
+ det_x = det_cx + u_offset * u_vec_x
83
+ det_y = det_cy + u_offset * u_vec_y
84
+ else:
85
+ det_x = d_detector_positions[iang, idet, 0] / voxel_spacing
86
+ det_y = d_detector_positions[iang, idet, 1] / voxel_spacing
82
87
 
83
88
  dir_x, dir_y = det_x - src_x, det_y - src_y
84
89
  length = math.sqrt(dir_x * dir_x + dir_y * dir_y)
@@ -203,10 +208,14 @@ def _fan_2d_geometry_vjp_kernel(
203
208
 
204
209
  cuda.atomic.add(d_grad_src, (iang, 0), g * ga_x)
205
210
  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)
211
+ if d_detector_positions is None or d_detector_positions.size == 0:
212
+ cuda.atomic.add(d_grad_det_center, (iang, 0), g * gb_x)
213
+ cuda.atomic.add(d_grad_det_center, (iang, 1), g * gb_y)
214
+ cuda.atomic.add(d_grad_det_u, (iang, 0), g * u_phys * gb_x)
215
+ cuda.atomic.add(d_grad_det_u, (iang, 1), g * u_phys * gb_y)
216
+ else:
217
+ d_grad_detector_positions[iang, idet, 0] = g * gb_x
218
+ d_grad_detector_positions[iang, idet, 1] = g * gb_y
210
219
 
211
220
 
212
221
  @_FASTMATH_DECORATOR
@@ -216,6 +225,7 @@ def _cone_3d_geometry_vjp_kernel(
216
225
  du, dv, d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
217
226
  cx, cy, cz, voxel_spacing,
218
227
  d_grad_src, d_grad_det_center, d_grad_det_u, d_grad_det_v,
228
+ d_detector_positions=None, d_grad_detector_positions=None,
219
229
  ):
220
230
  """Accumulate cone-beam geometry gradients of ``<grad_sino, A x>`` per view."""
221
231
  iv, iu, iview = cuda.grid(3)
@@ -228,22 +238,27 @@ def _cone_3d_geometry_vjp_kernel(
228
238
  src_x = d_src_pos[iview, 0] / voxel_spacing
229
239
  src_y = d_src_pos[iview, 1] / voxel_spacing
230
240
  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
241
+ if d_detector_positions is None or d_detector_positions.size == 0:
242
+ det_cx = d_det_center[iview, 0] / voxel_spacing
243
+ det_cy = d_det_center[iview, 1] / voxel_spacing
244
+ det_cz = d_det_center[iview, 2] / voxel_spacing
245
+ u_vec_x = d_det_u_vec[iview, 0]
246
+ u_vec_y = d_det_u_vec[iview, 1]
247
+ u_vec_z = d_det_u_vec[iview, 2]
248
+ v_vec_x = d_det_v_vec[iview, 0]
249
+ v_vec_y = d_det_v_vec[iview, 1]
250
+ v_vec_z = d_det_v_vec[iview, 2]
251
+ u_phys = (np.float32(iu) + _HALF - np.float32(n_u) * _HALF) * du
252
+ v_phys = (np.float32(iv) + _HALF - np.float32(n_v) * _HALF) * dv
253
+ u_offset = u_phys / voxel_spacing
254
+ v_offset = v_phys / voxel_spacing
255
+ det_x = det_cx + u_offset * u_vec_x + v_offset * v_vec_x
256
+ det_y = det_cy + u_offset * u_vec_y + v_offset * v_vec_y
257
+ det_z = det_cz + u_offset * u_vec_z + v_offset * v_vec_z
258
+ else:
259
+ det_x = d_detector_positions[iview, iu, iv, 0] / voxel_spacing
260
+ det_y = d_detector_positions[iview, iu, iv, 1] / voxel_spacing
261
+ det_z = d_detector_positions[iview, iu, iv, 2] / voxel_spacing
247
262
 
248
263
  dir_x, dir_y, dir_z = det_x - src_x, det_y - src_y, det_z - src_z
249
264
  length = math.sqrt(dir_x * dir_x + dir_y * dir_y + dir_z * dir_z)
@@ -406,15 +421,20 @@ def _cone_3d_geometry_vjp_kernel(
406
421
  cuda.atomic.add(d_grad_src, (iview, 0), g * ga_x)
407
422
  cuda.atomic.add(d_grad_src, (iview, 1), g * ga_y)
408
423
  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)
424
+ if d_detector_positions is None or d_detector_positions.size == 0:
425
+ cuda.atomic.add(d_grad_det_center, (iview, 0), g * gb_x)
426
+ cuda.atomic.add(d_grad_det_center, (iview, 1), g * gb_y)
427
+ cuda.atomic.add(d_grad_det_center, (iview, 2), g * gb_z)
428
+ cuda.atomic.add(d_grad_det_u, (iview, 0), g * u_phys * gb_x)
429
+ cuda.atomic.add(d_grad_det_u, (iview, 1), g * u_phys * gb_y)
430
+ cuda.atomic.add(d_grad_det_u, (iview, 2), g * u_phys * gb_z)
431
+ cuda.atomic.add(d_grad_det_v, (iview, 0), g * v_phys * gb_x)
432
+ cuda.atomic.add(d_grad_det_v, (iview, 1), g * v_phys * gb_y)
433
+ cuda.atomic.add(d_grad_det_v, (iview, 2), g * v_phys * gb_z)
434
+ else:
435
+ d_grad_detector_positions[iview, iu, iv, 0] = g * gb_x
436
+ d_grad_detector_positions[iview, iu, iv, 1] = g * gb_y
437
+ d_grad_detector_positions[iview, iu, iv, 2] = g * gb_z
418
438
 
419
439
 
420
440
  @_FASTMATH_DECORATOR
@@ -424,6 +444,7 @@ def _parallel_2d_geometry_vjp_kernel(
424
444
  det_spacing, d_ray_dir, d_det_origin, d_det_u_vec,
425
445
  cx, cy, voxel_spacing,
426
446
  d_grad_ray_dir, d_grad_det_origin, d_grad_det_u,
447
+ d_detector_positions=None, d_grad_detector_positions=None,
427
448
  ):
428
449
  """Accumulate parallel-beam geometry gradients of ``<grad_sino, A x>`` per view.
429
450
 
@@ -442,14 +463,18 @@ def _parallel_2d_geometry_vjp_kernel(
442
463
  # entry and exit faces.
443
464
  dir_x = np.float64(d_ray_dir[iang, 0])
444
465
  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
466
  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
467
+ if d_detector_positions is None or d_detector_positions.size == 0:
468
+ det_ox = np.float64(d_det_origin[iang, 0]) / voxel_spacing
469
+ det_oy = np.float64(d_det_origin[iang, 1]) / voxel_spacing
470
+ u_vec_x = np.float64(d_det_u_vec[iang, 0])
471
+ u_vec_y = np.float64(d_det_u_vec[iang, 1])
472
+ u_offset = (np.float64(idet) + _HALF - np.float64(n_det) * _HALF) * det_spacing / voxel_spacing
473
+ pnt_x = det_ox + u_offset * u_vec_x
474
+ pnt_y = det_oy + u_offset * u_vec_y
475
+ else:
476
+ pnt_x = np.float64(d_detector_positions[iang, idet, 0]) / voxel_spacing
477
+ pnt_y = np.float64(d_detector_positions[iang, idet, 1]) / voxel_spacing
453
478
 
454
479
  t_min, t_max = -_BIG, _BIG
455
480
  ent_axis = -1
@@ -556,7 +581,11 @@ def _parallel_2d_geometry_vjp_kernel(
556
581
 
557
582
  cuda.atomic.add(d_grad_ray_dir, (iang, 0), g * gr_x)
558
583
  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)
584
+ if d_detector_positions is None or d_detector_positions.size == 0:
585
+ cuda.atomic.add(d_grad_det_origin, (iang, 0), g * gp_x)
586
+ cuda.atomic.add(d_grad_det_origin, (iang, 1), g * gp_y)
587
+ cuda.atomic.add(d_grad_det_u, (iang, 0), g * u_phys * gp_x)
588
+ cuda.atomic.add(d_grad_det_u, (iang, 1), g * u_phys * gp_y)
589
+ else:
590
+ d_grad_detector_positions[iang, idet, 0] = g * gp_x
591
+ d_grad_detector_positions[iang, idet, 1] = g * gp_y