From bb83b3476d20a6e618c76f836529d6db2bc20254 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 15:27:48 +0000 Subject: [PATCH] =?UTF-8?q?research:=20VIV=20driver=20=E2=80=94=20exact=20?= =?UTF-8?q?seed=20window,=20error=20status,=20no=20overwrite,=20bounded=20?= =?UTF-8?q?smoke=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/validation/leontini_viv_predeclare.md | 13 ++++++++----- research/viv_re150/run.py | 20 ++++++++++++++++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/docs/validation/leontini_viv_predeclare.md b/docs/validation/leontini_viv_predeclare.md index c16adae..647169a 100644 --- a/docs/validation/leontini_viv_predeclare.md +++ b/docs/validation/leontini_viv_predeclare.md @@ -49,8 +49,10 @@ Claude, Grok and Astra at 1abfe954; implemented in research/viv_re150/. semantics: see the window section). Whether the measured force represents the interval's start, midpoint or end is not asserted; the one-step lag is declared, not corrected. -- Initial state: fluid at rest except the uniform inlet; y_0 = v_0 = 0; - F_last = 0 for the first step; geometry initialized at the declared centre. +- Initial state: fluid initialized by `Simulation.initialize()` to the uniform + 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 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, @@ -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. - 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 - inlet (our choice; the source gives no fluid initial condition). + source states), y_0 = v_0 = 0, F_last = 0; fluid uniform at U (as + initialized by the code; our choice, not the source's). - 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 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 diff --git a/research/viv_re150/run.py b/research/viv_re150/run.py index 31457c6..66d1940 100644 --- a/research/viv_re150/run.py +++ b/research/viv_re150/run.py @@ -40,14 +40,22 @@ def config(D=D, U=U): def main(): ap = argparse.ArgumentParser() 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() 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") 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() s, cyc = viv.Structure(D, U), viv.Cycles(D) 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)) + 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)) 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)) sim.initialize() 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 seed_step = n cyc.discard_current() @@ -92,14 +101,17 @@ def main(): status = "timeout_response_cycles" break 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: 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, columns=["n", "y", "v", "F_rhs_cme_wen", "F_cme", "F_wen", "F_obs", "node_cover_minus_uncover_y", "no_donor"]) - if status == "scored": + if status == "scored" and not args.max_steps: report["score"] = viv.score(cyc.window) with open(os.path.join(args.out, "report.json"), "w") as fh: json.dump(report, fh, indent=1)