Repository: pints-team/pints

Issue:
Make controllers raise exception if used twice
Now that we've decided they're officially single use objects. See: #640

Discussion:

Patch:
diff --git a/pints/_mcmc/__init__.py b/pints/_mcmc/__init__.py
index c6090196..fa5c8853 100644
--- a/pints/_mcmc/__init__.py
+++ b/pints/_mcmc/__init__.py
@@ -418,6 +418,9 @@ class MCMCController(object):
         self._n_workers = 1
         self.set_parallel()
 
+        # :meth:`run` can only be called once
+        self._has_run = False
+
         #
         # Stopping criteria
         #
@@ -507,6 +510,12 @@ class MCMCController(object):
         If storing chains to memory has been disabled with
         :meth:`set_chain_storage`, then ``None`` is returned instead.
         """
+
+        # Can only run once for each controller instance
+        if self._has_run:
+            raise RuntimeError("Controller is valid for single use only")
+        self._has_run = True
+
         # Check stopping criteria
         has_stopping_criterion = False
         has_stopping_criterion |= (self._max_iterations is not None)
diff --git a/pints/_nested/__init__.py b/pints/_nested/__init__.py
index f19315e6..d06a7ccf 100644
--- a/pints/_nested/__init__.py
+++ b/pints/_nested/__init__.py
@@ -315,6 +315,9 @@ class NestedController(object):
         # Performance metrics
         self._time = None
 
+        # :meth:`run` can only be called once
+        self._has_run = False
+
     def active_points(self):
         """
         Returns the active points from nested sampling.
