diff --git a/PREREG_empty_step_2026-09-21.md b/PREREG_empty_step_2026-09-21.md new file mode 100644 index 0000000..e4c1b0f --- /dev/null +++ b/PREREG_empty_step_2026-09-21.md @@ -0,0 +1,42 @@ +# PREREG — признак пустого шага и признак повтора рисунка (проверка различимости) + +Составлено до прогона. Основание: разбор `diag_contraction_sequence.py` (коммит `0ec7142`) показал, +что на плоских функциях xᵐ метод ходит циклом из 4 вычислений — два недолёта ρ = (1−1/m)^m, +перескок, **почти пустое вычисление ρ ≈ 1**. Похожесть соседних ρ режим не распознала (AUC 0.28–0.42). +Валерий: «пред-регистрируй и проверь признак пустого шага; длину периода всегда можно выбрать из +данных». + +⚠️ Оба признака подсказаны картинкой, которую я уже видел. Поэтому здесь — только **проверка +различимости** на записанных траекториях, без правила в методе. Правило строится, только если +проверка пройдена, и под отдельной пред-регистрацией. + +## Признаки (ρₙ = |fₙ / fₙ₋₁|, трасса предложенного метода и чтения (3)) + +**E — пустой шаг.** Похожесть нового значения на старое, форма отношения SIM: +`S_E = min(|fₙ|, |fₙ₋₁|) / max(|fₙ|, |fₙ₋₁|)`. Пустой шаг: S_E близко к 1. +Правило без выбранного порога: **S_E > ½** — шаг убрал меньше половины |f|, то есть проиграл +тому, что даёт деление пополам на простом корне. Число ½ — единственная константа метода +(определение деления), а не выбор. + +**R — повтор рисунка.** Период p выбирается **из данных** в каждом состоянии: среди лагов +2 … ⌊k/2⌋ (k — сколько ρ уже есть в истории) берётся тот, при котором средняя похожесть +`min(ρⱼ, ρⱼ₋ₚ)/max(ρⱼ, ρⱼ₋ₚ)` по доступной истории максимальна. Признак — эта максимальная +похожесть S_R. Правило: **S_R > ½**, та же константа. Верхняя граница лагов задаётся длиной истории +(нужно минимум два повтора), а не выбирается. + +## Критерии (объявлены до прогона) + +| # | что меряется | ПРОЙДЕНО, если | +|---|---|---| +| E1 | AUC S_E: плоские против APS, fun1, прочих CHA | > 0.5 против **всех трёх** групп | +| E2 | доля срабатываний S_E > ½ | на плоских выше, чем на APS **и** на fun1 | +| R1 | AUC S_R, те же три сравнения | > 0.5 против всех трёх | +| R2 | доля срабатываний S_R > ½ | на плоских выше, чем на APS и на fun1 | + +Планки для сравнения: `m ≥ 2` — 0.2% на APS, 5.8% на плоских; похожесть соседних ρ — AUC 0.28–0.42. + +**Фальсификатор:** если E1 или E2 ложны — признак пустого шага режим не различает, правило на нём не +строим. То же для R. + +**Что не заявляется даже при успехе:** что правило окупится. Срабатывание на APS — это лишние +деления пополам там, где метод выигрывает; цену покажет только отдельный прогон правила в методе. diff --git a/diag_empty_step.py b/diag_empty_step.py new file mode 100644 index 0000000..f8c2a60 --- /dev/null +++ b/diag_empty_step.py @@ -0,0 +1,90 @@ +"""Detectability check for PREREG_empty_step_2026-09-21.md: empty step (E) and repeating +pattern with the period chosen from the data (R). Method untouched - states come from the +behaviour-neutral trace hook. + + python -u diag_empty_step.py +""" +from __future__ import annotations + +from scipy.optimize._tstutils import _APS_TESTS_DICTS, _CHANDRUPATLA_TESTS_DICTS + +from diag_contraction_similarity import CONFIGS, FLAT, auc +from simroot.pure import pure_root + +XTOL, MAXFEV, ACC = 1e-12, 200, 1e-6 + + +def ratio_sim(a, b): + hi, lo = max(a, b), min(a, b) + return lo / hi if hi > 0.0 else 1.0 + + +def features(f, a, b, r, cfg): + """(S_E, S_R) at every traced state; S_R is None until two repeats fit the history.""" + st = {"trace": []} + x, _n, conv = pure_root(f, a, b, XTOL, MAXFEV, stats=st, **cfg) + if not (conv and abs(float(x) - r) <= ACC): + return [] + out, rhos = [], [] + for s in st["trace"]: + if s["xp2"] is None or s["fx"] == 0.0 or s["fp"] == 0.0: + continue + rho = abs(s["fx"] / s["fp"]) + if not (rho == rho) or rho == float("inf"): + continue + rhos.append(rho) + s_e = ratio_sim(abs(s["fx"]), abs(s["fp"])) + s_r = None + k = len(rhos) + best = None + for p in range(2, k // 2 + 1): # at least two repeats must fit the history + sims = [ratio_sim(rhos[j], rhos[j - p]) for j in range(p, k)] + v = sum(sims) / len(sims) + if best is None or v > best: + best = v + s_r = best + out.append((s_e, s_r)) + return out + + +def main(): + out = {} + for cname, cfg in CONFIGS.items(): + groups = {"APS": [], "CHA fun1": [], "CHA flat fun3-6": [], "CHA other": []} + 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"] += features(f, a, b, float(d["root"]), cfg) + for d in _CHANDRUPATLA_TESTS_DICTS: + a, b = (float(v) for v in d["bracket"]) + fam = str(d["ID"]).split(".")[0] + g = ("CHA flat fun3-6" if fam in FLAT else "CHA fun1" if fam == "fun1" + else "CHA other") + groups[g] += features(d["f"], a, b, float(d["root"]), cfg) + print(f"\n== {cname}") + print(f" {'group':<18} {'states':>7} {'E: S_E > 1/2':>13} {'R: S_R > 1/2':>13}" + f" {'(R defined)':>12}") + res = {"rates": {}, "auc": {}} + for g, v in groups.items(): + n = max(len(v), 1) + vr = [x[1] for x in v if x[1] is not None] + e_rate = 100.0 * sum(x[0] > 0.5 for x in v) / n + r_rate = 100.0 * sum(y > 0.5 for y in vr) / max(len(vr), 1) + res["rates"][g] = (len(v), e_rate, len(vr), r_rate) + print(f" {g:<18} {len(v):7d} {e_rate:12.1f}% {r_rate:12.1f}% {len(vr):12d}") + flat = groups["CHA flat fun3-6"] + print(" AUC, flat states against: E R") + for g in ("APS", "CHA fun1", "CHA other"): + ae = auc([x[0] for x in flat], [x[0] for x in groups[g]]) + ar = auc([x[1] for x in flat if x[1] is not None], + [x[1] for x in groups[g] if x[1] is not None]) + res["auc"][g] = (ae, ar) + print(f" {g:<30} {ae:.3f} {ar:.3f}") + out[cname] = res + print("\nbars: m>=2 fires 0.2% canonical / 5.8% flat; successive-ratio similarity AUC" + " 0.28-0.42") + return out + + +if __name__ == "__main__": + main()