diffct 2.3.0__py3-none-any.whl → 2.4.1__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
@@ -30,6 +30,8 @@ from .reconstruction import (
30
30
  from .solvers import SolverResult, cgls, sirt, tv_reconstruction, cgls_into, total_variation
31
31
 
32
32
  from .geometry import (
33
+ transform_trajectory,
34
+ circular_trajectory_3d_parallel,
33
35
  circular_trajectory_3d,
34
36
  random_trajectory_3d,
35
37
  spiral_trajectory_3d,
@@ -62,7 +64,7 @@ try:
62
64
  except ImportError:
63
65
  pass
64
66
 
65
- __version__ = '2.3.0'
67
+ __version__ = '2.4.1'
66
68
 
67
69
  __all__ = [
68
70
  'Projector',
@@ -88,6 +90,8 @@ __all__ = [
88
90
  'FanBackprojectorFunction',
89
91
  'ConeProjectorFunction',
90
92
  'ConeBackprojectorFunction',
93
+ 'transform_trajectory',
94
+ 'circular_trajectory_3d_parallel',
91
95
  'circular_trajectory_3d',
92
96
  'random_trajectory_3d',
93
97
  'spiral_trajectory_3d',
diffct/_solver_state.py CHANGED
@@ -11,6 +11,7 @@ import torch.distributed as dist
11
11
 
12
12
  from .pipeline import _fragments
13
13
  from .storage import NpyStore, TensorStore
14
+ from .distribution import control_status, preflight
14
15
 
15
16
 
16
17
  def count(value, name, positive=False):
@@ -25,47 +26,56 @@ def finite(value, name, positive=False):
25
26
  return float(value)
26
27
 
27
28
 
28
- def agreement(operator, error=None, token=None):
29
+ def agreement(operator, error=None, token=None, *, cache_key=None):
29
30
  """Every rank rejects a bad request before numerical execution starts."""
30
31
  world = getattr(operator, "world_size", 1)
31
32
  if world == 1:
32
33
  if error is not None:
33
34
  raise error
34
35
  return
35
- records = [None] * world
36
- dist.all_gather_object(records, (None if error is None else str(error), token),
37
- group=operator.process_group)
38
- failures = [record[0] for record in records if record[0] is not None]
39
- if failures or any(record[1] != records[0][1] for record in records):
40
- raise ValueError("distributed solver agreement failed: " + (failures[0] if failures else "inconsistent checkpoint iteration or controls"))
36
+ error = None if error is None else ValueError("distributed solver agreement failed: " + str(error))
37
+ mismatch = "distributed solver agreement failed: inconsistent checkpoint iteration or controls"
38
+ partition = getattr(operator, "_partition", None)
39
+ if partition is not None:
40
+ partition.preflight(error, token, cache_key=cache_key, mismatch=mismatch)
41
+ else:
42
+ preflight(error, token, process_group=operator.process_group, world_size=world,
43
+ mismatch=mismatch)
41
44
 
42
45
 
43
- def scalar_sum(operator, value, kind):
44
- """Count physical rays/voxels once, including view/space replicas."""
45
- world = getattr(operator, "world_size", 1)
46
+ def owns_values(operator, kind):
47
+ """Select one copy of each physical ray or voxel for scalar reductions."""
46
48
  partition = getattr(operator, "partition", "views")
47
49
  rank = getattr(operator, "rank", 0)
48
- owns = (getattr(operator, "view_rank", 0) == 0 if kind == "volume"
50
+ return (getattr(operator, "view_rank", 0) == 0 if kind == "volume"
49
51
  else getattr(operator, "space_rank", 0) == 0) if partition == "hybrid" else (
50
52
  rank == 0 if (kind == "volume") == (partition == "views") else True
51
53
  )
52
- value = float(value) if owns else 0.
54
+
55
+
56
+ def scalar_sum(operator, value, kind):
57
+ """Count physical rays/voxels once, including view/space replicas."""
58
+ world = getattr(operator, "world_size", 1)
59
+ value = value if owns_values(operator, kind) else 0.
53
60
  if world > 1:
54
61
  group = operator.process_group
55
62
  device = torch.device("cuda", torch.cuda.current_device()) if dist.get_backend(group) == "nccl" else torch.device("cpu")
56
- accumulator = torch.tensor(value, dtype=torch.float64, device=device)
63
+ accumulator = torch.as_tensor(value, dtype=torch.float64, device=device).clone()
57
64
  dist.all_reduce(accumulator, group=group)
58
- value = accumulator.item()
59
- return value
65
+ return accumulator.item()
66
+ return float(value)
60
67
 
61
68
 
62
69
  def squared_norm(operator, value, kind, elements=1048576):
63
70
  store = TensorStore(value) if isinstance(value, torch.Tensor) else value
64
71
  total = 0.
65
- for selected in _fragments(tuple(store.shape), elements):
66
- # Clone even an FP64 view: norms must never alter caller measurements.
67
- block = store.read(selected).to(dtype=torch.float64).clone()
68
- total += block.square_().sum().item()
72
+ if owns_values(operator, kind):
73
+ total = torch.zeros((), dtype=torch.float64, device=getattr(store, "device", "cpu"))
74
+ for selected in _fragments(tuple(store.shape), elements):
75
+ # Clone even an FP64 view: norms must never alter caller measurements.
76
+ block = store.read(selected).to(dtype=torch.float64).clone()
77
+ # Preserve the block reductions and their sequential FP64 order.
78
+ total.add_(block.square_().sum().to(device=total.device))
69
79
  return scalar_sum(operator, total, kind)
70
80
 
71
81
 
@@ -201,6 +211,8 @@ def load_checkpoint(operator, directory, expected, iterations, elements, names):
201
211
  try:
202
212
  directory = Path(directory).resolve()
203
213
  saved = json.loads((directory / "metadata.json").read_text(encoding="utf-8"))
214
+ if not isinstance(saved, dict):
215
+ raise ValueError("checkpoint metadata must be an object")
204
216
  # JSON canonicalization also normalizes tuple-valued physical spacing.
205
217
  canonical = json.loads(json.dumps(expected))
206
218
  if any(saved.get(key) != value for key, value in canonical.items()):
@@ -215,12 +227,16 @@ def load_checkpoint(operator, directory, expected, iterations, elements, names):
215
227
  if expected["algorithm"] in ("cgls", "cgls_into"):
216
228
  finite(saved.get("gamma"), "checkpoint gamma")
217
229
  manifest = saved.get("states", {})
230
+ if not isinstance(manifest, dict):
231
+ raise ValueError("checkpoint states must be an object")
218
232
  if set(manifest) != set(names):
219
233
  raise ValueError("checkpoint does not contain the complete solver state")
220
234
  states = {}
221
235
  for name in names:
222
- store = NpyStore(directory / f"{name}.npy", mode="r")
223
236
  specification = manifest[name]
237
+ if not isinstance(specification, dict):
238
+ raise ValueError("checkpoint state specification must be an object")
239
+ store = NpyStore(directory / f"{name}.npy", mode="r")
224
240
  shape = operator.projection_shape if name in ("r", "q", "row") else operator.volume_shape
225
241
  if name == "step":
226
242
  shape = (1,)
@@ -270,15 +286,22 @@ def save_checkpoint(operator, directory, states, metadata, iteration, history, m
270
286
  agreement(operator, error, iteration)
271
287
 
272
288
 
289
+ def callbacks_enabled(operator, callback):
290
+ """Check callback presence once, including callbacks on only some ranks."""
291
+ return bool(control_status((int(callback is not None),),
292
+ process_group=getattr(operator, "process_group", None),
293
+ world_size=getattr(operator, "world_size", 1))[0])
294
+
295
+
273
296
  def callback_stop(operator, callback, iteration, residual, volume):
274
297
  error, stop = None, False
275
298
  try:
276
299
  stop = callback is not None and callback(iteration, residual, volume) is False
277
300
  except Exception as caught:
278
301
  error = caught
279
- agreement(operator, error)
280
- if getattr(operator, "world_size", 1) > 1:
281
- records = [None] * operator.world_size
282
- dist.all_gather_object(records, stop, group=operator.process_group)
283
- stop = any(records)
284
- return stop
302
+ failed, stop = control_status((int(error is not None), int(stop)),
303
+ process_group=getattr(operator, "process_group", None),
304
+ world_size=getattr(operator, "world_size", 1))
305
+ if failed:
306
+ agreement(operator, error)
307
+ return bool(stop)