From 58f7110430de43ae02a2a8d2bd97aad3eff9b00b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 14:54:26 +0000 Subject: [PATCH 1/2] prompt: priors 15 caller analysis + new finding 16 (factor_gradient dead-on-call) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run the caller inventory PyAutoFit#1498 adjudication point 1 asked for (main @ 21288bb, numerically verified): - The EP/Laplace loop (MeanField.logpdf, FactorApproximation, from_mode) is coherently base-space — the missing log_det is its working convention, not a bug inside that loop. - PriorFactor is the one hybrid seam: it feeds the physical density (prior.factor) into a base-space tilted objective, one x-dependent log_det off either coherent convention. - Public Prior.logpdf / pdf promise a physical density but return base-space values for transformed priors; no non-linear search path is affected. - Corrects #1498 point 2: logpdf_gradient is the exact derivative of logpdf (chain rule), so there are two conventions, not three. - New finding filed as bug/priors/16: TransformedMessage.factor_gradient unpacks 4 values from a 3-tuple and crashes on first call (dead code, zero production callers). Recommendation recorded in the 15 prompt for human adjudication with PyAutoFit#1500; nothing actioned in PyAutoFit. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01LoqJ75SSYra4o33a5UQQNC --- .../15_transformed_message_logpdf_jacobian.md | 89 ++++++++++++++++++- ...nsformed_message_factor_gradient_unpack.md | 59 ++++++++++++ draft/bug/priors/z_features.md | 8 +- 3 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 draft/bug/priors/16_transformed_message_factor_gradient_unpack.md diff --git a/draft/bug/priors/15_transformed_message_logpdf_jacobian.md b/draft/bug/priors/15_transformed_message_logpdf_jacobian.md index 95d1188d..acac21a6 100644 --- a/draft/bug/priors/15_transformed_message_logpdf_jacobian.md +++ b/draft/bug/priors/15_transformed_message_logpdf_jacobian.md @@ -5,7 +5,8 @@ Target: priors Difficulty: medium Autonomy: supervised Priority: normal -Status: formalised — issue filed (PyAutoFit#1498), awaiting adjudication +Status: formalised — issue filed (PyAutoFit#1498); caller analysis complete +(2026-08-19, below), awaiting human adjudication of the contract choice Found 2026-08-18 while implementing the #1497 property sweep (prompt `bug/priors/09`). Same failure *shape* as census finding A4 (#1331-04): a @@ -61,6 +62,92 @@ The #1497 property tests assert the physical density via `factor` and cite `test_autofit/mapper/prior/test_prior_properties.py`); tighten them to `logpdf` once resolved. +## Caller analysis (2026-08-19, main @ `21288bb`) + +The inventory #1498 adjudication point 1 asked for, run over the full +`autofit/` production tree (tests excluded). Verified numerically: +`UniformPrior(0,2)` gives `logpdf(1.3) = −0.993174` vs +`factor(1.3) = −0.693147 = log 0.5`; `∫exp(logpdf)` over the support is +`0.564190 = 2/(2√π)` while `∫exp(factor)` is `1.000000`. + +**Callers expecting a physical density (exposed to the bug):** + +- `Prior.logpdf` — the public prior API. `UniformPrior.logpdf` + (`uniform.py:124`) calls `message.logpdf` explicitly, and its + boundary-epsilon nudge operates in *physical* coordinates, so the method + unambiguously intends physical semantics; every other prior reaches + `message.logpdf` via `Prior.__getattr__`. Uniform / LogUniform / + LogGaussian users are silently handed base-space values. +- `MessageInterface.pdf` (`interface.py:45`) — **zero production callers** + anywhere in `autofit/`; public-API surface only. +- The non-linear search stack never touches message `logpdf` — sampling and + MCMC/MLE go through `value_for` / `log_prior_from_value` / `factor`, all + verified correct. Exposure is API-level, not search-level. + +**EP-internal callers — all base-space, and mutually consistent:** + +- `MeanField.logpdf` / `logpdf_gradient` (`mean_field.py:294,300`), reached + from `FactorApproximation.__call__` / `func_gradient` + (`mean_field.py:631-633,647`) by the Laplace optimiser; `MeanField` is + itself a `Factor` wrapping `_logpdf`, so `projection(mode)` in the + `from_mode_covariance` `log_norm` calibration (`mean_field.py:404`) and + `AbstractMessage.__call__`/`factor_jacobian` (`abstract.py:392,398`) route + the same way. +- This loop is *coherent*, not buggy: a likelihood factor is a density over + data (measure-free in x), so `fval(x) + Σ log q_base(T(x))` is exactly the + **base-space tilted log density** at `z = T(x)`. The Laplace mode it finds + is the base-space tilted mode, and `from_mode` (`transform_jac` + + `jac.quad`) projects mode and covariance into base space consistently. + The missing `log_det` is the EP loop's working convention, and base-space + Laplace is arguably the better-conditioned choice (unbounded support). + +**The one genuine cross-convention seam — `PriorFactor`:** + +- `PriorFactor` (`declarative/factor/prior.py:23,62`) wraps `prior.factor` — + the *physical* density — as both its factor callable and its + `log_likelihood_function`. Its tilted objective under the numerical + optimiser is therefore + `log π_base(T(x)) + log_det(x) + Σ log q_base(T(x))` — one x-dependent + `log_det(x)` *more* than the coherent base-space tilted density (and one + short of the coherent physical one). Because the #1337 seam strips the + exact-update hooks (`graphical/README.md` roster row), declarative + PriorFactors really do take this numerical path: prior-factor EP updates + optimise a hybrid objective whose mode is neither the base-space nor the + physical tilted mode. + +**Correction to adjudication point 2:** `logpdf_gradient` is *not* a third +convention. Its analytic gradient equals the central-difference derivative +of `logpdf` exactly (−0.520142 both, at the probe point) — the jacobian +multiplication is just the chain rule for the base-space composition +`base.logpdf(T(x))`. So (`logpdf`, `logpdf_gradient`, +`numerical_logpdf_gradient_hessian`) form one self-consistent base-space +family and `factor` is the physical one: **two conventions, not three**. + +**New finding:** `TransformedMessage.factor_gradient` +(`composed_transform.py:360-378`) crashes on first call — it unpacks four +values from `_transform_det_jac`, which returns three. Zero production +callers (the `line_search.py` `factor_gradient` is an unrelated +FactorApproximation-level interface). Filed as `bug/priors/16`. + +**Recommendation (for the human + #1500 to ratify — not actioned):** the fix +is a two-sided contract repair, not a one-line `logpdf` patch: + +1. *EP-internal:* declare base-space `logpdf` the message contract (fix the + `composed_transform.py` module-docstring claim that `logpdf` accumulates + the Jacobian) and make `PriorFactor` consume the base-space density + (`message.logpdf`) instead of `prior.factor`, making the EP loop fully + base-space coherent. This changes prior-factor updates (removes the + spurious `log_det`) but preserves the deliberate-looking base-space + Laplace everywhere else. Belongs with #1500 Q2/Q3. +2. *Public API:* `Prior.logpdf` and `pdf` promise a physical density and + should route through `factor` (or be renamed/documented). Small, + standalone, user-facing correctness fix. + +The other coherent option — adding `log_det` to `logpdf` itself so +everything is physical — touches every EP path at once and shifts EP Laplace +modes to physical-space MAPs; that is a design change that should only be +taken inside the #1500 single-source-density decision. + ## Sequencing Adjudicate alongside the parked single-source-density design (census C1/C4, diff --git a/draft/bug/priors/16_transformed_message_factor_gradient_unpack.md b/draft/bug/priors/16_transformed_message_factor_gradient_unpack.md new file mode 100644 index 00000000..4c80b4cb --- /dev/null +++ b/draft/bug/priors/16_transformed_message_factor_gradient_unpack.md @@ -0,0 +1,59 @@ +# `@PyAutoFit` `TransformedMessage.factor_gradient` crashes on first call + +Type: bug +Target: priors +Difficulty: small +Autonomy: supervised +Priority: normal +Status: found 2026-08-19 during the #1498 caller analysis — not yet issued + +Same shape as census finding A1 (#1331-01): a code path that has never run +end-to-end, dead on arrival. + +## The finding + +`TransformedMessage.factor_gradient` (`composed_transform.py:360-378`, main @ +`21288bb`) unpacks four values from `self._transform_det_jac(x)`: + +```python +x, logd, logd_grad, jacs = self._transform_det_jac(x) +``` + +but `_transform_det_jac` (`composed_transform.py:281-289`) returns three: +`(x, logd, logd_jacs)` where `logd_jacs` is a list of +`(logd_grad, jac)` tuples. Any call raises +`ValueError: not enough values to unpack (expected 4, got 3)`. + +Reproducer on main: + +```python +import autofit as af +af.UniformPrior(0.0, 2.0).message.factor_gradient(1.3) +# ValueError: not enough values to unpack (expected 4, got 3) +``` + +Even past the unpack, the body is wrong for the actual return structure: the +`for jac in reversed(jacs): grad = grad * jac` loop and the final +`grad + logd_grad` assume flat lists that `_transform_det_jac` does not +produce. + +## Exposure + +Zero production callers in `autofit/` — the `factor_gradient` in +`graphical/laplace/line_search.py` is an unrelated +FactorApproximation-level interface, and the only `exp_factor` use is one +test. So this is dead code that crashes if ever exercised; no live search +or EP path is affected. + +## The fix (two options, adjudicate with #1498) + +1. Repair it to be the physical-density gradient companion of `factor` + (value `base.logpdf(T(x)) + log_det`, gradient chain-ruled **plus** the + `log_det` gradient) and add it to the #1497 property sweep so the + gradient is checked against a numerical derivative of `factor`. +2. Delete it — if the #1498/#1500 adjudication lands a single-source + `log_density` contract, a hand-rolled second gradient path is exactly + the kind of duplicate this cleanup exists to remove. + +Do not fix silently ahead of the #1498 contract decision; whichever +convention wins decides whether this method should exist at all. diff --git a/draft/bug/priors/z_features.md b/draft/bug/priors/z_features.md index 63b1605b..787f8fdd 100644 --- a/draft/bug/priors/z_features.md +++ b/draft/bug/priors/z_features.md @@ -5,7 +5,7 @@ Target: priors Difficulty: too-large Autonomy: supervised Priority: normal -Status: phases 1-3 SHIPPED; phase 4 = design (#1500) + parked 14 + finding 15 +Status: phases 1-3 SHIPPED; phase 4 = design (#1500) + parked 14 + findings 15-16 > **2026-08-18 wrap-up sweep.** All nine confirmed bugs (01-08, 10) are fixed > and merged on PyAutoFit main — Phase 1 batch via #1344/PR#1345 (merged @@ -18,6 +18,9 @@ Status: phases 1-3 SHIPPED; phase 4 = design (#1500) + parked 14 + finding 15 > 2026-08-18 update: 09+11 SHIPPED (PR#1499 merged `21288bb`); 12+13 filed as > design issue #1500 (decisions pending); 14 parked behind #1500; new finding > 15 (#1498) awaiting adjudication. +> 2026-08-19 update: the 15 caller analysis is done (adjudication brief in +> the prompt file) and turned up finding 16 (`factor_gradient` dead-on-call), +> drafted but not yet issued. ## Why this folder exists @@ -111,7 +114,8 @@ over them. | 12 | single_source_density_refactor (→ active/) | Each density is encoded in three places (`value_for` / `logpdf` / `log_prior_from_value`) | **design issue FILED 2026-08-18** (bundled with 13) | #1500 | — | | 13 | collapse_prior_and_message (→ active/) | `Prior` and `Message` carry duplicated responsibility | **design issue FILED 2026-08-18** (bundled with 12) | #1500 | — | | 14 | [replace_transform_stack_with_bijectors](14_replace_transform_stack_with_bijectors.md) | Replace hand-rolled `AbstractDensityTransform` with `tfp.bijectors` / `numpyro.transforms` | parked — go/no-go hangs off the #1500 design decision | — | — | -| 15 | [transformed_message_logpdf_jacobian](15_transformed_message_logpdf_jacobian.md) | `TransformedMessage.logpdf`/`pdf` omit the transform Jacobian (new finding from the 09 sweep) | **issue filed 2026-08-18** — awaiting contract adjudication (standalone or inside #1500) | #1498 | — | +| 15 | [transformed_message_logpdf_jacobian](15_transformed_message_logpdf_jacobian.md) | `TransformedMessage.logpdf`/`pdf` omit the transform Jacobian (new finding from the 09 sweep) | **caller analysis complete 2026-08-19** (brief in the prompt: EP loop is coherently base-space; `PriorFactor` is the one hybrid seam; public `Prior.logpdf` mis-promises physical) — awaiting human contract adjudication with #1500 | #1498 | — | +| 16 | [transformed_message_factor_gradient_unpack](16_transformed_message_factor_gradient_unpack.md) | `TransformedMessage.factor_gradient` crashes on first call (unpacks 4 from a 3-tuple; dead code, found by the 15 caller analysis) | drafted 2026-08-19 — issue via `/create_issue` after review; fix-or-delete hangs off the #1498 contract decision | — | — | --- From f5487a3f3a6d47f887418047565c521fe6fab4eb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 15:09:09 +0000 Subject: [PATCH 2/2] prompt: file issue for transformed_message_factor_gradient_unpack (#1501); post 15 caller-analysis brief to PyAutoFit#1498 --- .../16_transformed_message_factor_gradient_unpack.md | 3 ++- draft/bug/priors/z_features.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) rename {draft/bug/priors => active}/16_transformed_message_factor_gradient_unpack.md (93%) diff --git a/draft/bug/priors/16_transformed_message_factor_gradient_unpack.md b/active/16_transformed_message_factor_gradient_unpack.md similarity index 93% rename from draft/bug/priors/16_transformed_message_factor_gradient_unpack.md rename to active/16_transformed_message_factor_gradient_unpack.md index 4c80b4cb..ec9422eb 100644 --- a/draft/bug/priors/16_transformed_message_factor_gradient_unpack.md +++ b/active/16_transformed_message_factor_gradient_unpack.md @@ -5,7 +5,8 @@ Target: priors Difficulty: small Autonomy: supervised Priority: normal -Status: found 2026-08-19 during the #1498 caller analysis — not yet issued +Status: issued 2026-08-19 as PyAutoFit#1501 — awaiting external verification +(fix-or-delete hangs off the #1498 contract decision); do not start dev Same shape as census finding A1 (#1331-01): a code path that has never run end-to-end, dead on arrival. diff --git a/draft/bug/priors/z_features.md b/draft/bug/priors/z_features.md index 787f8fdd..0cae1a39 100644 --- a/draft/bug/priors/z_features.md +++ b/draft/bug/priors/z_features.md @@ -114,8 +114,8 @@ over them. | 12 | single_source_density_refactor (→ active/) | Each density is encoded in three places (`value_for` / `logpdf` / `log_prior_from_value`) | **design issue FILED 2026-08-18** (bundled with 13) | #1500 | — | | 13 | collapse_prior_and_message (→ active/) | `Prior` and `Message` carry duplicated responsibility | **design issue FILED 2026-08-18** (bundled with 12) | #1500 | — | | 14 | [replace_transform_stack_with_bijectors](14_replace_transform_stack_with_bijectors.md) | Replace hand-rolled `AbstractDensityTransform` with `tfp.bijectors` / `numpyro.transforms` | parked — go/no-go hangs off the #1500 design decision | — | — | -| 15 | [transformed_message_logpdf_jacobian](15_transformed_message_logpdf_jacobian.md) | `TransformedMessage.logpdf`/`pdf` omit the transform Jacobian (new finding from the 09 sweep) | **caller analysis complete 2026-08-19** (brief in the prompt: EP loop is coherently base-space; `PriorFactor` is the one hybrid seam; public `Prior.logpdf` mis-promises physical) — awaiting human contract adjudication with #1500 | #1498 | — | -| 16 | [transformed_message_factor_gradient_unpack](16_transformed_message_factor_gradient_unpack.md) | `TransformedMessage.factor_gradient` crashes on first call (unpacks 4 from a 3-tuple; dead code, found by the 15 caller analysis) | drafted 2026-08-19 — issue via `/create_issue` after review; fix-or-delete hangs off the #1498 contract decision | — | — | +| 15 | [transformed_message_logpdf_jacobian](15_transformed_message_logpdf_jacobian.md) | `TransformedMessage.logpdf`/`pdf` omit the transform Jacobian (new finding from the 09 sweep) | **caller analysis complete + posted to #1498 2026-08-19** (EP loop is coherently base-space; `PriorFactor` is the one hybrid seam; public `Prior.logpdf` mis-promises physical) — awaiting external verification + contract adjudication with #1500 | #1498 | — | +| 16 | transformed_message_factor_gradient_unpack (→ active/) | `TransformedMessage.factor_gradient` crashes on first call (unpacks 4 from a 3-tuple; dead code, found by the 15 caller analysis) | **issue filed 2026-08-19** — awaiting external verification; fix-or-delete hangs off the #1498 contract decision | #1501 | — | ---