"""
n23 — The equivalence-principle test. Does the medium weigh ENERGY,
or does it weigh a particular KIND of energy?

The framework's known debt (registered since the ledger): the equivalence
principle is ASSUMED (monitoring rate ~ E), not derived. This script asks the
lattice directly. Three objects of EQUAL stored energy but different nature,
in the 1D fast solver (mode-basis exact Gaussian NESS, uniform matched bath,
Hartree dressing as n18/n20/n21):

  (a) a MONITORED GRAIN   — noise energy   (n13 self-consistent record noise,
                            strain profile w = d/dx Gaussian, rate gamma_a)
  (b) a COHERENT BUMP     — field energy   (static stiffness profile
                            B_i = h_b exp(-(i-x0)^2/2 BW^2) added to mu^2)
  (c) a WARM SITE         — thermal energy (fixed-strength momentum diffusion
                            on a Gaussian profile: white-noise force = the
                            classical high-T drive; the uniform bath eta
                            carries the heat away -> a warm spot in NESS)

ENERGY — one functional for all three (pre-registered, primary):
  E_med(X) = sum_{|i-x0|<=W} [eps_i(X) - eps_i(vac)],  W = 40,
  eps_i = 1/2<p_i^2> + 1/2 MU2 <x_i^2> + 1/4[<(x_{i+1}-x_i)^2> + <(x_i-x_{i-1})^2>]
  i.e. the BARE uniform Hamiltonian's energy density, evaluated in each
  object's converged NESS. Nature-blind: it never references the object's
  coupling. Secondary (reported, gate EP2): E_tot = E_med + E_int with
  E_int = 1/2 sum_i B_i <x_i^2> (only the bump has a Hamiltonian coupling;
  for a and c, E_tot = E_med). The E_med/E_tot fork is physics, not
  bookkeeping: it decides whether the medium weighs its own deformation
  energy or total energy including the external potential.

CALIBRATION: E* := E_med(grain, gamma_a = 0.05, g = 1) = 0.6114 (pilot).
  h_b and s_th are root-found so that E_med(b) = E_med(c) = E* to 0.1%.
  Pilot values: h_b ~ 2.83, s_th ~ 0.0566.

PASSIVE WEIGHT (how the standard field pulls on X):
  standard source S = monitored grain, gamma_S = 0.05, at d = 24 from X.
  F on X by the ABBA-subtracted T^xx discontinuity around X (n18 observable,
  object-agnostic — this is why stress, not HF, is the passive primary).
  Pump subtraction: dF_p = F(g=1) - F(g=0). At g = 0 a noisy object has zero
  dressing (m = 3g sum dx2 = 0), so F(g=0) is pure pump — legitimate to
  remove. Every passive config contains the noisy S, so the rule is uniform.

ACTIVE WEIGHT (how X pulls on the standard test mass):
  standard test bump T: h_t = 0.2, BW = 2, at d = 24 from X. F on T by
  Hellmann-Feynman (n20/n21 observable), background-subtracted (T alone).
  dF_a = F(g=1) - F(g=0) for noisy X; for X = bump the config is fully
  static and F(g=0) IS the signal (Casimir of the profiles), so
  dF_a := F(g=1) with null N1 checking g-insensitivity.
  All forces are reported ATTRACTION-POSITIVE (stress F_att as in n18;
  active: -dF_HF, since T sits on the +x side of X).

PRE-REGISTERED GATES
 V0   undriven NESS == ground state (exact by construction; printed).
 V1   HF force on a static bump pair vs exact -dE0/dx_B: <= 5%
      (pilot: 0.3%).
 V2   stress-ABBA force vs HF on the same static pair: <= 10%
      (pilot: 1.7%; also fixes the attraction sign).
 V3   transparency length from the standard grain pair, d in {16,24,32,40}:
      lambda reported and used in the law cross-check (pilot: 9.8 sites —
      the uniform eta = 0.05 bath screens; EP at fixed d is unaffected).
 N1   bump g-insensitivity: |F_a(bump, g=1)/F_a(bump, g=0) - 1| <= 0.10.
 N2   test-bump linearity: F_a(grain) doubles with h_t (0.1 -> 0.2)
      within 10% (probe regime).
 N3   energy-window stability: |E(W=60)/E(W=40) - 1| <= 0.15 per object
      (pilot, grain: 6.9%).
 N4   composition control: warm variant c' = fixed-strength diffusion on
      the GRAIN'S OWN strain profile w. c' vs c isolates profile
      (composition) dependence; c' vs a isolates the role of
      self-consistency (records) alone.
 EP-P r_p(X) = dF_p(X)/E*: PASS if all three within +-20% of their
      geometric mean.
 EP-A r_a(X) = dF_a(X)/E*: same +-20% criterion.
 EP-R reciprocity: dF_a(X)/dF_p(X) constant across X to +-20%
      (active and passive weight are one thing — Newton's third law
      across natures; NOT built in, the two observables and partners
      differ).
 EP2  the bump row recomputed with E_tot instead of E_med. If EP-P fails
      with E_med but passes with E_tot, the medium weighs total energy
      including the pinning potential — a sharp statement, not a failure.
 LAW  cross-check against the measured force law
      F = xi (phi2_vac/2) m_A m_B exp(-d/lambda) / d^(D-1):
      dF_p(X)/dF_p(a) must equal m_X/m_a to +-30%, with
      m = 3 g sum_i dx2_i for noisy objects (the Hartree dressing the
      monitoring sustains) and m = sum_i B_i for the bump.
 REP  the full EP row repeated at d = 40 (screened regime: magnitudes drop
      ~x5, ratios must not move).

PILOT DISCLOSURE (honesty ledger, run 2026-07-05, same solver):
  phi2_vac(center) = 0.9525;  E* = 0.6114;  m_a = 4.546;  m_c = 5.466;
  m_b = sum B ~ 14.2;  lambda = 9.8.
  Passive magnitudes were PEEKED at near-calibrated knobs:
    dF_p(a) = +2.46e-3, dF_p(c) = +1.57e-3, dF_p(b) = +1.29e-2
  -> r_p spread ~ x8 with E_med (EP-P likely FAILS as registered), and the
  bump lands within ~15% of the grain if E_tot is used (EP2 likely the
  story). The ACTIVE column, reciprocity EP-R, N4 and REP rows are still
  blind at registration time. Expected active magnitude (grain -> test
  bump, d=24): dF_HF ~ -6.8e-3, i.e. F_att ~ +6.8e-3.

Known debts inherited: Hartree (leading order), 1D, preferred frame,
white-noise records, no error bars beyond the +-5% ABBA symmetry spread.
Runtime: ~2-4 min (numpy, N = 256; eigh(256) per Hartree iteration).
"""

