Skip to content

fix: TransformedMessage.factor_gradient crashes on first call #1501

Description

@Jammy2211

Overview

TransformedMessage.factor_gradient (autofit/messages/composed_transform.py:360-378) crashes on any call: it unpacks four values from self._transform_det_jac(x), which returns three. Found 2026-08-19 while running the caller inventory that #1498 asked for. It has zero production callers in autofit/, so no live search or EP path is affected — this is dead code that fails on first use, the same "never run end-to-end" shape as #1331 finding 01 (LogGaussianPrior.with_limits).

Reproducer on main @ 21288bb:

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)

The offending unpack:

x, logd, logd_grad, jacs = self._transform_det_jac(x)

_transform_det_jac (composed_transform.py:281-289) returns (x, logd, logd_jacs) where logd_jacs is a list of (logd_grad, jac) tuples. Even past the unpack, the body's flat-list handling (the for jac in reversed(jacs): grad = grad * jac loop and the final grad + logd_grad) does not match the structure _transform_det_jac actually produces.

Plan

External verification requested

As with the #1330/#1331 census, this finding is AI-generated. Before any fix, please sanity-check (a) the reproducer, and (b) the zero-production-callers claim — note the factor_gradient in autofit/graphical/laplace/line_search.py is an unrelated FactorApproximation-level interface, not this method, and the only exp_factor use is a single test.

Detailed implementation plan

Affected Repositories

  • PyAutoFit (primary)

Branch Survey

Repository Current Branch Dirty?
./PyAutoFit main clean

Suggested branch: fix/transformed-message-factor-gradient

Implementation Steps

  1. Decide fix-vs-delete per the TransformedMessage.logpdf/pdf omit the transform Jacobian (base-space density at physical coordinates) #1498 contract adjudication.
  2. If repair: rewrite factor_gradient to consume (x, logd, logd_jacs) as _transform_det_jac returns it — accumulate grad = grad * jac and logd_grad contributions per transform (mirroring how logpdf_gradient walks its jacobians), returning (base_logpdf + logd, chain_ruled_grad + total_logd_grad).
  3. If repair: extend test_autofit/mapper/prior/test_prior_properties.py (test: property-based correctness sweep over every Prior subclass #1497 sweep) with a factor_gradient-vs-numerical-derivative-of-factor property over every TransformedMessage-wrapped prior.
  4. If delete: remove factor_gradient (and re-check exp_factor's single test usage while in the file).

Key Files

Original Prompt

Click to expand starting prompt

PyAutoMind draft/bug/priors/16_transformed_message_factor_gradient_unpack.md:

Same shape as census finding A1 (#1331-01): a code path that has never run end-to-end, dead on arrival.

TransformedMessage.factor_gradient (composed_transform.py:360-378, main @ 21288bb) unpacks four values from 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).

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 and add it to the #1497 property sweep; (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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions