two more places for similarity: points='shape' (WHICH triple the model uses, argmin |phi-xi| over declared candidates — select, never weight) and direction='dissimilar' (the far end read by |f| rather than by distance); defaults bit-identical, P1-P5 pinned (7 green), inertness counters make an inert mode a failed EXPERIMENT rather than a pass; each grid writes to its own file

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
vvs
2026-09-20 23:39:41 +01:00
co-authored by Claude Opus 5
parent e4769be9a3
commit 04ee5c89b9
6 changed files with 320 additions and 8 deletions
+87
View File
@@ -0,0 +1,87 @@
# PREREG — похожесть в двух ещё незанятых местах: СОСТАВ ТОЧЕК и НАПРАВЛЕНИЕ
Составлено до реализации. Основание — `FINDINGS_slope_gate.md`: решает **валюта и место** чтения
похожести. Одна и та же идея «отказывайся до шага» дала 4449 вызовов на APS при отказе по прогрессу
скобки и 2848 при отказе по форме модели — разница 1601 вызов. Карта мест решения показала два
незанятых:
| место | чем решается сейчас |
|---|---|
| **состав точек для модели** | **давностью**: `x_prev2 ← x_prev ← x ← x_new`, всегда три последних |
| **направление** (какой конец «дальний») | **расстоянием**: `far = B if \|B−x\| ≥ \|A−x\| else A` |
---
## 1. Режим A — состав точек по ФОРМЕ (`points="shape"`)
Оценка наклона строится на тройке точек. Сейчас тройка берётся по давности, даже если она
плохо обусловлена. Brent, TOMS 748 и Chandrupatla все **управляют составом точек** — это часть их
конструкции, а у нас этого места нет вовсе.
**Признак — та же валюта, что уже сработала: форма конфигурации, безразмерно.** Для тройки
`(x, u, v)` считаются два отношения в духе Chandrupatla:
ξ = (x − u)/(v − u) — отношение ПОЗИЦИЙ
φ = (f(x) − f(u))/(f(v) − f(u)) — отношение ЗНАЧЕНИЙ
Для линейной `f` выполняется `φ = ξ` точно, поэтому `|φ − ξ|` — безразмерная мера непохожести
конфигурации на линейную. Правило: **argmin |φ − ξ|** по объявленному списку кандидатов.
**Список кандидатов объявляется здесь и не расширяется по результату:**
T1 = (x, x_prev, x_prev2) — нынешнее правило давности
T2 = (x, x_prev, far) — свежая точка плюс дальний конец скобки
T3 = (x, A, B) — оба конца доказанной скобки
Вырожденные (совпадающие абсциссы или значения) выбрасываются. Порога нет: это **argmin по
имеющимся вариантам**, а не сравнение с числом. Правило корпуса «select, never weight» соблюдено —
тройка **выбирается**, оценки не взвешиваются.
## 2. Режим B — направление по НЕПОХОЖЕСТИ (`direction="dissimilar"`)
Сейчас «дальний» конец — геометрически более удалённый. Похожесть даёт другой ответ: дальним считать
конец, чьё `|f|` **больше**, то есть менее похожее на ноль. Ни константы, ни порога — сравнение двух
имеющихся величин.
**Режимы независимы и мерятся отдельно** (правило «менять одно за раз»); комбинация A+B мерится
третьим прогоном только если оба по отдельности не ухудшили APS.
## 3. Цели
Точка отсчёта — уже достигнутое гейтом на наклоне (`slope.ratio.derived3.project`):
APS **2848**, CHA **3074**, два набора **5922**, 193/199, `over2x = 0`.
| величина | цель |
|---|---|
| APS-154 | **≤ 2848** (не хуже текущего; идеально — вернуть 2806) |
| CHA-45 | **≤ 2857** (недобранная цель ITP; сейчас 2903 по-наборно) |
| два набора | **< 5922** |
| решено | ≥ 193/199 |
| гарантия `2⌈log₂(w/xtol)⌉+3` | `over2x = 0` |
## 4. Предсказания и фальсификаторы
| # | Предсказание | ФАЛЬСИФИКАТОР → вывод |
|---|---|---|
| **A1** | режим A не ухудшает APS (≤2848) и улучшает CHA (<2903) | ухудшает APS → состав точек не то место, режим отвергается |
| **A2** | выбор тройки **не инертен**: отличается от давностной в ≥5 % шагов | <1 % → режим ничего не меняет, замер НЕ является свидетельством ни за, ни против |
| **B1** | режим B не ухудшает APS (≤2848) | ухудшает → направление по непохожести отвергается |
| **B2** | выбор конца отличается от геометрического в ≥5 % шагов | <1 % → инертен, замер пуст |
| **C** | гарантия цела в обоих режимах | нарушена → немедленное закрытие режима |
**Правило остановки:** APS ухудшился или гарантия нарушена ⇒ режим не принимается. Инертность (A2/B2)
⇒ результат не толкуется как подтверждение: это отсутствие эксперимента, а не его исход.
## 5. Гейты реализации (тесты ДО рейтинга)
| # | Условие | Проверка |
|---|---|---|
| **P1** | `points="recent"` и `direction="distance"` дают побитово прежний результат | равенство кортежей на наборе задач и сторожей |
| **P2** | в обоих признаках нет констант | греп по коду (без комментариев): только `0.0`, `0.5`, `1.0` |
| **P3** | счётчики `stats['triple_changed']`, `stats['dir_changed']` считают неинертность | тест на обоих |
| **P4** | вырожденные тройки отбрасываются, а не дают деление на ноль | тест на совпадающих абсциссах и значениях |
| **P5** | ни один режим не добавляет вызовов `f` | число вызовов ≤ число шагов + 2 |
**Дисциплина:** аудит после правки и до запуска; смоук ×2 локально; свипы в свои файлы
(`sweep_points.json`, `sweep_direction.json`), `sweep_full.json` не трогать; числа только с Linux;
замороженные копии; числа показываются немедленно.
+87 -6
View File
@@ -121,6 +121,47 @@ GUARDS = ("none", "bracket", "step", "both", "project", "safe", "credit")
SPACES = ("x", "log")
POINT_RULES = ("recent", "shape")
DIRECTIONS = ("distance", "dissimilar")
def _shape_defect(x, fx, u, fu, v, fv):
"""|phi - xi| for the triple, or None when it is degenerate.
xi = (x - u)/(v - u) is a ratio of POSITIONS, phi = (fx - fu)/(fv - fu) a ratio of
VALUES; both are invariant under affine rescaling of x and of f. For a linear f they
are equal, so their gap measures how unlike a linear configuration the triple is —
the same dimensionless currency Chandrupatla's acceptance band is written in."""
if v == u or fv == fu:
return None
xi = (x - u) / (v - u)
ph = (fx - fu) / (fv - fu)
if not (math.isfinite(xi) and math.isfinite(ph)):
return None
return abs(ph - xi)
def _pick_triple(x, fx, x_prev, f_prev, x_prev2, f_prev2, A, FA, B, FB, far):
"""SELECT the triple whose configuration is least unlike a linear one (PREREG
2026-09-21, mode A). Candidates are declared, not searched: the recency triple, the
recent point with the far bracket end, and the two proven bracket ends. Returns
((xu, fu), (xv, fv)) or None when nothing is admissible."""
far_f = FB if far == B else FA
cands = (((x_prev, f_prev), (x_prev2, f_prev2)),
((x_prev, f_prev), (far, far_f)),
((A, FA), (B, FB)))
best, best_d = None, None
for u, v in cands:
if u[0] is None or v[0] is None:
continue
d = _shape_defect(x, fx, u[0], u[1], v[0], v[1])
if d is None or x == u[0] or x == v[0]:
continue
if best_d is None or d < best_d:
best, best_d = (u, v), d
return best
def _log_map(t, c, s):
"""u = sign(t - c) * log1p(|t - c| / s). An ADDITIVE step in u is a
MULTIPLICATIVE step in x, which is what the wide-bracket families need: the
@@ -413,7 +454,8 @@ 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", prestep="none"):
budget_extra=None, endowment=1.0, space="x", prestep="none",
points="recent", direction="distance"):
"""The pure similarity root finder.
similarity : key into PURE_SIMS
@@ -469,6 +511,14 @@ def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
raise ValueError(f"unknown space {space!r}")
if prestep not in ("none", "progress", "slope"):
raise ValueError(f"unknown prestep {prestep!r}")
if points not in POINT_RULES:
raise ValueError(f"unknown points rule {points!r}")
if direction not in DIRECTIONS:
raise ValueError(f"unknown direction {direction!r}")
if points == "shape" and scale not in ("derived3", "derived3_rho", "derived3_m"):
# the triple only exists for the three-point scale; selecting one under a
# two-point scale would change nothing and hide that
raise ValueError("points='shape' requires a three-point scale (derived3*)")
if prestep == "slope" and scale not in ("derived", "derived3"):
# there is no slope to judge under a chosen or bracket scale; judging something
# else would hide that, so the call fails instead
@@ -501,11 +551,15 @@ def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
sg.credit = endowment
forced = 0
pre_refused = 0
triple_changed = 0
dir_changed = 0
slope = 0.0
slope_prev = None
if stats is not None:
stats['forced'] = 0
stats['pre_refused'] = 0
stats['triple_changed'] = 0
stats['dir_changed'] = 0
while abs(B - A) > xtol:
if sg.bisect_before_step(abs(B - A)):
@@ -516,11 +570,38 @@ def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
sg.note_step(x_new - x)
else:
far = B if abs(B - x) >= abs(A - x) else A
if direction == "dissimilar":
# PREREG 2026-09-21, mode B: the far end read by SIMILARITY instead of
# geometry — the end whose |f| is larger is the one LESS similar to zero,
# i.e. the one the root is further from. Two values already in hand are
# compared; no constant.
far_sim = B if abs(FB) >= abs(FA) else A
if far_sim != far:
dir_changed += 1
far = far_sim
if scale in ("derived", "derived_rho"):
slope = ((fx - f_prev) / (x - x_prev)) if x != x_prev else 0.0
sc = abs(slope) * abs(far - x)
elif scale in ("derived3", "derived3_rho", "derived3_m"):
slope = _slope3(x, fx, x_prev, f_prev, x_prev2, f_prev2)
if points == "shape":
# PREREG 2026-09-21, mode A: which THREE points the model is built
# on, chosen by the SHAPE of the configuration instead of by recency.
# For a linear f the position ratio and the value ratio coincide, so
# |phi - xi| is a dimensionless measure of how unlike a linear
# configuration a triple is; the triple minimising it is SELECTED
# (the corpus rule is select, never weight). argmin over the declared
# candidates needs no threshold.
pick = _pick_triple(x, fx, x_prev, f_prev, x_prev2, f_prev2,
A, FA, B, FB, far)
if pick is None:
slope = _slope3(x, fx, x_prev, f_prev, x_prev2, f_prev2)
else:
(xu, fu), (xv, fv) = pick
if xu != x_prev or xv != x_prev2:
triple_changed += 1
slope = _slope3(x, fx, xu, fu, xv, fv)
else:
slope = _slope3(x, fx, x_prev, f_prev, x_prev2, f_prev2)
sc = abs(slope) * abs(far - x)
if scale == "derived3_m":
# REFUTED as an improvement - kept for reproducibility.
@@ -665,7 +746,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['pre_refused'] = pre_refused
stats['forced'] = forced; stats['pre_refused'] = pre_refused; stats['triple_changed'] = triple_changed; stats['dir_changed'] = dir_changed
return x_new, g.n, abs(B - A) <= xtol
w_before = abs(B - A)
@@ -677,7 +758,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['pre_refused'] = pre_refused
stats['forced'] = forced; stats['pre_refused'] = pre_refused; stats['triple_changed'] = triple_changed; stats['dir_changed'] = dir_changed
return x_new, g.n, True
if FA * fnew < 0.0:
B, FB = x_new, fnew
@@ -686,9 +767,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['pre_refused'] = pre_refused
stats['forced'] = forced; stats['pre_refused'] = pre_refused; stats['triple_changed'] = triple_changed; stats['dir_changed'] = dir_changed
return 0.5 * (A + B), g.n, True
except _Budget:
if stats is not None:
stats['forced'] = forced; stats['pre_refused'] = pre_refused
stats['forced'] = forced; stats['pre_refused'] = pre_refused; stats['triple_changed'] = triple_changed; stats['dir_changed'] = dir_changed
return x, g.n, False
File diff suppressed because one or more lines are too long
+38 -2
View File
@@ -111,10 +111,13 @@ def make_method(spec):
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"
points = spec[6] if len(spec) > 6 else "recent"
direction = spec[7] if len(spec) > 7 else "distance"
def m(f, a, b, xtol, maxfev):
return pure_root(f, a, b, xtol, maxfev, similarity=sim,
scale=scale, guard=guard, space=space, prestep=prestep)
scale=scale, guard=guard, space=space, prestep=prestep,
points=points, direction=direction)
return m
@@ -124,6 +127,10 @@ def label(spec):
tag = "pure"
if len(spec) > 4 and spec[4] != "x":
tag = spec[4]
if len(spec) > 6 and spec[6] != "recent":
tag = f"{tag}+shape"
if len(spec) > 7 and spec[7] != "distance":
tag = f"{tag}+dissim"
if len(spec) > 5 and spec[5] != "none":
# name the gate by WHICH gate it is: "progress" is a measured negative and must
# never be confused with "slope" in a results file
@@ -191,6 +198,23 @@ def build_grid_log(smoke):
for sim in sims for sc in ("derived", "derived3") for gu in guards]
def build_grid_points(smoke):
"""Mode A (PREREG 2026-09-21): the triple chosen by shape, on top of the slope gate that
already reached the project target. Three-point scale only — a triple must exist."""
sims = ["ratio", "sqrt"] if smoke else sorted(PURE_SIMS)
guards = ("credit", "project") if smoke else GUARD_MODES
return [("pure", sim, "derived3", gu, "x", "slope", "shape", "distance")
for sim in sims for gu in guards]
def build_grid_direction(smoke):
"""Mode B (PREREG 2026-09-21): the far end read by dissimilarity, on top of the slope gate."""
sims = ["ratio", "sqrt"] if smoke else sorted(PURE_SIMS)
guards = ("credit", "project") if smoke else GUARD_MODES
return [("pure", sim, sc, gu, "x", "slope", "recent", "dissimilar")
for sim in sims for sc in ("derived", "derived3") for gu in guards]
def build_grid_slope(smoke):
"""The slope-stability gate grid (PREREG 2026-09-21). Only derived scales: there must be
a slope to judge. Baselines are not recomputed — they are in sweep_full.json."""
@@ -231,7 +255,11 @@ def main():
logmode = "--log" in sys.argv # the u-space grid, written to its OWN file
gatemode = "--gate" in sys.argv # the pre-step gate grid, likewise
slopemode = "--slope" in sys.argv # the slope-stability gate grid
specs = (build_grid_slope(smoke) if slopemode else
pointsmode = "--points" in sys.argv # mode A: triple by shape
dirmode = "--direction" in sys.argv # mode B: far end by dissimilarity
specs = (build_grid_points(smoke) if pointsmode else
build_grid_direction(smoke) if dirmode else
build_grid_slope(smoke) if slopemode else
build_grid_gate(smoke) if gatemode else
build_grid_log(smoke) if logmode else build_grid(smoke))
t0 = time.time()
@@ -249,6 +277,14 @@ 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 pointsmode or dirmode:
base = "sweep_points" if pointsmode else "sweep_direction"
out = f"{base}_smoke.json" if smoke else f"{base}.json"
with open(out, "w", encoding="utf8") as fh:
json.dump(results, fh)
print(f"wrote {out} ({time.time() - t0:.0f}s total)", flush=True)
report(results)
return
if slopemode:
out = "sweep_slope_smoke.json" if smoke else "sweep_slope.json"
with open(out, "w", encoding="utf8") as fh:
File diff suppressed because one or more lines are too long
+106
View File
@@ -0,0 +1,106 @@
"""Gates P1-P5 for the two new places similarity is read (PREREG_points_and_direction_2026-09-21).
mode A points="shape" — WHICH three points the model is built on, chosen by the shape
of the configuration (argmin |phi - xi|) instead of by recency
mode B direction="dissimilar" — WHICH end is "far", read as the end whose |f| is larger
(less similar to zero) instead of the geometrically distant one
Inertness is a declared failure of the EXPERIMENT, not a pass: if a mode changes nothing, its
numbers are not evidence either way, so the counters are pinned here.
"""
from __future__ import annotations
import inspect
import math
import re
import pytest
from simroot import pure as P
SMOOTH = [
(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 ** 3 - t + 0.2, -2.0, 2.0),
]
def test_P1_defaults_are_bit_identical_to_the_published_path():
for f, a, b in SMOOTH:
for guard in ("credit", "project", "safe", "step"):
base = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio",
scale="derived3", guard=guard)
same = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio", scale="derived3",
guard=guard, points="recent", direction="distance")
assert base == same, (guard, base, same)
def test_P2_neither_rule_carries_a_constant():
src = inspect.getsource(P._pick_triple) + inspect.getsource(P._shape_defect)
code = "\n".join(line.split("#", 1)[0] for line in src.splitlines())
literals = set(re.findall(r"(?<![\w.])\d+\.\d+(?![\w.])", code))
assert literals == set(), f"constants in the triple rule: {literals}"
body = inspect.getsource(P.pure_root)
start = body.index('if direction == "dissimilar":')
end = body.index('if scale in ("derived", "derived_rho")', start)
dcode = "\n".join(line.split("#", 1)[0] for line in body[start:end].splitlines())
assert set(re.findall(r"(?<![\w.])\d+\.\d+(?![\w.])", dcode)) == set()
def test_P3_the_counters_report_whether_a_mode_is_inert():
changed = 0
for f, a, b in SMOOTH:
st = {}
_x, _n, ok = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio", scale="derived3",
guard="credit", points="shape", stats=st)
assert ok and "triple_changed" in st
changed += st["triple_changed"]
assert changed >= 1, "mode A never changed the triple on any probe problem — inert"
dchanged = 0
for f, a, b in SMOOTH:
st = {}
_x, _n, ok = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio", scale="derived3",
guard="credit", direction="dissimilar", stats=st)
assert ok and "dir_changed" in st
dchanged += st["dir_changed"]
assert dchanged >= 1, "mode B never changed the far end — inert"
def test_P4_degenerate_triples_are_dropped_not_divided_by():
assert P._shape_defect(1.0, 1.0, 2.0, 5.0, 2.0, 7.0) is None # equal abscissae
assert P._shape_defect(1.0, 1.0, 2.0, 5.0, 3.0, 5.0) is None # equal values
d = P._shape_defect(1.0, 1.0, 0.0, 0.0, 2.0, 2.0) # exactly linear
assert d is not None and d == pytest.approx(0.0)
assert P._pick_triple(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0) is None
def test_P5_no_mode_adds_an_evaluation():
for f, a, b in SMOOTH:
st = {}
_x, n, ok = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio", scale="derived3",
guard="credit", points="shape", direction="dissimilar",
stats=st)
assert ok
assert st["triple_changed"] <= n - 2 and st["dir_changed"] <= n - 2, (st, n)
def test_modes_are_validated():
with pytest.raises(ValueError):
P.pure_root(lambda t: t, -1.0, 1.0, 1e-10, 100, points="whatever")
with pytest.raises(ValueError):
P.pure_root(lambda t: t, -1.0, 1.0, 1e-10, 100, direction="whatever")
with pytest.raises(ValueError): # a triple needs the three-point scale
P.pure_root(lambda t: t, -1.0, 1.0, 1e-10, 100, scale="derived", points="shape")
def test_both_modes_still_solve():
for f, a, b in SMOOTH:
for kw in ({"points": "shape"}, {"direction": "dissimilar"},
{"points": "shape", "direction": "dissimilar"}):
x, _n, ok = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio",
scale="derived3", guard="credit", **kw)
assert ok and abs(f(x)) < 1e-6, (kw, a, b, x)