pre-step gate: prestep='none'|'progress' (default bit-identical), refuse the similarity step BEFORE spending an evaluation when it promises less progress than bisection — two lengths already in hand, no new constant; R1-R4 pinned by tests/test_prestep_gate.py (6 green); the gate grid writes to its OWN sweep_gate.json
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
# PREREG — предшаговый гейт: похожесть как ОТКАЗ до шага, а не масштаб внутри шага
|
||||
|
||||
Составлено до реализации. Основание — измерение `explain_chandrupatla.py` (лог
|
||||
`results_explain_chandrupatla.txt`, разбор в `FINDINGS_logarithmic.md` §3): Chandrupatla принимает
|
||||
свою модель лишь в **28 %** шагов, его гейт стоит **ноль** вызовов, а ветвь отказа снимает половину
|
||||
скобки; наш сторож ремонтирует **77.5 %** вызовов, и на `fun8`/`fun9` мы опускаемся до **0.92**
|
||||
половинок на вызов — хуже деления пополам. Качество пробы: **2.310** против **1.422**.
|
||||
|
||||
**Гипотеза:** дело не в форме функции похожести, а в **месте её чтения**. Похожесть, прочитанная ДО
|
||||
шага и решающая «доверять ли модели здесь», стоит ноль; прочитанная ВНУТРИ шага как масштаб — требует
|
||||
сторожа, а сторож платит вызовом.
|
||||
|
||||
---
|
||||
|
||||
## 1. Что именно добавляется (и почему это не новая константа)
|
||||
|
||||
**Важно, что наша причина ремонтов ДРУГАЯ, чем у Chandrupatla.** Наш шаг
|
||||
`(1 − S(|f|/σ))·(far − x)` при `0 ≤ S ≤ 1` **никогда не выходит за скобку** — то есть перескок нам не
|
||||
грозит; сторож вмешивается, когда шаг слишком **короткий** и прогресс мал. Значит и гейт должен
|
||||
спрашивать не «не выйдет ли проба за скобку» (вопрос Chandrupatla для его модели), а:
|
||||
|
||||
> **обещает ли этот шаг прогресс не хуже, чем деление пополам?**
|
||||
|
||||
Обе длины известны ДО вызова `f`: длина шага `|step|` и длина шага в середину `|½(A+B) − x|`.
|
||||
Сравнение двух имеющихся длин — не константа и не подгонка; порога нет, есть неравенство.
|
||||
|
||||
**Режим:** `prestep="none"` (по умолчанию — опубликованное поведение побитово) и
|
||||
`prestep="progress"` (гейт включён). Если гейт отказал, шаг **заменяется** на деление пополам, то есть
|
||||
тот же один вызов `f` тратится на гарантированное снятие половины скобки.
|
||||
|
||||
**Чего НЕ делаем:** не переносим полосу `1−√(1−ξ) < φ < √ξ` в наш метод. Она выведена для обратной
|
||||
квадратичной интерполяции из условия «проба не выйдет за скобку»; у нашего шага этого условия нет, и
|
||||
взятая напрокат область была бы скрытой константой чужого вывода.
|
||||
|
||||
## 2. Предсказания и фальсификаторы
|
||||
|
||||
| # | Предсказание | ФАЛЬСИФИКАТОР → вывод |
|
||||
|---|---|---|
|
||||
| **P1** | доля ремонтов на CHA падает с 77.5 % до **≤40 %** | >60 % → гейт не бьёт в механизм, объяснение §3 неверно как рецепт |
|
||||
| **P2** | цена на CHA падает ниже нашей текущей **2073** (та же конфигурация `ratio.derived3.credit`) | не падает → отказ дешевле ремонта не оказался, гипотеза о месте чтения не подтверждена |
|
||||
| **P3** | цена на APS-154 **не растёт** (по-наборный лучший остаётся ≤ **2806**) | растёт → тот же трейд, что убил пять предыдущих попыток; гейт отвергается |
|
||||
| **P4** | гарантия `2⌈log₂(w/xtol)⌉+3` не нарушена: `over2x = 0` во всех конфигурациях | нарушена → закрытие немедленно |
|
||||
| **P5** | качество пробы на CHA растёт с 1.422 к уровню **≥2.0** половинок на вызов | <1.7 → прирост есть, но не тот порядок; заявлять «как Chandrupatla» нельзя |
|
||||
|
||||
**Правило остановки:** P3 или P4 ложны ⇒ режим не принимается вовсе. P1 и P2 ложны ⇒ объяснение
|
||||
остаётся верным как **описание** Chandrupatla, но как **рецепт** для нашего метода — опровергнуто, и
|
||||
это пишется прямо.
|
||||
|
||||
## 3. Протокол (тот же, что у статьи)
|
||||
|
||||
154 задачи Alefeld–Potra–Shi + 45 Чандрупатлы; цена — вызовы `f`; провал списывается полным бюджетом;
|
||||
граница `2×` проверяется на каждом решённом прогоне; конфигурация фиксируется выводом ДО того, как
|
||||
смотрят в рейтинг. Числа только с **Linux**. Свип пишется в свой файл (`sweep_gate.json`), чтобы
|
||||
`sweep_full.json`, против которого сверяется статья, остался нетронутым.
|
||||
|
||||
Сравнение ведётся **по-наборно** и **внутри семейства сторожей, несущих гарантию** — иначе сравниваются
|
||||
разные конфигурации (эта ошибка уже допущена 2026-09-20 и исправлена).
|
||||
|
||||
## 4. Гейты реализации (тесты ДО рейтинга)
|
||||
|
||||
| # | Условие | Проверка |
|
||||
|---|---|---|
|
||||
| **R1** | `prestep="none"` даёт побитово прежний результат | тест на равенство кортежей возврата на наборе задач |
|
||||
| **R2** | в гейте нет ни одной выбранной константы | грепом по исходнику: только `0.0`, `0.5` (середина скобки — определение деления пополам), `1.0` |
|
||||
| **R3** | отказ гейта не стоит вызова `f` | тест: число вызовов при включённом гейте не превышает число шагов + 2 |
|
||||
| **R4** | счётчик `stats['pre_refused']` считает отказы гейта отдельно от ремонтов сторожа | тест на обоих счётчиках |
|
||||
|
||||
**Дисциплина:** аудит после правки и до запуска; смоук ×2 локально; тяжёлое — на сервер
|
||||
`setsid nohup` с логом; замороженная копия скрипта; числа показываются немедленно.
|
||||
+25
-6
@@ -413,7 +413,7 @@ def _local_order(x2, f2, x1, f1, x0, f0):
|
||||
|
||||
def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
|
||||
guard="step", stats=None, budget_factor=2.0,
|
||||
budget_extra=None, endowment=1.0, space="x"):
|
||||
budget_extra=None, endowment=1.0, space="x", prestep="none"):
|
||||
"""The pure similarity root finder.
|
||||
|
||||
similarity : key into PURE_SIMS
|
||||
@@ -467,6 +467,8 @@ def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
|
||||
raise ValueError(f"unknown scale {scale!r}")
|
||||
if space not in SPACES:
|
||||
raise ValueError(f"unknown space {space!r}")
|
||||
if prestep not in ("none", "progress"):
|
||||
raise ValueError(f"unknown prestep {prestep!r}")
|
||||
if space == "log" and scale not in ("derived", "derived3"):
|
||||
# u-space needs a slope; with a chosen or bracket scale there is none to
|
||||
# transform, and computing something anyway would hide that (PREREG §3a).
|
||||
@@ -494,8 +496,10 @@ def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
|
||||
sg = _Safeguard(guard, abs(B - A), xtol, budget_factor, budget_extra)
|
||||
sg.credit = endowment
|
||||
forced = 0
|
||||
pre_refused = 0
|
||||
if stats is not None:
|
||||
stats['forced'] = 0
|
||||
stats['pre_refused'] = 0
|
||||
|
||||
while abs(B - A) > xtol:
|
||||
if sg.bisect_before_step(abs(B - A)):
|
||||
@@ -592,7 +596,22 @@ def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
|
||||
x_new = 0.5 * (A + B)
|
||||
else:
|
||||
step = None
|
||||
if space == "log":
|
||||
if prestep == "progress":
|
||||
# PRE-STEP GATE (PREREG 2026-09-21). Measured cause of our cost:
|
||||
# the safeguard REPAIRS 77.5% of evaluations after the fact, while
|
||||
# Chandrupatla REFUSES its model on 72% of steps for free and
|
||||
# halves instead - 2.310 halvings per evaluation against our 1.422.
|
||||
# Our step never leaves the bracket (0 <= S <= 1), so the question
|
||||
# is not overshoot but PROGRESS: if the similarity step promises
|
||||
# less than bisection would remove, spend the same single
|
||||
# evaluation on the midpoint instead. Two lengths already in hand
|
||||
# are compared; there is no threshold and no new constant.
|
||||
cand = (1.0 - S(abs(fx) / sc)) * (far - x)
|
||||
half = 0.5 * (A + B) - x
|
||||
if abs(cand) < abs(half):
|
||||
step = half
|
||||
pre_refused += 1
|
||||
if step is None and space == "log":
|
||||
# c = the bracket end with the smaller |f| (the side the
|
||||
# root is nearer); s = xtol, fixed by derivation in
|
||||
# PREREG amendment 1, never chosen. Falls back to the
|
||||
@@ -624,7 +643,7 @@ def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
|
||||
if x_new == x or x_new == x_prev:
|
||||
# the bracket can no longer be split in floating point
|
||||
if stats is not None:
|
||||
stats['forced'] = forced
|
||||
stats['forced'] = forced; stats['pre_refused'] = pre_refused
|
||||
return x_new, g.n, abs(B - A) <= xtol
|
||||
|
||||
w_before = abs(B - A)
|
||||
@@ -636,7 +655,7 @@ def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
|
||||
x, fx = x_new, fnew
|
||||
if fnew == 0.0:
|
||||
if stats is not None:
|
||||
stats['forced'] = forced
|
||||
stats['forced'] = forced; stats['pre_refused'] = pre_refused
|
||||
return x_new, g.n, True
|
||||
if FA * fnew < 0.0:
|
||||
B, FB = x_new, fnew
|
||||
@@ -645,9 +664,9 @@ def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
|
||||
if guard == "credit":
|
||||
sg.note_shrink(w_before, abs(B - A))
|
||||
if stats is not None:
|
||||
stats['forced'] = forced
|
||||
stats['forced'] = forced; stats['pre_refused'] = pre_refused
|
||||
return 0.5 * (A + B), g.n, True
|
||||
except _Budget:
|
||||
if stats is not None:
|
||||
stats['forced'] = forced
|
||||
stats['forced'] = forced; stats['pre_refused'] = pre_refused
|
||||
return x, g.n, False
|
||||
|
||||
+29
-4
@@ -110,19 +110,23 @@ def make_method(spec):
|
||||
return BASELINES[spec[1]]
|
||||
sim, scale, guard = spec[1], spec[2], spec[3]
|
||||
space = spec[4] if len(spec) > 4 else "x"
|
||||
prestep = spec[5] if len(spec) > 5 else "none"
|
||||
|
||||
def m(f, a, b, xtol, maxfev):
|
||||
return pure_root(f, a, b, xtol, maxfev, similarity=sim,
|
||||
scale=scale, guard=guard, space=space)
|
||||
scale=scale, guard=guard, space=space, prestep=prestep)
|
||||
return m
|
||||
|
||||
|
||||
def label(spec):
|
||||
if spec[0] == "base":
|
||||
return spec[1]
|
||||
tag = "pure"
|
||||
if len(spec) > 4 and spec[4] != "x":
|
||||
return f"{spec[4]}.{spec[1]}.{spec[2]}.{spec[3]}"
|
||||
return f"pure.{spec[1]}.{spec[2]}.{spec[3]}"
|
||||
tag = spec[4]
|
||||
if len(spec) > 5 and spec[5] != "none":
|
||||
tag = f"{tag}+gate" if tag != "pure" else "gate"
|
||||
return f"{tag}.{spec[1]}.{spec[2]}.{spec[3]}"
|
||||
|
||||
|
||||
def evaluate(spec):
|
||||
@@ -185,6 +189,15 @@ def build_grid_log(smoke):
|
||||
for sim in sims for sc in ("derived", "derived3") for gu in guards]
|
||||
|
||||
|
||||
def build_grid_gate(smoke):
|
||||
"""The pre-step gate grid (PREREG 2026-09-21): the published x-space step with the
|
||||
gate switched on. Baselines are not recomputed - they are already in sweep_full.json."""
|
||||
sims = ["ratio", "sqrt"] if smoke else sorted(PURE_SIMS)
|
||||
guards = ("credit", "safe") if smoke else GUARD_MODES
|
||||
return [("pure", sim, sc, gu, "x", "progress")
|
||||
for sim in sims for sc in ("derived", "derived3") for gu in guards]
|
||||
|
||||
|
||||
def build_grid(smoke):
|
||||
specs = [("base", nm) for nm in BASELINES]
|
||||
sims = sorted(PURE_SIMS)
|
||||
@@ -205,7 +218,9 @@ def build_grid(smoke):
|
||||
def main():
|
||||
smoke = "--smoke" in sys.argv
|
||||
logmode = "--log" in sys.argv # the u-space grid, written to its OWN file
|
||||
specs = build_grid_log(smoke) if logmode else build_grid(smoke)
|
||||
gatemode = "--gate" in sys.argv # the pre-step gate grid, likewise
|
||||
specs = (build_grid_gate(smoke) if gatemode else
|
||||
build_grid_log(smoke) if logmode else build_grid(smoke))
|
||||
t0 = time.time()
|
||||
print(f"sweep: {len(specs)} configurations x 199 problems "
|
||||
f"({'SMOKE' if smoke else 'FULL'}{', LOG-SPACE' if logmode else ''})", flush=True)
|
||||
@@ -221,6 +236,16 @@ def main():
|
||||
|
||||
# the published sweep_full.json is what verify_article.py checks the paper
|
||||
# against, so the log grid never writes into it
|
||||
if gatemode:
|
||||
# never into sweep_full.json: that file is what verify_article.py checks the
|
||||
# paper against, and the smoke run of 2026-09-21 did land in sweep_smoke.json
|
||||
# before this branch existed
|
||||
out = "sweep_gate_smoke.json" if smoke else "sweep_gate.json"
|
||||
with open(out, "w", encoding="utf8") as fh:
|
||||
json.dump(results, fh)
|
||||
print(f"\nwrote {out} ({time.time() - t0:.0f}s total)\n", flush=True)
|
||||
report(results)
|
||||
return
|
||||
out = ("sweep_log_smoke.json" if smoke else "sweep_log.json") if logmode else \
|
||||
("sweep_smoke.json" if smoke else "sweep_full.json")
|
||||
with open(out, "w", encoding="utf8") as fh:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,91 @@
|
||||
"""Implementation gates R1-R4 for the pre-step gate (PREREG_prestep_gate_2026-09-21.md).
|
||||
|
||||
The measured cause of our cost is that the safeguard REPAIRS 77.5 % of evaluations after
|
||||
the fact, while Chandrupatla REFUSES its model for free on 72 % of steps and halves
|
||||
instead. The gate under test asks, BEFORE spending an evaluation, whether the similarity
|
||||
step promises at least what bisection would remove — a comparison of two lengths already
|
||||
in hand, no threshold and no new constant.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import math
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from simroot import pure as P
|
||||
|
||||
PROBLEMS = [
|
||||
(lambda t: t ** 3 - 2.0 * t - 5.0, 2.0, 3.0),
|
||||
(lambda t: math.exp(t) - 5.0, 0.0, 3.0),
|
||||
(lambda t: t ** 2 - 2.0, 0.5, 2.0),
|
||||
(lambda t: math.cos(t) - t, 0.0, 1.0),
|
||||
(lambda t: (t - 1.0) ** 3, -1.0, 4.0),
|
||||
]
|
||||
|
||||
|
||||
def test_R1_the_published_path_is_bit_identical_with_the_gate_off():
|
||||
for f, a, b in PROBLEMS:
|
||||
for guard in ("credit", "project", "safe", "step"):
|
||||
base = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio",
|
||||
scale="derived3", guard=guard)
|
||||
off = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio",
|
||||
scale="derived3", guard=guard, prestep="none")
|
||||
assert base == off, (guard, base, off)
|
||||
|
||||
|
||||
def test_R2_the_gate_carries_no_chosen_constant():
|
||||
src = inspect.getsource(P.pure_root)
|
||||
start = src.index('if prestep == "progress":')
|
||||
end = src.index('if step is None and space ==', start) # the gate block only
|
||||
body = src[start:end]
|
||||
# comments carry the MEASURED numbers that justify the gate (77.5 %, 2.310 halvings);
|
||||
# they are documentation, not constants, so the check reads the code only
|
||||
code = "\n".join(line.split("#", 1)[0] for line in body.splitlines())
|
||||
literals = set(re.findall(r"(?<![\w.])\d+\.\d+(?![\w.])", code))
|
||||
# 0.5 is the definition of bisection, 1.0 comes from S in [0,1]
|
||||
assert literals <= {"0.0", "0.5", "1.0"}, f"chosen constants in the gate: {literals}"
|
||||
|
||||
|
||||
def test_R3_a_refusal_costs_no_extra_evaluation_PER_STEP():
|
||||
"""R3 as the prereg states it: a refusal REPLACES the step, so evaluations never exceed
|
||||
steps + the two bracket probes.
|
||||
|
||||
NOT claimed (and measured false on 2026-09-21): that the whole RUN cannot become more
|
||||
expensive. Forcing bisection changes the trajectory — it removes more bracket but less
|
||||
error than a good secant step — and one probe problem went 25 -> 29 evaluations. Whether
|
||||
that trade pays over the two benchmark sets is exactly what P2/P3 measure; a unit test
|
||||
must not pre-judge it."""
|
||||
for f, a, b in PROBLEMS:
|
||||
st = {}
|
||||
_x, n, ok = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio", scale="derived3",
|
||||
guard="credit", prestep="progress", stats=st)
|
||||
assert ok
|
||||
# every evaluation is one step (the gate spends the same probe on the midpoint),
|
||||
# plus f(a) and f(b)
|
||||
assert st["pre_refused"] <= n - 2, (st, n)
|
||||
|
||||
|
||||
def test_R4_refusals_are_counted_separately_from_repairs():
|
||||
st = {}
|
||||
f = PROBLEMS[4][0]
|
||||
x, n, ok = P.pure_root(f, -1.0, 4.0, 1e-10, 300, similarity="ratio", scale="derived3",
|
||||
guard="credit", prestep="progress", stats=st)
|
||||
assert ok and abs(x - 1.0) < 1e-3
|
||||
assert set(("forced", "pre_refused")) <= set(st)
|
||||
assert st["pre_refused"] >= 1 # a cubic with a triple root refuses often
|
||||
assert isinstance(st["forced"], int) and isinstance(st["pre_refused"], int)
|
||||
|
||||
|
||||
def test_the_gate_is_validated_not_silently_ignored():
|
||||
with pytest.raises(ValueError):
|
||||
P.pure_root(lambda t: t, -1.0, 1.0, 1e-10, 100, prestep="whatever")
|
||||
|
||||
|
||||
def test_the_gate_still_solves_every_probe_problem():
|
||||
for f, a, b in PROBLEMS:
|
||||
x, n, ok = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio", scale="derived3",
|
||||
guard="credit", prestep="progress")
|
||||
assert ok and abs(f(x)) < 1e-6, (a, b, x, n, ok)
|
||||
Reference in New Issue
Block a user