@@ -522,6 +525,11 @@ class NestedController(object):
         samples and an estimate of the marginal likelihood.
         """
 
+        # Can only run once for each controller instance
+        if self._has_run:
+            raise RuntimeError("Controller is valid for single use only")
+        self._has_run = True
+
         # Choose method to evaluate
         f = self._initialise_callable()
 
diff --git a/pints/_optimisers/__init__.py b/pints/_optimisers/__init__.py
index 66dc177f..6b72063e 100644
--- a/pints/_optimisers/__init__.py
+++ b/pints/_optimisers/__init__.py
@@ -382,6 +382,9 @@ class OptimisationController(object):
         self._n_workers = 1
         self.set_parallel()
 
+        # :meth:`run` can only be called once
+        self._has_run = False
+
         #
         # Stopping criteria
         #
@@ -452,6 +455,11 @@ class OptimisationController(object):
         """
         Runs the optimisation, returns a tuple ``(xbest, fbest)``.
         """
+        # Can only run once for each controller instance
+        if self._has_run:
+            raise RuntimeError("Controller is valid for single use only")
+        self._has_run = True
+
         # Check stopping criteria
         has_stopping_criterion = False
         has_stopping_criterion |= (self._max_iterations is not None)


Test patch:
diff --git a/pints/tests/test_mcmc_controller.py b/pints/tests/test_mcmc_controller.py
index 963a6a35..8657a3ac 100755
--- a/pints/tests/test_mcmc_controller.py
+++ b/pints/tests/test_mcmc_controller.py
@@ -379,6 +379,11 @@ class TestMCMCController(unittest.TestCase):
         self.assertEqual(chains.shape[2], nparameters)
 
         # Test with fixed number of worker processes
+        mcmc = pints.MCMCController(
+            self.log_posterior, nchains, xs,
+            method=pints.HaarioBardenetACMC)
+        mcmc.set_max_iterations(niterations)
+        mcmc.set_log_to_screen(debug)
         mcmc.set_parallel(5)
         mcmc.set_log_to_screen(True)
         self.assertIs(mcmc._parallel, True)
@@ -614,6 +619,22 @@ class TestMCMCController(unittest.TestCase):
             self.log_posterior, 1, [self.real_parameters])
         self.assertIsInstance(mcmc, pints.MCMCController)
 
+    def test_exception_on_multi_use(self):
+        # Controller should raise an exception if use multiple times
+
+        # Test simple run
+        n_chains = 1
+        n_iterations = 10
+        x0 = np.array(self.real_parameters) * 1.1
+        xs = [x0]
+        mcmc = pints.MCMCController(self.log_posterior, n_chains, xs)
+        mcmc.set_max_iterations(n_iterations)
+        mcmc.set_log_to_screen(False)
+        mcmc.run()
+        with self.assertRaisesRegex(
+                RuntimeError, 'Controller is valid for single use only'):
+            mcmc.run()
+
     def test_post_run_statistics(self):
         # Test method to obtain post-run statistics
 
diff --git a/pints/tests/test_nested_controller.py b/pints/tests/test_nested_controller.py
index ed8b1505..491abb0b 100755
--- a/pints/tests/test_nested_controller.py
+++ b/pints/tests/test_nested_controller.py
@@ -131,6 +131,8 @@ class TestNestedController(unittest.TestCase):
         sampler.run()
 
         # Test with fixed number of worker processes
+        sampler = pints.NestedController(
+            self.log_likelihood, self.log_prior)
         sampler.set_parallel(4)
         sampler.set_log_to_screen(False)
         self.assertEqual(sampler.parallel(), 4)
@@ -248,6 +250,8 @@ class TestNestedController(unittest.TestCase):
         logLikelihood1 = sampler.log_likelihood_vector()
         self.assertEqual(len(logLikelihood1), 400 + 100)
         self.assertTrue(ess1 > 0)
+        sampler = pints.NestedController(
+            self.log_likelihood, self.log_prior)
         iter = 2000
         sampler.set_iterations(iter)
         sampler.set_n_posterior_samples(100)
@@ -277,6 +281,8 @@ class TestNestedController(unittest.TestCase):
             self.assertTrue(elem <= 1)
 
         # Acive points
+        sampler = pints.NestedController(
+            self.log_likelihood, self.log_prior)
         sampler.set_iterations(100)
         sampler.set_log_to_screen(False)
         sampler.set_parallel(2)
@@ -324,6 +330,19 @@ class TestNestedController(unittest.TestCase):
         m_inactive = sampler.inactive_points()
         self.assertTrue(m_inactive.shape[0] < 200)
 
+    def test_exception_on_multi_use(self):
+        # Controller should raise an exception if use multiple times
+
+        sampler = pints.NestedController(
+            self.log_likelihood, self.log_prior)
+        sampler.set_n_posterior_samples(2)
+        sampler.set_iterations(10)
+        sampler.set_log_to_screen(False)
+        sampler.run()
+        with self.assertRaisesRegex(
+                RuntimeError, 'Controller is valid for single use only'):
+            sampler.run()
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/pints/tests/test_opt_optimisation_controller.py b/pints/tests/test_opt_optimisation_controller.py
index e5a78041..05739797 100755
--- a/pints/tests/test_opt_optimisation_controller.py
+++ b/pints/tests/test_opt_optimisation_controller.py
@@ -302,6 +302,22 @@ class TestOptimisationController(unittest.TestCase):
         self.assertGreater(opt.time(), 0)
         self.assertGreater(t_upper, opt.time())
 
+    def test_exception_on_multi_use(self):
+        # Controller should raise an exception if use multiple times
+
+        r = pints.toy.TwistedGaussianLogPDF(2, 0.01)
+        x = np.array([0, 1.01])
+        b = pints.RectangularBoundaries([-0.01, 0.95], [0.01, 1.05])
+        s = 0.01
+        opt = pints.OptimisationController(r, x, s, b, method=method)
+        opt.set_log_to_screen(False)
+        opt.set_max_unchanged_iterations(None)
+        opt.set_max_iterations(10)
+        opt.run()
+        with self.assertRaisesRegex(RuntimeError,
+                                    "Controller is valid for single use only"):
+            opt.run()
+
 
 if __name__ == '__main__':
     print('Add -v for more debug output')