def test_parallel(self): # Test parallelised running. r = pints.toy.RosenbrockError() x = np.array([1.1, 1.1]) b = pints.RectangularBoundaries([0.5, 0.5], [1.5, 1.5]) # Run with guessed number of cores opt = pints.OptimisationController(r, x, boundaries=b, method=method) opt.set_max_iterations(10) opt.set_log_to_screen(debug) opt.set_parallel(False) self.assertIs(opt.parallel(), False) opt.set_parallel(True) self.assertTrue(type(opt.parallel()) == int) self.assertTrue(opt.parallel() >= 1) opt.run() # Run with explicit number of cores opt = pints.OptimisationController(r, x, boundaries=b, method=method) opt.set_max_iterations(10) opt.set_log_to_screen(debug) opt.set_parallel(4) self.assertTrue(type(opt.parallel()) == int) self.assertEqual(opt.parallel(), 4) opt.run()
def test_transform(self): # Test optimisation with parameter transformation. # Test with LogPDF r = pints.toy.TwistedGaussianLogPDF(2, 0.01) x0 = np.array([0, 1.01]) b = pints.RectangularBoundaries([-0.01, 0.95], [0.01, 1.05]) s = 0.01 t = pints.RectangularBoundariesTransformation(b) opt = pints.OptimisationController(r, x0, s, b, t, method) opt.set_log_to_screen(False) opt.set_max_unchanged_iterations(None) opt.set_max_iterations(10) opt.run() # Test with ErrorMeasure r = pints.toy.ParabolicError() x0 = [0.1, 0.1] b = pints.RectangularBoundaries([-1, -1], [1, 1]) s = 0.1 t = pints.RectangularBoundariesTransformation(b) pints.OptimisationController(r, x0, boundaries=b, transform=t, method=method) opt = pints.OptimisationController(r, x0, s, b, t, method) opt.set_log_to_screen(False) opt.set_max_unchanged_iterations(None) opt.set_max_iterations(10) x, _ = opt.run() # Test output are detransformed self.assertEqual(x.shape, (2, )) self.assertTrue(b.check(x))
def optimise(self, data, sigma_fac=0.001, method="minimisation"): cmaes_problem = pints.MultiOutputProblem(self, self.frequency_range, data) if method == "likelihood": score = pints.GaussianLogLikelihood(cmaes_problem) sigma = sigma_fac * np.sum(data) / 2 * len(data) lower_bound = [self.param_bounds[x][0] for x in self.params] + [0.1 * sigma] * 2 upper_bound = [self.param_bounds[x][1] for x in self.params] + [10 * sigma] * 2 CMAES_boundaries = pints.RectangularBoundaries( lower_bound, upper_bound) random_init = abs(np.random.rand(self.n_parameters())) x0 = self.change_norm_group(random_init, "un_norm", "list") + [sigma] * 2 cmaes_fitting = pints.OptimisationController( score, x0, sigma0=None, boundaries=CMAES_boundaries, method=pints.CMAES) elif method == "minimisation": score = pints.SumOfSquaresError(cmaes_problem) lower_bound = [self.param_bounds[x][0] for x in self.params] upper_bound = [self.param_bounds[x][1] for x in self.params] CMAES_boundaries = pints.RectangularBoundaries( lower_bound, upper_bound) random_init = abs(np.random.rand(self.n_parameters())) x0 = self.change_norm_group(random_init, "un_norm", "list") cmaes_fitting = pints.OptimisationController( score, x0, sigma0=None, boundaries=CMAES_boundaries, method=pints.CMAES) cmaes_fitting.set_max_unchanged_iterations(iterations=200, threshold=1e-7) #cmaes_fitting.set_log_to_screen(False) cmaes_fitting.set_parallel(True) found_parameters, found_value = cmaes_fitting.run() if method == "likelihood": sim_params = found_parameters[:-2] sim_data = self.simulate(sim_params, self.frequency_range) else: found_value = -found_value sim_params = found_parameters sim_data = self.simulate(sim_params, self.frequency_range) """ log_score = pints.GaussianLogLikelihood(cmaes_problem) stds=self.get_std(data, sim_data) sigma=sigma_fac*np.sum(data)/2*len(data) score_params=list(found_parameters)+[sigma]*2 found_value=log_score(score_params) print(stds, found_value, "stds")""" #DOITDIMENSIONALLY#NORMALISE DEFAULT TO BOUND return found_parameters, found_value, cmaes_fitting._optimiser._es.sm.C, sim_data
def test_callback(self): # Tests running with a callback method # Define callback that just stores the argument(s) it was called with args = [] def cb(*arg): args.append(arg) # Set up a controller r = pints.toy.TwistedGaussianLogPDF(2, 0.01) x0 = np.array([0, 1.01]) s = 0.01 opt = pints.OptimisationController(r, x0, s, method=method) opt.set_log_to_screen(False) opt.set_max_unchanged_iterations(None) opt.set_max_iterations(10) # Pass in an invalid value self.assertRaisesRegex( ValueError, 'None or a callable', opt.set_callback, 3) # Now test using it correctly opt.set_callback(None) opt.set_callback(cb) opt.run() # Ensure callback was called at each iteration self.assertEqual(len(args), opt.iterations()) # Ensure first argument was iteration count a = np.array([arg[0] for arg in args]) self.assertTrue(np.all(a == np.arange(opt.iterations()))) # Ensure second argument was always the optimisation method b = tuple(set([arg[1] for arg in args])) self.assertEqual(len(b), 1) self.assertIs(b[0], opt.optimiser()) # Check unsetting works args.clear() self.assertEqual(len(args), 0) opt = pints.OptimisationController(r, x0, s, method=method) opt.set_log_to_screen(False) opt.set_max_unchanged_iterations(None) opt.set_max_iterations(10) opt.set_callback(cb) opt.set_callback(None) opt.run() self.assertEqual(len(args), 0)
def test_unbounded(self): """ Runs an optimisation without boundaries. """ r, x, s, b = self.problem() opt = pints.OptimisationController(r, x, method=method) opt.set_log_to_screen(debug) found_parameters, found_solution = opt.run() self.assertTrue(found_solution < 1e-3)
def test_set_population_size(self): # Tests the set_population_size method for this optimiser. r = pints.toy.RosenbrockError() x = np.array([1.01, 1.01]) opt = pints.OptimisationController(r, x, method=method) m = opt.optimiser() n = m.population_size() m.set_population_size(n + 1) self.assertEqual(m.population_size(), n + 1) # Test invalid size self.assertRaisesRegex(ValueError, 'at least 1', m.set_population_size, 0) # test hyper parameter interface self.assertEqual(m.n_hyper_parameters(), 1) m.set_hyper_parameters([n + 2]) self.assertEqual(m.population_size(), n + 2) self.assertRaisesRegex(ValueError, 'at least 1', m.set_hyper_parameters, [0]) # Test changing during run m.ask() self.assertRaises(Exception, m.set_population_size, 2)
def test_bounded_warning(self): # Boundaries are not supported r, x, s, b = self.problem() # Rectangular boundaries with self.assertLogs(level='WARN'): pints.OptimisationController(r, x, boundaries=b, method=method)
def test_rosenbrock(self): # Tests the actions of the optimiser against a stored result r = pints.toy.RosenbrockError() x0 = [-0.75, 3.5] opt = pints.OptimisationController(r, x0, method=method) opt.set_log_to_screen(True) with StreamCapture() as c: x, f = opt.run() log = c.text() self.assertTrue(np.all(x == np.array([1, 1]))) self.assertEqual(f, 0) exp_lines = ( 'Minimising error measure', 'Using Nelder-Mead', 'Running in sequential mode.', 'Iter. Eval. Best Current Time m:s', '0 3 865.9531 865.9531 0:00.0', '1 4 832.5452 832.5452 0:00.0', '2 5 832.5452 832.5452 0:00.0', '3 6 628.243 628.243 0:00.0', '20 23 4.95828 4.95828 0:00.0', '40 43 3.525867 3.525867 0:00.0', '60 63 2.377579 2.377579 0:00.0', '80 83 1.114115 1.114115 0:00.0', '100 103 0.551 0.551 0:00.0', '120 123 0.237 0.237 0:00.0', '140 143 0.0666 0.0666 0:00.0', '160 163 0.00181 0.00181 0:00.0', '180 183 6.96e-06 6.96e-06 0:00.0', '200 203 2.66e-08 2.66e-08 0:00.0', '220 223 5.06e-11 5.06e-11 0:00.0', '240 243 2.43e-15 2.43e-15 0:00.0', '260 263 5.58e-18 5.58e-18 0:00.0', '280 283 7.74e-20 7.74e-20 0:00.0', '300 303 6.66e-23 6.66e-23 0:00.0', '320 323 1.86e-25 1.86e-25 0:00.0', '340 343 3.16e-28 3.16e-28 0:00.0', '360 364 3.08e-31 3.08e-31 0:00.0', '380 390 0 0 0:00.0', '400 416 0 0 0:00.0', '420 443 0 0 0:00.0', '428 452 0 0 0:00.0', 'Halting: No significant change for 200 iterations.', ) # Compare lenght of log log_lines = [line.rstrip() for line in log.splitlines()] self.assertEqual(len(log_lines), len(exp_lines)) # Compare log lines, ignoring time bit (unles it's way too slow) for line1, line2 in zip(log_lines, exp_lines): if line2[-6:] == '0:00.0': line1 = line1[:-6] line2 = line2[:-6] self.assertEqual(line1, line2)
def test_bounded_and_sigma(self): # Runs an optimisation without boundaries and sigma. r, x, s, b = self.problem() opt = pints.OptimisationController(r, x, s, b, method) opt.set_log_to_screen(debug) found_parameters, found_solution = opt.run() self.assertTrue(found_solution < 1e-3)
def test_stopping_on_ill_conditioned_covariance_matrix(self): # Tests that ill conditioned covariance matrices are detected. from scipy.integrate import odeint #TODO: A quicker test-case for this would be great! def OnePopControlODE(y, t, p): a, b, c = p dydt = np.zeros(y.shape) k = (a - b) / c * (y[0] + y[1]) dydt[0] = a * y[0] - b * y[0] - k * y[0] dydt[1] = k * y[0] - b * y[1] return dydt class Model(pints.ForwardModel): def simulate(self, parameters, times): y0 = [2000000, 0] solution = odeint( OnePopControlODE, y0, times, args=(parameters,)) return np.sum(np.array(solution), axis=1) def n_parameters(self): return 3 model = Model() times = [0, 0.5, 2, 4, 8, 24] values = [2e6, 3.9e6, 3.1e7, 3.7e8, 1.6e9, 1.6e9] problem = pints.SingleOutputProblem(model, times, values) score = pints.SumOfSquaresError(problem) x = [3.42, -0.21, 5e6] opt = pints.OptimisationController(score, x, method=method) with StreamCapture() as c: opt.run() self.assertTrue('Ill-conditioned covariance matrix' in c.text())
def test_post_run_statistics(self): # Test the methods to return statistics, post-run. 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(50, 1e-11) np.random.seed(123) # Before run methods return None self.assertIsNone(opt.iterations()) self.assertIsNone(opt.evaluations()) self.assertIsNone(opt.time()) t = pints.Timer() opt.run() t_upper = t.time() self.assertEqual(opt.iterations(), 75) self.assertEqual(opt.evaluations(), 450) # Time after run is greater than zero self.assertIsInstance(opt.time(), float) self.assertGreater(opt.time(), 0) self.assertGreater(t_upper, opt.time())
def optimise(self, initial_parameter: np.ndarray, number_of_iterations: int = 10) -> None: """Find point in parameter space that optimises the objective function, i.e. find the set of parameters that minimises the distance of the model to the data with respect to the objective function. Arguments: initial_parameter {np.ndarray} -- Starting point in parameter space of the optimisation algorithm. Return: None """ # create sum of errors measure error_measure = pints.SumOfErrors(self.error_function_container) # initialise optimisation optimisation = pints.OptimisationController( function=error_measure, x0=initial_parameter, sigma0=self.initial_parameter_uncertainty, boundaries=self.parameter_boundaries, method=self.optimiser) # run optimisation 'number_of_iterations' times estimate_container = [] for _ in range(number_of_iterations): estimates, _ = optimisation.run() estimate_container.append(estimates) # return median parameters self.estimated_parameters = np.median(a=estimate_container, axis=0)
def test_logging(self): 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) opt.set_log_to_screen(True) opt.set_max_unchanged_iterations(None) opt.set_log_interval(3) opt.set_max_iterations(10) self.assertEqual(opt.max_iterations(), 10) with StreamCapture() as c: opt.run() log_should_be = ( 'Maximising LogPDF\n' 'using Exponential Natural Evolution Strategy (xNES)\n' 'Running in sequential mode.\n' 'Population size: 6\n' 'Iter. Eval. Best Time m:s\n' '0 6 -4.140462 0:00.0\n' '1 12 -4.140462 0:00.0\n' '2 18 -4.140462 0:00.0\n' '3 24 -4.140462 0:00.0\n' '6 42 -4.140462 0:00.0\n' '9 60 -4.140462 0:00.0\n' '10 60 -4.140462 0:00.0\n' 'Halting: Maximum number of iterations (10) reached.\n' ) self.assertEqual(log_should_be, c.text()) # Invalid log interval self.assertRaises(ValueError, opt.set_log_interval, 0)
def test_with_sigma(self): # Runs an optimisation with a sigma. r, x, s = self.problem() opt = pints.OptimisationController(r, x, s, method=method) opt.set_threshold(1e-3) opt.set_log_to_screen(debug) found_parameters, found_solution = opt.run() self.assertTrue(found_solution < 1e-3)
def optimise(objective_function, optimiser, initial_params, n_runs=1, boundaries=None, max_iterations=None): """ Returns parameters that optimise the objective function. """ if not isinstance(objective_function, (pints.ErrorMeasure, pints.LogPDF)): raise ValueError( 'Objective function has to be an instance of `pints.ErrorMeasure` ' 'or pints.LogPDF.') if not issubclass(optimiser, pints.Optimiser): raise ValueError( 'Optimiser has to be an instance of `pints.Optimiser`.') n_parameters = objective_function.n_parameters() initial_params = np.asarray(initial_params) if initial_params.shape != (n_runs, n_parameters): raise ValueError( 'Initial parameters has the wrong shape! Expected shape = ' '(%d, %d).' % (n_runs, n_parameters)) if boundaries is not None: if not isinstance(boundaries, pints.Boundaries): raise ValueError( 'Boundaries have to be an instance of `pints.Bouandries`.') # Define container for estimates and scores parameters = np.empty(shape=(n_runs, n_parameters)) scores = np.empty(shape=n_runs) # Run optimisation multiple times for run_id, init_p in enumerate(initial_params): opt = pints.OptimisationController(function=objective_function, x0=init_p, method=optimiser, boundaries=boundaries) # Configure optimisation routine opt.set_log_to_screen(False) opt.set_parallel(True) if max_iterations: opt.set_max_iterations(max_iterations) # Find optimal parameters try: estimates, score = opt.run() except Exception: # If inference breaks fill estimates with nan estimates = np.array([np.nan] * n_parameters) score = np.nan # Save estimates and score parameters[run_id, :] = estimates scores[run_id] = score return parameters, scores
def test_bounded(self): # Runs an optimisation with boundaries. r, x, s, b = self.problem() # Rectangular boundaries opt = pints.OptimisationController(r, x, boundaries=b, method=method) opt.set_log_to_screen(debug) found_parameters, found_solution = opt.run() self.assertTrue(found_solution < 1e-3) # Circular boundaries # Start near edge, to increase chance of out-of-bounds occurring. b = CircularBoundaries([0, 0], 1) x = [0.99, 0] opt = pints.OptimisationController(r, x, boundaries=b, method=method) opt.set_log_to_screen(debug) found_parameters, found_solution = opt.run() self.assertTrue(found_solution < 1e-3)
def test_hyper_parameter_interface(self): # Tests the hyper parameter interface for this optimiser. r, x, s, b = self.problem() opt = pints.OptimisationController(r, x, method=method) m = opt.optimiser() self.assertEqual(m.n_hyper_parameters(), 1) eta = m.learning_rate() * 2 m.set_hyper_parameters([eta]) self.assertEqual(m.learning_rate(), eta) self.assertRaisesRegex(ValueError, 'greater than zero', m.set_hyper_parameters, [0])
def test_hyper_parameter_interface(self): # Tests the hyper parameter interface for this optimiser. r, x, s, b = self.problem() opt = pints.OptimisationController(r, x, method=method) m = opt.optimiser() self.assertEqual(m.n_hyper_parameters(), 1) n = m.population_size() + 2 m.set_hyper_parameters([n]) self.assertEqual(m.population_size(), n) self.assertRaisesRegex( ValueError, 'at least 1', m.set_hyper_parameters, [0])
def test_bounded_warning(self): # Boundaries are not supported r, x, s, b = self.problem() # Rectangular boundaries import warnings with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') pints.OptimisationController(r, x, boundaries=b, method=method) self.assertEqual(len(w), 1) self.assertIn('does not support boundaries', str(w[-1].message))
def test_zeros_in_x(self): # Tests if the method copes with zeros in x0 (which can go wrong # depending on the initialisation method). r = pints.toy.RosenbrockError() x0 = [0, 0] opt = pints.OptimisationController(r, x0, method=method) opt.set_log_to_screen(False) x, f = opt.run() self.assertTrue(np.all(x == np.array([1, 1]))) self.assertEqual(f, 0)
def test_stopping_no_criterion(self): """ Tries to run an optimisation with the no stopping criterion. """ 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) opt.set_log_to_screen(debug) opt.set_max_iterations(None) opt.set_max_unchanged_iterations(None) self.assertRaises(ValueError, opt.run)
def test_simple(self): # Runs an optimisation r, x, s = self.problem() opt = pints.OptimisationController(r, x, sigma0=s, method=method) opt.set_log_to_screen(debug) found_parameters, found_solution = opt.run() # True solution is (0, 0) with error 0 self.assertTrue(found_solution < 1e-9) self.assertLess(abs(found_parameters[0]), 1e-8) self.assertLess(abs(found_parameters[1]), 1e-8)
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() self.assertRaisesRegex( RuntimeError, 'Controller is valid for single use only', opt.run)
def run_figureS2(num_runs=3, output_dir='./'): """Run the Gaussian process on block noise data. This function runs the simulations and saves the results to pickle. """ random.seed(12345) np.random.seed(12345) all_fits = [] iid_runs = [] sigmas = [] mult_runs = [] gp_runs = [] for run in range(num_runs): # Make a synthetic time series times, values, data = generate_time_series(model='logistic', noise='blocks', n_times=625) # Make Pints model and problem model = pints.toy.LogisticModel() problem = pints.SingleOutputProblem(model, times, data) # Initial conditions for model parameters model_starting_point = [0.08, 50] # Infer the nonstationary kernel fit # Run an optimization assumming IID log_prior = pints.UniformLogPrior([0] * 3, [1e6] * 3) log_likelihood = pints.GaussianLogLikelihood(problem) log_posterior = pints.LogPosterior(log_likelihood, log_prior) opt = pints.OptimisationController(log_posterior, model_starting_point + [2]) xbest, fbest = opt.run() # Run the GP fit, using the best fit for initialization gp_times = times[::25] kernel = flexnoise.kernels.GPLaplacianKernel gnp = flexnoise.GPNoiseProcess(problem, kernel, xbest[:2], gp_times) gnp.set_gp_hyperparameters(mu=0.0, alpha=1.0, beta_num_points=200) x = gnp.run_optimize(num_restarts=100, parallel=True, maxiter=150) all_fits.append(x) # Save all results to pickle kernel = kernel(None, gp_times) results = [all_fits, times, data, values, model, problem, kernel] fname = os.path.join(output_dir, 'figS2_data.pkl') with open(fname, 'wb') as f: pickle.dump(results, f)
def test_stopping_max_iterations(self): """ Runs an optimisation with the max_iter stopping criterion. """ 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) opt.set_log_to_screen(True) opt.set_max_unchanged_iterations(None) opt.set_max_iterations(10) self.assertEqual(opt.max_iterations(), 10) self.assertRaises(ValueError, opt.set_max_iterations, -1) with StreamCapture() as c: opt.run() self.assertIn('Halting: Maximum number of iterations', c.text())
def test_stopping_threshold(self): """ Runs an optimisation with the threshold stopping criterion. """ r = pints.toy.TwistedGaussianLogPDF(2, 0.01) x = np.array([0.008, 1.01]) b = pints.RectangularBoundaries([-0.01, 0.95], [0.01, 1.05]) s = 0.01 opt = pints.OptimisationController(r, x, s, b, method) opt.set_log_to_screen(True) opt.set_max_iterations(None) opt.set_max_unchanged_iterations(None) opt.set_threshold(5) self.assertEqual(opt.threshold(), 5) with StreamCapture() as c: opt.run() self.assertIn( 'Halting: Objective function crossed threshold', c.text())
def test_post_run_statistics(self): # Test the methods to return statistics, post-run. 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) opt.set_log_to_screen(False) opt.set_max_unchanged_iterations(50, 1e-11) np.random.seed(123) opt.run() self.assertEqual(opt.iterations(), 75) self.assertEqual(opt.evaluations(), 450) t = opt.time() self.assertTrue(0 < t < 5)
def test_set_hyper_parameters(self): # Tests the hyper-parameter interface for this optimiser. r, x, s, b = self.problem() opt = pints.OptimisationController(r, x, boundaries=b, method=method) m = opt.optimiser() self.assertEqual(m.n_hyper_parameters(), 2) n = m.population_size() m.set_hyper_parameters([n + 1, 0.5]) self.assertEqual(m.population_size(), n + 1) # Test invalid size self.assertRaisesRegex(ValueError, 'at least 1', m.set_hyper_parameters, [0, 0.5]) self.assertRaisesRegex(ValueError, 'in the range 0-1', m.set_hyper_parameters, [n, 1.5])
def test_logging(self): # Test with logpdf r, x, s = self.problem() opt = pints.OptimisationController(r, x, s, method=method) opt.set_log_to_screen(True) opt.set_max_unchanged_iterations(None) opt.set_max_iterations(2) with StreamCapture() as c: opt.run() lines = c.text().splitlines() self.assertEqual(lines[0], 'Minimising error measure') self.assertEqual(lines[1], 'Using Adam') self.assertEqual(lines[2], 'Running in sequential mode.') self.assertEqual(lines[3], 'Iter. Eval. Best Current Time m:s') self.assertEqual(lines[4][:-3], '0 1 0.02 0.02 0:0') self.assertEqual(lines[5][:-3], '1 2 5e-17 5e-17 0:0')
def test_stopping_max_unchanged(self): # Runs an optimisation with the max_unchanged stopping criterion. 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(True) opt.set_max_iterations(None) opt.set_max_unchanged_iterations(None) self.assertEqual(opt.max_unchanged_iterations(), (None, None)) opt.set_max_unchanged_iterations(2, 1e-6) self.assertEqual(opt.max_unchanged_iterations(), (2, 1e-6)) opt.set_max_unchanged_iterations(3) self.assertEqual(opt.max_unchanged_iterations(), (3, 1e-11)) self.assertRaises(ValueError, opt.set_max_unchanged_iterations, -1) self.assertRaises(ValueError, opt.set_max_unchanged_iterations, 10, -1) with StreamCapture() as c: opt.run() self.assertIn('Halting: No significant change', c.text())