Skip to content

FFS - BUG - Riccati Δ' carries a singfac_min matching-radius bias that dominates small matrix elements #412

Description

@d-burg

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

  1. ForceFreeStatesStructs.jl:272singfac_min::Float64 = 1e-4 # Matches Fortran STRIDE — the comment is wrong: Fortran STRIDE's code default is 1e-5 (stride/ode.F:39).
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions