research: VIV driver — exact seed window, error status, no overwrite, bounded smoke flag

Seed quiet check reads states y_t, t in [n_seed - 5 T_n, n_seed); an
exception sets status 'error: ...' before the report is written; an output
directory that already holds report.json or steps.npy is refused; --max-steps
gives a plumbing smoke that is never scored. Predeclare states the code's
actual uniform-U fluid initial condition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-10-30 15:27:48 +00:00
co-authored by Claude Opus 5.5
parent f08b5e671f
commit bb83b3476d
2 changed files with 24 additions and 9 deletions
+8 -5
View File
@@ -49,8 +49,10 @@ Claude, Grok and Astra at 1abfe954; implemented in research/viv_re150/.
semantics: see the window section). Whether the semantics: see the window section). Whether the
measured force represents the interval's start, midpoint or end is not measured force represents the interval's start, midpoint or end is not
asserted; the one-step lag is declared, not corrected. asserted; the one-step lag is declared, not corrected.
- Initial state: fluid at rest except the uniform inlet; y_0 = v_0 = 0; - Initial state: fluid initialized by `Simulation.initialize()` to the uniform
F_last = 0 for the first step; geometry initialized at the declared centre. inlet velocity U everywhere (the code's actual initial condition; the
source gives no fluid initial condition), body at rest at the declared
centre, y_0 = v_0 = 0, F_last = 0.
- Structural update over one step with constant force F = F_last (exact - Structural update over one step with constant force F = F_last (exact
harmonic solution, zeta = 0): y_e = F/K, w = 2 pi f_n, harmonic solution, zeta = 0): y_e = F/K, w = 2 pi f_n,
y^{n+1} = y_e + (y^n − y_e) cos w + (v^n / w) sin w, y^{n+1} = y_e + (y^n − y_e) cos w + (v^n / w) sin w,
@@ -67,10 +69,11 @@ Clocks: lattice dt = 1; T_n = 1/f_n = 5,000 steps. Response cycles are
counted from the measured signal; T_n is used only for the seed check. counted from the measured signal; T_n is used only for the seed check.
- Start: body at rest at the declared centre (the only initial condition the - Start: body at rest at the declared centre (the only initial condition the
source states), y_0 = v_0 = 0, F_last = 0; fluid at rest except the uniform source states), y_0 = v_0 = 0, F_last = 0; fluid uniform at U (as
inlet (our choice; the source gives no fluid initial condition). initialized by the code; our choice, not the source's).
- Seed (conditional, one time, declared): at step n_seed = 200,000 - Seed (conditional, one time, declared): at step n_seed = 200,000
(t = 40 T_n), if max |y| over steps [175,000, 200,000) is below .01 D, then (t = 40 T_n), if max |y_t| over states t in [175,000, 200,000) is below
.01 D, then
in that step, after the check and before the candidate update, set in that step, after the check and before the candidate update, set
v_n += 2 pi f_n (.01 D) (F_last unchanged) and proceed normally; the v_n += 2 pi f_n (.01 D) (F_last unchanged) and proceed normally; the
geometry moves continuously through the update. `seed_applied`, step and geometry moves continuously through the update. `seed_applied`, step and
+16 -4
View File
@@ -40,14 +40,22 @@ def config(D=D, U=U):
def main(): def main():
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument("--out", required=True) ap.add_argument("--out", required=True)
ap.add_argument("--max-steps", type=int, default=0,
help="plumbing smoke only: stop after N steps; never scored")
args = ap.parse_args() args = ap.parse_args()
if os.path.realpath(args.out).startswith(os.path.realpath(os.path.join(os.path.dirname(__file__), "..", ".."))): if os.path.realpath(args.out).startswith(os.path.realpath(os.path.join(os.path.dirname(__file__), "..", ".."))):
raise SystemExit("--out must be outside the checkout") raise SystemExit("--out must be outside the checkout")
os.makedirs(args.out, exist_ok=True) os.makedirs(args.out, exist_ok=True)
for name in ("report.json", "steps.npy"): # never overwrite earlier evidence
if os.path.exists(os.path.join(args.out, name)):
raise SystemExit(f"{args.out} already holds {name}; use a fresh output directory")
cfg, nx, ny = config() cfg, nx, ny = config()
s, cyc = viv.Structure(D, U), viv.Cycles(D) s, cyc = viv.Structure(D, U), viv.Cycles(D)
T_n = 1.0 / s.f_n T_n = 1.0 / s.f_n
step_cap, n_seed = int(round(viv.STEP_CAP_TN * T_n)), int(round(viv.SEED_TN * T_n)) step_cap, n_seed = int(round(viv.STEP_CAP_TN * T_n)), int(round(viv.SEED_TN * T_n))
quiet = int(round(5 * T_n))
if args.max_steps:
step_cap = min(step_cap, args.max_steps)
centre = (30.0 * D, 0.5 * (ny - 1)) centre = (30.0 * D, 0.5 * (ny - 1))
from CelerisLab.body.motion import Translation from CelerisLab.body.motion import Translation
@@ -61,7 +69,8 @@ def main():
translation=Translation((0.0, 0.0, -1.5 * D, 1.5 * D), 0.5)) translation=Translation((0.0, 0.0, -1.5 * D, 1.5 * D), 0.5))
sim.initialize() sim.initialize()
for n in range(step_cap): for n in range(step_cap):
if n == n_seed and max((abs(r[1]) for r in rows[-int(5 * T_n):]), default=0.0) < viv.ELIGIBLE * D: # rows[k] holds the state y_{k+1}; the declared window is y_t, t in [n_seed - 5 T_n, n_seed)
if n == n_seed and max(abs(r[1]) for r in rows[n_seed - quiet - 1:n_seed - 1]) < viv.ELIGIBLE * D:
s.v += s.seed_velocity(D) # after the quiet check, before the update s.v += s.seed_velocity(D) # after the quiet check, before the update
seed_step = n seed_step = n
cyc.discard_current() cyc.discard_current()
@@ -92,14 +101,17 @@ def main():
status = "timeout_response_cycles" status = "timeout_response_cycles"
break break
else: else:
status = "timeout_native_steps" status = "smoke_stopped" if args.max_steps else "timeout_native_steps"
except BaseException as exc:
status = f"error: {exc!r}"
raise
finally: finally:
np.save(os.path.join(args.out, "steps.npy"), np.asarray(rows, float)) np.save(os.path.join(args.out, "steps.npy"), np.asarray(rows, float))
report = dict(status=status, steps=len(rows), t_U_over_D=len(rows) * U / D, T_n=T_n, report = dict(status=status, smoke=bool(args.max_steps), steps=len(rows), t_U_over_D=len(rows) * U / D, T_n=T_n,
seed_step=seed_step, counted_cycles=cyc.counted, cycles=cyc.log, seed_step=seed_step, counted_cycles=cyc.counted, cycles=cyc.log,
columns=["n", "y", "v", "F_rhs_cme_wen", "F_cme", "F_wen", "F_obs", columns=["n", "y", "v", "F_rhs_cme_wen", "F_cme", "F_wen", "F_obs",
"node_cover_minus_uncover_y", "no_donor"]) "node_cover_minus_uncover_y", "no_donor"])
if status == "scored": if status == "scored" and not args.max_steps:
report["score"] = viv.score(cyc.window) report["score"] = viv.score(cyc.window)
with open(os.path.join(args.out, "report.json"), "w") as fh: with open(os.path.join(args.out, "report.json"), "w") as fh:
json.dump(report, fh, indent=1) json.dump(report, fh, indent=1)