You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Audit of the 6D classical-particle (CP) and classical-Pauli-particle (CPP) orbit pushers and their drivers. Most of the math is correct. There is one real correctness bug (structural non-convergence counted as a confinement loss, the caller-side completion of #417), one misleading magnetic-moment diagnostic that likely explains the reported bad mu conservation for the full particle, model-selection guidance to document, and a cleanup list. This issue collects everything so the cleanup can be done from it directly.
What is correct (verified by reading the code)
orbit_cpp_boris (modified Boris, the explicit large-step CPP pusher): symmetric macro-step (half drift, half mirror kick, exact magnetic rotation, half mirror kick, half drift); mirror force v -= 0.5*dt*(mu/mass)*gradB with correct sign and scaling, applied only when pauli=.true.; exact Boris/Rodrigues rotation; optional rotation-angle filter for large dt*Omega_c; v_perp = 0 slow-manifold seed; energy 0.5*m|v|^2 + mu|B|. Field push is consistent: B_cart = Jc B^ctr (contravariant), grad|B|_cart = Jc^{-T} d|B|/du (covariant). Frozen mu in the pusher vs a recomputed diagnostic mu is a correct, deliberate split.
orbit_cpp_canonical (implicit midpoint; CP, CPP_SYM, CPP_VAR): Hamiltonian H = |p - qc A|^2/(2m) + mu|B|, qc = charge/(c*ro0); symplectic-midpoint and variational (discrete Euler-Lagrange) residuals; analytic Jacobian (first-derivative form on spline charts to stay smooth through v_par -> 0); mu a fixed parameter. The integrator correctly flags a non-converged step as ierr=3 with an explicit comment that this is a structural root loss and must not be treated as a physical loss.
Plain Boris (orbit_fullboris_step_cart/boris_step_cyl): correct unmodified Boris (drift-kick-drift, exact rotation, Christoffel half-kicks in the curvilinear variant), no mirror force, as intended for the full charged particle.
Field provider: dBmod is the covariant gradient d|B|/du; the Cartesian push uses Jc^{-T} (correct for a gradient); the canonical models use dBmod directly in flux space (correct, no Jacobian needed there).
Real bug: CPP-midpoint non-convergence is counted as a confinement loss
The driver defeats that intent. In trace_orbit/macrostep, every nonzero ierr_orbit is collapsed to exit; the trajectory terminates early and the particle stops being counted as confined from that step, the same outcome as a genuine wall or boundary loss. There is no branch that distinguishes ierr=3 (structural, recoverable) from ierr=2 (radial boundary) or a real loss.
Consequence: a trapped particle that merely fails the midpoint solve at a bounce (the [orbit-6d-03] Fix CPP full-metric Newton Jacobian convergence #417 resonance) is spuriously counted as an alpha loss, biasing the confined fraction. This is exactly the failure mode the integrator comment warns about, occurring one level up in the caller.
Fix options: (a) for production large-step trapped tracing, prefer the explicit modified-Boris full-orbit pusher (ORBIT_CP6D_BORIS), which has no convergence floor and is regular through the axis; and/or (b) make the driver distinguish ierr=3 and recover (reduce dt for that macrostep, retry, or re-seed on the slow manifold) instead of terminating the trajectory.
Magnetic-moment diagnostic for the full particle (the reported bad-mu conservation)
For the full charged particle, mu = m v_perp^2 / (2|B|) is only an adiabatic invariant. At dt*Omega_c >> 1, plain Boris degrades it; that is expected, not a bug, and the mu-preserving remedy is the modified Boris (freeze mu as a parameter and add the mirror force).
set_mu_from_state is called only at initialization; state%mu is never refreshed after a Boris step. The carried state%mu is therefore stale (trivially constant), while a live diagnostic recomputes mu from the current state. These two are easy to confuse in post-processing.
The live recomputation evaluates |B| and the field direction at the start-of-step position, while the leapfrog evaluates the field at the half-drift point. That point/time inconsistency can show up as spurious mu drift even when the orbit itself is accurate.
Recommended: define a single, consistent mu diagnostic (same |B| and field direction as the push, ideally the gyro-averaged value), and either refresh state%mu after each step or document it as init-only. Then determine which of the three explains the observed degradation: genuine large-dt adiabatic breakdown, sampling inconsistency, or the stale state%mu. The first is cured by the modified Boris; the latter two are code fixes.
Model selection and misunderstandings to document
The 4D Pauli model (ORBIT_PAULI) is literally the guiding center (its own comment marks it "== GC, tautological"). The genuine 6D Pauli particle is ORBIT_CPP6D.
ORBIT_CPP6D (flux-canonical implicit midpoint) is singular at the magnetic axis and carries the [orbit-6d-03] Fix CPP full-metric Newton Jacobian convergence #417 trapped-bounce non-convergence. ORBIT_CP6D_BORIS (explicit modified Boris) is regular through the axis and has no convergence floor. For large-step, trapped, or near-axis full-orbit tracing, prefer the Boris pusher.
The large-step CPP scheme that is theoretically well-founded (modified Boris with the mirror force plus the rotation-angle filter; rigorously analyzed for finite time for the ordinary charged particle by Lubich and Shi, arXiv:2205.12895) is already implemented (orbit_cpp_boris) but marked "Experimental". It should be validated against the midpoint on trapped and near-axis cases and promoted, with the recommended production path documented.
Dead code, small mistakes, and cleanup
ORBIT_PAULI6D (Cartesian research model) error-stops in both the production validation and the main dispatcher; its constant and code path are effectively unreachable in production. Decide: gate clearly as test-only or remove.
ORBIT_BORIS is the module-local default of FullOrbitState, yet it is rejected by the production validation ("not available in production alpha-loss tracing"). Harmless, but an inconsistent default; set the default to a production model or document why not.
set_mu_from_state init-only / stale state%mu (above): fix or document.
cpp_boris_mu computes |B| two ways (the field-returned Bmod for the denominator and Bmod_of(Bvec) for the field-direction normalization). They agree to round-off by construction; collapse to one.
Pre-existing TODOs worth folding into the cleanup: orbit_symplectic.f90 ("this doesn't work well yet"; three "move coeff_rk_gauss to preprocessing"), diag_newton.f90 ("fix criterion for convergence"), simple.f90 ("Remove side effects"), simple_main.f90 ("Must be called in this order. TODO: Fix"), params.f90 (deprecated-alias handling).
Test gap: no test exercises orbit_full's boris_step_cart/boris_step_cyl directly (the CP-Boris tests target orbit_cpp_boris). Add coverage if orbit_full is kept, or consolidate.
Suggested acceptance criteria for the cleanup
Structural non-convergence (ierr=3) never maps to a confinement loss anywhere in the tracing path; it is recovered or retried.
A single, documented mu diagnostic with consistent field sampling; state%mu either refreshed each step or clearly marked init-only.
Orbit models are clearly partitioned into production vs test-only; unreachable models are gated or removed.
The modified-Boris full-orbit/CPP pusher is validated against the implicit midpoint on trapped and near-axis cases, and the recommended production path is documented.
Related: #417 (the integrator-side root-loss flag this completes on the caller side) and #418.
Summary
Audit of the 6D classical-particle (CP) and classical-Pauli-particle (CPP) orbit pushers and their drivers. Most of the math is correct. There is one real correctness bug (structural non-convergence counted as a confinement loss, the caller-side completion of #417), one misleading magnetic-moment diagnostic that likely explains the reported bad
muconservation for the full particle, model-selection guidance to document, and a cleanup list. This issue collects everything so the cleanup can be done from it directly.What is correct (verified by reading the code)
orbit_cpp_boris(modified Boris, the explicit large-step CPP pusher): symmetric macro-step (half drift, half mirror kick, exact magnetic rotation, half mirror kick, half drift); mirror forcev -= 0.5*dt*(mu/mass)*gradBwith correct sign and scaling, applied only whenpauli=.true.; exact Boris/Rodrigues rotation; optional rotation-angle filter for largedt*Omega_c;v_perp = 0slow-manifold seed; energy0.5*m|v|^2 + mu|B|. Field push is consistent:B_cart = Jc B^ctr(contravariant),grad|B|_cart = Jc^{-T} d|B|/du(covariant). Frozenmuin the pusher vs a recomputed diagnosticmuis a correct, deliberate split.orbit_cpp_canonical(implicit midpoint; CP, CPP_SYM, CPP_VAR): HamiltonianH = |p - qc A|^2/(2m) + mu|B|,qc = charge/(c*ro0); symplectic-midpoint and variational (discrete Euler-Lagrange) residuals; analytic Jacobian (first-derivative form on spline charts to stay smooth throughv_par -> 0);mua fixed parameter. The integrator correctly flags a non-converged step asierr=3with an explicit comment that this is a structural root loss and must not be treated as a physical loss.orbit_fullboris_step_cart/boris_step_cyl): correct unmodified Boris (drift-kick-drift, exact rotation, Christoffel half-kicks in the curvilinear variant), no mirror force, as intended for the full charged particle.dBmodis the covariant gradientd|B|/du; the Cartesian push usesJc^{-T}(correct for a gradient); the canonical models usedBmoddirectly in flux space (correct, no Jacobian needed there).Real bug: CPP-midpoint non-convergence is counted as a confinement loss
orbit_cpp_canonicalflags a non-converged trapped-bounce step asierr=3, with the explicit intent that this structural root loss is "never silently treated as a loss" (see [orbit-6d-03] Fix CPP full-metric Newton Jacobian convergence #417).trace_orbit/macrostep, every nonzeroierr_orbitis collapsed toexit; the trajectory terminates early and the particle stops being counted as confined from that step, the same outcome as a genuine wall or boundary loss. There is no branch that distinguishesierr=3(structural, recoverable) fromierr=2(radial boundary) or a real loss.ORBIT_CP6D_BORIS), which has no convergence floor and is regular through the axis; and/or (b) make the driver distinguishierr=3and recover (reducedtfor that macrostep, retry, or re-seed on the slow manifold) instead of terminating the trajectory.Magnetic-moment diagnostic for the full particle (the reported bad-
muconservation)mu = m v_perp^2 / (2|B|)is only an adiabatic invariant. Atdt*Omega_c >> 1, plain Boris degrades it; that is expected, not a bug, and themu-preserving remedy is the modified Boris (freezemuas a parameter and add the mirror force).set_mu_from_stateis called only at initialization;state%muis never refreshed after a Boris step. The carriedstate%muis therefore stale (trivially constant), while a live diagnostic recomputesmufrom the current state. These two are easy to confuse in post-processing.|B|and the field direction at the start-of-step position, while the leapfrog evaluates the field at the half-drift point. That point/time inconsistency can show up as spuriousmudrift even when the orbit itself is accurate.mudiagnostic (same|B|and field direction as the push, ideally the gyro-averaged value), and either refreshstate%muafter each step or document it as init-only. Then determine which of the three explains the observed degradation: genuine large-dtadiabatic breakdown, sampling inconsistency, or the stalestate%mu. The first is cured by the modified Boris; the latter two are code fixes.Model selection and misunderstandings to document
ORBIT_PAULI) is literally the guiding center (its own comment marks it "== GC, tautological"). The genuine 6D Pauli particle isORBIT_CPP6D.ORBIT_CPP6D(flux-canonical implicit midpoint) is singular at the magnetic axis and carries the [orbit-6d-03] Fix CPP full-metric Newton Jacobian convergence #417 trapped-bounce non-convergence.ORBIT_CP6D_BORIS(explicit modified Boris) is regular through the axis and has no convergence floor. For large-step, trapped, or near-axis full-orbit tracing, prefer the Boris pusher.orbit_cpp_boris) but marked "Experimental". It should be validated against the midpoint on trapped and near-axis cases and promoted, with the recommended production path documented.Dead code, small mistakes, and cleanup
ORBIT_PAULI6D(Cartesian research model) error-stops in both the production validation and the main dispatcher; its constant and code path are effectively unreachable in production. Decide: gate clearly as test-only or remove.ORBIT_BORISis the module-local default ofFullOrbitState, yet it is rejected by the production validation ("not available in production alpha-loss tracing"). Harmless, but an inconsistent default; set the default to a production model or document why not.set_mu_from_stateinit-only / stalestate%mu(above): fix or document.cpp_boris_mucomputes|B|two ways (the field-returnedBmodfor the denominator andBmod_of(Bvec)for the field-direction normalization). They agree to round-off by construction; collapse to one.orbit_symplectic.f90("this doesn't work well yet"; three "move coeff_rk_gauss to preprocessing"),diag_newton.f90("fix criterion for convergence"),simple.f90("Remove side effects"),simple_main.f90("Must be called in this order. TODO: Fix"),params.f90(deprecated-alias handling).orbit_full'sboris_step_cart/boris_step_cyldirectly (the CP-Boris tests targetorbit_cpp_boris). Add coverage iforbit_fullis kept, or consolidate.Suggested acceptance criteria for the cleanup
ierr=3) never maps to a confinement loss anywhere in the tracing path; it is recovered or retried.mudiagnostic with consistent field sampling;state%mueither refreshed each step or clearly marked init-only.Related: #417 (the integrator-side root-loss flag this completes on the caller side) and #418.