Repository: pints-team/pints

Issue:
Add note to CMA-ES about interpretation of sigma
Also add some more info to the optimiser base class

Missing bits:

- cmaes: requires sigma to be a scalar #1693
- cmaes: expects x0 + 3 * sigma to contain optimum #1693
- cmaes: 1d not supported #1504 and #1524
- all: if no sigma0 is given it becomes `boundaries.range() / 6` or ...

Hints from issue comments:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee7aa2390..75088f182 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.
 - [#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.
 ### Changed
+- [#1698](https://github.com/pints-team/pints/pull/1698) CMA-ES now raises a more informative exception when an unsupported 1-d optimisation is attempted.
 - [#1503](https://github.com/pints-team/pints/pull/1503) Stopped showing time units in controller logs, because the units change depending on the output type (see #1467).
 - [#1699](https://github.com/pints-team/pints/pull/1699) PINTS is no longer tested on Python 3.7. Testing for Python 3.12 and 3.13 has been added.
 ### Deprecated
@@ -19,6 +20,7 @@ All notable changes to this project will be documented in this file.
 - [#1505](https://github.com/pints-team/pints/pull/1505) Fixed issues with toy problems that accept invalid inputs.
 
 
+
 ## [0.5.0] - 2023-07-27
 
 ### Added
diff --git a/pints/_optimisers/__init__.py b/pints/_optimisers/__init__.py
index 363a4ee2a..17c99d86a 100644
--- a/pints/_optimisers/__init__.py
+++ b/pints/_optimisers/__init__.py
@@ -34,15 +34,23 @@ class Optimiser(pints.Loggable, pints.TunableMethod):
     Parameters
     ----------
     x0
-        A starting point for searches in the parameter space. This value may be
-        used directly (for example as the initial position of a particle in
+        A starting point for searches in the parameter space. This must be a
+        1-dimensional vector, and its length will determine the dimensionality
+        of the search space. The initial position ``x0`` may may be used
+        directly (for example as the initial position of a particle in
         :class:`PSO`) or indirectly (for example as the center of a
         distribution in :class:`XNES`).
     sigma0
         An optional initial standard deviation around ``x0``. Can be specified
         either as a scalar value (one standard deviation for all coordinates)
         or as an array with one entry per dimension. Not all methods will use
-        this information.
+        this information, and some methods will only use part of the provided
+        information (e.g. ``CMA-ES`` will only use the smallest value in
+        ``sigma0``). If no value for ``sigma0`` is provided, a guess will be
+        made. If a :meth:`range<Boundaries.range>` can be obtained from
+        provided boundaries, then 1/6th of this range will be used. If not, the
+        value will be set as ``abs(x0[i]) / 3`` for any ``i`` where
+        ``x0[i] != 0`` or as ``1`` where ``x0[i] == 0``.
     boundaries
         An optional set of boundaries on the parameter space.
 
@@ -340,17 +348,14 @@ class OptimisationController(object):
         An :class:`pints.ErrorMeasure` or a :class:`pints.LogPDF` that
         evaluates points in the parameter space.
     x0
-        The starting point for searches in the parameter space. This value may
-        be used directly (for example as the initial position of a particle in
-        :class:`PSO`) or indirectly (for example as the center of a
-        distribution in :class:`XNES`).
+        The starting point for searches in the parameter space. For details,
+        see :class:`Optimiser`.
     sigma0
-        An optional initial standard deviation around ``x0``. Can be specified
-        either as a scalar value (one standard deviation for all coordinates)
-        or as an array with one entry per dimension. Not all methods will use
-        this information.
+        An optional initial standard deviation around ``x0``. For details, see
+        :class:`Optimiser`.
     boundaries
-        An optional set of boundaries on the parameter space.
+        An optional set of boundaries on the parameter space. For details, see
+        :class:`Optimiser`.
     transformation
         An optional :class:`pints.Transformation` to allow the optimiser to
         search in a transformed parameter space. If used, points shown or
@@ -359,6 +364,7 @@ class OptimisationController(object):
     method
         The class of :class:`pints.Optimiser` to use for the optimisation.
         If no method is specified, :class:`CMAES` is used.
+
     """
 
     def __init__(
diff --git a/pints/_optimisers/_cmaes.py b/pints/_optimisers/_cmaes.py
index b223a1c41..6cb55c443 100644
--- a/pints/_optimisers/_cmaes.py
+++ b/pints/_optimisers/_cmaes.py
@@ -18,6 +18,25 @@ class CMAES(pints.PopulationBasedOptimiser):
     CMA-ES stands for Covariance Matrix Adaptation Evolution Strategy, and is
     designed for non-linear derivative-free optimization problems.
 
+    To initialise, set an initial position ``x0``, an initial covariance
+    ``sigma0``, and optionally a set of :class:`pints.Boundaries`.
+
+    CMA-ES is designed for multivariate optimisation, so ``x0`` should have a
+    dimension of 2 or greater.
+
+    The initial covariance ``sigma0`` should be a scalar (one standard
+    deviation for all parameters), but for compatibility with other optimisers
+    an array is also accepted: in this case the smallest value in ``sigma0``
+    will be used. If no ``sigma0`` is given a guess will be made based on the
+    boundaries (if provided) or ``x0``, see :meth:`Optimiser` for details. The
+    method's authors suggest choosing ``sigma0`` such that the optimum is
+    expected to be within ``3 * sigma0`` from the initial position ``x0``.
+
+    :class:`Rectangular boundaries<pints.RectangularBoundaries>` are supported
+    by the ``cma`` module natively. Other boundary shapes are also supported
+    but will be handled by ``PINTS`` (which will pass ``nan`` to ``cma`` for
+    any point requested outside of the boundaries).
+
     Extends :class:`PopulationBasedOptimiser`.
 
     References
@@ -38,6 +57,11 @@ class CMAES(pints.PopulationBasedOptimiser):
     def __init__(self, x0, sigma0=None, boundaries=None):
         super(CMAES, self).__init__(x0, sigma0, boundaries)
 
+        # 1-D is not supported
+        if len(x0) < 2:
+            raise ValueError(
+                '1-dimensional optimisation is not supported by CMA-ES.')
+
         # Set initial state
         self._running = False
         self._ready_for_tell = False


Test patch:
diff --git a/pints/tests/test_opt_cmaes.py b/pints/tests/test_opt_cmaes.py
index 7536149d0..8cf985055 100755
--- a/pints/tests/test_opt_cmaes.py
+++ b/pints/tests/test_opt_cmaes.py
@@ -7,6 +7,8 @@
 # copyright notice and full license details.
 #
 import unittest
+import warnings
+
 import numpy as np
 
 import pints
@@ -124,8 +126,15 @@ def test_ask_tell(self):
         self.assertEqual(opt.f_guessed(), np.inf)
 
         # Test deprecated xbest and fbest
-        self.assertEqual(list(opt.xbest()), list(x))
-        self.assertEqual(opt.fbest(), np.inf)
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter('always')
+            self.assertEqual(list(opt.xbest()), list(x))
+            self.assertEqual(len(w), 1)
+            self.assertIn('xbest` is deprecated', str(w[-1].message))
+        with warnings.catch_warnings(record=True) as w:
+            self.assertEqual(opt.fbest(), np.inf)
+            self.assertEqual(len(w), 1)
+            self.assertIn('fbest` is deprecated', str(w[-1].message))
 
         # Tell before ask
         self.assertRaisesRegex(
@@ -154,6 +163,11 @@ def test_name(self):
         opt = method(np.array([0, 1.01]))
         self.assertIn('CMA-ES', opt.name())
 
+    def test_one_dimensional(self):
+        # Tests 1-d is not supported
+        self.assertRaisesRegex(
+            ValueError, '1-dimensional optimisation is', pints.CMAES, [1])
+
 
 if __name__ == '__main__':
     print('Add -v for more debug output')