feat: optional observation-window raw exchange ledger

FixedPatchStepper(observe_window_exchange=True), which requires
observe_raw_exchange, allocates a second per-link FP64 ledger per body
level. While a window is open, the unchanged BlockAccumulateExchange
kernel also accumulates into it after each fine hop. Small API:
start_exchange_window() zeroes and opens at the current completed step;
read_exchange_window(body_id, level=) is non-destructive and reports
impulse with rates over the integer window duration. initialize/
_initialize_raw and step failure invalidate; remap builds a new instance
with no open window; Jet amplitude updates may stay in-window. Default
disabled: no allocation and no extra launch; last_substep/last_base_step
unchanged. summarize_exchange gains window='window' with duration.
Host contract tests (mocked driver) and a GPU gate against summed
per-base reads with a reordering-bound FP64 tolerance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-18 12:54:44 +00:00
1 parent 6c53d2e74a
commit d930323479
5 files changed
+280 -6

No files matched your search

+15 -4
View File
@@ -7,16 +7,26 @@ import numpy as np
def summarize_exchange(rows, body_ids, aperture, *, body_id, level, h,
time_end, window, reference_center):
time_end, window, reference_center, duration=None):
"""Reduce FP64 interval integrals or convert FP32 last-substep raw rows.
dt_l=h_l under acoustic 2:1 refinement. Local momentum j -> base force
h*j -> impulse h*dt*j; torque adds one h; mass is h**2*dm.
window="window" reduces an FP64 observation-window ledger spanning
``duration`` (integer >= 1) base intervals; rates divide by that duration.
"""
if window not in ("last_base_step", "last_substep"):
if window not in ("last_base_step", "last_substep", "window"):
raise ValueError("unknown exchange window")
if window == "window":
if isinstance(duration, bool) or not isinstance(duration, (int, np.integer)) or duration < 1:
raise ValueError("window duration must be an integer number of base intervals >= 1")
elif duration is not None:
raise ValueError("duration applies only to window='window'")
values = np.array(rows, dtype=np.float64, copy=True)
duration = 1.0 if window == "last_base_step" else h
if window == "window":
duration = float(duration)
else:
duration = 1.0 if window == "last_base_step" else h
if window == "last_substep":
values *= np.array([h*h, h*h, h*h*h, h*h])
selected = np.asarray(body_ids) == body_id
@@ -31,6 +41,7 @@ def summarize_exchange(rows, body_ids, aperture, *, body_id, level, h,
return dict(wall=reduce(selected & ~aperture), aperture=reduce(selected & aperture),
total=reduce(selected), body_id=int(body_id), level=int(level),
time_start=time_end-duration, time_end=time_end, dt=h,
sample_count=round(1/h) if window == "last_base_step" else 1,
sample_count=(round(duration/h) if window == "window"
else round(1/h) if window == "last_base_step" else 1),
window=window, units="base_lattice_per_unit_depth",
definition="raw_boundary_exchange", reference_center=tuple(reference_center), valid=True)
+69 -2
View File
@@ -172,6 +172,9 @@ class FixedPatchStepper:
base interval or last substep in base-lattice units per unit depth. This
is boundary momentum reaction, not a validated nozzle/stress load. Dense
body layouts are not supported by this new diagnostic in this candidate.
``observe_window_exchange=True`` (requires observe_raw_exchange) adds a second
per-link FP64 ledger accumulated by the same kernel between
start_exchange_window() and read_exchange_window(); disabled by default.
``jets`` (body.jets.Jet) use the same edge frame as ``fine_objects`` and
drive fine cut links selected at their true hits; ``set_jet_amplitude``
re-uploads the per-link velocity column only.
@@ -216,7 +219,7 @@ class FixedPatchStepper:
fine_block=None, base_block=None, jets=(), child=None, child_block=None,
child_objects=(), child_jets=(), child_rectangles_in_parent=None,
refinements=None, level_blocks=None, level_objects=None, level_jets=None,
observe_raw_exchange=False):
observe_raw_exchange=False, observe_window_exchange=False):
if HALO != 4:
raise RuntimeError("refinement.cu requires a four-cell fine halo")
if coarse_boundary not in ("periodic", "slot_reservoir", "channel"):
@@ -315,6 +318,13 @@ class FixedPatchStepper:
for b, layout in zip(self._boundary_hosts, self._layouts)):
raise ValueError("raw exchange currently requires blocked body levels")
self._observe_raw_exchange = observe_raw_exchange
if not isinstance(observe_window_exchange, bool):
raise ValueError("observe_window_exchange must be bool")
if observe_window_exchange and not observe_raw_exchange:
raise ValueError("observe_window_exchange requires observe_raw_exchange")
self._observe_window = observe_window_exchange
self._window_valid = False
self._window_start = None
self._exchange_valid = False
self._exchange_spacing = tuple(spacings)
self._exchange_centers = tuple(tuple(tuple(o.geometry.center) for o in objects)
@@ -365,6 +375,8 @@ class FixedPatchStepper:
count = len(b.tables["cell"])
b.last_raw = self._allocate(count * 4 * 4)
b.interval_integral = self._allocate(count * 4 * 8)
if self._observe_window:
b.window_integral = self._allocate(count * 4 * 8)
b.accumulate = level.module.get_function("BlockAccumulateExchange")
b.kernel = level.module.get_function("BlockCurvedBoundary")
if self._level_boundaries[1] is not None:
@@ -559,6 +571,7 @@ class FixedPatchStepper:
cuda.memset_d8(self._fine_curved.link_exchange_gpu, 0, self._fine_curved.count * 12)
self._step_count = 0
self._exchange_valid = False
self._window_valid = False
self._status = "ready"
except BaseException:
self._status = "failed"
@@ -639,7 +652,8 @@ class FixedPatchStepper:
fine_block=fine_block, child_block=child_block,
child_rectangles_in_parent=new.child.bounds,
child_objects=child_objects, child_jets=jets,
observe_raw_exchange=self._observe_raw_exchange)
observe_raw_exchange=self._observe_raw_exchange,
observe_window_exchange=self._observe_window)
try:
if self._ddf_shifting:
candidate._initialize_raw(_StoredPopulations(transferred.g, transferred.shifted))
@@ -771,6 +785,10 @@ class FixedPatchStepper:
b.accumulate(b.last_raw, b.interval_integral, np.uint32(count),
np.float64(h), np.float64(h),
grid=((count + self._block[0] - 1) // self._block[0], 1), **kw)
if self._window_valid and b.window_integral is not None:
b.accumulate(b.last_raw, b.window_integral, np.uint32(count),
np.float64(h), np.float64(h),
grid=((count + self._block[0] - 1) // self._block[0], 1), **kw)
flags = np.uintp(0) if level.boundary is None else level.boundary.flags
level.kernel(src, dst, np.uint32(level.layout.halo), flags, level.kind, grid=level.grid, **kw)
@@ -804,9 +822,57 @@ class FixedPatchStepper:
self._exchange_valid = self._observe_raw_exchange
self._status = "ready"
except BaseException:
self._window_valid = False
self._status = "failed"
raise
def start_exchange_window(self):
"""Zero the window ledgers and open a window at the current completed step count."""
with self._lock, self.ctx.activate():
self._require("ready")
if not self._observe_window:
raise RuntimeError("window exchange observation is disabled")
self._window_valid = False
try:
for b in self._level_boundaries:
if b is not None and b.window_integral is not None:
cuda.memset_d8(b.window_integral, 0, len(b.tables["cell"]) * 32)
except BaseException:
self._status = "failed"
raise
self._window_start = self._step_count
self._window_valid = True
def read_exchange_window(self, body_id, *, level=1):
"""Non-destructive read of [window start, current step count] in base-lattice units."""
with self._lock, self.ctx.activate():
self._require("ready")
if not self._observe_window:
raise RuntimeError("window exchange observation is disabled")
if not self._window_valid:
raise RuntimeError("no valid exchange window; call start_exchange_window()")
duration = self._step_count - self._window_start
if duration < 1:
raise RuntimeError("exchange window requires at least one completed base interval")
if (isinstance(level, bool) or not isinstance(level, Integral)
or not 0 <= level < len(self._levels)):
raise ValueError("level must name an existing level")
b = self._level_boundaries[level]
if (b is None or isinstance(body_id, bool) or not isinstance(body_id, Integral)
or not 0 <= body_id < len(b.host.objects)):
raise ValueError("body_id must name a body on this level")
rows = np.empty((len(b.tables["cell"]), 4), np.float64)
try:
cuda.memcpy_dtoh(rows, b.window_integral)
except BaseException:
self._status = "failed"
raise
return summarize_exchange(rows, b.tables["body_id"], b.tables["aperture"],
body_id=body_id, level=level, h=self._exchange_spacing[level],
time_end=self._step_count, window="window",
reference_center=self._exchange_centers[level][body_id],
duration=duration)
def read_raw_exchange(self, body_id, *, level=1, window="last_base_step"):
"""Read raw boundary reaction about its geometric center in the root edge frame.
@@ -885,6 +951,7 @@ class FixedPatchStepper:
if b is not None:
b.flags = b.device = b.kernel = None
b.last_raw = b.interval_integral = b.accumulate = None
b.window_integral = None
self._levels.clear()
self._pairs = []
if getattr(self, "_fine_curved", None) is not None:
+1
View File
@@ -85,6 +85,7 @@ class _BlockedBoundary:
last_raw: object = None
interval_integral: object = None
accumulate: object = None
window_integral: object = None # optional observation-window FP64 ledger
def _prepare_blocked_boundary(boundary, layout, cfg, jets, *, _shifted_blocked=False):