Why the wide-bracket gap remains, taken apart on the SAME evaluations: 'order of the model' explains only a third. Of the 262 evaluations lost on 33 problems, 178 sit on fun3-fun6 - pure powers of order 3, 5, 9, 19 - where no interpolating model works (our step 0.41-0.81 bracket halvings per evaluation, IQI from our own points 0.01-0.17) and Chandrupatla wins by BISECTING 93-100% of his steps; our step is slower than bisection there and our 2x-bisection guarantee licenses it. His band inside our step confirms it per family: fun4/5/6 573/456/456 -> 296/239/235 against his 293/228/228, while +164/+111/+170/+144 on fun1/2/8/9 where it refuses good steps - slot 2's total was two opposite effects summed. Only fun1 (58) is model order: IQI from our three points 10.37 halvings against our 0.62; a convex two-point step cannot carry IQI's negative weight (428/428 non-degenerate states; five underflowed to -0.0).
The flat regime is not detectable by the three-point order estimate: m >= 2 (the exact condition for a tangent step to lose to a halving) fires on 0.2% of APS states and on only 5.8% of states of pure powers of order 3-19. Paper corrected where it said the whole gap is model order (widelimit, decision, abstract, future work - the refuted log transform removed). WARNING recorded in FINDINGS_model_order.md: article/sim_numerics.tex has carried a STALE header since the first commit (current text = sim_numerics.md, unchanged since 08-24); every paper edit of 09-20/21 went into the .tex, the .md has none of them. Which file is canonical is Valeri's call. The new text numbers trace to results_diag_*.txt but are not yet machine-checked by verify_article.py. Trace hook in pure_root is behaviour-neutral and pinned; 762 pass on the server twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
"""Why the model-order gap remains: the same information, two models.
|
||||
|
||||
Hypothesis (stated before the run, 2026-09-21). Our step x' = x + (1-S)(far - x) is a
|
||||
CONVEX combination of two points, x' = S.x + (1-S).far, with weights in [0,1]; a third
|
||||
point enters only through the scale sigma, i.e. through a slope. Chandrupatla's step is
|
||||
inverse quadratic interpolation (IQI): x' = sum w_i x_i with Lagrange weights of the
|
||||
values at 0, and whenever the triple's values bracket zero ONE weight is necessarily
|
||||
NEGATIVE - the step pushes away from the stale point. A [0,1] similarity cannot produce
|
||||
a negative weight, so a similarity-weighted average of points is capped at a first-order
|
||||
inverse model: Proposition 4 of the paper, extended from two points to three.
|
||||
|
||||
The test uses NO new method: replay our own trajectory and, at every state, place IQI on
|
||||
the SAME evaluations we already paid for. If the gap is the model, IQI from our points
|
||||
must contract the error more than our step did.
|
||||
|
||||
Pre-declared predictions
|
||||
Q1 in every state whose triple brackets zero, exactly one Lagrange weight is < 0
|
||||
(a sign identity; measured as a check of the argument, must be 100%)
|
||||
Q2 on the problems where we lose to Chandrupatla, IQI from the same points gives
|
||||
more halvings of the error per step than our raw step, on states where IQI lands
|
||||
inside the proven bracket
|
||||
Q3 the advantage sits in the APPROACH class (error at least half the current bracket
|
||||
- the one constant of the method, the bisection 1/2) and shrinks in the endgame
|
||||
FALSIFIER: if IQI on the same points does NOT contract more, the gap is not how the
|
||||
information is used, and the explanation must be sought elsewhere - e.g. WHICH points are
|
||||
kept (Chandrupatla keeps a bracketing triple, we keep the most recent), which is why IQI
|
||||
is placed twice: on our recent triple and on (x, A, B), the bracketing one.
|
||||
|
||||
python -u diag_model_order.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from scipy.optimize._tstutils import _CHANDRUPATLA_TESTS_DICTS
|
||||
|
||||
from simroot.baselines import chandrupatla, chandrupatla_scipy
|
||||
from simroot.pure import pure_root
|
||||
|
||||
XTOL, MAXFEV, ACC = 1e-12, 200, 1e-6
|
||||
OURS = dict(similarity="ratio", scale="derived3", guard="safe", prestep="slope",
|
||||
direction="dissimilar") # reading (3), CHA 2683 per set
|
||||
|
||||
|
||||
def iqi(p0, p1, p2):
|
||||
"""Inverse quadratic interpolation at f = 0, plus its Lagrange weights."""
|
||||
(x0, f0), (x1, f1), (x2, f2) = p0, p1, p2
|
||||
if len({f0, f1, f2}) < 3 or len({x0, x1, x2}) < 3:
|
||||
return None, None
|
||||
d0 = (f0 - f1) * (f0 - f2)
|
||||
d1 = (f1 - f0) * (f1 - f2)
|
||||
d2 = (f2 - f0) * (f2 - f1)
|
||||
# distinct values can still give a product that underflows to 0 (first run of
|
||||
# 2026-09-21 crashed on exactly that); the model is then undefined, not an error
|
||||
if d0 == 0.0 or d1 == 0.0 or d2 == 0.0:
|
||||
return None, None
|
||||
w0 = f1 * f2 / d0
|
||||
w1 = f0 * f2 / d1
|
||||
w2 = f0 * f1 / d2
|
||||
x = w0 * x0 + w1 * x1 + w2 * x2
|
||||
if not all(math.isfinite(v) for v in (x, w0, w1, w2)):
|
||||
return None, None
|
||||
return x, (w0, w1, w2)
|
||||
|
||||
|
||||
def halvings(e_before, landing, r):
|
||||
"""-log2 of the error contraction; distances below one ulp of r are not measurable,
|
||||
so the floor is the ulp, not a chosen number."""
|
||||
after = max(abs(landing - r), math.ulp(r) if r != 0.0 else 5e-324)
|
||||
return math.log2(e_before / after)
|
||||
|
||||
|
||||
def ch_cost(f, a, b, r):
|
||||
best = None
|
||||
for impl in (chandrupatla, chandrupatla_scipy):
|
||||
try:
|
||||
x, n, c = impl(f, a, b, XTOL, MAXFEV)
|
||||
except Exception:
|
||||
continue
|
||||
if c and math.isfinite(float(x)) and abs(float(x) - r) <= ACC:
|
||||
best = int(n) if best is None else min(best, int(n))
|
||||
return best
|
||||
|
||||
|
||||
|
||||
MODELS = ("ours", "iqiR", "iqiB")
|
||||
|
||||
|
||||
def bracket_step(f, lo, flo, hi, fhi, landing):
|
||||
"""One evaluation at `landing`, the standard bracket update, and the new width.
|
||||
Evaluated for the diagnosis only - nothing is charged - so every model gets exactly
|
||||
ONE evaluation from the same state, which is the comparison the gap is stated in."""
|
||||
fl = f(landing)
|
||||
if fl == 0.0:
|
||||
return 0.0
|
||||
if (fl > 0.0) == (flo > 0.0):
|
||||
lo = landing
|
||||
else:
|
||||
hi = landing
|
||||
return abs(hi - lo)
|
||||
|
||||
|
||||
def main():
|
||||
# per model, per phase: [sum bracket-halvings, sum error-halvings, count]
|
||||
agg = {(m, ph): [0.0, 0.0, 0] for m in MODELS for ph in ("app", "end")}
|
||||
paired = {"iqiR": [0.0, 0], "iqiB": [0.0, 0]} # sum(h_model - h_ours) on common states
|
||||
neg_ok = neg_all = 0
|
||||
valid = {m: 0 for m in MODELS}
|
||||
states_all = 0
|
||||
lose = []
|
||||
print(f"{'problem':<8} {'ours':>5} {'CH':>4} {'states':>6} "
|
||||
f"bracket halvings per evaluation: {'ours':>6} {'IQI-rec':>8} {'IQI-brk':>8}")
|
||||
for d in _CHANDRUPATLA_TESTS_DICTS:
|
||||
pid = str(d["ID"])
|
||||
f = d["f"]
|
||||
a, b = (float(v) for v in d["bracket"])
|
||||
r = float(d["root"])
|
||||
st = {"trace": []}
|
||||
x, n, conv = pure_root(f, a, b, XTOL, MAXFEV, stats=st, **OURS)
|
||||
if not (conv and abs(float(x) - r) <= ACC):
|
||||
continue
|
||||
ch = ch_cost(f, a, b, r)
|
||||
if ch is None or n <= ch:
|
||||
continue # only the problems where we lose
|
||||
lose.append(pid)
|
||||
per = {m: [] for m in MODELS}
|
||||
cnt = 0
|
||||
for s in st["trace"]:
|
||||
if s["xp2"] is None:
|
||||
continue
|
||||
e = abs(s["x"] - r)
|
||||
if e == 0.0:
|
||||
continue
|
||||
A, FA, B, FB = s["A"], s["FA"], s["B"], s["FB"]
|
||||
lo, flo, hi, fhi = (A, FA, B, FB) if A < B else (B, FB, A, FA)
|
||||
W = hi - lo
|
||||
if W <= 0.0:
|
||||
continue
|
||||
cnt += 1
|
||||
phase = "app" if e >= 0.5 * W else "end"
|
||||
recent = ((s["x"], s["fx"]), (s["xp"], s["fp"]), (s["xp2"], s["fp2"]))
|
||||
fs = [p[1] for p in recent]
|
||||
if min(fs) < 0.0 < max(fs):
|
||||
_xq, wq = iqi(*recent)
|
||||
if wq is not None:
|
||||
neg_all += 1
|
||||
neg_ok += sum(w < 0.0 for w in wq) == 1
|
||||
# Chandrupatla's own triple: both proven ends plus the latest point that is
|
||||
# neither of them (x itself is always an end by the time a state is recorded)
|
||||
third = None
|
||||
for cx, cf in ((s["xp"], s["fp"]), (s["xp2"], s["fp2"])):
|
||||
if cx is not None and cx != A and cx != B:
|
||||
third = (cx, cf)
|
||||
break
|
||||
land = {"ours": s["x"] + s["raw"],
|
||||
"iqiR": iqi(*recent)[0],
|
||||
"iqiB": iqi((A, FA), (B, FB), third)[0] if third else None}
|
||||
hb = {}
|
||||
for m in MODELS:
|
||||
L = land[m]
|
||||
if L is None or not (lo < L < hi):
|
||||
continue
|
||||
Wn = bracket_step(f, lo, flo, hi, fhi, L)
|
||||
# an exact hit makes the width 0; the floor is one ulp of the root, the
|
||||
# same unmeasurable-distance floor `halvings` uses - not a chosen cap
|
||||
h_b = math.log2(W / max(Wn, math.ulp(r) if r != 0.0 else 5e-324))
|
||||
h_e = halvings(e, L, r)
|
||||
hb[m] = h_b
|
||||
valid[m] += 1
|
||||
per[m].append(h_b)
|
||||
cell = agg[(m, phase)]
|
||||
cell[0] += h_b
|
||||
cell[1] += h_e
|
||||
cell[2] += 1
|
||||
for m in ("iqiR", "iqiB"):
|
||||
if m in hb and "ours" in hb:
|
||||
paired[m][0] += hb[m] - hb["ours"]
|
||||
paired[m][1] += 1
|
||||
states_all += cnt
|
||||
mean = lambda v: (sum(v) / len(v)) if v else float("nan") # noqa: E731
|
||||
print(f"{pid:<8} {n:5d} {ch:4d} {cnt:6d} {'':33} "
|
||||
f"{mean(per['ours']):6.2f} {mean(per['iqiR']):8.2f} {mean(per['iqiB']):8.2f}")
|
||||
|
||||
print(f"\nproblems where we lose to Chandrupatla: {len(lose)}, states {states_all}")
|
||||
print(f"\nQ1 triples bracketing zero with exactly ONE negative Lagrange weight: "
|
||||
f"{neg_ok}/{neg_all}"
|
||||
+ (" -> identity holds" if neg_ok == neg_all else " -> IDENTITY FAILS"))
|
||||
print(" model defined and landing strictly inside the proven bracket: " +
|
||||
", ".join(f"{m} {100.0 * valid[m] / max(states_all, 1):.0f}%" for m in MODELS))
|
||||
print("\nQ2/Q3 mean halvings per ONE evaluation from the same state "
|
||||
"(bracket | error), valid states only")
|
||||
for ph, name in (("app", "approach (error >= 1/2 bracket)"), ("end", "endgame")):
|
||||
cells = []
|
||||
for m in MODELS:
|
||||
sb, se, c = agg[(m, ph)]
|
||||
cells.append(f"{m} {sb / c if c else float('nan'):5.2f} | "
|
||||
f"{se / c if c else float('nan'):5.2f} (n={c})")
|
||||
print(f" {name:<32} " + " ".join(cells))
|
||||
print("\nPAIRED on states where both are valid: mean (IQI - ours) bracket halvings")
|
||||
for m, (s_, c) in paired.items():
|
||||
print(f" {m}: {s_ / c if c else float('nan'):+.2f} per evaluation (n={c})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user