Skip to content

Commit 37c4778

Browse files
Merge pull request #1700 from pints-team/1691-pints-logpdf-storage
LogPDF storage issue with multi-chain methods
2 parents f248aa6 + 8db1e23 commit 37c4778

20 files changed

+130
-29
lines changed

pints/_mcmc/__init__.py

+14-5
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,8 @@ def run(self):
735735

736736
if reply is not None:
737737
# Unpack reply into position, evaluation, and status
738+
# Note that evaluation is (fy, dfy) with sensitivities
739+
# enabled.
738740
y, fy, accepted = reply
739741

740742
# Inverse transform to model space if transformation is
@@ -754,7 +756,10 @@ def run(self):
754756
if store_evaluations:
755757
# If accepted, update log_pdf and prior for logging
756758
if accepted:
757-
current_logpdf[i] = fy
759+
if self._needs_sensitivities:
760+
current_logpdf[i] = fy[0]
761+
else:
762+
current_logpdf[i] = fy
758763
if prior is not None:
759764
current_prior[i] = prior(y)
760765

@@ -818,7 +823,10 @@ def run(self):
818823
# Check if accepted, if so, update log_pdf and
819824
# prior to be logged
820825
if accepted[i]:
821-
current_logpdf[i] = fys[i]
826+
if self._needs_sensitivities:
827+
current_logpdf[i] = fys[i][0]
828+
else:
829+
current_logpdf[i] = fys[i]
822830
if prior is not None:
823831
current_prior[i] = prior(ys[i])
824832

@@ -1034,9 +1042,10 @@ def set_log_pdf_storage(self, store_in_memory=False):
10341042
Store :class:`LogPDF` evaluations in memory as they are generated.
10351043
10361044
By default, evaluations of the :class:`LogPDF` are not stored. This
1037-
method can be used to enable storage of the evaluations for the
1038-
accepted samples.
1039-
After running, evaluations can be obtained using :meth:`evaluations()`.
1045+
method can be used to enable (or disable) storage for the accepted
1046+
samples, in memory.
1047+
1048+
After running, evaluations can be obtained using :meth:`log_pdfs()`.
10401049
"""
10411050
self._evaluations_in_memory = bool(store_in_memory)
10421051

pints/_mcmc/_adaptive_covariance.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class AdaptiveCovarianceMC(pints.SingleChainMCMC):
2323
A basic implementation is provided for each, which extending methods can
2424
choose to override.
2525
26-
Extends :class:`SingleChainMCMC`.
26+
Extends :class:`SingleChainMCMC`, does not use sensitivities.
2727
"""
2828

2929
def __init__(self, x0, sigma0=None):

pints/_mcmc/_differential_evolution.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class DifferentialEvolutionMCMC(pints.MultiChainMCMC):
2929
If ``x_proposed / x[i,r] > u ~ U(0,1)``, then
3030
``x[i+1,r] = x_proposed``; otherwise, ``x[i+1,r] = x[i]``.
3131
32-
Extends :class:`MultiChainMCMC`.
32+
Extends :class:`MultiChainMCMC`, does not use sensitivities.
3333
3434
.. note::
3535
This sampler requires a number of chains :math:`n \ge 3`, and

pints/_mcmc/_dram_ac.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class DramACMC(pints.AdaptiveCovarianceMC):
5050
are then adapted as ``[scale_1, scale_2] * sigma``, where the
5151
scale factors are set using ``set_sigma_scale``.
5252
53-
*Extends:* :class:`GlobalAdaptiveCovarianceMC`
53+
*Extends:* :class:`GlobalAdaptiveCovarianceMC`, does not use sensitivities.
5454
5555
References
5656
----------

pints/_mcmc/_dream.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class DreamMCMC(pints.MultiChainMCMC):
5353
5454
Here b > 0, b* > 0, 1 >= p_g >= 0, 1 >= CR >= 0.
5555
56-
Extends :class:`MultiChainMCMC`.
56+
Extends :class:`MultiChainMCMC`, does not use sensitivities.
5757
5858
References
5959
----------

pints/_mcmc/_emcee_hammer.py

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ class EmceeHammerMCMC(pints.MultiChainMCMC):
3434
``1 / sqrt(z)`` if ``z`` is in ``[1 / a, a]`` or to 0, otherwise (where
3535
``a`` is a parameter with default value ``2``).
3636
37+
Extends :class:`MultiChainMCMC`, does not use sensitivities.
38+
3739
References
3840
----------
3941
.. [1] "emcee: The MCMC Hammer", Daniel Foreman-Mackey, David W. Hogg,