import sys
import time
import numpy as np

np.seterr(divide='ignore', over='ignore', invalid='ignore')

N       = 256
MU2     = 4e-4
ETA     = 0.05
G       = 1.0        # interacting medium (n18-n21 convention)
SMEAR   = 2.0        # grain strain-profile width (n18 1D)
BW      = 2.0        # bump / warm-profile width
CENTER  = N // 2
W_EN    = 40         # energy window half-width (pre-registered)
OFF     = 8          # stress-jump offset (n18)
D_MAIN  = 24
D_REP   = 40
GAMMA_S = 0.05       # standard source grain
GAMMA_A = 0.05       # object (a); defines E*
H_T     = 0.2        # standard test bump
ALPHA   = 0.7
TOL     = 1e-10
MAXIT   = 80
T0      = time.time()

def say(m):
    print(f"[{time.time()-T0:6.1f}s] {m}", flush=True)

# ---------------------------------------------------------------- lattice ---

def Kmat(mu2):
    K = np.diag(2.0 + mu2)
    K -= np.diag(np.ones(N - 1), 1) + np.diag(np.ones(N - 1), -1)
    return K

def grain_w(site):
    """n13/n18 grain: fluctuating index coupled to the local strain."""
    idx = np.arange(N)
    g = np.exp(-0.5 * ((idx - site) / SMEAR) ** 2)
    g[np.abs(idx - site) > 4 * SMEAR] = 0.0
    c = np.zeros(N)
    c[:-1] = np.diff(g)
    return c / np.linalg.norm(c)

