Skip to content

GVEC QA Boozer chartmap validation#334

Merged
krystophny merged 25 commits into
mainfrom
feature/boozer-chartmap-gvec-qa
Jun 5, 2026
Merged

GVEC QA Boozer chartmap validation#334
krystophny merged 25 commits into
mainfrom
feature/boozer-chartmap-gvec-qa

Conversation

@krystophny

@krystophny krystophny commented Mar 28, 2026

Copy link
Copy Markdown
Member

Summary

GVEC QA branch for Boozer chartmap validation after #360. The branch is now restacked on
main at 171c998 and keeps the remaining GVEC converter, QA, fixture, and
documentation work.

Current contracts in this branch:

  • Chartmap A_phi is tabulated on the file rho grid and chain-ruled to s on readback.
  • Chartmap export and the GVEC converter write rmajor; the reader restores it before
    parameter initialization so VMEC and chartmap runs use the same dtaumin and ntau.
  • The e2e chartmap test checks the exported rmajor and compares the VMEC/chartmap
    microstep directly.
  • The optional GVEC QA CTest is registered only when the configured Python can import
    gvec.
  • The older broad field-equivalence tests were removed. The remaining tests check smaller
    contracts: reader metadata, A_phi abscissa, start-mode sampling, scaling, roundtrip,
    and selected e2e equilibria.

Stack

Merge order is now:

  1. boozer chartmap: fix rho-grid fields and start sampling #360, already merged into main.
  2. GVEC QA Boozer chartmap validation #334, this branch, after review and green checks.

Already on main before this branch:

Verification

Failing before data refresh:

OMP_NUM_THREADS=1 ctest --test-dir build -R '^(test_chartmap_aphi_abscissa|generate_test_boozer_chartmap_data|test_boozer_chartmap|test_chartmap_startmode1|test_boozer_chartmap_roundtrip|test_e2e_boozer_chartmap|test_figure8_boozer_chartmap|test_coord_transform_roundtrip)$' --output-on-failure -j1
88% tests passed, 1 tests failed out of 8
test_figure8_boozer_chartmap failed because the external figure8 start.dat was a Git LFS pointer

Passing after pulling the external figure8 LFS files:

python3 -m py_compile test/tests/test_e2e_boozer_chartmap.py test/tests/boozer_chartmap_artifacts.py tools/gvec_to_boozer_chartmap.py test/tests/run_gvec_qa_roundtrip.py test/tests/test_figure8_boozer_chartmap.py
$HOME/code/prompts/scripts/check-writing-slop.py DOC/coordinates-and-fields.md
PASS: no writing-slop candidates at threshold medium
cmake -S . -B build -G Ninja
-- SIMPLE version: v1.5.0-68-g4c662b7
-- gvec Python package not found; skipping GVEC chartmap QA test
-- Configuring done
-- Generating done
cmake --build build -j2
[153/153] Linking Fortran executable test/tests/orbit/test_orbit.x
OMP_NUM_THREADS=1 ctest --test-dir build -R '^(test_chartmap_aphi_abscissa|generate_test_boozer_chartmap_data|test_boozer_chartmap|test_chartmap_startmode1|test_boozer_chartmap_roundtrip|test_figure8_boozer_chartmap|test_coord_transform_roundtrip)$' --output-on-failure -j1
100% tests passed, 7 tests passed out of 7
Total Test time (real) = 20.58 sec
OMP_NUM_THREADS=1 ctest --test-dir build -R '^test_figure8_boozer_chartmap$' --output-on-failure -j1
100% tests passed, 1 tests passed out of 1
Total Test time (real) = 5.98 sec

The long e2e test passed before the external LFS-pointer failure stopped the first
focused run:

test_e2e_boozer_chartmap ... Passed 420.24 sec

The default build Python on this machine does not provide gvec, so
test_boozer_chartmap_gvec_qa is not registered in this local CTest configuration.

Base automatically changed from feature/boozer-chartmap-realcart to main March 28, 2026 17:38
@krystophny krystophny force-pushed the feature/boozer-chartmap-gvec-qa branch from 3a42c9b to fbd730d Compare March 28, 2026 18:07
@krystophny krystophny force-pushed the feature/boozer-chartmap-gvec-qa branch from 08e0b6d to 4b1c23a Compare April 14, 2026 20:56
@krystophny krystophny enabled auto-merge (squash) April 14, 2026 21:41
krystophny added a commit that referenced this pull request Jun 3, 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 #334 after this PR. Do not merge #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)
```
@krystophny krystophny changed the title GVEC QA Boozer chartmap validation (restack after #360) GVEC QA Boozer chartmap validation Jun 3, 2026
@Rykath

Rykath commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

From my side this looks good! 🎉

Comparison on W7-X with 1% beta at s=0.25 with 16k particles:

image
Name Passing Trapped Total
gvec/f1-r99-t33-z180-d0604/s25-p16k 0.671204 0.00921631 0.68042
gvec/f1-r99-t33-z180-d0604-pol/s25-p16k 0.671204 0.00970459 0.680908
vmec/s25-p16k-d0604 0.663147 0.00378418 0.666931
vmec+gvec/f1-r99-t33-z180-d0604/s25-p16k 0.666077 0.00860596 0.674683
vmec+chartmap/s25-p16k-d0604 0.663757 0.0045166 0.668274

The remaining differences are most likely due to actual differences in the equilibrium (representation).

The figure-8 benchmark requires external QUASR data fetched via
GITLAB_ACCESS_TOKEN. When the token or data is unavailable (e.g. fork
PRs), exit with code 77 so CTest marks the test as skipped instead of
failed.
The figure-8 benchmark no longer depends on private GitLab data.
Instead it downloads the QUASR surface (ID 112714) from the public
Flatiron API, builds the boundary and chartmap via GVEC on the fly,
and caches the result keyed by script content hash.

Static test inputs (simple.in, start.dat) and the golden signature
reference are committed to test/test_data/figure8/.

The CI workflow no longer needs the GITLAB_ACCESS_TOKEN-gated
figure-8 fetch step.
The GVEC solver is not bit-reproducible across environments (different
BLAS, FP ordering), so the figure-8 golden record generated locally
never matches what CI produces from a fresh GVEC solve.
@krystophny krystophny merged commit 968a578 into main Jun 5, 2026
7 checks passed
@krystophny krystophny deleted the feature/boozer-chartmap-gvec-qa branch June 5, 2026 10:47
field_input = 'boozer_chartmap.nc' ! Boozer chartmap NetCDF file for field data
coord_input = 'boozer_chartmap.nc' ! same file for reference coordinates
isw_field_type = 2 ! Boozer field type
startmode = 2 ! particle coordinates given in chartmap Boozer space

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I know, this comment isn't quite correct. It would also be preferable if the example didn't force startmode = 2, which requires a start.dat file, right?

Comment thread README.md

Key differences from VMEC mode:
- Both `field_input` and `coord_input` reference the same chartmap NetCDF file
- `startmode = 2` means particle coordinates are in chartmap Boozer space

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, not quite right.

@krystophny

Copy link
Copy Markdown
Member Author

@Rykath your review comments here are now addressed in #368:

  • examples/simple_chartmap.in: dropped the forced startmode = 2. You were right — the comment was wrong and it required a start.dat. The example now relies on the default startmode = 1, which samples the sbeg = 0.3 flux surface and needs no start.dat.
  • README.md: removed the incorrect "startmode = 2 means particle coordinates are in chartmap Boozer space" bullet (startmode is field-type independent).
  • boozer_chartmap_io.f90 rmajor: dropped legacy support entirely. rmajor is now a required attribute (removed has_rmajor and the 0.0 default), so there is no bogus default.
  • boozer_chartmap_io.f90 Bmod: dropped the geometry-grid fallback; the endpoint-included theta_field/zeta_field grid is now mandatory.

map2disc-based chartmaps are unaffected: they go through the libneo coordinate reader, not read_boozer_chartmap. The C test generator was updated to emit the current format.

krystophny added a commit that referenced this pull request Jun 8, 2026
)

Follow-up to the review comments on #334 (cc @Rykath — this is now
changed).

## Risk tier

- [x] T2: local numerical logic

## Correctness contract

### Intended behavior change
- `read_boozer_chartmap` no longer supports legacy chartmap files. It
now requires:
- the `rmajor` global attribute (previously optional via a `has_rmajor`
flag that defaulted `rmajor` to `0.0`), and
- the endpoint-included `theta_field`/`zeta_field` Bmod grid (previously
fell back to the geometry grid).
GVEC-exported chartmaps (`tools/gvec_to_boozer_chartmap.py`) always
write both, so current files are unaffected. Pre-converter legacy files
now abort with a clear NetCDF error instead of being silently accepted
with `rmajor = 0`.

### Behavior that must not change
- map2disc-based chartmaps: unaffected. They are read through the libneo
coordinate reader (`make_chartmap_coordinate_system`), not
`read_boozer_chartmap`.
- Field values, scaling, and the symplectic/Boozer paths for valid
current-format files.

### Coordinate / unit conventions
- Unchanged. Bmod stays on the endpoint-included field grid spanning the
full 2*pi (poloidal) and 2*pi/nfp (toroidal) period.

### Numerical invariants
- Unchanged.

## Tests added
- unit: updated `generate_test_boozer_chartmap.c` to emit the current
format (`rmajor` + field-grid Bmod); updated
`test_chartmap_aphi_abscissa.f90` (the `rmajor`-present check is now
covered by mandatory read + value assertion).

## Golden-record impact
- [x] unchanged

## Failure modes considered
- Legacy file fed to the strict reader: now aborts with `NetCDF error at
att rmajor` / `inq_dim theta_field`, by design.

## Manual validation
Also addresses the two doc comments on #334:
- `examples/simple_chartmap.in`: dropped the forced `startmode = 2` (the
comment was incorrect, and it required a `start.dat`). The default
`startmode = 1` samples the `sbeg` flux surface and needs no
`start.dat`.
- `README.md`: removed the incorrect "`startmode = 2` means particle
coordinates are in chartmap Boozer space" bullet (startmode is
field-type independent).

## Verification

Failing before (strict reader against the old legacy-format test
generator):

```
read_boozer_chartmap: NetCDF error at att rmajor: NetCDF: Attribute not found
ERROR STOP read_boozer_chartmap failed
0% tests passed, 1 tests failed out of 1
	 20 - test_boozer_chartmap (Failed)
```

Passing after (generator updated to current format):

```
1/5 Test #18: test_chartmap_aphi_abscissa ......   Passed
2/5 Test #20: test_boozer_chartmap .............   Passed
3/5 Test #21: test_chartmap_startmode1 .........   Passed
4/5 Test #22: test_chartmap_scaling ............   Passed
5/5 Test #23: test_boozer_chartmap_roundtrip ...   Passed
100% tests passed, 0 tests failed out of 5
```

Pre-existing, unrelated to this PR: the `*_map2disc` chartmap tests fail
on a clean `main` in this environment because the optional libneo Python
binding `_efit_to_boozer` is not built (`ModuleNotFoundError: No module
named '_efit_to_boozer'`). This PR introduces no new failures relative
to that baseline.
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.

2 participants