From ae53fade1366a204269a4aef123bed24f0f400e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 02:45:30 +0000 Subject: [PATCH] Contract: name the in-EDGE half-cell projection; repair DFG runner current-checkout Jet path - No NODE<->EDGE input conversion still leaves the root-edge -> fine-local half-cell projection (prepare_fine_boundary, fine_hits_to_edge_frame); say so. - run_refined_dfg.py (jet_scenario != none, current checkout only): pass level=1 to FixedPatchStepper.set_jet_amplitude on reversal; use blocked_coarse_tables there. jet_scenario=none keeps the pinned source's blocked_base_tables; manifests and results unchanged. Co-Authored-By: Claude Opus 5.5 --- docs/development/src_coherence.md | 5 ++++- src/CelerisLab/lbm/hierarchy.py | 4 +++- tests/validation/run_refined_dfg.py | 18 ++++++++++++------ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/development/src_coherence.md b/docs/development/src_coherence.md index 8d90040..8d537b0 100644 --- a/docs/development/src_coherence.md +++ b/docs/development/src_coherence.md @@ -14,7 +14,10 @@ pending a GPU run). - 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. + 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 diff --git a/src/CelerisLab/lbm/hierarchy.py b/src/CelerisLab/lbm/hierarchy.py index 09adc62..bca3333 100644 --- a/src/CelerisLab/lbm/hierarchy.py +++ b/src/CelerisLab/lbm/hierarchy.py @@ -32,7 +32,9 @@ class Hierarchy: EDGE frame: root cell i covers [i, i+1), center i+.5. Uniform Simulation objects and the static circles of a single-level (depth 1) FixedPatchStepper use the integer-NODE frame (CircleGeometry.build_flag_mask at integer (x, y)). No - conversion between the two frames is applied anywhere; + NODE<->EDGE input conversion is applied anywhere; inside the EDGE frame, refined + boundaries still project root-edge coordinates to fine-local cell indices with + the half-cell shift ((v - o)/h - .5, see prepare_fine_boundary); * cell (i, j) of level l's allocated array (ghost band and holes included) covers [o + h*i, o + h*(i+1)) with (o, h) = edge_frame(l); level 1 is built from the root config directly, deeper levels from their parent frame (see edge_frame). diff --git a/tests/validation/run_refined_dfg.py b/tests/validation/run_refined_dfg.py index 9cbeb70..3e0eeb7 100644 --- a/tests/validation/run_refined_dfg.py +++ b/tests/validation/run_refined_dfg.py @@ -81,7 +81,7 @@ def public_jets(center, radius, scenario): ) -def jet_interval(handle, scenario, begin, n, diameter): +def jet_interval(handle, scenario, begin, n, diameter, level=None): """Apply a reversal only at a completed interval boundary, before advancing.""" if scenario != "none": boundaries = [round(t * diameter / SPEED) for t in (250, 260, 270, 280, 290, 300, 305)] @@ -92,8 +92,10 @@ def jet_interval(handle, scenario, begin, n, diameter): if begin < switch: return min(n, switch - begin), None if begin == switch: - handle.set_jet_amplitude(0, -JET_PEAK) - handle.set_jet_amplitude(1, JET_PEAK) + # FixedPatchStepper needs an explicit Jet level; Simulation has one level. + kw = {} if level is None else {"level": level} + handle.set_jet_amplitude(0, -JET_PEAK, **kw) + handle.set_jet_amplitude(1, JET_PEAK, **kw) return n, dict(step=begin, tstar=300.0, amplitudes=[-JET_PEAK, JET_PEAK]) return n, None @@ -256,7 +258,7 @@ def host_geometry(source, h, collision="SRT", jet_scenario="none", placement="wi validate_channel_config, validate_channel_transfers, ) - from CelerisLab.lbm.refinement_tables import blocked_base_tables + from CelerisLab.lbm import refinement_tables from CelerisLab.body.geometry.circle import CircleGeometry from CelerisLab.body.objects import SimObject @@ -268,7 +270,11 @@ def host_geometry(source, h, collision="SRT", jet_scenario="none", placement="wi pair = FixedPatch2D.from_config(cfg, PLACEMENTS[placement]) layout = BlockLayout2D.build(cfg.ny, cfg.nx, *BASE_BLOCK) validate_channel_config(cfg, [BASE_BLOCK, FINE_BLOCK]) - targets = blocked_base_tables(pair, layout, np.argsort(pair.coalesce_coarse))[1] + # Historical pinned source (jet_scenario none) names it blocked_base_tables; + # the current checkout (public Jet scenarios) names it blocked_coarse_tables. + coarse_tables = (refinement_tables.blocked_base_tables if jet_scenario == "none" + else refinement_tables.blocked_coarse_tables) + targets = coarse_tables(pair, layout, np.argsort(pair.coalesce_coarse))[1] validate_channel_transfers(pair, layout, targets) BlockLayout2D.build(*pair.fine_shape, *FINE_BLOCK, periodic=(False, False)) body = SimObject( @@ -448,7 +454,7 @@ def run_case(a, h, periodic, case): ) n = min(max(1, round(width * p["diameter"] / SPEED)), max_steps - begin) n, change = jet_interval(backend if refined else sim, a.jet_scenario, - begin, n, p["diameter"]) + begin, n, p["diameter"], level=1 if refined else None) if change is not None: amplitude_changes.append(change) impulse = np.zeros(2)