def gauss_prof(site, h, bw=BW):
    idx = np.arange(N)
    return h * np.exp(-0.5 * ((idx - site) / bw) ** 2)

def gauss_prof_dx(site, h, bw=BW):
    idx = np.arange(N)
    return h * ((idx - site) / bw ** 2) * np.exp(-0.5 * ((idx - site) / bw) ** 2)

def warm_prof(site):
    v = gauss_prof(site, 1.0)
    return v / np.linalg.norm(v)

# ------------------------------------------------- fast NESS (mode basis) ---

class Ness:
    """Exact Gaussian NESS, uniform matched bath, closed-form 2x2 blocks.
    records  = ((w, gamma), ...)  n13 self-consistent record noise
    thermals = ((v, s), ...)      fixed-strength momentum diffusion"""
    def __init__(self, mu2, records=(), thermals=()):
        self.w2, self.U = np.linalg.eigh(Kmat(mu2))
        self.w = np.sqrt(np.maximum(self.w2, 1e-14))
        h = ETA / 2
        a = self.w2[:, None]; b = self.w2[None, :]
        den = 2 * h * (a + b) + (b - a) ** 2 / (4 * h) + 4 * h ** 3
        f11 = 1.0 / den
        f22 = f11 * ((a + b) / 2 + 2 * h * h)
        self.Xxx = np.zeros((N, N)); self.Xpp = np.zeros((N, N))
        for v, s in thermals:
            vt = self.U.T @ v
            P = np.outer(vt, vt)
            self.Xxx += s * f11 * P
            self.Xpp += s * f22 * P
        wts = [self.U.T @ w for w, _ in records]
        gam = [gv for _, gv in records]
        ng = len(records)
        self.svals = np.zeros(ng)
        if ng:
            Xg = [(f11 * np.outer(wt, wt), f22 * np.outer(wt, wt)) for wt in wts]
            bvec = np.array([np.sum(wt ** 2 / (2 * self.w)) + wt @ self.Xxx @ wt
                             for wt in wts])
            M = np.array([[gam[j] * (wts[i] @ Xg[j][0] @ wts[i])
                           for j in range(ng)] for i in range(ng)])
            ev = np.linalg.eigvals(M)
            if np.max(ev.real) >= 1.0:
                raise RuntimeError(f"parametric instability ({np.max(ev.real):.3f})")
            self.svals = np.linalg.solve(np.eye(ng) - M, bvec)
            for j in range(ng):
                self.Xxx += gam[j] * self.svals[j] * Xg[j][0]
                self.Xpp += gam[j] * self.svals[j] * Xg[j][1]

    def site_xx(self):
        return (self.U / (2 * self.w)) @ self.U.T + self.U @ self.Xxx @ self.U.T

    def pp_diag(self):
        return ((self.U * (self.w / 2)) * self.U).sum(1) \
             + ((self.U @ self.Xpp) * self.U).sum(1)

    def dx2(self):
        return ((self.U @ self.Xxx) * self.U).sum(1)

def hartree(g, base=None, records=(), thermals=()):
    base_v = np.full(N, MU2) if base is None else base
    mu2 = base_v.copy()
    for it in range(MAXIT):
        ns = Ness(mu2, records, thermals)
        tgt = base_v + 3.0 * g * ns.dx2()
        new = (1 - ALPHA) * mu2 + ALPHA * tgt
        delta = np.max(np.abs(new - mu2))
        mu2 = new
        if delta < TOL:
            break
    else:
        say(f"    [warn] Hartree not converged (delta {delta:.1e})")
    return Ness(mu2, records, thermals), mu2

# ------------------------------------------------------------ observables ---

