Skip to content

Commit 2db2880

Browse files
authored
Merge pull request #1424 from pints-team/25-drop-triangle-transform
Stop using triangle wave transform in optimisations
2 parents 532e9eb + 4165b6e commit 2db2880

File tree

10 files changed

+52
-165
lines changed

10 files changed

+52
-165
lines changed

CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,20 @@ All notable changes to this project will be documented in this file.
55
## Unreleased
66

77
### Added
8-
- [#1420](https://github.com/pints-team/pints/pull/1420) The `Optimisation` objects now distinguish between a best-visited point (`x_best`, with score `f_best`) and a best-guessed point (`x_guessed`, with approximate score `f_guessed`). For most optimisers, the two values are equivalent. As before, the `OptimisationController` now tracks `x_best` and `f_best` by default, but this can be modified using the methods `set_f_guessed_tracking` and `f_guessed_tracking`.
9-
-
8+
- [#1420](https://github.com/pints-team/pints/pull/1420) The `Optimiser` class now distinguishes between a best-visited point (`x_best`, with score `f_best`) and a best-guessed point (`x_guessed`, with approximate score `f_guessed`). For most optimisers, the two values are equivalent. The `OptimisationController` still tracks `x_best` and `f_best` by default, but this can be modified using the methods `set_f_guessed_tracking` and `f_guessed_tracking`.
9+
1010
### Changed
11+
- [#1424](https://github.com/pints-team/pints/pull/1424) Fixed a bug in PSO that caused it to use more particles than advertised.
12+
- [#1424](https://github.com/pints-team/pints/pull/1424) xNES, SNES, PSO, and BareCMAES no longer use a `TriangleWaveTransform` to handle rectangular boundaries (this was found to lead to optimisers diverging in some cases).
13+
1114
### Deprecated
15+
1216
### Removed
17+
- [#1424](https://github.com/pints-team/pints/pull/1424) Removed the `TriangleWaveTransform` class previously used in some optimisers.
18+
1319
### Fixed
1420

21+
1522
## [0.4.0] - 2021-12-07
1623

1724
### Added

docs/source/optimisers/boundary_transformations.rst

Lines changed: 0 additions & 8 deletions
This file was deleted.

docs/source/optimisers/index.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ or the :class:`OptimisationController` class.
1616
running
1717
base_classes
1818
convenience_methods
19-
boundary_transformations
2019
cmaes_bare
2120
cmaes
2221
gradient_descent

pints/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ def version(formatted=False):
170170
optimise,
171171
Optimiser,
172172
PopulationBasedOptimiser,
173-
TriangleWaveTransform,
174173
)
175174
from ._optimisers._cmaes import CMAES
176175
from ._optimisers._cmaes_bare import BareCMAES

pints/_optimisers/__init__.py

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -973,43 +973,6 @@ def optimise(
973973
function, x0, sigma0, boundaries, transformation, method).run()
974974

975975

976-
class TriangleWaveTransform(object):
977-
"""
978-
Transforms from unbounded to (rectangular) bounded parameter space using a
979-
periodic triangle-wave transform.
980-
981-
Note: The transform is applied _inside_ optimisation methods, there is no
982-
need to wrap this around your own problem or score function.
983-
984-
This can be applied as a transformation on ``x`` to implement _rectangular_
985-
boundaries in methods with no natural boundary mechanism. It effectively
986-
mirrors the search space at every boundary, leading to a continuous (but
987-
non-smooth) periodic landscape. While this effectively creates an infinite
988-
number of minima/maxima, each one maps to the same point in parameter
989-
space.
990-
991-
It should work well for methods that maintain a single search position or a
992-
single search distribution (e.g. :class:`CMAES`, :class:`xNES`,
993-
:class:`SNES`), which will end up in one of the many mirror images.
994-
However, for methods that use independent search particles (e.g.
995-
:class:`PSO`) it could lead to a scattered population, with different
996-
particles exploring different mirror images. Other strategies should be
997-
used for such problems.
998-
"""
999-
1000-
def __init__(self, boundaries):
1001-
self._lower = boundaries.lower()
1002-
self._upper = boundaries.upper()
1003-
self._range = self._upper - self._lower
1004-
self._range2 = 2 * self._range
1005-
1006-
def __call__(self, x):
1007-
y = np.remainder(x - self._lower, self._range2)
1008-
z = np.remainder(y, self._range)
1009-
return ((self._lower + z) * (y < self._range)
1010-
+ (self._upper - z) * (y >= self._range))
1011-
1012-
1013976
def curve_fit(f, x, y, p0, boundaries=None, threshold=None, max_iter=None,
1014977
max_unchanged=200, verbose=False, parallel=False, method=None):
1015978
"""

pints/_optimisers/_cmaes_bare.py

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,6 @@ def __init__(self, x0, sigma0=0.1, boundaries=None):
8383
/ gamma(self._n_parameters / 2)
8484
)
8585

86-
# Optional transformation to within-the-boundaries
87-
self._boundary_transform = None
88-
8986
def ask(self):
9087
""" See :meth:`Optimiser.ask()`. """
9188
# Initialise on first call
@@ -106,19 +103,14 @@ def ask(self):
106103
# Samples from N(mu, eta**2 * C)
107104
self._xs = np.array([self._mu + self._eta * y for y in self._ys])
108105

109-
# Apply boundaries; creating safe points for evaluation
110-
# Rectangular boundaries? Then perform boundary transform
111-
if self._boundary_transform is not None:
112-
self._xs = self._boundary_transform(self._xs)
113-
114-
# Manual boundaries? Then pass only xs that are within bounds
115-
if self._manual_boundaries:
106+
# Boundaries? Then only pass user xs that are within bounds
107+
if self._boundaries is not None:
116108
self._user_ids = np.nonzero(
117109
[self._boundaries.check(x) for x in self._xs])
118110
self._user_xs = self._xs[self._user_ids]
119111
if len(self._user_xs) == 0: # pragma: no cover
120-
warnings.warn('All points requested by CMA-ES are outside the'
121-
' boundaries.')
112+
warnings.warn('All points requested by BareCMAES are outside'
113+
' the boundaries.')
122114
else:
123115
self._user_xs = self._xs
124116

@@ -161,14 +153,6 @@ def _initialise(self):
161153
"""
162154
assert (not self._running)
163155

164-
# Create boundary transform, or use manual boundary checking
165-
self._manual_boundaries = False
166-
if isinstance(self._boundaries, pints.RectangularBoundaries):
167-
self._boundary_transform = pints.TriangleWaveTransform(
168-
self._boundaries)
169-
elif self._boundaries is not None:
170-
self._manual_boundaries = True
171-
172156
# Parent generation population size
173157
# The parameter parent_pop_size is the mu in the papers. It represents
174158
# the size of a parent population used to update our paramters.
@@ -280,8 +264,8 @@ def tell(self, fx):
280264
npo = self._population_size
281265
npa = self._parent_pop_size
282266

283-
# Manual boundaries? Then reconstruct full fx vector
284-
if self._manual_boundaries and len(fx) < npo:
267+
# Boundaries? Then reconstruct full fx vector
268+
if self._boundaries is not None and len(fx) < npo:
285269
user_fx = fx
286270
fx = np.ones((npo, )) * float('inf')
287271
fx[self._user_ids] = user_fx
@@ -377,6 +361,4 @@ def x_best(self):
377361

378362
def x_guessed(self):
379363
""" See :meth:`Optimiser.x_guessed()`. """
380-
if self._boundary_transform is not None:
381-
return self._boundary_transform(self._mu)
382364
return np.array(self._mu, copy=True)

pints/_optimisers/_pso.py

Lines changed: 18 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,22 @@ def _initialise(self):
113113

114114
# Set initial positions
115115
self._xs.append(np.array(self._x0, copy=True))
116+
117+
# Attempt to sample n - 1 points from the boundaries
116118
if self._boundaries is not None:
117-
# Attempt to sample n - 1 points from the boundaries
118119
try:
119120
self._xs.extend(
120121
self._boundaries.sample(self._population_size - 1))
121122
except NotImplementedError:
122-
# Not all boundaries implement sampling
123+
# Not all boundaries implement sampling.
123124
pass
125+
124126
# If we couldn't sample from the boundaries, use gaussian sampling
125127
# around x0.
126-
for i in range(1, self._population_size):
127-
self._xs.append(np.random.normal(self._x0, self._sigma0))
128-
self._xs = np.array(self._xs, copy=True)
128+
if len(self._xs) < self._population_size:
129+
for i in range(self._population_size - 1):
130+
self._xs.append(np.random.normal(self._x0, self._sigma0))
131+
self._xs = np.array(self._xs)
129132

130133
# Set initial velocities
131134
for i in range(self._population_size):
@@ -141,29 +144,15 @@ def _initialise(self):
141144
self._fg = float('inf')
142145
self._pg = self._xs[0]
143146

144-
# Create boundary transform, or use manual boundary checking
145-
self._manual_boundaries = False
146-
self._boundary_transform = None
147-
if isinstance(self._boundaries, pints.RectangularBoundaries):
148-
self._boundary_transform = pints.TriangleWaveTransform(
149-
self._boundaries)
150-
elif self._boundaries is not None:
151-
self._manual_boundaries = True
152-
153-
# Create safe xs to pass to user
154-
if self._boundary_transform is not None:
155-
# Rectangular boundaries? Then apply transform to xs
156-
self._xs = self._boundary_transform(self._xs)
157-
if self._manual_boundaries:
158-
# Manual boundaries? Then filter out out-of-bounds points from xs
147+
# Boundaries? Then filter out out-of-bounds points from xs
148+
self._user_xs = self._xs
149+
if self._boundaries is not None:
159150
self._user_ids = np.nonzero(
160151
[self._boundaries.check(x) for x in self._xs])
161152
self._user_xs = self._xs[self._user_ids]
162153
if len(self._user_xs) == 0: # pragma: no cover
163154
warnings.warn(
164155
'All initial PSO particles are outside the boundaries.')
165-
else:
166-
self._user_xs = self._xs
167156

168157
# Set local/global exploration balance
169158
self.set_local_global_balance()
@@ -194,8 +183,8 @@ def running(self):
194183
def set_local_global_balance(self, r=0.5):
195184
"""
196185
Set the balance between local and global exploration for each particle,
197-
using a parameter `r` such that `r = 1` is a fully local search and
198-
`r = 0` is a fully global search.
186+
using a parameter ``r`` such that ``r = 1`` is a fully local search and
187+
``r = 0`` is a fully global search.
199188
"""
200189
if self._running:
201190
raise Exception('Cannot change settings during run.')
@@ -234,8 +223,8 @@ def tell(self, fx):
234223
raise Exception('ask() not called before tell()')
235224
self._ready_for_tell = False
236225

237-
# Manual boundaries? Then reconstruct full fx vector
238-
if self._manual_boundaries and len(fx) < self._population_size:
226+
# Boundaries? Then reconstruct full fx vector
227+
if self._boundaries is not None and len(fx) < self._population_size:
239228
user_fx = fx
240229
fx = np.ones((self._population_size, )) * float('inf')
241230
fx[self._user_ids] = user_fx
@@ -265,19 +254,14 @@ def tell(self, fx):
265254
# Update position
266255
self._xs[i] += self._vs[i]
267256

268-
# Create safe xs to pass to user
269-
if self._boundary_transform is not None:
270-
# Rectangular boundaries? Then apply transform to xs
271-
self._user_xs = self._xs = self._boundary_transform(self._xs)
272-
elif self._manual_boundaries:
273-
# Manual boundaries? Then filter out out-of-bounds points from xs
257+
# Boundaries? Then filter out out-of-bounds points from xs
258+
self._user_xs = self._xs
259+
if self._boundaries is not None:
274260
self._user_ids = np.nonzero(
275261
[self._boundaries.check(x) for x in self._xs])
276262
self._user_xs = self._xs[self._user_ids]
277263
if len(self._user_xs) == 0: # pragma: no cover
278264
warnings.warn('All PSO particles are outside the boundaries.')
279-
else:
280-
self._user_xs = self._xs
281265

282266
# Update global best score
283267
i = np.argmin(self._fl)

pints/_optimisers/_snes.py

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,6 @@ def __init__(self, x0, sigma0=None, boundaries=None):
5353
# We don't have f(mu), so we approximate it by max f(sample)
5454
self._f_guessed = float('inf')
5555

56-
# Optional transformation to within-the-boundaries
57-
self._boundary_transform = None
58-
5956
def ask(self):
6057
""" See :meth:`Optimiser.ask()`. """
6158
# Initialise on first call
@@ -70,12 +67,8 @@ def ask(self):
7067
for i in range(self._population_size)])
7168
self._xs = self._mu + self._sigmas * self._ss
7269

73-
# Create safe xs to pass to user
74-
if self._boundary_transform is not None:
75-
# Rectangular boundaries? Then perform boundary transform
76-
self._xs = self._boundary_transform(self._xs)
77-
if self._manual_boundaries:
78-
# Manual boundaries? Then pass only xs that are within bounds
70+
# Boundaries? Then only pass user xs that are within bounds
71+
if self._boundaries is not None:
7972
self._user_ids = np.nonzero(
8073
[self._boundaries.check(x) for x in self._xs])
8174
self._user_xs = self._xs[self._user_ids]
@@ -103,15 +96,6 @@ def _initialise(self):
10396
"""
10497
assert(not self._running)
10598

106-
# Create boundary transform, or use manual boundary checking
107-
self._manual_boundaries = False
108-
self._boundary_transform = None
109-
if isinstance(self._boundaries, pints.RectangularBoundaries):
110-
self._boundary_transform = pints.TriangleWaveTransform(
111-
self._boundaries)
112-
elif self._boundaries is not None:
113-
self._manual_boundaries = True
114-
11599
# Shorthands
116100
d = self._n_parameters
117101
n = self._population_size
@@ -154,8 +138,8 @@ def tell(self, fx):
154138
raise Exception('ask() not called before tell()')
155139
self._ready_for_tell = False
156140

157-
# Manual boundaries? Then reconstruct full fx vector
158-
if self._manual_boundaries and len(fx) < self._population_size:
141+
# Boundaries? Then reconstruct full fx vector
142+
if self._boundaries is not None and len(fx) < self._population_size:
159143
user_fx = fx
160144
fx = np.ones((self._population_size, )) * float('inf')
161145
fx[self._user_ids] = user_fx
@@ -186,7 +170,5 @@ def x_best(self):
186170

187171
def x_guessed(self):
188172
""" See :meth:`Optimiser.x_guessed()`. """
189-
if self._boundary_transform is not None:
190-
return self._boundary_transform(self._mu)
191173
return np.array(self._mu, copy=True)
192174

pints/_optimisers/_xnes.py

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,6 @@ def __init__(self, x0, sigma0=None, boundaries=None):
5858
# We don't have f(mu), so we approximate it by min f(sample)
5959
self._f_guessed = float('inf')
6060

61-
# Optional transformation to within-the-boundaries
62-
self._boundary_transform = None
63-
6461
def ask(self):
6562
""" See :meth:`Optimiser.ask()`. """
6663
# Initialise on first call
@@ -76,12 +73,8 @@ def ask(self):
7673
self._xs = np.array([self._mu + np.dot(self._A, self._zs[i])
7774
for i in range(self._population_size)])
7875

79-
# Create safe xs to pass to user
80-
if self._boundary_transform is not None:
81-
# Rectangular boundaries? Then perform boundary transform
82-
self._xs = self._boundary_transform(self._xs)
83-
if self._manual_boundaries:
84-
# Manual boundaries? Then pass only xs that are within bounds
76+
# Boundaries? Then only pass user xs that are within bounds
77+
if self._boundaries is not None:
8578
self._bounded_ids = np.nonzero(
8679
[self._boundaries.check(x) for x in self._xs])
8780
self._bounded_xs = self._xs[self._bounded_ids]
@@ -109,15 +102,6 @@ def _initialise(self):
109102
"""
110103
assert(not self._running)
111104

112-
# Create boundary transform, or use manual boundary checking
113-
self._manual_boundaries = False
114-
self._boundary_transform = None
115-
if isinstance(self._boundaries, pints.RectangularBoundaries):
116-
self._boundary_transform = pints.TriangleWaveTransform(
117-
self._boundaries)
118-
elif self._boundaries is not None:
119-
self._manual_boundaries = True
120-
121105
# Shorthands
122106
d = self._n_parameters
123107
n = self._population_size
@@ -163,8 +147,8 @@ def tell(self, fx):
163147
raise Exception('ask() not called before tell()')
164148
self._ready_for_tell = False
165149

166-
# Manual boundaries? Then reconstruct full fx vector
167-
if self._manual_boundaries and len(fx) < self._population_size:
150+
# Boundaries? Then reconstruct full fx vector
151+
if self._boundaries is not None and len(fx) < self._population_size:
168152
bounded_fx = fx
169153
fx = np.ones((self._population_size, )) * float('inf')
170154
fx[self._bounded_ids] = bounded_fx
@@ -198,7 +182,5 @@ def x_best(self):
198182

199183
def x_guessed(self):
200184
""" See :meth:`Optimiser.x_guessed()`. """
201-
if self._boundary_transform is not None:
202-
return self._boundary_transform(self._mu)
203185
return np.array(self._mu, copy=True)
204186

0 commit comments

Comments
 (0)