291 lines
12 KiB
Python
291 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
"""cascade_sim.py — структурный симулятор тракта маски soothe2 (float-путь).
|
||
|
||
Цель (24mm3): воспроизвести scr(f)=дизайн-сигнал детектора; применённая
|
||
маска = exp(γ·scr), γ=1.760561 (точно), trk@688=exp(scr@628) бит-в-бит.
|
||
|
||
Структура канонической цепи (BLOCKMAP 24hh/24ii + 24mm2):
|
||
шаг 9a: vec698 *= (1 − [54087c]) ; zero при дефолтах
|
||
шаг 9b: vec6f8 += [54087c]·0.8 ; xmm10=0.8 @1824c3e28
|
||
шаг 9c: bands_curve_i /= ... divide-ядро ; dst=678i, A/B уточняются
|
||
шаг 10: vec6f8 = bands_curve_i − ACC_i ; dc40, ACC @таблицы 0x5407c8
|
||
шаг 11: fma att/rel (тройки re/im/coef) ; коэф @6c8/6e8
|
||
шаг 12: COPY ; memcpy
|
||
шаг 13: зеркало 9
|
||
шаг 14: expf(bands_curve); bands_curve += (−1) ; ПОРЯДОК исправлен 24mm2
|
||
шаг 15: bands_curve *= track_i ; th2000 array-mul
|
||
шаг 16: bands_curve *= kWarp@[5406a8]
|
||
шаг 17: expf ещё раз ; call-site 52b32c
|
||
пост-17: exp-вариант(140a40) + pow?(140b00)
|
||
FIR-секция: кривая-float(140b30→1803831c0) + sincos-twiddle(140aa0)
|
||
|
||
ЯДРА (структурная фаза — математически точные numpy-эквиваленты;
|
||
канонический C++ порт = инструкци-точная транскрипция, см. BLOCKMAP 24mm2):
|
||
"""
|
||
import numpy as np
|
||
import glob
|
||
import os
|
||
|
||
GAMMA = 1.760561 # 24mm3: показатель степени, rms фита 0 на чистых кадрах
|
||
N = 2049 # число бинов полной сетки
|
||
|
||
|
||
# ---------------------------------------------------------------- ядра ----
|
||
def k_exp(x):
|
||
"""expf-ядро 180296c80. Структурная фаза: np.exp.
|
||
Каноническая формула (для C++ порта, FMA-точно):
|
||
n = fma(log2e_hi=1.4427f, x, 12582912.0f); k = n - MAGIC
|
||
r = (x - 0.693146f*k) - 1.42861e-06f*k
|
||
p = (((0.00829172f*r+0.0418735f)*r+0.166674f)*r+0.499994f)*r+1)*r+1
|
||
out = bits((k<<23) + bits(p)); guard |x|>87.3365 -> slow path
|
||
"""
|
||
return np.exp(x)
|
||
|
||
|
||
def k_exp_exact(x):
|
||
"""Bit-exact expf 180296c80 (BLOCKMAP:569) — float32 FMA poly, guard 87.3365.
|
||
Python структурный прокси: np.exp (float64) — точный C++ порт в dsp/exp2.cpp
|
||
через fmaf+бит-манипуляции (1824c...), ошибка <0.5 ulp vs плагин."""
|
||
return np.exp(np.asarray(x, dtype=np.float64)).astype(np.float32)
|
||
|
||
|
||
def k_div(a, b):
|
||
"""divide-ядро 1803a06a0: dst = B/A (~0.5 ulp, rcp+таблицы+полином).
|
||
Структурная фаза: точное деление."""
|
||
return b / a
|
||
|
||
|
||
def k_div_exact(a, b):
|
||
"""Bit-exact DIVIDE 1803a06a0 (BLOCKMAP:580) — rcp+quant+vpermps+poly.
|
||
Структурный прокси: точное деление; C++ порт копирует 0.5ulp полином
|
||
0.207515 -0.241687 0.288535 -0.360671 ... 0.240264 через vpermps tbl@1821269c0."""
|
||
return np.asarray(b, dtype=np.float64) / np.asarray(a, dtype=np.float64)
|
||
|
||
|
||
def chain_9_19_sim(bands, tmp6f8, accVec, warp, att, rel, nbin=None):
|
||
"""CHAIN 9-19 BLOCKMAP:620-644 op-by-op (python структурный прокси).
|
||
bands: log-domain bands_curve_i (size nbin), tmp6f8: f6f8, accVec: ACC_i
|
||
warp: kWarp@5406a8, att@5406c8 rel@5406e8 — все 2049.
|
||
Возвращает bands mutated (log domain после IIR, перед FIR)."""
|
||
if nbin is None:
|
||
nbin = len(bands)
|
||
bands = np.asarray(bands, dtype=np.float64)
|
||
tmp6f8 = np.asarray(tmp6f8, dtype=np.float64)
|
||
accVec = np.asarray(accVec, dtype=np.float64)
|
||
# 9a zero already (vec698 irrelevant), 9b: tmp6f8 += 0.8 (p=1.0 *0.8)
|
||
# 9c DIVIDE dst=bands A=bands B=tmp6f8+0.8? Actually B=tmp6f8, but step 9b already added 0.8
|
||
# BLOCKMAP 24mm5: bands = tmp6f8 / bands (divide B/A)
|
||
# Use k_div_exact
|
||
b = tmp6f8 + 0.8 # proxy for step 9b effect (when tmp6f8 initially 0, b=0.8)
|
||
# If tmp6f8 already has data, 9b is additive 0.8, so b = tmp6f8 +0.8
|
||
# For calibration where tmp6f8 is 0, this yields 0.8/bands
|
||
bands = k_div_exact(np.maximum(bands, 1e-30), b)
|
||
# 10 dc40: tmp6f8 = bands - ACC
|
||
tmp6f8 = bands - accVec
|
||
# 11 FMA ATT/REL half-split
|
||
# true triples 12B re/im/coef, scalar proxy: upper half ATT, lower REL
|
||
for i in range(nbin):
|
||
if i < nbin // 2:
|
||
tmp6f8[i] += att[i] * accVec[i]
|
||
else:
|
||
tmp6f8[i] += rel[i] * accVec[i]
|
||
# 14 EXP#1 + -1 (order fixed 24mm2)
|
||
bands = k_exp_exact(bands) - 1.0
|
||
# 15 array-mul X*track (track is external, warp mul is step16)
|
||
# 16 *kWarp + LOG#2
|
||
bands = bands * warp
|
||
bands = np.log(np.maximum(bands, 1e-30))
|
||
# 16b IIR4x2 bidir log-domain base 0x340510 — proxy via kIIR_A1/B1
|
||
# keep as identity for now (real IIR is mild 0.003-0.19 per 24cc)
|
||
# FIR will be applied externally via fir_kernel
|
||
return bands, tmp6f8
|
||
|
||
|
||
# ------------------------------------------------------------ данные -----
|
||
def load_tract(path):
|
||
"""tract_*.txt: k am res lvl_raw band_level prewarp w"""
|
||
t = np.loadtxt(path)
|
||
return {'am': t[:, 1], 'res': t[:, 2], 'lvl': t[:, 3]}
|
||
|
||
|
||
def load_frame(npz):
|
||
"""Слоты кадра rendersnap2 → dict[int, np.ndarray]."""
|
||
d = np.load(npz)
|
||
out = {}
|
||
for k in d.keys():
|
||
if k.startswith('0x'):
|
||
out[int(k[2:], 16)] = d[k]
|
||
return out, d['t_snap']
|
||
|
||
|
||
def pick_clean_frame(ds_dir, min_bins=8):
|
||
"""Отбор чистых стационарных кадров по фазам (24mm3):
|
||
возвращает лучший на фазе γ* (~1.7606, маска применена)
|
||
и лучший на фазе γ=1 (степень ещё не применена)."""
|
||
classes = {'gamma': None, 'identity': None}
|
||
for f in sorted(glob.glob(os.path.join(ds_dir, 'ph*.npz'))):
|
||
try:
|
||
S, ts = load_frame(f)
|
||
except Exception:
|
||
continue
|
||
if not all(x in S for x in (0x540628, 0x540688, 0x540678)):
|
||
continue
|
||
s = S[0x540628][:1025].astype(np.float64)
|
||
t = S[0x540688][:1025].astype(np.float64)
|
||
c = S[0x540678][:1025].astype(np.float64)
|
||
ok = (t > 1e-30) & (c > 1e-30) & np.isfinite(s)
|
||
if ok.sum() < 50:
|
||
continue
|
||
lt = np.log(t[ok])
|
||
lc = np.log(c[ok])
|
||
sel = np.abs(lt) > 0.05
|
||
if sel.sum() < min_bins:
|
||
continue
|
||
g = float(np.sum(lt[sel] * lc[sel]) / np.sum(lt[sel] ** 2))
|
||
rms = float(np.sqrt(np.mean((lc[sel] - g * lt[sel]) ** 2)))
|
||
depth = float(-lc.min())
|
||
key = 'gamma' if abs(g - GAMMA) < 0.01 else \
|
||
('identity' if abs(g - 1.0) < 1e-4 else None)
|
||
if key is None or rms > 1e-4:
|
||
continue
|
||
cand = (depth, f, s, t, c, g, rms)
|
||
if classes[key] is None or depth > classes[key][0]:
|
||
classes[key] = cand
|
||
return classes
|
||
|
||
|
||
# ------------------------------------------------------- валидация -------
|
||
def validate_scr(sim_scr, cap_scr, tol_db=0.05):
|
||
"""rms в дБ между симулированным и захваченным scr."""
|
||
m = np.abs(cap_scr) > 0.02
|
||
err = (sim_scr[m] - cap_scr[m]) * (20 / np.log(10))
|
||
return float(np.sqrt(np.mean(err ** 2))), int(m.sum())
|
||
|
||
|
||
def win_periodic_hann(N):
|
||
return 0.5 * (1.0 - np.cos(2.0 * np.pi * np.arange(N) / N))
|
||
|
||
|
||
# ------------------------------------------------ FIR-цепь (24mm9) --------
|
||
NFRAME = 4096 # n=[ctx+0x540534]
|
||
NBINS_FIR = NFRAME // 2 + 1
|
||
Q_EXP = 0.8002203702926636 # .rdata 1820013f0, refines 0.80 (BLOCKMAP 52b716)
|
||
|
||
|
||
def winfreq_fall():
|
||
"""WINfreq@[ctx+0x540658]: периодический Hann(4096), падающая половина."""
|
||
return win_periodic_hann(NFRAME)[NFRAME // 2:]
|
||
|
||
|
||
def fir_kernel(scr, q=Q_EXP):
|
||
"""Полная FIR-цепь (BLOCKMAP 24mm9): min-phase кепстральный сэндвич.
|
||
|
||
scr(2049) → pack(re=scr,im=0) → FIR[n]=0 (Найквост)
|
||
→ inv-RFFT → fold(y[1..2047]*=2.0 @1824c41e0; y[2049..4095]=0)
|
||
→ fwd-RFFT → комплексная EXP (1803831c0, аргумент ×q)
|
||
→ inv-RFFT → ×падающий Hann → ноль хвоста → fwd-RFFT
|
||
→ FIR[0]=1, FIR[1]=0. Возвращает |F| (2049).
|
||
"""
|
||
h = np.asarray(scr, dtype=np.complex128).copy()
|
||
h[-1] = 0.0
|
||
y = np.fft.irfft(h, n=NFRAME)
|
||
y[1:NFRAME // 2] *= 2.0
|
||
y[NFRAME // 2 + 1:] = 0.0
|
||
w = np.fft.irfft(np.exp(q * np.fft.rfft(y, n=NFRAME)), n=NFRAME)
|
||
w[:NFRAME // 2] *= winfreq_fall()
|
||
w[NFRAME // 2:] = 0.0
|
||
F = np.abs(np.fft.rfft(w, n=NFRAME))
|
||
F[0] = 1.0
|
||
return F
|
||
|
||
|
||
def mask_from_frame(S, q=Q_EXP):
|
||
"""mask_sim из слотов кадра: cur ≈ trk · |F(scr)| (df0 complex-mul)."""
|
||
scr = S[0x540628][:NBINS_FIR].astype(np.float64)
|
||
trk = S[0x540688][:NBINS_FIR].astype(np.float64)
|
||
return trk * fir_kernel(scr, q)
|
||
|
||
|
||
def validate_mask_stage(ds, q=Q_EXP, cap=60):
|
||
"""Валидация масочной ветви на чистых γ-кадрах (24mm9-протокол).
|
||
|
||
Отбор: |γ_fit−1.760561|<5e-4 и fit-rms<1e-5 (жёстче pick_clean_frame).
|
||
Критерий: rms по ВСЕМ 2049 бинам < 0.05 дБ (структурная фаза).
|
||
"""
|
||
import glob
|
||
rmss, gpred = [], []
|
||
for f in sorted(glob.glob(os.path.join(ds, 'ph*.npz'))):
|
||
try:
|
||
d = np.load(f)
|
||
except Exception:
|
||
continue
|
||
if '0x540628' not in d:
|
||
continue
|
||
scr = d['0x540628'][:NBINS_FIR].astype(np.float64)
|
||
trk = d['0x540688'][:NBINS_FIR].astype(np.float64)
|
||
cur = d['0x540678'][:NBINS_FIR].astype(np.float64)
|
||
ok = (trk > 1e-30) & (cur > 1e-30) & np.isfinite(scr)
|
||
if ok.sum() < 50:
|
||
continue
|
||
lt, lc = np.log(trk[ok]), np.log(cur[ok])
|
||
sel = np.abs(lt) > 0.05
|
||
if sel.sum() < 8:
|
||
continue
|
||
g = float(np.sum(lt[sel] * lc[sel]) / np.sum(lt[sel] ** 2))
|
||
frms = float(np.sqrt(np.mean((lc[sel] - g * lt[sel]) ** 2)))
|
||
if not (abs(g - GAMMA) < 5e-4 and frms < 1e-5):
|
||
continue
|
||
F = fir_kernel(scr, q)
|
||
lf = np.log(F[sel])
|
||
lt_s = np.log(trk[sel])
|
||
sF = float(np.sum(lf * lt_s) / np.sum(lt_s ** 2))
|
||
gpred.append(1.0 + sF)
|
||
m = trk * F
|
||
mm = (cur > 1e-12) & (m > 1e-12)
|
||
e = (np.log(m[mm]) - np.log(cur[mm])) * 20 / np.log(10)
|
||
rmss.append(float(np.sqrt(np.mean(e ** 2))))
|
||
if len(rmss) >= cap:
|
||
break
|
||
if not rmss:
|
||
print('нет ультрачистых кадров в', ds)
|
||
return
|
||
rmss = np.array(rmss)
|
||
print('кадров=%d | rms медиана=%.4f дБ p90=%.4f max=%.4f | '
|
||
'gamma_pred(1+s_F)=%.6f' %
|
||
(len(rmss), np.median(rmss), np.percentile(rmss, 90), rmss.max(),
|
||
float(np.median(gpred))))
|
||
|
||
|
||
def main():
|
||
import sys
|
||
if len(sys.argv) > 1 and sys.argv[1] == '--mask':
|
||
validate_mask_stage(sys.argv[2] if len(sys.argv) > 2
|
||
else '/tmp/opencode/sc_multi4b')
|
||
return
|
||
ds = sys.argv[1] if len(sys.argv) > 1 else '/tmp/opencode/sc_multi6'
|
||
tract = sys.argv[2] if len(sys.argv) > 2 else '/tmp/opencode/tract_multi6.txt'
|
||
|
||
classes = pick_clean_frame(ds)
|
||
ph_g = classes['gamma']
|
||
ph_i = classes['identity']
|
||
if not ph_g and not ph_i:
|
||
print('нет чистых кадров в', ds)
|
||
return
|
||
for lbl, best in (('γ-фаза', ph_g), ('identity', ph_i)):
|
||
if not best:
|
||
continue
|
||
_, f, scr, trk, cur, gamma_fit, grms = best
|
||
n = len(scr)
|
||
cut_meas = -20 / np.log(10) * np.log(np.maximum(cur, 1e-30))
|
||
g_use = gamma_fit
|
||
cut_sim = g_use * (-scr) * 20 / np.log(10)
|
||
e = cut_sim - cut_meas
|
||
sel = np.abs(cut_meas) > 0.1
|
||
rms_db = float(np.sqrt(np.mean(e[sel] ** 2))) if sel.any() else 0.0
|
||
print(f'{lbl}: {os.path.basename(f)} γ={gamma_fit:.6f} (rms {grms:.1e}) '
|
||
f'закон: rms={rms_db:.4f} дБ / {int(sel.sum())} бинов')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|