def energy_density(ns):
    """Bare-H energy density per site (MU2 uniform), bonds split half/half."""
    Sxx = ns.site_xx()
    xx = np.diag(Sxx); ox = np.diag(Sxx, 1)
    ppd = ns.pp_diag()
    bond = 0.5 * (xx[:-1] + xx[1:] - 2 * ox)
    eps = 0.5 * ppd + 0.5 * MU2 * xx
    eps[:-1] += 0.5 * bond
    eps[1:]  += 0.5 * bond
    return eps

def stress(ns, mu2):
    """T^xx per bond with the config's own converged stiffness (n18)."""
    Sxx = ns.site_xx()
    xx = np.diag(Sxx); ox = np.diag(Sxx, 1)
    ppd = ns.pp_diag()
    pp = 0.5 * (ppd[:-1] + ppd[1:])
    dx2b = xx[:-1] + xx[1:] - 2 * ox
    mxx = 0.5 * (mu2[:-1] * xx[:-1] + mu2[1:] * xx[1:])
    return 0.5 * pp + 0.5 * dx2b - 0.5 * mxx

def hf_force(ns, dBdx):
    return -0.5 * float(dBdx @ np.diag(ns.site_xx()))

def force_att(T_by, a, b):
    dT = T_by["AB"] - T_by["A0"] - T_by["0B"] + T_by["00"]
    FA = dT[a - OFF] - dT[a + OFF - 1]
    FB = dT[b + OFF - 1] - dT[b - OFF]
    return 0.5 * (FA + FB)

# ----------------------------------------------------------- object menu ----

def make_object(kind, site, knob):
    """-> (base_addition, records, thermals) for one object."""
    if kind == "grain":
        return np.zeros(N), ((grain_w(site), knob),), ()
    if kind == "bump":
        return gauss_prof(site, knob), (), ()
    if kind == "warm":
        return np.zeros(N), (), ((warm_prof(site), knob),)
    if kind == "warm_strain":                      # N4 variant c'
        return np.zeros(N), (), ((grain_w(site), knob),)
    raise ValueError(kind)

def solve_config(g, objects):
    """objects = ((kind, site, knob), ...) -> converged (ns, mu2)."""
    base = np.full(N, MU2)
    rec, th = [], []
    for kind, site, knob in objects:
        badd, r, t = make_object(kind, site, knob)
        base = base + badd
        rec += list(r); th += list(t)
    return hartree(g, base=base, records=tuple(rec), thermals=tuple(th))

# ----------------------------------------------------------------- stages ---

def stage_vacuum():
    ns, _ = solve_config(0.0, ())
    eps = energy_density(ns)
    say(f"V0: vacuum dx2 max = {np.max(np.abs(ns.dx2())):.1e}   "
        f"phi2_vac(center) = {np.diag(ns.site_xx())[CENTER]:.4f}")
    return eps, np.diag(ns.site_xx())[CENTER]

def E_of(ns, site, eps_vac, W=W_EN):
    d = energy_density(ns) - eps_vac
    return float(d[max(site - W, 0):min(site + W + 1, N)].sum())

def stage_gates(eps_vac):
    say("V1: HF vs exact -dE0/dx_B (static pair, h=0.5, d=24)")
    h0, d = 0.5, D_MAIN
    a, b = CENTER - d // 2, CENTER - d // 2 + d
    def E0_of(bp):
        m = np.full(N, MU2) + gauss_prof(a, h0) + gauss_prof(bp, h0)
        return 0.5 * np.sum(np.sqrt(np.linalg.eigvalsh(Kmat(m))))
    F_ex = -(E0_of(b + 1) - E0_of(b - 1)) / 2
    m = np.full(N, MU2) + gauss_prof(a, h0) + gauss_prof(b, h0)
    F_hf = hf_force(Ness(m), gauss_prof_dx(b, h0))
    ok1 = abs(F_hf / F_ex - 1) <= 0.05
    say(f"V1: HF = {F_hf:+.5e}  exact = {F_ex:+.5e}  "
        f"ratio = {F_hf/F_ex:.4f}  {'PASS' if ok1 else 'FAIL'}")
    say("V2: stress-ABBA vs HF on the same static pair")
    T_by = {}
    for tag, sites in (("00", ()), ("A0", (a,)), ("0B", (b,)), ("AB", (a, b))):
        mm = np.full(N, MU2) + sum((gauss_prof(s_, h0) for s_ in sites),
                                   np.zeros(N))
        T_by[tag] = stress(Ness(mm), mm)
    F_st = force_att(T_by, a, b)
    ok2 = abs(F_st / (-F_hf) - 1) <= 0.10
    say(f"V2: stress F_att = {F_st:+.5e}  (-HF = {-F_hf:+.5e})  "
        f"ratio = {F_st/(-F_hf):.4f}  {'PASS' if ok2 else 'FAIL'}")

