Generated from the full canonical file for this source snapshot. Line numbers match the library source.
Source SHA256: 2d47d20d38b03b837c673d96981bd4bb985bfe9ba88286bd28d29edf05744ad9
1"""Canonical complete-history Warp kernels, including the likelihood-ratio replay.23No path-by-event storage, queue allocation, roulette or hidden truncation. One4thread owns one original history and writes one sparse detector contribution.5Derivative replay runs the *same* flight function with a selected active material.6This initial scheduling candidate is unprofiled: divergence, register pressure,7FP64 cost and tally contention require actual-device acceptance before any claim.8"""910# pyright: reportInvalidTypeForm=false, reportUnknownParameterType=false11# pyright: reportUnknownMemberType=false, reportUnknownArgumentType=false12# pyright: reportUnknownVariableType=false, reportUntypedFunctionDecorator=false13# pyright: reportMissingImports=false, reportUntypedClassDecorator=false14# Integer constructors declare mutable Warp loop variables.15# ruff: noqa: UP018, RUF04616import warp as wp1718from dpt.kernels.random import random41920from ._constants import LOG_SOURCE_AMPLITUDE, RANDOM_NAMESPACE_BIT, SOURCE_AMPLITUDE2122wp.set_module_options({"fast_math": False, "fuse_fp": True, "enable_backward": False})2324MAX_DISTANCE = wp.constant(wp.float64(1.7976931348623157e308))25TWO_PI = wp.constant(wp.float64(6.2831853071795864769))26ELECTRON_REST_ENERGY_KEV = wp.constant(wp.float64(510.99895069))27MISS_PIXEL = wp.constant(-1)28INVALID_PIXEL = wp.constant(-2)293031@wp.func_native("""32#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 70033// Every non-exited lane reaches this ballot, including misses and invalid34// histories. Membership is established before the participating branch.35const unsigned members = __ballot_sync(0xffffffffu, participating != 0);36if (!participating) return wp::vec_t<2, double>(0.0, 0.0);37const unsigned peers = __match_any_sync(members, target);38const int leader = __ffs(peers) - 1;39const int lane = threadIdx.x & 31;40const int count = __popc(peers);41double total = value;42for (int delta = 1; delta < count; delta <<= 1) {43// __fns selects the delta-th participating lane after this lane, even44// for interleaved detector addresses or inactive/missing histories.45const unsigned source = __fns(peers, lane, delta + 1);46const double next = __shfl_sync(peers, total, source < 32 ? source : lane);47if (source < 32)48total = maximum ? fmax(total, next) : total + next;49}50return wp::vec_t<2, double>(total, lane == leader ? double(count) : 0.0);51#else52return wp::vec_t<2, double>(participating ? value : 0.0, participating ? 1.0 : 0.0);53#endif54""")55def grouped_tally(value: wp.float64, target: int, maximum: int, participating: int) -> wp.vec2d:56"""Combine equal detector addresses selected by a converged warp ballot.5758Call unconditionally from every live kernel lane, after computing its input59and participation flag. Every lane named by a peer mask then executes the60same shuffles. Misses and the final partial warp contribute nothing. Only the61peer leader updates global storage; unrelated detector addresses stay separate.62Inputs are nonnegative magnitudes or bounded normalised scores/deviations.63Summation order remains unspecified, as it was for per-history atomics.64CUDA targets below SM 70 retain individual atomics; no unsupported match65intrinsic is emitted there. The measured optimisation targets SM 121.66"""67...686970@wp.func_native("""71if (a == 0.0 || b == 0.0 || c == 0.0) return 0.0;72int ea, eb, ec;73double ma = frexp(a, &ea);74double mb = frexp(b, &eb);75double mc = frexp(c, &ec);76return ldexp((ma * mb) * mc, ea + eb + ec);77""")78def product3(a: wp.float64, b: wp.float64, c: wp.float64) -> wp.float64:79"""Form a finite-factor product without intermediate exponent overflow/underflow."""80...818283@wp.func_native("""84if (a == 0.0 || b == 0.0 || c == 0.0 || d == 0.0) return 0.0;85int ea, eb, ec, ed;86double ma = frexp(a, &ea);87double mb = frexp(b, &eb);88double mc = frexp(c, &ec);89double md = frexp(d, &ed);90return ldexp(((ma * mb) * mc) * md, ea + eb + ec + ed);91""")92def product4(a: wp.float64, b: wp.float64, c: wp.float64, d: wp.float64) -> wp.float64:93"""A derivative product has its own range, independent of a rounded primal score."""94...959697@wp.func_native("""98if (a == 0.0 || b == 0.0 || c == 0.0 || d == 0.0) return 0.0;99int ea, eb, ec, ed;100const double ma = frexp(a, &ea);101const double mb = frexp(b, &eb);102const double mc = frexp(c, &ec);103const double md = frexp(d, &ed);104const double mantissa = ((ma * mb) * mc) * md;105// Four finite binary64 factors cannot rescue attenuation above this bound.106// Check before converting the optical depth to an integer exponent.107if (tau > 8192.0) return copysign(0.0, mantissa);108const int attenuation_exponent = static_cast<int>(floor(tau * 1.4426950408889634074));109// Split ln(2) and one FMA retain the residual when tau is near 800 or larger.110const double residual = fma(double(attenuation_exponent), 0.69314718055994530942, -tau)111+ double(attenuation_exponent) * 2.3190468138462995584e-17;112return ldexp(mantissa * exp(residual), ea + eb + ec + ed - attenuation_exponent);113""")114def attenuated_product4(115tau: wp.float64, a: wp.float64, b: wp.float64, c: wp.float64, d: wp.float64116) -> wp.float64:117"""Multiply original factors by exp(-tau) before the final binary64 rounding.118119Finite factors and nonnegative finite optical depth are checked by callers.120An underflowed attenuation or forward score never determines derivative range.121"""122...123124125@wp.struct126class Parameters:127origin: wp.vec3d128spacing: wp.vec3d129shape: wp.vec3i130detector_lower: wp.vec2d131detector_spacing: wp.vec2d132detector_shape: wp.vec2i133detector_z: wp.float64134source_energy: wp.float64135energy_bins: int136max_events: int137max_crossings: int138max_angle_trials: int139compton: int140energy_score: int141continuous_absorption: int142143144@wp.struct145class Trace:146pixel: int147events: int148status: int149energy: wp.float64150score: wp.float64151density_score: wp.float64152absorption_depth: wp.float64153154155@wp.struct156class GridEntry:157position: wp.vec3d158cell: wp.vec3i159alive: int160status: int161162163@wp.struct164class FaceCrossing:165position: wp.vec3d166cell: wp.vec3i167inside: int168169170@wp.func171def detector_pixel(position: wp.vec3d, direction: wp.vec3d, p: Parameters) -> int:172pixel = int(MISS_PIXEL)173if direction[2] > wp.float64(0.0):174distance = (p.detector_z - position[2]) / direction[2]175if not wp.isfinite(distance):176return INVALID_PIXEL177if distance >= wp.float64(0.0):178x = position[0] + distance * direction[0] - p.detector_lower[0]179y = position[1] + distance * direction[1] - p.detector_lower[1]180if not wp.isfinite(x) or not wp.isfinite(y):181return INVALID_PIXEL182# Test support before converting potentially huge floating coordinates.183if (184x >= wp.float64(0.0)185and y >= wp.float64(0.0)186and x < wp.float64(p.detector_shape[0]) * p.detector_spacing[0]187and y < wp.float64(p.detector_shape[1]) * p.detector_spacing[1]188):189# Physical support has already been tested; quotient rounding190# at its upper edge must not alias a neighbouring row.191column = wp.min(int(wp.floor(x / p.detector_spacing[0])), p.detector_shape[0] - 1)192row = wp.min(int(wp.floor(y / p.detector_spacing[1])), p.detector_shape[1] - 1)193pixel = row * p.detector_shape[0] + column194return pixel195196197@wp.func198def cell_index(position: wp.vec3d, direction: wp.vec3d, p: Parameters) -> wp.vec3i:199cell = wp.vec3i(0)200for axis in range(3):201coordinate = (position[axis] - p.origin[axis]) / p.spacing[axis]202index = int(wp.floor(coordinate))203# A point on a face belongs to the downstream cell. No epsilon displaces204# a physical path or silently changes a thin voxel's optical thickness.205if direction[axis] < wp.float64(0.0) and coordinate == wp.float64(index):206index -= 1207cell[axis] = wp.clamp(index, 0, p.shape[axis] - 1)208return cell209210211@wp.func212def coefficients(213material: int,214energy: wp.float64,215energies: wp.array(dtype=wp.float64),216absorption: wp.array(dtype=wp.float64),217scattering: wp.array(dtype=wp.float64),218p: Parameters,219) -> wp.vec2d:220offset = material * p.energy_bins221if p.energy_bins == 1:222return wp.vec2d(absorption[offset], scattering[offset])223lower = int(0)224upper = p.energy_bins - 1225while upper - lower > 1:226middle = (lower + upper) // 2227if energies[middle] <= energy:228lower = middle229else:230upper = middle231fraction = (energy - energies[lower]) / (energies[upper] - energies[lower])232return wp.vec2d(233(wp.float64(1.0) - fraction) * absorption[offset + lower]234+ fraction * absorption[offset + upper],235(wp.float64(1.0) - fraction) * scattering[offset + lower]236+ fraction * scattering[offset + upper],237)238239240@wp.func241def scatter_direction(direction: wp.vec3d, cosine: wp.float64, azimuth: wp.float64) -> wp.vec3d:242# Choose the auxiliary axis away from collinearity; polar singularities do243# not justify dividing by sin(theta) of the incoming direction.244auxiliary = wp.vec3d(wp.float64(0.0), wp.float64(0.0), wp.float64(1.0))245if wp.abs(direction[2]) > wp.float64(0.9):246auxiliary = wp.vec3d(wp.float64(1.0), wp.float64(0.0), wp.float64(0.0))247tangent = wp.normalize(wp.cross(auxiliary, direction))248bitangent = wp.cross(direction, tangent)249sine = wp.sqrt(wp.max(wp.float64(0.0), wp.float64(1.0) - cosine * cosine))250return wp.normalize(251cosine * direction + sine * (wp.cos(azimuth) * tangent + wp.sin(azimuth) * bitangent)252)253254255# region book:transport-compton-law256@wp.func257def compton_scatter(258energy: wp.float64,259seed: wp.uint64,260history: wp.uint64,261event: wp.uint32,262max_trials: int,263) -> wp.vec4d:264"""Cosine, azimuth, outgoing energy and status from the conditional free-electron law."""265result = wp.vec4d(wp.float64(0.0), wp.float64(0.0), energy, wp.float64(6.0))266for trial in range(max_trials):267angular = random4(seed, history, event, wp.uint32(RANDOM_NAMESPACE_BIT) + wp.uint32(trial))268cosine = wp.float64(2.0) * angular[0] - wp.float64(1.0)269ratio = wp.float64(1.0) / (270wp.float64(1.0) + energy / ELECTRON_REST_ENERGY_KEV * (wp.float64(1.0) - cosine)271)272# Klein-Nishina density divided by its envelope 2. This conditional273# scattering law is independent of the active material-density scale.274acceptance = wp.float64(0.5) * (275ratio * ratio * ratio + ratio - ratio * ratio * (wp.float64(1.0) - cosine * cosine)276)277if angular[1] < acceptance:278result = wp.vec4d(279cosine,280TWO_PI * angular[2],281energy * ratio,282wp.float64(0.0),283)284break285return result286287288# endregion book:transport-compton-law289290291# region book:transport-grid-traversal292@wp.func293def enter_grid(position: wp.vec3d, direction: wp.vec3d, p: Parameters) -> GridEntry:294"""Intersect a forward ray and assign its downstream cell without an epsilon."""295result = GridEntry()296result.status = 0297result.cell = wp.vec3i(0)298entry = wp.float64(0.0)299exit_distance = wp.float64(MAX_DISTANCE)300intersects = int(1)301for axis in range(3):302lower = p.origin[axis]303upper = lower + wp.float64(p.shape[axis]) * p.spacing[axis]304if direction[axis] == wp.float64(0.0):305if position[axis] < lower or position[axis] >= upper:306intersects = 0307else:308first = (lower - position[axis]) / direction[axis]309second = (upper - position[axis]) / direction[axis]310if not wp.isfinite(first) or not wp.isfinite(second):311result.status = 4312intersects = 0313break314entry = wp.max(entry, wp.min(first, second))315exit_distance = wp.min(exit_distance, wp.max(first, second))316if exit_distance <= entry:317intersects = 0318result.alive = intersects319if result.alive != 0:320position += entry * direction321# Only round the intersection coordinate to a face, not the travelled322# distance. The entry came from these same faces and cannot be outside.323for axis in range(3):324position[axis] = wp.clamp(325position[axis],326p.origin[axis],327p.origin[axis] + wp.float64(p.shape[axis]) * p.spacing[axis],328)329if result.alive != 0:330result.cell = cell_index(position, direction, p)331result.position = position332return result333334335@wp.func336def distances_to_faces(337position: wp.vec3d, direction: wp.vec3d, cell: wp.vec3i, p: Parameters338) -> wp.vec3d:339"""Distances to the next cell face on each axis, retaining exact ties."""340distances = wp.vec3d(MAX_DISTANCE)341for axis in range(3):342if direction[axis] != wp.float64(0.0):343face_index = cell[axis]344if direction[axis] > wp.float64(0.0):345face_index += 1346face = p.origin[axis] + wp.float64(face_index) * p.spacing[axis]347distances[axis] = (face - position[axis]) / direction[axis]348return distances349350351@wp.func352def cross_faces(353position: wp.vec3d,354direction: wp.vec3d,355cell: wp.vec3i,356face_distances: wp.vec3d,357distance: wp.float64,358p: Parameters,359) -> FaceCrossing:360"""Cross every tied face and snap only its coordinate to the known cell face."""361result = FaceCrossing()362result.position = position363result.cell = cell364result.inside = 1365for axis in range(3):366if face_distances[axis] == distance:367if direction[axis] > wp.float64(0.0):368result.cell[axis] += 1369result.position[axis] = (370p.origin[axis] + wp.float64(result.cell[axis]) * p.spacing[axis]371)372else:373result.position[axis] = (374p.origin[axis] + wp.float64(result.cell[axis]) * p.spacing[axis]375)376result.cell[axis] -= 1377if result.cell[axis] < 0 or result.cell[axis] >= p.shape[axis]:378result.inside = 0379return result380381382# endregion book:transport-grid-traversal383384385# region book:transport-residual-flight386@wp.func387def walk(388initial_position: wp.vec3d,389initial_direction: wp.vec3d,390source_weight: wp.float64,391source_amplitude: wp.float64,392seed: wp.uint64,393history: wp.uint64,394active_material: int,395material_ids: wp.array(dtype=wp.int32),396energies: wp.array(dtype=wp.float64),397absorption: wp.array(dtype=wp.float64),398scattering: wp.array(dtype=wp.float64),399density: wp.array(dtype=wp.float64),400p: Parameters,401) -> Trace:402result = Trace()403result.pixel = MISS_PIXEL404result.events = 0405result.status = 0406result.energy = p.source_energy407result.score = wp.float64(0.0)408result.density_score = wp.float64(0.0)409result.absorption_depth = wp.float64(0.0)410position = initial_position411direction = initial_direction412entry = enter_grid(position, direction, p)413position = entry.position414cell = entry.cell415alive = entry.alive416result.status = entry.status417crossings = int(0)418while alive != 0:419if result.events >= p.max_events:420result.status = 2421break422if result.energy < energies[0] or result.energy > energies[p.energy_bins - 1]:423result.status = 5424break425draws = random4(seed, history, wp.uint32(result.events), wp.uint32(0))426residual = -wp.log(draws[0])427collision = int(0)428material = int(0)429interaction = wp.vec2d(wp.float64(0.0))430while alive != 0 and collision == 0:431flat = (cell[2] * p.shape[1] + cell[1]) * p.shape[0] + cell[0]432material = material_ids[flat]433interaction = coefficients(material, result.energy, energies, absorption, scattering, p)434extinction = density[material] * (interaction[0] + interaction[1])435absorption_rate = wp.float64(0.0)436if p.continuous_absorption != 0:437extinction = density[material] * interaction[1]438absorption_rate = density[material] * interaction[0]439face_distance = distances_to_faces(position, direction, cell, p)440distance = wp.min(face_distance[0], wp.min(face_distance[1], face_distance[2]))441if distance < wp.float64(0.0) or not wp.isfinite(distance):442result.status = 4443alive = 0444break445optical_distance = extinction * distance446if (447not wp.isfinite(extinction)448or not wp.isfinite(optical_distance)449or not wp.isfinite(absorption_rate)450):451result.status = 4452alive = 0453break454absorption_distance = wp.float64(0.0)455if p.continuous_absorption != 0:456segment_distance = distance457if extinction > wp.float64(0.0) and residual < optical_distance:458segment_distance = residual / extinction459# Use the realised segment, not the entire distance to the face:460# scattering can interrupt this segment before the face is reached.461absorption_distance = absorption_rate * segment_distance462result.absorption_depth += absorption_distance463if not wp.isfinite(result.absorption_depth):464result.status = 4465alive = 0466break467if extinction > wp.float64(0.0) and residual < optical_distance:468distance = residual / extinction469position += distance * direction470if material == active_material:471result.density_score += wp.float64(1.0) - residual472if p.continuous_absorption != 0:473result.density_score -= absorption_distance474collision = 1475else:476if material == active_material:477result.density_score -= optical_distance478if p.continuous_absorption != 0:479result.density_score -= absorption_distance480residual -= optical_distance481position += distance * direction482crossing = cross_faces(position, direction, cell, face_distance, distance, p)483position = crossing.position484cell = crossing.cell485alive = crossing.inside486crossings += 1487if alive != 0 and crossings >= p.max_crossings:488result.status = 3489alive = 0490if collision != 0:491result.events += 1492absorption_probability = wp.float64(0.0)493if p.continuous_absorption == 0:494absorption_probability = interaction[0] / (interaction[0] + interaction[1])495if p.continuous_absorption == 0 and draws[1] < absorption_probability:496result.status = 1497alive = 0498else:499cosine = wp.float64(2.0) * draws[2] - wp.float64(1.0)500azimuth = TWO_PI * draws[3]501if p.compton != 0:502angular = compton_scatter(503result.energy,504seed,505history,506wp.uint32(result.events - 1),507p.max_angle_trials,508)509cosine = angular[0]510azimuth = angular[1]511result.energy = angular[2]512if angular[3] != wp.float64(0.0):513result.status = 6514alive = 0515if alive != 0:516direction = scatter_direction(direction, cosine, azimuth)517if result.status == 0:518result.pixel = detector_pixel(position, direction, p)519if result.pixel == INVALID_PIXEL:520result.pixel = MISS_PIXEL521result.status = 4522if result.pixel >= 0:523score_energy = wp.float64(1.0)524if p.energy_score != 0:525score_energy = result.energy526result.score = product3(source_weight, source_amplitude, score_energy)527if p.continuous_absorption != 0:528result.score = attenuated_product4(529result.absorption_depth,530source_weight,531source_amplitude,532score_energy,533wp.float64(1.0),534)535if not wp.isfinite(result.score) or not wp.isfinite(result.density_score):536result.status = 4537return result538539540# endregion book:transport-residual-flight541542543@wp.kernel544def validate_model(545material_ids: wp.array(dtype=wp.int32),546absorption: wp.array(dtype=wp.float64),547scattering: wp.array(dtype=wp.float64),548materials: int,549status: wp.array(dtype=wp.int32),550):551index = wp.tid()552if index < material_ids.shape[0]:553if material_ids[index] < 0 or material_ids[index] >= materials:554wp.atomic_or(status, 0, 1)555if index < absorption.shape[0]:556a = absorption[index]557s = scattering[index]558if (559not wp.isfinite(a)560or not wp.isfinite(s)561or a < wp.float64(0.0)562or s < wp.float64(0.0)563or not wp.isfinite(a + s)564):565wp.atomic_or(status, 0, 1)566567568@wp.kernel569def validate_sources(570positions: wp.array(dtype=wp.vec3d),571directions: wp.array(dtype=wp.vec3d),572weights: wp.array(dtype=wp.float64),573density: wp.array(dtype=wp.float64),574detector_z: wp.float64,575status: wp.array(dtype=wp.int32),576):577index = wp.tid()578if index < positions.shape[0]:579position = positions[index]580direction = directions[index]581for axis in range(3):582if not wp.isfinite(position[axis]) or not wp.isfinite(direction[axis]):583wp.atomic_or(status, 0, 1)584if (585wp.abs(wp.dot(direction, direction) - wp.float64(1.0)) > wp.float64(1.0e-12)586or not wp.isfinite(weights[index])587or weights[index] < wp.float64(0.0)588or position[2] >= detector_z589):590wp.atomic_or(status, 0, 1)591if index < density.shape[0]:592if not wp.isfinite(density[index]) or density[index] <= wp.float64(0.0):593wp.atomic_or(status, 0, 1)594595596@wp.kernel597def trace_histories(598positions: wp.array(dtype=wp.vec3d),599directions: wp.array(dtype=wp.vec3d),600weights: wp.array(dtype=wp.float64),601density: wp.array(dtype=wp.float64),602material_ids: wp.array(dtype=wp.int32),603energies: wp.array(dtype=wp.float64),604absorption: wp.array(dtype=wp.float64),605scattering: wp.array(dtype=wp.float64),606parameters: Parameters,607seed: wp.uint64,608first_history: wp.uint64,609source_amplitude: wp.float64,610out_pixel: wp.array(dtype=wp.int32),611out_score: wp.array(dtype=wp.float64),612out_energy: wp.array(dtype=wp.float64),613out_events: wp.array(dtype=wp.int32),614out_status: wp.array(dtype=wp.int32),615status: wp.array(dtype=wp.int32),616):617index = wp.tid()618result = walk(619positions[index],620directions[index],621weights[index],622source_amplitude,623seed,624first_history + wp.uint64(index),625SOURCE_AMPLITUDE,626material_ids,627energies,628absorption,629scattering,630density,631parameters,632)633out_pixel[index] = result.pixel634out_score[index] = result.score635if not wp.isfinite(out_score[index]):636wp.atomic_or(status, 0, 16)637out_energy[index] = result.energy638out_events[index] = result.events639out_status[index] = result.status640if result.status >= 2:641wp.atomic_or(status, 0, 1 << result.status)642643644# region book:transport-density-score645@wp.kernel646def derivative_histories(647positions: wp.array(dtype=wp.vec3d),648directions: wp.array(dtype=wp.vec3d),649weights: wp.array(dtype=wp.float64),650density: wp.array(dtype=wp.float64),651material_ids: wp.array(dtype=wp.int32),652energies: wp.array(dtype=wp.float64),653absorption: wp.array(dtype=wp.float64),654scattering: wp.array(dtype=wp.float64),655parameters: Parameters,656seed: wp.uint64,657first_history: wp.uint64,658active_material: int,659source_amplitude: wp.float64,660out_pixel: wp.array(dtype=wp.int32),661out_derivative: wp.array(dtype=wp.float64),662out_status: wp.array(dtype=wp.int32),663status: wp.array(dtype=wp.int32),664):665index = wp.tid()666amplitude = source_amplitude667base_weight = weights[index]668if active_material >= 0:669# A density derivative is formed directly below. An unused primal670# overflow/underflow must not determine its representability.671base_weight = wp.float64(0.0)672if active_material == SOURCE_AMPLITUDE:673amplitude = wp.float64(1.0)674result = walk(675positions[index],676directions[index],677base_weight,678amplitude,679seed,680first_history + wp.uint64(index),681active_material,682material_ids,683energies,684absorption,685scattering,686density,687parameters,688)689out_pixel[index] = result.pixel690derivative = wp.float64(0.0)691if active_material >= 0 and result.pixel >= 0:692score_energy = wp.float64(1.0)693if parameters.energy_score != 0:694score_energy = result.energy695derivative = product4(weights[index], source_amplitude, score_energy, result.density_score)696if parameters.continuous_absorption != 0:697derivative = attenuated_product4(698result.absorption_depth,699weights[index],700source_amplitude,701score_energy,702result.density_score,703)704if active_material == SOURCE_AMPLITUDE:705# d(a * base_score)/da, including at a=0; never divide by amplitude.706derivative = result.score707if active_material == LOG_SOURCE_AMPLITUDE:708derivative = result.score709out_derivative[index] = derivative710out_status[index] = result.status711if result.status >= 2:712wp.atomic_or(status, 0, 1 << result.status)713if not wp.isfinite(derivative):714wp.atomic_or(status, 0, 16)715716717# endregion book:transport-density-score718719720@wp.kernel721def centred_moments(722pixel: wp.array(dtype=wp.int32),723score: wp.array(dtype=wp.float64),724mean: wp.array(dtype=wp.float64),725scale: wp.array(dtype=wp.float64),726out_variance: wp.array(dtype=wp.float64),727out_hits: wp.array(dtype=wp.int32),728status: wp.array(dtype=wp.int32),729):730history = wp.tid()731target = pixel[history]732participating = int(0)733contribution = wp.float64(0.0)734if target >= 0 and target < mean.shape[0]:735residual = wp.float64(0.0)736if scale[target] > wp.float64(0.0):737value = score[history]738average = mean[target]739if (value >= wp.float64(0.0)) == (average >= wp.float64(0.0)):740residual = (value - average) / scale[target]741else:742residual = value / scale[target] - average / scale[target]743contribution = residual * residual744if not wp.isfinite(contribution):745wp.atomic_or(status, 0, 16)746else:747participating = 1748aggregate = grouped_tally(contribution, target, 0, participating)749if aggregate[1] > wp.float64(0.0):750wp.atomic_add(out_variance, target, aggregate[0])751wp.atomic_add(out_hits, target, int(aggregate[1]))752753754@wp.kernel755def finish_variance(756mean: wp.array(dtype=wp.float64),757scale: wp.array(dtype=wp.float64),758hits: wp.array(dtype=wp.int32),759count: int,760out_variance: wp.array(dtype=wp.float64),761status: wp.array(dtype=wp.int32),762):763pixel = wp.tid()764n = wp.float64(count)765missing = count - hits[pixel]766centred = out_variance[pixel]767if missing > 0 and scale[pixel] > wp.float64(0.0):768residual = mean[pixel] / scale[pixel]769centred += wp.float64(missing) * residual * residual770# Accumulate normalised squares before restoring their exponent. Squaring771# each tiny history first can turn a representable aggregate variance to zero.772sigma = scale[pixel] * wp.sqrt(centred / n / (n - wp.float64(1.0)))773variance = sigma * sigma774out_variance[pixel] = variance775if missing < 0 or not wp.isfinite(variance):776wp.atomic_or(status, 0, 16)777778779@wp.kernel780def independent_product(781mean_a: wp.array(dtype=wp.float64),782other_b: wp.array(dtype=wp.float64),783observed: wp.array(dtype=wp.float64),784weights: wp.array(dtype=wp.float64),785loss_mode: int,786out_components: wp.array(dtype=wp.float64),787status: wp.array(dtype=wp.int32),788):789pixel = wp.tid()790a = mean_a[pixel]791b = other_b[pixel]792y = observed[pixel]793w = weights[pixel]794if (795not wp.isfinite(a)796or not wp.isfinite(b)797or not wp.isfinite(y)798or not wp.isfinite(w)799or w < wp.float64(0.0)800):801wp.atomic_or(status, 0, 1)802value = w * (a - y) * b803if loss_mode != 0:804value = wp.float64(0.5) * w * (a - y) * (b - y)805out_components[pixel] = value806if not wp.isfinite(value):807wp.atomic_or(status, 0, 16)808809810@wp.kernel811def sample_parallel_source(812lower: wp.vec3d,813extent: wp.vec2d,814direction: wp.vec3d,815weight: wp.float64,816seed: wp.uint64,817first_history: wp.uint64,818out_position: wp.array(dtype=wp.vec3d),819out_direction: wp.array(dtype=wp.vec3d),820out_weight: wp.array(dtype=wp.float64),821):822index = wp.tid()823# The high event bit separates source draws from every supported collision.824draw = random4(825seed, first_history + wp.uint64(index), wp.uint32(RANDOM_NAMESPACE_BIT), wp.uint32(0)826)827out_position[index] = lower + wp.vec3d(828extent[0] * draw[0], extent[1] * draw[1], wp.float64(0.0)829)830out_direction[index] = direction831out_weight[index] = weight832833834@wp.kernel835def update_density_chart(836parameters: wp.array(dtype=wp.float64),837material_parameter: wp.array(dtype=wp.int32),838base_density: wp.array(dtype=wp.float64),839out_density: wp.array(dtype=wp.float64),840status: wp.array(dtype=wp.int32),841):842material = wp.tid()843active = material_parameter[material]844value = base_density[material]845if active >= 0:846value = wp.exp(parameters[active])847out_density[material] = value848if not wp.isfinite(value) or value <= wp.float64(0.0):849wp.atomic_or(status, 0, 16)850851852@wp.kernel853def find_tally_scale(854pixel: wp.array(dtype=wp.int32),855score: wp.array(dtype=wp.float64),856history_status: wp.array(dtype=wp.int32),857pixels: int,858out_scale: wp.array(dtype=wp.float64),859status: wp.array(dtype=wp.int32),860):861history = wp.tid()862target = pixel[history]863value = score[history]864participating = int(0)865terminal = history_status[history]866if terminal >= 2 and terminal <= 6:867wp.atomic_or(status, 0, 1 << terminal)868elif (869target < MISS_PIXEL870or target >= pixels871or not wp.isfinite(value)872or terminal < 0873or terminal > 6874or (target == MISS_PIXEL and value != wp.float64(0.0))875or (terminal == 1 and target != MISS_PIXEL)876):877wp.atomic_or(status, 0, 1)878elif target >= 0:879participating = 1880aggregate = grouped_tally(wp.abs(value), target, 1, participating)881if aggregate[1] > wp.float64(0.0):882wp.atomic_max(out_scale, target, aggregate[0])883884885@wp.kernel886def tally_sum(887pixel: wp.array(dtype=wp.int32),888score: wp.array(dtype=wp.float64),889history_status: wp.array(dtype=wp.int32),890pixels: int,891scale: wp.array(dtype=wp.float64),892out_sum: wp.array(dtype=wp.float64),893status: wp.array(dtype=wp.int32),894):895history = wp.tid()896target = pixel[history]897participating = int(0)898contribution = wp.float64(0.0)899if (900history_status[history] <= 1901and history_status[history] >= 0902and target >= 0903and target < pixels904and scale[target] > wp.float64(0.0)905):906contribution = score[history] / scale[target]907if not wp.isfinite(contribution):908wp.atomic_or(status, 0, 16)909else:910participating = 1911aggregate = grouped_tally(contribution, target, 0, participating)912if aggregate[1] > wp.float64(0.0):913wp.atomic_add(out_sum, target, aggregate[0])914915916@wp.kernel917def finish_mean(918sums: wp.array(dtype=wp.float64),919scale: wp.array(dtype=wp.float64),920count: int,921out_mean: wp.array(dtype=wp.float64),922status: wp.array(dtype=wp.int32),923):924pixel = wp.tid()925# Scale after dividing the bounded sum: neither a huge raw sum nor a926# subnormal score divided prematurely by the history count is required.927value = scale[pixel] * (sums[pixel] / wp.float64(count))928out_mean[pixel] = value929if not wp.isfinite(value):930wp.atomic_or(status, 0, 16)931932933@wp.kernel934def validate_measurement(935observation: wp.array(dtype=wp.float64),936weights: wp.array(dtype=wp.float64),937status: wp.array(dtype=wp.int32),938):939pixel = wp.tid()940if (941not wp.isfinite(observation[pixel])942or not wp.isfinite(weights[pixel])943or weights[pixel] < wp.float64(0.0)944):945wp.atomic_or(status, 0, 1)946