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 CHANGED
@@ -64,7 +64,7 @@ try:
64
64
  except ImportError:
65
65
  pass
66
66
 
67
- __version__ = '2.4.2'
67
+ __version__ = '2.4.4'
68
68
 
69
69
  __all__ = [
70
70
  'Projector',
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, including view/space replicas."""
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 = 0.
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="cpu", dtype=target.dtype).clone()
96
- block.mul_(scale).add_(source.read(selected).to(device="cpu", dtype=target.dtype), alpha=factor)
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
- global_value = scalar_sum(operator, local.detach().item(), "volume")
108
- total = local + (local.new_tensor(global_value) - local.detach())
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, _DTYPE(voxel_spacing),
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, _DTYPE(voxel_spacing),
526
- _DTYPE(isocenter[0].item() / voxel_spacing),
527
- _DTYPE(isocenter[1].item() / voxel_spacing),
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, _DTYPE(voxel_spacing),
589
- _DTYPE(isocenter[0].item() / voxel_spacing),
590
- _DTYPE(isocenter[1].item() / voxel_spacing),
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.item() / parameters[-1]) for value in isocenter)
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
- _DTYPE(spacing), *iso,
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), _DTYPE(spacing), *iso,
663
+ _DTYPE(W * 0.5), _DTYPE(H * 0.5), _native_spacing(spacing), *iso,
664
664
  )
665
665
  return sinogram * scale
666
666
 
@@ -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.spacing[0])
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.spacing[0], isocenter=isocenter)
348
+ self.voxel_spacing, isocenter=isocenter)
343
349
  return cone_weighted_backproject(data, *geometry, *shape, *self.pitch,
344
- self.spacing[0], isocenter=isocenter)
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.