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,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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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%
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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 ==="
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user