Generated from the full canonical file for this source snapshot. Line numbers match the library source.
Source SHA256: 98b3fb34a3384ab52653fef356b849add27e77cab5c58a17d12de6547c749771
1"""Complete a CT-derived registration and then select, observe and fit a new view.23All projection, derivatives and optimisation use the canonical dpt CUDA code.4The known case is deliberately a teaching example, not a held-out clinical study.5Future noisy observations are generated only after the view decision is saved.6"""78from __future__ import annotations910import argparse11import importlib.util12import itertools13import json14import math15import sys16import time17from dataclasses import asdict18from pathlib import Path1920import numpy as np2122from dpt.experiments import RunRecorder, experiment_sources, private_output, repository_root23from dpt.geometry import RigidTransform, compose_pose24from dpt.projection import ProjectionSpec, prepare_projection, project_optical_depth25from dpt.validation.projection import integrate_sampled_field26from dpt.validation.recovery import detector_coordinate, pose_error2728# Reuse the supplied-input experiment orchestration; there is no second solver.29APPLICATION = Path(__file__).resolve().parents[1] / "application-study"30sys.path.insert(0, str(APPLICATION))31from _study import ( # noqa: E40232checked_array,33digest,34geometry,35grid_spec,36keyed_rng,37read_json,38registration_case,39rigid,40save_array,41)42from batch import rank_design # noqa: E402434445def _load_fit():46# This entrypoint is itself called run.py: load its sibling driver by path.47spec = importlib.util.spec_from_file_location("application_fit_driver", APPLICATION / "run.py")48assert spec is not None and spec.loader is not None49module = importlib.util.module_from_spec(spec)50spec.loader.exec_module(module)51return module.fit_case525354def validate_protocol(protocol):55"""Reject changes that would invalidate this experiment's shared contracts."""56acquisition = protocol["acquisition"]57angles = acquisition["candidate_angles_degrees"]58if protocol["task_coordinates_mm"] != [-40.0, 40.0]:59raise ValueError("the reused design driver requires the eight ±40-mm task corners")60if acquisition["base_view_degrees"] != 0.0:61raise ValueError("this worked sequence starts from the declared zero-degree base view")62if (63not angles64or any(not math.isfinite(a) or a != round(a) for a in angles)65or len(set(angles)) != len(angles)66or acquisition["fixed_view_degrees"] not in angles67):68raise ValueError(69"candidate angles must be distinct integral degrees including the baseline"70)71if protocol["randomness"]["replicates"] != 8:72raise ValueError("the paired summary is declared for eight independent replicates")73if (74protocol["numerics"]["policy"]["gradient_tolerance"]75!= protocol["acceptance"]["gradient_tolerance"]76):77raise ValueError("the solver and acceptance gradient thresholds must agree")78if set(protocol["held_out_views_degrees"]) & {790.0,80*angles,81*protocol["registration"]["views_degrees"],82}:83raise ValueError("reserved views must be disjoint from every fitting candidate")848586def _project(wp, field_device, grid, protocol, angle, pose, samples, device, *, generating=False):87geom = geometry(protocol, angle)88numerics = protocol["numerics"]89precision = numerics["generation_precision" if generating else "precision"]90integration = numerics["generation_integration" if generating else "integration"]91workspace = prepare_projection(92grid,93geom,94ProjectionSpec(samples, precision=precision, integration=integration),95device=device,96)97output = wp.empty(geom.pixels, dtype=workspace.dtype, device=device)98packed = wp.array(pose.packed(), dtype=wp.float64, device=device)99project_optical_depth(field_device, packed, workspace=workspace, out_L=output)100return output.numpy().reshape(geom.shape).astype(np.float64)101102103def _sampling(wp, field, field_device, grid, protocol, target, angles, device):104"""Qualify every prospective angle before producing any noisy data."""105shape = protocol["geometry"]["detector_shape_hw"]106rows = tuple(min(shape[0] - 1, round(shape[0] * x)) for x in (0.125, 0.375, 0.625, 0.875))107cols = tuple(min(shape[1] - 1, round(shape[1] * x)) for x in (0, 0.25, 0.4375, 0.5, 0.75, 1))108pixels = list(itertools.product(rows, cols))109values = field.ravel().tolist()110means = {}111diagnostics = []112cfg = protocol["numerics"]113beam = cfg["sampling_gate_open_beam_per_pixel"]114for angle in angles:115geom = geometry(protocol, angle)116corners = [117grid.grid_to_object(tuple(index))118for index in itertools.product(119(-0.5, grid.shape[2] - 0.5),120(-0.5, grid.shape[1] - 0.5),121(-0.5, grid.shape[0] - 0.5),122)123]124coverage = [detector_coordinate(geom, target.point(corner)) for corner in corners]125if any(126not (-0.5 <= c <= geom.shape[1] - 0.5 and -0.5 <= r <= geom.shape[0] - 0.5)127for c, r in coverage128):129raise ValueError("the detector does not cover the declared transformed field")130generated = _project(131wp,132field_device,133grid,134protocol,135angle,136target,137cfg["generation_samples_per_ray"],138device,139generating=True,140)141fitted = _project(142wp,143field_device,144grid,145protocol,146angle,147target,148cfg["fitting_samples_per_ray"],149device,150)151exact = np.array(152[153integrate_sampled_field(154values, grid, geometry(protocol, angle), target, r, c, validate=False155)156for r, c in pixels157]158)159exact_mean = beam * np.exp(-exact)160independent_error = np.abs(161beam * np.exp(-np.array([generated[r, c] for r, c in pixels])) - exact_mean162) / np.sqrt(exact_mean)163fitting_independent_error = np.abs(164beam * np.exp(-np.array([fitted[r, c] for r, c in pixels])) - exact_mean165) / np.sqrt(exact_mean)166reference_mean = beam * np.exp(-generated)167full_error = np.abs(beam * np.exp(-fitted) - reference_mean) / np.sqrt(reference_mean)168row = {169"angle_degrees": angle,170"projected_support_corners_column_row": coverage,171"pixels": pixels,172"independent_exact_depths": exact.tolist(),173"generation_independent_p95_sd": float(np.quantile(independent_error, 0.95)),174"generation_independent_max_sd": float(independent_error.max()),175"generation_integration": cfg["generation_integration"],176"fitting_integration": cfg["integration"],177"fitting_independent_p95_sd": float(np.quantile(fitting_independent_error, 0.95)),178"fitting_independent_max_sd": float(fitting_independent_error.max()),179"fitting_full_image_p95_sd": float(np.quantile(full_error, 0.95)),180"fitting_full_image_max_sd": float(full_error.max()),181}182row["passed"] = (183max(184row["generation_independent_p95_sd"],185row["fitting_independent_p95_sd"],186row["fitting_full_image_p95_sd"],187)188<= cfg["sampling_error_poisson_sd_p95_max"]189and max(190row["generation_independent_max_sd"],191row["fitting_independent_max_sd"],192row["fitting_full_image_max_sd"],193)194<= cfg["sampling_error_poisson_sd_max"]195)196diagnostics.append(row)197means[angle] = np.exp(-generated)198print(f"sampling angle={angle:g} passed={row['passed']}", flush=True)199return means, diagnostics200201202def _observation(record, public, means, protocol, angle, beam, role, replicate, seed):203code = round(angle) + 180204identifier = f"r{role}-n{replicate:02d}-a{code:03d}"205if identifier in public["views"]:206return identifier207counts64 = keyed_rng(seed, role, replicate, code).poisson(beam * means[angle])208if counts64.max() > 2**24:209raise ValueError("generated counts exceed exact binary32 integers")210counts = counts64.astype(np.float32)211mask = np.ones(counts.shape, dtype=np.float32)212observation = save_array(213record.output,214f"observations/{identifier}.npy",215counts,216units="counts",217role="simulated fitting observation",218recorder=record,219)220mask_record = save_array(221record.output,222f"masks/{identifier}.npy",223mask,224units="dimensionless",225role="fixed binary fitting mask",226recorder=record,227)228public["views"][identifier] = {229"angle_degrees": angle,230"geometry": asdict(geometry(protocol, angle)),231"open_beam_counts": beam,232"observation": observation,233"mask": mask_record,234}235return identifier236237238def _fit(known, output, public, protocol, name, identifiers, anchor, device):239case_path = output / "cases" / f"{name}.json"240registration_case(known, output, public, protocol, identifiers, anchor, case_path)241fit_case = _load_fit()242result = fit_case(243case_path,244output / "fits" / name,245device=device,246deadline=time.perf_counter() + protocol["numerics"]["solve_soft_seconds"],247)248print(249f"fit {name}: {result['result']['reason']} "250f"gradient={max(map(abs, result['result']['evaluation']['gradient'])):.6g}",251flush=True,252)253return result254255256def _assess(wp, field_device, grid, protocol, means, target, outcome, points, device):257recovered = rigid(outcome["pose_object_to_world"])258errors = asdict(pose_error(recovered, target, tuple(map(tuple, points))))259initial = asdict(260pose_error(rigid(outcome["chart"]["anchor"]), target, tuple(map(tuple, points)))261)262reserved = []263beam = protocol["acquisition"]["base_and_candidate_open_beam_per_pixel"]264for angle in protocol["held_out_views_degrees"]:265depth = _project(266wp,267field_device,268grid,269protocol,270angle,271recovered,272protocol["numerics"]["fitting_samples_per_ray"],273device,274)275expected = beam * means[angle]276discrepancy = (beam * np.exp(-depth) - expected) / np.sqrt(expected)277reserved.append(278{279"angle_degrees": angle,280"expected_error_poisson_sd_rms": float(np.sqrt(np.mean(discrepancy**2))),281}282)283gate = protocol["acceptance"]284# pose_error exposes radians; the protocol intentionally uses reader-facing degrees.285geometric = (286errors["landmark_rms_mm"] <= gate["rms_probe_error_mm_max"]287and errors["landmark_max_mm"] <= gate["maximum_probe_error_mm_max"]288and math.degrees(errors["rotation_radians"]) <= gate["rotation_error_degrees_max"]289)290return {291"stationary": outcome["stationary"],292"termination": outcome["result"]["reason"],293"errors": errors,294"initial_errors": initial,295"held_out": reserved,296"geometric_pass": geometric,297"held_out_pass": all(298x["expected_error_poisson_sd_rms"] <= gate["held_out_expected_error_poisson_sd_rms_max"]299for x in reserved300),301}302303304def execute(known_root, output, config, device, development=False):305import warp as wp306307wp.init()308known_root = known_root.resolve()309protocol = read_json(config)310validate_protocol(protocol)311known = read_json(known_root / "known.json")312if (313known["case"] != protocol["case"]314or known["attenuation"]["sha256"] != protocol["known_attenuation_sha256"]315):316raise ValueError("this worked example requires its declared CT-derived field")317field = checked_array(known_root, known["attenuation"])318grid = grid_spec(known["grid"])319target = compose_pose(RigidTransform(), tuple(protocol["generating_local_se3_mm_rad"]))320points = list(itertools.product(protocol["task_coordinates_mm"], repeat=3))321angles = sorted(322set(323[324*protocol["registration"]["views_degrees"],325protocol["acquisition"]["base_view_degrees"],326*protocol["acquisition"]["candidate_angles_degrees"],327*protocol["held_out_views_degrees"],328]329)330)331replicates = 1 if development else protocol["randomness"]["replicates"]332seed = protocol["randomness"]["development_root_seed" if development else "root_seed"]333sources = experiment_sources(334__file__,335config,336extra={337"known.json": known_root / "known.json",338"known-attenuation.npy": known_root / known["attenuation"]["file"],339**{f"application-study/{p.name}": p for p in APPLICATION.glob("*.py")},340},341)342output = private_output(output, repository_root(__file__))343with RunRecorder(344output,345configuration={346"protocol": protocol,347"development": development,348"replicates": replicates,349"root_seed": seed,350"scope": "CT-derived simulated teaching example; not clinical validation",351},352sources=sources,353) as record:354record.set_metadata(device=str(wp.get_device(device)), warp=wp.__version__)355with wp.ScopedDevice(device):356field_device = wp.array(field.ravel(), dtype=wp.float32, device=device)357means, sampling = _sampling(358wp, field, field_device, grid, protocol, target, angles, device359)360record.write_json("sampling.json", sampling)361if not all(x["passed"] for x in sampling):362raise ValueError("prospective quadrature checks failed; no observations generated")363record.write_json(364"evaluation-reference.json",365{366"pose_object_to_world": asdict(target),367"task_points_object_mm": points,368"role": "generation/evaluation only; not supplied to fit or selector",369},370)371public = {372"known_sha256": digest(known_root / "known.json"),373"views": {},374"effective_fitting_samples": protocol["numerics"]["fitting_samples_per_ray"],375}376(output / "cases").mkdir()377(output / "fits").mkdir()378reg_ids = [379_observation(380record,381public,382means,383protocol,384a,385protocol["registration"]["open_beam_per_view"],3861,3870,388seed,389)390for a in protocol["registration"]["views_degrees"]391]392# region book:worked-registration393registration = _fit(394known_root,395output,396public,397protocol,398"registration",399reg_ids,400RigidTransform(),401device,402)403# endregion book:worked-registration404results = []405# region book:worked-application-sequence406for replicate in range(replicates):407beam = protocol["acquisition"]["base_and_candidate_open_beam_per_pixel"]408base_id = _observation(409record, public, means, protocol, 0.0, beam, 2, replicate, seed410)411base = _fit(412known_root,413output,414public,415protocol,416f"base-{replicate:02d}",417[base_id],418RigidTransform(),419device,420)421accepted = rigid(base["pose_object_to_world"])422decision_path = output / f"decision-{replicate:02d}"423selection = rank_design(424known_root,425output,426public,427protocol,428{429"angles": protocol["acquisition"]["candidate_angles_degrees"],430},431accepted,432decision_path,433device,434)435# Selection is on disk before either future count image exists.436record.write_json(f"decision-{replicate:02d}-binding.json", selection)437pair = {"replicate": replicate, "base": base, "selection": selection}438for policy, angle in (439("selected", selection["selected_angle"]),440("near_parallel", protocol["acquisition"]["fixed_view_degrees"]),441):442future_id = _observation(443record, public, means, protocol, angle, beam, 3, replicate, seed444)445pair[policy] = _fit(446known_root,447output,448public,449protocol,450f"{policy}-{replicate:02d}",451[base_id, future_id],452accepted,453device,454)455results.append(pair)456# endregion book:worked-application-sequence457record.write_json("fitting-observations.json", public)458evaluations = {459"registration": _assess(460wp, field_device, grid, protocol, means, target, registration, points, device461),462"pairs": [],463}464for pair in results:465evaluations["pairs"].append(466{467"replicate": pair["replicate"],468"selection": pair["selection"],469**{470policy: _assess(471wp,472field_device,473grid,474protocol,475means,476target,477pair[policy],478points,479device,480)481for policy in ("base", "selected", "near_parallel")482},483}484)485selected = np.array(486[p["selected"]["errors"]["landmark_rms_mm"] ** 2 for p in evaluations["pairs"]]487)488fixed = np.array(489[p["near_parallel"]["errors"]["landmark_rms_mm"] ** 2 for p in evaluations["pairs"]]490)491differences = selected - fixed492ratio = float(selected.mean() / fixed.mean())493upper = (494None495if development496else float(497differences.mean()498+ protocol["acceptance"]["paired_t_critical_7df"]499* differences.std(ddof=1)500/ np.sqrt(len(differences))501)502)503all_outcomes = [504evaluations["registration"],505*[506p[k]507for p in evaluations["pairs"]508for k in ("base", "selected", "near_parallel")509],510]511completed = all(512x["stationary"] and x["geometric_pass"] and x["held_out_pass"] for x in all_outcomes513)514benefit = (515ratio516<= protocol["acceptance"]["selected_vs_fixed_mean_squared_task_error_ratio_max"]517and upper is not None518and upper < protocol["acceptance"]["paired_error_difference_t95_upper_max"]519)520record.write_json("evaluation.json", evaluations)521summary = {522"development": development,523"replicates": replicates,524"all_solves_stationary_and_accurate": completed,525"selected_mean_squared_task_error_mm2": float(selected.mean()),526"near_parallel_mean_squared_task_error_mm2": float(fixed.mean()),527"selected_to_near_parallel_ratio": ratio,528"paired_mean_difference_mm2": float(differences.mean()),529"paired_descriptive_t95_upper_mm2": upper,530"benefit_pass": bool(benefit),531"passed": bool(not development and completed and benefit),532"uncertainty_scope": (533"Eight independent prescribed Poisson pairs on one teaching anatomy; "534"descriptive t interval, not a population or clinical claim."535),536}537record.write_json("summary.json", summary)538children = {539str(p.relative_to(output)): digest(p)540for p in sorted(output.rglob("run.json"))541if p != output / "run.json" and "sources" not in p.parts542}543record.write_json("child-records.json", children)544print(json.dumps(summary, indent=2), flush=True)545return summary546547548def main():549parser = argparse.ArgumentParser(description=__doc__)550parser.add_argument("--known", type=Path, required=True)551parser.add_argument("--config", type=Path, default=Path(__file__).with_name("config.json"))552parser.add_argument("--output", type=Path, required=True)553parser.add_argument("--device", default="cuda:0")554parser.add_argument(555"--development",556action="store_true",557help="Separate prescribed development seed and one pair; never a final passing result.",558)559args = parser.parse_args()560result = execute(args.known, args.output, args.config.resolve(), args.device, args.development)561if not args.development and not result["passed"]:562raise SystemExit("worked-example acceptance failed; retained all outcomes")563564565if __name__ == "__main__":566main()567