[kim-fp-03] Verify the off-diagonal Fokker-Planck Fourier kernel#217
Conversation
|
Review (standards + spec vs #187) — keeping this in draft. The structural spec items are genuinely delivered: Blocking
Also missing vs spec
Minor
|
Review follow-ups for #187: - Oracle assertions now compare per-row RELATIVE (rows committed as exactly zero must stay exactly zero). The old max(1,|expected|) scaling made every rho-B row (magnitudes 1e-14..1e-22) vacuous: a stubbed-zero fp_rho_B_harmonic passed the oracle subtest. With the fix, a mutation battery of sign, 4*pi, Fourier phase, Bessel order, and ks^2 each fails the ctest; the pristine kernel's maximum scaled error is 6.4e-14 against the predeclared 1e-12 tolerance. - The enforced oracle immediately caught a real defect: the speed of light in constants_mod lacked _dp and was rounded through single precision (29979246592 vs 29979245800, 2.6e-8 relative), exactly the rho-B mismatch observed. Fixed here (same correction as #218 carries); every c-dependent result shifts by ~2.6e-8, so the golden baseline needs a re-bless. - fp_rho_phi_harmonic/fp_rho_B_harmonic take rho_L as an explicit argument instead of recomputing abs(vT/omega_c); hatG_* pass the stored species rho_L, so ion_flr_scale_factor acts on the off-diagonal path exactly as on the diagonal one (regression test added). - fp_ks_model is now configurable: KIM_CONFIG key fp_ks_model_name ('kperp_full' default, 'kr_only' legacy approximation), mapped in kim_read_config with a hard error on unknown names; documented in both shipped nmls and KIM/README.md including the default-behavior change relative to the old kern_include_ks2=.false. - New fp_mphi_truncation_check validation gate (fails closed on unassessable sums) with a slow-decay negative fixture; infinite on-axis ks negative fixture; kernel tests restore the production default model; FORMULA_INDEX rho-B/KIM-05/KIM-06 claims reworded to what is actually verified (the rho-B WL entry remains a transcription; an independent K^{rho B} derivation pass is still open).
|
Review follow-up fixes pushed (638fadb), addressing the blocking findings above. All changes developed red→green with the failure observed first; full suite (33/33 non-excluded) passes locally. 1. Vacuous rho-B oracle — fixed, and it found a real bug.
2. 3. 4. Truncation-tail negative fixture — added. New production gate 5. Smaller items. Infinite (on-axis) k_s now covered by the model-domain negative fixture alongside NaN; all kernel tests restore Still open (needs Mathematica, which I don't have here): the K^{ρB} entry in |
Close the open #187 review item: the rho-B entry in 04_charge_potential_kernels.wl was a transcription of the Fortran assembly. Derive it from thesis (4.39)/(4.41)/(13.23)/(5.12): the B drive u' B^r/B0 maps the verified rho-Phi assembly under the exact drive ratio u'/(-i c k_s) onto -vT^3/(4 pi lambda^2 omega_c nu c) * (I01 (A1 F0 + A2 F2) + A2 I03 F0/2). Extend the FP moment matrix to third order and gate every oracle case on I02=I20 and I03=I21 ((5.13), (A.3)-(A.4)), which justify the production I20/I21 slots; the derived kernel must match the committed assembly per case. The derivation fixes the rho-B prefactor to vT^3: the vT^2 printed in thesis (14.4) is a dimensional misprint. Committed oracle rows are unchanged (derived == transcribed to ~1e-49 relative), so the Fortran side needs no re-bless. All new checks were executed independently via an exact-series mpmath replica at 50 digits (local Wolfram license expired): max |I03-I21|/|I21| = 1.0e-49 across all 13 cases; derived rho-B matches the committed rows at the 34-digit rounding floor (3.9e-34).
|
Mathematica derivation pass done (314a564) — the K^{ρB} entry in Derivation. Starting from thesis (4.39) with drive (4.41) and gradient (13.23): the B drive Two consequences:
Oracle unchanged. Derived and production forms agree to ~1e-49 relative, far below the 34 committed digits, so Execution caveat + independent cross-check. The local Wolfram license on this machine turned out to be expired, so the extended
mpmath reproduction script#!/usr/bin/env python3
"""Independent verification of the K^{rho B} FP Fourier kernel (PR #217 / issue #187).
Derivation chain (Markl thesis, DOI 10.3217/efp2p-0x485):
(4.39) f_m(v_perp,u) = -Int du' G_m(u,u') df0/dr(v_perp,u') v^r_m(v_perp,u')
(4.41) v^r_m = (u'/B0) B^r_m - (i c k_s/B0) Phi_m
(13.23) df0/dr_g = (A1 + (v_perp^2+u'^2)/(2 vT^2) A2) f0
(5.12) I^{kl}(x1,x2) = nu/(sqrt(2 pi) vT^{k+l+1}) Int du Int du' G(u,u')
exp(-u'^2/(2 vT^2)) u^k u'^l
(5.13)-(5.15), (A.1)-(A.2): practical generating-integral definition.
I_D^{kl} series: with s = exp(-tau), the (A.2) integrand derivative at
alpha=beta=0 is P_kl(s; x1) * exp(x1^2) * exp(-x1^2 s) * exp((i x2 - x1^2) tau),
and Int_0^inf s^j exp((i x2 - x1^2) tau) dtau = 1/(j + x1^2 - i x2), so
I_D^{kl} = exp(x1^2) * Sum_j d_j / (j + x1^2 - i x2),
Sum_j d_j s^j = P_kl(s) exp(-x1^2 s) (entire; truncated at J=80).
"""
import sympy as sp
import mpmath as mp
mp.mp.dps = 50
KMAX = 3
JMAX = 80
SOL = mp.mpf(29979245800) # speed of light, cm/s (matches constants_mod fix)
a, b, s, x1s = sp.symbols('a b s x1')
h = (a + sp.I*x1s)*(b + sp.I*x1s)*(s - 1) + (a + b)**2/2
Pkl = {}
for k in range(KMAX + 1):
for l in range(KMAX + 1):
expr = sp.diff(sp.exp(h), a, k, b, l) * sp.exp(-h)
poly = sp.Poly(sp.expand(expr.subs({a: 0, b: 0})), s)
Pkl[(k, l)] = [(int(mono[0]), sp.lambdify(x1s, coef, 'mpmath'))
for mono, coef in zip(poly.monoms(), poly.coeffs())]
def i_diff_matrix(x1, x2):
denom = [mp.mpc(j) + x1**2 - 1j*x2 for j in range(JMAX + 1)]
e = [mp.mpf(1)]
for n in range(1, JMAX + 1):
e.append(e[-1] * (-x1**2) / n)
out = {}
pref = mp.e**(x1**2)
for k in range(KMAX + 1):
for l in range(KMAX + 1):
total = mp.mpc(0)
for r, coef_fn in Pkl[(k, l)]:
c = coef_fn(x1)
if c == 0:
continue
acc = mp.mpc(0)
for j in range(r, JMAX + 1):
acc += e[j - r] / denom[j]
total += c * acc
out[(k, l)] = pref * total
return out
def i_cons(idm, k, l):
den = 1 - idm[(0, 0)] + 2*idm[(2, 0)] - idm[(2, 2)]
return idm[(k, l)] + (idm[(k, 0)] - idm[(k, 2)])*(idm[(l, 0)] - idm[(l, 2)])/den
ORACLE = 'verification/oracles/rho_phi_kernels.dat'
rows = []
with open(ORACLE) as fh:
for line in fh:
line = line.strip()
if not line or line.startswith('#'):
continue
rows.append(line.split())
COLS = ('id charge lambdaD vT omegaC nu ks kr krp rg mphi A1 A2 kpar omegaE '
'Vpar omega debye x1 x2 rhoL bplus bcross phaseRe phaseIm I00Re I00Im '
'I20Re I20Im I01Re I01Im I21Re I21Im F0 F2 rhoPhiDynamicRe '
'rhoPhiDynamicIm debyeRe debyeIm rhoPhiTotalRe rhoPhiTotalIm '
'rhoBDynamicRe rhoBDynamicIm').split()
assert all(len(r) == len(COLS) for r in rows) and len(rows) == 13
def g(row, name):
return mp.mpf(row[COLS.index(name)].replace('E', 'e'))
def gc(row, re_name, im_name):
return mp.mpc(g(row, re_name), g(row, im_name))
def rel(x, y):
d = abs(x - y)
m = max(abs(x), abs(y))
return d/m if m > 0 else d
report = {t: mp.mpf(0) for t in ('T1', 'T2', 'T3', 'T4', 'T5', 'T5b')}
for row in rows:
x1, x2 = g(row, 'x1'), g(row, 'x2')
idm = i_diff_matrix(x1, x2)
I00, I20 = i_cons(idm, 0, 0), i_cons(idm, 2, 0)
I01, I21 = i_cons(idm, 0, 1), i_cons(idm, 2, 1)
I02, I03 = i_cons(idm, 0, 2), i_cons(idm, 0, 3)
for val, rn, im in ((I00, 'I00Re', 'I00Im'), (I20, 'I20Re', 'I20Im'),
(I01, 'I01Re', 'I01Im'), (I21, 'I21Re', 'I21Im')):
report['T1'] = max(report['T1'], rel(val, gc(row, rn, im)))
report['T2'] = max(report['T2'], rel(I02, I20))
report['T3'] = max(report['T3'], rel(I03, I21))
lam, vT, om_c, nu = g(row, 'lambdaD'), g(row, 'vT'), g(row, 'omegaC'), g(row, 'nu')
ks, kr, krp, rg = g(row, 'ks'), g(row, 'kr'), g(row, 'krp'), g(row, 'rg')
mphi = int(row[COLS.index('mphi')])
A1, A2 = g(row, 'A1'), g(row, 'A2')
rho = abs(vT/om_c)
bp = rho**2*(2*ks**2 + kr**2 + krp**2)/2
bx = rho**2*mp.sqrt(ks**2 + kr**2)*mp.sqrt(ks**2 + krp**2)
al, alp = mp.atan2(kr, ks), mp.atan2(krp, ks)
ph = mp.e**(1j*((kr - krp)*rg + mphi*(alp - al)))
F0 = mp.exp(-bp)*mp.besseli(abs(mphi), bx)
F2 = mp.exp(-bp)*((1 - bp - mphi)*mp.besseli(abs(mphi), bx)
+ bx*mp.besseli(abs(mphi - 1), bx))
dyn_phi = (ph/(4*mp.pi*lam**2)) * 1j*vT**2*ks/(om_c*nu) \
* (I00*(A1*F0 + A2*F2) + A2*I20*F0/2)
report['T4'] = max(report['T4'], rel(dyn_phi, gc(row, 'rhoPhiDynamicRe',
'rhoPhiDynamicIm')))
rhoB_derived = -(ph/(4*mp.pi*lam**2)) * vT**3/(om_c*nu*SOL) \
* (I01*(A1*F0 + A2*F2) + A2*I03*F0/2)
rhoB_transcribed = -(ph/(4*mp.pi*lam**2)) * vT**3/(om_c*nu*SOL) \
* (I01*(A1*F0 + A2*F2) + A2*I21*F0/2)
committed = gc(row, 'rhoBDynamicRe', 'rhoBDynamicIm')
report['T5'] = max(report['T5'], rel(rhoB_derived, committed))
report['T5b'] = max(report['T5b'], rel(rhoB_derived, rhoB_transcribed))
for key, desc in (('T1', 'replica vs committed I columns'),
('T2', 'symmetry I02=I20'),
('T3', 'identity I03=I21'),
('T4', 'rho-Phi re-assembly vs committed'),
('T5', 'derived rho-B vs committed'),
('T5b', 'derived vs production rho-B form')):
print(f'{desc:36s} max rel err = {mp.nstr(report[key], 3)}')FORMULA_INDEX updated accordingly (KIM-04 coverage paragraph). With this, all four blocking findings plus the derivation follow-up from the review are addressed; the remaining known reds are the pre-existing |
|
Execution caveat resolved — the local Wolfram license issue is fixed (kernel path configure + reactivation), so the official gate has now been executed locally on 314a564:
The mpmath cross-check above stands as an independent-engine corroboration; the canonical Mathematica path is now verified as well. Nothing remains open from the review on this PR. |
Summary
Why
Dense periodic Fourier assembly now uses the intended nonlocal FP response.
Validation
Validated cumulatively at the stack head:
cmake --build build -j2ctest --test-dir build -E 'test_rhs_balance|test_periodic_convergence|test_periodic_vs_global' --output-on-failure— 35/35 passedKnown red gates are intentionally excluded from that command: the pre-existing
test_rhs_balancebaseline failure, the periodization-deformation acceptance gate, and the unresolved periodic-vs-global 5% agreement gate.Stack
Base:
fix/kim-fp-196-charge-kernel-oracleHead:
fix/kim-fp-187-offdiagonal-kernelCloses #187