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 +6 -1
- diffct/chunking.py +201 -0
- diffct/kernels/cone_beam.py +46 -36
- diffct/kernels/fan_beam.py +30 -22
- diffct/kernels/geometry_vjp.py +77 -48
- diffct/kernels/parallel_beam.py +32 -22
- diffct/operators.py +1551 -59
- diffct/pipeline.py +258 -0
- diffct/projectors.py +155 -125
- diffct/storage.py +125 -0
- diffct/streaming.py +48 -0
- diffct/surfaces.py +103 -0
- diffct/utils.py +5 -0
- {diffct-2.0.2.dist-info → diffct-2.2.0.dist-info}/METADATA +45 -19
- diffct-2.2.0.dist-info/RECORD +23 -0
- diffct-2.0.2.dist-info/RECORD +0 -18
- {diffct-2.0.2.dist-info → diffct-2.2.0.dist-info}/WHEEL +0 -0
- {diffct-2.0.2.dist-info → diffct-2.2.0.dist-info}/licenses/LICENSE +0 -0
- {diffct-2.0.2.dist-info → diffct-2.2.0.dist-info}/licenses/NOTICE +0 -0
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
|
|
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]
|
diffct/kernels/cone_beam.py
CHANGED
|
@@ -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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
diffct/kernels/fan_beam.py
CHANGED
|
@@ -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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
90
|
+
u_vec_x = d_det_u_vec[iang, 0]
|
|
91
|
+
u_vec_y = d_det_u_vec[iang, 1]
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
260
|
+
u_vec_x = d_det_u_vec[iang, 0]
|
|
261
|
+
u_vec_y = d_det_u_vec[iang, 1]
|
|
257
262
|
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
diffct/kernels/geometry_vjp.py
CHANGED
|
@@ -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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|