diff --git a/handoff/NOTES_LEVEL.md b/handoff/NOTES_LEVEL.md index 2038445..4f9bfed 100644 --- a/handoff/NOTES_LEVEL.md +++ b/handoff/NOTES_LEVEL.md @@ -2149,3 +2149,50 @@ up=exp(-2π·κ·c·g·tau), down=1-up. Константы из дампа: DAT_ 2. comb 2.57 при живом faithful — разобраться после детектора. 3. RT_DUMP_BIN-трасса живого плагина на dual-конфиге (покадрово mask/lvl) — прямой ответ на вопрос «какой lvl реально у тона 2000». + +## ============ UPDATE 2026-08-23e (22x): ТЕОРЕМА О ОТСУТСТВИИ α + ТАБЛИЦА DUAL ПО q ============ + +Инструмент: scripts/resalpha.py — RT_DUMP_BIN-тракты всех уникальных рендеров +(57 пар case/freq: lvl_a1, res, am на бине тона + реальные cut из рефов), +CSV /tmp/opencode/resalpha_pairs.csv. + +### Теорема об отсутствии α +Модель lvl = am/res^α с ЕДИНЫМ законом cut=L(log2 lvl): имплицитные наклоны +семей (концевые точки, дБ/октава lvl) при НИКАКОМ α ∈ [−0.6..1] не совпадают: +- dual требует α≈−0.5 (наклон +0.2..+0.4); +- при α≤0 t1k/t1kq/res получают БЕСКОНЕЧНЫЙ/отрицательный наклон (весь fc-скан + схлопывается в точку по x — res перестаёт различать fc); +- при α>0 dual-пара даёт отрицательный наклон (запрещено для монотонного закона). +⇒ ВЕСЬ класс точечных моделей lvl=f(am, res^α) с общим законом ОТВЕРГНУТ. +Глобальный LSQ по α плоский (1.59-1.63 дБ) — семейные смещения доминируют. + +### Таблица dual по q (реальные cut, одна полоса @500) +| q | res500 | res2000 | cut@500 | cut@2000 | Δ | +|---|--------|---------|---------|----------|---| +| 0.1 | 0.117 | 0.216 | 10.32 | 15.33 | +5.01 | +| 0.5 | 0.117 | 0.839 | 10.32 | 11.82 | +1.50 | +| 1.0 | 0.117 | 1.353 | 10.32 | 10.78 | +0.46 | +| 3.0 | 0.119 | 1.880 | 10.29 | 10.32 | +0.03 | +| 10 | 0.139 | 1.988 | 9.94 | 10.25 | −0.07 | + +Структура: cut@500 КОНСТАНТА по q (драйвер центра не зависит от ширины полосы); +Δcut(res2000) ≈ экспонента 5.4·exp(−res/0.49), схлопывание при res→2. +cut@2000 ПАДАЕТ с ростом res2000 — противоположно любому «lvl растёт с res» +и противоположно делению. Геометрическая интерпретация: широкий нотч (малое q) +НАКРЫВАЕТ тон-конкурент сам по себе; вклад центра всегда полный. Это свойство +ФОРМЫ twin-шаблона на мелкой сетке (0.25 Гц, FUN_180536300), а не пер-биновой +арифметики на сетке 4096. + +### al-группа (якорь уровня, fc=tone=1000) +lvl_a1: 10.08/7.14/5.05/3.58/1.79/0.90 → cut: 13.64/12.44/11.28/10.13/7.97/6.01. +Ломаная: наклон ≈1.16 дБ/окт для lvl≥1.8 и ≈2.06 ниже — кривизна закона +подтверждена независимо (ср. 22s линии). + +### Следствие +Детекторный уровень НЕ восстанавливается из спектра бина функционально. +Кандидаты: (а) скалярная огибающая сигнала через time-domain twin-банк +(536300 строит res-кривые на сетке 0.25 Гц — возможно, тот же банк даёт и +уровни), (б) форма кривой = res-шаблон × скалярный драйв, что естественно +даёт постоянство центра и геометрическое покрытие скайрта. +Решающий эксперимент — живая покадровая трасса детектора на dual-конфиге +(registry-heartbeat инфраструктура готова, NOTES_CAPTURE.md). diff --git a/scripts/resalpha.py b/scripts/resalpha.py new file mode 100644 index 0000000..55fa1f9 --- /dev/null +++ b/scripts/resalpha.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""resalpha.py — find detector res-exponent alpha from existing references. + +Model: lvl_case(alpha) = lvl_case(alpha=1) * res_case^(1-alpha) + real cut = L(log2 lvl) with ONE monotone law L shared by all cases. +For an alpha grid -> global LSQ of cut on (1, log2 lvl) -> mean|resid|(alpha). +The alpha minimising residuals is the detector's true res-weighting. + +lvl(1), res and am at the tone bin come from RT_DUMP_BIN tracts of the current +build (bare-chain env; detector path identical). comb skipped (multiband). +""" +import os +import subprocess +import sys + +import numpy as np + +sys.path.insert(0, "/home/m/re-tools/scripts") +sys.path.insert(0, "/home/m/re-tools/handoff") +import corpus +from render_parity import FS +import corpus as _corpus_mod +load = _corpus_mod.load_mono + +RB = '/home/m/re-tools/dsp/build/render48k' +ENVBASE = {'RT_LUT_OFF': '1', 'RT_IIR12': '0', 'RT_NOWARP': '1', + 'RT_NOBLEND': '1', 'RT_NOIIR3': '1', + 'RT_DUMP_BIN': '/tmp/opencode/resalpha_tract.txt'} +OUT = '/tmp/opencode/resalpha_pairs.csv' + + +def bin_of(f): + return int(round(f / 48000 * 4096)) + + +def ta(x, f, sr=44100): + x = x[-int(0.75 * sr):].astype(np.float64) + t = np.arange(len(x)) / sr + w = 2 * np.pi * f + return np.hypot(2 * np.sum(x * np.cos(w * t)) / len(x), + 2 * np.sum(x * np.sin(w * t)) / len(x)) + + +def db(a): + return 20 * np.log10(max(a, 1e-12)) + + +def main(): + all_cases = [] + for name, inp, args, ref, f in corpus.build_cases(): + joined = [','.join(args)] if len(args) == 3 else args + all_cases.append((name, inp, joined, ref, f)) + + uniq = {} + case_meta = {c[0]: c for c in all_cases} + for name, inp, args, ref, f in all_cases: + uniq.setdefault((inp, tuple(args)), []).append((name, ref, f)) + + rows = [] + out_wav = '/tmp/opencode/resalpha_out.wav' + for (inp, args), items in sorted(uniq.items()): + tract = '/tmp/opencode/resalpha_tract.txt' + if os.path.exists(tract): + os.remove(tract) + env = {**os.environ, **ENVBASE, + 'RT_DUMP_FRAME': str(120 * max(1, len(args)) - 1)} + subprocess.run([RB, inp, out_wav] + list(args), + capture_output=True, text=True, env=env, + cwd='/home/m/re-tools') + try: + d = np.loadtxt(tract) + except Exception as ex: + print('load fail', items[0][0], ex, flush=True) + continue + if d.ndim == 1: + continue + k = d[:, 0].astype(int) + am, res, lvl = d[:, 1], d[:, 2], d[:, 3] + for name, ref, f in items: + b = min(bin_of(f), len(lvl) - 1) + rows.append((name.split('_')[0], name, f, float(lvl[b]), + float(res[b]), float(am[b]), inp, ref)) + print(f'{items[0][0]:>18}: dumped', flush=True) + + final = [] + for g, name, f, lvl1, res, am, inp, ref in rows: + if g == 'comb': + continue + cut = db(ta(load(inp), f)) - db(ta(load(ref), f)) + final.append((g, name, f, lvl1, res, cut)) + + with open(OUT, 'w') as fh: + fh.write('group,case,freq,lvl_a1,res,cut_db\n') + for g, name, f, lvl1, res, cut in final: + fh.write(f'{g},{name},{f},{lvl1:.9g},{res:.9g},{cut:.6f}\n') + print(f'\nwrote {len(final)} rows -> {OUT}') + + # ---- alpha scan ---- + lx = np.array([np.log2(max(r[3], 1e-9)) for r in final]) + lr = np.array([np.log2(max(r[4], 1e-12)) for r in final]) + y = np.array([r[5] for r in final]) + gs = np.array([r[0] for r in final]) + + print(f'\n{"alpha":>6} {"mean|resid|":>11} per-group resid mean') + best = None + for alpha in np.arange(-0.5, 1.51, 0.05): + x = lx + (1 - alpha) * lr + A = np.stack([np.ones_like(x), x], axis=1) + coef, *_ = np.linalg.lstsq(A, y, rcond=None) + resid = y - A @ coef + m = float(np.mean(np.abs(resid))) + pergrp = ' '.join( + f'{g_}:{np.mean(resid[gs == g_]):+.2f}' for g_ in ['t1kq', 't1k', 'al', 'res', 'dual']) + print(f'{alpha:>6.2f} {m:>11.3f} {pergrp}') + if best is None or m < best[1]: + best = (float(alpha), m) + print(f'\nBEST alpha={best[0]:.2f} mean|resid|={best[1]:.3f}') + + +if __name__ == '__main__': + main()