progenax’s value proposition is that a differentiable forward model turns cluster
inference into information geometry: because every initial condition is jax.grad-able,
the Fisher matrix is computable and forecasts what a survey can and cannot measure
Cartwright & Whitworth, 2004 (see the science-demos throughline).
That guarantee has a sharp failure mode. The Fisher information is
with . A silently zeroed
column of makes that parameter look infinitely well-constrained, or makes
rank-deficient — a confident wrong uncertainty, with no error raised. A NaN at
least crashes loudly; a zeroed gradient does not. Gradient integrity is Fisher
integrity.
This page is the living status of a systematic audit that, for every public sampling and
physics entry point, measures the autodiff gradient against a central finite difference and
classifies it. Test gate: tests/validation/test_grad_audit.py (parametrised over
tests/validation/grad_audit/registry.py); numbers regenerated by
scripts/audit_gradients.py → validation/data/grad_audit_results.json (this page cites
that file — every ratio below is a real measured number).
How a case is classified¶
Each registry case declares an expect class and the engine computes the status — it is
never hand-set:
| Passes when | Meaning |
|---|---|---|
| finite and and | the gradient flows and matches finite differences (the silent-zero guard catches a blocked gradient) |
| and | the quantity genuinely does not depend on the parameter (pinned: a re-appeared gradient fails) |
| finite | a deliberately non-differentiable / branch-limited site; AD need not match FD |
Tolerances are per class: 10-5 for closed-form paths (Plummer inverse-CDF, IMF
ppf/cdf, Kepler), 10-3 for ODE/quadrature paths (King/Michie/EFF/multicomponent), and
for the linear-interpolation derivative. The central FD uses a relative
step , . A confirmed-but-unfixed
hazard is encoded as xfail(strict=True) so that fixing it later forces the marker’s
removal (a self-cleaning ratchet).
Status — spatial profiles & velocity DFs (params → IC)¶
Generic and edge/boundary parameters; every ratio measured this run.
Entry point | Param | AD/FD ratio | Status |
|---|---|---|---|
| 1.000000 | ✅ clean | |
| 1.000000 | ✅ clean | |
| (+ edge at ) | 1.000000 / 0.999574 | ✅ clean |
| (+ edge ) | 1.000076 / 0.999989 | ✅ clean |
| 1.000000 | ✅ clean | |
| 1.000140 | ✅ clean | |
| 1.000000 | ✅ clean (Tier 4) | |
| 0.999494 | 🔧 fixed (was silent-zero) | |
| 0.999631 | ✅ clean | |
| 0.995328 | 🔧 fixed (shared | |
| 0.999839 | ✅ clean | |
| / | 1.000000 / 1.000000 | ✅ clean (Tier 4) |
| (+ edge ) | 0.999982 / 0.999998 | ✅ clean |
| 1.000006 | ✅ clean | |
| (scale radius) | 0.999985 | ✅ clean (Tier 4) |
| 1.000013 | ✅ clean | |
| (scale radius) | 0.999997 | ✅ clean (Tier 4) |
| (positions / velocities) | 1.000000 / 1.000000 | ✅ clean |
Status — initial mass functions (params → IC and → summary)¶
Entry point | Param | Direction | AD/FD ratio | Status |
|---|---|---|---|---|
| (+ edge ) | IC | 1.000000 | ✅ clean |
| exactly | IC | 0 (branch) | ⚠ known-limitation |
| IC | 1.000000 | ✅ clean | |
| summary | 1.000000 | ✅ clean (clip benign) | |
| IC | 1.000000 | ✅ clean (Tier 4) | |
| IC | 1.000000 | ✅ clean (clamp benign) | |
| IC | 0.999669 | ✅ clean (Tier 4 — real probe) | |
| , | IC | 1.000000 | ✅ clean |
| , , | IC | 1.000000 | ✅ clean |
| IC | 0.999880 | ✅ clean | |
| (+ edge ) | summary | 1.000000 | ✅ clean |
| exactly | summary | 1.4679 (branch) | ⚠ known-limitation |
| (+ edge ) | summary | 1.000000 / branch | ✅ clean / ⚠ at |
Status — differentiable diagnostics (params → summary)¶
Entry point | Param (through sampled positions) | AD/FD ratio | Status |
|---|---|---|---|
| (EFF; is scale-invariant in ) | 1.000183 | ✅ clean |
| geometry, | 1.000368 | 🔧 fixed ( |
Status — multi-component clusters, binaries & rotation (params → IC)¶
The Tier-2 layer: the two multi-component cluster engines, the binary orbital-mechanics layer, and the rotation kinematic overlays. Every ratio measured this run; all clean, no new hazards.
Entry point | Param | AD/FD ratio | Status |
|---|---|---|---|
| (+ edge , the H2 boundary probe) | 0.999970 / 1.000258 | ✅ clean |
| (truncation sharpness) | 0.999889 | ✅ clean |
| (Gieles–Zocchi segregation exponent) | 0.999992 | ✅ clean |
| (halo Plummer scale, prescribed density) | 0.999936 | ✅ clean |
| (core EFF slope, prescribed density) | 0.999704 | ✅ clean |
| (halo Osipkov–Merritt anisotropy → velocities) | 1.000000 | ✅ clean |
| (+ edge , near-parabolic) | 1.000000 / 1.000000 | ✅ clean |
| (semi-major axis) | 1.000000 | ✅ clean (Tier 4) |
| (mean anomaly) | 1.000000 | ✅ clean (Tier 4) |
| velocity scale | 1.000000 | ✅ clean (Tier 4) |
| 1.000000 | ✅ clean | |
| 1.000000 | ✅ clean (NaN-safe) | |
| scale (→ ) | 1.000000 | ✅ clean |
| 1.000000 | ✅ clean | |
| 1.000000 | ✅ clean | |
| 1.0000001 | ✅ clean |
The two linear overlays (, ) are machine-exact (); the case is genuinely nonlinear (it enters both the prefactor and the rotation curve) and so carries the real teeth, FD-consistent to .
Tier-2 findings (all suspects cleared with a measured number)¶
The H2 boundary is benign (Engine A). The lowered-isothermal mass-CDF draw places stars by mass, and the truncation-shell carries zero mass, so no sampled star ever reaches the
jnp.where(psi>0, ...)support mask or themax(\psi,0)sampling clamp. Measured at the most extended edge (the boundary most stretched, ): the minimum sampled is 0.153 (max sampled radius ), with 0/400 stars hitting the clamp and 0/400 categorical-assignment flips at . The edge is FD-consistent (). The shared tidal radius is itself differentiable in throughfrom_imf(Tier-1 unclamped- fix, ratio 1.000000).The discrete
is_binary/is_circularbranches do not block the / gradient (MoeCompanions). The draw is a smooth grid-CDF inverse; the only discrete selections are theis_binaryHeaviside mask and theis_circular() gate. Measured at the baseline: 0/400is_binaryflips and 0/400is_circularflips at , so the discrete selection injects zero finite-difference noise and the mask-independent channel flows cleanly (ratio 1.000000).The single-star sanitization is NaN-safe (a Fisher-integrity win). The mixed-
is_binaryresolve_binary_componentscase (25 binaries + 25 singles) activates the sanitization path.jacfwdof the resolved positions w.r.t. the per-element is NaN-free, with on every single slot (a finite zero, not aNaN) and a live non-zero gradient on every binary slot — directly verifying the sanitization cannot leak aNaNinto a Fisher matrix.Engine A/B and the OM anisotropy are differentiable. The Engine B density-defined Eddington/OM build (Poisson quadrature + Abel/Eddington inversion, no ODE) is FD-consistent in the prescribed-density scale leaves (, ) and in the Osipkov–Merritt anisotropy radius (whose gradient lives in the velocities). The rotation overlays are exact: the solid-body gradient matches the closed form to , and the nonlinear differential- derivative is FD-consistent to .
Status — binned-kinematic Fisher path (params → summary)¶
The Tier-3 layer: the binned summary statistics a Fisher matrix is actually built from, with . The radial bin edges are frozen (the observer fixes the bins; gradients flow through the model that fills them). The dispersion and anisotropy bin values move smoothly with the parameters, so their gradients are genuinely live; the count is the illustration that the Fisher gradient lives in the model , not the frozen data.
Entry point | Param | AD/FD ratio | Status |
|---|---|---|---|
| 1.000000 | ✅ clean (teeth-checked) | |
| 1.000005 | ✅ clean | |
| 1.000000 | ✅ clean | |
| 0 (AD by design; FD bin-crossing) | ⚠ known-limitation (pinned non-diff) |
The case carries the mutation-check teeth: wrapping the sampled in jax.lax.stop_gradient before the binner collapses AD to 0 exactly while
the finite difference stays live (), proving the case differentiates the real
params → summary channel and not a constant
(test_grad_audit.py::test_binned_sigma_mutation_has_teeth).
The split is the arc’s headline principle made concrete. A Poisson number-density
likelihood writes the expected per-shell count ,
where and is the new public
PlummerProfile.enclosed_mass_fraction CDF. The gradient flows through — i.e.
through the model — and is closed-form machine-exact (ratio 1.0). The observed
binned count , by contrast, is a sum of indicator functions on frozen edges: its
autodiff gradient is identically 0 (a finite, correct-by-design answer), while a finite
difference registers a nonzero discrete step as stars cross the frozen edges. That FD step is
the data-side bin-crossing the audit treats as frozen and out-of-scope; AD is the right
answer, so the case is pinned known_blocked (it passes because AD is finite — not
known_zero, which would mis-flag the nonzero FD as a hazard). Frozen data is out of
scope; the Fisher gradient lives in the model.
Tier 4 — the registry as the release gradient-gate¶
Tiers 0–3 built the harness and a 57-case coverage map. Tier 4 makes the registry the single source of truth for gradient correctness — the release gradient-gate — and consolidates the scattered ad-hoc gradient tests the codebase had accumulated.
A read-only inventory catalogued every released-core test whose assertion is about a gradient (autodiff finiteness, AD-vs-FD consistency, gradient sign/value, differentiability-through-a-path). Each was classified migrate / keep / delete:
~24 duplicate AD-vs-FD assertions were migrated into the registry. These re-derived a finite-difference comparison the registry already owns (or now owns); the duplicate assertion was removed and the scattered test carries a one-line pointer comment to the registry Case that is the source of truth.
~15 finite-only
isfinite(grad)smoke tests were deleted. This is audit finding T6: anassert jnp.isfinite(jax.grad(f)(x))with no finite-difference comparison gives false confidence — a silently zeroed gradient passes it. The silent-zero is exactly the Fisher-corrupting failure mode this arc exists to catch, so a test that cannot see it is worse than no test. The registry’s guard, paired with the FD comparison, has the teeth those smoke tests lacked.~62 tests were kept — genuine non-gradient physics, or unique gradient properties the registry should not absorb: the
find_alphacustom-VJP / implicit-function-theorem regression, the pins (forward-value smoothness plus, post-S4, AD FD at exactly — properties the registry rows do not assert), the Engine-B anisotropy anchor, the energy-kernel double-wheresoftening=0 guards, and the uncovered-channel grad-correctness pins (binary period/eccentricity distributions, mass-ratio samplers, the environment-IMF relations). Each kept test gained a registry-pointer comment.
The migration surfaced 9 new entry-point × parameter Cases the duplicates exercised but
the registry did not yet own — added in Tier 4 and visible in the tables above tagged
(Tier 4): KingVelocityDF.sample_velocities,
EFFProfile.sample_positions, EFFVelocityDF.sample_velocities,
PowerLawIMF.ppf, the three Kepler Jacobian columns
(KeplerElements.to_state in and , and KeplerElements.from_state), the Michie
density observable (, ), and the standalone H6 boundary probe. (The Michie density
observable’s two parameter channels count as one Case here, matching its single dual-param table
row — so the gate’s full row count is 66, of which these are the nine additions.) The H6 case was
promoted: it had been a folded edge on the Chabrier Case (which never actually drove
to the clamp); it is now a standalone Case with the real boundary draw,
measured FD-consistent at ratio 0.999669.
A safety interlock guarded the consolidation: a fast per-file gate after each edit and a full released-core gate per logical batch (new-cases, migrates, deletes). The interlock earned its keep — the migrate batch surfaced two real coverage gaps the duplicates had quietly covered (the Michie density- channel and the EFF positions- channel), which were then added as Cases rather than lost. Tier 4 grew the gate from 57 to 66 cases and found 0 new hazards.

Figure 1:Per-case AD-vs-FD residual on a log axis, one point per registry
Case (98 total), generated by scripts/audit_gradients.py --plots directly from
validation/data/grad_audit_results.json — the same file this page’s tables cite. Every
clean (consistent) case sits below its per-class tolerance band (the loosest gate, the
linear-interpolation derivative at 10-2, is dashed; the tightest closed-form gate at
10-5 is dotted). The known-limitation cases (orange ) have an undefined or
blocked ratio and are annotated rather than plotted on the residual axis. 0 hazards.

Figure 2:One-glance summary of the 98-case release gradient-gate, by protected direction (params IC vs params summary) and status. 72 clean in the IC direction, 25 clean / 1 known-limitation in the summary direction — 97 clean, 1 pinned known-limitation, 0 hazards overall.
Tier 5 — the self-policing release gate¶
Tiers 0–4 measured gradient integrity and made the registry the gate; nothing yet enforced it. A future commit could reintroduce a silent-zero, drift the registry away from its committed numbers, or add a new public sampler with no gradient case — and CI would not notice. Tier 5 makes the gate permanent, self-policing, and complete: a manifest pins what the registry must cover, three CI layers fail loudly on any drift, and the D4 completion closes the last deferred Fisher-coverage holes.
The manifest — the gradient-coverage source of truth¶
tests/validation/grad_audit/manifest.py holds three hand-curated, independently-frozen
structures (deliberately not derived from the registry or __all__ at runtime — a derived
manifest could not catch a deleted case or a new symbol):
MUST_AUDIT— the(id, param)coverage units the registry must keep covering, each with a one-line rationale. The ratchet checks the live registry against this literal, so a deleted case reds CI. (SeveralMUST_AUDITids are internal/submodule entry points legitimately absent fromprogenax.__all__— the frozen-edge binners,q_approx,lambda_msr_approx,Schechter,IMFParams, the Plummer+OM velocity DF — and are ratcheted by a direct(id, param)set check.)SYMBOL_CATEGORY— a complete map from everyprogenax.__all__symbol to eitherAUDITED(must resolve to ≥1 registry case) or a typedEXEMPT_*reason (PROTOCOL,CONTAINER,ANALYTICAL_IC,NON_FISHER_DIAGNOSTIC,HELPER,COVERED_ELSEWHERE). The Layer-3 invariant is a keys-equality (set(SYMBOL_CATEGORY) == set(progenax.__all__)), not a frozen count, so it tracks the live public-API size (reported on the Test Dashboard); a newly-added public sampler is absent from the map, so the keys-equality check fails and the symbol cannot ship ungated.PARAM_ALLOWLIST— the carry-forward known-limitations, each tied to a registryknown_blocked/expect and a reason: the binned-count data side (binned_number_density, AD correct-by-design) and the du-monotonicity data-side property. (The IMF branch points formerly carried here were fixed by audit S4 and now audit asconsistent.)
The three CI enforcement layers¶
A dedicated gradient-gate CI job runs all three layers as pytest tests, so local
pytest tests/validation enforces them identically to CI (one engine, no CI-only path):
Run-gate (Layer 1).
test_grad_audit.pyasserts every registry case isclean/known-limitation, andscripts/audit_gradients.pyexits 0 iff 0 hazards. A reintroduced silent-zero reds this layer.Staleness comparator (Layer 2).
test_json_fresh.pyregenerates the audit into a temp path and semantic-diffs it against the committedvalidation/data/grad_audit_results.json— exact on the discrete/structural projection , rtol-tolerant on the floats (rtol measured cross-arch). The registry and its committed measured numbers can never silently drift apart.Coverage ratchet +
__all__cross-check (Layer 3).test_manifest_coverage.pyasserts everyMUST_AUDIT(id, param)is in the live registry and thatset(SYMBOL_CATEGORY) == set(progenax.__all__)with everyAUDITEDsymbol resolving to ≥1 case. A deleted case, a removed coverage unit, or a new/ungated public entry point reds CI.
Plainly: CI fails on a reintroduced silent-zero, a registry↔JSON drift, or a manifest /
__all__ entry point without a case.
D4 — closing the deferred Fisher-coverage holes¶
Tier 5 also completes the gate. Eleven cases close the holes the earlier tiers had deferred, each landed measured-first (≥3 seeds, ±3σ band, no weakened tolerance), growing the gate 66 → 77 with 0 new hazards:
Engine-A velocity-scale ratio
w_j(from_components) and equipartition OM anisotropy radiusr_a(from_mass_segregation) — finite-only smoke before, now FD-consistent.build_binary_clusterend-to-end — the full IMF→companion→spatial assembly gradient in , machine-exact (the assembly is -linear, AD/FD ratio 1.0 to ).Binary orbital-distribution params — three period samplers, four eccentricity samplers, and the
IndependentCompanionsthermale_maxthrough the full companion assembly.
The closed-form anchors landed exactly: LogNormalPeriod gives
; the Uniform/LogUniform channels
recover (0.4939); and ThermalEccentricity recovers
(0.6620).
The 11 Tier-5 (D4) cases — all params → IC, all FD-consistent, 0 new hazards
Entry point | Param | AD/FD ratio | Status |
|---|---|---|---|
| (velocity-scale ratio) | 0.999965 | ✅ clean |
| (equipartition OM radius) | 1.001542 | ✅ clean |
| 1.000000 | ✅ clean (machine-exact) | |
| power (OB period index) | 1.000000 | ✅ clean |
| 1.000000 | ✅ clean () | |
| 1.000000 | ✅ clean () | |
| 1.000000 | ✅ clean () | |
| 1.000000 | ✅ clean () | |
| (Roche cap at long ) | 1.000000 | ✅ clean |
| (DM91 circular→thermal blend) | 1.000000 | ✅ clean |
| (through the full companion assembly) | 1.000000 | ✅ clean |
The gate is currently enforced via local pytest; the dedicated gradient-gate CI job is wired
into the tests aggregator and fires when CI runs.
Intentional non-differentiable sites¶
These are documented, deliberate limitations — not bugs. The audit pins them so an unannounced change fails loudly.
IMF singularity at — FIXED (audit S4), no longer a limitation. The historical
exp_safedouble-where guard removed the singularity but selected an -independent branch at exactly , so AD there was finite yet silently wrong (ppf: AD vs FD ). The segment kernels now use theexpm1/log1p-stable forms (progenax.numerics.power_integral_stable/power_ppf_stable) — one smooth expression in — so the gradient is FD-exact at exactly (and at formean_mass’s numerator). Auditedconsistent; pinned intests/unit/imf/test_imf_gradients.py::TestAlphaOneGradientsand the registry edges.
Changelog of fixes¶
Finding | Symptom (measured) | Fix | Commit |
|---|---|---|---|
King | AD ; true –48 | unclamped- dual-return → interpolated crossing; differentiable across King/Michie/LIMEPY/multicomponent A+B; forward now the (more accurate) interpolated crossing |
|
Segregation surrogate | AD/FD at default (~27% of the gradient omitted) | dropped the |
|
| the Fisher gradient had no model-side entry point; the binned data count is non-diff (AD ) | new public CDF supplies the differentiable per-shell occupancy — the model-side Fisher channel (ratio 1.0); pins the data-count limitation |
|
Tier 4 — registry consolidated into the release gradient-gate (no new hazards) | the codebase had ~24 duplicate AD-vs-FD assertions and ~15 finite-only | migrated the 24 duplicates into the registry (scattered tests now carry pointer comments), deleted the 15 smoke tests, added 9 new Cases (King/EFF DF scale params, |
|
Tier 5 — self-policing release gate + D4 completion (no new hazards) | the measured gate had no enforcement (a future commit could silently reintroduce a zero, drift the JSON, or ship an ungated public sampler); three Fisher-coverage holes remained deferred (Engine-A | added the | (this arc) |
What this proves, and what it does not¶
The audit proves that across the released spatial/kinematic/IMF samplers and the packaged
differentiable diagnostics, the parameter gradients used to build a Fisher matrix are finite
and finite-difference-consistent — or are explicitly pinned limitations. It is a gradient
coverage map: the entries that say “clean” are the ones a forecasting study can trust, and the
two fixed rows are entries that would previously have produced a confidently wrong Fisher
element. The audit does not cover non-public internals. (The formerly-pinned
known_blocked points were eliminated by the audit-S4 expm1-stable kernels and
now audit consistent — see the fixed-site note above.)
- Cartwright, A., & Whitworth, A. P. (2004). The statistical analysis of star clusters. Monthly Notices of the Royal Astronomical Society, 348, 589–598. 10.1111/j.1365-2966.2004.07360.x