Skip to content

boozer chartmap: fix rho-grid fields and start sampling#360

Merged
krystophny merged 5 commits into
mainfrom
fix/chartmap-aphi-rho-abscissa
Jun 3, 2026
Merged

boozer chartmap: fix rho-grid fields and start sampling#360
krystophny merged 5 commits into
mainfrom
fix/chartmap-aphi-rho-abscissa

Conversation

@krystophny
Copy link
Copy Markdown
Member

@krystophny krystophny commented Jun 1, 2026

Summary

Fix the Boozer chartmap radial contract end to end.

Changes:

  • A_phi, B_theta, B_phi, and Bmod are read on the chartmap rho grid. Radial derivatives are converted to s = rho^2 by chain rule.
  • Chartmap files now carry and restore rmajor; both chartmap field consumers use boozer_chartmap_io.read_boozer_chartmap.
  • Bmod is read on the endpoint-included theta_field/zeta_field grid.
  • Chartmap startmode=1 now stores sampled starts in reference coordinates. The tracer returns integrator coordinates (s, theta_B, phi_B); chartmap zstart stores (rho, theta_B, phi_B), so sbeg=0.5 now writes rho=sqrt(0.5).
  • Chartmap generate_start_only now stops after writing corrected starts instead of tracing orbits.
  • Grid starts use the same conversion, and their linear index stride is fixed.
  • Duplicate chartmap diagnostics were removed. The synthetic test_chartmap_aphi_abscissa now covers reader metadata and the analytic A_phi(rho) chain-rule contract.
  • Chartmap tolerances were tightened to measured floors. The roundtrip Bmod floor is the current export resolution, about 7.90e-5; the orbit floor is about 1.68e-9.

Closes #358 and fixes the chartmap sbeg issue tracked in #359.

Stack

Base: main.

Merge order:

  1. Merge this PR first.
  2. Restack GVEC QA Boozer chartmap validation #334 after this PR. Do not merge GVEC QA Boozer chartmap validation #334 as it stands; its branch is based before several commits that are already on main and before this fix.

Already on main: #329, #332, #333, #338, #340, #346, #348, #355, and the Boozer flux-sign doc commit.

Verification

Test fails before the start-sampling fix

$ OMP_NUM_THREADS=1 ctest --test-dir build -R '^test_chartmap_startmode1$' --output-on-failure -j1
AssertionError: chartmap startmode=1 wrote s instead of rho
0% tests passed, 1 tests failed out of 1

Tests pass after the fix

$ cmake --build build --target simple.x -j2
[25/26] Linking Fortran executable simple.x
$ OMP_NUM_THREADS=1 ctest --test-dir build -R '^test_chartmap_startmode1$' --output-on-failure -j1
100% tests passed, 0 tests failed out of 1
Total Test time (real) =   3.96 sec
$ OMP_NUM_THREADS=1 ctest --test-dir build -R '^test_chartmap_aphi_abscissa$|^test_boozer_chartmap$|^test_chartmap_scaling$|^test_boozer_chartmap_roundtrip$|^test_chartmap_startmode1$' --output-on-failure -j1
100% tests passed, 0 tests failed out of 5
Total Test time (real) =   8.43 sec
$ OMP_NUM_THREADS=1 ctest --test-dir build -R '^test_chartmap_pipeline$|^test_chartmap_rz_consistency$|^test_coord_transform_roundtrip$' --output-on-failure -j1
100% tests passed, 0 tests failed out of 3
Total Test time (real) =  94.36 sec
$ OMP_NUM_THREADS=1 ctest --test-dir build -R '^test_e2e_boozer_chartmap$' --output-on-failure -j1
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 102.43 sec
$ check-writing-slop.py test/tests/test_chartmap_startmode1.py
PASS: no writing-slop candidates at threshold medium

External W7X/GVEC reruns

Run with OMP_NUM_THREADS=1 using the rebased fix/chartmap-aphi-rho-abscissa build. These cases do not use generate_start_only, so they were not rerun after cf3ff9e.

W7X/vmec/simple-s1-p4k:
  final confined_fraction.dat line:
  1.0000000000000000E-003  0.67993164062500000  1.4648437500000000E-003  4096

W7X/vmec2gvec/simple-f1-n50/s316-p1k:
  final confined_fraction.dat line:
  9.9999999999999980E-004  0.64746093750000000  1.0742187500000000E-002  1024
  start.dat radial min=max=0.56213877290220782 = sqrt(0.316)

W7X/vmec2gvec/simple-f1-n50/s317-p1k:
  final confined_fraction.dat line:
  9.9999999999999980E-004  0.64843750000000000  4.8828125000000000E-003  1024
  start.dat radial min=max=0.5630275304103699 = sqrt(0.317)

The Boozer chartmap stores A_phi, B_theta, B_phi against the file's rho
grid, but splint_boozer_coord read A_phi off a synthetic uniform-s grid
while reading B_theta/B_phi/Bmod on rho. rho is uniform, so s = rho^2 is
not, so the mismatched abscissa corrupted dA_phi/ds and therefore
iota = -dA_phi_ds/dA_theta_ds, shifting the trapped/passing boundary for
GVEC-exported chartmaps.

