#!/usr/bin/env python3
"""
The same-machine reference for the revised exactness check (V1-revised, prelock_amendment.md), October 13, 2026.

V1 as preregistered (prelock_checks_plan.md) asked every re-run to reproduce its saved run exactly. On this machine
the cause test's own base-run code does so for old starts 13 to 18 but not for 1 to 12. The saved runs of 1 to 12 were
made in pieces that paused, which most likely explains it (prelock_amendment.md, "The cause"; corrected at about 10:25
Halifax: this note first said the saved runs were made on another machine, an inference the later starts did not
support). This runs the cause test's base-run code (ct.base_run, unchanged), in one go, for the given old
starts on this machine, with its own seeds and settings, and compares it with:
  (a) the saved run: where the first difference appears (the original V1, recorded as it is);
  (b) the calibration's re-run of the same start, if it is done: coherence (to complex64), every log growth of the
      gap and the settling, to the last digit;
  (c) feasibility's base run of the same start, if it is done: coherence at full precision and every kept state
      (T - 500, T - 100 and T of each moment), to the last digit.
It records the environment (processor, Python, C library, numpy and its routines) and the settings. It changes
nothing, runs no new start, makes no change to any copy, and reads no growth after a change.

A sandbox script kept with the project. Nothing here is served by the site.

Usage (runs in the foreground and ends by itself; nothing is left running)
    timeout -k 10 285 python3 -B prelock_machine_check.py 8 9 10 11 12 13 14     # old start numbers, 7 at a time
"""
import json
import sys
import time
from multiprocessing import Pool

sys.dont_write_bytecode = True
from prelock_common import (HERE, WORK, WORKERS, N, K, EPS, DT, GAP, EVERY, RUN, ct, nt, stamp, shown,  # noqa: E402
                            write_json, lock_changes, machine)
import numpy as np  # noqa: E402

OUT_JSON, OUT_TXT = HERE / "machine_check.json", HERE / "machine_check.txt"
SETTINGS = dict(n=N, k=K, eps=EPS, dt=DT, settle=nt.SETTLE, run=RUN, gap=GAP, put_back_every=EVERY,
                gap_seed=nt.LYAP_SEED, starting_phases="nt.starting_phases(N, slow, seed, 1)",
                gap_draw="np.random.default_rng((gap_seed, seed))")


def first_difference(a, b):
    """The first index along the last axis at which a and b differ (None if they never do)."""
    differ = np.flatnonzero(np.asarray(a != b).reshape(-1, a.shape[-1]).any(0))
    return int(differ[0]) if len(differ) else None


def check(s):
    feasibility = WORK / "feasibility" / f"base{s + 1:02d}.npz"
    times = ()
    if feasibility.exists():
        with np.load(feasibility) as p:
            times = tuple(int(t) for t in p["times"])
    res = ct.base_run(s, keep=times)                       # keeping copies of states leaves its arithmetic as it is
    z, logs, trace = res["z"][0].astype(np.complex64), res["logs"][0], res["trace"][0]
    saved = ct.saved_run(s)
    out = dict(start=s + 1, seed=s, seconds=round(res["seconds"], 1),
               saved=dict(settling=bool(np.array_equal(trace, saved["trace"])),
                          first_gap_interval=first_difference(logs, saved["logs"][:len(logs)]),
                          first_coherence_time=first_difference(z, saved["z"][:, :z.shape[-1]])))
    part = WORK / "calibration" / f"start{s + 1:02d}.npz"
    if part.exists():
        with np.load(part) as p:
            out["calibration"] = dict(coherence=bool(np.array_equal(z, p["z"])), gap=bool(np.array_equal(logs, p["logs"])),
                                      settling=bool(np.array_equal(trace, p["trace"])))
    if times:
        with np.load(feasibility) as p:
            kept = len(res["states"]) == len(times) and all(
                np.array_equal(u[0], ku) and np.array_equal(f[0], kf) for (u, f), ku, kf in zip(res["states"], p["us"], p["uf"]))
            out["feasibility"] = dict(coherence=bool(np.array_equal(res["z"][0], p["z"])), kept_states=bool(kept))
    return out


def main(starts):
    before = lock_changes()
    env = machine()
    clock = time.time()
    with Pool(min(WORKERS, len(starts))) as pool:
        results = pool.map(check, starts)
    after = lock_changes()
    record = dict(made=stamp(), machine=env, settings=SETTINGS, seconds=round(time.time() - clock, 1),
                  results=results, lock_unchanged=not before and not after)
    old = json.loads(OUT_JSON.read_text()) if OUT_JSON.exists() else dict(checks=[])
    old["checks"].append(record)
    write_json(OUT_JSON, old)
    lines = [f"Machine check, {shown(record['made'])}: the cause test's unchanged base-run code, in one go, on "
             f"{env['cpu']} (Python {env['python']}, {env['libc']}, numpy {env['numpy']}; dispatch "
             f"{', '.join(env['numpy_dispatch'])}; BLAS threads {env['blas_threads']}; container started "
             f"{shown(env['container_started'])})"]
    for r in results:
        sv = r["saved"]
        gap_t = None if sv["first_gap_interval"] is None else 5 * (sv["first_gap_interval"] + 1)
        line = (f"  start {r['start']}: against the saved run, settling same {sv['settling']}, first gap difference "
                f"at t = {gap_t}, first coherence difference at t = {sv['first_coherence_time']}")
        if "calibration" in r:
            line += f"; against the calibration's re-run, every number the same: {r['calibration']}"
        if "feasibility" in r:
            line += f"; against feasibility's base run, the same to the last digit: {r['feasibility']}"
        lines.append(line + f" ({r['seconds']:.0f} s)")
    lines.append(f"  the cause test's locked files unchanged: {record['lock_unchanged']}; {record['seconds']:.0f} s")
    with open(OUT_TXT, "a") as f:
        f.write("\n".join(lines) + "\n\n")
    print("\n".join(lines))


if __name__ == "__main__":
    numbers = [int(x) for x in sys.argv[1:]]
    if not numbers or any(not 1 <= x <= 18 for x in numbers):
        sys.exit(__doc__)
    main([x - 1 for x in numbers])