pints/_mcmc/_haario_ac.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class HaarioACMC(pints.AdaptiveCovarianceMC):
4444
log lambda = log lambda + gamma (alpha - self._target_acceptance)
4545
gamma = adaptation_count^-eta
4646
47-
Extends :class:`AdaptiveCovarianceMC`.
47+
Extends :class:`AdaptiveCovarianceMC`, does not use sensitivities.
4848
4949
References
5050
----------

pints/_mcmc/_haario_bardenet_ac.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class HaarioBardenetACMC(pints.AdaptiveCovarianceMC):
4242
log lambda = log lambda + gamma (alpha - self._target_acceptance)
4343
gamma = adaptation_count^-eta
4444
45-
Extends :class:`AdaptiveCovarianceMC`.
45+
Extends :class:`AdaptiveCovarianceMC`, does not use sensitivities.
4646
4747
References
4848
----------

pints/_mcmc/_hamiltonian.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class HamiltonianMCMC(pints.SingleChainMCMC):
4646
In particular, the algorithm we implement follows eqs. (4.14)-(4.16) in
4747
[1]_, since we allow different epsilon according to dimension.
4848
49-
Extends :class:`SingleChainMCMC`.
49+
Extends :class:`SingleChainMCMC`, this method uses sensitivities.
5050
5151
References
5252
----------
@@ -190,7 +190,10 @@ def name(self):
190190
return 'Hamiltonian Monte Carlo'
191191

192192
def needs_sensitivities(self):
193-
""" See :meth:`pints.MCMCSampler.needs_sensitivities()`. """
193+
"""
194+
This method requires sensitivities.
195+
196+
See :meth:`pints.MCMCSampler.needs_sensitivities()`. """
194197
return True
195198

196199
def scaled_epsilon(self):

pints/_mcmc/_mala.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class MALAMCMC(pints.SingleChainMCMC):
6767
where :math:`\epsilon' = \epsilon \sqrt{M}` is given by the initial value
6868
of ``sigma0``.
6969
70-
Extends :class:`SingleChainMCMC`.
70+
Extends :class:`SingleChainMCMC`, this method uses sensitivities.
7171
7272
References
7373
----------
@@ -181,7 +181,11 @@ def name(self):
181181
return 'Metropolis-Adjusted Langevin Algorithm (MALA)'
182182

183183
def needs_sensitivities(self):
184-
""" See :meth:`pints.MCMCSampler.needs_sensitivities()`. """
184+
"""
185+
This method requires sensitivities.
186+
187+
See :meth:`pints.MCMCSampler.needs_sensitivities()`.
188+
"""
185189
return True
186190

187191
def n_hyper_parameters(self):

pints/_mcmc/_metropolis.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class MetropolisRandomWalkMCMC(pints.SingleChainMCMC):
2424
2525
here Sigma is the covariance matrix of the proposal.
2626
27-
Extends :class:`SingleChainMCMC`.
27+
Extends :class:`SingleChainMCMC`, does not use sensitivities.
2828
2929
References
3030
----------

pints/_mcmc/_monomial_gamma_hamiltonian.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class MonomialGammaHamiltonianMCMC(pints.SingleChainMCMC):
6363
In particular, the algorithm we implement follows eqs. (4.14)-(4.16) in
6464
[2]_, since we allow different epsilon according to dimension.
6565
66-
Extends :class:`SingleChainMCMC`.
66+
Extends :class:`SingleChainMCMC`, this method uses sensitivities.
6767
6868
References
6969
----------
@@ -297,7 +297,11 @@ def name(self):
297297
return 'Monomial-Gamma Hamiltonian Monte Carlo'
298298

299299
def needs_sensitivities(self):
300-
""" See :meth:`pints.MCMCSampler.needs_sensitivities()`. """
300+
"""
301+
This method requires sensitivities.
302+
303+
See also :meth:`pints.MCMCSampler.needs_sensitivities()`.
304+
"""
301305
return True
302306

303307
def n_hyper_parameters(self):

pints/_mcmc/_nuts.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ class NoUTurnMCMC(pints.SingleChainMCMC):
499499
500500
Note: This sampler is only supported on Python versions 3.3 and newer.
501501
502-
Extends :class:`SingleChainMCMC`.
502+
Extends :class:`SingleChainMCMC`, this method uses sensitivities.
503503
504504
References
505505
----------
@@ -664,7 +664,11 @@ def name(self):
664664
return 'No-U-Turn MCMC'
665665

