Generated from the full canonical file for this source snapshot. Line numbers match the library source.
Source SHA256: 8f4bd863f8dd07a88402d6e26ca441cdc1620921dff2ae8ad7421c72a9ae972a
1"""Fixed-density material-fraction paths using the canonical sampled-field projector.23Fields are material-major, flat FP32 arrays with M*V values; paths have M*P4values in millimetres, using the supplied ProjectionSpec precision for both5paths and their cotangents. Field-gradient storage remains FP32.6Each basis field is a volume fraction. Fractions are7non-negative and sum to at most one at every sample; the remainder is vacuum.8The FP64 sum admits 2^-24 excess for independently rounded binary32 fractions.9The explicit nonnegative domain instead accepts dimensionless equivalent-basis10fields without a sum cap. It uses the same integration and derivatives; basis11scales belong in the supplied attenuation coefficients. There is no hidden12normalisation or inferred CT-to-material conversion.13"""1415from __future__ import annotations1617from dataclasses import dataclass, field18from typing import Any, Literal1920from dpt._runtime import DeviceContext, load_kernels, require_no_tape21from dpt.contracts import ContractError, integer22from dpt.geometry import DetectorGeometry23from dpt.projection import ProjectionSpec, ProjectionWorkspace, prepare_projection24from dpt.projection import project_optical_depth as _project25from dpt.projection import projection_vjp as _vjp26from dpt.volumes import GridSpec272829@dataclass(slots=True)30class MaterialProjectionWorkspace:31"""Prepared views retain their caller-owned parent buffers for the whole solve.3233M serial launches reuse a single projection workspace, avoiding duplicated34ray geometry and quadrature code. This is an execution candidate, not a35measured throughput claim. No per-evaluation views or device arrays are made.36"""3738projection: ProjectionWorkspace39materials: int40fields: Any41paths: Any42adj_paths: Any43grad_fields: Any44field_views: tuple[Any, ...]45path_views: tuple[Any, ...]46seed_views: tuple[Any, ...]47gradient_views: tuple[Any, ...]48kernels: Any = field(repr=False)49status: Any = field(repr=False)50field_domain: Literal["fractions", "nonnegative"] = "fractions"5152@property53def context(self) -> DeviceContext:54return self.projection.context5556@property57def scratch_bytes(self) -> int:58return self.projection.scratch_bytes + 45960def clear_status(self) -> None:61self.projection.clear_status()62with self.context.scope():63self.status.zero_()6465def check_status(self) -> None:66self.projection.check_status()67if int(self.status.numpy()[0]):68name = "fractions" if self.field_domain == "fractions" else "nonnegative fields"69raise ContractError(f"invalid material {name} or path cotangents")7071def validate_inputs(self, *, seeds: bool = False, stream: Any = None) -> None:72"""Check current bound fractions and optional cotangents without projecting.7374Fraction-domain roundoff permits a total up to 1 + 2^-24. Nonnegative75fields have no sum cap. Values are never changed. This checks no pose76and does not certify later field mutations.77"""78require_no_tape()79self.context.assert_stream(stream)80if seeds and self.adj_paths is None:81raise ContractError("material validation requires a bound path cotangent")82_validate(self, seeds=seeds)838485def prepare_material_projection(86grid: GridSpec,87geometry: DetectorGeometry,88spec: ProjectionSpec,89*,90materials: int,91fields: Any,92out_paths: Any,93adj_paths: Any = None,94out_fields: Any = None,95device: str = "cuda:0",96stream: Any = None,97field_domain: Literal["fractions", "nonnegative"] = "fractions",98) -> MaterialProjectionWorkspace:99"""Bind fields; nonnegative equivalent-basis fields have no fraction sum cap.100101Both domains use dimensionless fields, paths in mm and supplied basis102attenuation in mm^-1. The caller records any mass-density normalisation.103``spec.precision`` selects FP32 or FP64 paths and path cotangents; fields104and their accumulated volume gradients remain FP32.105"""106if field_domain not in ("fractions", "nonnegative"):107raise ContractError("field_domain must be fractions or nonnegative")108integer(materials, "materials", minimum=1, maximum=32)109integer(materials * grid.voxels, "material field length", minimum=1)110integer(materials * geometry.pixels, "material path length", minimum=1)111projection = prepare_projection(grid, geometry, spec, device=device, stream=stream)112ctx, wp = projection.context, projection.context.wp113ctx.array(fields, "fields", dtype=wp.float32, shape=(materials * grid.voxels,))114path_dtype = wp.float64 if spec.precision == "float64" else wp.float32115ctx.array(out_paths, "out_paths", dtype=path_dtype, shape=(materials * geometry.pixels,))116reads = [("fields", fields)]117writes = [("out_paths", out_paths)]118if adj_paths is not None:119ctx.array(adj_paths, "adj_paths", dtype=path_dtype, shape=(materials * geometry.pixels,))120reads.append(("adj_paths", adj_paths))121if out_fields is not None:122if not spec.active_volume:123raise ContractError("material fields are fixed in this projection specification")124if adj_paths is None:125raise ContractError("material field gradients require a bound path cotangent")126ctx.array(out_fields, "out_fields", dtype=wp.float32, shape=(materials * grid.voxels,))127writes.append(("out_fields", out_fields))128ctx.disjoint(reads, writes)129with ctx.scope():130status = wp.zeros(1, dtype=wp.int32, device=ctx.device)131field_views = tuple(fields[m * grid.voxels : (m + 1) * grid.voxels] for m in range(materials))132path_views = tuple(133out_paths[m * geometry.pixels : (m + 1) * geometry.pixels] for m in range(materials)134)135seeds = (136tuple(adj_paths[m * geometry.pixels : (m + 1) * geometry.pixels] for m in range(materials))137if adj_paths is not None138else ()139)140gradients = (141tuple(out_fields[m * grid.voxels : (m + 1) * grid.voxels] for m in range(materials))142if out_fields is not None143else ()144)145return MaterialProjectionWorkspace(146projection,147materials,148fields,149out_paths,150adj_paths,151out_fields,152field_views,153path_views,154seeds,155gradients,156load_kernels("dpt.kernels.material_projection"),157status,158field_domain,159)160161162def _validate(workspace: MaterialProjectionWorkspace, *, seeds: bool) -> None:163ctx, projection = workspace.context, workspace.projection164workspace.clear_status()165ctx.wp.launch(166workspace.kernels.validate_fractions167if workspace.field_domain == "fractions"168else workspace.kernels.validate_nonnegative,169dim=projection.grid.voxels,170inputs=[workspace.fields, workspace.materials, projection.grid.voxels],171outputs=[workspace.status],172stream=ctx.stream,173record_tape=False,174)175if seeds:176ctx.wp.launch(177workspace.kernels.validate_seeds(projection.spec.precision),178dim=workspace.materials * projection.geometry.pixels,179inputs=[workspace.adj_paths],180outputs=[workspace.status],181stream=ctx.stream,182record_tape=False,183)184workspace.check_status()185186187# region book:material-path-composition188def project_material_paths(189pose: Any,190*,191workspace: MaterialProjectionWorkspace,192stream: Any = None,193validate: bool = True,194) -> None:195"""Overwrite material-major path lengths in mm, using one canonical projector.196197All material data stay on CUDA. The host loop schedules M operations and198never visits a voxel, pixel or quadrature sample. Explicit VJPs compose199with spectral_signal; ambient tape recording is rejected.200"""201require_no_tape()202ctx, projection = workspace.context, workspace.projection203ctx.assert_stream(stream)204ctx.array(pose, "pose", dtype=ctx.wp.float64, shape=(12,))205ctx.disjoint([("pose", pose)], [("paths", workspace.paths)])206if validate:207workspace.validate_inputs(stream=ctx.stream)208for material in range(workspace.materials):209_project(210workspace.field_views[material],211pose,212workspace=projection,213out_L=workspace.path_views[material],214stream=ctx.stream,215validate=validate and material == 0,216)217if validate:218workspace.check_status()219220221# endregion book:material-path-composition222223224def material_projection_vjp(225pose: Any,226*,227workspace: MaterialProjectionWorkspace,228out_pose: Any = None,229stream: Any = None,230validate: bool = True,231accumulate: bool = False,232) -> None:233"""Accumulate material cotangents into one local-right six-coordinate pose VJP.234235The optional bound field gradient is an ambient derivative. Feasible236fraction perturbations must still respect non-negativity and the simplex;237an optimiser must provide its own declared constrained parameterisation.238Field scatter order follows the canonical projector's atomic contract.239"""240require_no_tape()241ctx, projection = workspace.context, workspace.projection242ctx.assert_stream(stream)243ctx.array(pose, "pose", dtype=ctx.wp.float64, shape=(12,))244if not workspace.seed_views:245raise ContractError("prepare a path cotangent before requesting material VJPs")246writes: list[tuple[str, Any]] = []247if out_pose is not None:248if not projection.spec.active_pose:249raise ContractError("pose is fixed in this projection specification")250ctx.array(out_pose, "out_pose", dtype=ctx.wp.float64, shape=(6,))251writes.append(("out_pose", out_pose))252if workspace.grad_fields is not None:253writes.append(("out_fields", workspace.grad_fields))254if not writes:255raise ContractError("request a pose or material field gradient")256ctx.disjoint(257[258("fields", workspace.fields),259("paths", workspace.paths),260("adj_paths", workspace.adj_paths),261("pose", pose),262],263writes,264)265if validate:266workspace.validate_inputs(seeds=True, stream=ctx.stream)267for material in range(workspace.materials):268# Pose contributions share one destination; distinct field slices must269# each honour the caller's overwrite/accumulate request independently.270field_gradient = workspace.gradient_views[material] if workspace.gradient_views else None271if out_pose is not None and field_gradient is not None and material > 0 and not accumulate:272with ctx.scope():273field_gradient.zero_()274_vjp(275workspace.field_views[material],276pose,277adj_L=workspace.seed_views[material],278workspace=projection,279out_pose=out_pose,280out_mu=field_gradient,281stream=ctx.stream,282validate=validate and material == 0,283accumulate=accumulate or (out_pose is not None and material > 0),284)285if validate:286workspace.check_status()287