spam-classifier
/
venv
/lib
/python3.11
/site-packages
/scipy
/optimize
/tests
/test__basinhopping.py
| """ | |
| Unit tests for the basin hopping global minimization algorithm. | |
| """ | |
| import copy | |
| from numpy.testing import (assert_almost_equal, assert_equal, assert_, | |
| assert_allclose) | |
| import pytest | |
| from pytest import raises as assert_raises | |
| import numpy as np | |
| from numpy import cos, sin | |
| from scipy.optimize import basinhopping, OptimizeResult | |
| from scipy.optimize._basinhopping import ( | |
| Storage, RandomDisplacement, Metropolis, AdaptiveStepsize) | |
| def func1d(x): | |
| f = cos(14.5 * x - 0.3) + (x + 0.2) * x | |
| df = np.array(-14.5 * sin(14.5 * x - 0.3) + 2. * x + 0.2) | |
| return f, df | |
| def func2d_nograd(x): | |
| f = cos(14.5 * x[0] - 0.3) + (x[1] + 0.2) * x[1] + (x[0] + 0.2) * x[0] | |
| return f | |
| def func2d(x): | |
| f = cos(14.5 * x[0] - 0.3) + (x[1] + 0.2) * x[1] + (x[0] + 0.2) * x[0] | |
| df = np.zeros(2) | |
| df[0] = -14.5 * sin(14.5 * x[0] - 0.3) + 2. * x[0] + 0.2 | |
| df[1] = 2. * x[1] + 0.2 | |
| return f, df | |
| def func2d_easyderiv(x): | |
| f = 2.0*x[0]**2 + 2.0*x[0]*x[1] + 2.0*x[1]**2 - 6.0*x[0] | |
| df = np.zeros(2) | |
| df[0] = 4.0*x[0] + 2.0*x[1] - 6.0 | |
| df[1] = 2.0*x[0] + 4.0*x[1] | |
| return f, df | |
| class MyTakeStep1(RandomDisplacement): | |
| """use a copy of displace, but have it set a special parameter to | |
| make sure it's actually being used.""" | |
| def __init__(self): | |
| self.been_called = False | |
| super().__init__() | |
| def __call__(self, x): | |
| self.been_called = True | |
| return super().__call__(x) | |
| def myTakeStep2(x): | |
| """redo RandomDisplacement in function form without the attribute stepsize | |
| to make sure everything still works ok | |
| """ | |
| s = 0.5 | |
| x += np.random.uniform(-s, s, np.shape(x)) | |
| return x | |
| class MyAcceptTest: | |
| """pass a custom accept test | |
| This does nothing but make sure it's being used and ensure all the | |
| possible return values are accepted | |
| """ | |
| def __init__(self): | |
| self.been_called = False | |
| self.ncalls = 0 | |
| self.testres = [False, 'force accept', True, np.bool_(True), | |
| np.bool_(False), [], {}, 0, 1] | |
| def __call__(self, **kwargs): | |
| self.been_called = True | |
| self.ncalls += 1 | |
| if self.ncalls - 1 < len(self.testres): | |
| return self.testres[self.ncalls - 1] | |
| else: | |
| return True | |
| class MyCallBack: | |
| """pass a custom callback function | |
| This makes sure it's being used. It also returns True after 10 | |
| steps to ensure that it's stopping early. | |
| """ | |
| def __init__(self): | |
| self.been_called = False | |
| self.ncalls = 0 | |
| def __call__(self, x, f, accepted): | |
| self.been_called = True | |
| self.ncalls += 1 | |
| if self.ncalls == 10: | |
| return True | |
| class TestBasinHopping: | |
| def setup_method(self): | |
| """ Tests setup. | |
| Run tests based on the 1-D and 2-D functions described above. | |
| """ | |
| self.x0 = (1.0, [1.0, 1.0]) | |
| self.sol = (-0.195, np.array([-0.195, -0.1])) | |
| self.tol = 3 # number of decimal places | |
| self.niter = 100 | |
| self.disp = False | |
| self.kwargs = {"method": "L-BFGS-B", "jac": True} | |
| self.kwargs_nograd = {"method": "L-BFGS-B"} | |
| def test_TypeError(self): | |
| # test the TypeErrors are raised on bad input | |
| i = 1 | |
| # if take_step is passed, it must be callable | |
| assert_raises(TypeError, basinhopping, func2d, self.x0[i], | |
| take_step=1) | |
| # if accept_test is passed, it must be callable | |
| assert_raises(TypeError, basinhopping, func2d, self.x0[i], | |
| accept_test=1) | |
| def test_input_validation(self): | |
| msg = 'target_accept_rate has to be in range \\(0, 1\\)' | |
| with assert_raises(ValueError, match=msg): | |
| basinhopping(func1d, self.x0[0], target_accept_rate=0.) | |
| with assert_raises(ValueError, match=msg): | |
| basinhopping(func1d, self.x0[0], target_accept_rate=1.) | |
| msg = 'stepwise_factor has to be in range \\(0, 1\\)' | |
| with assert_raises(ValueError, match=msg): | |
| basinhopping(func1d, self.x0[0], stepwise_factor=0.) | |
| with assert_raises(ValueError, match=msg): | |
| basinhopping(func1d, self.x0[0], stepwise_factor=1.) | |
| def test_1d_grad(self): | |
| # test 1-D minimizations with gradient | |
| i = 0 | |
| res = basinhopping(func1d, self.x0[i], minimizer_kwargs=self.kwargs, | |
| niter=self.niter, disp=self.disp) | |
| assert_almost_equal(res.x, self.sol[i], self.tol) | |
| def test_2d(self): | |
| # test 2d minimizations with gradient | |
| i = 1 | |
| res = basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs, | |
| niter=self.niter, disp=self.disp) | |
| assert_almost_equal(res.x, self.sol[i], self.tol) | |
| assert_(res.nfev > 0) | |
| def test_njev(self): | |
| # test njev is returned correctly | |
| i = 1 | |
| minimizer_kwargs = self.kwargs.copy() | |
| # L-BFGS-B doesn't use njev, but BFGS does | |
| minimizer_kwargs["method"] = "BFGS" | |
| res = basinhopping(func2d, self.x0[i], | |
| minimizer_kwargs=minimizer_kwargs, niter=self.niter, | |
| disp=self.disp) | |
| assert_(res.nfev > 0) | |
| assert_equal(res.nfev, res.njev) | |
| def test_jac(self): | |
| # test Jacobian returned | |
| minimizer_kwargs = self.kwargs.copy() | |
| # BFGS returns a Jacobian | |
| minimizer_kwargs["method"] = "BFGS" | |
| res = basinhopping(func2d_easyderiv, [0.0, 0.0], | |
| minimizer_kwargs=minimizer_kwargs, niter=self.niter, | |
| disp=self.disp) | |
| assert_(hasattr(res.lowest_optimization_result, "jac")) | |
| # in this case, the Jacobian is just [df/dx, df/dy] | |
| _, jacobian = func2d_easyderiv(res.x) | |
| assert_almost_equal(res.lowest_optimization_result.jac, jacobian, | |
| self.tol) | |
| def test_2d_nograd(self): | |
| # test 2-D minimizations without gradient | |
| i = 1 | |
| res = basinhopping(func2d_nograd, self.x0[i], | |
| minimizer_kwargs=self.kwargs_nograd, | |
| niter=self.niter, disp=self.disp) | |
| assert_almost_equal(res.x, self.sol[i], self.tol) | |
| def test_all_minimizers(self): | |
| # Test 2-D minimizations with gradient. Nelder-Mead, Powell, COBYLA, and | |
| # COBYQA don't accept jac=True, so aren't included here. | |
| i = 1 | |
| methods = ['CG', 'BFGS', 'Newton-CG', 'L-BFGS-B', 'TNC', 'SLSQP'] | |
| minimizer_kwargs = copy.copy(self.kwargs) | |
| for method in methods: | |
| minimizer_kwargs["method"] = method | |
| res = basinhopping(func2d, self.x0[i], | |
| minimizer_kwargs=minimizer_kwargs, | |
| niter=self.niter, disp=self.disp) | |
| assert_almost_equal(res.x, self.sol[i], self.tol) | |
| def test_all_nograd_minimizers(self): | |
| # Test 2-D minimizations without gradient. Newton-CG requires jac=True, | |
| # so not included here. | |
| i = 1 | |
| methods = ['CG', 'BFGS', 'L-BFGS-B', 'TNC', 'SLSQP', | |
| 'Nelder-Mead', 'Powell', 'COBYLA', 'COBYQA'] | |
| minimizer_kwargs = copy.copy(self.kwargs_nograd) | |
| for method in methods: | |
| # COBYQA takes extensive amount of time on this problem | |
| niter = 10 if method == 'COBYQA' else self.niter | |
| minimizer_kwargs["method"] = method | |
| res = basinhopping(func2d_nograd, self.x0[i], | |
| minimizer_kwargs=minimizer_kwargs, | |
| niter=niter, disp=self.disp, seed=1234) | |
| tol = self.tol | |
| if method == 'COBYLA': | |
| tol = 2 | |
| assert_almost_equal(res.x, self.sol[i], decimal=tol) | |
| def test_pass_takestep(self): | |
| # test that passing a custom takestep works | |
| # also test that the stepsize is being adjusted | |
| takestep = MyTakeStep1() | |
| initial_step_size = takestep.stepsize | |
| i = 1 | |
| res = basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs, | |
| niter=self.niter, disp=self.disp, | |
| take_step=takestep) | |
| assert_almost_equal(res.x, self.sol[i], self.tol) | |
| assert_(takestep.been_called) | |
| # make sure that the build in adaptive step size has been used | |
| assert_(initial_step_size != takestep.stepsize) | |
| def test_pass_simple_takestep(self): | |
| # test that passing a custom takestep without attribute stepsize | |
| takestep = myTakeStep2 | |
| i = 1 | |
| res = basinhopping(func2d_nograd, self.x0[i], | |
| minimizer_kwargs=self.kwargs_nograd, | |
| niter=self.niter, disp=self.disp, | |
| take_step=takestep) | |
| assert_almost_equal(res.x, self.sol[i], self.tol) | |
| def test_pass_accept_test(self): | |
| # test passing a custom accept test | |
| # makes sure it's being used and ensures all the possible return values | |
| # are accepted. | |
| accept_test = MyAcceptTest() | |
| i = 1 | |
| # there's no point in running it more than a few steps. | |
| basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs, | |
| niter=10, disp=self.disp, accept_test=accept_test) | |
| assert_(accept_test.been_called) | |
| def test_pass_callback(self): | |
| # test passing a custom callback function | |
| # This makes sure it's being used. It also returns True after 10 steps | |
| # to ensure that it's stopping early. | |
| callback = MyCallBack() | |
| i = 1 | |
| # there's no point in running it more than a few steps. | |
| res = basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs, | |
| niter=30, disp=self.disp, callback=callback) | |
| assert_(callback.been_called) | |
| assert_("callback" in res.message[0]) | |
| # One of the calls of MyCallBack is during BasinHoppingRunner | |
| # construction, so there are only 9 remaining before MyCallBack stops | |
| # the minimization. | |
| assert_equal(res.nit, 9) | |
| def test_minimizer_fail(self): | |
| # test if a minimizer fails | |
| i = 1 | |
| self.kwargs["options"] = dict(maxiter=0) | |
| self.niter = 10 | |
| res = basinhopping(func2d, self.x0[i], minimizer_kwargs=self.kwargs, | |
| niter=self.niter, disp=self.disp) | |
| # the number of failed minimizations should be the number of | |
| # iterations + 1 | |
| assert_equal(res.nit + 1, res.minimization_failures) | |
| def test_niter_zero(self): | |
| # gh5915, what happens if you call basinhopping with niter=0 | |
| i = 0 | |
| basinhopping(func1d, self.x0[i], minimizer_kwargs=self.kwargs, | |
| niter=0, disp=self.disp) | |
| def test_rng_reproducibility(self): | |
| # rng should ensure reproducibility between runs | |
| minimizer_kwargs = {"method": "L-BFGS-B", "jac": True} | |
| f_1 = [] | |
| def callback(x, f, accepted): | |
| f_1.append(f) | |
| basinhopping(func2d, [1.0, 1.0], minimizer_kwargs=minimizer_kwargs, | |
| niter=10, callback=callback, rng=10) | |
| f_2 = [] | |
| def callback2(x, f, accepted): | |
| f_2.append(f) | |
| basinhopping(func2d, [1.0, 1.0], minimizer_kwargs=minimizer_kwargs, | |
| niter=10, callback=callback2, rng=10) | |
| assert_equal(np.array(f_1), np.array(f_2)) | |
| def test_random_gen(self): | |
| # check that np.random.Generator can be used (numpy >= 1.17) | |
| rng = np.random.default_rng(1) | |
| minimizer_kwargs = {"method": "L-BFGS-B", "jac": True} | |
| res1 = basinhopping(func2d, [1.0, 1.0], | |
| minimizer_kwargs=minimizer_kwargs, | |
| niter=10, rng=rng) | |
| rng = np.random.default_rng(1) | |
| res2 = basinhopping(func2d, [1.0, 1.0], | |
| minimizer_kwargs=minimizer_kwargs, | |
| niter=10, rng=rng) | |
| assert_equal(res1.x, res2.x) | |
| def test_monotonic_basin_hopping(self): | |
| # test 1-D minimizations with gradient and T=0 | |
| i = 0 | |
| res = basinhopping(func1d, self.x0[i], minimizer_kwargs=self.kwargs, | |
| niter=self.niter, disp=self.disp, T=0) | |
| assert_almost_equal(res.x, self.sol[i], self.tol) | |
| class Test_Storage: | |
| def setup_method(self): | |
| self.x0 = np.array(1) | |
| self.f0 = 0 | |
| minres = OptimizeResult(success=True) | |
| minres.x = self.x0 | |
| minres.fun = self.f0 | |
| self.storage = Storage(minres) | |
| def test_higher_f_rejected(self): | |
| new_minres = OptimizeResult(success=True) | |
| new_minres.x = self.x0 + 1 | |
| new_minres.fun = self.f0 + 1 | |
| ret = self.storage.update(new_minres) | |
| minres = self.storage.get_lowest() | |
| assert_equal(self.x0, minres.x) | |
| assert_equal(self.f0, minres.fun) | |
| assert_(not ret) | |
| def test_lower_f_accepted(self, success): | |
| new_minres = OptimizeResult(success=success) | |
| new_minres.x = self.x0 + 1 | |
| new_minres.fun = self.f0 - 1 | |
| ret = self.storage.update(new_minres) | |
| minres = self.storage.get_lowest() | |
| assert (self.x0 != minres.x) == success # can't use `is` | |
| assert (self.f0 != minres.fun) == success # left side is NumPy bool | |
| assert ret is success | |
| class Test_RandomDisplacement: | |
| def setup_method(self): | |
| self.stepsize = 1.0 | |
| self.N = 300000 | |
| def test_random(self): | |
| # the mean should be 0 | |
| # the variance should be (2*stepsize)**2 / 12 | |
| # note these tests are random, they will fail from time to time | |
| rng = np.random.RandomState(0) | |
| x0 = np.zeros([self.N]) | |
| displace = RandomDisplacement(stepsize=self.stepsize, rng=rng) | |
| x = displace(x0) | |
| v = (2. * self.stepsize) ** 2 / 12 | |
| assert_almost_equal(np.mean(x), 0., 1) | |
| assert_almost_equal(np.var(x), v, 1) | |
| class Test_Metropolis: | |
| def setup_method(self): | |
| self.T = 2. | |
| self.met = Metropolis(self.T) | |
| self.res_new = OptimizeResult(success=True, fun=0.) | |
| self.res_old = OptimizeResult(success=True, fun=1.) | |
| def test_boolean_return(self): | |
| # the return must be a bool, else an error will be raised in | |
| # basinhopping | |
| ret = self.met(res_new=self.res_new, res_old=self.res_old) | |
| assert isinstance(ret, bool) | |
| def test_lower_f_accepted(self): | |
| assert_(self.met(res_new=self.res_new, res_old=self.res_old)) | |
| def test_accept(self): | |
| # test that steps are randomly accepted for f_new > f_old | |
| one_accept = False | |
| one_reject = False | |
| for i in range(1000): | |
| if one_accept and one_reject: | |
| break | |
| res_new = OptimizeResult(success=True, fun=1.) | |
| res_old = OptimizeResult(success=True, fun=0.5) | |
| ret = self.met(res_new=res_new, res_old=res_old) | |
| if ret: | |
| one_accept = True | |
| else: | |
| one_reject = True | |
| assert_(one_accept) | |
| assert_(one_reject) | |
| def test_GH7495(self): | |
| # an overflow in exp was producing a RuntimeWarning | |
| # create own object here in case someone changes self.T | |
| met = Metropolis(2) | |
| res_new = OptimizeResult(success=True, fun=0.) | |
| res_old = OptimizeResult(success=True, fun=2000) | |
| with np.errstate(over='raise'): | |
| met.accept_reject(res_new=res_new, res_old=res_old) | |
| def test_gh7799(self): | |
| # gh-7799 reported a problem in which local search was successful but | |
| # basinhopping returned an invalid solution. Show that this is fixed. | |
| def func(x): | |
| return (x**2-8)**2+(x+2)**2 | |
| x0 = -4 | |
| limit = 50 # Constrain to func value >= 50 | |
| con = {'type': 'ineq', 'fun': lambda x: func(x) - limit}, | |
| res = basinhopping( | |
| func, | |
| x0, | |
| 30, | |
| seed=np.random.RandomState(1234), | |
| minimizer_kwargs={'constraints': con} | |
| ) | |
| assert res.success | |
| assert_allclose(res.fun, limit, rtol=1e-6) | |
| def test_accept_gh7799(self): | |
| # Metropolis should not accept the result of an unsuccessful new local | |
| # search if the old local search was successful | |
| met = Metropolis(0) # monotonic basin hopping | |
| res_new = OptimizeResult(success=True, fun=0.) | |
| res_old = OptimizeResult(success=True, fun=1.) | |
| # if new local search was successful and energy is lower, accept | |
| assert met(res_new=res_new, res_old=res_old) | |
| # if new res is unsuccessful, don't accept - even if energy is lower | |
| res_new.success = False | |
| assert not met(res_new=res_new, res_old=res_old) | |
| # ...unless the old res was unsuccessful, too. In that case, why not? | |
| res_old.success = False | |
| assert met(res_new=res_new, res_old=res_old) | |
| def test_reject_all_gh7799(self): | |
| # Test the behavior when there is no feasible solution | |
| def fun(x): | |
| return x@x | |
| def constraint(x): | |
| return x + 1 | |
| kwargs = {'constraints': {'type': 'eq', 'fun': constraint}, | |
| 'bounds': [(0, 1), (0, 1)], 'method': 'slsqp'} | |
| res = basinhopping(fun, x0=[2, 3], niter=10, minimizer_kwargs=kwargs) | |
| assert not res.success | |
| class Test_AdaptiveStepsize: | |
| def setup_method(self): | |
| self.stepsize = 1. | |
| self.ts = RandomDisplacement(stepsize=self.stepsize) | |
| self.target_accept_rate = 0.5 | |
| self.takestep = AdaptiveStepsize(takestep=self.ts, verbose=False, | |
| accept_rate=self.target_accept_rate) | |
| def test_adaptive_increase(self): | |
| # if few steps are rejected, the stepsize should increase | |
| x = 0. | |
| self.takestep(x) | |
| self.takestep.report(False) | |
| for i in range(self.takestep.interval): | |
| self.takestep(x) | |
| self.takestep.report(True) | |
| assert_(self.ts.stepsize > self.stepsize) | |
| def test_adaptive_decrease(self): | |
| # if few steps are rejected, the stepsize should increase | |
| x = 0. | |
| self.takestep(x) | |
| self.takestep.report(True) | |
| for i in range(self.takestep.interval): | |
| self.takestep(x) | |
| self.takestep.report(False) | |
| assert_(self.ts.stepsize < self.stepsize) | |
| def test_all_accepted(self): | |
| # test that everything works OK if all steps were accepted | |
| x = 0. | |
| for i in range(self.takestep.interval + 1): | |
| self.takestep(x) | |
| self.takestep.report(True) | |
| assert_(self.ts.stepsize > self.stepsize) | |
| def test_all_rejected(self): | |
| # test that everything works OK if all steps were rejected | |
| x = 0. | |
| for i in range(self.takestep.interval + 1): | |
| self.takestep(x) | |
| self.takestep.report(False) | |
| assert_(self.ts.stepsize < self.stepsize) | |