Generated from the full canonical file for this source snapshot. Line numbers match the library source.
Source SHA256: 2f4572577fa639412f4e6d190eeaefa5ea5e42c0bc7354f3855d292c1c023da9
1"""Prepared CUDA transport, sparse history scoring and mandatory completion checks.23Device arrays are caller-owned. Model buffers remain immutable for the workspace4lifetime; dynamic density/source arrays remain unchanged until the owning stream5finishes. Preparation uploads only the small declared energy grid. Repeated calls6allocate no device arrays. Explicitly unchecked calls require ``check_status``7before accepting outputs; incomplete histories never become absorbed histories.8"""910from __future__ import annotations1112# Workspace internals are shared only within this package.13# pyright: reportPrivateUsage=false14import math15from dataclasses import dataclass, field16from typing import Any1718from dpt._runtime import DeviceContext, load_kernels, prepare_context, require_no_tape19from dpt.contracts import NumericalError2021from .model import IncompleteHistoryError, TransportError, TransportSpec, _physical22from .rng import HistoryBatch232425@dataclass(slots=True)26class TransportWorkspace:27"""One stream, immutable material tables, small diagnostics and no history tape."""2829spec: TransportSpec30max_histories: int31context: DeviceContext32material_ids: Any33absorption: Any34scattering: Any35_energies: Any = field(repr=False)36_parameters: Any = field(repr=False)37_status: Any = field(repr=False)38_kernels: Any = field(repr=False)3940@property41def scratch_bytes(self) -> int:42return 4 + 8 * len(self.spec.energy_nodes)4344def clear_status(self) -> None:45"""Reset at a declared independent run boundary, after discarding failures."""46with self.context.scope():47self._status.zero_()4849def check_status(self) -> None:50"""Synchronise this stream and transfer only the four-byte status word."""51self.context.wp.synchronize_stream(self.context.stream)52code = int(self._status.numpy()[0])53if code & (4 | 8 | 64):54raise IncompleteHistoryError(55"event, voxel-crossing or angular-rejection budget exhausted; discard the estimate"56)57if code & 32:58raise TransportError("a history left the supplied coefficient energy support")59if code & ~(4 | 8 | 16 | 32 | 64):60raise TransportError("invalid device transport inputs")61if code & 16:62raise NumericalError("nonfinite transport arithmetic or an invalid moment")6364def _launch(self, kernel: Any, dim: int, inputs: list[Any]) -> None:65self.context.wp.launch(66kernel,67dim=dim,68inputs=inputs,69device=self.context.device,70stream=self.context.stream,71block_dim=self.spec.block_dim,72record_tape=False,73)7475def _model_inputs(self) -> list[Any]:76return [77self.material_ids,78self._energies,79self.absorption,80self.scattering,81self._parameters,82]8384def _reads(self) -> list[tuple[str, Any]]:85return [86("material_ids", self.material_ids),87("absorption", self.absorption),88("scattering", self.scattering),89("energy_nodes", self._energies),90("workspace_status", self._status),91]929394def prepare_transport(95spec: TransportSpec,96*,97material_ids: Any,98absorption: Any,99scattering: Any,100max_histories: int,101device: str = "cuda:0",102stream: Any = None,103validate: bool = True,104) -> TransportWorkspace:105"""Bind CUDA buffers and validate supplied immutable coefficients explicitly.106107Material IDs are int32, flattened x fastest. Coefficients are binary64,108material-major with energy fastest. Their partial sum is total extinction;109no inconsistent independent total coefficient is accepted. The tables are110already linear coefficients in inverse mm. For Compton mode they describe111free-electron scattering in the user's declared model, not bound-electron112corrections inferred by this package.113"""114if type(max_histories) is not int or not 0 < max_histories < 2**31:115raise TransportError("max_histories must be a positive signed 32-bit integer")116context = prepare_context(device=device, stream=stream)117require_no_tape()118wp = context.wp119context.array(material_ids, "material_ids", dtype=wp.int32, shape=(math.prod(spec.grid.shape),))120table_size = spec.grid.materials * len(spec.energy_nodes)121if table_size >= 2**31:122raise TransportError("material-energy table exceeds signed 32-bit indexing")123for name, value in (("absorption", absorption), ("scattering", scattering)):124context.array(value, name, dtype=wp.float64, shape=(table_size,))125kernels = load_kernels("dpt.transport.kernels")126with context.scope():127energies = wp.array(list(spec.energy_nodes), dtype=wp.float64, device=context.device)128status = wp.zeros(1, dtype=wp.int32, device=context.device)129parameters = kernels.Parameters()130parameters.origin = wp.vec3d(*spec.grid.origin_mm)131parameters.spacing = wp.vec3d(*spec.grid.spacing_mm)132parameters.shape = wp.vec3i(*spec.grid.shape_xyz)133parameters.detector_lower = wp.vec2d(*spec.detector.lower_xy_mm)134parameters.detector_spacing = wp.vec2d(*spec.detector.spacing_xy_mm)135parameters.detector_shape = wp.vec2i(*spec.detector.shape)136parameters.detector_z = spec.detector.z_mm137parameters.source_energy = spec.energy_kev138parameters.energy_bins = len(spec.energy_nodes)139parameters.max_events = spec.max_events140parameters.max_crossings = spec.max_crossings141parameters.max_angle_trials = spec.max_angle_trials142parameters.compton = int(spec.scattering_law == "free-electron-compton")143parameters.energy_score = int(spec.scoring == "energy-kev")144parameters.continuous_absorption = int(spec.estimator == "continuous-absorption")145workspace = TransportWorkspace(146spec,147max_histories,148context,149material_ids,150absorption,151scattering,152energies,153parameters,154status,155kernels,156)157if validate:158workspace._launch(159kernels.validate_model,160max(int(material_ids.size), table_size),161[material_ids, absorption, scattering, spec.grid.materials, status],162)163workspace.check_status()164return workspace165166167def _validate_call(168workspace: TransportWorkspace,169batch: HistoryBatch,170positions: Any,171directions: Any,172weights: Any,173density: Any,174writes: list[tuple[str, Any, Any]],175source_amplitude: float,176stream: Any,177validate: bool,178) -> None:179require_no_tape()180context = workspace.context181context.assert_stream(stream)182wp = context.wp183if batch.count > workspace.max_histories:184raise TransportError("history batch exceeds workspace capacity")185_physical(source_amplitude, "source amplitude", nonnegative=True)186source_arrays = [("positions", positions), ("directions", directions), ("weights", weights)]187for name, value in source_arrays:188context.array(189value,190name,191dtype=wp.float64 if name == "weights" else wp.vec3d,192shape=(batch.count,),193)194context.array(density, "density", dtype=wp.float64, shape=(workspace.spec.grid.materials,))195for name, value, dtype in writes:196context.array(value, name, dtype=dtype, shape=(batch.count,))197context.disjoint(198workspace._reads() + source_arrays + [("density", density)],199[(name, value) for name, value, _ in writes],200)201# Unchecked calls retain prior failure status. A successful later launch202# cannot launder an incomplete earlier estimate into an accepted one.203if validate:204workspace.check_status()205workspace._launch(206workspace._kernels.validate_sources,207max(batch.count, workspace.spec.grid.materials),208[209positions,210directions,211weights,212density,213workspace.spec.detector.z_mm,214workspace._status,215],216)217workspace.check_status()218219220# region book:transport-history-execution221def trace_histories(222positions: Any,223directions: Any,224weights: Any,225density: Any,226*,227batch: HistoryBatch,228workspace: TransportWorkspace,229out_pixel: Any,230out_score: Any,231out_energy: Any,232out_events: Any,233out_status: Any,234source_amplitude: float = 1.0,235stream: Any = None,236validate: bool = True,237) -> None:238"""Write one sparse detector score per independent original history.239240Binary64 positions are in mm; directions must be unit vectors to 1e-12 in241squared norm. Source positions lie below the detector plane. Nonnegative242base weights encode the caller's fixed source importance sampling, while243``source_amplitude`` scales the expected measurement. ``density`` contains244strictly positive dimensionless scales multiplying both partial coefficients.245246The score is per launched history; averaging includes misses and absorption247as zeros. It is not normalised by detected photons. `out_pixel=-1` means no248detector hit. Score, final energy (keV), collision count and status are always249written, including on failure; invalid outputs must be discarded as a batch.250Continuous absorption samples scattering flights and weights each detector251hit by absorption along its entire path; its event count counts scatterings.252The analogue default retains sampled absorption and total-extinction flights.253"""254wp = workspace.context.wp255_validate_call(256workspace,257batch,258positions,259directions,260weights,261density,262[263("out_pixel", out_pixel, wp.int32),264("out_score", out_score, wp.float64),265("out_energy", out_energy, wp.float64),266("out_events", out_events, wp.int32),267("out_status", out_status, wp.int32),268],269source_amplitude,270stream,271validate,272)273workspace._launch(274workspace._kernels.trace_histories,275batch.count,276[277positions,278directions,279weights,280density,281*workspace._model_inputs(),282wp.uint64(batch.seed),283wp.uint64(batch.first_history),284source_amplitude,285out_pixel,286out_score,287out_energy,288out_events,289out_status,290workspace._status,291],292)293if validate:294workspace.check_status()295296297# endregion book:transport-history-execution298