diff --git a/dsp/framed_model.cpp b/dsp/framed_model.cpp index d17684c..239f9d6 100644 --- a/dsp/framed_model.cpp +++ b/dsp/framed_model.cpp @@ -245,6 +245,17 @@ static void process_band_structural( } } + // RT_RESPRP=1 (NOTES 22t): keep ONLY the res^rp factor of the warp cascade + // while NOWARP skips the full kBand768*kWarp*res^rp blanket. Two-factor law: + // cut(lvl) affine + geometry weight res^rp (decomp-sourced form, rp EMPIRICAL). + static const int resrp_only = getenv("RT_RESPRP") ? atoi(getenv("RT_RESPRP")) : 0; + if (nowarp && resrp_only) { + for (size_t k = 0; k < nbin; k++) { + double res_k = std::max(static_cast(res[k]), 1e-12); + mask_out[k] *= std::pow(res_k, rp); + } + } + // Step 9 (NOTES_LEVEL:830 + consumers_out.txt:955-1075): IIR3 inline, // TWO bidirectional passes [reset, forward, backward] x2 (state persists // from forward into backward within a pair; reset between pairs). @@ -330,6 +341,11 @@ void FramedDetector::setParams(const std::vector& bands) { res_.clear(); track_.clear(); + // RT_DUMPRESPATH= (NOTES 22t): static twin-response spectra per band, + // binary {int32 band, int32 nbin, float res[nbin]} records (append). + FILE* rp_dump = nullptr; + if (const char* dp = getenv("RT_DUMPRESPATH")) rp_dump = fopen(dp, "ab"); + for (const auto& b : bands_) { std::vector r(half + 1, 1.0f); float sens_lin = std::pow(10.0f, b.sens * SENS_SCALE / 20.0f); @@ -348,8 +364,16 @@ void FramedDetector::setParams(const std::vector& bands) { r[k] = std::sqrt(out[k].re * out[k].re + out[k].im * out[k].im); r[k] = std::max(r[k], 1e-12f); } + if (rp_dump) { + int32_t bi = static_cast(res_.size()); + int32_t nb = static_cast(r.size()); + fwrite(&bi, sizeof(int32_t), 1, rp_dump); + fwrite(&nb, sizeof(int32_t), 1, rp_dump); + fwrite(r.data(), sizeof(float), r.size(), rp_dump); + } res_.push_back(std::move(r)); } + if (rp_dump) fclose(rp_dump); track_.assign(bands_.size(), std::vector(half + 1, 1.0f)); } diff --git a/handoff/NOTES_LEVEL.md b/handoff/NOTES_LEVEL.md index 53e2e51..ef98143 100644 --- a/handoff/NOTES_LEVEL.md +++ b/handoff/NOTES_LEVEL.md @@ -2016,3 +2016,45 @@ x_i=−dA*/dS — точная эффективная координата ке не решается, подтверждено покейсовой бисекцией. 3. Реплей-инфраструктура готова для быстрого перебора ЛЮБЫХ параметрических законов (2 c корпус) — использовать вместо реальных рендеров до финальной валидации. + +## ============ UPDATE 2026-08-23b (22t): ДВУХФАКТОРНЫЕ ЗАКОНЫ ОТВЕРГНУТЫ — БЮДЖЕТ ОШИБКИ В DUAL ============ + +Инструменты расширены: +- `RT_DUMPRESPATH=` в FramedDetector::setParams — статические спектры twin-response + res[band][bin] ({i32 band,i32 nbin,f32[]}, append); lawfit collect кладёт их в npz рядом с lvl. +- `RT_RESPRP=1` в process_band_structural — применять res^rp при NOWARP (для реальных + рендеров двухфакторного закона). +- `lawfit22r.py fit`: координатный спуск по семействам законов на реплее (2 c/корпус). + +### Результат фит-семейств (офлайн, старт 7.6/1.6) +| семейство | A | S | Q | rp | TOTAL(sim) | +|-----------|---|---|---|----|-----------| +| scalar | 7.600 | 1.694 | 0 | 0 | **1.860** | +| quad (Q·x²) | →scalar | | **→0** | | 1.860 | +| resrp (mask·res^rp) | →scalar | | | **→0** | 1.860 | +| quad+resrp | 7.452 | 1.540 | +0.069 | 0 | 1.865 | + +ОБА вторых фактора отброшены спуском: кривизна не нужна, res-вес не нужен. +Объяснение resrp: lvl = am/res УЖЕ содержит геометрию твин-юбки; домножение на +res^rp сверху — двойной учёт (в каноне res^rp компенсировал другое — отсутствие +res в xv-домене LUT). + +### Реальный рендер скалярного оптимума (7.6, 1.694) +t1kq 0.954 / t1k 0.842 / al 0.799 / res **0.117** / dual **3.246** / comb 4.855 +⇒ TOTAL **1.898** (sim 1.860 + стат. смещения реплея ~0.04). Против HEAD 1.931: +выигрыш мизерный, t1kq/t1k/comb РЕГРЕССИРУЮТ ⇒ гейт не пройден, канон НЕ тронут. + +### Бюджет ошибки — где лежит остаток (решающий факт) +abs-сумма по корпусу при (7.6,1.694): 115.4 дБ·кейсов, из них: +- **dual: 22 кейса × 3.246 = 71.4 (62%!)** — межполосный механизм (Шаг 2/9, консюмер acc/f6f8); +- comb: 24.3 (21%) — мультиполосное взаимодействие; +- всё прочее: ~20 (17%). +Если бы dual был на уровне bridge (0.726), TOTAL упал бы до ~0.96. Никакая +донастройка закона это не даст — ПРИОРИТЕТ №1 подтверждён количественно: +искать точку потребления acc/f6f8 (межполосный каскад), а не крутить маску. + +### NEXT +1. dual/межполосный консюмер: декомп-охота вокруг FUN_180529fe0 callers + (52e260 render-loop, multiband combine), трассировка acc/f6f8 в живом плагине. +2. comb: после dual — пере-тест 4-полосного взаимодействия. +3. Реплей-инфра готова; для любых новых гипотез сначала офлайн (2 c), потом рендер. diff --git a/scripts/lawfit22r.py b/scripts/lawfit22r.py index 740a974..56c5db8 100644 --- a/scripts/lawfit22r.py +++ b/scripts/lawfit22r.py @@ -91,10 +91,14 @@ def collect(): store = {} traj_bin = '/tmp/opencode/lawfit_traj.bin' + resp_bin = '/tmp/opencode/lawfit_res.bin' for key, u in sorted(uniq.items()): if os.path.exists(traj_bin): os.remove(traj_bin) - env = {**os.environ, **BASE_ENV, 'RT_DUMP_ALL': traj_bin} + if os.path.exists(resp_bin): + os.remove(resp_bin) + env = {**os.environ, **BASE_ENV, 'RT_DUMP_ALL': traj_bin, + 'RT_DUMPRESPATH': resp_bin} subprocess.run([corpus.RB, u['inp'], '/tmp/opencode/lawfit_out.wav'] + u['args'], capture_output=True, text=True, env=env, cwd='/home/m/re-tools') @@ -104,9 +108,26 @@ def collect(): continue lvl = arr.reshape(-1, u['nb'], RBIN) # [frame][band][bin] store[key] = lvl - print(f'{key}: {lvl.shape[0]} frames x {u["nb"]} bands') + # static twin-response spectra per band ({i32 band, i32 nbin, f32[nbin]}) + res = None + if os.path.exists(resp_bin): + raw = open(resp_bin, 'rb').read() + off = 0 + bands = [] + while off < len(raw): + bi, nb = struct.unpack_from(' {TRAJ}') + print(f'\nwrote {len(store)} entries -> {TRAJ}') def load_traj(): @@ -135,16 +156,24 @@ class CaseInput: return lvl.shape[0] == self.nf -def masks_from_lvl(lvl, nb, A, S): - """Per-frame lower-half mask exactly as bare chain + LAWAFFINE produces.""" +def masks_from_lvl(lvl, nb, A, S, Q=0.0, res=None, rp=0.0): + """Per-frame lower-half mask exactly as bare chain + LAWAFFINE produces. + + Law families (22t): scalar cut=A+S*log2(lvl); quad adds curvature Q*x^2; + resrp multiplies per-band mask by res^rp (decomp warp-cascade factor, + applied BEFORE cross-band min like the C++ warp section). + """ nf = lvl.shape[0] lv64 = lvl.astype(np.float64) - mm = np.exp2(-((A + S * np.log2(np.maximum(lv64, 1e-12))) / 6.0174)) + x = np.log2(np.maximum(lv64, 1e-12)) + mm = np.exp2(-((A + S * x + Q * x * x) / 6.0174)) low = lv64 <= 1e-6 # C++ fallback: exp2(-level) if low.any(): mm[low] = np.exp2(-lv64[low]) + if res is not None and rp: + mm = mm * res[None].astype(np.float64) ** rp if nb > 1: - lo = np.min(mm, axis=1) + lo = np.min(mm, axis=1) # min across bands else: lo = mm[:, 0, :] full = np.ones((nf, NFFT)) @@ -153,10 +182,10 @@ def masks_from_lvl(lvl, nb, A, S): return full -def replay(ci, lvl, nb, A, S): - """Return trimmed 44.1k output for law (A,S) on prepared CaseInput ci.""" +def replay(ci, lvl, nb, A, S, Q=0.0, res=None, rp=0.0): + """Return trimmed 44.1k output for law params on prepared CaseInput ci.""" assert ci.frames_ok(lvl), f'traj {lvl.shape[0]} != frames {ci.nf}' - full = masks_from_lvl(lvl, nb, A, S) + full = masks_from_lvl(lvl, nb, A, S, Q, res, rp) segs = ci.xext[ci.offs[:, None] + np.arange(NFFT)[None, :]] * _WIN[None, :] yspec = np.fft.fft(segs, axis=1) * full td = np.real(np.fft.ifft(yspec, axis=1)) @@ -203,20 +232,18 @@ def build_eval_index(trajs): return idx -def sim_errors(trajs, idx, A, S, ref_cache=None): +def sim_errors(trajs, idx, A, S, Q=0.0, rp=0.0): errs = {} ycache = {} for e in idx: - ck = (e['key'], e['nb']) + ck = (e['key'], e['nb'], round(Q, 6), round(rp, 6)) if ck not in ycache: ci = case_input(e['key'], e['inp']) - ycache[ck] = replay(ci, trajs[e['key']], e['nb'], A, S) + ycache[ck] = replay(ci, trajs[e['key']], e['nb'], A, S, + Q, trajs.get(e['key'] + '|res'), rp) y44 = ycache[ck] - if ref_cache is None: - errs[e['name']] = corpus.db(corpus.ta(y44, e['f'])) - \ - corpus.db(corpus.ta(corpus.load_mono(e['ref']), e['f'])) - else: - errs[e['name']] = corpus.db(corpus.ta(y44, e['f'])) - ref_cache[e['name']] + errs[e['name']] = corpus.db(corpus.ta(y44, e['f'])) - \ + corpus.db(corpus.ta(corpus.load_mono(e['ref']), e['f'])) return errs @@ -250,45 +277,72 @@ def sanity(A, S, json_path): def fit(A0, S0): trajs = load_traj() idx = build_eval_index(trajs) + fit2(trajs, idx, A0, S0) - def objective(A, S, sub=None): + +def fit2(trajs, idx, A0, S0): + """Law-family comparison: scalar / quad / resrp / quad+resrp (NOTES 22t).""" + + def objective(p, sub=None): + A, S, Q, rp = p ii = idx if sub is None else sub - return group_stats(sim_errors(trajs, ii, A, S)) + return group_stats(sim_errors(trajs, ii, A, S, Q, rp)) - best = (A0, S0) - bst = objective(*best) - print(f'start A={A0} S={S0}: TOTAL={bst["TOTAL"]:.3f}') - stepA, stepS = 0.8, 0.25 - for it in range(4): - moved = False - for A, S in [(best[0] + stepA, best[1]), (best[0] - stepA, best[1]), - (best[0], best[1] + stepS), (best[0], best[1] - stepS)]: - st = objective(A, S) - mark = '' - if st['TOTAL'] < bst['TOTAL'] - 1e-4: - best, bst = (A, S), st - moved = True - mark = ' *' - print(f' [{it}] A={A:+7.3f} S={S:+6.3f}: TOTAL={st["TOTAL"]:.3f}{mark}') - if not moved: - stepA /= 2 - stepS /= 2 - if stepA < 0.05: - break - print(f'\nBEST global: A={best[0]:.3f} S={best[1]:.3f} TOTAL={bst["TOTAL"]:.3f}') - for k, v in bst.items(): - print(f' {k:>6}: {v:.3f}') + def descend(p0, steps, sub=None, label=''): + best = list(p0) + bst = objective(tuple(best), sub) + print(f'{label} start {[round(v, 4) for v in best]}: ' + f'TOTAL={bst["TOTAL"]:.3f}', flush=True) + while all(s > 1e-4 for s in steps.values()): + moved = False + for i, nm in enumerate(['A', 'S', 'Q', 'rp']): + if nm not in steps: + continue + st = steps[nm] + for d in (+st, -st): + cand = list(best) + cand[i] = round(cand[i] + d, 6) + if cand[3] < 0 or (nm == 'Q' and abs(cand[2]) > 3): + continue + s2 = objective(tuple(cand), sub) + if s2['TOTAL'] < bst['TOTAL'] - 1e-4: + best, bst = cand, s2 + moved = True + print(f' {label} {nm}{d:+.4g}: TOTAL={s2["TOTAL"]:.3f} ' + f'{[round(v, 4) for v in best]}', flush=True) + if not moved: + for nm in steps: + steps[nm] /= 2 + return best, bst - print('\n=== per-group greedy optima (grid around global best) ===') - for g in ['t1kq', 't1k', 'al', 'res', 'dual', 'comb']: - sub = [e for e in idx if e['grp'] == g] - bA, bS, bval = None, None, 1e9 - for A in np.arange(best[0] - 2.5, best[0] + 2.51, 0.5): - for S in np.arange(max(0.25, best[1] - 1.0), best[1] + 1.01, 0.25): - st = objective(float(A), float(S), sub) - if st[g] < bval: - bA, bS, bval = float(A), float(S), st[g] - print(f'{g:>6}: A={bA:6.2f} S={bS:5.2f} mean|e|={bval:.3f}', flush=True) + res_all = {} + + b, t = descend([A0, S0, 0.0, 0.0], {'A': 0.4, 'S': 0.15}, label='[scalar]') + res_all['scalar'] = (list(b), dict(t)) + + bA, bS, _, _ = res_all['scalar'][0] + b, t = descend([bA, bS, 0.0, 0.0], {'A': 0.3, 'S': 0.15, 'Q': 0.06}, + label='[quad]') + res_all['quad'] = (list(b), dict(t)) + + b, t = descend([bA, bS, 0.0, 0.03], {'A': 0.3, 'S': 0.15, 'rp': 0.01}, + label='[resrp]') + res_all['resrp'] = (list(b), dict(t)) + + bA, bS, bQ, _ = res_all['quad'][0] + b, t = descend([bA, bS, bQ, 0.03], {'A': 0.25, 'S': 0.12, 'Q': 0.05, + 'rp': 0.008}, label='[quad+resrp]') + res_all['quad+resrp'] = (list(b), dict(t)) + + print('\n================ LAW FAMILY SUMMARY ================') + for fam, (p, st) in res_all.items(): + gs = ' '.join(f'{g}={st[g]:.3f}' for g in + ['t1kq', 't1k', 'al', 'res', 'dual', 'comb']) + print(f'{fam:>12}: A={p[0]:7.3f} S={p[1]:6.3f} Q={p[2]:+6.3f} ' + f'rp={p[3]:5.3f} TOTAL={st["TOTAL"]:.3f}\n{"":>14}{gs}') + json.dump({f: {'params': p, 'groups': s} for f, (p, s) in res_all.items()}, + open('/tmp/opencode/lawfit_fit2.json', 'w'), indent=1) + print('\nwrote /tmp/opencode/lawfit_fit2.json') def freq_of(name):