# src coherence stage (from main 914906e) Owner direction 2026-10-28: unified style over historical compatibility, inside `src`. No numerical change is intended; kernel text was moved verbatim (PTX/bit parity is pending a GPU run). ## Conventions (defined once, in `lbm.hierarchy.Hierarchy`) - Level `l = 0 .. depth-1` (0 = root). Pair `hierarchy.pairs[l-1]` builds level `l`. - Array shapes are `(ny, nx)`; rectangles, origins and centers are `(x, y)`. - Coarse/fine name the two members (parent/child) of ANY pair, not absolute L0/L1: `fine_shape`, `fine_origin`, `prepare_fine_boundary`, `blocked_fine_tables`, `blocked_coarse_tables`. - Refined levels (l >= 1) of FixedPatchStepper place bodies/Jets in the root EDGE frame (root cell `i` = `[i, i+1)`). Uniform `Simulation` objects AND the static circles of a single-level (depth 1) FixedPatchStepper use the integer-NODE frame. Nothing converts between them. This concerns input placement only: within the EDGE frame, refined levels still project root-edge coordinates to fine-local cell indices with the half-cell shift (`prepare_fine_boundary`: `(v - o)/dx - .5`; hits back via `fine_hits_to_edge_frame` `origin + dx*(hit + .5)`), using `edge_frame`/`fine_origin` below. - `Hierarchy.edge_frame(l)` -> `((ox, oy), h)`: cell `(i, j)` of level `l`'s allocated array (ghost band and holes included) covers `[o + h*i, o + h*(i+1))`. A scalar FixedPatch2D window carries the half-coarse-cell ghost shift `bounds - HALO/2` in `fine_origin`; a set's level-wide grid has origin 0. Level 1 boundaries are built from the root config (the wall-aware fields live there); deeper ones from the parent frame. - `FixedPatch2D.fine_level_offset` (was `fine_level_origin`) is an array INDEX offset `(y, x)`, not an `(x, y)` origin. ## FixedPatchStepper migration map | removed / changed | replacement | |---|---| | `patch`, `child_patch` | `hierarchy.pairs[0]`, `hierarchy.pairs[1]` | | `base_layout`, `fine_layout`, `child_layout` (stepper properties) | `layouts[0]`, `layouts[1]`, `layouts[2]` (`FixedPatchSet2D.fine_layout(block)` is unchanged) | | `fine_boundary`, `child_boundary` | `boundaries[1]`, `boundaries[2]` (host geometry; `None` = no bodies/Jets) | | `_jets` | `jets[1]` | | `_boundaries[l]` (dict from L1, level keys), `_level_boundaries` (b4eacf7), `_device_boundaries` (65339ab) | `_boundaries[l]`: the one combined per-level `_BlockedBoundary` record (list from L0, `None` at L0 unless single-level static circles): host geometry `.host`, `.tables`, `.jets`, `.jet_cache`, exchange `.spacing`/`.centers`, device handles | | `_fine_tables`, `_actuated`, `_jet_cache` | `_boundaries[1].tables` / `.actuated` / `.jet_cache` | | `_level_jets` (private) | `_boundaries[l].jets` (public `jets[l]` is derived) | | `_exchange_spacing[l]`, `_exchange_centers[l]` (private) | `_boundaries[l].spacing` (h_l, root units) / `.centers` (root edge frame); only levels with a boundary report exchange | | `_level_blocks` (private) | `_layouts[l].block` | | `_remap_cfg` (private) | `_root_cfg` | | `refinement_tables.blocked_base_tables` (also via `refinement_stepper`) | `refinement_tables.blocked_coarse_tables` (import from `refinement_tables`; the stepper no longer re-exports) | | `_fine_leaf`, `_child_leaf` | `leaf[1]`, `leaf[2]` (copies) | | `_middle_kind` | `hierarchy.kinds[1]` | | `_boundary_hosts` (private) | public `boundaries[l]` = `_boundaries[l].host` (host geometry only; the private record is the combined one) | | `remap_child(rects, child_objects=...)` | `remap_finest(rects, objects=...)` | | `set_jet_amplitude(i, a)` (implicit level 1) | `set_jet_amplitude(i, a, level=l)` (required) | | `read_raw_exchange(b)` / `read_exchange_window(b)` (implicit level 1) | `read_raw_exchange(b, level=l)` / `read_exchange_window(b, level=l)` (required) | | `_level_name(l)` "base/fine/child" messages | `"level l"` | | `refinement.MIN_PATCH_GAP` | deleted (no gap policy exists) | | `params.cuh HaloPlan`, `collide_trt_no_force` | deleted (zero references) | Kept: `step_count`, constructor keywords (`refinements`, `level_blocks`, `level_objects`, `level_jets`, `coarse_boundary`, `explosion`), `Simulation.set_jet_amplitude(i, a)` (one level), `RemapTopology.from_config(..., child_objects=, child_jets=)` (internal remap helper; "child" = the replaced level relative to its parent). ## Responsibilities - Construction: `_check_levels` (inputs), `_kernel_configs` (per-level kernel configs), `Hierarchy.edge_frame` (geometry frames), `_build_level` / `_build_pair` / `_upload_boundary` (device). Observation reads share `_observed_boundary` + `_summarize`. - Lifecycles: `_layouts`, `_level_cfgs`, `_boundaries`, `_leaf`, `_state` are the immutable construction plan (only a record's Jets/velocity column change, in `set_jet_amplitude`). They are built and read by `_planned_bytes()` before any device allocation and stay readable after `close()` (public views). `_Level` stays device-only: created after the memory preflight, cleared on `close()`; its `.layout`/`.boundary` are references into the plan for `_hop`. Folding the plan into `_Level` would mix both lifecycles in one record. - Blocked kernels: `operators/block_collision.cuh block_collide` is the one collision extension point of `BlockStep` and `BlockChannelStep`. MRT shares one path; SRT keeps its two explicit arithmetic variants (level: unclamped, no-force, shifted direct-momentum feq; channel: uniform OneStep arithmetic with omega clamp), selected by `BLOCK_ROOT_BC`. - `explosion="projected_linear"` stays an optional capability; `"constant"` stays default. ## Not done / open - Two per-level config derivations stay distinct on purpose: `Hierarchy.configs` (`_middle_config`, wall fields reset on every refined level, geometry validation) vs. `FixedPatchStepper._kernel_configs` (reset only on level 1 of a slot_reservoir root; generates each level's config.h). Folding them needs a config.h/PTX diff gate. - No single-`Simulation` facade over FixedPatchStepper: the two own different state models (uniform node frame, body sync, forcing, checkpoint vs. immutable blocked hierarchy, edge frame, remap-as-new-instance). A facade would only forward calls; no concrete need found. - Tests, examples and validation callers are migrated separately (pinned runners stay frozen).