tests/docs: unique-name driver loading; moving-path lifecycle and settings notes; VIV predeclare renamed

Tests load research drivers via importlib under unique module names
instead of a bare 'import run' (shared-process collision between the
Dütsch and VIV drivers). lifecycle.md qualifies immutable geometry and the
host-only get_flags for translating circles; supported_settings.md points to
the dense-only moving-circle feature; leontini_viv_predeclare.md is renamed
viv_re150_predeclare.md (Re 150 Kolahdouz/Qin case) with references updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-10-30 16:12:05 +00:00
co-authored by Claude Opus 5.5
parent 307468b671
commit 48ee0982c9
8 changed files with 47 additions and 26 deletions
+2 -2
View File
@@ -6,8 +6,8 @@ it is; it adds no framework. Line references are to candidate 0f8fdbc3.
| Class | Contents | How it changes | Owner rule |
|---|---|---|---|
| 1. Module and its globals | Compile-time (visible to the compiler, fixed per module): `config.h` macros incl. `NX`/`NY`, `COLLISION_MODEL`, `BLOCK_ROOT_BC`, `WALL_*`. Runtime per-module globals (not visible to the compiler; written by host code): `__constant__` `d_params`, `d_w`, `d_cx`, `d_cy`, `d_block_channel`, `d_block_wall`; plus bound kernel handles | Globals are written once by the owner right after `load_module`. Uniform grid: `Simulation._recompile` (construction, body-topology sync) loads a new module and re-uploads; `update_runtime_params` rewrites `d_params` (omega, force, n_objects) between runs. Steppers: never rewritten after construction, so they are immutable for the instance's lifetime although they are runtime values. The blocked slot/reservoir root reads its slot from the `d_block_wall` constant (the former dense route used `WALL_*` macros; both were bit-identical in the parity gates). | Every owner loads its own module (`compiler.load_module` = `module_from_file`, no cache). A loaded module is never shared between live owners: globals are per module, so sharing would share mutable `d_params`. |
| 2. Geometry, layout, tables | flags, curved-link tables, block layouts and halo pairs, transfer (explode/coalesce) tables, block kinds | Immutable for the life of an instance. Uniform grid: rebuilt by body sync. `FixedPatchStepper`: a geometry change is a new instance (`remap_finest`), committed only after a successful transfer. | No in-place table edits. |
| 3. Controls and dependent boundary columns | Jet amplitude -> per-link `surface_velocity`; body actions (uniform grid) | Between steps, without a table or module rebuild (`set_jet_amplitude`). | Input (index, level, amplitude) is validated before any device write; invalid input changes nothing and the instance stays usable. The host copy commits only after the upload returned. Any error after validation leaves the device column undefined and fails the instance: `FixedPatchStepper` status `failed`, `Simulation._failed`; every later device-touching public method (`Simulation`: `stream`, `set_jet_amplitude`, `initialize`, `sync_bodies`, `run`/`step`, `read_surface_exchange`, `read_body(s)`, `get_macroscopic`, `get_ddf`, `update_runtime_params`, `snapshot`, `restore`, save/load checkpoint) raises; cached host readers and host-side staging (e.g. `get_flags`, `add_body`/`remove_body` before a sync) do not touch the device and stay callable. `close()` still releases resources and stays retryable. |
| 2. Geometry, layout, tables | flags, curved-link tables, block layouts and halo pairs, transfer (explode/coalesce) tables, block kinds | Immutable for the life of an instance, except translating circles on the uniform grid: their swept-band flags and cut links are rewritten on the device every advancing step (`MovingCircleCells`/`MovingCircleLinks`, docs/moving_bodies.md); the host flag mirror is then stale for those cells. Uniform grid otherwise: rebuilt by body sync. `FixedPatchStepper`: a geometry change is a new instance (`remap_finest`), committed only after a successful transfer. | No in-place table edits. |
| 3. Controls and dependent boundary columns | Jet amplitude -> per-link `surface_velocity`; body actions (uniform grid) | Between steps, without a table or module rebuild (`set_jet_amplitude`). | Input (index, level, amplitude) is validated before any device write; invalid input changes nothing and the instance stays usable. The host copy commits only after the upload returned. Any error after validation leaves the device column undefined and fails the instance: `FixedPatchStepper` status `failed`, `Simulation._failed`; every later device-touching public method (`Simulation`: `stream`, `set_jet_amplitude`, `initialize`, `sync_bodies`, `run`/`step`, `read_surface_exchange`, `read_body(s)`, `get_macroscopic`, `get_ddf`, `update_runtime_params`, `snapshot`, `restore`, save/load checkpoint) raises; cached host readers and host-side staging (e.g. `get_flags` without translating circles, `add_body`/`remove_body` before a sync) do not touch the device and stay callable; with translating circles `get_flags` reads the device flags back and refuses after a failure. Translating-circle targets follow the same rule: `set_body` validates before mutation, the applied offset commits only after a successful synchronized step, and a failed moving step fails the instance (docs/moving_bodies.md). `close()` still releases resources and stays retryable. |
| 4. Per-step state | population double buffers and their parity, raw-exchange ledgers, observations | Device-only during `step(n)` / `run(n)`; buffer swaps are host-handle swaps. | Valid on the host only after the single wait at the end of the call. |
Storage routes are separate concerns:
+7
View File
@@ -82,6 +82,13 @@ Tests: `tests/unit/test_jets.py`, `test_jets_review.py`, `test_shifted_jets.py`,
- DFG 2D-1 Re 20 (V4, SRT, t* 60, pressure-NEQ outlet and local Zou-He inlet): refined and uniform
runs were NOT_STEADY under the frozen steady rule, so no interval verdict was given.
## Moving circles (dense `Simulation`, uniform grid only)
Translating circles with optional spin are supported on the dense double
buffer D2Q9 FP32 path only (not on `FixedPatchStepper` or refined levels).
API, supported/rejected modes and limits: `docs/moving_bodies.md`; stage-1
evidence: `docs/validation/moving_bodies_stage1.md`.
## Not supported in this release
3D, dynamic/automatic AMR (tagging, time-varying hierarchies beyond `remap_finest`),
+2 -2
View File
@@ -29,8 +29,8 @@ and the Mac evidence directory (not in git).
## Free transverse VIV (Kolahdouz §4.1.1 setup, Qin §4.5 target)
- Predeclaration: `docs/validation/leontini_viv_predeclare.md` (agreed at
`1abfe954`); run at `fb507aa1`, single case, D = 40, 75 D × 60 D.
- Predeclaration: `docs/validation/viv_re150_predeclare.md` (agreed at
`1abfe954` under its former name `leontini_viv_predeclare.md`); run at `fb507aa1`, single case, D = 40, 75 D × 60 D.
- Result: status `scored`, 336,384 steps (t U/D = 336.4); A_max = .5464 D vs
.520 D (+5.1 %, inside the declared ±10 % band); mean half peak-to-peak
.5448 D; f/f_n = .955; block drifts .78/.29/.18 %, last-five spreads
@@ -1,4 +1,6 @@
# Single-cylinder free transverse VIV — PREDECLARATION (agreed at 1abfe954)
# Single-cylinder free transverse VIV, Re 150 (Kolahdouz/Qin) — PREDECLARATION (agreed at 1abfe954)
(File renamed from `leontini_viv_predeclare.md`; the Leontini Re 200 case is only the queued second case below.)
Stage goal (Yanqi via TRON): prescribed Dütsch case + one VIV case both
"reasonably close" to a predeclared reference = development-stage success, then
+1 -1
View File
@@ -1,4 +1,4 @@
"""One-shot predeclared VIV run (docs/validation/leontini_viv_predeclare.md).
"""One-shot predeclared VIV run (docs/validation/viv_re150_predeclare.md).
Re 150, U* = 5, m/(rho D^2) = 2, zeta = 0, D = 40 on a 75 D x 60 D native
channel (uniform inlet, NEQ outlet, free-slip y walls), cylinder at
+1 -1
View File
@@ -1,6 +1,6 @@
"""Pure logic of the predeclared Re 150 free transverse VIV case (stdlib only).
Contract: docs/validation/leontini_viv_predeclare.md. Oscillator update with a
Contract: docs/validation/viv_re150_predeclare.md. Oscillator update with a
constant force over one lattice step, response-cycle tracking with fixed y = 0
crossings, eligibility, 10-cycle blocks, settling/scoring with deterministic
restart, and the conditional seed.
+16 -13
View File
@@ -11,6 +11,20 @@ _spec = importlib.util.spec_from_file_location(
p = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(p)
def _load(rel, name):
"""Load a research driver module under a unique name (no bare ``import run``)."""
import sys
path = Path(__file__).resolve().parents[2] / rel
sys.path.insert(0, str(path.parent)) # for the driver's own sibling imports
try:
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
finally:
sys.path.remove(str(path.parent))
return mod
T = 2520
@@ -93,12 +107,7 @@ def test_exact_point_inside_body_is_missing_even_with_fluid_corners():
def test_generated_config_passes_the_real_loader(tmp_path):
import json
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "research/duetsch_re100_kc5"))
try:
import run
finally:
sys.path.pop(0)
run = _load("research/duetsch_re100_kc5/run.py", "duetsch_run")
from CelerisLab.config import load_lbm_config
cfg_dict, u_max, n = run.config(20, 10.0, 2520)
path = tmp_path / "config_lbm.json"
@@ -111,13 +120,7 @@ def test_generated_config_passes_the_real_loader(tmp_path):
def _analyze():
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "research/duetsch_re100_kc5"))
try:
import analyze
finally:
sys.path.pop(0)
return analyze
return _load("research/duetsch_re100_kc5/analyze.py", "duetsch_analyze")
def test_hf_energy_is_parseval_weighted():
+15 -6
View File
@@ -8,6 +8,20 @@ _spec = importlib.util.spec_from_file_location(
viv = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(viv)
def _load(rel, name):
"""Load a research driver module under a unique name (no bare ``import run``)."""
import sys
path = Path(__file__).resolve().parents[2] / rel
sys.path.insert(0, str(path.parent)) # for the driver's own sibling imports
try:
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
finally:
sys.path.remove(str(path.parent))
return mod
D, U = 40, 0.04
@@ -72,12 +86,7 @@ def test_seed_reset_discards_incomplete_cycle_and_proof():
def test_viv_config_passes_the_real_loader(tmp_path):
import json
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "research/viv_re150"))
try:
import run
finally:
sys.path.pop(0)
run = _load("research/viv_re150/run.py", "viv_run")
from CelerisLab.config import load_lbm_config
cfg_dict, nx, ny = run.config()
path = tmp_path / "config_lbm.json"