Skip to content

Commit

Permalink
Merge pull request #1527 from pints-team/1209-composed-boundaries
Browse files Browse the repository at this point in the history
Adds a ComposedBoundaries, similar to ComposedLogPrior.
  • Loading branch information
MichaelClerx authored Jun 13, 2024
2 parents 40b5c78 + 0711f7c commit 3f2f153
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
## Unreleased

### Added
- [#1527](https://github.com/pints-team/pints/pull/1527) Added a `ComposedBoundaries` class that lets you compose multiple `Boundaries` classes into a higher dimensional one.
- [#1500](https://github.com/pints-team/pints/pull/1500) Added a `CensoredGaussianLogLikelihood` class that calculates the censored Gaussian log-likelihood.
- [#1505](https://github.com/pints-team/pints/pull/1505) Added notes to `ErrorMeasure` and `LogPDF` to say parameters must be real and continuous.
- [#1499](https://github.com/pints-team/pints/pull/1499) Added a log-uniform prior class.
Expand Down
3 changes: 3 additions & 0 deletions docs/source/boundaries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ implementation of the :class:`Boundaries` interface.
Overview:

- :class:`Boundaries`
- :class:`ComposedBoundaries`
- :class:`LogPDFBoundaries`
- :class:`RectangularBoundaries`


.. autoclass:: Boundaries

.. autoclass:: ComposedBoundaries

.. autoclass:: LogPDFBoundaries

.. autoclass:: RectangularBoundaries
1 change: 1 addition & 0 deletions pints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ def version(formatted=False):
#
from ._boundaries import (
Boundaries,
ComposedBoundaries,
LogPDFBoundaries,
RectangularBoundaries,
)
Expand Down
60 changes: 60 additions & 0 deletions pints/_boundaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,66 @@ def sample(self, n=1):
raise NotImplementedError


class ComposedBoundaries(Boundaries):
r"""
Boundaries composed of one or more other :class:`Boundaries`.
The resulting N-dimensional :class:`Boundaries` is composed of one or more
:class:`Boundaries` of dimensions ``N[i] <= N`` such that the sum of all
``N[i]`` equals ``N``.
The dimensionality of the individual boundaries objects does not have to be
the same.
The input parameters of the :class:`ComposedBoundaries` have to be ordered
in the same way as the individual boundaries.
Extends :class:`Boundaries`.
"""

def __init__(self, *boundaries):
# Check if sub-boundaries given
if len(boundaries) < 1:
raise ValueError('Must have at least one sub-boundaries object.')

# Check if boundaries, count dimension
self._n_parameters = 0
self._n_sub = []
for b in boundaries:
if not isinstance(b, pints.Boundaries):
raise ValueError(
'All objects in ``boundaries`` must extend'
' pints.Boundaries.')
n = b.n_parameters()
self._n_parameters += n
self._n_sub.append(n)

# Store
self._boundaries = boundaries

def check(self, x):
""" See :meth:`pints.Boundaries.check()`. """
i = 0
for b, n in zip(self._boundaries, self._n_sub):
if not b.check(x[i:i + n]):
return False
i += n
return True

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

def sample(self, n=1):
""" See :meth:`pints.Boundaries.sample()`. """
x = np.zeros((n, self._n_parameters))
i = 0
for b, m in zip(self._boundaries, self._n_sub):
x[:, i:i + m] = b.sample(n)
i += m
return x


class RectangularBoundaries(Boundaries):
"""
Represents a set of lower and upper boundaries for model parameters.
Expand Down
23 changes: 23 additions & 0 deletions pints/tests/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,26 @@ def to_model(self, q):

def to_search(self, p):
return p[::-1]


class UnitCircleBoundaries2D(pints.Boundaries):
"""
A 2d set of circular boundaries (similar but different to the hypersphere
boundaries above...) with radius 1.
"""
def __init__(self, mx=0, my=0):
self._c = np.array([mx, my])

def n_parameters(self):
return 2

def check(self, parameters):
x, y = parameters # Let Python do exception
return np.sum((parameters - self._c)**2) < 1

def sample(self, n=1):
r = np.sqrt(np.random.uniform(0, 1, size=(n,)))
t = np.random.uniform(0, np.pi, size=(n, ))
return np.array([self._c[0] + r * np.cos(t),
self._c[1] + r * np.sin(t)]).T

70 changes: 69 additions & 1 deletion pints/tests/test_boundaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@
# copyright notice and full license details.
#
import unittest
import pints

import numpy as np

import pints

from shared import UnitCircleBoundaries2D


class TestRectangularBoundaries(unittest.TestCase):
"""
Expand Down Expand Up @@ -123,5 +127,69 @@ def __call__(self, x):
b.sample(2)


class TestComposedBoundaries(unittest.TestCase):
"""
Tests boundaries composed of other boundaries.
"""
def test_composed_boundaries(self):
p = UnitCircleBoundaries2D()
q = pints.RectangularBoundaries([-5, 0, 5], [-4, 2, 10])
r = UnitCircleBoundaries2D(30, -20)
b = pints.ComposedBoundaries(p, q, r)

# Test selected points
self.assertEqual(b.n_parameters(), 7)
x = [0.5, 0.5] + [-4.9, 0.1, 5.2] + [30.1, -20.8]
self.assertTrue(p.check(x[:2]))
self.assertTrue(q.check(x[2:5]))
self.assertTrue(r.check(x[5:]))
self.assertTrue(b.check(x))
x = [0.9, 0.5] + [-4.9, 0.1, 5.2] + [30.1, -20.8]
self.assertFalse(p.check(x[:2]))
self.assertTrue(q.check(x[2:5]))
self.assertTrue(r.check(x[5:]))
self.assertFalse(b.check(x))
x = [0.5, 0.5] + [-4.9, -0.1, 5.2] + [30.1, -20.8]
self.assertTrue(p.check(x[:2]))
self.assertFalse(q.check(x[2:5]))
self.assertTrue(r.check(x[5:]))
self.assertFalse(b.check(x))
x = [0.5, 0.5] + [-4.9, 0.1, 5.2] + [30.1, 20.8]
self.assertTrue(p.check(x[:2]))
self.assertTrue(q.check(x[2:5]))
self.assertFalse(r.check(x[5:]))
self.assertFalse(b.check(x))

# Test points sampled from the individual sub boundaries
xs = np.concatenate(
(p.sample(100), q.sample(100), r.sample(100)), axis=1)
for x in xs:
self.assertTrue(b.check(x))

# Test points sampled from the composed prior
for x in b.sample(100):
self.assertTrue(b.check(x))
self.assertTrue(p.check(x[:2]))
self.assertTrue(q.check(x[2:5]))
self.assertTrue(r.check(x[5:]))

# Just one boundary reduces to original
b = pints.ComposedBoundaries(q)
self.assertEqual(b.n_parameters(), 3)
self.assertTrue(b.check([-4.5, 1, 7]))
self.assertFalse(b.check([-4.5, 3, 7]))
for x in b.sample(100):
self.assertTrue(q.check(x))

# No boundaries is not allowed
self.assertRaisesRegex(
ValueError, 'at least one', pints.ComposedBoundaries)

# Components must be boundaries
self.assertRaisesRegex(
ValueError, 'must extend', pints.ComposedBoundaries,
p, q, pints.ExponentialLogPrior(3))


if __name__ == '__main__':
unittest.main()
40 changes: 39 additions & 1 deletion pints/tests/test_test_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
import sys
import unittest

from shared import StreamCapture, TemporaryDirectory
import numpy as np

from shared import StreamCapture, TemporaryDirectory, UnitCircleBoundaries2D


class TestSharedTestModule(unittest.TestCase):
Expand Down Expand Up @@ -74,6 +76,42 @@ def test_temporary_directory(self):
# Test runtime error when used outside of context
self.assertRaises(RuntimeError, d.path, 'hello.txt')

def test_unit_circle_boundaries_2d(self):
# Tests the 2d unit circle boundaries used in composed boundaries
# testing.
c = UnitCircleBoundaries2D()
self.assertEqual(c.n_parameters(), 2)
self.assertTrue(c.check([0, 0]))
self.assertTrue(c.check([0.5, 0]))
self.assertTrue(c.check([-0.5, 0]))
self.assertTrue(c.check([0, 0.5]))
self.assertTrue(c.check([0, -0.5]))
self.assertFalse(c.check([1, 0]))
self.assertFalse(c.check([-1, 0]))
self.assertFalse(c.check([0, 1]))
self.assertFalse(c.check([0, -1]))
self.assertTrue(c.check([1 - 1e-12, 0]))
self.assertTrue(c.check([-1 + 1e-12, 0]))
self.assertTrue(c.check([0, 1 - 1e-12]))
self.assertTrue(c.check([0, -1 + 1e-12]))
x, y = np.cos(0.123), np.sin(0.123)
self.assertFalse(c.check([x, y]))
xs = c.sample(100)
self.assertEqual(xs.shape, (100, 2))
for i, x in enumerate(xs):
self.assertTrue(c.check(x))

c = UnitCircleBoundaries2D(-5, 2)
self.assertEqual(c.n_parameters(), 2)
self.assertFalse(c.check([0, 0]))
self.assertTrue(c.check([-5, 2]))
self.assertTrue(c.check([-5, 3 - 1e-12]))
for i, x in enumerate(c.sample(10)):
self.assertTrue(c.check(x))

self.assertRaises(Exception, c.check, [0, 0, 0])
self.assertRaises(Exception, c.check, [0])


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

0 comments on commit 3f2f153

Please sign in to comment.