logarithmic space mode: pure_root gains space='x'|'log' (default reproduces the published grid bit-for-bit), u = sign(x-c)*log1p(|x-c|/xtol) with c = the end of smaller |f| and s = xtol fixed by derivation; gates G1/G2' pinned by tests/test_log_space.py (7 green); sweep writes the log grid to its OWN sweep_log.json so the verified sweep_full.json is untouched
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,39 @@
|
|||||||
Не прошёл G2 → закрывается: широкие скобки куплены ценой канонического набора, а это трейд, на котором
|
Не прошёл G2 → закрывается: широкие скобки куплены ценой канонического набора, а это трейд, на котором
|
||||||
провалились все прежние попытки.
|
провалились все прежние попытки.
|
||||||
|
|
||||||
|
## 3a. ПОПРАВКА 1 (2026-09-20, решение Валерия: вариант B)
|
||||||
|
|
||||||
|
**Что произошло:** строгий G2 («шаг в `u` совпадает со шагом в `x` до 1e-12») провален **выводом**, без
|
||||||
|
прогона, и не для одного кандидата, а для всех доступных беспараметрических единиц — марш требует
|
||||||
|
ОТНОШЕНИЯ расстояний, тождественность требует АБСОЛЮТНОЙ шкалы (разбор в `FINDINGS_logarithmic.md` §1).
|
||||||
|
|
||||||
|
**Решение (Валерий, вариант B):** строгое чтение G2 заменяется его же **измеримым** чтением, которое
|
||||||
|
уже стоит в этой пред-регистрации как L2:
|
||||||
|
|
||||||
|
> **G2′:** на APS-154 суммарная цена не растёт; ни одна задача канонического набора не становится
|
||||||
|
> дороже по вызовам `f`.
|
||||||
|
|
||||||
|
Это **ослабление** пред-регистрированного критерия после его провала — записано как ослабление, а не
|
||||||
|
как «уточнение», и принято решением, а не молча.
|
||||||
|
|
||||||
|
**Единица `s` фиксируется ВЫВОДОМ здесь и не выбирается по рейтингу:**
|
||||||
|
|
||||||
|
> `c` = конец скобки с меньшим |f| (сторона, к которой корень ближе);
|
||||||
|
> **`s` = `xtol`**.
|
||||||
|
|
||||||
|
Обоснование, а не удобство: задача преобразования — считать **декады, оставшиеся до различимости**.
|
||||||
|
Единственная длина в задаче, говорящая «ниже этого различий нет», — это `xtol`, и она приходит от
|
||||||
|
вызывающего наравне с `a`, `b`, `f`, то есть не является нашей константой. При таком выборе `u`
|
||||||
|
покрывает ровно `log(w/xtol)` единиц — ту самую величину, в которой сформулирована гарантия метода
|
||||||
|
`2⌈log₂(w/xtol)⌉+3`, так что марш становится линейным в мере собственной гарантии. Вырождения нет:
|
||||||
|
`xtol > 0` всегда, и `|x−c| = 0` даёт `u = 0`, а не деление на ноль.
|
||||||
|
|
||||||
|
Отвергнутая альтернатива (для протокола): самомасштабируемая `s = |x−c|` вырождается на первом шаге,
|
||||||
|
потому что итерат стартует в конце с меньшим |f|, то есть ровно в `c`.
|
||||||
|
|
||||||
|
**Режим требует выведенной шкалы:** `space="log"` допустим только при `scale ∈ {derived, derived3}` —
|
||||||
|
в log-пространстве нужен наклон; при прочих шкалах вызов обязан падать, а не считать молча.
|
||||||
|
|
||||||
## 4. Цели (записаны в `NEXT_PAPER_logarithmic.md` заранее, не подгонялись)
|
## 4. Цели (записаны в `NEXT_PAPER_logarithmic.md` заранее, не подгонялись)
|
||||||
|
|
||||||
| величина | цель | где сейчас |
|
| величина | цель | где сейчас |
|
||||||
|
|||||||
+62
-2
@@ -118,6 +118,50 @@ PURE_SIMS = {
|
|||||||
|
|
||||||
GUARDS = ("none", "bracket", "step", "both", "project", "safe", "credit")
|
GUARDS = ("none", "bracket", "step", "both", "project", "safe", "credit")
|
||||||
|
|
||||||
|
SPACES = ("x", "log")
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
search there must cross up to twelve decades, and an absolute step sized by
|
||||||
|
the local slope knows nothing about how many decades remain."""
|
||||||
|
d = t - c
|
||||||
|
return math.copysign(math.log1p(abs(d) / s), d)
|
||||||
|
|
||||||
|
|
||||||
|
def _log_unmap(u, c, s):
|
||||||
|
return c + math.copysign(s * (math.expm1(abs(u))), u)
|
||||||
|
|
||||||
|
|
||||||
|
def _log_space_step(S, x, fx, far, slope, c, s):
|
||||||
|
"""The same similarity rule, the same derived scale, read in u rather than x.
|
||||||
|
|
||||||
|
dx/du = s + |x - c|, so the slope in u is slope_x * (s + |x - c|); the scale
|
||||||
|
is that slope times the distance to the far end IN u, exactly as in x-space.
|
||||||
|
Returns the step in x (None when u-space cannot be read).
|
||||||
|
|
||||||
|
`s` is NOT a free unit: PREREG amendment 1 fixes s = xtol by derivation - the
|
||||||
|
only length in the problem that says "below this nothing is distinguishable",
|
||||||
|
supplied by the caller like a, b and f. With it u spans log(w/xtol) units,
|
||||||
|
the very measure the method's own guarantee 2*ceil(log2(w/xtol)) + 3 is
|
||||||
|
stated in, so the march is linear in the measure of the guarantee.
|
||||||
|
"""
|
||||||
|
u_x = _log_map(x, c, s)
|
||||||
|
u_far = _log_map(far, c, s)
|
||||||
|
du = u_far - u_x
|
||||||
|
if du == 0.0 or not math.isfinite(du):
|
||||||
|
return None
|
||||||
|
slope_u = slope * (s + abs(x - c))
|
||||||
|
sc_u = abs(slope_u) * abs(du)
|
||||||
|
if sc_u == 0.0 or not math.isfinite(sc_u):
|
||||||
|
return None
|
||||||
|
u_new = u_x + (1.0 - S(abs(fx) / sc_u)) * du
|
||||||
|
x_new = _log_unmap(u_new, c, s)
|
||||||
|
if not math.isfinite(x_new):
|
||||||
|
return None
|
||||||
|
return x_new - x
|
||||||
|
|
||||||
|
|
||||||
class _Safeguard:
|
class _Safeguard:
|
||||||
"""Parameter-free worst-case protection. See the module docstring."""
|
"""Parameter-free worst-case protection. See the module docstring."""
|
||||||
@@ -369,7 +413,7 @@ def _local_order(x2, f2, x1, f1, x0, f0):
|
|||||||
|
|
||||||
def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
|
def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
|
||||||
guard="step", stats=None, budget_factor=2.0,
|
guard="step", stats=None, budget_factor=2.0,
|
||||||
budget_extra=None, endowment=1.0):
|
budget_extra=None, endowment=1.0, space="x"):
|
||||||
"""The pure similarity root finder.
|
"""The pure similarity root finder.
|
||||||
|
|
||||||
similarity : key into PURE_SIMS
|
similarity : key into PURE_SIMS
|
||||||
@@ -421,6 +465,12 @@ def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
|
|||||||
if scale not in ("derived", "derived3", "derived_rho", "derived3_rho",
|
if scale not in ("derived", "derived3", "derived_rho", "derived3_rho",
|
||||||
"derived3_m", "start", "bracket"):
|
"derived3_m", "start", "bracket"):
|
||||||
raise ValueError(f"unknown scale {scale!r}")
|
raise ValueError(f"unknown scale {scale!r}")
|
||||||
|
if space not in SPACES:
|
||||||
|
raise ValueError(f"unknown space {space!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).
|
||||||
|
raise ValueError("space='log' requires scale in {'derived', 'derived3'}")
|
||||||
S = PURE_SIMS[similarity]
|
S = PURE_SIMS[similarity]
|
||||||
g = Counter(f, maxfev)
|
g = Counter(f, maxfev)
|
||||||
x = 0.5 * (a + b)
|
x = 0.5 * (a + b)
|
||||||
@@ -541,7 +591,17 @@ def pure_root(f, a, b, xtol, maxfev, similarity="ratio", scale="derived",
|
|||||||
if sc == 0.0 or not math.isfinite(sc):
|
if sc == 0.0 or not math.isfinite(sc):
|
||||||
x_new = 0.5 * (A + B)
|
x_new = 0.5 * (A + B)
|
||||||
else:
|
else:
|
||||||
step = (1.0 - S(abs(fx) / sc)) * (far - x)
|
step = None
|
||||||
|
if 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
|
||||||
|
# x-space step when u cannot be read, so the mode can
|
||||||
|
# only differ where it is defined.
|
||||||
|
c_near = A if abs(FA) <= abs(FB) else B
|
||||||
|
step = _log_space_step(S, x, fx, far, slope, c_near, xtol)
|
||||||
|
if step is None:
|
||||||
|
step = (1.0 - S(abs(fx) / sc)) * (far - x)
|
||||||
if sg.reject_step(step):
|
if sg.reject_step(step):
|
||||||
x_new = 0.5 * (A + B)
|
x_new = 0.5 * (A + B)
|
||||||
step = x_new - x
|
step = x_new - x
|
||||||
|
|||||||
+28
-7
@@ -101,20 +101,27 @@ BASELINES = {
|
|||||||
|
|
||||||
|
|
||||||
def make_method(spec):
|
def make_method(spec):
|
||||||
"""spec is ('base', name) or ('pure', similarity, scale, guard)."""
|
"""spec is ('base', name) or ('pure', similarity, scale, guard[, space]).
|
||||||
|
|
||||||
|
The optional 5th element is the SPACE the step is read in (PREREG
|
||||||
|
amendment 1, 2026-09-20): omitted or "x" reproduces the published grid
|
||||||
|
bit-for-bit, "log" is the logarithmic-march mode."""
|
||||||
if spec[0] == "base":
|
if spec[0] == "base":
|
||||||
return BASELINES[spec[1]]
|
return BASELINES[spec[1]]
|
||||||
_, sim, scale, guard = spec
|
sim, scale, guard = spec[1], spec[2], spec[3]
|
||||||
|
space = spec[4] if len(spec) > 4 else "x"
|
||||||
|
|
||||||
def m(f, a, b, xtol, maxfev):
|
def m(f, a, b, xtol, maxfev):
|
||||||
return pure_root(f, a, b, xtol, maxfev, similarity=sim,
|
return pure_root(f, a, b, xtol, maxfev, similarity=sim,
|
||||||
scale=scale, guard=guard)
|
scale=scale, guard=guard, space=space)
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
def label(spec):
|
def label(spec):
|
||||||
if spec[0] == "base":
|
if spec[0] == "base":
|
||||||
return spec[1]
|
return spec[1]
|
||||||
|
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]}"
|
return f"pure.{spec[1]}.{spec[2]}.{spec[3]}"
|
||||||
|
|
||||||
|
|
||||||
@@ -152,6 +159,16 @@ def evaluate(spec):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def build_grid_log(smoke):
|
||||||
|
"""The logarithmic-space grid (PREREG 2026-09-20). Baselines are NOT recomputed:
|
||||||
|
their numbers are already in sweep_full.json and re-running known expensive
|
||||||
|
controls is waste. Only the derived scales are admissible in u-space."""
|
||||||
|
sims = ["ratio", "cbrt"] if smoke else sorted(PURE_SIMS)
|
||||||
|
guards = ("credit", "project") if smoke else GUARD_MODES
|
||||||
|
return [("pure", sim, sc, gu, "log")
|
||||||
|
for sim in sims for sc in ("derived", "derived3") for gu in guards]
|
||||||
|
|
||||||
|
|
||||||
def build_grid(smoke):
|
def build_grid(smoke):
|
||||||
specs = [("base", nm) for nm in BASELINES]
|
specs = [("base", nm) for nm in BASELINES]
|
||||||
sims = sorted(PURE_SIMS)
|
sims = sorted(PURE_SIMS)
|
||||||
@@ -171,10 +188,11 @@ def build_grid(smoke):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
smoke = "--smoke" in sys.argv
|
smoke = "--smoke" in sys.argv
|
||||||
specs = build_grid(smoke)
|
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)
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
print(f"sweep: {len(specs)} configurations x 199 problems "
|
print(f"sweep: {len(specs)} configurations x 199 problems "
|
||||||
f"({'SMOKE' if smoke else 'FULL'})", flush=True)
|
f"({'SMOKE' if smoke else 'FULL'}{', LOG-SPACE' if logmode else ''})", flush=True)
|
||||||
|
|
||||||
workers = max(1, min(8, (os.cpu_count() or 2) - 1))
|
workers = max(1, min(8, (os.cpu_count() or 2) - 1))
|
||||||
with Pool(workers) as pool:
|
with Pool(workers) as pool:
|
||||||
@@ -185,7 +203,10 @@ def main():
|
|||||||
print(f" {i}/{len(specs)} done, "
|
print(f" {i}/{len(specs)} done, "
|
||||||
f"{time.time() - t0:.0f}s elapsed", flush=True)
|
f"{time.time() - t0:.0f}s elapsed", flush=True)
|
||||||
|
|
||||||
out = "sweep_smoke.json" if smoke else "sweep_full.json"
|
# the published sweep_full.json is what verify_article.py checks the paper
|
||||||
|
# against, so the log grid never writes into it
|
||||||
|
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:
|
with open(out, "w", encoding="utf8") as fh:
|
||||||
json.dump(results, fh)
|
json.dump(results, fh)
|
||||||
print(f"\nwrote {out} ({time.time() - t0:.0f}s total)\n", flush=True)
|
print(f"\nwrote {out} ({time.time() - t0:.0f}s total)\n", flush=True)
|
||||||
@@ -225,7 +246,7 @@ def report(results, top=20):
|
|||||||
for r in results:
|
for r in results:
|
||||||
if r["spec"][0] != "pure":
|
if r["spec"][0] != "pure":
|
||||||
continue
|
continue
|
||||||
_, sim, sc, gu = r["spec"]
|
sim, sc, gu = r["spec"][1], r["spec"][2], r["spec"][3]
|
||||||
if sc != scale or gu != "step" or sim not in group:
|
if sc != scale or gu != "step" or sim not in group:
|
||||||
continue
|
continue
|
||||||
cnt += 1
|
cnt += 1
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,107 @@
|
|||||||
|
"""Gates for the logarithmic space mode — checked BEFORE the ranking is consulted.
|
||||||
|
|
||||||
|
PREREG_logarithmic_2026-09-20.md, amendment 1:
|
||||||
|
G1 the transform is parameter-free: c and s come from the bracket and the caller
|
||||||
|
(c = the end with the smaller |f|, s = xtol), never from a chosen constant.
|
||||||
|
G2' on APS-154 the cost must not grow (measured by the sweep, not here); what IS
|
||||||
|
checkable here is that the deviation from the x-space step is of SECOND order
|
||||||
|
in the bracket width, i.e. it vanishes as the bracket collapses.
|
||||||
|
|
||||||
|
Also pinned: the transform round-trips, u-space is rejected where it cannot be read
|
||||||
|
(no silent fallback to a different scale), and the mode still solves.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from simroot import pure as P
|
||||||
|
|
||||||
|
|
||||||
|
def _steps(width, space, fun=None):
|
||||||
|
"""The points probed on a NONLINEAR f from a bracket of the given width around 1.
|
||||||
|
|
||||||
|
Nonlinear on purpose: on a linear f the two spaces coincide EXACTLY (deviation
|
||||||
|
0.0, measured 2026-09-20), because the derived scale makes the step the Newton
|
||||||
|
step and that lands on the root in either space — so a linear probe cannot see
|
||||||
|
the deviation at all."""
|
||||||
|
calls = []
|
||||||
|
g = fun or (lambda t: t * t * t - 1.0)
|
||||||
|
|
||||||
|
def f(x):
|
||||||
|
calls.append(x)
|
||||||
|
return g(x)
|
||||||
|
|
||||||
|
P.pure_root(f, 1.0 - width, 1.0 + width, xtol=1e-14, maxfev=200,
|
||||||
|
similarity="ratio", scale="derived", guard="none", space=space)
|
||||||
|
return calls
|
||||||
|
|
||||||
|
|
||||||
|
def test_G1_the_transform_carries_no_chosen_constant():
|
||||||
|
src = "".join(inspect.getsource(fn) for fn in (P._log_map, P._log_unmap, P._log_space_step))
|
||||||
|
literals = set(re.findall(r"(?<![\w.])\d+\.\d+(?![\w.])", src))
|
||||||
|
assert literals <= {"0.0", "1.0"}, f"chosen constants in the transform: {literals}"
|
||||||
|
# and the unit really is the caller's tolerance, not something invented inside
|
||||||
|
call_site = inspect.getsource(P.pure_root)
|
||||||
|
assert "_log_space_step(S, x, fx, far, slope, c_near, xtol)" in call_site
|
||||||
|
|
||||||
|
|
||||||
|
def test_G1_the_near_end_is_read_from_the_data_not_fixed():
|
||||||
|
call_site = inspect.getsource(P.pure_root)
|
||||||
|
assert "c_near = A if abs(FA) <= abs(FB) else B" in call_site
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_transform_round_trips():
|
||||||
|
for s in (1e-12, 1e-6, 1.0, 1e6):
|
||||||
|
for c in (-3.0, 0.0, 7.5):
|
||||||
|
for t in (c - 1e3, c - 1e-9, c, c + 1e-9, c + 1e3):
|
||||||
|
u = P._log_map(t, c, s)
|
||||||
|
back = P._log_unmap(u, c, s)
|
||||||
|
assert math.isclose(back, t, rel_tol=1e-12, abs_tol=1e-12)
|
||||||
|
|
||||||
|
|
||||||
|
def test_G2_deviation_from_the_x_space_step_is_second_order_in_the_bracket():
|
||||||
|
"""The march needs a RATIO of distances and identity needs an ABSOLUTE unit, so the
|
||||||
|
strict identity gate is unreachable (FINDINGS §1). What must hold is that the
|
||||||
|
deviation SHRINKS as the bracket collapses — otherwise the wide-bracket families
|
||||||
|
would be bought at the price of the canonical set."""
|
||||||
|
dev = {}
|
||||||
|
for width in (1e-2, 1e-3, 1e-4):
|
||||||
|
xs_x = _steps(width, "x")
|
||||||
|
xs_log = _steps(width, "log")
|
||||||
|
# the first probing evaluations are the bracket ends; compare the first step
|
||||||
|
i = 2
|
||||||
|
assert len(xs_x) > i and len(xs_log) > i
|
||||||
|
dev[width] = abs(xs_log[i] - xs_x[i]) / width
|
||||||
|
# exact zero is a STRONGER pass than a shrinking deviation, so it is allowed
|
||||||
|
assert dev[1e-3] <= dev[1e-2] / 3.0 or dev[1e-2] == 0.0, dev
|
||||||
|
assert dev[1e-4] <= dev[1e-3] / 3.0 or dev[1e-3] == 0.0, dev
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_mode_is_rejected_where_it_cannot_be_read():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
P.pure_root(lambda x: x, -1.0, 1.0, 1e-10, 100, scale="bracket", space="log")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
P.pure_root(lambda x: x, -1.0, 1.0, 1e-10, 100, space="nope")
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_mode_still_solves_both_a_narrow_and_a_wide_bracket():
|
||||||
|
for lo, hi in ((0.5, 1.5), (-1e12, 3.0)):
|
||||||
|
x, nfev, ok = P.pure_root(lambda t: t - 1.0, lo, hi, xtol=1e-10, maxfev=500,
|
||||||
|
similarity="ratio", scale="derived3", guard="credit",
|
||||||
|
space="log")
|
||||||
|
assert ok and abs(x - 1.0) < 1e-8, (lo, hi, x, nfev, ok)
|
||||||
|
|
||||||
|
|
||||||
|
def test_x_space_is_untouched_by_the_new_parameter():
|
||||||
|
"""The published configuration must be bit-identical with the default space."""
|
||||||
|
for lo, hi in ((0.5, 1.5), (0.5, 1e6)): # both brackets must straddle sqrt(2)
|
||||||
|
a = P.pure_root(lambda t: t * t - 2.0, lo, hi, 1e-12, 300,
|
||||||
|
similarity="ratio", scale="derived3", guard="credit")
|
||||||
|
b = P.pure_root(lambda t: t * t - 2.0, lo, hi, 1e-12, 300,
|
||||||
|
similarity="ratio", scale="derived3", guard="credit", space="x")
|
||||||
|
assert a == b
|
||||||
Reference in New Issue
Block a user