Generated from the full canonical file for this source snapshot. Line numbers match the library source.
Source SHA256: 4ac2635c0de25033ce65de630179eb9fc1811bef49a15fbe13dba416c5c22695
1"""Fit one rigid pose to supplied calibrated primary-count radiographs.23All image formation and derivatives use the canonical CUDA operators. NumPy4handles supplied-file validation and final reports, never an image optimiser.5See the examples README for the provenance-bearing JSON/NPY input contract6and the separately recorded registration study.7"""89from __future__ import annotations1011import importlib12import math13import re14from dataclasses import asdict, dataclass15from typing import Any, cast1617from dpt.contracts import ContractError, finite_scalar, integer18from dpt.examples._common import example_parser, load_case, write_array19from dpt.geometry import RigidTransform, Vector320from dpt.objectives import ObjectiveSpec21from dpt.recovery import PoseChart, PrimaryPoseEvaluator, PrimaryPoseProblem22from dpt.registration import Evaluation, RecoveryPolicy, Vector, recover_parameters23from dpt.validation.recovery import pose_error24from dpt.volumes import GridSpec252627def _mapping(value: Any, name: str) -> dict[str, Any]:28if not isinstance(value, dict):29raise ContractError(f"{name} must be a JSON object; see the examples README")30return cast(dict[str, Any], value)313233def _required(values: dict[str, Any], name: str) -> Any:34if name not in values:35raise ContractError(f"registration configuration is missing {name!r}")36return values[name]373839def _immutable_geometry(values: dict[str, Any]) -> dict[str, Any]:40"""Decode JSON arrays at the immutable host-geometry boundary."""41return {42key: tuple(cast(list[Any], value)) if isinstance(value, list) else value43for key, value in values.items()44}454647@dataclass(frozen=True, slots=True)48class PreparedView:49identifier: str50evaluator: PrimaryPoseEvaluator51included_pixels: int525354# region book:example-registration-shared-objective55class SharedPoseObjective:56"""Sum views in one immutable chart; each evaluator owns its image scratch."""5758def __init__(self, views: tuple[PreparedView, ...], chart: PoseChart) -> None:59if not views or any(view.evaluator.chart is not chart for view in views):60raise ContractError("all views must share the same PoseChart object")61self.views, self.chart = views, chart6263def __call__(self, parameters: Vector) -> Evaluation:64values = tuple(view.evaluator(parameters) for view in self.views)65return Evaluation(66math.fsum(value.loss for value in values),67tuple(math.fsum(value.gradient[k] for value in values) for k in range(6)),68)697071# endregion book:example-registration-shared-objective727374def prepare_views(75case: Any, configuration: dict[str, Any], chart: PoseChart, device: str76) -> tuple[PreparedView, ...]:77"""Validate supplied arrays and upload fixed inputs once on a shared stream."""78from dpt.geometry import DetectorGeometry7980wp: Any = importlib.import_module("warp")81np: Any = importlib.import_module("numpy")82grid = GridSpec(83**_immutable_geometry(_mapping(_required(configuration, "grid"), "registration.grid"))84)85attenuation_host = case.array(86_required(configuration, "attenuation"), shape=grid.shape, units="mm^-1"87)88if not bool(np.all(np.isfinite(attenuation_host) & (attenuation_host >= 0))):89raise ContractError("attenuation must contain finite nonnegative values in mm^-1")90samples = integer(_required(configuration, "samples_per_ray"), "samples_per_ray", minimum=1)91raw_views = _required(configuration, "views")92if not isinstance(raw_views, list) or not raw_views:93raise ContractError("registration.views needs at least one calibrated count view")94views: list[dict[str, Any]] = [95_mapping(value, "registration.views entry") for value in cast(list[Any], raw_views)96]97identifiers: set[str] = set()98prepared: list[PreparedView] = []99with wp.ScopedDevice(device):100stream = wp.Stream(device=device)101with wp.ScopedStream(stream):102try:103attenuation = wp.array(104attenuation_host.reshape(-1), dtype=wp.float32, device=device105)106for view in views:107identifier = _required(view, "id")108if not isinstance(identifier, str) or not re.fullmatch(109r"[A-Za-z0-9_-]+", identifier110):111raise ContractError(112"view id must use letters, digits, underscores or hyphens"113)114if identifier in identifiers:115raise ContractError(116f"duplicate view id {identifier!r}; give each view its own id"117)118identifiers.add(identifier)119geometry = DetectorGeometry(120**_immutable_geometry(121_mapping(_required(view, "geometry"), "view.geometry")122)123)124observed = case.array(125_required(view, "observation"), shape=geometry.shape, units="counts"126)127mask = case.array(128_required(view, "mask"), shape=geometry.shape, units="dimensionless"129)130valid_counts = np.isfinite(observed) & (observed >= 0) & (observed <= 2**24)131if not bool(np.all(valid_counts & (observed == np.floor(observed)))):132raise ContractError(133f"view {identifier}: counts must be integers in [0, 2**24], stored as "134"float32; processed display images cannot use this Poisson example"135)136if not bool(np.all((mask == 0) | (mask == 1))) or not bool(np.any(mask == 1)):137raise ContractError(138f"view {identifier}: mask must be binary with included pixels"139)140if bool(np.any((mask == 0) & (observed != 0))):141raise ContractError(142f"view {identifier}: set every mask=0 observation to zero in the "143"supplied preprocessed array; preserve the raw source and record "144"this preprocessing in its provenance"145)146beam = finite_scalar(_required(view, "open_beam_counts"), "open_beam_counts")147if beam <= 0:148raise ContractError(f"view {identifier}: open_beam_counts must be positive")149# region book:example-registration-prepare-view150problem = PrimaryPoseProblem(151grid=grid,152geometry=geometry,153attenuation=attenuation,154observation=wp.array(observed.reshape(-1), dtype=wp.float32, device=device),155objective=ObjectiveSpec(156kind="poisson", domain="counts", reduction="sum", weighted=True157),158open_beam=beam,159weights=wp.array(mask.reshape(-1), dtype=wp.float32, device=device),160samples_per_ray=samples,161precision=configuration.get("precision", "float64"),162integration=configuration.get("integration", "midpoint"),163)164evaluator = PrimaryPoseEvaluator(problem, chart, device=device, stream=stream)165# endregion book:example-registration-prepare-view166prepared.append(167PreparedView(identifier, evaluator, int(np.count_nonzero(mask)))168)169finally:170# Drain uploads and setup work before their owners can be released.171wp.synchronize_stream(stream)172return tuple(prepared)173174175def evaluate_reference(case: Any, configuration: dict[str, Any], recovered: RigidTransform) -> Any:176"""Read the optional reference only after the accepted pose has been fixed."""177if "evaluation" not in configuration:178return {"status": "no independent geometric reference supplied"}179evaluation = _mapping(configuration["evaluation"], "registration.evaluation")180for key in ("source", "uncertainty"):181if not isinstance(evaluation.get(key), str) or not evaluation[key].strip():182raise ContractError(f"evaluation.{key} must describe the reference and its limits")183reference = RigidTransform(184**_immutable_geometry(_mapping(_required(evaluation, "reference_pose"), "reference_pose"))185)186landmarks = case.array(_required(evaluation, "landmarks"), dtype="float64", units="mm")187np: Any = importlib.import_module("numpy")188if landmarks.ndim != 2 or landmarks.shape[1] != 3 or landmarks.shape[0] < 1:189raise ContractError("evaluation landmarks must have shape (N, 3), with N >= 1")190if not bool(np.all(np.isfinite(landmarks))):191raise ContractError(192"evaluation landmarks must contain finite object-frame coordinates in mm"193)194points = [cast(Vector3, tuple(float(value) for value in point)) for point in landmarks]195return {196"status": "evaluated against supplied reference",197"source": evaluation["source"],198"uncertainty": evaluation["uncertainty"],199"reference_pose": asdict(reference),200"errors": asdict(pose_error(recovered, reference, points)),201}202203204def main() -> None:205parser = example_parser("Register a known attenuation volume to supplied primary-count views.")206args = parser.parse_args()207case = load_case(args.case)208configuration = _mapping(case.config.get("registration"), "registration")209chart_values = dict(_mapping(_required(configuration, "chart"), "registration.chart"))210anchor = RigidTransform(211**_immutable_geometry(_mapping(_required(chart_values, "anchor"), "chart.anchor"))212)213chart_values["anchor"] = anchor214_required(chart_values, "scales")215chart = PoseChart(**chart_values)216policy = RecoveryPolicy(**_mapping(_required(configuration, "policy"), "registration.policy"))217with case.record(218args.output,219entrypoint=__file__,220metadata={221"application": "registration",222"device_requested": args.device,223"measurement_model": "independent primary counts with scalar open beam per view",224"objective": "sum of masked Poisson half-deviances",225"evaluation_reference_used_for_fit": False,226},227) as run:228views = prepare_views(case, configuration, chart, args.device)229objective = SharedPoseObjective(views, chart)230run.set_metadata(device=str(views[0].evaluator.context.device))231# region book:example-registration-solve232initial: Vector = (0.0,) * 6233result = recover_parameters(objective, initial, policy=policy)234recovered = chart.pose(result.parameters)235run.write_json(236"optimisation.json",237{238"pose_object_to_world": asdict(recovered),239"chart": asdict(chart),240"policy": asdict(policy),241"precision": views[0].evaluator.problem.precision,242"integration": views[0].evaluator.problem.integration,243"result": asdict(result),244"stationary": result.stationary,245},246)247# endregion book:example-registration-solve248final_losses: dict[str, float] = {}249# region book:example-registration-export250for view in views:251evaluator = view.evaluator252# A rejected line-search trial may be the last occupant of scratch.253final_value = evaluator(result.parameters)254final_losses[view.identifier] = final_value.loss255prediction = evaluator.prediction.numpy().reshape(evaluator.problem.geometry.shape)256write_array(run, f"prediction-{view.identifier}.npy", prediction)257run.write_json(258"fit-report.json",259{260"final_half_deviance_by_view": final_losses,261"included_pixels_by_view": {v.identifier: v.included_pixels for v in views},262"solver_evaluations": result.evaluations,263# A rejected trial can fail before some view evaluators are called.264"solver_view_evaluation_upper_bound": result.evaluations * len(views),265"additional_final_export_evaluations": len(views),266"evaluation": evaluate_reference(case, configuration, recovered),267},268)269# endregion book:example-registration-export270271272if __name__ == "__main__":273main()274