666666
def needs_sensitivities(self):
667-
""" See :meth:`pints.MCMCSampler.needs_sensitivities()`. """
667+
"""
668+
This method requires sensitivities.
669+
670+
See :meth:`pints.MCMCSampler.needs_sensitivities()`.
671+
"""
668672
return True
669673

670674
def number_adaption_steps(self):

pints/_mcmc/_population.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class PopulationMCMC(pints.SingleChainMCMC):
3939
``delta_T = 1 / num_temperatures``, and the chain with ``T_i = 0`` is the
4040
one whose target distribution we want to sample.
4141
42-
Extends :class:`SingleChainMCMC`.
42+
Extends :class:`SingleChainMCMC`, does not use sensitivities.
4343
4444
References
4545
----------

pints/_mcmc/_rao_blackwell_ac.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class RaoBlackwellACMC(pints.AdaptiveCovarianceMC):
3737
Y_t+1 ~ N(theta_t, lambda * sigma0) rather than
3838
Y_t+1 ~ N(theta_t, sigma0)
3939
40-
Extends :class:`AdaptiveCovarianceMC`.
40+
Extends :class:`AdaptiveCovarianceMC`, does not use sensitivities.
4141
4242
References
4343
----------

pints/_mcmc/_relativistic.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class RelativisticMCMC(pints.SingleChainMCMC):
5757
In particular, the algorithm we implement follows eqs. in section 2.1 of
5858
[1]_.
5959
60-
Extends :class:`SingleChainMCMC`.
60+
Extends :class:`SingleChainMCMC`, this method uses sensitivities.
6161
6262
References
6363
----------
@@ -325,7 +325,11 @@ def name(self):
325325
return 'Relativistic MCMC'
326326

327327
def needs_sensitivities(self):
328-
""" See :meth:`pints.MCMCSampler.needs_sensitivities()`. """
328+
"""
329+
This method requires sensitivities.
330+
331+
See :meth:`pints.MCMCSampler.needs_sensitivities()`.
332+
"""
329333
return True
330334

331335
def _sample_momentum(self):

pints/_mcmc/_slice_doubling.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class SliceDoublingMCMC(pints.SingleChainMCMC):
109109
:math:`\epsilon \sim \text{exp}(1)` and define the slice as
110110
:math:`S = {x : z < g(x)}`.
111111
112-
Extends :class:`SingleChainMCMC`.
112+
Extends :class:`SingleChainMCMC`, does not use sensitivities.
113113
114114
References
115115
----------

pints/_mcmc/_slice_rank_shrinking.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class SliceRankShrinkingMCMC(pints.SingleChainMCMC):
6363
:math:`\epsilon \sim \text{exp}(1)` and define the slice as
6464
:math:`S = {x : z < log f(x)}`.
6565
66-
Extends :class:`SingleChainMCMC`.
66+
Extends :class:`SingleChainMCMC`, this method uses sensitivities.
6767
6868
References
6969
----------
@@ -156,7 +156,11 @@ def name(self):
156156
return 'Slice Sampling - Covariance-Adaptive: Rank Shrinking.'
157157

158158
def needs_sensitivities(self):
159-
""" See :meth:`pints.MCMCSampler.needs_sensitivities()`. """
159+
"""
160+
This method requires sensitivities.
161+
162+
See :meth:`pints.MCMCSampler.needs_sensitivities()`.
163+
"""
160164
return True
161165

162166
def n_hyper_parameters(self):

pints/_mcmc/_slice_stepout.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class SliceStepoutMCMC(pints.SingleChainMCMC):
120120
:math:`\epsilon \sim \text{exp}(1)` and define the slice as
121121
:math:`S = {x : z < g(x)}`.
122122
123-
Extends :class:`SingleChainMCMC`.
123+
Extends :class:`SingleChainMCMC`, does not use sensitivities.
124124
125125
References
126126
----------

pints/tests/test_mcmc_controller.py

+68-1
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,26 @@ def test_log_pdf_storage_in_memory_single(self):
690690
likelihoods = [self.log_likelihood(x) for x in chain]
691691
self.assertTrue(np.all(evals[i] == likelihoods))
692692

