PP20 magnification factor ζ(p) — transcription bug fix¶
Date: 2026-04-28
Module (at the time): progenax.gravoturb.pp20_magnification → now gravoturb.theory.dense_gas_sfr
Companion fix: swindlax.physics.density_gradient (already locked, 29 regression tests)
What changed¶
| Path | Δ lines | Nature of change |
|---|---|---|
src/progenax/gravoturb/pp20_magnification.py | +112 / −56 | Replace buggy ζ(p) with canonical analytic form; add P_MAX = 1.95; rewrite module + function docstrings |
tests/unit/physics/test_parmentier.py | +178 / −131 | Re-anchor every numerical assertion on PP20 Eq. 6 / analytic values; replace “p=1.3 singularity” tests with “smooth across p=1.3” + “diverges only at p=2” |
tests/unit/physics/test_pp20_zeta_canonical.py | +303 / 0 | New regression file ported from swindlax — 28 tests anchoring ζ(p) on PP20 Eq. 6, the integral-derived analytic form, and Kainulainen+2014 |
No production code outside pp20_magnification.py was modified. No JSON / NetCDF / committed validation outputs were regenerated.
The bug¶
Inline form prior to this fix:
zeta = (3.0 - p) / (2.6 - 2.0 * p) ** 1.5Three transcription errors against PP20 Eq. 6 (Parmentier & Pasquali 2020, ApJ 903, 56, page 2):
The constant
2.6was moved inside the 3/2 power.The
(3 − p)factor lost its 3/2 exponent.The multiplicative factor
2.6·(2 − p)was garbled into linear(2.6 − 2p).
Numerical impact: the buggy denominator 2.6 − 2p vanishes at p = 1.3, which the docstring rationalised as a “domain limit.” PP20 Eq. 6 has no singularity at p = 1.3; the only true singularity is at p = 2 (singular isothermal collapse). The buggy form gave ζ(0.5) ≈ 1.235, ζ(1) ≈ 4.30 (clamped via max(., 1.0)), and arbitrary post-clamp values for p ≥ 1.3 — none of which match PP20.
The fix¶
Canonical form, derived from the integral definition ζ = ∫ρ^(3/2)dV / (M·√⟨ρ⟩) for a power-law profile ρ(r) = ρ_R(r/R)^(−p) on a sphere of outer radius R:
P_MAX = 1.95
@jax.jit
def magnification_factor(p):
p_safe = jnp.clip(p, 0.0, P_MAX)
return 2.0 * (3.0 - p_safe) ** 1.5 / (3.0 ** 1.5 * (2.0 - p_safe))Equivalent to PP20 Eq. 6 to 0.08% — PP20’s “2.6” is the rounded value of 3^(3/2)/2 = 2.598. Spot values now reproduce PP20 / analytic anchors:
| p | This fix | PP20 Eq. 6 | Buggy form (pre-fix) |
|---|---|---|---|
| 0 | 1.0 (exact) | 0.999 (rounding) | clamped to 1.0 |
| 0.5 | 1.014 | 1.014 | 1.235 |
| 1.0 | 1.0887 (= 2·2^(3/2)/3^(3/2)) | 1.0879 | 4.30 |
| 1.5 | √2 (exact) | 1.4142 | ∞ (denom → 0) |
| 1.67 | 1.789 | 1.789 | clamped / garbage |
| 1.95 | 8.28 | 8.29 | clamped / garbage |
P_MAX = 1.95 keeps gradients well-behaved under HMC/NUTS by shielding the p → 2 divergence, matching the same convention used in swindlax.physics.density_gradient and PP20 Fig. 1.
Test-count delta¶
| Suite | Pre-fix | Post-fix | Δ |
|---|---|---|---|
tests/unit/physics/test_parmentier.py | 34 passing (against the bug) | 36 passing (against PP20) | +2 (replaced p=1.3 singularity tests with PP20-anchored monotonicity / divergence / Kainulainen / exact-analytic anchors) |
tests/unit/physics/test_pp20_zeta_canonical.py | — | 35 passing | +35 new |
progenax/tests/ total (excluding pre-existing import error in test_knobs_pipeline.py) | 777 | 812 | +35 |
The test_knobs_pipeline.py ModuleNotFoundError: progenax.profiles.mass_segregation predates this work (commit 13df53f removed the module without removing the test); not in scope.
Verification¶
All 71 tests across
test_parmentier.py+test_pp20_zeta_canonical.pypass.Full progenax suite: 812 passed, 0 failed (excluding the pre-existing collection error).
magnification_factor_with_coreandzeta_from_fieldwere not modified — they are independent of the analytic typo (numerical integration / 3D-field measurement). A new test intest_pp20_zeta_canonical.pyconfirmsmagnification_factor_with_core(p, r_c/R)approaches the corrected analyticmagnification_factor(p)asr_c/R → 0(parametrised over p ∈ {0.5, 1.0, 1.5}).A
test_no_buggy_inline_zeta_form_in_progenaxsource-text scanner is in place; it walkssrc/progenax/and fails if any of(2.6 - 2.0 * p),(2.6 - 2*p),2.6 - 2.0*p,2.6 - 2 * preappears in live source.
Validation artefacts (resolved in the 2026-06 rewrite)¶
At the time of this fix, the legacy validation/turbulence/bm19_fdf_suite/
suite still held stale, typo-rationalising claims (e.g. a “singularity at
p = 1.3” caption in its VALIDATION_SUMMARY.md, and b5/b6/e5 comparison
plots drawn against the buggy analytic ζ curve). These were flagged for
regeneration rather than fixed in the 2026-04-28 commit.
They were not regenerated — they were removed. The entire
bm19_fdf_suite (and the progenax.gravoturb / cluster.fdf* modules it
imported) was deleted in the 2026-06 clean-room rewrite. The corrected ζ(p)
and the honest gravoturbulent validation now live in the experimental
gravoturb package, whose acceptance suite prints its ζ anchors
(AC3) and the BM19 cornerstone (AC6) — see
src/experimental/gravoturb/VALIDATION_SUMMARY.md. There is no longer a
monolithic bm19_pipeline/BM19Result orchestrator; the forward chain is
composed from gravoturb.theory.* + gravoturb.realization.*.
References¶
Parmentier, G. & Pasquali, A. 2020, ApJ, 903, 56 (arXiv:2009.10652), Eq. 6.
Kainulainen, J., Federrath, C., & Henning, T. 2014, Science, 344, 183 — observational anchor for ζ(p ≈ 1.67) ≈ 1.79.
Companion fix and full integral derivation:
swindlax/src/swindlax/physics/density_gradient.py.Hardening report:
papers/rosen-burkhart-swindle-2026/reviews/audit-2026-04-28-pp20-validation.md.