From 47361dabea596ec3ff3488b022631ac3ba019bcb Mon Sep 17 00:00:00 2001 From: Valeri Sitnikov Date: Mon, 21 Sep 2026 19:30:27 +0100 Subject: [PATCH] PREREG before any run: out-of-sample corpus and ablation for the two strongest variants. corpus_new.py (seed 20260921): SciPy 'original' (6) + five generated classes x 40 (smooth simple, transcendental simple, multiple/flat |x-r|^p p~U(2,20), exponential flank, near-cancelling terms), built and validity-checked only (no method has seen it). O1-O3 = transfers (total below Chandrupatla, per-problem sign test p<0.05, guarantee); O4 vs the plain rule; per class. Leave-one-out ablation of every component on APS, CHA and the new corpus. Co-Authored-By: Claude Opus 5 --- PREREG_out_of_sample_2026-09-21.md | 51 ++++++++++++ corpus_new.py | 124 +++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 PREREG_out_of_sample_2026-09-21.md create mode 100644 corpus_new.py diff --git a/PREREG_out_of_sample_2026-09-21.md b/PREREG_out_of_sample_2026-09-21.md new file mode 100644 index 0000000..7be6a3d --- /dev/null +++ b/PREREG_out_of_sample_2026-09-21.md @@ -0,0 +1,51 @@ +# PREREG — проверка двух сильнейших вариантов: абляция и новый корпус + +Составлено до прогона, ветка `signfix-and-gates`. Валерий: «результат сильный и уже достоин +статьи»; «давай проверим оба сильнейших варианта». + +## Зачем + +Все решения сегодня принимались на тех же 199 задачах (APS 154 + Chandrupatla 45). Предрегистрация +защищает каждый шаг, но не цепочку: итоговые ячейки отобраны по этим наборам. Нужны (1) абляция +каждой составляющей и (2) задачи, на которых ничего не разрабатывалось. + +## Варианты (проекция, tent, трёхточечный масштаб) + +- **A** = Г2 + `contra+flatanalogmempt+exp+half` (APS 2559, CHA 2047) +- **B** = `defect+flatanalogmempt+exp`, без Г2 (APS 2531, CHA 1972) +- справочно: предложенный метод без добавок; соперники — библиотечные Чандрупатла + (`elementwise.find_root`), Брент (`brentq`), TOMS748. + +## Часть 1 — новый корпус (`corpus_new.py`, зерно 20260921, закоммичен до прогона) + +206 задач, не использованных в разработке: C0 — набор SciPy `original` (6); C1–C5 по 40, +параметры из заранее заданных распределений: C1 гладкий простой корень, C2 трансцендентный простой +(atan/tanh/expm1/sinh), C3 кратный/плоский корень |x−r|^p, p ~ U(2, 20), C4 экспоненциальный склон, +C5 почти сокращающиеся члены (как у fun9, параметры случайные). Корень известен по построению. +Учёт тот же: XTOL 10⁻¹², MAXFEV 200, решено при |x − r| ≤ 10⁻⁶, неудача = 200. + +Для каждого варианта V ∈ {A, B}: + +| # | ПРОЙДЕНО, если | +|---|---| +| **O1** | сумма вызовов V на корпусе меньше, чем у Чандрупатлы | +| **O2** | по задачам против Чандрупатлы побед больше, чем поражений, знаковый тест p < 0.05 | +| **O3** | гарантия: 0 нарушений (≤ 2⌈log₂(w/xtol)⌉ + 4 на решённых); решено не меньше, чем у Чандрупатлы | +| **O4** | сумма V меньше, чем у предложенного метода без добавок (добавки переносятся) | + +**Вывод «переносится на новые задачи»** — только при O1, O2, O3 одновременно. Сообщается всё, по +каждому классу отдельно, против всех трёх соперников. **Никаких изменений метода после этого +прогона в рамках этой проверки**; корпус и критерии не меняются. + +Классы C3–C5 близки к тому, на чём мы искали (плоские, экспонента, сокращение); C0–C2 — нейтральны +и выгодны модели Чандрупатлы. Вывод сообщается и отдельно по C0–C2. + +## Часть 2 — абляция (APS, CHA и новый корпус) + +Снимается по одной составляющей, Δ = (без неё) − (с ней), плюс = составляющая заслужена: + +- A: Г2; `contra`; `half`; `exp`; точка (`flatanalogmempt` → `flatanalogmem`); весь плоский режим + (снять `flatanalogmempt` и `exp`). +- B: `defect`; `exp`; точка; весь плоский режим. + +Составляющая с Δ ≤ 0 на всех трёх наборах **не заслужена** и из предложения убирается. diff --git a/corpus_new.py b/corpus_new.py new file mode 100644 index 0000000..fd5908a --- /dev/null +++ b/corpus_new.py @@ -0,0 +1,124 @@ +"""An OUT-OF-SAMPLE corpus for PREREG_out_of_sample_2026-09-21: problems no method variant +was developed on. Fixed before any method is run on it; seed and distributions below are +part of the pre-registration and are never changed afterwards. + +Six classes. C0 is SciPy's 'original' collection (6 problems, never used here before). +C1-C5 are generated, 40 problems each, from one numpy Generator(seed=20260921): + + C1 smooth simple root f = (x - r) * g(x), g = exp(a sin(c x)) + b x^2 > 0 + C2 transcendental simple f = one of atan(k(x-r)), tanh(k(x-r)), expm1(k(x-r)), sinh(k(x-r)) + C3 multiple / flat root f = sign(x-r) |x-r|^p * g(x), p ~ U(2, 20) (real, not only odd) + C4 exponential flank f = exp(k x) - exp(k r) (steep one-sided growth) + C5 near-cancelling terms f = exp(x) - e^r + a (1/x^2 - 1/r^2) - b (1/x^3 - 1/r^3), x > 0 + (the kind of end that misled us on fun9, with random parameters) + +Brackets: root r ~ U(-1, 1) (C5: r ~ U(0.3, 2)); width L log-uniform on [0.1, 1000] (C4: +capped so that k * right end <= 600, C5: left end >= r/1000); the root sits at a fraction +u ~ U(0.02, 0.98) of the bracket. Every problem is checked for finite values of opposite +sign at the ends; one that fails is redrawn (the redraw count is reported). +""" +from __future__ import annotations + +import math + +import numpy as np + +SEED = 20260921 +PER_CLASS = 40 + + +def _bracket(rng, r, lo_w=0.1, hi_w=1000.0): + L = math.exp(rng.uniform(math.log(lo_w), math.log(hi_w))) + u = rng.uniform(0.02, 0.98) + return r - u * L, r + (1.0 - u) * L + + +def _ok(f, a, b): + try: + fa, fb = f(a), f(b) + except (OverflowError, ValueError, ZeroDivisionError): + return False + return (math.isfinite(fa) and math.isfinite(fb) and fa != 0.0 and fb != 0.0 + and (fa < 0.0) != (fb < 0.0)) + + +def _c1(rng): + r = rng.uniform(-1, 1) + a, b, c = rng.uniform(0, 1), rng.uniform(0, 0.5), rng.uniform(0.5, 5) + f = lambda x, r=r, a=a, b=b, c=c: (x - r) * (math.exp(a * math.sin(c * x)) + b * x * x) + return f, r, _bracket(rng, r) + + +def _c2(rng): + r = rng.uniform(-1, 1) + k = math.exp(rng.uniform(math.log(0.2), math.log(20))) + kind = int(rng.integers(4)) + base = (math.atan, math.tanh, math.expm1, math.sinh)[kind] + a, b = _bracket(rng, r) + if kind in (2, 3): # keep exp/sinh finite on the bracket + span = max(abs(a - r), abs(b - r)) + if k * span > 600: + k = 600 / span + return (lambda x, r=r, k=k, g=base: g(k * (x - r))), r, (a, b) + + +def _c3(rng): + r = rng.uniform(-1, 1) + p = rng.uniform(2, 20) + a, c = rng.uniform(0, 1), rng.uniform(0.5, 5) + lo, hi = _bracket(rng, r, hi_w=30.0) # |x - r|^20 stays finite + f = lambda x, r=r, p=p, a=a, c=c: math.copysign(abs(x - r) ** p, x - r) * math.exp(a * math.sin(c * x)) + return f, r, (lo, hi) + + +def _c4(rng): + r = rng.uniform(-1, 1) + k = math.exp(rng.uniform(math.log(0.5), math.log(5))) + lo, hi = _bracket(rng, r) + if k * hi > 600: + hi = 600 / k + er = math.exp(k * r) + return (lambda x, k=k, er=er: math.exp(k * x) - er), r, (lo, hi) + + +def _c5(rng): + r = rng.uniform(0.3, 2.0) + a = math.exp(rng.uniform(math.log(1e-3), math.log(1e-1))) + b = a * rng.uniform(0.01, 0.5) * r + L = math.exp(rng.uniform(math.log(0.1), math.log(100))) + u = rng.uniform(0.02, 0.98) + lo, hi = max(r - u * L, r / 1000.0), r + (1.0 - u) * L + hi = min(hi, 600.0) + er = math.exp(r) + f = lambda x, r=r, a=a, b=b, er=er: (math.exp(x) - er + a * (1 / x ** 2 - 1 / r ** 2) + - b * (1 / x ** 3 - 1 / r ** 3)) + return f, r, (lo, hi) + + +def build(): + """[(class, id, f, a, b, root)] and the number of redraws per class.""" + from scipy.optimize._tstutils import _ORIGINAL_TESTS_DICTS + out, redraws = [], {} + for i, d in enumerate(_ORIGINAL_TESTS_DICTS): + a, b = d["bracket"] + args = d.get("args", ()) + f = (lambda ff, aa: lambda x: ff(x, *aa))(d["f"], args) + out.append(("C0", f"C0.{i:02d}", f, float(a), float(b), float(d["root"]))) + rng = np.random.default_rng(SEED) + for cls, gen in (("C1", _c1), ("C2", _c2), ("C3", _c3), ("C4", _c4), ("C5", _c5)): + n = red = 0 + while n < PER_CLASS: + f, r, (a, b) = gen(rng) + if _ok(f, a, b) and min(a, b) < r < max(a, b): + out.append((cls, f"{cls}.{n:02d}", f, float(a), float(b), float(r))) + n += 1 + else: + red += 1 + redraws[cls] = red + return out, redraws + + +if __name__ == "__main__": + probs, red = build() + from collections import Counter + print("problems per class:", dict(Counter(p[0] for p in probs)), " redraws:", red)