diffct 2.4.2__py3-none-any.whl → 2.4.4__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 +1 -1
- diffct/_solver_state.py +12 -9
- diffct/_solver_tv.py +3 -2
- diffct/analytical.py +12 -12
- diffct/analytical_execution.py +24 -3
- diffct/backends.py +513 -113
- diffct/chunking.py +4 -2
- diffct/constants.py +12 -0
- diffct/kernels/analytical_transpose.py +40 -16
- diffct/kernels/cone_beam.py +117 -63
- diffct/kernels/fan_beam.py +49 -33
- diffct/kernels/geometry_vjp.py +153 -51
- diffct/kernels/parallel_beam.py +45 -15
- diffct/kernels/spacing.py +79 -0
- diffct/kernels/surface_geometry.py +22 -9
- diffct/operators.py +300 -106
- diffct/projectors.py +13 -6
- diffct/reconstruction.py +7 -4
- diffct/solvers.py +22 -11
- diffct/surfaces.py +20 -14
- diffct/utils.py +10 -0
- {diffct-2.4.2.dist-info → diffct-2.4.4.dist-info}/METADATA +12 -9
- diffct-2.4.4.dist-info/RECORD +34 -0
- {diffct-2.4.2.dist-info → diffct-2.4.4.dist-info}/licenses/NOTICE +2 -0
- diffct-2.4.2.dist-info/RECORD +0 -33
- {diffct-2.4.2.dist-info → diffct-2.4.4.dist-info}/WHEEL +0 -0
- {diffct-2.4.2.dist-info → diffct-2.4.4.dist-info}/licenses/LICENSE +0 -0
diffct/__init__.py
CHANGED
diffct/_solver_state.py
CHANGED
|
@@ -53,30 +53,32 @@ def owns_values(operator, kind):
|
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def scalar_sum(operator, value, kind):
|
|
57
|
-
"""Count physical rays/voxels once,
|
|
56
|
+
def scalar_sum(operator, value, kind, *, return_tensor=False):
|
|
57
|
+
"""Count physical rays/voxels once, optionally retaining the reduction tensor."""
|
|
58
58
|
world = getattr(operator, "world_size", 1)
|
|
59
|
+
result_device = value.device if isinstance(value, torch.Tensor) else torch.device("cpu")
|
|
59
60
|
value = value if owns_values(operator, kind) else 0.
|
|
60
61
|
if world > 1:
|
|
61
62
|
group = operator.process_group
|
|
62
63
|
device = torch.device("cuda", torch.cuda.current_device()) if dist.get_backend(group) == "nccl" else torch.device("cpu")
|
|
63
64
|
accumulator = torch.as_tensor(value, dtype=torch.float64, device=device).clone()
|
|
64
65
|
dist.all_reduce(accumulator, group=group)
|
|
65
|
-
return accumulator.item()
|
|
66
|
+
return accumulator if return_tensor else accumulator.item()
|
|
67
|
+
if return_tensor:
|
|
68
|
+
return torch.as_tensor(value, dtype=torch.float64, device=result_device)
|
|
66
69
|
return float(value)
|
|
67
70
|
|
|
68
71
|
|
|
69
|
-
def squared_norm(operator, value, kind, elements=1048576):
|
|
72
|
+
def squared_norm(operator, value, kind, elements=1048576, *, return_tensor=False):
|
|
70
73
|
store = TensorStore(value) if isinstance(value, torch.Tensor) else value
|
|
71
|
-
total =
|
|
74
|
+
total = torch.zeros((), dtype=torch.float64, device=getattr(store, "device", "cpu"))
|
|
72
75
|
if owns_values(operator, kind):
|
|
73
|
-
total = torch.zeros((), dtype=torch.float64, device=getattr(store, "device", "cpu"))
|
|
74
76
|
for selected in _fragments(tuple(store.shape), elements):
|
|
75
77
|
# Clone even an FP64 view: norms must never alter caller measurements.
|
|
76
78
|
block = store.read(selected).to(dtype=torch.float64).clone()
|
|
77
79
|
# Preserve the block reductions and their sequential FP64 order.
|
|
78
80
|
total.add_(block.square_().sum().to(device=total.device))
|
|
79
|
-
return scalar_sum(operator, total, kind)
|
|
81
|
+
return scalar_sum(operator, total, kind, return_tensor=return_tensor)
|
|
80
82
|
|
|
81
83
|
|
|
82
84
|
def copy_blocks(source, target, elements):
|
|
@@ -91,9 +93,10 @@ def fill_blocks(target, elements):
|
|
|
91
93
|
|
|
92
94
|
|
|
93
95
|
def update_blocks(target, source, factor, elements, scale=1.):
|
|
96
|
+
device = getattr(target, "device", "cpu")
|
|
94
97
|
for selected in _fragments(tuple(target.shape), elements):
|
|
95
|
-
block = target.read(selected).to(device=
|
|
96
|
-
block.mul_(scale).add_(source.read(selected).to(device=
|
|
98
|
+
block = target.read(selected).to(device=device, dtype=target.dtype).clone()
|
|
99
|
+
block.mul_(scale).add_(source.read(selected).to(device=device, dtype=target.dtype), alpha=factor)
|
|
97
100
|
target.write(selected, block)
|
|
98
101
|
|
|
99
102
|
|
diffct/_solver_tv.py
CHANGED
|
@@ -104,7 +104,8 @@ def total_variation(volume, *, operator=None, voxel_spacing=1., eps=1e-6, reduct
|
|
|
104
104
|
total = local
|
|
105
105
|
elements = volume.numel()
|
|
106
106
|
if operator is not None:
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
if getattr(operator, "world_size", 1) > 1:
|
|
108
|
+
global_value = scalar_sum(operator, local.detach(), "volume", return_tensor=True)
|
|
109
|
+
total = local + (global_value.to(local) - local.detach())
|
|
109
110
|
elements = math.prod(operator.global_volume_shape)
|
|
110
111
|
return total if reduction == "sum" else total / max(elements, 1)
|
diffct/analytical.py
CHANGED
|
@@ -27,7 +27,7 @@ import math
|
|
|
27
27
|
|
|
28
28
|
import torch
|
|
29
29
|
|
|
30
|
-
from .constants import _DTYPE
|
|
30
|
+
from .constants import _DTYPE, _native_spacing
|
|
31
31
|
from .utils import (
|
|
32
32
|
DeviceManager,
|
|
33
33
|
TorchCUDABridge,
|
|
@@ -431,7 +431,7 @@ def _parallel_weighted_backproject_raw(sinogram, ray_dir, det_origin, det_u_vec,
|
|
|
431
431
|
_resolve_kernel('_parallel_2d_fbp_backproject_kernel')[grid, tpb, numba_stream](
|
|
432
432
|
d_sino, n_views, n_det, d_reco, Nx, Ny,
|
|
433
433
|
_DTYPE(detector_spacing), d_ray_dir, d_det_origin, d_det_u_vec,
|
|
434
|
-
cx, cy,
|
|
434
|
+
cx, cy, _native_spacing(voxel_spacing),
|
|
435
435
|
)
|
|
436
436
|
|
|
437
437
|
return reco * (1.0 / (2.0 * math.pi))
|
|
@@ -522,9 +522,9 @@ def _fan_weighted_backproject_raw(sinogram, src_pos, det_center, det_u_vec,
|
|
|
522
522
|
_resolve_kernel('_fan_2d_fbp_backproject_kernel')[grid, tpb, numba_stream](
|
|
523
523
|
d_sino, n_views, n_det, d_reco, Nx, Ny,
|
|
524
524
|
_DTYPE(detector_spacing), d_src_pos, d_det_center, d_det_u_vec,
|
|
525
|
-
cx, cy,
|
|
526
|
-
_DTYPE(
|
|
527
|
-
|
|
525
|
+
cx, cy, _native_spacing(voxel_spacing),
|
|
526
|
+
*(_DTYPE(value if isinstance(voxel_spacing, (tuple, list)) else value / voxel_spacing)
|
|
527
|
+
for value in isocenter.tolist()),
|
|
528
528
|
)
|
|
529
529
|
|
|
530
530
|
return reco
|
|
@@ -585,10 +585,9 @@ def _cone_weighted_backproject_raw(sinogram, src_pos, det_center, det_u_vec, det
|
|
|
585
585
|
d_sino, n_views, n_u, n_v, d_reco, Nx, Ny, Nz,
|
|
586
586
|
_DTYPE(du), _DTYPE(dv),
|
|
587
587
|
d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
|
|
588
|
-
cx, cy, cz,
|
|
589
|
-
_DTYPE(
|
|
590
|
-
|
|
591
|
-
_DTYPE(isocenter[2].item() / voxel_spacing),
|
|
588
|
+
cx, cy, cz, _native_spacing(voxel_spacing),
|
|
589
|
+
*(_DTYPE(value if isinstance(voxel_spacing, (tuple, list)) else value / voxel_spacing)
|
|
590
|
+
for value in isocenter.tolist()),
|
|
592
591
|
)
|
|
593
592
|
|
|
594
593
|
# Convert back to (D, H, W) layout for the user.
|
|
@@ -640,7 +639,8 @@ def _analytical_transpose_raw(image, beam, geometry, parameters, isocenter, sino
|
|
|
640
639
|
scale = (sdd_n / (2.0 * math.pi * sid_n)).view(
|
|
641
640
|
(-1,) + (1,) * (len(sinogram_shape) - 1)
|
|
642
641
|
)
|
|
643
|
-
iso = tuple(_DTYPE(value
|
|
642
|
+
iso = tuple(_DTYPE(value if isinstance(parameters[-1], (tuple, list)) else value / parameters[-1])
|
|
643
|
+
for value in isocenter.tolist())
|
|
644
644
|
|
|
645
645
|
if beam == "cone":
|
|
646
646
|
D, H, W, du, dv, spacing = parameters
|
|
@@ -650,7 +650,7 @@ def _analytical_transpose_raw(image, beam, geometry, parameters, isocenter, sino
|
|
|
650
650
|
_resolve_kernel('_cone_3d_fdk_transpose_kernel')[grid, tpb, stream](
|
|
651
651
|
d_sino, *sinogram_shape, d_image, W, H, D, _DTYPE(du), _DTYPE(dv),
|
|
652
652
|
*d_geometry, _DTYPE(W * 0.5), _DTYPE(H * 0.5), _DTYPE(D * 0.5),
|
|
653
|
-
|
|
653
|
+
_native_spacing(spacing), *iso,
|
|
654
654
|
)
|
|
655
655
|
else:
|
|
656
656
|
pitch, H, W, spacing = parameters
|
|
@@ -660,7 +660,7 @@ def _analytical_transpose_raw(image, beam, geometry, parameters, isocenter, sino
|
|
|
660
660
|
else '_fan_2d_fbp_transpose_kernel')
|
|
661
661
|
kernel[grid, tpb, stream](
|
|
662
662
|
d_sino, *sinogram_shape, d_image, W, H, _DTYPE(pitch), *d_geometry,
|
|
663
|
-
_DTYPE(W * 0.5), _DTYPE(H * 0.5),
|
|
663
|
+
_DTYPE(W * 0.5), _DTYPE(H * 0.5), _native_spacing(spacing), *iso,
|
|
664
664
|
)
|
|
665
665
|
return sinogram * scale
|
|
666
666
|
|
diffct/analytical_execution.py
CHANGED
|
@@ -16,6 +16,7 @@ from .analytical import (
|
|
|
16
16
|
fan_weighted_backproject, parallel_weighted_backproject,
|
|
17
17
|
)
|
|
18
18
|
from .backends import _zero_dependency, select_backend, voxel_spacing_tuple
|
|
19
|
+
from .constants import _native_spacing
|
|
19
20
|
from .chunking import _memory_budget, _pixel_tiles
|
|
20
21
|
from .distribution import Partition, preflight
|
|
21
22
|
from .operators import _detector_spacing, _normalize_devices, _positive_int, _shape, _validate_trajectory
|
|
@@ -53,6 +54,8 @@ def _sum(value, group, count):
|
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
def _sum_blocks(value, group, count, block_elements):
|
|
57
|
+
if count == 1:
|
|
58
|
+
return value
|
|
56
59
|
flat = value.reshape(-1)
|
|
57
60
|
blocks = [_sum(flat.narrow(0, start, min(block_elements, flat.numel() - start)), group, count)
|
|
58
61
|
for start in range(0, flat.numel(), block_elements)]
|
|
@@ -141,6 +144,9 @@ class AnalyticalExecution:
|
|
|
141
144
|
self.rank = 3 if beam == "cone" else 2
|
|
142
145
|
self.global_volume_shape = _shape(volume_shape, self.rank, "volume_shape")
|
|
143
146
|
self.spacing = voxel_spacing_tuple(voxel_spacing, self.rank)
|
|
147
|
+
self.voxel_spacing = self.spacing if isinstance(voxel_spacing, (tuple, list)) else self.spacing[0]
|
|
148
|
+
if self.backend == "numba" and isinstance(self.voxel_spacing, tuple):
|
|
149
|
+
_native_spacing(self.voxel_spacing)
|
|
144
150
|
self.pitch = _detector_spacing(detector_spacing, beam)
|
|
145
151
|
self.devices = _normalize_devices(devices)
|
|
146
152
|
if not isinstance(source, torch.Tensor):
|
|
@@ -334,14 +340,14 @@ class AnalyticalExecution:
|
|
|
334
340
|
device=data.device, dtype=self.dtype)
|
|
335
341
|
if self.beam == "parallel":
|
|
336
342
|
geometry = (geometry[0], geometry[1] - centre, geometry[2])
|
|
337
|
-
return parallel_weighted_backproject(data, *geometry, self.pitch[0], *shape, self.
|
|
343
|
+
return parallel_weighted_backproject(data, *geometry, self.pitch[0], *shape, self.voxel_spacing)
|
|
338
344
|
geometry = (geometry[0] - centre, geometry[1] - centre, *geometry[2:])
|
|
339
345
|
isocenter = self.isocenter.to(data.device) - centre
|
|
340
346
|
if self.beam == "fan":
|
|
341
347
|
return fan_weighted_backproject(data, *geometry, self.pitch[0], *shape,
|
|
342
|
-
self.
|
|
348
|
+
self.voxel_spacing, isocenter=isocenter)
|
|
343
349
|
return cone_weighted_backproject(data, *geometry, *shape, *self.pitch,
|
|
344
|
-
self.
|
|
350
|
+
self.voxel_spacing, isocenter=isocenter)
|
|
345
351
|
|
|
346
352
|
def _tile(self, spatial):
|
|
347
353
|
shape = tuple(part.stop - part.start for part in spatial)
|
|
@@ -385,6 +391,21 @@ class AnalyticalExecution:
|
|
|
385
391
|
def tensor_result(self):
|
|
386
392
|
self._prepare_gradients()
|
|
387
393
|
result = torch.zeros(self.volume_shape, device=self.output_device, dtype=self.dtype)
|
|
394
|
+
inputs = (self.source, self.projection_weights, self.angular_weights, self.filter_response)
|
|
395
|
+
active_gradients = torch.is_grad_enabled() and any(value is not None and value.requires_grad
|
|
396
|
+
for value in inputs)
|
|
397
|
+
if self.partition.world_size == 1 and not active_gradients:
|
|
398
|
+
# Each tile still accumulates view batches in their original order.
|
|
399
|
+
# The numerical result itself holds the partial sums between batches.
|
|
400
|
+
for views, device in self._view_batches():
|
|
401
|
+
filtered, geometry = self._filtered(views, device)
|
|
402
|
+
for spatial, _ in _pixel_tiles(self.volume_shape, self.chunk_shape):
|
|
403
|
+
partial = (self._native_gather(filtered, geometry, spatial) if self.backend == "numba"
|
|
404
|
+
else self._portable_gather(filtered, geometry, views, spatial))
|
|
405
|
+
result[spatial].add_(partial.to(self.output_device))
|
|
406
|
+
del partial
|
|
407
|
+
del filtered, geometry
|
|
408
|
+
return result
|
|
388
409
|
# Every owned tile's backward collective precedes the bundled input
|
|
389
410
|
# transpose, even when this rank owns no data views. Empty space groups
|
|
390
411
|
# have no tile collectives but still contribute zero input cotangents.
|