diff --git a/FINDINGS_model_order.md b/FINDINGS_model_order.md new file mode 100644 index 0000000..df5ad31 --- /dev/null +++ b/FINDINGS_model_order.md @@ -0,0 +1,74 @@ +# FINDINGS — почему остаётся разрыв на широких скобках + +Запрос Валерия 2026-09-21: «Разбери, почему остаётся разрыв по порядку модели». Все числа — с сервера +(Linux), выводы лежат рядом: `results_diag_model_order.txt`, `results_diag_model_order_checks.txt`, +`results_diag_flat_detectable.txt`, `results_diag_band_by_family.txt`. + +## Метод разбора: та же информация, две модели + +Нового метода нет. Трассировочный крючок в `pure_root` (не меняет поведения, пишет только по запросу) +записывает состояние перед каждым шагом. В каждом состоянии нашей собственной траектории на ОКИ из тех +же трёх уже оплаченных оценок и наш шаг даётся по одной дополнительной оценке; мера — половинения +скобки, как в исходном замере 0.93 против 2.26. Выборка: 33 задачи CHA, которые мы решаем и на которых +проигрываем Чандрупатле, 822 состояния. + +## Главный вывод: «порядок модели» объясняет только треть + +Разрыв на этих задачах — 262 оценки: + +| часть | семейства | оценок | чем выигрывает Чандрупатла | +|---|---|---|---| +| **2/3** | fun3–fun6 = (x−3)³, 6(x−2)⁵, x⁹, x¹⁹ | **178** | **делением**: 93%, 96%, 100%, 100% его шагов | +| **~1/5** | fun1 = x³−2x−5 на широких скобках | **58** | моделью: ОКИ из НАШИХ точек 10.37 половинения против наших 0.62 (fun1.4) | +| ~1/10 | fun2, fun8, fun9 | 26 | смешанно, 40–84% делений | + +**Плоские семейства — не модель, а решение, когда от модели отказаться.** Там не работает никакая +интерполяционная модель: наш шаг снимает 0.41–0.81 половинения скобки за оценку, ОКИ из тех же точек — +0.01–0.17. Чандрупатла не выигрывает их моделью — он её не использует и делит, получая ровно 1.0. Мы +проигрываем, потому что наш шаг медленнее деления, а наша гарантия (≤ 2× деление) именно это и +разрешает. + +**Подтверждение по семействам из уже посчитанной сетки** (его полоса в нашем шаге, предохранитель +`project`): fun4/5/6 — 573/456/456 → **296/239/235** при его собственных **293/228/228**; и одновременно ++164, +111, +170, +144 на fun1/2/8/9, где полоса отвергает хорошие шаги. Итог полосы по набору (3066) +был суммой этих двух противоположных эффектов — поэтому слот 2 и выглядел как «место решения разрыва не +держит». **Держит, но ни одно измеренное чтение не принимает это решение верно для НАШЕГО шага.** + +**fun1 — действительно порядок модели, и причина структурная.** Наш шаг +`x + (1−S)(far − x) = S·x + (1−S)·far` — выпуклая комбинация двух точек. У квадратичного интерполянта +в тройке, охватывающей корень, ровно один лагранжев вес **отрицателен** (проверено на всех 428 +невырожденных состояниях; в остальных пяти отрицательный вес ушёл в −0.000 переполнением вниз). +Похожесть в [0,1] отрицательного веса дать не может — это Предложение 4 статьи, продолженное с двух +точек на три. + +## Можно ли узнать плоский режим — измерено, пока нет + +Условие проигрыша касательного шага делению выводится точно: шаг ложится в `e(1−1/m)`, что хуже +половины при `m ≥ 2` (двойка — это ½ деления, не выбор). Но трёхточечная оценка `_local_order` +режима не видит: + +| группа | состояний | m ≥ 2 | +|---|---|---| +| APS | 990 | **0.2%** — верное молчание | +| CHA прочие | 240 | 0.0% | +| CHA плоские (m = 3…19 по построению) | 659 | **5.8%** — слепота | + +Гейт по ней построить нельзя. Непроверенный кандидат (только идея, не замер): отношение двух подряд +остатков — на подходе порядка m под касательными шагами оно устанавливается на `(1−1/m)^m`, а у +простого корня уходит в ноль. + +## Что исправлено в статье + +`§sec:widelimit` утверждал, что весь предел — порядок модели; это верно для fun1 и **неверно для двух +третей разрыва**. Исправлено, как уже дважды исправлялся неверно записанный механизм. Исправлены +также вывод в `§sec:decision`, аннотация (рекорд APS 2523, раскладка разрыва) и «Future work»: +логарифмическая замена оттуда убрана, она опровергнута. + +## ⚠️ Открытый вопрос, который решает Валерий + +`article/sim_numerics.tex` с **первого** коммита репозитория несёт заголовок «STALE … current text is +sim_numerics.md, do not build a PDF from it». Все правки статьи сессий 20–21 сентября сделаны в `.tex`; +в `.md` (не менялся с 24 августа) нет ни одного из этих результатов, а в `.tex` — первых +поправок из `.md`. Какой файл каноничен, не решено. Новые числа в тексте **ещё не проведены через +`verify_article.py`** — они прослеживаются к файлам `results_diag_*.txt`, но машинной сверки для них +пока нет. diff --git a/article/sim_numerics.tex b/article/sim_numerics.tex index 1ba024f..025f6e2 100644 --- a/article/sim_numerics.tex +++ b/article/sim_numerics.tex @@ -73,8 +73,15 @@ model is built on, selected by the shape of their configuration rather than by recency --- lowers the Alefeld--Potra--Shi count to $2527$, below every method measured here including Chandrupatla's $2785$, with the guarantee intact and still no tuning constant; reading it instead on the choice of far end gives -$5511$ over both sets. Where the similarity is read turns out to matter more -than which similarity it is. +$5511$ over both sets. Reading it in the decision to trust the model or halve +lowers the canonical count further, to $2523$. Where the similarity is read +turns out to matter more than which similarity it is. The gap that remains on +the wide-bracket set is taken apart on the same evaluations: a third of it is +the order of the model --- a convex two-point step cannot carry the negative +weight of quadratic interpolation --- and two thirds is not a model at all but +the decision when to give the model up, which Chandrupatla takes by bisecting +on up to all of his steps and which no reading measured here takes correctly +for ours. \end{abstract} \section{Introduction} @@ -646,13 +653,15 @@ the root, $S(|\varphi-\xi|)= 2" separate the states of +the flat families from the states of the canonical set? _local_order is known to fire +falsely (derived3_m: m > 1 on 27.9% of APS steps, excursions to 34.8), so the separation +is the whole question. If APS states cross m >= 2 about as often as flat ones, the regime +is NOT detectable this way and no gate may be built on it. + + python -u diag_flat_detectable.py +""" +from __future__ import annotations + +from collections import defaultdict + +from scipy.optimize._tstutils import _APS_TESTS_DICTS, _CHANDRUPATLA_TESTS_DICTS + +from diag_model_order import ACC, MAXFEV, OURS, XTOL +from simroot.pure import _local_order, pure_root + +FLAT = ("fun3", "fun4", "fun5", "fun6") + + +def states(f, a, b, r): + st = {"trace": []} + x, _n, conv = pure_root(f, a, b, XTOL, MAXFEV, stats=st, **OURS) + if not (conv and abs(float(x) - r) <= ACC): + return [] + out = [] + for s in st["trace"]: + if s["xp2"] is None: + continue + out.append(_local_order(s["x"], s["fx"], s["xp"], s["fp"], s["xp2"], s["fp2"])) + return out + + +def main(): + groups = defaultdict(list) + for d in _APS_TESTS_DICTS: + a, b = (float(v) for v in d["bracket"]) + f = (lambda ff, aa: lambda t: ff(t, *aa))(d["f"], d.get("args", ())) + groups["APS (canonical)"] += states(f, a, b, float(d["root"])) + for d in _CHANDRUPATLA_TESTS_DICTS: + a, b = (float(v) for v in d["bracket"]) + fam = str(d["ID"]).split(".")[0] + key = "CHA flat fun3-6" if fam in FLAT else "CHA other" + groups[key] += states(d["f"], a, b, float(d["root"])) + + print(f"{'group':<18} {'states':>7} {'m > 1':>8} {'m >= 2':>8}") + for k in ("APS (canonical)", "CHA other", "CHA flat fun3-6"): + ms = groups[k] + n = max(len(ms), 1) + gt1 = 100.0 * sum(m > 1.0 for m in ms) / n + ge2 = 100.0 * sum(m >= 2.0 for m in ms) / n + print(f"{k:<18} {len(ms):7d} {gt1:7.1f}% {ge2:7.1f}%") + + +if __name__ == "__main__": + main() diff --git a/diag_model_order.py b/diag_model_order.py new file mode 100644 index 0000000..6e66930 --- /dev/null +++ b/diag_model_order.py @@ -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() diff --git a/diag_model_order_checks.py b/diag_model_order_checks.py new file mode 100644 index 0000000..3266a1c --- /dev/null +++ b/diag_model_order_checks.py @@ -0,0 +1,83 @@ +"""Two checks on diag_model_order.py before its result may be stated. + + C1 WHAT Chandrupatla does on the problems where we lose, by family: the share of his + steps that are bisections, from the traced mirror whose evaluation count is asserted + equal to the original's. If the flat families are won by bisecting, the gap there + is not a better model on his side. + C2 the 5 states where "exactly one negative Lagrange weight" failed: print them. The + sign argument is exact in real arithmetic, so each exception must be a + floating-point degeneracy (a weight that underflowed to 0, or f values so close + that a difference cancelled) - or the argument is wrong and must be withdrawn. + + python -u diag_model_order_checks.py +""" +from __future__ import annotations + +import math +from collections import defaultdict + +from scipy.optimize._tstutils import _CHANDRUPATLA_TESTS_DICTS + +from diag_model_order import ACC, MAXFEV, OURS, XTOL, ch_cost, iqi +from explain_chandrupatla import chandrupatla_traced +from simroot.baselines import chandrupatla +from simroot.pure import pure_root + + +def main(): + fam = defaultdict(lambda: [0, 0, 0, 0, 0]) # bisect, iqi, ours n, ch n, problems + exceptions = [] + 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 + _xc, nc, okc, tr = chandrupatla_traced(f, a, b, XTOL, MAXFEV) + _xo, no, _oko = chandrupatla(f, a, b, XTOL, MAXFEV) + assert nc == no, f"mirror drifted on {pid}: {nc} vs {no}" + k = pid.split(".")[0] + cell = fam[k] + cell[0] += tr["bisect"] + cell[1] += tr["iqi"] + cell[2] += n + cell[3] += ch + cell[4] += 1 + for s in st["trace"]: + if s["xp2"] is None: + continue + pts = ((s["x"], s["fx"]), (s["xp"], s["fp"]), (s["xp2"], s["fp2"])) + fs = [p[1] for p in pts] + if not (min(fs) < 0.0 < max(fs)): + continue + _xq, w = iqi(*pts) + if w is not None and sum(v < 0.0 for v in w) != 1: + exceptions.append((pid, fs, w)) + + print("C1 Chandrupatla on the problems where we lose, by family") + print(f" {'family':<6} {'probs':>5} {'ours':>6} {'CH':>6} {'gap':>5} " + f"{'CH bisect share':>16}") + tot_gap = 0 + for k in sorted(fam): + bi, iq, on, cn, p = fam[k] + tot_gap += on - cn + share = 100.0 * bi / max(bi + iq, 1) + print(f" {k:<6} {p:5d} {on:6d} {cn:6d} {on - cn:5d} {share:15.0f}%") + print(f" total gap on the losing problems: {tot_gap}") + + print(f"\nC2 the {len(exceptions)} states breaking 'exactly one negative weight'") + for pid, fs, w in exceptions: + tiny = min(abs(v) for v in fs) + print(f" {pid:<8} f = [{', '.join(f'{v:.3e}' for v in fs)}] " + f"w = [{', '.join(f'{v:.3e}' for v in w)}] " + f"zero weights: {sum(v == 0.0 for v in w)} min|f| {tiny:.1e}") + + +if __name__ == "__main__": + main() diff --git a/results_diag_band_by_family.txt b/results_diag_band_by_family.txt new file mode 100644 index 0000000..43ab023 --- /dev/null +++ b/results_diag_band_by_family.txt @@ -0,0 +1,22 @@ +guard=project: family no-gate band delta + fun1 99 263 +164 + fun2 141 252 +111 + fun3 338 310 -28 + fun4 573 296 -277 + fun5 456 239 -217 + fun6 456 235 -221 + fun7 1000 1000 +0 + fun8 52 222 +170 + fun9 105 249 +144 + +guard=credit: family no-gate band delta + fun1 258 239 -19 + fun2 292 271 -21 + fun3 274 237 -37 + fun4 294 234 -60 + fun5 238 238 +0 + fun6 238 235 -3 + fun7 1000 1000 +0 + fun8 220 218 -2 + fun9 206 227 +21 + diff --git a/results_diag_flat_detectable.txt b/results_diag_flat_detectable.txt new file mode 100644 index 0000000..896e5f8 --- /dev/null +++ b/results_diag_flat_detectable.txt @@ -0,0 +1,4 @@ +group states m > 1 m >= 2 +APS (canonical) 990 43.2% 0.2% +CHA other 240 23.3% 0.0% +CHA flat fun3-6 659 48.9% 5.8% diff --git a/results_diag_model_order.txt b/results_diag_model_order.txt new file mode 100644 index 0000000..8095f5d --- /dev/null +++ b/results_diag_model_order.txt @@ -0,0 +1,47 @@ +problem ours CH states bracket halvings per evaluation: ours IQI-rec IQI-brk +fun1.3 19 15 7 5.59 8.26 8.43 +fun1.4 59 24 8 0.62 10.37 4.49 +fun1.5 63 44 7 3.26 1.30 3.96 +fun2.2 26 23 21 4.36 5.71 9.16 +fun2.3 30 29 26 6.99 10.05 13.77 +fun2.4 43 42 33 7.32 7.98 7.55 +fun3.1 64 44 45 0.41 0.17 0.17 +fun3.2 56 46 29 0.59 0.13 0.19 +fun3.3 66 60 31 0.57 0.10 0.14 +fun3.4 73 69 33 0.63 0.13 0.16 +fun3.5 85 82 28 0.56 0.06 0.14 +fun4.1 54 44 27 0.45 0.10 0.10 +fun4.2 56 46 28 0.47 0.10 0.10 +fun4.3 70 56 38 0.49 0.06 0.08 +fun4.4 73 67 34 0.61 0.13 0.15 +fun4.5 86 80 35 0.60 0.16 0.17 +fun5.1 57 44 31 0.44 0.03 0.03 +fun5.2 54 44 28 0.58 0.10 0.11 +fun5.3 55 45 27 0.63 0.01 0.02 +fun5.4 57 47 30 0.64 0.04 0.05 +fun5.5 59 48 32 0.63 0.04 0.06 +fun6.1 52 44 36 0.63 0.04 0.04 +fun6.2 54 44 39 0.55 0.01 0.01 +fun6.3 50 45 35 0.80 0.03 0.03 +fun6.4 52 47 35 0.81 0.03 0.03 +fun6.5 55 48 38 0.79 0.03 0.03 +fun8.1 13 10 7 7.59 10.28 10.41 +fun8.4 16 14 12 5.68 7.72 11.15 +fun8.5 18 15 13 4.98 7.24 9.41 +fun9.2 12 9 7 5.02 0.66 0.66 +fun9.3 14 11 10 0.94 3.08 3.05 +fun9.4 15 13 7 7.54 9.23 3.31 +fun9.5 19 14 5 7.39 3.79 5.02 + +problems where we lose to Chandrupatla: 33, states 822 + +Q1 triples bracketing zero with exactly ONE negative Lagrange weight: 428/433 -> IDENTITY FAILS + model defined and landing strictly inside the proven bracket: ours 86%, iqiR 77%, iqiB 77% + +Q2/Q3 mean halvings per ONE evaluation from the same state (bracket | error), valid states only + approach (error >= 1/2 bracket) ours 1.32 | 3.39 (n=40) iqiR 0.57 | 2.28 (n=6) iqiB 0.53 | 2.14 (n=6) + endgame ours 1.24 | 0.90 (n=669) iqiR 0.83 | 1.27 (n=628) iqiB 0.88 | 1.31 (n=631) + +PAIRED on states where both are valid: mean (IQI - ours) bracket halvings + iqiR: -0.34 per evaluation (n=628) + iqiB: -0.23 per evaluation (n=631) diff --git a/results_diag_model_order_checks.txt b/results_diag_model_order_checks.txt new file mode 100644 index 0000000..53fabb1 --- /dev/null +++ b/results_diag_model_order_checks.txt @@ -0,0 +1,18 @@ +C1 Chandrupatla on the problems where we lose, by family + family probs ours CH gap CH bisect share + fun1 3 141 83 58 74% + fun2 3 99 94 5 84% + fun3 5 344 301 43 93% + fun4 5 339 293 46 96% + fun5 5 282 228 54 100% + fun6 5 263 228 35 100% + fun8 3 47 39 8 50% + fun9 4 60 47 13 40% + total gap on the losing problems: 262 + +C2 the 5 states breaking 'exactly one negative weight' + fun6.1 f = [1.000e-175, -1.460e-160, -1.153e-150] w = [1.000e+00, 0.000e+00, -0.000e+00] zero weights: 2 min|f| 1.0e-175 + fun6.1 f = [4.634e-163, 1.000e-175, -1.460e-160] w = [-0.000e+00, 1.000e+00, 0.000e+00] zero weights: 2 min|f| 1.0e-175 + fun6.3 f = [-2.588e-172, 2.588e-153, 2.588e-153] w = [1.000e+00, 0.000e+00, -0.000e+00] zero weights: 2 min|f| 2.6e-172 + fun6.3 f = [6.667e-160, -2.588e-172, 2.588e-153] w = [0.000e+00, 1.000e+00, -0.000e+00] zero weights: 2 min|f| 2.6e-172 + fun6.3 f = [5.745e-156, 6.667e-160, -2.588e-172] w = [-0.000e+00, 0.000e+00, 1.000e+00] zero weights: 2 min|f| 2.6e-172 diff --git a/run_diag_model_order.sh b/run_diag_model_order.sh new file mode 100644 index 0000000..37555fd --- /dev/null +++ b/run_diag_model_order.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Frozen runner: regression first (the trace hook touched simroot/pure.py), then the +# model-order diagnosis. Detached, logged, explicit end marker. +export OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1 +cd "$HOME/sim-numerics" || exit 1 +for pass in 1 2; do + echo "=== REGRESSION PASS $pass ===" + ~/torchenv/bin/python -m pytest tests -q -p no:cacheprovider -n 14 2>&1 | tail -3 +done +echo "=== DIAG ===" +~/torchenv/bin/python -u diag_model_order.py +echo "=== ALL DONE ===" diff --git a/simroot/pure.py b/simroot/pure.py index 2a02f37..e528975 100644 --- a/simroot/pure.py +++ b/simroot/pure.py @@ -863,6 +863,15 @@ def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived", step = _log_space_step(S, x, fx, far, slope, c_near, xtol) if step is None: step = (1.0 - S(z_now)) * (far - x) + if stats is not None and "trace" in stats: + # DIAGNOSTIC HOOK, behaviour-neutral: records the state each + # step was taken from, so another model can be evaluated on the + # SAME information (diag_model_order.py). Written only when the + # caller asks for it by putting a "trace" list into stats. + stats["trace"].append({ + "x": x, "fx": fx, "xp": x_prev, "fp": f_prev, + "xp2": x_prev2, "fp2": f_prev2, "A": A, "FA": FA, + "B": B, "FB": FB, "far": far, "raw": step}) if sg.reject_step(step): x_new = 0.5 * (A + B) step = x_new - x diff --git a/tests/test_trace_hook.py b/tests/test_trace_hook.py new file mode 100644 index 0000000..06c9fb7 --- /dev/null +++ b/tests/test_trace_hook.py @@ -0,0 +1,42 @@ +"""Pins for the diagnostic trace hook in pure_root (diag_model_order.py relies on it).""" +from __future__ import annotations + +import math + +import pytest + +from simroot.pure import pure_root + +PROBS = ( + (lambda t: t ** 3 - 2.0 * t - 5.0, 1.0, 4.0), + (lambda t: math.exp(t) - 2.0, 0.0, 3.0), + (lambda t: (t - 3.0) ** 3, 0.0, 10.0), +) +CFG = dict(similarity="ratio", scale="derived3", guard="safe", prestep="slope", + direction="dissimilar") + + +@pytest.mark.parametrize("f,a,b", PROBS) +def test_trace_is_behaviour_neutral(f, a, b): + """Asking for a trace must not change a single evaluation.""" + plain = pure_root(f, a, b, 1e-12, 200, **CFG) + st = {"trace": []} + traced = pure_root(f, a, b, 1e-12, 200, stats=st, **CFG) + assert plain == traced + + +@pytest.mark.parametrize("f,a,b", PROBS) +def test_trace_records_every_similarity_step(f, a, b): + st = {"trace": []} + _x, n, ok = pure_root(f, a, b, 1e-12, 200, stats=st, **CFG) + assert ok and 0 < len(st["trace"]) < n + for s in st["trace"]: + lo, hi = min(s["A"], s["B"]), max(s["A"], s["B"]) + assert lo <= s["x"] <= hi + assert s["far"] in (s["A"], s["B"]) + + +def test_no_trace_key_means_no_trace(): + st = {} + pure_root(lambda t: t - 1.0, 0.0, 3.0, 1e-10, 100, stats=st, **CFG) + assert "trace" not in st