693+
# Test with a sensitivity-using method
694+
mcmc = pints.MCMCController(
695+
self.log_posterior, n_chains, xs, method=pints.MALAMCMC)
696+
mcmc.set_max_iterations(n_iterations)
697+
mcmc.set_log_to_screen(False)
698+
mcmc.set_log_pdf_storage(True)
699+
chains = mcmc.run()
700+
evals = mcmc.log_pdfs()
701+
self.assertEqual(len(evals.shape), 3)
702+
self.assertEqual(evals.shape[0], n_chains)
703+
self.assertEqual(evals.shape[1], n_iterations)
704+
self.assertEqual(evals.shape[2], 3)
705+
for i, chain in enumerate(chains):
706+
posteriors = [self.log_posterior(x) for x in chain]
707+
self.assertTrue(np.all(evals[i, :, 0] == posteriors))
708+
likelihoods = [self.log_likelihood(x) for x in chain]
709+
self.assertTrue(np.all(evals[i, :, 1] == likelihoods))
710+
priors = [self.log_prior(x) for x in chain]
711+
self.assertTrue(np.all(evals[i, :, 2] == priors))
712+
693713
# Test disabling again
694714
mcmc = pints.MCMCController(self.log_posterior, n_chains, xs)
695715
mcmc.set_max_iterations(n_iterations)
@@ -706,7 +726,9 @@ def test_log_pdf_storage_in_memory_multi(self):
706726
x0 = np.array(self.real_parameters) * 1.05
707727
x1 = np.array(self.real_parameters) * 1.15
708728
x2 = np.array(self.real_parameters) * 0.95
709-
xs = [x0, x1, x2]
729+
x3 = np.array(self.real_parameters) * 0.95
730+
x4 = np.array(self.real_parameters) * 0.95
731+
xs = [x0, x1, x2, x3, x4]
710732
n_chains = len(xs)
711733
n_iterations = 100
712734
meth = pints.DifferentialEvolutionMCMC
@@ -737,6 +759,28 @@ def test_log_pdf_storage_in_memory_multi(self):
737759
priors = [self.log_prior(x) for x in chain]
738760
self.assertTrue(np.all(evals[i, :, 2] == priors))
739761

762+
# Test with a sensitivity-using multi-chain method
763+
# We don't have any of these!
764+
mcmc = pints.MCMCController(
765+
self.log_posterior, n_chains, xs,
766+
method=FakeMultiChainSamplerWithSensitivities)
767+
mcmc.set_max_iterations(n_iterations)
768+
mcmc.set_log_to_screen(False)
769+
mcmc.set_log_pdf_storage(True)
770+
chains = mcmc.run()
771+
evals = mcmc.log_pdfs()
772+
self.assertEqual(len(evals.shape), 3)
773+
self.assertEqual(evals.shape[0], n_chains)
774+
self.assertEqual(evals.shape[1], n_iterations)
775+
self.assertEqual(evals.shape[2], 3)
776+
for i, chain in enumerate(chains):
777+
posteriors = [self.log_posterior(x) for x in chain]
778+
self.assertTrue(np.all(evals[i, :, 0] == posteriors))
779+
likelihoods = [self.log_likelihood(x) for x in chain]
780+
self.assertTrue(np.all(evals[i, :, 1] == likelihoods))
781+
priors = [self.log_prior(x) for x in chain]
782+
self.assertTrue(np.all(evals[i, :, 2] == priors))
783+
740784
# Test with a loglikelihood
741785
mcmc = pints.MCMCController(
742786
self.log_likelihood, n_chains, xs, method=meth)
@@ -1651,6 +1695,29 @@ def tell(self, fx):
16511695
return None if x is None else (x, fx, np.array([True] * self._n))
16521696

16531697

1698+
class FakeMultiChainSamplerWithSensitivities(pints.MultiChainMCMC):
1699+
"""
1700+
Fake sampler that pretends to be a multi-chain method that uses
1701+
sensitivities. At the moment (2024-12-18) we don't have these in PINTS, but
1702+
we need to check that code potentially handling this type of sampler exists
1703+
or raises not-implemented errors.
1704+
"""
1705+
def ask(self):
1706+
self._xs = self._x0 + 1e-3 * np.random.uniform(
1707+
size=(self._n_chains, self._n_parameters))
1708+
return self._xs
1709+
1710+
def current_log_pdfs(self):
1711+
return self._fxs
1712+
1713+
def tell(self, fxs):
1714+
self._fxs = fxs
1715+
return self._xs, self._fxs, [True] * self._n_chains
1716+
1717+
def needs_sensitivities(self):
1718+
return True
1719+
1720+
16541721
class TestMCMCControllerSingleChainStorage(unittest.TestCase):
16551722
"""
16561723
Tests storage of samples and evaluations to disk, running with a

0 commit comments

Comments
 (0)