Found while implementing the #1497 property sweep (priors/messages census follow-up; hubs #1330/#1331). Same failure shape as #1331-04: a generic-interface density path is silently wrong while the direct paths are right.
The finding
TransformedMessage inherits MessageInterface.logpdf (natural-parameter path). Its overrides forward natural_parameters, calc_log_base_measure and to_canonical_form to the base message — to_canonical_form is @transform-decorated, so the physical input is mapped to base coordinates — but no log_det change-of-variables term is ever added. The result: logpdf(x) at a physical x returns the base-space density at the mapped point, not the physical density. Meanwhile TransformedMessage.factor(x) does it correctly (base.logpdf(transform(x)) + log_det).
Reproduction on main @ 7d4d931 (numbers exact):
import numpy as np
from scipy.integrate import quad
import autofit as af
m = af.UniformPrior(0.0, 1.0).message # TransformedMessage(UniformNormalMessage, LinearShift)
m.logpdf(np.asarray(0.7)) # -1.05644 — physical density is 1.0, expect 0.0
m.factor(np.asarray(0.7)) # 0.0 — correct
quad(lambda x: float(m.pdf(np.asarray(x))), 0.0, 1.0) # 0.282095 = 1/(2*sqrt(pi)), not 1.0
quad(lambda x: float(np.exp(m.factor(np.asarray(x)))), 0.0, 1.0) # 1.000000
For LogUniformPrior(0.01, 100) the generic-path pdf integrates to 15.83. Every TransformedMessage-wrapped prior (Uniform, LogUniform, LogGaussian) is affected; plain NormalMessage / TruncatedNormalMessage priors are not.
Doc contradiction
The composed_transform.py module docstring (added in #1334) states: "Evaluating logpdf(x) at a physical x runs the same three steps in reverse (physical → base) via _transform, accumulating the log-Jacobian log_det of each transform for the change of variables." The code does not accumulate it. Either the docstring describes the intended contract (then logpdf needs the log_det term) or logpdf is deliberately base-space for EP message arithmetic (then the docstring and pdf() are wrong/misleading).
What needs adjudication before a fix
- Which callers consume
TransformedMessage.logpdf/pdf vs factor? The EP factor-graph path uses factor/exp_factor (correct). If any EP projection / log_norm path evaluates logpdf on transformed messages at physical coordinates, it inherits this error.
logpdf_gradient returns the base log_likelihood with a Jacobian-corrected gradient — a third convention (gradient physical, value base-space) worth settling in the same pass.
- Note
value_for, cdf, log_prior_from_value and factor are all verified correct — user-facing sampling and MCMC/MLE log-priors are unaffected. The exposure is anything treating transformed_message.pdf() as a physical density.
The #1497 property tests assert the physical-density path via factor for transformed messages and cite this issue at the site; when this is resolved they can be tightened to assert logpdf directly.
Sequencing suggestion: adjudicate alongside the parked single-source-density design (census C1/C4, prompts 12/13) — this is a fourth density-convention divergence of exactly the kind that design exists to eliminate.
Found while implementing the #1497 property sweep (priors/messages census follow-up; hubs #1330/#1331). Same failure shape as #1331-04: a generic-interface density path is silently wrong while the direct paths are right.
The finding
TransformedMessageinheritsMessageInterface.logpdf(natural-parameter path). Its overrides forwardnatural_parameters,calc_log_base_measureandto_canonical_formto the base message —to_canonical_formis@transform-decorated, so the physical input is mapped to base coordinates — but nolog_detchange-of-variables term is ever added. The result:logpdf(x)at a physicalxreturns the base-space density at the mapped point, not the physical density. MeanwhileTransformedMessage.factor(x)does it correctly (base.logpdf(transform(x)) + log_det).Reproduction on
main@7d4d931(numbers exact):For
LogUniformPrior(0.01, 100)the generic-path pdf integrates to 15.83. EveryTransformedMessage-wrapped prior (Uniform, LogUniform, LogGaussian) is affected; plainNormalMessage/TruncatedNormalMessagepriors are not.Doc contradiction
The
composed_transform.pymodule docstring (added in #1334) states: "Evaluatinglogpdf(x)at a physicalxruns the same three steps in reverse (physical → base) via_transform, accumulating the log-Jacobianlog_detof each transform for the change of variables." The code does not accumulate it. Either the docstring describes the intended contract (thenlogpdfneeds thelog_detterm) orlogpdfis deliberately base-space for EP message arithmetic (then the docstring andpdf()are wrong/misleading).What needs adjudication before a fix
TransformedMessage.logpdf/pdfvsfactor? The EP factor-graph path usesfactor/exp_factor(correct). If any EP projection /log_normpath evaluateslogpdfon transformed messages at physical coordinates, it inherits this error.logpdf_gradientreturns the baselog_likelihoodwith a Jacobian-corrected gradient — a third convention (gradient physical, value base-space) worth settling in the same pass.value_for,cdf,log_prior_from_valueandfactorare all verified correct — user-facing sampling and MCMC/MLE log-priors are unaffected. The exposure is anything treatingtransformed_message.pdf()as a physical density.The #1497 property tests assert the physical-density path via
factorfor transformed messages and cite this issue at the site; when this is resolved they can be tightened to assertlogpdfdirectly.Sequencing suggestion: adjudicate alongside the parked single-source-density design (census C1/C4, prompts 12/13) — this is a fourth density-convention divergence of exactly the kind that design exists to eliminate.