Symptom
The Riccati-path Δ' matrix carries a matching-radius (singfac_min) truncation error that the asymptotic Frobenius series does not absorb. It is an absolute error, so it is invisible on large diagonal elements (~1%) and dominant on small ones (~15%). Found while benchmarking against Fortran STRIDE, TJ, and our own Galerkin path on a circular TJ equilibrium (ε = 0.1499, q_edge = 3.6, n = 1; ldp mpsi=256 unless noted).
Δ'_31 (small element, consensus ≈ 1.05–1.07):
singfac_min |
Δ'_31 |
Δ'_21 |
| 1e-4 (default) |
0.9087 |
10.8678 |
| 3e-5 |
1.0217 |
10.8709 |
| 1e-5 |
1.0464 |
10.8767 |
| 3e-6 |
1.0545 |
10.8743 |
| 1e-6 |
1.0552 (plateau) |
10.8750 |
Cross-checks at the same point: julia Galerkin path (gal_flag=true) = 1.0567; Fortran STRIDE = 1.05 ± 0.02 (mpsi 128/192/256, at the same singfac_min=1e-4, sing_order=6); TJ (converted to the ψ_N reference) = 1.067. So the default-singfac_min Riccati value is ~14 % low and everything else agrees.
The bias is grid-converged: identical (±0.2 %) across ldp mpsi 128/512/1024 and the auto grid at τ = 1e-3/3e-4 — resolution scans cannot detect it. It is also insensitive to qhigh truncation (0.001 per 0.02 in q_edge) and to eulerlagrange_tolerance (1e-8 → 1e-10 changes nothing).
What it is NOT (measured/audited)
- Not the asymptotic series order.
sing_order = 2, 6, 10 give identical Δ' to 4+ digits — expected, since the expansion parameter √dpsi ≈ 5e-3 makes terms beyond k=4 numerically irrelevant at these radii. Raising sing_order cannot fix this.
- Not a port defect in the series or the BVP. A line-by-line comparison against Fortran STRIDE (
stride/sing.F, stride/ode.F) found the vmat recursion, sing_get_ua power restoration, matching-point formula (singfac_min/|n·q'|), BVP assembly, boundary blocks, and PEST3 four-term combination all term-for-term equivalent.
- Not spline-knot pollution of the series coefficients: the same bias appears on the auto grid, which brackets rationals away from knots.
Mechanism status
The residual decays as ~dpsi^1.2 — a non-half-integer power outside the span of the Frobenius series, consistent with the one error family the series provably cannot cancel (big-solution contamination of the extracted small-solution coefficient, expected to scale as dpsi^(2α); here 2α = 1.00 at both surfaces, vs 1.22 measured — right family, imperfect exponent match, so mechanism is indicated, not proven). Open question: Fortran shows only −1.4 % over the same singfac decade at identical settings, and the audits found no convention difference that explains its immunity. Remaining unexamined candidates: per-column atol policy (see below), left/right alpha_override sharing, axis-midpoint placement.
Two concrete defects found along the way
ForceFreeStatesStructs.jl:272 — singfac_min::Float64 = 1e-4 # Matches Fortran STRIDE — the comment is wrong: Fortran STRIDE's code default is 1e-5 (stride/ode.F:39).
Riccati.jl:1411/1421 (and the other solve(...) calls in the FM path) never pass abstol, so DifferentialEquations' uniform default abstol=1e-6 applies to columns spanning ~10⁻³ to ~10³. Fortran scales atol per column to the column's own magnitude (ode.F:411–431). Independent fidelity gap; tightening it is safe (it strengthens, not relaxes, the tolerance).
Proposed fixes — NOT applied, need maintainer sign-off (published Δ' values move)
- P1 (safe, do first): per-column-scaled
abstol in integrate_fm_with_ua_ic and the FM solves, mirroring ode.F:422-431.
- P2 (the main fix, needs decision): lower the default matching radius — the measured plateau is at
singfac_min ≈ 3e-6–1e-6 (runtime unchanged in tests) — or decouple the chunk-layout radius (EulerLagrange.jl:533) from the ua-matching radius so the layout stays at 1e-4. This moves Δ' (+16 % on the benchmark's 3/1) and every downstream tearing quantity; regression harness + human review required.
- P3: fix the misleading comment either way.
Repro
Scripts + logs: julia_deltaprime_harness/outputs/develop_benchmark/{riccati31_sens.jl,riccati31_sens2.jl,galerkin31_probe.jl,dprime31_probes.jl,stride31_probe.py} in the maintainer's benchmark repo (equilibrium TJ_epsilon_scan_0.1499.geqdsk, settings in-script). Measured on develop @ 9491f89.
🤖 Generated with Claude Code
Symptom
The Riccati-path Δ' matrix carries a matching-radius (
singfac_min) truncation error that the asymptotic Frobenius series does not absorb. It is an absolute error, so it is invisible on large diagonal elements (~1%) and dominant on small ones (~15%). Found while benchmarking against Fortran STRIDE, TJ, and our own Galerkin path on a circular TJ equilibrium (ε = 0.1499, q_edge = 3.6, n = 1;ldpmpsi=256 unless noted).Δ'_31 (small element, consensus ≈ 1.05–1.07):
singfac_minCross-checks at the same point: julia Galerkin path (
gal_flag=true) = 1.0567; Fortran STRIDE = 1.05 ± 0.02 (mpsi 128/192/256, at the samesingfac_min=1e-4,sing_order=6); TJ (converted to the ψ_N reference) = 1.067. So the default-singfac_minRiccati value is ~14 % low and everything else agrees.The bias is grid-converged: identical (±0.2 %) across
ldpmpsi 128/512/1024 and the auto grid at τ = 1e-3/3e-4 — resolution scans cannot detect it. It is also insensitive toqhightruncation (0.001 per 0.02 in q_edge) and toeulerlagrange_tolerance(1e-8 → 1e-10 changes nothing).What it is NOT (measured/audited)
sing_order= 2, 6, 10 give identical Δ' to 4+ digits — expected, since the expansion parameter √dpsi ≈ 5e-3 makes terms beyond k=4 numerically irrelevant at these radii. Raisingsing_ordercannot fix this.stride/sing.F,stride/ode.F) found the vmat recursion,sing_get_uapower restoration, matching-point formula (singfac_min/|n·q'|), BVP assembly, boundary blocks, and PEST3 four-term combination all term-for-term equivalent.Mechanism status
The residual decays as ~dpsi^1.2 — a non-half-integer power outside the span of the Frobenius series, consistent with the one error family the series provably cannot cancel (big-solution contamination of the extracted small-solution coefficient, expected to scale as dpsi^(2α); here 2α = 1.00 at both surfaces, vs 1.22 measured — right family, imperfect exponent match, so mechanism is indicated, not proven). Open question: Fortran shows only −1.4 % over the same singfac decade at identical settings, and the audits found no convention difference that explains its immunity. Remaining unexamined candidates: per-column
atolpolicy (see below), left/rightalpha_overridesharing, axis-midpoint placement.Two concrete defects found along the way
ForceFreeStatesStructs.jl:272—singfac_min::Float64 = 1e-4 # Matches Fortran STRIDE— the comment is wrong: Fortran STRIDE's code default is1e-5(stride/ode.F:39).Riccati.jl:1411/1421(and the othersolve(...)calls in the FM path) never passabstol, so DifferentialEquations' uniform defaultabstol=1e-6applies to columns spanning ~10⁻³ to ~10³. Fortran scalesatolper column to the column's own magnitude (ode.F:411–431). Independent fidelity gap; tightening it is safe (it strengthens, not relaxes, the tolerance).Proposed fixes — NOT applied, need maintainer sign-off (published Δ' values move)
abstolinintegrate_fm_with_ua_icand the FM solves, mirroringode.F:422-431.singfac_min ≈ 3e-6–1e-6(runtime unchanged in tests) — or decouple the chunk-layout radius (EulerLagrange.jl:533) from the ua-matching radius so the layout stays at 1e-4. This moves Δ' (+16 % on the benchmark's 3/1) and every downstream tearing quantity; regression harness + human review required.Repro
Scripts + logs:
julia_deltaprime_harness/outputs/develop_benchmark/{riccati31_sens.jl,riccati31_sens2.jl,galerkin31_probe.jl,dprime31_probes.jl,stride31_probe.py}in the maintainer's benchmark repo (equilibriumTJ_epsilon_scan_0.1499.geqdsk, settings in-script). Measured ondevelop@ 9491f89.🤖 Generated with Claude Code