Skip to content

Fix covariant B_theta and add psimax stop in EQDSK chartmap converter#399

Merged
krystophny merged 1 commit into
mainfrom
fix/chartmap-covariant-btheta
Jul 16, 2026
Merged

Fix covariant B_theta and add psimax stop in EQDSK chartmap converter#399
krystophny merged 1 commit into
mainfrom
fix/chartmap-covariant-btheta

Conversation

@krystophny

Copy link
Copy Markdown
Member

Two defects in eqdsk_to_boozer_chartmap (follow-up to #346), found while regenerating the reactor-size circular benchmark fixture for itpplasma/benchmark-simple-potato:

1. B_theta = G/q is not the covariant poloidal component. The inline comment reasoned iota = I/G => I = G/q, but iota is the ratio of the contravariant components; the covariant I(s) measures the enclosed toroidal current (2*pi*I = mu0*I_tor). For a tokamak the true I is ~40x smaller than G/q. SIMPLE consumes B_theta as the covariant surface function (h_theta = B_theta/Bmod), so the fabricated value inflated the field-line length factor (G + iota*I) and made every traced orbit's parallel dynamics too slow by O(1/q^2) — 16 % too-low bounce frequencies at q = 2.26. Now computed as the signed circulation of B_pol = |grad psi|/R from the g-file psi map (using sqrt(B^2 - B_tor^2) instead would cancel catastrophically on inner surfaces).

Cross-code check on the circular benchmark EQDSK (5 keV deuteron, s_tor = 0.3), SIMPLE canonical frequency API on the converted chartmap vs POTATO adaptive-RK tracing of the same g-file:

case omega_b SIMPLE omega_b POTATO omega_phi SIMPLE omega_phi POTATO
trapped xi=0.3 1.2805e4 1.288e4 -273.3 -272.6
passing xi=0.8 3.7288e4 3.738e4 8.455e4 8.506e4

(before the fix: omega_b was 1.0796e4 / 3.1317e4, i.e. 16 % low)

2. psimax was hardwired to the disabled placeholder 1e10. For real diverted g-files the field-line scan leaves the box just past the X-point, but synthetic equilibria without an X-point have psi rising smoothly to the box edge: the scan then grinds through thousands of vacuum-region surfaces (hours instead of seconds, since flint_for_Boozer also ignores its nstep argument) and labels the last box-confined surface s = 1 instead of the LCFS. convert_eqdsk_to_chartmap now accepts psimax (CGS, absolute) and passes it into efit_to_boozer.inp; default behaviour is unchanged.

Tests: Ampere check of I at the outermost stored surface against the g-file Ip, |I/G| ordering, and psimax .inp pass-through. Full python/tests/test_eqdsk_to_boozer_chartmap.py + test_eqdsk_base.py: 13 passed.

The converter wrote B_theta = G/q, reasoning "iota = I/G". That confuses
the covariant with the contravariant component ratio: iota is
B^theta/B^phi, while the covariant I(s) measures the enclosed toroidal
current (2*pi*I = mu0*I_tor). For a tokamak I is ~40x smaller than G/q,
and SIMPLE consumes B_theta as the covariant surface function
(h_theta = B_theta/Bmod), so the fabricated value inflated the parallel
field-line length factor (G + iota*I) and slowed every traced orbit's
parallel dynamics by O(1/q^2): 16 % too-low bounce frequencies on the
circular benchmark equilibrium (q = 2.26 at s_tor = 0.3).

Compute I(s) instead as the signed circulation of B_pol along the flux
surface, with B_pol = |grad psi|/R taken from the g-file psi map itself
(sqrt(B^2 - B_tor^2) from the Boozer splines cancels catastrophically on
inner surfaces). Verified against POTATO tracing the same EQDSK: bounce
and toroidal frequencies of trapped and passing 5 keV deuterons now agree
within 0.7 % (previously 16 % off).

Also expose psimax (poloidal boundary flux, CGS) through
convert_eqdsk_to_chartmap. Without it the flux-surface scan locates the
boundary purely by field-line integration, which for synthetic equilibria
without an X-point (psi rising to the box edge) runs to nsurfmax through
hours of vacuum-region surfaces and labels the wrong surface s = 1.

Tests: Ampere check of I at the outermost surface against the g-file Ip,
|I/G| ordering, and psimax .inp pass-through.
krystophny added a commit to itpplasma/benchmark-simple-potato that referenced this pull request Jul 16, 2026
Replace the R0 = 1.6 m circular equilibrium by a reactor-size case
(R0 = 6.2 m, a = 2.0 m, B0 = 5.3 T) so that both the 5 keV deuterium
benchmark particle and SIMPLE's default 3.5 MeV alpha are confined at
mid-radius. gen_circular_eqdsk.py writes the EQDSK with a poloidal flux
realizing q(r) = 1.5 + 2.5 (r/a)^2 exactly on the concentric-circle model.

Add two conversion tools usable for any axisymmetric g-file:
- tools/eqdsk_to_simple_chartmap.py: libneo EQDSK-to-Boozer converter plus
  SIMPLE regrid. Stops the converter's flux-surface scan at the g-file
  boundary flux (needed for equilibria without an X-point) and requires
  libneo with the corrected covariant B_theta (itpplasma/libneo#399,
  which fixes 16 % too-slow parallel dynamics from the earlier I = G/q).
- tools/eqdsk_to_vmec.py: VMEC++ fixed-boundary re-solve with pressure and
  q profile derived from the psi map; produces rung0/wout_circ.nc for
  SIMPLE's native VMEC input path.

Cross-code validation on the committed files (5 keV deuteron, s_tor = 0.3,
canonical frequency API vs POTATO on the same EQDSK): omega_b and
omega_phi agree within 0.7 % for trapped (xi = 0.3) and passing (xi = 0.8)
orbits; the VMEC variant agrees to 2.5 % in omega_b (force-balanced
re-solve, Shafranov shift). Checksums and the numbers are recorded in
rung0/README.md.

Drop the superseded small-machine chart maps, the nzeta expansion helper
(folded into fix_chartmap_for_simple.py), and the POTATO-generated
eqmagprofs.dat run product.
@krystophny
krystophny merged commit fe3f76e into main Jul 16, 2026
6 checks passed
@krystophny
krystophny deleted the fix/chartmap-covariant-btheta branch July 16, 2026 18:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant