Generated from the full canonical file for this source snapshot. Line numbers match the library source.
Source SHA256: 1641dd708437045407b2808be3a9e286b6dcdd98496b13e3d6d71308e1293cc5
1"""Finite-segment optical-depth projection with caller-owned device buffers.23Coordinates and quadrature accumulators are FP64; field samples remain FP32.4Depth and depth-cotangent storage is explicitly FP32 (default) or FP64.5Numerical acceptance and performance must be established on CUDA;6this interface does not equate a finite result with an accurate projection.7"""89from __future__ import annotations1011# Mathematical output names follow the optical-depth contract.12# ruff: noqa: N80313# Module-owned workspace scratch stays private to this implementation.14# pyright: reportPrivateUsage=false15import math16from dataclasses import dataclass, field17from typing import Any, Literal1819from dpt._reductions import ReductionTree, prepare_reduction20from dpt._runtime import DeviceContext, ensure_tape, load_kernels, prepare_context, require_no_tape21from dpt.contracts import ContractError, NumericalError, integer22from dpt.geometry import DetectorGeometry23from dpt.volumes import GridSpec2425_BLOCK = 128262728@dataclass(frozen=True, slots=True)29class ProjectionSpec:30"""Declared ray integration and explicitly supported active inputs.3132Midpoint uses samples_per_ray. Cell Gauss ignores this count and splits at33every interpolation plane, integrating each cubic segment with two nodes.3435Grid/acquisition calibration is fixed. Pose derivatives include active36intersection bounds. At ties, grazing rays and interpolation knots the37returned branch derivative does not assert differentiability.38"""3940samples_per_ray: int41active_pose: bool = True42active_volume: bool = False43precision: Literal["float32", "float64"] = "float32"44integration: Literal["midpoint", "cell_gauss"] = "midpoint"4546def __post_init__(self) -> None:47integer(self.samples_per_ray, "samples_per_ray", minimum=1)48if self.integration not in ("midpoint", "cell_gauss"):49raise ContractError("projection integration must be midpoint or cell_gauss")50if self.precision not in ("float32", "float64"):51raise ContractError("projection precision must be float32 or float64")52if type(self.active_pose) is not bool or type(self.active_volume) is not bool:53raise ContractError("active input flags must be booleans")545556@dataclass(slots=True)57class ProjectionWorkspace:58"""Persistent O(12 P/128) FP64 reduction storage, one owning CUDA stream.5960Original volume and pose buffers must remain immutable through backward.61One recorded forward may be outstanding per workspace. Use another62workspace for concurrent/re-entrant evaluations. No sample history is kept.63"""6465grid: GridSpec66geometry: DetectorGeometry67spec: ProjectionSpec68context: DeviceContext69_kernels: Any = field(repr=False)70_geometry_kernels: Any = field(repr=False)71_configuration: Any = field(repr=False)72_status: Any = field(repr=False)73_empty: Any = field(repr=False)74_reduction: ReductionTree | None = field(repr=False)75_empty_partials: Any = field(repr=False)76_recorded_tape: Any = field(default=None, repr=False)7778@property79def dtype(self) -> Any:80"""Declared depth and depth-cotangent storage; field storage stays FP32."""81return getattr(self.context.wp, self.spec.precision)8283@property84def device(self) -> Any:85return self.context.device8687@property88def stream(self) -> Any:89return self.context.stream9091@property92def scratch_bytes(self) -> int:93return 4 + (self._reduction.scratch_bytes if self._reduction is not None else 0)9495def clear_status(self) -> None:96with self.context.scope():97self._status.zero_()9899def check_status(self) -> None:100self.context.wp.synchronize_stream(self.stream)101code = int(self._status.numpy()[0])102if code & 1:103raise ContractError("projection input violates finite non-negative field or rigid pose")104if code:105raise NumericalError("non-finite projection or gradient; discard these outputs")106107def discard_recording(self, tape: Any) -> None:108"""Release a forward abandoned by tape.reset(), after discarding its graph."""109if self._recorded_tape is not None and self._recorded_tape is not tape:110raise ContractError("this workspace belongs to another tape")111if tape.launches:112raise ContractError("reset the abandoned tape before releasing its input lifetime")113self._recorded_tape = None114115116def prepare_projection(117grid: GridSpec,118geometry: DetectorGeometry,119spec: ProjectionSpec,120*,121device: str = "cuda:0",122stream: Any = None,123) -> ProjectionWorkspace:124"""Prepare fixed metadata and device scratch; never allocate image outputs."""125context = prepare_context(device=device, stream=stream)126wp = context.wp127kernels = load_kernels("dpt.kernels.projection")128geometry_kernels = load_kernels("dpt.kernels.geometry")129config = kernels.Configuration()130config.shape = wp.vec3i(grid.shape[2], grid.shape[1], grid.shape[0])131# diag(1/h) G^T: spacing follows grid axes, not object/world coordinates.132inverse_grid = tuple(133grid.orientation[3 * j + i] / grid.spacing_mm[i] for i in range(3) for j in range(3)134)135if not all(math.isfinite(value) for value in inverse_grid):136raise ContractError("grid spacing overflows the FP64 coordinate map")137for row in (0, geometry.shape[0] - 1):138for column in (0, geometry.shape[1] - 1):139endpoint = geometry.pixel_centre(row, column)140if not all(math.isfinite(value) for value in endpoint) or not math.isfinite(141math.dist(endpoint, geometry.source_mm)142):143raise ContractError("detector extent exceeds the FP64 finite-ray range")144config.object_to_grid = wp.mat33d(*inverse_grid)145config.grid_origin = wp.vec3d(*grid.origin_mm)146config.source = wp.vec3d(*geometry.source_mm)147config.detector_origin = wp.vec3d(*geometry.origin_mm)148config.column_step = wp.vec3d(*(x * geometry.spacing_mm[0] for x in geometry.u))149config.row_step = wp.vec3d(*(x * geometry.spacing_mm[1] for x in geometry.v))150config.width = geometry.shape[1]151config.pixels = geometry.pixels152config.samples = spec.samples_per_ray153reduction = None154with context.scope():155status = wp.zeros(1, dtype=wp.int32, device=context.device)156empty = wp.empty(0, dtype=wp.float32, device=context.device)157empty_partials = wp.empty(0, dtype=wp.float64, device=context.device)158if spec.active_pose:159reduction = prepare_reduction(context, geometry.pixels, tile=_BLOCK, components=12)160return ProjectionWorkspace(161grid,162geometry,163spec,164context,165kernels,166geometry_kernels,167config,168status,169empty,170reduction,171empty_partials,172)173174175def _inputs(mu: Any, pose: Any, workspace: ProjectionWorkspace, stream: Any) -> None:176ctx = workspace.context177ctx.assert_stream(stream)178ctx.array(mu, "mu", dtype=ctx.wp.float32, shape=(workspace.grid.voxels,))179ctx.array(pose, "pose", dtype=ctx.wp.float64, shape=(12,))180181182def _validate(mu: Any, pose: Any, workspace: ProjectionWorkspace, seed: Any = None) -> None:183wp = workspace.context.wp184workspace.clear_status()185wp.launch(186workspace._kernels.validate_nonnegative,187dim=workspace.grid.voxels,188inputs=[mu],189outputs=[workspace._status],190stream=workspace.stream,191record_tape=False,192)193wp.launch(194workspace._geometry_kernels.validate_pose,195dim=1,196inputs=[pose],197outputs=[workspace._status],198stream=workspace.stream,199record_tape=False,200)201if seed is not None:202wp.launch(203workspace._kernels.get_validate_finite(workspace.spec.precision == "float64"),204dim=workspace.geometry.pixels,205inputs=[seed],206outputs=[workspace._status],207stream=workspace.stream,208record_tape=False,209)210workspace.check_status()211212213# region book:projection-public-operator214def project_optical_depth(215mu: Any,216pose: Any,217*,218workspace: ProjectionWorkspace,219out_L: Any,220stream: Any = None,221tape: Any = None,222validate: bool = True,223) -> None:224"""Overwrite dimensionless optical depth for every finite detector ray.225226mu is a flat non-negative FP32 field in inverse mm. pose is twelve FP64227values (R_WO row-major, t_WO in mm). out_L is flat row-major detector228storage in the precision declared by ProjectionSpec. A checked call229synchronises domain diagnostics before writing and230range diagnostics afterwards. Unchecked calls promise valid current inputs231and require check_status() at the next acceptance checkpoint.232"""233ensure_tape(tape)234_inputs(mu, pose, workspace, stream)235if workspace._recorded_tape is not None:236raise ContractError("finish or discard the outstanding projection tape before reuse")237ctx = workspace.context238ctx.array(out_L, "out_L", dtype=workspace.dtype, shape=(workspace.geometry.pixels,))239ctx.disjoint([("mu", mu), ("pose", pose)], [("out_L", out_L)])240if tape is not None:241_check_tape_arrays(mu, pose, out_L, workspace)242if validate:243_validate(mu, pose, workspace)244ctx.wp.launch(245workspace._kernels.get_forward(246workspace.spec.precision == "float64", workspace.spec.integration == "cell_gauss"247),248dim=workspace.geometry.pixels,249inputs=[mu, pose, workspace._configuration],250outputs=[out_L, workspace._status],251stream=workspace.stream,252block_dim=_BLOCK,253record_tape=False,254)255if validate:256workspace.check_status()257if tape is not None:258_record(tape, mu, pose, out_L, workspace)259260261# endregion book:projection-public-operator262263264def _check_tape_arrays(265mu: Any, pose: Any, output: Any, workspace: ProjectionWorkspace266) -> list[Any]:267arrays = [output]268if workspace.spec.active_pose:269arrays.append(pose)270if workspace.spec.active_volume:271arrays.append(mu)272if len(arrays) == 1:273raise ContractError("recording requires at least one active input")274if any(array.grad is None for array in arrays):275raise ContractError("active tape arrays require requires_grad=True at allocation")276workspace.context.disjoint(277[("mu", mu), ("pose", pose), ("output", output)],278[("gradient", array.grad) for array in arrays],279)280return arrays281282283def _record(tape: Any, mu: Any, pose: Any, output: Any, workspace: ProjectionWorkspace) -> None:284arrays = _check_tape_arrays(mu, pose, output, workspace)285workspace._recorded_tape = tape286if workspace.context.wp.config.verify_autograd_array_access:287mu.mark_read()288pose.mark_read()289output.mark_write()290# Register fixed inputs too, so Tape.reset() clears debug read markers.291tape.record_launch(292workspace._kernels.get_dependency_marker(workspace.spec.precision == "float64"),293dim=0,294max_blocks=0,295inputs=[mu, pose],296outputs=[output],297device=workspace.device,298block_dim=_BLOCK,299)300301def backward() -> None:302# Array storage has twelve coordinates. Its cotangent is NOT the303# six-coordinate right tangent accepted by the standalone optimiser.304projection_vjp(305mu,306pose,307adj_L=output.grad,308workspace=workspace,309out_pose_matrix=pose.grad if workspace.spec.active_pose else None,310out_mu=mu.grad if workspace.spec.active_volume else None,311accumulate=True,312validate=False,313_from_tape=True,314)315with workspace.context.scope():316if not output.retain_grad:317output.grad.zero_()318workspace._recorded_tape = None319320tape.record_func(backward, arrays)321322323def projection_vjp(324mu: Any,325pose: Any,326*,327adj_L: Any,328workspace: ProjectionWorkspace,329out_pose: Any = None,330out_pose_matrix: Any = None,331out_mu: Any = None,332stream: Any = None,333validate: bool = True,334accumulate: bool = False,335_from_tape: bool = False,336) -> None:337"""First-order discrete VJP, with explicit tangent or storage coordinates.338339out_pose is six FP64 values for a *local right* SE(3) increment in mm/rad.340out_pose_matrix is twelve FP64 unconstrained storage partials, for a tape341chain rule. They are mutually exclusive. Convert local right gradients342using geometry.chart_gradient for a fixed-anchor optimisation chart.343out_mu is FP32 and uses non-deterministic-order scatter atomics. No grid,344acquisition, higher-order or non-smooth boundary derivative is promised.345"""346require_no_tape()347_inputs(mu, pose, workspace, stream)348if workspace._recorded_tape is not None and not _from_tape:349raise ContractError("the outstanding tape owns the projection inputs")350if out_pose is not None and out_pose_matrix is not None:351raise ContractError("request either a right-tangent or a storage pose gradient")352target = out_pose if out_pose is not None else out_pose_matrix353if target is None and out_mu is None:354raise ContractError("request at least one projection gradient")355if target is not None and not workspace.spec.active_pose:356raise ContractError("pose is fixed in this workspace")357if out_mu is not None and not workspace.spec.active_volume:358raise ContractError("volume is fixed in this workspace")359ctx, wp = workspace.context, workspace.context.wp360ctx.array(adj_L, "adj_L", dtype=workspace.dtype, shape=(workspace.geometry.pixels,))361components = 12 if out_pose_matrix is not None else 6362writes: list[tuple[str, Any]] = []363if target is not None:364ctx.array(target, "pose gradient", dtype=wp.float64, shape=(components,))365writes.append(("pose gradient", target))366if out_mu is not None:367ctx.array(out_mu, "out_mu", dtype=wp.float32, shape=(workspace.grid.voxels,))368writes.append(("out_mu", out_mu))369ctx.disjoint([("mu", mu), ("pose", pose), ("adj_L", adj_L)], writes)370if validate:371_validate(mu, pose, workspace, adj_L)372if out_mu is not None and not accumulate:373with ctx.scope():374out_mu.zero_()375count = (workspace.geometry.pixels + _BLOCK - 1) // _BLOCK376wp.launch_tiled(377workspace._kernels.get_vjp(378out_mu is not None,379components == 12,380target is not None,381double_seed=workspace.spec.precision == "float64",382cell_gauss=workspace.spec.integration == "cell_gauss",383),384dim=count,385inputs=[mu, pose, workspace._configuration, adj_L],386outputs=[387out_mu if out_mu is not None else workspace._empty,388workspace._reduction.partials[0]389if workspace._reduction is not None390else workspace._empty_partials,391workspace._status,392],393stream=workspace.stream,394device=workspace.device,395block_dim=_BLOCK,396record_tape=False,397)398if target is not None:399assert workspace._reduction is not None400workspace._reduction.finish(401count,402target,403workspace._status,404components=components,405accumulate=accumulate,406)407if out_mu is not None:408# Atomic accumulation may overflow after individually finite increments.409wp.launch(410workspace._kernels.validate_finite,411dim=workspace.grid.voxels,412inputs=[out_mu],413outputs=[workspace._status],414stream=workspace.stream,415record_tape=False,416)417if validate:418workspace.check_status()419420421# region book:projection-pose-sensitivities422def projection_pose_sensitivities(423mu: Any,424pose: Any,425depth_seeds: Any,426*,427out_jacobian: Any,428workspace: ProjectionWorkspace,429stream: Any = None,430validate: bool = True,431) -> None:432"""Export seeded per-ray local pose derivatives for bounded offline diagnostics.433434``out_jacobian[6*p + axis]`` is ``depth_seeds[p] * dL[p]/d(delta[axis])``435for ``pose @ exp(delta^)`` at zero, ordered tx, ty, tz, rx, ry, rz in mm/rad.436All arrays are caller-owned, contiguous and on the workspace's CUDA device:437seeds follow the declared depth precision, shape ``(pixels,)``; output is438FP64 ``(6*pixels,)``. Ones export439the optical-depth Jacobian; canonical transmission cotangents export the440corresponding signal sensitivities. Missed rays and zero seeds give zero.441442The existing per-ray VJP calculation writes before reduction in O(P S)443work, where S is samples per ray. This call allocates no device buffers and444performs no image transfers. Checked calls synchronise for input/output445status; unchecked calls require valid current inputs and a later446``workspace.check_status()``. The caller retains the O(6 P) diagnostic,447unlike production optimisation. Ambient tapes and outstanding projection448passes are rejected. Branch derivatives at knots/ties/grazing rays do not449assert smoothness. Only the current local right chart is differentiated.450"""451require_no_tape()452_inputs(mu, pose, workspace, stream)453if workspace._recorded_tape is not None:454raise ContractError("the outstanding tape owns the projection inputs")455if not workspace.spec.active_pose:456raise ContractError("pose is fixed in this workspace")457ctx, wp = workspace.context, workspace.context.wp458pixels = workspace.geometry.pixels459ctx.array(depth_seeds, "depth_seeds", dtype=workspace.dtype, shape=(pixels,))460ctx.array(out_jacobian, "out_jacobian", dtype=wp.float64, shape=(6 * pixels,))461ctx.disjoint(462[("mu", mu), ("pose", pose), ("depth_seeds", depth_seeds)],463[("out_jacobian", out_jacobian)],464)465if validate:466_validate(mu, pose, workspace, depth_seeds)467wp.launch_tiled(468workspace._kernels.get_vjp(469False,470False,471True,472per_ray=True,473double_seed=workspace.spec.precision == "float64",474cell_gauss=workspace.spec.integration == "cell_gauss",475),476dim=(pixels + _BLOCK - 1) // _BLOCK,477inputs=[mu, pose, workspace._configuration, depth_seeds],478outputs=[workspace._empty, out_jacobian, workspace._status],479stream=workspace.stream,480device=workspace.device,481block_dim=_BLOCK,482record_tape=False,483)484if validate:485workspace.check_status()486487488# endregion book:projection-pose-sensitivities489