Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ All notable changes to this project will be documented in this file.


## Unreleased

### Added
- [#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.
- [#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.
- [#1724](https://github.com/pints-team/pints/pull/1724) Added `PooledLogLikelihood` and `SumOfIndependentLogLikelihoods`.
- [#1716](https://github.com/pints-team/pints/pull/1716) PINTS is now tested on Python 3.14.
Expand All @@ -15,10 +15,12 @@ All notable changes to this project will be documented in this file.
- [#1724](https://github.com/pints-team/pints/pull/1724) Some methods that accepted `LogPDF`s now specifically require `LogLikelihood`s (e.g. `LogPosterior`, `NestedController`).
- [#1713](https://github.com/pints-team/pints/pull/1713) PINTS now requires matplotlib 2.2 or newer.
### Deprecated
- [#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.
- [#1724](https://github.com/pints-team/pints/pull/1724) The classes `PooledLogPDF` and `SumOfIndependentLogPDFs` are deprecated, in favour of `PooledLogLikelihood` and `SumOfIndependentLogLikelihoods` respectively.
- [#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.
### Removed
### Fixed
- [#1730](https://github.com/pints-team/pints/pull/1730) Log-likelihoods are now transformed correctly.
- [#1713](https://github.com/pints-team/pints/pull/1713) Fixed Numpy 2.4.1 compatibility issues.
- [#1690](https://github.com/pints-team/pints/pull/1690) Fixed bug in optimisation controller if population size left at `None`.

Expand Down
21 changes: 21 additions & 0 deletions docs/source/transformations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,25 @@ Example::
transform = pints.LogTransformation(n_parameters)
mcmc = pints.MCMCController(log_posterior, n_chains, x0, transform=transform)

Transformation types:

- :class:`ComposedTransformation`
- :class:`IdentityTransformation`
- :class:`LogitTransformation`
- :class:`LogTransformation`
- :class:`RectangularBoundariesTransformation`
- :class:`ScalingTransformation`
- :class:`UnitCubeTransformation`

Transformed classes:

- :class:`Transformation`
- :class:`TransformedBoundaries`
- :class:`TransformedErrorMeasure`
- :class:`TransformedLogLikelihood`
- :class:`TransformedLogPDF`
- :class:`TransformedLogPrior`


Transformation types
********************
Expand Down Expand Up @@ -58,6 +77,8 @@ Transformed objects

.. autoclass:: TransformedErrorMeasure

.. autoclass:: TransformedLogLikelihood

.. autoclass:: TransformedLogPDF

.. autoclass:: TransformedLogPrior
Expand Down
1 change: 1 addition & 0 deletions pints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ def version(formatted=False):
Transformation,
TransformedBoundaries,
TransformedErrorMeasure,
TransformedLogLikelihood,
TransformedLogPDF,
TransformedLogPrior,
TransformedRectangularBoundaries,
Expand Down
5 changes: 3 additions & 2 deletions pints/_optimisers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ def xbest(self):
def x_best(self):
"""
Returns the best position seen during an optimisation, i.e. the point
for which the minimal error or maximum LogPDF was observed.
for which the minimal error or maximum probability density was
observed.
"""
raise NotImplementedError

Expand All @@ -245,7 +246,7 @@ def x_guessed(self):
Returns the optimiser's current best estimate of where the optimum is.

For many optimisers, this will simply be the point for which the
minimal error or maximum LogPDF was observed, so that
minimal error or maximum probability density was observed, so that
``x_guessed = x_best``. However, optimisers like :class:`pints.CMAES`
and its derivatives, maintain a separate "best guess" value that does
not necessarily correspond to any of the points evaluated during the
Expand Down
93 changes: 89 additions & 4 deletions pints/_transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,46 @@ class Transformation():
"""
def convert_log_pdf(self, log_pdf):
"""
Returns a transformed log-PDF class.
Returns a transformed :class:`pints.LogPDF`.

If `log_pdf` is a :class:`LogPrior`, a :class:`TransformedLogPrior`
will be returned, which also transforms the output of the
:meth:`sample` method.

If `log_pdf` is a :class:`LogLikelihood`, a
:class:`TransformedLogLikelihood` is returned, which is assumed to be
invariant with respect to the transform (because it is a probability of
the data, not the parameters). For all other types (including
``LogPrior``) a non-invariant transform is used, see
:class:`TransformedLogPDF` for details.
"""
if isinstance(log_pdf, pints.LogLikelihood):
return TransformedLogLikelihood(log_pdf, self)
if isinstance(log_pdf, pints.LogPrior):
return TransformedLogPrior(log_pdf, self)
return TransformedLogPDF(log_pdf, self)

def convert_log_prior(self, log_prior):
"""
Returns a transformed log-prior class.
Deprecated function: Use :meth:`convert_log_pdf` instead.
"""
# Deprecated on 2026-02-06
import warnings
warnings.warn(
'The method `convert_log_prior` is deprecated. Please use'
' `convert_log_pdf` instead (which will automatically detect'
' detect LogPDF subtypes).')
return TransformedLogPrior(log_prior, self)

def convert_error_measure(self, error_measure):
"""
Returns a transformed error measure class.
Returns a transformed :class:`pints.ErrorMeasure`.
"""
return TransformedErrorMeasure(error_measure, self)

def convert_boundaries(self, boundaries):
"""
Returns a transformed boundaries class.
Returns a transformed :class:`pints.Boundaries` object.
"""
if isinstance(boundaries, pints.RectangularBoundaries):
if self.elementwise():
Expand Down Expand Up @@ -1212,6 +1233,70 @@ def sample(self, n):
return qs


class TransformedLogLikelihood(pints.LogLikelihood):
r"""
A :class:`pints.LogLikelihood` that accepts parameters in a transformed
search space.

Unlike a :class:`TransformedLogPDF`, a likelihood (a measure of how well
the data, $\boldsymbol{x}, is explained by a model, given fixed parameters)
is invariant to a parameter transform (but not to a data transform),
and so no Jacobian term appears. Instead for some :class:`Transformation`
:math:`\boldsymbol{q}=\boldsymbol{f}(\boldsymbol{p})`

.. math::
\underset{\boldsymbol{q}}{\text{max}}(\log L(\boldsymbol{q}|\boldsymbol{x})) =
\underset{\boldsymbol{q}}{\text{max}}(\log L(\boldsymbol{f}^{-1}(\boldsymbol{q}|\boldsymbol{x}))).

For the first order sensitivity, the transformation is done using

.. math::
\frac{\partial \log L(\boldsymbol{q}|\boldsymbol{x})}{\partial q_i} &=
\frac{\partial \log L(\boldsymbol{f}^{-1}(\boldsymbol{q})|\boldsymbol{x})}{\partial q_i}\\
&= \sum_l \frac{\partial \log L(\boldsymbol{p|\boldsymbol{x}})}{\partial p_l}
\frac{\partial p_l}{\partial q_i}.

Extends :class:`pints.LogLikelihood`.

Parameters
----------
log_likelihood
A :class:`pints.LogLikelihood`.
transformation
A :class:`pints.Transformation`.
"""
def __init__(self, log_likelihood, transformation):
self._log_likelihood = log_likelihood
self._transform = transformation
self._n_parameters = self._log_pdf.n_parameters()
if self._transform.n_parameters() != self._n_parameters:
raise ValueError('Number of parameters for log_likelihood and '
'transformation must match.')

def __call__(self, q):
# Compute LogLikelihood in the model space
return self._log_likelihood(self._transform.to_model(q))

def evaluateS1(self, q):
""" See :meth:`LogPDF.evaluateS1()`. """

# Get parameters in the model space
p = self._transform.to_model(q)

# Call evaluateS1 of LogLikelihood in the model space
logl, dlogl_nojac = self._log_likelihood.evaluateS1(p)

# Calculate the S1 using change of variable (see ErrorMeasure above)
jacobian = self._transform.jacobian(q)
dlogl = np.matmul(dlogl_nojac, jacobian) # Jacobian must be 2nd term

return logl, dlogl

def n_parameters(self):
""" See :meth:`LogPDF.n_parameters()`. """
return self._n_parameters


class UnitCubeTransformation(ScalingTransformation):
"""
Maps a parameter space onto the unit (hyper)cube.
Expand Down
10 changes: 9 additions & 1 deletion pints/tests/test_transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -954,7 +954,8 @@ def test_transformed_log_prior(self):
d = 2
t = pints.LogTransformation(2)
r = pints.UniformLogPrior([0.1, 0.1], [0.9, 0.9])
tr = t.convert_log_prior(r)
tr = t.convert_log_pdf(r)
self.assertIsInstance(tr, pints.TransformedLogPrior)

# Test sample
n = 1
Expand All @@ -966,6 +967,13 @@ def test_transformed_log_prior(self):
self.assertEqual(x.shape, (n, d))
self.assertTrue(np.all(x < 0.))

# Test deprecated alias
with warnings.catch_warnings(record=True) as w:
tr = t.convert_log_prior(r)
self.assertEqual(len(w), 1)
self.assertIn('deprecated', str(w[0].message))
self.assertIsInstance(tr, pints.TransformedLogPrior)


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