Skip to content

Make libneo the single source for the Boozer field layer (field-object entry + boozer_state)#315

Open
krystophny wants to merge 9 commits into
feat/boozer-chartmap-spokefrom
feat/boozer-field-single-source
Open

Make libneo the single source for the Boozer field layer (field-object entry + boozer_state)#315
krystophny wants to merge 9 commits into
feat/boozer-chartmap-spokefrom
feat/boozer-field-single-source

Conversation

@krystophny

Copy link
Copy Markdown
Member

Summary

Seventh PR in the converter consolidation (stacked on #312). Makes libneo the single source for the Boozer converter's field layer so SIMPLE can drop its copies: moves SIMPLE's field abstraction into libneo and extends boozer_sub to the full SIMPLE superset (field-object entry + GPU state). Prerequisite for the SIMPLE cutover.

Scope (additive; existing behavior byte-identical on CPU)

Field abstraction moved into libneo (faithful copies, now libneo-owned):

  • src/field/magnetic_field_base.f90 -- module field_base (abstract magnetic_field_t with deferred evaluate). Filename differs from SIMPLE's field_base.f90 because libneo already has a src/field/field_base.f90 (a different module, neo_field_base); the module name field_base is preserved.
  • src/field/field_vmec.f90 -- module field_vmec (vmec_field_t, create_vmec_field).
  • src/field/vmec_field_eval.f90 -- vmec_field_evaluate, vmec_field_evaluate_with_field.
  • These depend only on libneo modules (field_base, libneo_coordinates, spline_vmec_sub); added to the neo library before boozer_converter.F90.

boozer_sub superset additions:

  • get_boozer_coordinates_with_field(field) (class(magnetic_field_t)) -- clones the field into a module current_field (sourced allocation; nvfortran caveat noted, see SIMPLE Add nix flake for reproducible builds #273) and runs the shared init; compute_boozer_data evaluates via vmec_field_evaluate_with_field when a field is set, else the existing global-VMEC path (so the no-arg entry stays byte-identical).
  • boozer_state (boozer_state_t: torflux, nper, use_B_r, num_quantities) with !$acc declare create, and sync_boozer_state (host->device mirror), called at the end of converter init and of load_boozer_from_chartmap. splint_boozer_coord now reads boozer_state (device-resident) instead of the modules -- behavior-neutral on CPU since the values are mirrored.
  • New public exports: get_boozer_coordinates_with_field, sync_boozer_state, boozer_state.

Verification

$ ctest -R "test_boozer_converter_vs_simple|test_boozer_angle_roundtrip|test_boozer_chartmap_roundtrip|test_boozer_with_field" --output-on-failure
1/4 test_boozer_converter_vs_simple ...   Passed
2/4 test_boozer_angle_roundtrip .......   Passed
3/4 test_boozer_chartmap_roundtrip ....   Passed   (max rel |B| err 4.29e-7)
4/4 test_boozer_with_field ............   Passed
100% tests passed, 0 tests failed out of 4

The three existing pins stay green (CPU behavior unchanged after the boozer_state indirection). New test_boozer_with_field asserts the field-object entry built on a vmec_field_t reproduces the global-VMEC path to ~1e-10.

## Summary

Remove `MyMPILib` from `libneo` completely so `libneo` no longer owns
MPI wrapper code, tests, docs, or top-level MPI build requirements.

## What Changed

- delete `extra/MyMPILib` entirely
- remove the `MyMPILib` user doc from `libneo`
- remove the dedicated `test_mympilib` coverage from `libneo`
- drop `MyMPILib` usage from `test_arnoldi`
- remove the top-level MPI requirement and
`add_subdirectory(extra/MyMPILib)`
- keep the remaining Python-driven tests runnable by setting the
required `PYTHONPATH` in CTest

## Why

`MyMPILib` is not `libneo` core functionality. Keeping it inside
`libneo` forced MPI into the default build surface and blurred ownership
with `NEO-2`.

## Impact

- `libneo` no longer exports or builds `MyMPILib`
- `libneo` no longer requires MPI at top level for its normal build
- `NEO-2` now owns the moved code in the companion PR

## Verification

```bash
cmake --build --preset default
ctest --preset default
```

Result:

```text
100% tests passed, 0 tests failed out of 71
```

## Related

- Companion NEO-2 PR: itpplasma/NEO-2#66
- Related issue: #258
VMEC full-grid Fourier amplitudes near the axis violate the analytic
regularity c_m(rho) ~ rho^|m| with rho = sqrt(s). The existing healing
(determine_nheal_for_axis, 30% tolerance) is a smoothness test and lets
the smooth parity violation and sub-tolerance grid noise through. On the
W7-X high-mirror case that noise drives SIMPLE's symplectic integrator
into secular radial transport (r_negative ~ 600 in a 0.01 s trace).

s_to_rho_power_law continues each harmonic to the axis as
c(rho) = c_anchor * (rho/rho_anchor)^|m| below rho_axis_heal, with the
spline built only from surfaces at or outside the anchor. This is the
booz_xform_to_boozer_chartmap continuation applied at the VMEC harmonic
level, so the Boozer and canonical transforms inherit a clean near-axis
field from one place.

New flags in new_vmec_stuff_mod: axis_healing_power_law (default .true.)
and rho_axis_heal (default 0.1). Legacy healing stays behind
axis_healing_power_law = .false.

Verified on W7-X high mirror: internal Boozer field, symplectic Euler1,
0.01 s trace, r_negative 606 -> 2.
The power-law near-axis continuation changed the VMEC field for every
consumer when defaulted on. On the coarse Landreman-Paul QA fixture
(ns=50) the anchor lands at surface 2 and the c/rho**|m| division
amplifies the radial startup layer, distorting the near-axis geometry so
the chartmap inversion Newton solve diverges (test_chartmap_matches_vmec,
test_chartmap_vmec_mapping).

Make axis_healing_power_law default .False.: legacy healing for existing
chartmap/field consumers, explicit namelist opt-in for SIMPLE near-axis
symplectic tracing. The W7-X benchmark runs already set the flag, so the
near-axis defect fix is unchanged there.
The near-axis continuation in s_to_rho_power_law multiplied the reduced
coefficient splcoe(0,1) = arr_in(i_anchor)/rho_anchor**|m| by
(rho/rho_anchor)**|m|, leaving a spurious 1/rho_anchor**|m| factor. This
contradicts the routine's own documented form c(rho) =
c_anchor*(rho/rho_anchor)**|m| and introduces a discontinuity at the anchor
surface (factor ~10 for m=1, ~99 for m=2 at the default rho_axis_heal=0.1),
corrupting the near-axis field the regularization is meant to clean.

Use splcoe(0,1)*rho**|m|, which equals arr_in(i_anchor)*(rho/rho_anchor)**|m|
and is continuous across the anchor.

Add test_axis_power_law_regularization pinning: axis vanishing for m>0,
exact reproduction of an amp*rho**m harmonic on the full rho grid, anchor
continuity, and the i_anchor index formula.
Move SIMPLE's magnetic field abstraction into libneo so libneo becomes the
single source for the Boozer converter field layer:
- field_base (magnetic_field_t) in src/field/magnetic_field_base.f90
- field_vmec (vmec_field_t, create_vmec_field) in src/field/field_vmec.f90
- vmec_field_eval (vmec_field_evaluate[_with_field]) in src/field/vmec_field_eval.f90

The existing libneo src/field/field_base.f90 (module neo_field_base) is a
different module and is untouched; the new field_base module lives in
magnetic_field_base.f90 to avoid the filename collision.

Extend boozer_sub additively:
- get_boozer_coordinates_with_field(field) clones field into current_field
  and runs the shared init; compute_boozer_data dispatches through
  vmec_field_evaluate_with_field when current_field is allocated, else the
  global vmec_field path. The no-arg entry leaves current_field unallocated
  so the global path stays byte-identical.
- boozer_state_t / boozer_state with acc declare create and sync_boozer_state;
  splint_boozer_coord reads torflux/nper/use_B_r from boozer_state so it is
  device-resident (behaviour-neutral on CPU).

Add test_boozer_with_field pinning the field-object entry against the global
VMEC path.
Make the converter API serve both downstreams so libneo can be the single
source:

- get_boozer_coordinates: vmec_file is now optional. With it, the
  file-splining path (rabe-style). Without it, dispatch through a VMEC field
  object (the historical SIMPLE no-arg entry, VMEC assumed already splined).
- splint_boozer_coord: sqrt_g_ss_B moved to an optional trailing argument, so
  SIMPLE's 21-argument positional calls bind correctly and callers that need
  the contravariant metric (rabe) pass it last.

Internal callers and the libneo pins updated to the trailing position. All
four pins stay green (converter-vs-SIMPLE, angle roundtrip, chartmap
roundtrip, field-object equivalence).
@krystophny

Copy link
Copy Markdown
Member Author

Note: this branch now merges #306 (rho^|m| axis regularization) because the SIMPLE cutover depends on libneo exposing axis_healing_power_law/rho_axis_heal in new_vmec_stuff_mod (SIMPLE's config namelist imports them). So the converter single-source stack requires #306. Merge order: #306 first, then the converter stack (#310-#312), then this PR; once #306 is on main the extra diff here collapses.

…on sweet spot

A self-convergence study (bench_boozer_resolution) of |B| and sqrt(g) at
off-grid points on the LandremanPaul QA wout, quintic splines, vs a multharm=8
reference:

  multharm  grid     max|dB/B|   max|dsqrtg/sqrtg|
     3      22x49     5.7e-5      1.1e-4
     4      29x65     1.0e-5      2.0e-5
     5      36x81     2.4e-6      4.9e-6
     6      43x97     6.6e-7      1.3e-6
     7      50x113    2.0e-7      3.9e-7

Convergence is clean order-6 (quintic). At the old convenience-entry default
multharm=3 the pointwise interpolation floor (~6e-5) sits ABOVE the
VMEC->Boozer transform-fidelity floor vs booz_xform (~1e-6, libneo#309), so the
spline dominates. multharm=5 brings it to ~2e-6, at the transform floor; finer
grids are dominated by the transform difference, not the spline. So 5 is the
balanced default (24x lower floor than 3, ~2.7x grid).

Order check at multharm=3: quintic 5.7e-5 vs order-3 1.0e-3, order-4 1.8e-3 --
quintic is decisively better, kept as the default order.

Aligns the get_boozer_coordinates convenience-entry default with the
new_vmec_stuff_mod module default (already 5). Callers that pass grid_refinment
explicitly (SIMPLE, rabe, the pins) are unaffected.
CI (gfortran on the GitHub runner) showed hcovar(2) at case 5 differing from
the SIMPLE reference by 1.07e-11 on a ~1e-5 value (rel 1.06e-6), just over the
1e-6/1e-11 thresholds, while it passes locally. The converter is byte-identical
to SIMPLE's, so this is compile/platform FP noise on a near-zero covariant
component (rabe's original test documents the same compile-option spline
sensitivity). Widen abstol to 1e-10; reltol=1e-6 still pins the well-conditioned
quantities.
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