Pvar-dissip-struct: velocity-dependent variational equations in C (+ Chandrasekhar Jacobians)#933
Pvar-dissip-struct: velocity-dependent variational equations in C (+ Chandrasekhar Jacobians)#933jobovy wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## feat/variational3d #933 +/- ##
=======================================================
- Coverage 99.92% 15.85% -84.08%
=======================================================
Files 225 225
Lines 36207 36319 +112
Branches 785 787 +2
=======================================================
- Hits 36180 5757 -30423
- Misses 27 30562 +30535 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
85497c1 to
fa982b7
Compare
|
Review addressed (branch reworked in place, force-pushed):
Batteries: #933 126 passed (the −1 vs before is exactly the deleted ctypes test), #934 128, #935 130 + test_noninertial 53; all physics numbers reproduce (phase-volume 1.59e-6/det M=0.677 for FDM; det M−1 ≤ 9.2e-11 for the rotating frame). |
fa982b7 to
862c9e5
Compare
|
Round-2 review addressed (amended in place):
|
…an + Chandrasekhar friction
Extend the 3D variational equations (Orbit.integrate_dxdv, phasedim==6) to
velocity-dependent (dissipative) forces: the variational Jacobian is now the
general J = [[0,I],[K + dF/dx, dF/dv]], with the dissipative blocks computed
in rectangular coordinates per force.
C interface:
- struct potentialArg gains RectDissipativeForceJacobian(t, q, jac_x, jac_v):
fills the row-major 3x3 blocks dF/d(x,y,z) and dF/d(vx,vy,vz). It exists
only for velocity-dependent forces, which in galpy's class hierarchy are
the DissipativeForce subclasses (including NonInertialFrameForce);
conservative potentials leave it NULL (NULL-initialized centrally in
init_potentialArgs, called by all parse_* functions).
- New NULL-safe aggregator calcRectDissipativeForceJacobian, which returns
exact zeros when no component provides the Jacobian, plus conservative-only
calcRforceConservative/calcphitorqueConservative: the cylindrical->Cartesian
conversion terms of the conservative Hessian K must not contain the
dissipative force (its dF/dx is already complete in rectangular form).
- evalRectDeriv_dxdv: threads (vR,vT,vz) into the force evaluators and
computes the deviation RHS as the single straight-line formula
d(dv)/dt = K.dx + Jx_dissip.dx + Jv_dissip.dv, with (Jx, Jv) from the
NULL-safe aggregator (zero-filled for purely conservative potentials) --
no per-case branching. The pure-conservative path is bit-identical to
before (verified byte-for-byte against a pre-change build for
MiyamotoNagai/LogHalo/DehnenBar x dopr54_c/rk4_c/dop853_c x generic-3D and
planar ICs x dense and canonical deviation seeds: 72/72 output arrays
byte-identical).
- ChandrasekharDynamicalFrictionForce.c: analytic rectangular Jacobian of
F = A(x,|v|) v with A = -amp lnLambda Xf(X) rho / v^3, covering all three
Coulomb-log configurations (constant; variable r-only branch GMvs<rhm;
variable r&v branch incl. d lnLambda/dv), the clamped sigma_r spline
(consistent sigma_r'=0 outside [minr,maxr]) and the r<minr zero gate. The
background-density gradient (not available analytically through the C
interface) is the single finite-differenced term (central,
h=1e-5 sqrt(1+r^2), documented).
Python plumbing:
- DissipativeForce gains hasC_dxdv3d=False default;
ChandrasekharDynamicalFrictionForce sets it to hasC;
FDMDynamicalFrictionForce explicitly overrides it back to False (its FDM
factor modifies the amplitude, so the inherited Jacobian does not apply).
- CompositePotential now aggregates hasC_dxdv3d from its components (it
aggregated hasC/hasC_dxdv/hasC_dens but not the new flag, which made
integrate_dxdv silently fall back to odeint for ALL list/composite inputs,
including purely conservative ones like MWPotential2014).
- The pure-Python integrate_dxdv methods ('odeint'/'dop853') raise a clear
NotImplementedError for dissipative forces instead of silently dropping
the dissipative Jacobian.
Tests (orbit-level only, following galpy's convention that C code is tested
through regular galpy orbit usage):
- FD-of-the-flow STM test: MWPotential2014 + Chandrasekhar friction
(GMs=0.008, rhm=0) on a decaying orbit, all 6 columns to <1e-4.
- Phase-volume law: det M(t) = exp(int tr(dF/dv) dt') to <1e-5 relative
along the orbit (measured 3.1e-6), with tr(dF/dv) computed by central
finite differences of the pure-Python forces (evaluateRforces/
evaluatephitorques/evaluatezforces with v=..., converted to Cartesian) --
a code path independent of the C-integrated STM it validates;
det M(t_end)~0.52<1 (friction contracts phase volume).
- Branch-coverage FD-of-flow tests (parametrized) for the other Jacobian
configurations, each selected through regular constructor options with a
non-vacuity guard that the branch condition genuinely holds along the
orbit: constant lnLambda (const_lnLambda=7); the rhm-based Coulomb log
(GMs/v^2 < rhm asserted along the orbit); the r < minr zero gate (orbit
entirely inside minr -- the force is discontinuous across minr, so the
flow derivative of a CROSSING orbit has a saltation term the gate's zero
Jacobian deliberately omits and FD-of-flow would genuinely disagree); and
the clamped sigma_r spline (orbit entirely beyond the sigmar grid,
sigma_r'=0). Mutation-validated: perturbing each branch's Jacobian term
in C fails exactly the corresponding configuration.
- Loud-failure tests for the Python methods and for dissipative forces
without the C Jacobian (forced-flag), plus a tripwire that the conftest
liouville3d (det(M)=1/symplecticity) registry contains no dissipative
forces, which fail those tests by construction.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
862c9e5 to
2e23404
Compare
|
Round-3 refinements (amended; #934/#935 deliberately NOT rebased pending agreement here):
Conservative path bit-identical (hash-equal vs base: |
Summary
Extends the 3D variational equations to velocity-dependent (dissipative) forces, per the design doc: the variational Jacobian becomes the general$J = [[0, I], [\partial F/\partial x,\ \partial F/\partial v]]$ (non-symmetric position block + velocity block), with the conservative case as the special case $\partial F/\partial v = 0$ .
C machinery
struct potentialArggainsRectForceJacobian(t, q, jac_x, jac_v, ...)filling the rectangular 3×3init_potentialArgs, verified used by every parser).evalRectDeriv_dxdvgeneralized tocalcRforceConservative/calcphitorqueConservativekeep friction out of the cylindrical→Cartesian K-assembly terms. Conservative path verified bit-identical (pre/post builds compared on MN/LogHalo/DehnenBar × 3 integrators).calcDensity(no density-gradient C interface; ~1e-8 accurate).Python gating
DissipativeForce.hasC_dxdv3d = Falsedefault; Chandrasekhar → True. FDMDynamicalFrictionForce explicitly overridden back to False — it subclasses Chandrasekhar and would otherwise silently use wrong Jacobians (hole caught and closed).NotImplementedErrorfor dissipative pots (the Python_EOM_dxdvis conservative-only) — loud failure instead of silently wrong results.Validation (all committed as tests)
Flagged numpy-path change (standing rule)
CompositePotentialnow aggregateshasC_dxdv3d(it didn't) — so conservative list inputs like MWPotential2014 previously fell back to odeint silently; they now take the C path (outputs differ 1e-11–4e-8 = odeint-vs-C, both correct). Note: #926 contains the same one-line fix independently — whichever merges second rebases and dedupes (trivial).Follow-ups (separate PRs)
FDM Jacobians; planar (4D) dissipative dxdv; NonInertialFrameForce.
🤖 Generated with Claude Code