def passive_force(X, d, g):
    """S = standard grain at a; X = (kind, knob) at b. Stress-ABBA F_att."""
    a, b = CENTER - d // 2, CENTER - d // 2 + d
    kind, knob = X
    T_by = {}
    for tag in ("00", "A0", "0B", "AB"):
        objs = []
        if "A" in tag: objs.append(("grain", a, GAMMA_S))
        if "B" in tag: objs.append((kind, b, knob))
        ns, mu2 = solve_config(g, tuple(objs))
        T_by[tag] = stress(ns, mu2)
    return force_att(T_by, a, b)

def active_force(X, d, g, h_t=H_T):
    """X at a; standard test bump T at b. HF on T, background-subtracted.
    Returns ATTRACTION-POSITIVE force (= -dF_HF)."""
    a, b = CENTER - d // 2, CENTER - d // 2 + d
    kind, knob = X
    dBdx = gauss_prof_dx(b, h_t)
    ns_bg, _ = solve_config(g, (("bump", b, h_t),))
    F_bg = hf_force(ns_bg, dBdx)
    ns, _ = solve_config(g, ((kind, a, knob), ("bump", b, h_t)))
    return -(hf_force(ns, dBdx) - F_bg)

def stage_calibration(eps_vac):
    say("CAL: object (a) sets the target")
    ns_a, mu2_a = solve_config(G, (("grain", CENTER, GAMMA_A),))
    E_star = E_of(ns_a, CENTER, eps_vac)
    E60 = E_of(ns_a, CENTER, eps_vac, 60)
    m_a = 3 * G * float(ns_a.dx2().sum())
    say(f"CAL: E* = {E_star:.5e}   E(W=60)/E(W=40) = {E60/E_star:.3f}   "
        f"m_a = {m_a:.4f}   s = {ns_a.svals}")

    say("CAL: bump h_b by bisection (bracket 0.5 .. 8)")
    lo, hi = 0.5, 8.0
    for _ in range(40):
        mid = 0.5 * (lo + hi)
        ns, _ = solve_config(G, (("bump", CENTER, mid),))
        if E_of(ns, CENTER, eps_vac) < E_star: lo = mid
        else: hi = mid
    h_b = 0.5 * (lo + hi)
    ns_b, _ = solve_config(G, (("bump", CENTER, h_b),))
    E_b = E_of(ns_b, CENTER, eps_vac)
    E_int = 0.5 * float(gauss_prof(CENTER, h_b) @ np.diag(ns_b.site_xx()))
    m_b = float(gauss_prof(CENTER, h_b).sum())
    say(f"CAL: h_b = {h_b:.4f}   E_med = {E_b:.5e}   E_int = {E_int:.5e}   "
        f"E_tot = {E_b + E_int:.5e}   m_b = sum B = {m_b:.4f}")

    say("CAL: warm s_th by secant (linear response + Hartree feedback)")
    s_th = 0.05
    for _ in range(10):
        ns, _ = solve_config(G, (("warm", CENTER, s_th),))
        E_c = E_of(ns, CENTER, eps_vac)
        if abs(E_c / E_star - 1) < 1e-3:
            break
        s_th *= E_star / E_c
    ns_c, _ = solve_config(G, (("warm", CENTER, s_th),))
    E_c = E_of(ns_c, CENTER, eps_vac)
    m_c = 3 * G * float(ns_c.dx2().sum())
    say(f"CAL: s_th = {s_th:.5e}   E_med = {E_c:.5e}   m_c = {m_c:.4f}   "
        f"local pp(center) = {ns_c.pp_diag()[CENTER]:.4f}")

    say("CAL: N4 variant c' (strain-profile warm) matched to E*")
    s_cp = s_th
    for _ in range(10):
        ns, _ = solve_config(G, (("warm_strain", CENTER, s_cp),))
        E_cp = E_of(ns, CENTER, eps_vac)
        if abs(E_cp / E_star - 1) < 1e-3:
            break
        s_cp *= E_star / E_cp
    ns_cp, _ = solve_config(G, (("warm_strain", CENTER, s_cp),))
    m_cp = 3 * G * float(ns_cp.dx2().sum())
    say(f"CAL: s_c' = {s_cp:.5e}   m_c' = {m_cp:.4f}")

    # N3 window gate per object
    for name, ns in (("a", ns_a), ("b", ns_b), ("c", ns_c)):
        r = E_of(ns, CENTER, eps_vac, 60) / E_of(ns, CENTER, eps_vac)
        say(f"N3 [{name}]: E(60)/E(40) = {r:.3f}  "
            f"{'PASS' if abs(r-1) <= 0.15 else 'FAIL'}")

    return dict(E_star=E_star,
                knobs={"a": ("grain", GAMMA_A), "b": ("bump", h_b),
                       "c": ("warm", s_th), "cp": ("warm_strain", s_cp)},
                masses={"a": m_a, "b": m_b, "c": m_c, "cp": m_cp},
                E_tot_b=E_b + E_int)

def stage_lambda():
    say("V3: transparency length from the standard grain pair")
    res = {}
    for d in (16, 24, 32, 40):
        F1 = passive_force(("grain", GAMMA_A), d, G)
        F0 = passive_force(("grain", GAMMA_A), d, 0.0)
        res[d] = F1 - F0
        say(f"V3: d = {d}: dF = {res[d]:+.4e}")
    ds = np.array(sorted(res), float)
    dfs = np.array([res[d] for d in ds])
    lam = -1 / np.polyfit(ds, np.log(np.abs(dfs)), 1)[0]
    say(f"V3: lambda = {lam:.1f} sites (pilot: 9.8)")
    return lam, res

def stage_weights(cal, d):
    say(f"=== weights at d = {d} ===")
    rows = {}
    for name in ("a", "b", "c", "cp"):
        kind, knob = cal["knobs"][name]
        F1p = passive_force((kind, knob), d, G)
        F0p = passive_force((kind, knob), d, 0.0)
        dFp = F1p - F0p          # S is always noisy: F(0) = pure pump
        F1a = active_force((kind, knob), d, G)
        F0a = active_force((kind, knob), d, 0.0)
        if kind == "bump":       # fully static config: F(0) IS the signal
            dFa = F1a
            n1 = abs(F1a / F0a - 1) if F0a else np.inf
            say(f"N1 [{name}]: F_a(g=1)/F_a(g=0) - 1 = {n1:+.3f}  "
                f"{'PASS' if n1 <= 0.10 else 'FAIL'}")
        else:
            dFa = F1a - F0a
        rows[name] = dict(dFp=dFp, dFa=dFa, F0p=F0p, F0a=F0a)
        say(f"  [{name}] dF_passive = {dFp:+.5e}  (pump {F0p:+.2e})   "
            f"dF_active = {dFa:+.5e}  (g=0 {F0a:+.2e})")
    return rows

def geometric_gate(vals, tol, label):
    v = np.array(vals, float)
    if np.any(v <= 0):
        say(f"{label}: FAIL (non-attractive entry) {v}")
        return
    gm = np.exp(np.mean(np.log(v)))
    dev = np.max(np.abs(v / gm - 1))
    say(f"{label}: values = {v}  gm = {gm:.4e}  max dev = {dev:.2%}  "
        f"{'PASS' if dev <= tol else 'FAIL'} (tol {tol:.0%})")

