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:
vvs
2026-09-21 07:38:16 +01:00
co-authored by Claude Opus 5
parent 46c713cdd3
commit f5d036c9d8
13 changed files with 693 additions and 28 deletions
+74
View File
@@ -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`, но машинной сверки для них
пока нет.
+74 -28
View File
@@ -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|)<S(z)$, with the \emph{same} $S$ in both places
step rule. It is not a proposal; it is an instrument, and it answers the only
question that matters here --- whether the remaining gap lives in this place.
It does not. With Chandrupatla's band in our step, the wide-bracket set costs
In total, no. With Chandrupatla's band in our step, the wide-bracket set costs
$3066$ under the projection guard and $2899$ under credit, against his $2423$,
while the canonical set rises from $2806$ to $5432$. The band is not idle ---
it refuses $63.6\%$ of steps --- so this is a decision that was taken and did
not pay. That is the fifth independent test of the same conclusion: the gap is
the \emph{order of the model} the step is built on, not the place where the
similarity is read.
not pay \emph{in total}. Per family it did pay, and where: it closes almost the
whole gap on the flat families and gives back more than that on the families
where our step is good. \S\ref{sec:widelimit} takes this apart; the short form is
that the decision place does hold most of the gap, but no reading measured so far
makes that decision correctly for \emph{our} step.
Our own readings do not close it either (best $2868$ on the wide-bracket set),
but ``defect'' sets a new best on the canonical set: $\mathbf{2523}$ at
@@ -747,10 +756,46 @@ probe lands well short and the new bracket is about half the old one; a model
that carries the curvature --- inverse quadratic interpolation --- lands far
closer and removes about four fifths.
So the limit is stated exactly, and it is the \emph{order of the model the
similarity is read against}, not the placement of the reading and not the
guarantee. Three parameter-free ways round it have now been measured and all
three fail in the same way, buying the wide brackets at the price of the
That mechanism is right for \texttt{fun1} and \emph{wrong as a statement about the
whole gap}, which an earlier version of this section made. Replaying our own
trajectories on the $33$ problems where we lose, and placing both models on the
\emph{same} evaluations --- one extra evaluation each, from the same state ---
splits the $262$ evaluations of the gap in two. On \texttt{fun1} ($58$ of them)
inverse quadratic interpolation from our own three points removes far more of
the bracket than our step did: $10.37$ halvings against $0.62$ on the
$2\times10^4$-wide bracket. There the gap is the order of the model, and the
reason is structural: our step $x+(1-S)(x_{\mathrm{far}}-x)=S\,x+(1-S)\,x_{\mathrm{far}}$
is a convex combination of two points, while the quadratic interpolant's
Lagrange weights at $0$ always include exactly one \emph{negative} weight when
the triple brackets the root (verified on all $428$ non-degenerate states; in
the other five the negative weight underflowed to $-0$). A similarity in $[0,1]$
cannot produce a negative weight --- Proposition~\ref{prop:falsi} extended from
two points to three.
But $178$ of the $262$ --- two thirds --- sit on \texttt{fun3}--\texttt{fun6},
which are pure powers of order $3$, $5$, $9$ and $19$, and there \emph{no}
interpolating model works: our step removes $0.41$--$0.81$ halvings of the
bracket per evaluation, inverse quadratic interpolation from the same points only
$0.01$--$0.17$. Chandrupatla does not win those families with his model --- his
traced run bisects $93$, $96$, $100$ and $100\%$ of his steps. We lose them
because our step is slower than bisection there and our guarantee, at most twice
bisection, licenses exactly that. His band inside our step confirms it family by
family: under the projection guard it takes \texttt{fun4}, \texttt{fun5},
\texttt{fun6} from $573$, $456$, $456$ to $296$, $239$, $235$ --- against his own
$293$, $228$, $228$ --- and costs $+164$, $+111$, $+170$, $+144$ on \texttt{fun1},
\texttt{fun2}, \texttt{fun8}, \texttt{fun9}, where it refuses steps that were good.
So most of what remains is not the order of the model but the decision \emph{when
to give the model up}, and it is open because every detector measured so far
confounds ``the model is useless here'' with something else: the band with ``the
configuration is not shaped for inverse quadratic interpolation'', bracket
progress with ``the step has not moved the bracket yet'' ($4449$ on the canonical
set), and the local order with nothing at all --- $m\ge2$ is exactly the
condition under which a tangent step loses to a halving, since it lands at
$e(1-1/m)$, and it is clean ($0.2\%$ of canonical states) but blind: it reports
$m\ge2$ on only $5.8\%$ of states of families whose order is by construction $3$
to $19$. For the \texttt{fun1} part, three parameter-free ways round the model
order have been measured and all three fail in the same way, buying the wide brackets at the price of the
canonical set: a fixed fraction of the bracket (\texttt{cbrt}), a logarithmic
change of variable, and a curvature-corrected scale (\texttt{derived3\_m},
whose derivation holds but whose estimator of the local order fires on $27.9\%$
@@ -895,25 +940,26 @@ asserted.
\subsection*{Future work}
One weakness has a clear diagnosis and no fix inside this framework. On
brackets many orders of magnitude wider than the distance to the root, the
search must cross those decades, and an \emph{additive} step of $|f|/|f'|$ is
sized by the local slope, which knows nothing about how many decades remain.
That is why the cube-root similarity, whose step is a fixed \emph{fraction} of
the bracket, beats the proposed rule $2.6$-fold on those families, and why ITP
keeps its lead on the Chandrupatla set.
The diagnosis of \S\ref{sec:widelimit} leaves one question, and it is narrower
than the one this section used to pose. An earlier version proposed a
logarithmic change of variable to turn an additive step into a geometric
march; that was measured and refuted (\S\ref{sec:widelimit} and the negative
results), and the step turned out to be a fraction of the bracket already.
The regime is therefore multiplicative versus additive movement, not one
similarity function versus another --- a change of the space the method works
in, not of a coefficient inside it. The natural instrument is a logarithmic
change of variable, $u = \operatorname{sign}(x-c)\log(1+|x-c|/s)$, in which an
additive step becomes a geometric march and the exponent that the cube root
fixes at $1/3$ by accident disappears as a free quantity. Two conditions
decide whether that is a method or another refuted idea: the origin $c$ and
unit $s$ must be read from the bracket rather than chosen, and the transform
must be the identity to within rounding on narrow brackets, so that the
wide-bracket families are not bought at the price of the canonical set. Every
attempt reported in Section~7 failed on exactly that second condition.
What is open is a decision, not a model. On two thirds of the remaining gap
the right move is to halve, and Chandrupatla halves; our guarantee permits a
step that is slower than halving, and nothing we have measured tells our step
when it is. The condition itself is known exactly --- a tangent step on an
approach of local order $m$ lands at $e(1-1/m)$ and loses to a halving when
$m\ge2$ --- so the constant is bisection's $\tfrac12$ and not a choice. What is
missing is a reading of $m$ that sees it: the three-point estimator reports
$m\ge2$ on $0.2\%$ of canonical states, which is the right silence, and on
$5.8\%$ of states of pure powers of order $3$ to $19$, which is blindness. Two
successive residuals of the run itself carry the order more directly than a
parabola through three points --- on an order-$m$ approach under tangent steps
their ratio settles at $(1-1/m)^m$, while at a simple root it goes to zero ---
and that is the reading we would test next, under the same rule as every other
here: pre-registered, with the canonical set as the price it must not charge.
\begin{thebibliography}{9}
\bibitem{aps} G.~E. Alefeld, F.~A. Potra and Y.~Shi, ``Algorithm 748: Enclosing
+37
View File
@@ -0,0 +1,37 @@
"""Consequence check of the new diagnosis, read off the existing sweep_decision.json.
If the flat families are won by BISECTING (Chandrupatla bisects 93-100% there), then his
band inside our step - which refuses and bisects - must HELP on fun3-fun6 and HURT on
the families where our step is good. Per-problem costs, band against no gate, same
similarity, scale and guard. No new run: the numbers are the Linux sweep's.
python -u diag_band_by_family.py
"""
from __future__ import annotations
import json
from collections import defaultdict
def main():
with open("sweep_decision.json", encoding="utf8") as fh:
rows = json.load(fh)
by = {(r["spec"][1], r["spec"][5], r["spec"][3]): r for r in rows}
for gu in ("project", "credit"):
base = by[("ratio", "none", gu)]["CHA"]["per"]
band = by[("ratio", "band", gu)]["CHA"]["per"]
fam = defaultdict(lambda: [0, 0])
for pid, (n0, ok0) in base.items():
n1, ok1 = band[pid]
k = pid.split(".")[0]
fam[k][0] += n0 if ok0 else 200
fam[k][1] += n1 if ok1 else 200
print(f"guard={gu}: family no-gate band delta")
for k in sorted(fam):
a, b = fam[k]
print(f" {k:<6} {a:7d} {b:6d} {b - a:+6d}")
print()
if __name__ == "__main__":
main()
+66
View File
@@ -0,0 +1,66 @@
"""Is the flat regime DETECTABLE from evaluations already paid for?
Established by diag_model_order.py and its checks: two thirds of the wide-bracket gap sit
on the flat families (fun3-fun6), where Chandrupatla bisects 93-100% of his steps and our
step removes 0.41-0.81 halvings of the bracket per evaluation - less than bisection's 1.
On an approach |f| ~ K e^m the tangent step lands at e(1 - 1/m): it beats bisection's
halving only while 1 - 1/m < 1/2, i.e. m < 2. The 2 is bisection's 1/2 rewritten, not a
chosen number. m is what _local_order reads from the three latest evaluations.
Question measured here, BEFORE anything is built: does "m >= 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()
+205
View File
@@ -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()
+83
View File
@@ -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()
+22
View File
@@ -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
+4
View File
@@ -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%
+47
View File
@@ -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)
+18
View File
@@ -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
+12
View File
@@ -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 ==="
+9
View File
@@ -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
+42
View File
@@ -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