Generated from the full canonical file for this source snapshot. Line numbers match the library source.
Source SHA256: 466f166e046a334d3eb9f5cabd9bea3ef6f7868e03e4072858ea28454895813b
1"""Composition of the canonical projector, transmission, objective and optimiser.23The fixed volume, observation and reusable image buffers remain on CUDA. Each4evaluation explicitly uploads a twelve-value rigid transform and downloads one5loss and six local pose derivatives, plus explicit numerical-status checkpoints.6This is a deterministic primary-model7adapter; spectral/nuisance adapters use the same scaled optimiser contract.8"""910from __future__ import annotations1112import math13from dataclasses import dataclass, field14from typing import Any, Literal1516from dpt._runtime import require_no_tape17from dpt.contracts import ContractError, NumericalError, finite_scalar18from dpt.geometry import DetectorGeometry, RigidTransform, chart_gradient, compose_pose19from dpt.objectives import (20ObjectiveSpec,21evaluate_objective,22evaluate_primary_objective,23prepare_objective,24)25from dpt.projection import ProjectionSpec, prepare_projection, project_optical_depth, projection_vjp26from dpt.registration import Evaluation, RecoveryPolicy, RecoveryResult, Vector, recover_parameters27from dpt.transmission import TransmissionSpec, prepare_transmission, transmission_vjp, transmit28from dpt.volumes import GridSpec293031@dataclass(frozen=True, slots=True)32class PoseChart:33"""Dimensionless optimiser coordinates mapped into one fixed SE(3) chart."""3435anchor: RigidTransform = field(default_factory=RigidTransform)36scales: tuple[float, float, float, float, float, float] = (1.0, 1.0, 1.0, 0.01, 0.01, 0.01)37rotation_radius_radians: float = math.pi3839def __post_init__(self) -> None:40if not isinstance(self.anchor, RigidTransform): # pyright: ignore[reportUnnecessaryIsInstance]41raise ContractError("pose anchor must be a validated RigidTransform")42if len(self.scales) != 6:43raise ContractError("pose scales must contain three mm and three radian values")44for value in self.scales:45if finite_scalar(value, "pose scale", minimum=0.0) == 0:46raise ContractError("pose scales must be strictly positive")47object.__setattr__(self, "scales", tuple(float(value) for value in self.scales))48radius = finite_scalar(self.rotation_radius_radians, "rotation radius", minimum=0.0)49if not 0 < radius <= math.pi:50raise ContractError("fixed pose chart rotation radius must lie in (0, pi]")5152def increment(self, parameters: Vector) -> Vector:53if len(parameters) != 6:54raise ContractError("a rigid-pose evaluator needs six chart parameters")55values = tuple(a * b for a, b in zip(parameters, self.scales, strict=True))56if (57not all(map(math.isfinite, values))58or math.hypot(*values[3:]) >= self.rotation_radius_radians59):60raise NumericalError("trial pose lies outside the declared fixed rotation chart")61return values6263def pose(self, parameters: Vector) -> RigidTransform:64increment = self.increment(parameters)65try:66return compose_pose(self.anchor, increment)67except (ValueError, OverflowError) as error:68raise NumericalError("trial transform exceeds the finite rigid-pose range") from error6970def gradient(self, parameters: Vector, local_gradient: Vector) -> Vector:71gradient = chart_gradient(self.increment(parameters), local_gradient)72return tuple(a * b for a, b in zip(gradient, self.scales, strict=True))737475@dataclass(frozen=True, slots=True)76class PrimaryPoseProblem:77grid: GridSpec78geometry: DetectorGeometry79attenuation: Any80observation: Any81objective: ObjectiveSpec = field(default_factory=lambda: ObjectiveSpec(domain="counts"))82open_beam: float = 1.083weights: Any = None84samples_per_ray: int = 25685precision: Literal["float32", "float64"] = "float64"86integration: Literal["midpoint", "cell_gauss"] = "midpoint"878889@dataclass(frozen=True, slots=True)90class PoseRecoveryResult:91pose: RigidTransform92optimisation: RecoveryResult939495class PrimaryPoseEvaluator:96"""Prepared single-stream composition; never share concurrently.9798Attenuation, observations, weights and chart are immutable for a solve.99The first evaluation checks their device values; later calls rely on that100immutability and check numerical output flags at explicit completion points.101FP64 depth, signal and depth cotangents are retained by default so a pose102line search can resolve the loss changes indicated by its gradient. Explicit103precision="float32" reproduces the earlier composed storage contract.104Scratch contents after return may correspond to a rejected trial; the result105returned by recover_pose identifies the accepted pose and objective. Call106evaluate explicitly at that pose if a final predicted image is needed.107"""108109def __init__(110self,111problem: PrimaryPoseProblem,112chart: PoseChart,113*,114device: str = "cuda:0",115stream: Any = None,116) -> None:117self.problem, self.chart = problem, chart118if problem.objective.domain not in ("counts", "log_transmission"):119raise ContractError("primary pose recovery requires counts or log-transmission data")120self.projection = prepare_projection(121problem.grid,122problem.geometry,123ProjectionSpec(124problem.samples_per_ray,125precision=problem.precision,126integration=problem.integration,127),128device=device,129stream=stream,130)131self.context = self.projection.context132ctx, wp, count = self.context, self.context.wp, problem.geometry.pixels133ctx.array(134problem.attenuation, "attenuation", dtype=wp.float32, shape=(problem.grid.voxels,)135)136ctx.array(problem.observation, "observation", dtype=wp.float32, shape=(count,))137if problem.objective.weighted:138ctx.array(problem.weights, "weights", dtype=wp.float32, shape=(count,))139elif problem.weights is not None:140raise ContractError("weights require an explicitly weighted objective")141self.transmission = (142prepare_transmission(143TransmissionSpec(beam="scalar" if problem.objective.domain == "counts" else "none"),144max_pixels=count,145device=device,146stream=ctx.stream,147)148if problem.precision == "float32"149else None150)151self.objective = prepare_objective(152problem.objective, max_pixels=count, device=device, stream=ctx.stream153)154with ctx.scope():155self.pose_device = wp.empty(12, dtype=wp.float64, device=ctx.device)156self.optical_depth = wp.empty(count, dtype=self.projection.dtype, device=ctx.device)157self.prediction = wp.empty(count, dtype=self.projection.dtype, device=ctx.device)158self.image_seed = wp.empty(159count if problem.precision == "float32" else 0, dtype=wp.float32, device=ctx.device160)161self.depth_seed = wp.empty(count, dtype=self.projection.dtype, device=ctx.device)162self.pose_seed = wp.empty(6, dtype=wp.float64, device=ctx.device)163self.loss = wp.empty(1, dtype=wp.float64, device=ctx.device)164# Pinned host staging has a persistent owner and a fixed-size NumPy view.165# These are explicit control-plane transfers, not per-pixel Python work.166self._pose_host = wp.empty(12, dtype=wp.float64, device="cpu", pinned=True)167self._loss_host = wp.empty(1, dtype=wp.float64, device="cpu", pinned=True)168self._gradient_host = wp.empty(6, dtype=wp.float64, device="cpu", pinned=True)169self._pose_view = self._pose_host.numpy()170self._loss_view = self._loss_host.numpy()171self._gradient_view = self._gradient_host.numpy()172self._inputs_checked = False173174# region book:recovery-device-composition175def __call__(self, parameters: Vector) -> Evaluation:176require_no_tape()177try:178return self._evaluate(parameters)179finally:180# Even a failed launch can follow an enqueued asynchronous H2D copy.181# Release its read of pinned staging before a retry may overwrite it.182self.context.wp.synchronize_stream(self.context.stream)183184def _evaluate(self, parameters: Vector) -> Evaluation:185pose = self.chart.pose(parameters)186ctx, wp, problem = self.context, self.context.wp, self.problem187validate = not self._inputs_checked188self.projection.clear_status()189if self.transmission is not None:190self.transmission.clear_status()191self.objective.clear_status()192with ctx.scope():193self._pose_view[:] = pose.packed()194wp.copy(self.pose_device, self._pose_host, stream=ctx.stream)195project_optical_depth(196problem.attenuation,197self.pose_device,198out_L=self.optical_depth,199workspace=self.projection,200stream=ctx.stream,201validate=validate,202)203if problem.precision == "float64":204evaluate_primary_objective(205self.optical_depth,206problem.observation,207open_beam=problem.open_beam,208weights=problem.weights,209out_prediction=self.prediction,210out_loss=self.loss,211out_depth_seed=self.depth_seed,212workspace=self.objective,213stream=ctx.stream,214validate=validate,215)216else:217assert self.transmission is not None218counts = problem.objective.domain == "counts"219transmit(220self.optical_depth,221problem.open_beam if counts else None,222out_counts=self.prediction if counts else None,223out_log_T=None if counts else self.prediction,224workspace=self.transmission,225stream=ctx.stream,226validate=validate,227)228evaluate_objective(229self.prediction,230problem.observation,231weights=problem.weights,232out_loss=self.loss,233out_seed=self.image_seed,234workspace=self.objective,235stream=ctx.stream,236validate=validate,237)238transmission_vjp(239self.optical_depth,240problem.open_beam if counts else None,241seed_counts=self.image_seed if counts else None,242seed_log_T=None if counts else self.image_seed,243out_grad_L=self.depth_seed,244workspace=self.transmission,245stream=ctx.stream,246validate=validate,247)248projection_vjp(249problem.attenuation,250self.pose_device,251adj_L=self.depth_seed,252out_pose=self.pose_seed,253workspace=self.projection,254stream=ctx.stream,255validate=validate,256)257self.projection.check_status()258if self.transmission is not None:259self.transmission.check_status()260self.objective.check_status()261self._inputs_checked = True262wp.copy(self._loss_host, self.loss, stream=ctx.stream)263wp.copy(self._gradient_host, self.pose_seed, stream=ctx.stream)264wp.synchronize_stream(ctx.stream)265gradient = tuple(float(value) for value in self._gradient_view)266return Evaluation(float(self._loss_view[0]), self.chart.gradient(parameters, gradient))267268# endregion book:recovery-device-composition269270271def recover_pose(272evaluator: PrimaryPoseEvaluator,273*,274initial: Vector = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0),275policy: RecoveryPolicy | None = None,276) -> PoseRecoveryResult:277"""Run the canonical safeguarded driver without rebasing its curvature chart."""278result = recover_parameters(evaluator, initial, policy=policy)279return PoseRecoveryResult(evaluator.chart.pose(result.parameters), result)280