def stage_verdict(cal, rows, lam, phi2):
    E = cal["E_star"]
    say("=== VERDICT (primary triple a, b, c; E = E_med) ===")
    geometric_gate([rows[n]["dFp"] / E for n in ("a", "b", "c")], 0.20, "EP-P")
    geometric_gate([rows[n]["dFa"] / E for n in ("a", "b", "c")], 0.20, "EP-A")
    geometric_gate([rows[n]["dFa"] / rows[n]["dFp"] for n in ("a", "b", "c")],
                   0.20, "EP-R")
    say("EP2: bump re-scored with E_tot")
    r = [rows["a"]["dFp"] / E, rows["b"]["dFp"] / cal["E_tot_b"],
         rows["c"]["dFp"] / E]
    geometric_gate(r, 0.20, "EP2-P")
    r = [rows["a"]["dFa"] / E, rows["b"]["dFa"] / cal["E_tot_b"],
         rows["c"]["dFa"] / E]
    geometric_gate(r, 0.20, "EP2-A")
    say("N4: composition — c' (strain warm) vs c (gaussian warm) vs a")
    say(f"N4: r_p(c')/r_p(c) = {rows['cp']['dFp']/rows['c']['dFp']:.3f}   "
        f"r_p(c')/r_p(a) = {rows['cp']['dFp']/rows['a']['dFp']:.3f}")
    say("LAW: dF_p ratios vs m ratios (m = dressing / sum B)")
    m = cal["masses"]
    for n in ("b", "c", "cp"):
        fr = rows[n]["dFp"] / rows["a"]["dFp"]
        mr = m[n] / m["a"]
        ok = abs(fr / mr - 1) <= 0.30
        say(f"LAW [{n}]: F-ratio = {fr:.3f}  m-ratio = {mr:.3f}  "
            f"F/m = {fr/mr:.3f}  {'PASS' if ok else 'FAIL'}")
    say("XI: effective coupling in this solver's m-normalization")
    for n in ("a", "b", "c"):
        kindm = m[n]
        xi = rows[n]["dFp"] * np.exp(D_MAIN / lam) / ((phi2 / 2) * m["a"] * kindm)
        say(f"XI [{n}]: xi_1D = {xi:.3e}  (n22's 0.59 used a different "
            f"m-normalization; only CONSTANCY across rows is the test)")

# ------------------------------------------------------------------- main ---

if __name__ == "__main__":
    say(f"n23 — equivalence-principle test. N={N}, eta={ETA}, g={G}, "
        f"d={D_MAIN}, W={W_EN}")
    eps_vac, phi2 = stage_vacuum()
    stage_gates(eps_vac)
    cal = stage_calibration(eps_vac)
    lam, _ = stage_lambda()

    say("N2: test-bump linearity (object a)")
    Fa1 = active_force(cal["knobs"]["a"], D_MAIN, G, h_t=0.1) \
        - active_force(cal["knobs"]["a"], D_MAIN, 0.0, h_t=0.1)
    Fa2 = active_force(cal["knobs"]["a"], D_MAIN, G, h_t=0.2) \
        - active_force(cal["knobs"]["a"], D_MAIN, 0.0, h_t=0.2)
    say(f"N2: F(h_t=0.2)/F(h_t=0.1) = {Fa2/Fa1:.3f}  "
        f"{'PASS' if abs(Fa2/Fa1 - 2) <= 0.2 else 'FAIL'}")

    rows = stage_weights(cal, D_MAIN)
    stage_verdict(cal, rows, lam, phi2)

    say(f"REP: full row at d = {D_REP} (ratios must not move)")
    rows40 = stage_weights(cal, D_REP)
    E = cal["E_star"]
    geometric_gate([rows40[n]["dFp"] / E for n in ("a", "b", "c")],
                   0.20, "EP-P(d=40)")
    geometric_gate([rows40[n]["dFa"] / E for n in ("a", "b", "c")],
                   0.20, "EP-A(d=40)")
    say("done.")
