Skip to content

test: property-based correctness sweep over every Prior subclass #1497

Description

@Jammy2211

Overview

Phase 3 of the priors/messages cleanup (census: PyAutoMind/draft/research/autofit/priors_and_messages_math_audit.md, finding C3; prompt bug/priors/09; hub #1331). All nine confirmed bugs from the audit are fixed and merged (#1345, #1348), each locked by a pointwise regression test. This task adds the general property-based sweep over every Prior subclass — the tests that would have caught the original LogUniform sign bug (#1266), the with_limits crash, the Gamma from_mode inversion and the TruncatedNormal normalisation gap in one stroke — so the next latent math bug of this shape cannot survive. It also folds in the remaining small half of bug/priors/11: the LinearShiftTransform class docstring (the reversal-convention half shipped via #1334).

Plan

  • Add a parametrised property-test module sweeping every concrete Prior subclass (Uniform, LogUniform, Gaussian, LogGaussian, TruncatedGaussian; ≥2 parameterisations each).
  • Five properties: inverse-CDF round-trip; normalised pdf integrates to 1 (locking the Priors & messages: 9 confirmed bugs — guidance wanted on 5 decisions #1331 Option A drop-constants + log_normalisation() contract, and extending check_dist_norm-style coverage to TruncatedNormalMessage and transformed priors — the exact gap that hid the log-partition bug); log-prior gradient matches the message-density gradient by finite differences (constants cancel — the sign-convention canary); with_limits round-trip for every family; from_mode(m, V) mean/variance invariants for NormalMessage and GammaMessage with V≠1 discriminating points.
  • NumPy-only at the library level (house rule; EP-level integration coverage was Phase 3 of the EP framework review and complements this).
  • Add the LinearShiftTransform docstring: shift/scale are physical-space parameters, the stored parent Jacobian is 1/scale because the transform runs physical → base, and the log_det sign follows.
  • Expect all tests to PASS on current main; any failure is a new finding to file separately, not to bundle here.
Detailed implementation plan

Affected Repositories

  • PyAutoFit (primary)

Branch Survey

Repository Current Branch Dirty?
/workspace/pyautofit main @ 7d4d931 (shallow clone, cloud session) clean

PyAutoFit is also claimed by two in-flight tasks (stored-sample-reconstruction-guardautofit/non_linear/samples/; version-stamp-sync-guardsautofit/__init__.py) — file-disjoint from this task's surface (test_autofit/mapper/prior/, autofit/messages/transform.py), proceeding under the documented disjoint-override convention.

Suggested branch: feature/prior-property-tests

Implementation Steps

  1. New test_autofit/mapper/prior/test_prior_properties.py:
    • all_priors() fixture list: UniformPrior(0,1), UniformPrior(-3,7.5), GaussianPrior(0,1), GaussianPrior(2.5,0.3), LogUniformPrior(1e-2,1e2), LogGaussianPrior(0,1), LogGaussianPrior(1,0.5), TruncatedGaussianPrior(0,1,-2,2), TruncatedGaussianPrior(1,0.5,0,3). Skip containers/point-masses (TuplePrior, Constant, DeferredArgument).
    • P1 — inverse CDF: prior.cdf(prior.value_for(u)) ≈ u for u ∈ {0.01, 0.1, 0.3, 0.5, 0.7, 0.9, 0.99}, abs=1e-6.
    • P2 — normalisation: ∫ exp(log_prior_from_value(x) + log_normalisation()) dx ≈ 1 over the support (infinite limits truncated via value_for(1e-9) / value_for(1-1e-9); scipy.integrate.quad, abs=1e-3). Also ∫ message.pdf ≈ 1 where the generic exponential-family path exists — extends check_dist_norm to TruncatedNormalMessage and TransformedMessage-wrapped priors (the Priors & messages: 9 confirmed bugs — guidance wanted on 5 decisions #1331-04 gap).
    • P3 — gradient consistency: central finite difference of log_prior_from_value matches central finite difference of message.logpdf at x = value_for(u), u ∈ {0.3, 0.5, 0.7} (rtol=1e-2, atol=1e-3); constants cancel, so this catches exactly the sign/convention drift class of fix: log_prior_from_value sign-convention bug across Prior subclasses #1266.
    • P4 — with_limits: constructs for every family (regression breadth for Priors & messages: 9 confirmed bugs — guidance wanted on 5 decisions #1331-01); for limit-respecting families value_for(1e-6)/value_for(1-1e-6) lie within [lower, upper]; LogGaussianPrior.with_limits asserts construction + positive support only (the shipped fix drops the kwargs by design).
    • P5 — from_mode: NormalMessage.from_mode(m, V) and GammaMessage.from_mode(m, V) reproduce .mean ≈ m, .variance ≈ V at V≠1 discriminating points (e.g. (2, 0.25), (2, 4.0)) — locks the Priors & messages: 9 confirmed bugs — guidance wanted on 5 decisions #1331-D3 mean+variance invariant.
  2. autofit/messages/transform.pyLinearShiftTransform class docstring (bug/priors/11 §2): kwargs describe physical space; stored parent Jacobian is DiagonalMatrix(1/scale) because transform() maps physical → base; log_det = -log(scale) accordingly.
  3. Run: pytest test_autofit/mapper/prior/ test_autofit/messages/ test_autofit/graphical/functionality/test_messages.py.

Key Files

  • test_autofit/mapper/prior/test_prior_properties.py — new property sweep
  • autofit/messages/transform.pyLinearShiftTransform docstring
  • test_autofit/graphical/functionality/test_messages.py — pattern source (check_dist_norm, tolerance precedents); unchanged
  • autofit/mapper/prior/abstract.pylog_normalisation() contract (P2's anchor)

Original Prompt

Click to expand starting prompt (bug/priors/09_prior_property_tests.md)

@PyAutoFit Add property-based correctness tests for every Prior subclass

Type: bug
Target: priors
Difficulty: large
Autonomy: supervised
Priority: normal
Status: formalised

Found during the priors/messages audit (see
PyAutoPrompt/autofit/priors_and_messages_math_audit.md, finding C3).

Prerequisite: Prompts 01-08 should be acked (and ideally merged)
first. The point of these tests is to lock in the fixes — adding them
before the fixes would just produce a long list of red tests with no
clear action.

Problem

The audit found three real bugs (LogGaussianPrior.with_limits crash,
GammaMessage.from_mode wrong formula, TruncatedNormalMessage pdf
not normalised) that would each have been caught by a single
property-based test. None of them were caught because the existing
test suite is hand-rolled per-class and covers each method in
isolation.

The original LogUniformPrior sign-convention bug (e95295b83) is
in the same category — it would have been caught by a single test:
"for every prior, the analytic gradient of log_prior_from_value
matches a finite-difference gradient".

Wider context — what exists already

@PyAutoFit/test_autofit/graphical/functionality/test_messages.py:

  • check_dist_norm(dist) — uses scipy.integrate.quad to verify
    pdf integrates to 1. Run on NormalMessage, BetaMessage,
    GammaMessage, LogNormalMessage only.
    Not run on
    TruncatedNormalMessage, LogGaussianPrior, any Prior subclass,
    or any TransformedMessage-wrapped distribution. That's why
    prompt 04's bug survived.

  • check_log_normalisation(ms) — verifies the product-of-messages
    log normalisation matches numerical integration.

  • check_numerical_gradient_hessians(message, x=None) — verifies
    analytic gradient and Hessian match finite differences. Same limited
    coverage as above.

So the patterns are there; they just don't sweep every prior.

Proposed scope

Add test_autofit/mapper/prior/test_prior_properties.py (and/or
test_autofit/messages/test_message_properties.py) with the five
properties above. Parametrise over every concrete subclass.

For each property, pick the tolerance carefully:

  • Integrals: abs=1e-3 is enough to catch all the audit findings;
    tighter would slow CI.
  • Inverse-CDF round-trip: abs=1e-6 works for double precision.
  • Gradient finite-difference: rtol=1e-2, atol=1e-3 (existing
    check_numerical_gradient_hessians uses these).

Fable verdict (2026-07-08, PyAutoFit main @ 0f26ff2; PyAutoFit#1330)

Verdict: still wanted, unchanged — schedule after 01–08 land.
Coverage gap re-verified: check_dist_norm-style sweeps in
test_autofit/graphical/functionality/test_messages.py still exclude
TruncatedNormalMessage, LogGaussianPrior, prior subclasses and
transformed messages — which is why 04 survived. Keep library-level property
tests numpy-only (house rule); EP-level integration coverage is Phase 3 of
research/graphical_ep/ep_framework_review.md and complements, not
replaces, this.

(Full prompt with the test-sketch code block: PyAutoMind/active/09_prior_property_tests.md. Folded-in second half: PyAutoMind/draft/bug/priors/11_transformed_message_semantics_doc.md §2 only — the LinearShiftTransform docstring.)

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