Read A_phi on the rho grid and chain-rule its s-derivatives, matching the
existing Bmod/B_theta treatment in splint_boozer_coord; gate by
aphi_over_rho so the VMEC-derived direct Boozer path keeps A_phi on VMEC's
native uniform-s grid. Sample A_phi natively on the rho grid in
compute_boozer_data (aphi_rho) and write that from export_boozer_chartmap,
so the exported profile shares the abscissa of B_theta/B_phi without
resampling.

Add test_chartmap_aphi_abscissa: analytic A_phi nonlinear in s, fails on
the old uniform-s reader, passes now. Document the profile contract in
DOC/coordinates-and-fields.md.
The chartmap A_phi branch in splint_boozer_coord returned d3A_phi/ds3 = 0.
It is unused by the Boozer symplectic path today, but the other s-derivatives
are chain-ruled from rho, so complete the third one too:

  f'''(s) = g'''(rho) rho'^3 + 3 g''(rho) rho' rho'' + g'(rho) rho''',
  rho = sqrt(s),  rho' = 1/(2 rho),  rho'' = -1/(4 rho^3),  rho''' = 3/(8 rho^5).

Extend test_chartmap_aphi_abscissa to check d2A_phi/ds2 and d3A_phi/ds3
against the analytic profile; both match to ~1e-9 relative (a wrong term
would show order-unity error since d3 is O(100) at the test points).
…ify readers

A VMEC-free Boozer chartmap run diverged from the equivalent VMEC run in
two ways that Robert reported.

rmajor was never restored on the chartmap path. load_boozer_from_chartmap
set nper but not new_vmec_stuff_mod::rmajor, so stevvo returned R0 = 1 m and
params_init produced dtaumin about ten times too small (the reported
dtaumin/ntau 2.45/528 against the VMEC 24.90/52). export_boozer_chartmap now
writes the rmajor attribute and both readers restore it.

boozer_chartmap_field_t read Bmod on the endpoint-excluded geometry grid
(theta/zeta) instead of the endpoint-included field grid
(theta_field/zeta_field), so its periodic spline period was one cell short.
It now reads the field grid and spans the full 2*pi and 2*pi/nfp period.

The two chartmap readers (boozer_chartmap_field_t and
load_boozer_from_chartmap) duplicated the whole NetCDF parse and disagreed on
scaling. Both now go through one parser, boozer_chartmap_io.read_boozer_chartmap,
and apply vmec_B_scale/vmec_RZ_scale consistently. Dead read_3d_reordered and
check_nc helpers are removed.

The reported ~0.8% bmin/bmax gap is not a conversion defect: the Boozer
reconstruction of |B| matches raw VMEC to ~1e-6 on a flux surface and along a
field line, and for the same equilibrium the VMEC and chartmap runs give
identical confined fractions. The gap comes from comparing different field
sources, not from the chartmap code.

Tests: unit test_boozer_chartmap_io (field-grid dims, periodic spans, rmajor),
regression test_boozer_vmec_bfield_match (|B| extrema match VMEC), and
test_e2e_boozer_chartmap now asserts dtaumin/ntau agree between the two runs.

## Verification

Before (chartmap run, rmajor unset):
  tau: 1294.87  dtaumin=2.452  ntau=528

After (rmajor restored from file):
  tau: 1294.87  dtaumin=24.901 ntau=52   (== VMEC run)

  test_boozer_chartmap_io: PASS
  test_boozer_vmec_bfield_match: surface bmax rel 3.3e-6, bmin 9.8e-7;
    field-line bmax 1.6e-7, bmin 2.3e-6; PASS
  test_boozer_chartmap_roundtrip: Bmod 7.9e-5, orbit < 1e-6; PASS
  test_e2e_boozer_chartmap: QA and NCSX PASS, dtaumin/ntau match
  21/21 chartmap+field tests, 16/16 unit tests: PASS

Pre-existing, unrelated: golden_record_classifier_fast diverges from main on
this branch independently of this change (identical failure with these edits
stashed). Its VMEC-direct A_phi path is unchanged here; the divergence tracks
the NetCDF/HDF5 toolchain commit and needs a maintainer golden-record review.
Chartmap Boozer startmode=1 samples the starting field line in integrator coordinates (s, theta_B, phi_B) but stores zstart in reference coordinates. Convert those sampled starts with integ_to_ref before writing start.dat on the chartmap path, so sbeg remains flux s while chartmap reference rho is sqrt(s). Apply the same conversion to grid starts and fix the grid linear index stride.

Fold the parser-only chartmap unit test into the analytic A_phi/rho test, drop duplicate slow diagnostics, and tighten chartmap tolerances to the measured interpolation floors. The startmode regression uses sbeg=0.5 to catch s-vs-rho confusion directly.
@krystophny krystophny force-pushed the fix/chartmap-aphi-rho-abscissa branch from 4ea46a0 to b77fe0e Compare June 3, 2026 17:31
@krystophny krystophny changed the title boozer chartmap: tabulate A_phi on the rho grid like B_theta/B_phi boozer chartmap: fix rho-grid fields and start sampling Jun 3, 2026
@krystophny krystophny merged commit 171c998 into main Jun 3, 2026
7 checks passed
@krystophny krystophny deleted the fix/chartmap-aphi-rho-abscissa branch June 3, 2026 19:54
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.

Boozer chartmap: A_phi interpolated on a uniform-s grid instead of the file rho grid

1 participant