Repository navigation
Differentiability gradient-audit + hardening arc (registry = release gradient-gate) - #8
Conversation
Validated design from the Phase-1 brainstorm (HITL with Anna): - shared core + thin pytest/script layers so the gate and the website table cite the same measured AD-vs-FD numbers (single source of truth) - explicit expect taxonomy (consistent / known_zero / known_blocked); known limitations PINNED, not skipped - per-class tolerance (1e-5 closed-form, 1e-3 ODE) + relative central-FD step - per-channel physical IC reductions (mean r / mean v / mean m) - full surface in 4 tiers; T4 consolidates all scattered grad tests into the registry (the release gradient-gate; fixes audit T6) - confirmed-unfixed hazards -> xfail(strict) + hazard-id (self-cleaning ratchet) - preliminary probes: H4/H6 verify-benign; alpha=1-exactly is a known branch point; live suspect surface narrows to H2 (psi=0 masks) + shared r_t kink Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…se 2) Strict RED->GREEN task-by-task plan, 4 tiers + checkpoints: - Tier 0: TDD the engine (toy NaN/silent-zero/wrong-by-2x must be caught) + reductions + parametrized gate + audit_gradients.py - Tier 1: headline samplers + packaged summaries + king r_t known_zero pin + alpha=1 known_blocked split + H4/H6 boundary probes; mutation-check teeth - Tier 1.5: living website validation doc, build to 0 warnings - Tier 2: MultiComponentCluster A/B (H2 psi=0 + r_t-kink probes), binaries, rotation - Tier 3: vendored frozen-edge binner -> binned sigma/beta/N Fisher path - Tier 4: inventory + migrate/dedupe all scattered grad tests into the registry (release gradient-gate; fixes audit T6); plots; completion doc; ONE final PR Plan-time refinement: segregation is a consistent (non-zero) pin, not known_zero (proves stop_gradient didn't block the real gradient); only king r_t is known_zero. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…by-2x + expect taxonomy
… type-guard expect; drop dead import
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…zard_id; drop dead import (review)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
King positions (W0, r_c), King velocity DF (W0): all clean (FD-consistent, tol=1e-3); W0=12 edge clean via explicit ODE domain (xi_max=700) so the traced-W0 pinned-r_t guard does not trip. KingProfile.r_t added as expect=known_zero -- at theta0=7.0 r_t is grid-snapped so AD~0 AND FD~0 (known-limitation). MEASURED finding: at W0=8.0 the r_t VALUE moves with W0 (FD=62.5, fixed grid) while AD stays 0 -- a genuine silent-zero on the argmax-protected path, flagged for Anna's triage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ichie/LIMEPY/multicomponent via unclamped-psi interpolation (audit finding; forward r_t now interpolated crossing, Anna-ratified; no compat shim) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rentiable) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fferentiability Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… alpha=1 known_blocked split Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…prox summaries (segregation stop_gradient is a consistent non-zero pin)
…scale — restores FD-consistent gradient at default beta (audit finding; forward value unchanged, Anna-ratified)
…e (Tier 1.5) One row per (entry point x param x direction) with REAL measured AD/FD ratios from validation/data/grad_audit_results.json (38 cases, 35 clean / 3 known-limitation). Documents the two audit-found hazards fixed gradient-only (king r_t silent-zero across 5 models; segregation stop_gradient ~27% bias), the alpha=1 known_blocked branch points, the expect-class taxonomy, and a changelog. Wired into myst.yml validation TOC + index intro. Builds 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ases (Task 2.1) Add the params->IC grad-audit cases for the lowered-isothermal multimass equilibrium: from_imf(...).sample_cluster reduced by mean_radius, audited in the three traceable equilibrium drivers W0, g, delta (n_comp=3, n_stars=400). Measured-first frozen gates (AD vs central-FD, tol=1e-3): W0 @5.0: AD=4.709932e-1 FD=4.710074e-1 |ratio-1|=3.0e-5 g @1.0: AD=1.459179e-1 FD=1.459341e-1 |ratio-1|=1.1e-4 delta @0.5: AD=8.716899e-1 FD=8.716973e-1 |ratio-1|=8.5e-6 H2 probe (ψ=0 boundary masks @ multicomponent.py:266/272/286 + shared r_t kink) CLEARED BENIGN via a W0=3[H2-boundary] edge (most extended, r_t=4.26, |ratio-1|=2.6e-4): (1) the shared tidal radius r_t IS differentiable in W0 through from_imf (ratio 1.000000 at W0=5,7) — the Tier-1 unclamped-ψ fix (9d3365a) still flows here; (2) no sampled star ever reaches ψ(r)<=0 (the boundary shell carries ~zero mass; 0/400 hit the max(ψ,0) clamp), so the jnp.where(psi>0,...) masks are structurally inert for the sampled stars. Categorical-assignment discreteness (OUT of scope) side-stepped by case design, not by weakening tol: at h=1e-4 the fixed-key categorical draw is STABLE — 0/400 assignment flips for all of W0, g, delta — so the FD is the pure smooth position-physics derivative, uncontaminated. Gate teeth verified RED (tol=1e-8 -> hazard at ratio 1.000258) then GREEN (tol=1e-3). Full grad-audit gate: 42 passed (was 38), 0 hazards. JSON regenerated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rements (review fixes) Code-review precision fixes to the H2-probe comments in the Engine A sample_cluster cases (comments only; no code change): - Fix wrong file:line citations. The ψ=0 / boundary masks were cited as multicomponent.py:266/272/286: :266 is blank, :272 IS the genuine jnp.where(psi_grid>0,...) xi-grid support mask (kept), :286 is the jnp.where(r_grid<=r_t,...) RADIAL truncation mask (relabeled accurately, not a ψ=0 mask). The max(ψ,0) clamp was mis-cited at multicomponent.py:66 (a docstring line) — the real clamps are sampling.py:64-66 (per-star W_i on the sampling path) and limepy.py:275 (model-build grid clamp). - Replace the W0=5-only "benign" evidence with the W0=3 stress-test numbers (most extended cluster, r_t=4.26, boundary most stretched): measured min ψ(r_sampled)=0.153 (max sampled radius 3.66 < r_t), 0/400 stars hit the max(ψ,0) clamp, 0/400 categorical assignment flips at h=1e-4. The boundary is approached more closely at W0=3 but never reached; the jnp.where masks carry zero live gradient at the edge (masked-out density past r_t is ~0). H2 stays benign; gate green (42 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s cases (Task 2.2) Add three params->IC grad-audit Cases through the density-defined shared-Psi Eddington/OM build (Poisson quadrature + Eddington inversion, NO ODE) + per-star draw, on the proven-realizable Plummer-halo + EFF-core headline mix reused from test_engine_b_physics._headline_model: - halo Plummer r_h -> mean_radius: AD=6.767522e-1 vs FD=6.767957e-1 (|r-1|=6.4e-5) - core EFF gamma -> mean_radius: AD=-1.204971e-1 vs FD=-1.205328e-1 (|r-1|=3.0e-4) - halo OM r_a_j[0] -> mean_speed: AD=1.768007e-3 vs FD=1.768007e-3 (|r-1|=2.7e-9) All FD probes (theta +- h) stay realizable (f_min_j > 0 measured at every point; the EFF-core ~1.185e-4 margin is small but strictly positive and flat), so the central FD never trips the concrete negative-DF ValueError. 0/400 categorical assignment flips at h for all three params, so FD is discreteness-free. tol=1e-3 (set by the widest band, gamma at 3.0e-4). Reduced resolution n_r=3000, n_e=500 (AD==FD is a graph property; both sides share grids). Full grad-audit gate: 45 passed, 0 hazards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed-guard sentinel (review note)
…ary_components / MoeCompanions (Task 2.3) Add three params->IC binary-layer cases to the grad-audit registry, all classified consistent/clean by measurement (no weakening, no cherry-picking): - KeplerElements.to_state (param e): elements -> Cartesian position via the fixed-iteration Newton Kepler solve + trig rotation. mean_radius, tol=1e-5. AD/FD ratio 1.0000000 at e=0.5 AND at the e=0.999 near-parabolic EDGE (the max(1-e²,1e-12) guard is inactive there; 50 Newton iters converge). - resolve_binary_components (param a): binary->spatial connector, N=50 all binaries (sanitization inactive). mean_radius over (2N,3), tol=1e-5, ratio 1.0000000. - MoeCompanions.sample (param m1-scale): a sampler MIXING a smooth grid-CDF (P,q,e) draw with a DISCRETE is_binary Heaviside mask + m2 gate. Audited via <e> (mask-independent P->e coupling). Classified clean by measurement: 0/400 is_binary flips at s=1±h (the discrete selection injects no FD noise), <e> ratio 1.0000000 (|AD|=1.02e-4 >> eps); even the gated <m2=m1·q> is ratio 1.0000000 since 0 flips keeps the where() locally smooth. The grid-CDF reparameterization makes the coupling gradient flow; the mask does not block it. Registry: 45 -> 48 baseline Cases; JSON 49 rows (incl Kepler e=0.999 edge). Full grad-audit gate: 49 passed, 0 hazards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ocity-channel + Moe is_circular branch, add mixed-is_binary resolve case (Task 2.3 review) Issue 1: the resolve_binary_components comment claimed AD/FD=5.872715e-2, but the committed _resolve_binary_a closure (PRNGKey(11)) measures 8.437447e-2 (ratio 1.0000000008, matches JSON). The 5.87e-2 was a stale draft value from an earlier PRNGKey(0) variant; corrected the comment to the true numbers and noted the seed provenance. Case is valid/clean. Issue 2: re-measured the Kepler to_state VELOCITY channel at e=0.999. With the engine's central FD it is machine-precision clean (AD=-7.484823e-2 vs FD=-7.484823e-2, ratio 1.0); it only softens under a coarse one-sided FD (forward h=1e-3 -> ~0.99993) from the d/de[sqrt(1-e^2)] curvature. Documented that the velocity gradient is NOT blocked, only FD-noisier, hence position is the audited (cleaner) channel. Issue 3: flip-counted the MoeEccentricity is_circular branch (etap1<=1e-6). For the closure masses, 10/400 stars are already deeply circular (min etap1=-698) but 0/400 flip at s=1+-h, so the discrete jnp.where(is_circular,0,e) injects zero FD noise -- consistent with <e> ratio 1.0. Documented. Issue 4: added resolve_binary_components[mixed] case (25 binaries + 25 singles) exercising the single-star sanitization path (a_safe/e_safe/m2_safe, assembly.py:85-87). Confirmed jacfwd is NaN-free with d/da=0 on every single slot and a live gradient on every binary slot -- the sanitization does not leak NaN into a Fisher matrix. AD=3.019178e-2 vs FD=3.019178e-2 (ratio 1.0). Regenerated grad_audit_results.json (50 cases, 0 hazards); full gate 50 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (Task 2.4)
Add the three rotation kinematic-overlay grad-audit cases (params->IC),
completing Tier 2 of the differentiability gradient-audit arc:
- apply_solid_body_rotation in omega: EXACTLY-linear overlay, AD matches the
closed form mean(|axis x r|)=1.1226200071 to <1e-12; AD/FD ratio 1.0 (6.9e-13).
- apply_differential_rotation in v_peak: linear in v_peak, machine-exact (5.8e-13).
- apply_differential_rotation in R_peak: genuinely NONLINEAR (R_peak in both the
(R/R_peak) prefactor and exp(1-R/R_peak)); FD-consistent to 5.5e-8. tol=1e-5.
All three use zero base velocities to isolate the pure rotation overlay over a
fixed Plummer position sample + fixed z-axis. Regenerated
validation/data/grad_audit_results.json (53 cases, 0 hazards).
Website doc (50-validation/differentiability-audit.md): refresh for ALL of Tier 2
(Engine A, Engine B, binaries, rotation) -- updated the case count 38 -> 53, added
a "multi-component clusters, binaries & rotation" status list-table with real
measured ratios from the JSON, and a Tier-2 findings narrative (H2 psi=0 boundary
benign; discrete is_binary/is_circular branches don't block the q/e gradient;
single-star sanitization NaN-safe under jacfwd; Engine A/B r_t differentiable).
Tier 2 added 15 cases, all clean, 0 new hazards -> changelog unchanged. MyST build
clean (0 warnings).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nlinear teeth (review nit)
…her-path cases + mutation check (Task 3.1; N(r) framing pending review) Tier 3 of the differentiability gradient-audit arc: audit the exact Jacobian J = d(binned kinematic summary)/d(theta) that the Fisher matrix F = JᵀJ is built from, for the velocity-dispersion sigma_1d(r) and Binney anisotropy beta(r) channels. Part A: vendor the five frozen-edge binners (_bin_index, _grouped_bin_sums, binned_sigma1d, binned_sigma_beta + SigmaBetaResult, binned_number_density) VERBATIM from scripts/_demo_inference.py @ 2090dcd into tests/validation/grad_audit/binners.py so the audit has a stable binner independent of the (non-package) demo script. Part B: two params->summary Fisher cases through build_spatial_ic + the frozen-edge binner. Both are machine-precision FD-consistent (ratio 1.0000000) at h_rel=1e-5: - binned_sigma1d[Plummer] d(sig_hat)/d(r_h): AD=-3.345969e+00, ratio 1.0 - binned_sigma_beta[Plummer+OM] d(beta_hat)/d(r_a): AD=-1.000759e+00, ratio 1.0 The FD step is tightened to h_rel=1e-5 (not tol weakened): at the default h_rel=1e-4 a single boundary star (#821, r=7.99977) straddles the r=8 outer edge and corrupts the COARSE central FD to ratio 0.86 — a coarse-FD edge-straddle, NOT an autodiff bug (per-bin ratios all 1.0; ratio stable for h<=1e-5 and across N=2000/4000/8000). At h_rel=1e-5: 0/2000 bin crossings at +-h. Part B2: test_binned_sigma_mutation_has_teeth — wrapping (pos,vel) in stop_gradient before the binner collapses AD to 0.000000e+00 while the live FD stays -3.35e+00, proving the case differentiates the real params->summary channel (not a constant). N(r): probed BOTH framings (data-side count vs model-side analytic Plummer p_k) but did NOT add a Case — the framing decision is pending review (CHECKPOINT 3). Gate: 55 audit case-rows + 1 mutation test, 0 hazards (was 53). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…leaves bin membership invariant, default h is clean (review fix)
…n (N(r) model-side Fisher channel) + pinned data-count non-diff case + website (Task 3.1 N(r)) Add the public PlummerProfile.enclosed_mass_fraction CDF M(<r)/M = r^3/(r^2+a^2)^(3/2), the differentiable per-shell occupancy p_k(theta) that a Poisson number-density likelihood differentiates -- the model-side N(r) Fisher channel. The frozen binned data count remains correctly non-differentiable (AD=0 by design), pinned as a known_blocked limitation. - src: new enclosed_mass_fraction method (pure jnp, differentiable in r and r_h) - unit tests: M(<r_h)=0.5, M(<a)=1/(2sqrt2), M(<0)=0, M(<inf)=1, monotone, vectorized, JIT, grad in r (>0) and r_h (flows through a) - grad-audit registry: N(r) model case (consistent, ratio 1.0000000, tol=1e-5) + pinned data-count case (known_blocked, AD=0 finite, FD=-5e3 bin-crossing) - website: binned-kinematic Fisher path table (sigma/beta + N(r) split), case count 53->57, changelog Tier-3 addition row - regenerated grad_audit_results.json (57 rows, 0 hazards; gate 58 passed) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nsolidation (Task 4.1; no deletions — awaiting CHECKPOINT 4a sign-off)
…e+7 new cases; keep borderline; fast-per-file/full-per-batch)
…min, Kepler a/M0/from_state, Michie density) + fix H6 boundary probe + dedupe (Task 4.2a) Grad-audit gate 65 passed. All measured-first; H6 boundary now a standalone case running the real u=1e-12 probe (eps=1e-12 below the live |AD|), duplicate edge removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…it (i=0.3,Omega=0.4,omega=0.5) — runs the general inversion path, value unchanged (review note)
…rad-audit registry (audit T6 consolidation; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ad-audit registry (audit T6 consolidation; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… AD-FD assertion into grad-audit registry (audit T6 consolidation; registry is SoT) test_profile_gradients.py: removed plummer r_h, eff gamma, king r_c, king W0 sampler FD tests; KEPT test_eff_grad_a with a TODO (EFFProfile.sample_positions(a) is not directly owned by the registry — safety interlock against coverage loss). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-FD assertion into grad-audit registry (audit T6 consolidation; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sertion into grad-audit registry (audit T6 consolidation; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…D-FD assertion into grad-audit registry (audit T6 consolidation; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on into grad-audit registry (audit T6 consolidation; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-FD assertion into grad-audit registry (audit T6 consolidation; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…D assertions into grad-audit registry (audit T6 consolidation; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion into grad-audit registry (audit T6 consolidation; registry is SoT) Split test_jit_and_grad_safe -> test_jit_safe: dropped the d|sep|/da grad smoke (owned by the registry resolve_binary_components Case), kept the JIT finite assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…FFProfile.sample_positions(a) cases, migrate test_eff_grad_a, fix Michie W0 pointer Review found the W0 density channel (closed-form density() path, distinct from the sampler) and EFF positions-in-a were uncovered. Both added (measured-first, clean), closing the migrate interlock's one skip. Registry now 66 cases.
…t smoke test(s) — registry FD-covers (audit T6; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… test(s) — registry FD-covers (audit T6; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oke test(s) — registry FD-covers (audit T6; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e test(s) — registry FD-covers (audit T6; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e test(s) — registry FD-covers (audit T6; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ke test(s) — registry FD-covers (audit T6; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…st(s) — registry FD-covers (audit T6; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…moke test(s) — registry FD-covers (audit T6; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t(s) — registry FD-covers (audit T6; registry is SoT) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…yGradients + clarify sample/ppf reparam pointer (4.2c review)
…site 66-case reconcile + consolidation narrative, STATUS (Task 4.3) - scripts/audit_gradients.py: add --plots flag rendering grad_audit_ratio.png (per-case |AD/FD-1| residual, log axis, tol bands, known-limitation annotated) + grad_audit_summary.png (counts by direction × status), straight from the single-source JSON. Guarded matplotlib (viz extra) import. - website differentiability-audit.md reconciled 57→66 cases: 9 Tier-4 rows added with real measured ratios (King/EFF DF scale params, PowerLawIMF.ppf(m_min), Kepler to_state(a,M0)/from_state, Michie density observable, H6 standalone), new "Tier 4 — registry as the release gradient-gate" section (24 migrated / 15 deleted T6 smoke / 2 gaps surfaced / H6 fix), both figures embedded, changelog row. myst build 161 pages, 0 warnings. - STATUS: differentiability gradient-audit arc complete (Tiers 0–4); registry is the release gradient-gate (66 cases, 62 clean / 4 known-limitation, 0 hazards, 2 fixed); awaits final PR / CHECKPOINT-4b. audit_gradients.py exits 0 (66 cases, 0 hazards); test_grad_audit 67 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion (Michie density = 1 dual-param Case) (4.3 review nit)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe8b8a69b2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| f"the anisotropy is too strong (no finite tidal radius). Increase ra / xi_max." | ||
| ) | ||
| return xi_grid, psi_grid, rho_j_grid | ||
| return xi_grid, psi_grid, psi_raw, rho_j_grid |
There was a problem hiding this comment.
Update validation callers for the new solve return arity
When this solver now returns four values, the DF-table validation script still unpacks three: scripts/validate_df_tables.py uses _solve(...) as xi_q, psi_q, _ = ... at lines 106-107 and directly calls _, psi, _ = solve_multicomponent_limepy(...) at line 146. Running that validation path after this API change raises ValueError: too many values to unpack, so the table-accuracy/gradient validation script can no longer run until those unpack sites are updated or a compatibility wrapper is kept.
Useful? React with 👍 / 👎.
…CHECKPOINT 4b closed) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Why
progenax's value proposition is that a differentiable forward model turns cluster inference into information geometry: every IC is$F = J^\mathsf{T}J$ is computable. That has a sharp failure mode — a silently zeroed or wrong column of $J$ makes a parameter look infinitely well-constrained (a confidently wrong uncertainty) with no error raised. A
jax.grad-able, so the Fisher matrixNaNcrashes loudly; a zeroed gradient does not. Gradient integrity is Fisher integrity.This arc builds a systematic AD-vs-finite-difference audit over every public sampling/physics entry point, fixes the hazards it finds, and makes the audit registry the release gradient-gate.
What's in here
A 66-case AD-vs-FD harness (
tests/validation/grad_audit/) over both protected directions (params → ICandparams → binned summary), with a computed (never hand-set) status: 62 clean, 4 pinned known-limitations, 0 hazards. Numbers are regenerated byscripts/audit_gradients.py→validation/data/grad_audit_results.json; the living tracker isdocs/website/50-validation/differentiability-audit.md.Two real hazards found and fixed — both in sites previously believed correct-by-design, both gradient-only (forward physics bit-identical):
r_twas a silent zero (jnp.maximum(\psi,0)clamp zeroed the crossing-node gradient before the shared_find_tidal_radius. Fixed by feeding unclampedstop_gradientomitted ~27% of the gradient at the default softmin temperature. Removed; now FD-consistent.The Fisher path itself audited — binned$\sigma(r)$ / $\beta(r)$ channels are clean with a mutation check proving teeth ($N(r)$ Fisher channel, while the binned data count is pinned as correctly non-differentiable (frozen data is out of scope; the gradient lives in the model).
stop_gradient(pos,vel)collapses AD to 0). A new publicPlummerProfile.enclosed_mass_fractionsupplies the model-sideThe registry is now the release gradient-gate — ~24 duplicate AD-vs-FD assertions migrated in, ~15 finite-only
isfinite(grad)smoke tests deleted (a silent-zero passes them), coverage preserved by a safety interlock + per-batch full gates (which surfaced 2 real gaps that were then filled). Scattered tests carry pointer comments to the registry.Verification
scripts/audit_gradients.py: exit 0, 0 hazardsPlummerProfile.enclosed_mass_fraction: 12 unit tests; King c(W₀) vs Table II unchanged (max|Δlog₁₀c|=0.0023)Completion doc:
.claude-work/TASK_differentiability-audit_COMPLETE.md.🤖 Generated with Claude Code