Skip to content

Audit and cleanup: 6D CP/CPP pushers (correctness, the #417 loss-vs-nonconvergence bug, mu diagnostic, dead code) #419

Description

@krystophny

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 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_full boris_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

  • orbit_cpp_canonical flags a non-converged trapped-bounce step as ierr=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).
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions