Generated from the full canonical file for this source snapshot. Line numbers match the library source.
Source SHA256: 624b3d66f3a478db7fd4b6d2659c98f972f2d1c2a2b26afb69f0fba13e4071db
1"""Original-history uncertainty and independent products for stochastic inversion.23Detector misses count as zero histories. Photon branches are not independent4samples, and plug-in squared sample means are not unbiased squared expectations.5The product estimators below explicitly require disjoint random-stream identities.6"""78from __future__ import annotations910# Workspace internals are shared only within this package.11# pyright: reportPrivateUsage=false12import math13from dataclasses import dataclass, field14from typing import Any1516from dpt._runtime import require_no_tape17from dpt.statistics import mean_standard_error1819from .forward import TransportWorkspace20from .model import TransportError21from .rng import HistoryBatch, require_independent2223_NO_VARIANCE = object()242526@dataclass(slots=True)27class EstimatorWorkspace:28"""Reusable detector moments; no per-call allocations or hidden downloads."""2930transport: TransportWorkspace31_sum: Any = field(repr=False)32_hits: Any = field(repr=False)33_scale: Any = field(repr=False)3435@property36def scratch_bytes(self) -> int:37return 20 * self.transport.spec.detector.pixels383940def prepare_estimators(workspace: TransportWorkspace) -> EstimatorWorkspace:41"""Allocate binary64 sum/scale images and int32 hit counts on the owning stream."""42require_no_tape()43context = workspace.context44with context.scope():45sums = context.wp.empty(46workspace.spec.detector.pixels, dtype=context.wp.float64, device=context.device47)48hits = context.wp.empty(49workspace.spec.detector.pixels, dtype=context.wp.int32, device=context.device50)51scale = context.wp.empty_like(sums)52return EstimatorWorkspace(workspace, sums, hits, scale)535455def _accumulate_history_mean(56pixel: Any,57score: Any,58history_status: Any,59*,60batch: HistoryBatch,61workspace: EstimatorWorkspace,62out_mean: Any,63out_variance_of_mean: Any = _NO_VARIANCE,64stream: Any,65validate: bool,66) -> None:67"""Validate shared ownership, clear scratch and launch scaled original-history means."""68transport = workspace.transport69context = transport.context70context.assert_stream(stream)71require_no_tape()72wp = context.wp73pixels = transport.spec.detector.pixels74for name, value, dtype in (75("pixel", pixel, wp.int32),76("score", score, wp.float64),77("history_status", history_status, wp.int32),78):79context.array(value, name, dtype=dtype, shape=(batch.count,))80outputs = [("out_mean", out_mean)]81scratch = [("moment_sum", workspace._sum), ("moment_scale", workspace._scale)]82if out_variance_of_mean is not _NO_VARIANCE:83outputs.append(("out_variance_of_mean", out_variance_of_mean))84scratch.append(("moment_hits", workspace._hits))85for name, value in outputs:86context.array(value, name, dtype=wp.float64, shape=(pixels,))87context.disjoint(88[89*transport._reads(),90("pixel", pixel),91("score", score),92("history_status", history_status),93],94outputs + scratch,95)96if validate:97transport.check_status()98with context.scope():99workspace._sum.zero_()100workspace._scale.zero_()101if out_variance_of_mean is not _NO_VARIANCE:102workspace._hits.zero_()103out_variance_of_mean.zero_()104transport._launch(105transport._kernels.find_tally_scale,106batch.count,107[pixel, score, history_status, pixels, workspace._scale, transport._status],108)109transport._launch(110transport._kernels.tally_sum,111batch.count,112[pixel, score, history_status, pixels, workspace._scale, workspace._sum, transport._status],113)114transport._launch(115transport._kernels.finish_mean,116pixels,117[workspace._sum, workspace._scale, batch.count, out_mean, transport._status],118)119120121# region book:transport-original-history-moments122def history_moments(123pixel: Any,124score: Any,125history_status: Any,126*,127batch: HistoryBatch,128workspace: EstimatorWorkspace,129out_mean: Any,130out_variance_of_mean: Any,131stream: Any = None,132validate: bool = True,133) -> None:134"""Mean and unbiased variance of that mean, for IID original-history scores.135136Source histories must be independently identically distributed; a repeated137deterministic ray also qualifies. This sampling-law precondition cannot be138verified from the realised score arrays.139Deterministically stratified sources require variance across independent140*replicated complete batches*, not this within-history formula. The two141output images contain marginal variances; they do not claim independent142detector pixels or supply a covariance-free loss-error bar.143144FP64 scale, mean and centred-deviation passes preserve the result range. Their145floating summation order is not bitwise reproducible. This analogue engine146produces at most one contribution per original history; descendant splitting147must first combine contributions before using these moment operations.148"""149if batch.count < 2:150raise TransportError("variance of a mean requires at least two original histories")151_accumulate_history_mean(152pixel,153score,154history_status,155batch=batch,156workspace=workspace,157out_mean=out_mean,158out_variance_of_mean=out_variance_of_mean,159stream=stream,160validate=validate,161)162transport = workspace.transport163pixels = transport.spec.detector.pixels164transport._launch(165transport._kernels.centred_moments,166batch.count,167[168pixel,169score,170out_mean,171workspace._scale,172out_variance_of_mean,173workspace._hits,174transport._status,175],176)177transport._launch(178transport._kernels.finish_variance,179pixels,180[181out_mean,182workspace._scale,183workspace._hits,184batch.count,185out_variance_of_mean,186transport._status,187],188)189if validate:190transport.check_status()191192193# endregion book:transport-original-history-moments194195196def _product(197mean_a: Any,198other_b: Any,199observed: Any,200weights: Any,201*,202batches: tuple[HistoryBatch, HistoryBatch],203workspace: TransportWorkspace,204out_components: Any,205loss: bool,206stream: Any,207validate: bool,208) -> None:209require_independent(*batches)210require_no_tape()211context = workspace.context212context.assert_stream(stream)213arrays = [214("mean_a", mean_a),215("other_b", other_b),216("observed", observed),217("weights", weights),218]219shape = (workspace.spec.detector.pixels,)220for name, value in [*arrays, ("out_components", out_components)]:221context.array(value, name, dtype=context.wp.float64, shape=shape)222context.disjoint(workspace._reads() + arrays, [("out_components", out_components)])223if validate:224workspace.check_status()225workspace._launch(226workspace._kernels.independent_product,227shape[0],228[mean_a, other_b, observed, weights, int(loss), out_components, workspace._status],229)230if validate:231workspace.check_status()232233234# region book:transport-independent-loss-gradient235def independent_squared_gradient(236mean_a: Any,237derivative_mean_b: Any,238observed: Any,239weights: Any,240*,241batches: tuple[HistoryBatch, HistoryBatch],242workspace: TransportWorkspace,243out_components: Any,244stream: Any = None,245validate: bool = True,246) -> None:247"""Write unbiased per-pixel contributions to the loss-of-expected-score gradient.248249For fixed finite observation y and nonnegative fixed pixel weight w, the250target is 1/2 sum_p w_p (E[S_p]-y_p)^2. Independent unbiased estimates A of251E[S] and B of its derivative give E[w(A-y)B] = w(E[S]-y)dE[S]. Using the same252histories in both factors generally adds their covariance and is rejected.253Sum components on device using the shared reduction operator. Estimate the254scalar gradient's uncertainty across independent product replicates, because255pixel covariances generally do not vanish.256"""257_product(258mean_a,259derivative_mean_b,260observed,261weights,262batches=batches,263workspace=workspace,264out_components=out_components,265loss=False,266stream=stream,267validate=validate,268)269270271# endregion book:transport-independent-loss-gradient272273274def independent_squared_loss(275mean_a: Any,276mean_b: Any,277observed: Any,278weights: Any,279*,280batches: tuple[HistoryBatch, HistoryBatch],281workspace: TransportWorkspace,282out_components: Any,283stream: Any = None,284validate: bool = True,285) -> None:286"""Write unbiased loss components from two independent score estimates.287288Individual estimates may be negative even though the expected squared-error289loss is nonnegative. Clamping a noisy estimate to zero would introduce bias.290A candidate-minus-incumbent difference may use common random numbers between291the two parameter values, provided the two product factors remain independent.292Independent replicate differences provide the acceptance-policy uncertainty.293"""294_product(295mean_a,296mean_b,297observed,298weights,299batches=batches,300workspace=workspace,301out_components=out_components,302loss=True,303stream=stream,304validate=validate,305)306307308@dataclass(frozen=True, slots=True)309class ReplicateEstimate:310"""A scalar checkpoint, after device reduction and explicit host transfer."""311312value: float313batches: tuple[HistoryBatch, ...]314315def __post_init__(self) -> None:316if not math.isfinite(self.value) or not self.batches:317raise TransportError(318"replicate estimates need a finite value and their batch identities"319)320require_independent(*self.batches)321322323@dataclass(frozen=True, slots=True)324class MeanUncertainty:325mean: float326standard_error: float327replicates: int328329330def summarise_replicates(estimates: tuple[ReplicateEstimate, ...]) -> MeanUncertainty:331"""Estimate uncertainty across genuinely independent complete scalar replicates.332333The caller may pass paired candidate-minus-incumbent objective changes as334values. Within each pair, common random numbers are permitted; record each335*distinct* batch once. Across replicate pairs no streams may overlap.336"""337if len(estimates) < 2:338raise TransportError("uncertainty requires at least two independent replicates")339for index, estimate in enumerate(estimates):340for other in estimates[index + 1 :]:341require_independent(*estimate.batches, *other.batches)342count = len(estimates)343mean, standard_error = mean_standard_error(tuple(estimate.value for estimate in estimates))344return MeanUncertainty(mean, standard_error, count)345346347def history_mean(348pixel: Any,349score: Any,350history_status: Any,351*,352batch: HistoryBatch,353workspace: EstimatorWorkspace,354out_mean: Any,355stream: Any = None,356validate: bool = True,357) -> None:358"""Accumulate a mean without inventing a within-batch uncertainty estimate.359360This supports single-history batches and independent replicated batch designs.361It avoids both the extra squared-score atomic and avoidable score-square362overflow when only the mean is requested by a nonlinear product estimator.363"""364_accumulate_history_mean(365pixel,366score,367history_status,368batch=batch,369workspace=workspace,370out_mean=out_mean,371stream=stream,372validate=validate,373)374if validate:375workspace.transport.check_status()376