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>
48 lines
2.5 KiB
Python
48 lines
2.5 KiB
Python
"""Host-only reduction of raw cut-link observations; no stress/GI correction.
|
|
|
|
Rows are unique physical links. All quantities are 2D, per unit out-of-plane
|
|
thickness; centers are in the root edge frame, torque is about the named body.
|
|
"""
|
|
import numpy as np
|
|
|
|
|
|
def summarize_exchange(rows, body_ids, aperture, *, body_id, level, h,
|
|
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", "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)
|
|
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
|
|
aperture = np.asarray(aperture, bool)
|
|
|
|
def reduce(mask):
|
|
integral = values[mask].sum(axis=0, dtype=np.float64)
|
|
return dict(impulse=integral[:2].copy(), angular_impulse=float(integral[2]),
|
|
mass_into_fluid=float(integral[3]), force=integral[:2] / duration,
|
|
torque=float(integral[2] / duration), mass_rate=float(integral[3] / duration))
|
|
|
|
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(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)
|