Skip to content

Commit 27e20ac

Browse files
authored
Merge pull request #1730 from pints-team/1705-transformed-likelihoods-2
Adding TransformedLogLikelihood class
2 parents 3d51e0b + a6a9920 commit 27e20ac

6 files changed

Lines changed: 163 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ All notable changes to this project will be documented in this file.
44

55

66
## Unreleased
7-
87
### Added
8+
- [#1730](https://github.com/pints-team/pints/pull/1730) Added a `TransformedLogLikelihood` class which, unlike generic `TransformedLogPDF` objects, is invariant with respect to the parameters.
99
- [#1724](https://github.com/pints-team/pints/pull/1724) The `LogLikelihood` class has been reintroduced, to differentiate between probabilities of parameters and probabilities of data, given fixed parameters.
1010
- [#1724](https://github.com/pints-team/pints/pull/1724) Added `PooledLogLikelihood` and `SumOfIndependentLogLikelihoods`.
1111
- [#1716](https://github.com/pints-team/pints/pull/1716) PINTS is now tested on Python 3.14.
@@ -15,11 +15,13 @@ All notable changes to this project will be documented in this file.
1515
- [#1724](https://github.com/pints-team/pints/pull/1724) Some methods that accepted `LogPDF`s now specifically require `LogLikelihood`s (e.g. `LogPosterior`, `NestedController`).
1616
- [#1713](https://github.com/pints-team/pints/pull/1713) PINTS now requires matplotlib 2.2 or newer.
1717
### Deprecated
18+
- [#1730](https://github.com/pints-team/pints/pull/1730) The method `Transformation.convert_log_prior` is deprecated, as `convert_log_pdf` now calls the appropriate method based on the type of `LogPDF` passed in.
1819
- [#1724](https://github.com/pints-team/pints/pull/1724) The classes `PooledLogPDF` and `SumOfIndependentLogPDFs` are deprecated, in favour of `PooledLogLikelihood` and `SumOfIndependentLogLikelihoods` respectively.
1920
- [#1508](https://github.com/pints-team/pints/pull/1508) The methods `OptimisationController.max_unchanged_iterations` and `set_max_unchanged_iterations` are deprecated, in favour of `function_tolerance` and `set_function_tolerance` respectively.
2021
### Removed
2122
- [#1731](https://github.com/pints-team/pints/pull/1731) The method `ToyLogPDF.suggested_bounds()` and its implementations have been removed.
2223
### Fixed
24+
- [#1730](https://github.com/pints-team/pints/pull/1730) Log-likelihoods are now transformed correctly.
2325
- [#1713](https://github.com/pints-team/pints/pull/1713) Fixed Numpy 2.4.1 compatibility issues.
2426
- [#1690](https://github.com/pints-team/pints/pull/1690) Fixed bug in optimisation controller if population size left at `None`.
2527

docs/source/transformations.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,25 @@ Example::
3131
transform = pints.LogTransformation(n_parameters)
3232
mcmc = pints.MCMCController(log_posterior, n_chains, x0, transform=transform)
3333

34+
Transformation types:
35+
36+
- :class:`ComposedTransformation`
37+
- :class:`IdentityTransformation`
38+
- :class:`LogitTransformation`
39+
- :class:`LogTransformation`
40+
- :class:`RectangularBoundariesTransformation`
41+
- :class:`ScalingTransformation`
42+
- :class:`UnitCubeTransformation`
43+
44+
Transformed classes:
45+
46+
- :class:`Transformation`
47+
- :class:`TransformedBoundaries`
48+
- :class:`TransformedErrorMeasure`
49+
- :class:`TransformedLogLikelihood`
50+
- :class:`TransformedLogPDF`
51+
- :class:`TransformedLogPrior`
52+
3453

3554
Transformation types
3655
********************
@@ -58,6 +77,8 @@ Transformed objects
5877

5978
.. autoclass:: TransformedErrorMeasure
6079

80+
.. autoclass:: TransformedLogLikelihood
81+
6182
.. autoclass:: TransformedLogPDF
6283

6384
.. autoclass:: TransformedLogPrior

pints/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ def version(formatted=False):
275275
Transformation,
276276
TransformedBoundaries,
277277
TransformedErrorMeasure,
278+
TransformedLogLikelihood,
278279
TransformedLogPDF,
279280
TransformedLogPrior,
280281
TransformedRectangularBoundaries,

pints/_optimisers/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,8 @@ def xbest(self):
236236
def x_best(self):
237237
"""
238238
Returns the best position seen during an optimisation, i.e. the point
239-
for which the minimal error or maximum LogPDF was observed.
239+
for which the minimal error or maximum probability density was
240+
observed.
240241
"""
241242
raise NotImplementedError
242243

@@ -245,7 +246,7 @@ def x_guessed(self):
245246
Returns the optimiser's current best estimate of where the optimum is.
246247
247248
For many optimisers, this will simply be the point for which the
248-
minimal error or maximum LogPDF was observed, so that
249+
minimal error or maximum probability density was observed, so that
249250
``x_guessed = x_best``. However, optimisers like :class:`pints.CMAES`
250251
and its derivatives, maintain a separate "best guess" value that does
251252
not necessarily correspond to any of the points evaluated during the

pints/_transformation.py

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,46 @@ class Transformation():
3232
"""
3333
def convert_log_pdf(self, log_pdf):
3434
"""
35-
Returns a transformed log-PDF class.
35+
Returns a transformed :class:`pints.LogPDF`.
36+
37+
If `log_pdf` is a :class:`LogPrior`, a :class:`TransformedLogPrior`
38+
will be returned, which also transforms the output of the
39+
:meth:`sample` method.
40+
41+
If `log_pdf` is a :class:`LogLikelihood`, a
42+
:class:`TransformedLogLikelihood` is returned, which is assumed to be
43+
invariant with respect to the transform (because it is a probability of
44+
the data, not the parameters). For all other types (including
45+
``LogPrior``) a non-invariant transform is used, see
46+
:class:`TransformedLogPDF` for details.
3647
"""
48+
if isinstance(log_pdf, pints.LogLikelihood):
49+
return TransformedLogLikelihood(log_pdf, self)
50+
if isinstance(log_pdf, pints.LogPrior):
51+
return TransformedLogPrior(log_pdf, self)
3752
return TransformedLogPDF(log_pdf, self)
3853

3954
def convert_log_prior(self, log_prior):
4055
"""
41-
Returns a transformed log-prior class.
56+
Deprecated function: Use :meth:`convert_log_pdf` instead.
4257
"""
58+
# Deprecated on 2026-02-06
59+
import warnings
60+
warnings.warn(
61+
'The method `convert_log_prior` is deprecated. Please use'
62+
' `convert_log_pdf` instead (which will automatically detect'
63+
' detect LogPDF subtypes).')
4364
return TransformedLogPrior(log_prior, self)
4465

4566
def convert_error_measure(self, error_measure):
4667
"""
47-
Returns a transformed error measure class.
68+
Returns a transformed :class:`pints.ErrorMeasure`.
4869
"""
4970
return TransformedErrorMeasure(error_measure, self)
5071

5172
def convert_boundaries(self, boundaries):
5273
"""
53-
Returns a transformed boundaries class.
74+
Returns a transformed :class:`pints.Boundaries` object.
5475
"""
5576
if isinstance(boundaries, pints.RectangularBoundaries):
5677
if self.elementwise():
@@ -1212,6 +1233,74 @@ def sample(self, n):
12121233
return qs
12131234

12141235

1236+
class TransformedLogLikelihood(pints.LogLikelihood):
1237+
r"""
1238+
A :class:`pints.LogLikelihood` that accepts parameters in a transformed
1239+
search space.
1240+
1241+
Unlike a :class:`TransformedLogPDF`, a likelihood (a measure of how well
1242+
the data, $\boldsymbol{x}, is explained by a model, given fixed parameters)
1243+
is invariant to a parameter transform (but not to a data transform),
1244+
and so no Jacobian term appears. Instead for some :class:`Transformation`
1245+
:math:`\boldsymbol{q}=\boldsymbol{f}(\boldsymbol{p})`
1246+
1247+
.. math::
1248+
\underset{\boldsymbol{q}}{\text{max}}(
1249+
\log L(\boldsymbol{q}|\boldsymbol{x})) =
1250+
\underset{\boldsymbol{q}}{\text{max}}(
1251+
\log L(\boldsymbol{f}^{-1}(\boldsymbol{q}|\boldsymbol{x}))).
1252+
1253+
For the first order sensitivity, the transformation is done using
1254+
1255+
.. math::
1256+
\frac{\partial \log L(\boldsymbol{q}|\boldsymbol{x})}{\partial q_i} &=
1257+
\frac{\partial \log L(\boldsymbol{f}^{-1}(\boldsymbol{q})
1258+
| \boldsymbol{x})}{\partial q_i} \\
1259+
&= \sum_l \frac{
1260+
\partial \log L(\boldsymbol{p|\boldsymbol{x}})}{\partial p_l}
1261+
\frac{\partial p_l}{\partial q_i}.
1262+
1263+
Extends :class:`pints.LogLikelihood`.
1264+
1265+
Parameters
1266+
----------
1267+
log_likelihood
1268+
A :class:`pints.LogLikelihood`.
1269+
transformation
1270+
A :class:`pints.Transformation`.
1271+
"""
1272+
def __init__(self, log_likelihood, transformation):
1273+
self._log_likelihood = log_likelihood
1274+
self._transform = transformation
1275+
self._n_parameters = self._log_likelihood.n_parameters()
1276+
if self._transform.n_parameters() != self._n_parameters:
1277+
raise ValueError('Number of parameters for log_likelihood and '
1278+
'transformation must match.')
1279+
1280+
def __call__(self, q):
1281+
# Compute LogLikelihood in the model space
1282+
return self._log_likelihood(self._transform.to_model(q))
1283+
1284+
def evaluateS1(self, q):
1285+
""" See :meth:`LogPDF.evaluateS1()`. """
1286+
1287+
# Get parameters in the model space
1288+
p = self._transform.to_model(q)
1289+
1290+
# Call evaluateS1 of LogLikelihood in the model space
1291+
logl, dlogl_nojac = self._log_likelihood.evaluateS1(p)
1292+
1293+
# Calculate the S1 using change of variable (see ErrorMeasure above)
1294+
jacobian = self._transform.jacobian(q)
1295+
dlogl = np.matmul(dlogl_nojac, jacobian) # Jacobian must be 2nd term
1296+
1297+
return logl, dlogl
1298+
1299+
def n_parameters(self):
1300+
""" See :meth:`LogPDF.n_parameters()`. """
1301+
return self._n_parameters
1302+
1303+
12151304
class UnitCubeTransformation(ScalingTransformation):
12161305
"""
12171306
Maps a parameter space onto the unit (hyper)cube.

pints/tests/test_transformation.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,7 @@ def test_transformed_log_pdf(self):
931931
j = np.diag(x)
932932
log_j_det = -2.9857819427008230
933933
tr = t.convert_log_pdf(r)
934+
self.assertIsInstance(tr, pints.TransformedLogPDF)
934935

935936
# Test before and after transformed give the same result
936937
self.assertAlmostEqual(tr(tx), r(x) + log_j_det)
@@ -954,7 +955,8 @@ def test_transformed_log_prior(self):
954955
d = 2
955956
t = pints.LogTransformation(2)
956957
r = pints.UniformLogPrior([0.1, 0.1], [0.9, 0.9])
957-
tr = t.convert_log_prior(r)
958+
tr = t.convert_log_pdf(r)
959+
self.assertIsInstance(tr, pints.TransformedLogPrior)
958960

959961
# Test sample
960962
n = 1
@@ -966,6 +968,45 @@ def test_transformed_log_prior(self):
966968
self.assertEqual(x.shape, (n, d))
967969
self.assertTrue(np.all(x < 0.))
968970

971+
# Test deprecated alias
972+
with warnings.catch_warnings(record=True) as w:
973+
tr = t.convert_log_prior(r)
974+
self.assertEqual(len(w), 1)
975+
self.assertIn('deprecated', str(w[0].message))
976+
self.assertIsInstance(tr, pints.TransformedLogPrior)
977+
978+
def test_transformed_log_likelihood(self):
979+
# Test TransformedLogLikelihood class
980+
981+
class Fakelihood(pints.toy.TwistedGaussianLogPDF, pints.LogLikelihood):
982+
pass
983+
984+
t = pints.LogTransformation(2)
985+
r = Fakelihood(2, 0.01)
986+
self.assertIsInstance(r, pints.LogLikelihood)
987+
988+
x = [0.05, 1.01]
989+
tx = [-2.9957322735539909, 0.0099503308531681]
990+
j = np.diag(x)
991+
tr = t.convert_log_pdf(r)
992+
self.assertIsInstance(tr, pints.TransformedLogLikelihood)
993+
self.assertEqual(tr.n_parameters(), r.n_parameters())
994+
995+
# Test before and after transformed give the same result
996+
self.assertAlmostEqual(tr(tx), r(x))
997+
998+
# Test evaluateS1()
999+
rx, s1 = r.evaluateS1(x)
1000+
trtx, trts1 = tr.evaluateS1(tx)
1001+
self.assertTrue(np.allclose(trtx, rx))
1002+
ts1 = np.matmul(s1, j)
1003+
self.assertTrue(np.allclose(trts1, ts1))
1004+
1005+
# Test wrong number of parameters
1006+
self.assertRaisesRegex(
1007+
ValueError, 'Number of parameters',
1008+
pints.TransformedLogLikelihood, r, pints.LogTransformation(3))
1009+
9691010

9701011
if __name__ == '__main__':
9711012
unittest.main()

0 commit comments

Comments
 (0)