Skip to content

Add full-orbit (FO) Boris pusher (orbit_model=7)#426

Merged
krystophny merged 2 commits into
mainfrom
feature/full-orbit-fo
Jun 22, 2026
Merged

Add full-orbit (FO) Boris pusher (orbit_model=7)#426
krystophny merged 2 commits into
mainfrom
feature/full-orbit-fo

Conversation

@krystophny

Copy link
Copy Markdown
Member

Risk tier

  • T3: physics, output behavior, coordinate convention

Correctness contract

Intended behavior change

Adds a new orbit model: orbit_model = 7 (ORBIT_FULL_ORBIT) selects a
gyro-resolved full-orbit (FO) Boris pusher, the ASCOT-style counterpart to the
guiding-center (GC) model. A charged particle advances by explicit Boris
drift-rotate-drift in Cartesian (x, v); field and geometry come from the
production Boozer field through the chartmap (B = curl A, tangent to the flux
surface), with the magnetic axis healed by a pseudo-Cartesian (X,Y,phi) chart.
Output z(1:5) is the guiding-centre reduction each step, so times_lost and
confined_fraction are written exactly as for GC.

Behavior that must not change

orbit_model = 0 (guiding center, the default) is untouched: the symplectic GC
path, its init, and its macrostep are unchanged. No existing test output moves.

Coordinate / unit conventions

Cartesian (x, v) in the scaled chartmap frame; logical chart u=(rho, theta_B,
phi_B) with rho = sqrt(s). CGS-style normalized velocity with the GC sqrt(2)
convention, matching the GC seed so both integrators start from the identical
particle. orbit_coord = 1 (Boozer) is required and enforced.

Numerical invariants

Energy is conserved to machine precision (the Boris rotation is exact for
constant B over a step; no electric field here). The only confinement loss is
the guiding-centre crossing s >= 1; a field-inversion non-convergence is a
numerical fault (fo_fault), reported but never counted as a loss.

Tests added

  • unit:
  • integration: test_fo_boris (energy bound < 1e-3, near-axis crossing,
    no spurious loss; passing/trapped/near-axis on the reactor-scale Boozer field)
  • system:
  • golden record:

Golden-record impact

  • unchanged

GC is the default and is untouched; FO is opt-in via orbit_model = 7.

Failure modes considered

  • Field-inversion non-convergence near a field-period seam: classified as a
    numerical fault (fo_fault), never a loss, so a mid-radius particle is not
    spuriously lost.
  • Near-axis singularity of the polar chart: healed by the pseudo-Cartesian
    (X,Y,phi) chart; the near-axis test orbit crosses s in [0.04, 0.07] with
    energy bounded.
  • Unsupported combinations (wall loss, collisions, orbit classification, or
    orbit_coord /= 1) stop at config validation with a clear message instead of
    running silently wrong.

Manual validation

Reactor-scale W7-X benchmark (separate run): FO confined fraction agrees with
ASCOT5 full orbit (0.894 vs 0.898 at 1 ms, 1000 alphas), and with GC within
marker statistics.

Verification

Before: there is no full-orbit model on main; orbit_model and the FO modules
do not exist.

$ git grep -l 'ORBIT_FULL_ORBIT\|orbit_fo_boris' origin/main -- src test
(no matches)

After: test_fo_boris passes and the fast suite is green.

$ make test TEST=fo_boris
1/1 Test #6: test_fo_boris ....................   Passed    6.01 sec

  passing   s band [0.5000,0.7700]  max|dE/E0|=2.22E-14  ierr_lost=0
  trapped   s band [0.4999,0.5894]  max|dE/E0|=1.11E-14  ierr_lost=0  drift=8.85E-03
  near-axis s band [0.0400,0.0708]  max|dE/E0|=8.88E-15  ierr_lost=0
  ALL FO-BORIS TESTS PASSED

$ make test-fast
100% tests passed, 0 tests failed out of 53

Relationship to #408

This is the working full-orbit Boris slice, extracted clean off main. The
experimental models on #408 (CPP Pauli, the adaptive RK45 ORBIT_CP6D_RK, the 6D
canonical CP/CPP, device/mock variants) stay on that branch; #408 rebases on top
once this merges.

Full orbit (FO) is the gyro-resolved Lorentz counterpart to the guiding-center
(GC) model, the ASCOT-style reference for it. A charged particle advances by an
explicit Boris drift-rotate-drift in Cartesian (x, v): the magnetic rotation is
exact per step, the kinetic metric is the identity, and the magnetic axis is an
ordinary point. Field and geometry come from the production Boozer field through
the chartmap: invert the Cartesian point to (rho, theta_B, phi_B), evaluate the
flux potential, and build B = curl A so B is tangent to the flux surface; the
axis is healed by a pseudo-Cartesian (X,Y,phi) chart. The only confinement loss
is the guiding-centre crossing s >= 1; a field-inversion non-convergence is a
numerical fault, reported (fo_fault) but never counted as a loss.

orbit_model = 7 (ORBIT_FULL_ORBIT) selects it; orbit_coord = 1 (Boozer) is
required. Wall loss, collisions, and orbit classification are not yet wired and
stop at config validation with a clear message.

New modules orbit_fo_field (B = curl A from field_can, chartmap-reparametrized)
and orbit_fo_boris (the pusher). diag_counters gains fo_loss / fo_fault.
test_fo_boris validates machine-exact energy conservation, the near-axis
crossing, and absence of spurious loss on the reactor-scale Boozer field.
@krystophny krystophny added tier/T3 physics or output behavior size/L review size up to 1000 changed lines labels Jun 22, 2026
The FO field assembly builds B = curl A through ref_coords%covariant_basis;
a transposed Jacobian (the libneo cart-spline chartmap bug) flips the field
while Boris still conserves energy, so the existing energy gates would not catch
it. Check covariant_basis directly against a finite difference of evaluate_cart
at a generic interior point; rel err 2e-10 with the fix, O(1) without it.
@krystophny

Copy link
Copy Markdown
Member Author

Depends on libneo #325 (fix transposed Cartesian Jacobian from chartmap covariant_basis, cart-spline charts). The FO field assembly builds B = curl A through ref_coords%covariant_basis; without that fix on libneo main, a fresh CI build of this branch falls back to libneo main and gets the transposed Jacobian, silently flipping the field. test_fo_boris now asserts the Jacobian convention directly (covariant_basis vs finite-difference of evaluate_cart), so this fails loudly instead of passing with wrong orbits. Merge libneo #325 before merging this.

@krystophny krystophny merged commit 36d97ef into main Jun 22, 2026
7 checks passed
@krystophny krystophny deleted the feature/full-orbit-fo branch June 22, 2026 21:18
krystophny added a commit that referenced this pull request Jun 22, 2026
Brings in the merged full-orbit (FO) Boris pusher (orbit_model=7,
orbit_fo_boris/orbit_fo_field, test_fo_boris). The four files both branches
touch (params, diag_counters, simple, simple_main) kept this branch's
experimental CP/CPP/RK code; the FO modules build alongside under distinct
names and test_fo_boris passes. ORBIT_FULL_ORBIT vs this branch's
ORBIT_CP6D_BORIS both claim model 7, so the field/dispatch dedup (adopt FO as
the canonical model 7, renumber the experimental models) is a follow-up.
krystophny added a commit that referenced this pull request Jun 25, 2026
…d symplectic FO (#429)

## Summary

Rebase-and-strip of the shelved shared-symplectic-core work (PR #407)
onto
current `main`. Keeps only the keep-worthy nucleus: the guiding-centre
(GC)
Gauss implicit-RK / Newton core, extracted into a new device-callable
module
`orbit_rk_core`. Everything CPP and symplectic-full-orbit is dropped.

### What lands
- New `src/orbit_rk_core.f90`: the GC Gauss-collocation residual,
analytic
Jacobian, a device-portable dense LU solve (`rk_solve`), and a
device-callable
Newton shell (`rk_gauss_newton`). Reduced to a single GC kernel; the
former
`MODEL_CPP` / `MODEL_FO_SYMPL` codes and their dispatch shells are gone.
- `src/orbit_symplectic.f90`: `f_rk_gauss` / `jac_rk_gauss` become thin
wrappers
over the shared core (`MODEL_GC`). The arithmetic is moved
byte-identically,
  so GC numerics do not change.
- `src/CMakeLists.txt`: compile `orbit_rk_core.f90` before
`orbit_symplectic.f90`.

### What is dropped (not merged)
- The CPP pusher (`orbit_cpp.f90`, `MODEL_CPP`) and its tests
  (`test_cpp_equals_gc_largestep`, `test_cpp_invariants`).
- The shelved symplectic / mocked-Boris full-orbit prototype
(`orbit_full.f90`,
`MODEL_FO_SYMPL`), its provider seam (`orbit_full_provider.f90`) and the
two
  mocks, and their tests (`test_fo_symplectic`, `test_full_orbit`,
  `test_orbit_model_dispatch`).
- The branch's `params.f90` / `simple_main.f90` `orbit_model` hunks:
`main`
already owns `orbit_model` (`ORBIT_GC=0`, `ORBIT_FULL_ORBIT=7`) and the
validated explicit Boris full-orbit pusher (PR #426), which this PR
leaves
  untouched.

## Why

`orbit_rk_core` is the single home for the GC Gauss step shared by the
CPU
integrator and (later) the OpenACC GPU kernel; it is written `!$acc
routine
seq`-ready with its own device-portable LU. CPP is retired; the
full-orbit path
is the explicit Cartesian Boris (`orbit_model=7`), not the shelved
symplectic
prototype.

## Verification

`make CONFIG=Fast`: build OK. `make test`: **100% tests passed, 0 failed
out of
77** (total 677.80 s). All 14 golden-record regression cases (albert,
albert_coils, boozer, boozer_ncsx, canonical, classifier,
classifier_combined,
classifier_fast, collision, meiss, meiss_coils, and the tokamak
variants)
pass, confirming the GC numerics are byte-identical after the
extraction.

Before (on `main`): the same 14 golden-record cases pass with the
inlined GC
residual/Jacobian. After (this branch): identical results with the
arithmetic
moved into `orbit_rk_core` -- a pure refactor, no numeric change.

## Follow-ups (separate PRs)

- Wire `rk_gauss_newton` / `rk_solve` onto the fortnum multiroot core in
place of
  `dgesv` for the CPU GC Newton; must preserve the verified GC numerics.
- The device (OpenACC) GC solver path uses fortnum's
reverse-communication
  `multiroot_step` (fortnum PR for the device-capable solver), keeping
  `orbit_rk_core` as the shared CPU+GPU home.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L review size up to 1000 changed lines tier/T3 physics or output behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant