| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- # Dual annealing unit tests implementation.
- # Copyright (c) 2018 Sylvain Gubian <sylvain.gubian@pmi.com>,
- # Yang Xiang <yang.xiang@pmi.com>
- # Author: Sylvain Gubian, PMP S.A.
- """
- Unit tests for the dual annealing global optimizer
- """
- from scipy.optimize import dual_annealing, Bounds
- from scipy.optimize._dual_annealing import EnergyState
- from scipy.optimize._dual_annealing import LocalSearchWrapper
- from scipy.optimize._dual_annealing import ObjectiveFunWrapper
- from scipy.optimize._dual_annealing import StrategyChain
- from scipy.optimize._dual_annealing import VisitingDistribution
- from scipy.optimize import rosen, rosen_der
- import pytest
- import numpy as np
- from numpy.testing import assert_equal, assert_allclose, assert_array_less
- from pytest import raises as assert_raises
- from scipy._lib._util import check_random_state
- import threading
- class TestDualAnnealing:
- def setup_method(self):
- # A function that returns always infinity for initialization tests
- self.weirdfunc = lambda x: np.inf
- # 2-D bounds for testing function
- self.ld_bounds = [(-5.12, 5.12)] * 2
- # 4-D bounds for testing function
- self.hd_bounds = self.ld_bounds * 4
- # Number of values to be generated for testing visit function
- self.nbtestvalues = 5000
- self.high_temperature = 5230
- self.low_temperature = 0.1
- self.qv = 2.62
- self.seed = 1234
- self.nb_fun_call = threading.local()
- self.ngev = threading.local()
- def callback(self, x, f, context):
- # For testing callback mechanism. Should stop for e <= 1 as
- # the callback function returns True
- if f <= 1.0:
- return True
- def func(self, x, args=()):
- # Using Rastrigin function for performing tests
- if args:
- shift = args
- else:
- shift = 0
- y = np.sum((x - shift) ** 2 - 10 * np.cos(2 * np.pi * (
- x - shift))) + 10 * np.size(x) + shift
- if not hasattr(self.nb_fun_call, 'c'):
- self.nb_fun_call.c = 0
- self.nb_fun_call.c += 1
- return y
- def rosen_der_wrapper(self, x, args=()):
- if not hasattr(self.ngev, 'c'):
- self.ngev.c = 0
- self.ngev.c += 1
- return rosen_der(x, *args)
- # FIXME: there are some discontinuities in behaviour as a function of `qv`,
- # this needs investigating - see gh-12384
- @pytest.mark.parametrize('qv', [1.1, 1.41, 2, 2.62, 2.9])
- def test_visiting_stepping(self, qv):
- rng = np.random.default_rng(1234)
- lu = list(zip(*self.ld_bounds))
- lower = np.array(lu[0])
- upper = np.array(lu[1])
- dim = lower.size
- vd = VisitingDistribution(lower, upper, qv, rng)
- values = np.zeros(dim)
- x_step_low = vd.visiting(values, 0, self.high_temperature)
- # Make sure that only the first component is changed
- assert_equal(np.not_equal(x_step_low, 0), True)
- values = np.zeros(dim)
- x_step_high = vd.visiting(values, dim, self.high_temperature)
- # Make sure that component other than at dim has changed
- assert_equal(np.not_equal(x_step_high[0], 0), True)
- @pytest.mark.parametrize('qv', [2.25, 2.62, 2.9])
- def test_visiting_dist_high_temperature(self, qv):
- rng = np.random.default_rng(1234)
- lu = list(zip(*self.ld_bounds))
- lower = np.array(lu[0])
- upper = np.array(lu[1])
- vd = VisitingDistribution(lower, upper, qv, rng)
- # values = np.zeros(self.nbtestvalues)
- # for i in np.arange(self.nbtestvalues):
- # values[i] = vd.visit_fn(self.high_temperature)
- values = vd.visit_fn(self.high_temperature, self.nbtestvalues)
- # Visiting distribution is a distorted version of Cauchy-Lorentz
- # distribution, and as no 1st and higher moments (no mean defined,
- # no variance defined).
- # Check that big tails values are generated
- assert_array_less(np.min(values), 1e-10)
- assert_array_less(1e+10, np.max(values))
- def test_reset(self):
- owf = ObjectiveFunWrapper(self.weirdfunc)
- lu = list(zip(*self.ld_bounds))
- lower = np.array(lu[0])
- upper = np.array(lu[1])
- es = EnergyState(lower, upper)
- assert_raises(ValueError, es.reset, owf, check_random_state(None))
- def test_low_dim(self):
- ret = dual_annealing(
- self.func, self.ld_bounds, rng=self.seed)
- assert_allclose(ret.fun, 0., atol=1e-12)
- assert ret.success
- @pytest.mark.fail_slow(10)
- def test_high_dim(self):
- ret = dual_annealing(self.func, self.hd_bounds, rng=self.seed)
- assert_allclose(ret.fun, 0., atol=1e-12)
- assert ret.success
- def test_low_dim_no_ls(self):
- ret = dual_annealing(self.func, self.ld_bounds,
- no_local_search=True, seed=self.seed)
- assert_allclose(ret.fun, 0., atol=1e-4)
- @pytest.mark.fail_slow(10)
- def test_high_dim_no_ls(self):
- ret = dual_annealing(self.func, self.hd_bounds,
- no_local_search=True, rng=self.seed)
- assert_allclose(ret.fun, 0., atol=1.2e-4)
- def test_nb_fun_call(self):
- self.nb_fun_call.c = 0
- ret = dual_annealing(self.func, self.ld_bounds, rng=self.seed)
- assert_equal(self.nb_fun_call.c, ret.nfev)
- def test_nb_fun_call_no_ls(self):
- self.nb_fun_call.c = 0
- ret = dual_annealing(self.func, self.ld_bounds,
- no_local_search=True, rng=self.seed)
- assert_equal(self.nb_fun_call.c, ret.nfev)
- def test_max_reinit(self):
- assert_raises(ValueError, dual_annealing, self.weirdfunc,
- self.ld_bounds)
- @pytest.mark.fail_slow(10)
- def test_reproduce(self):
- res1 = dual_annealing(self.func, self.ld_bounds, rng=self.seed)
- res2 = dual_annealing(self.func, self.ld_bounds, rng=self.seed)
- res3 = dual_annealing(self.func, self.ld_bounds, rng=self.seed)
- # If we have reproducible results, x components found has to
- # be exactly the same, which is not the case with no seeding
- assert_equal(res1.x, res2.x)
- assert_equal(res1.x, res3.x)
- def test_rand_gen(self):
- # check that np.random.Generator can be used (numpy >= 1.17)
- # obtain a np.random.Generator object
- rng = np.random.default_rng(1)
- res1 = dual_annealing(self.func, self.ld_bounds, rng=rng)
- # seed again
- rng = np.random.default_rng(1)
- res2 = dual_annealing(self.func, self.ld_bounds, rng=rng)
- # If we have reproducible results, x components found has to
- # be exactly the same, which is not the case with no seeding
- assert_equal(res1.x, res2.x)
- def test_bounds_integrity(self):
- wrong_bounds = [(-5.12, 5.12), (1, 0), (5.12, 5.12)]
- assert_raises(ValueError, dual_annealing, self.func,
- wrong_bounds)
- def test_bound_validity(self):
- invalid_bounds = [(-5, 5), (-np.inf, 0), (-5, 5)]
- assert_raises(ValueError, dual_annealing, self.func,
- invalid_bounds)
- invalid_bounds = [(-5, 5), (0, np.inf), (-5, 5)]
- assert_raises(ValueError, dual_annealing, self.func,
- invalid_bounds)
- invalid_bounds = [(-5, 5), (0, np.nan), (-5, 5)]
- assert_raises(ValueError, dual_annealing, self.func,
- invalid_bounds)
- def test_deprecated_local_search_options_bounds(self):
- def func(x):
- return np.sum((x - 5) * (x - 1))
- bounds = list(zip([-6, -5], [6, 5]))
- # Test bounds can be passed (see gh-10831)
- with pytest.warns(RuntimeWarning, match=r"Method CG cannot handle "):
- dual_annealing(
- func,
- bounds=bounds,
- minimizer_kwargs={"method": "CG", "bounds": bounds})
- def test_minimizer_kwargs_bounds(self):
- def func(x):
- return np.sum((x - 5) * (x - 1))
- bounds = list(zip([-6, -5], [6, 5]))
- # Test bounds can be passed (see gh-10831)
- dual_annealing(
- func,
- bounds=bounds,
- minimizer_kwargs={"method": "SLSQP", "bounds": bounds})
- with pytest.warns(RuntimeWarning, match=r"Method CG cannot handle "):
- dual_annealing(
- func,
- bounds=bounds,
- minimizer_kwargs={"method": "CG", "bounds": bounds})
- def test_max_fun_ls(self):
- ret = dual_annealing(self.func, self.ld_bounds, maxfun=100,
- rng=self.seed)
- ls_max_iter = min(max(
- len(self.ld_bounds) * LocalSearchWrapper.LS_MAXITER_RATIO,
- LocalSearchWrapper.LS_MAXITER_MIN),
- LocalSearchWrapper.LS_MAXITER_MAX)
- assert ret.nfev <= 100 + ls_max_iter
- assert not ret.success
- def test_max_fun_no_ls(self):
- ret = dual_annealing(self.func, self.ld_bounds,
- no_local_search=True, maxfun=500, rng=self.seed)
- assert ret.nfev <= 500
- assert not ret.success
- def test_maxiter(self):
- ret = dual_annealing(self.func, self.ld_bounds, maxiter=700,
- rng=self.seed)
- assert ret.nit <= 700
- # Testing that args are passed correctly for dual_annealing
- def test_fun_args_ls(self):
- ret = dual_annealing(self.func, self.ld_bounds,
- args=((3.14159,)), rng=self.seed)
- assert_allclose(ret.fun, 3.14159, atol=1e-6)
- # Testing that args are passed correctly for pure simulated annealing
- def test_fun_args_no_ls(self):
- ret = dual_annealing(self.func, self.ld_bounds,
- args=((3.14159, )), no_local_search=True,
- rng=self.seed)
- assert_allclose(ret.fun, 3.14159, atol=1e-4)
- def test_callback_stop(self):
- # Testing that callback make the algorithm stop for
- # fun value <= 1.0 (see callback method)
- ret = dual_annealing(self.func, self.ld_bounds,
- callback=self.callback, rng=self.seed)
- assert ret.fun <= 1.0
- assert 'stop early' in ret.message[0]
- assert not ret.success
- @pytest.mark.parametrize('method, atol', [
- ('Nelder-Mead', 2e-5),
- ('COBYLA', 1e-5),
- ('COBYQA', 1e-8),
- ('Powell', 1e-8),
- ('CG', 1e-8),
- ('BFGS', 1e-8),
- ('TNC', 1e-8),
- ('SLSQP', 2e-7),
- ])
- def test_multi_ls_minimizer(self, method, atol):
- ret = dual_annealing(self.func, self.ld_bounds,
- minimizer_kwargs=dict(method=method),
- rng=self.seed)
- assert_allclose(ret.fun, 0., atol=atol)
- def test_wrong_restart_temp(self):
- assert_raises(ValueError, dual_annealing, self.func,
- self.ld_bounds, restart_temp_ratio=1)
- assert_raises(ValueError, dual_annealing, self.func,
- self.ld_bounds, restart_temp_ratio=0)
- def test_gradient_gnev(self):
- minimizer_opts = {
- 'jac': self.rosen_der_wrapper,
- }
- ret = dual_annealing(rosen, self.ld_bounds,
- minimizer_kwargs=minimizer_opts,
- rng=self.seed)
- assert ret.njev == self.ngev.c
- @pytest.mark.fail_slow(10)
- def test_from_docstring(self):
- def func(x):
- return np.sum(x * x - 10 * np.cos(2 * np.pi * x)) + 10 * np.size(x)
- lw = [-5.12] * 10
- up = [5.12] * 10
- ret = dual_annealing(func, bounds=list(zip(lw, up)), rng=1234)
- assert_allclose(ret.x,
- [-4.26437714e-09, -3.91699361e-09, -1.86149218e-09,
- -3.97165720e-09, -6.29151648e-09, -6.53145322e-09,
- -3.93616815e-09, -6.55623025e-09, -6.05775280e-09,
- -5.00668935e-09], atol=4e-8)
- assert_allclose(ret.fun, 0.000000, atol=5e-13)
- @pytest.mark.parametrize('new_e, temp_step, accepted, accept_rate', [
- (0, 100, 1000, 1.0097587941791923),
- (0, 2, 1000, 1.2599210498948732),
- (10, 100, 878, 0.8786035869128718),
- (10, 60, 695, 0.6812920690579612),
- (2, 100, 990, 0.9897404249173424),
- ])
- def test_accept_reject_probabilistic(
- self, new_e, temp_step, accepted, accept_rate):
- # Test accepts unconditionally with e < current_energy and
- # probabilistically with e > current_energy
- rs = check_random_state(123)
- count_accepted = 0
- iterations = 1000
- accept_param = -5
- current_energy = 1
- for _ in range(iterations):
- energy_state = EnergyState(lower=None, upper=None)
- # Set energy state with current_energy, any location.
- energy_state.update_current(current_energy, [0])
- chain = StrategyChain(
- accept_param, None, None, None, rs, energy_state)
- # Normally this is set in run()
- chain.temperature_step = temp_step
- # Check if update is accepted.
- chain.accept_reject(j=1, e=new_e, x_visit=[2])
- if energy_state.current_energy == new_e:
- count_accepted += 1
- assert count_accepted == accepted
- # Check accept rate
- pqv = 1 - (1 - accept_param) * (new_e - current_energy) / temp_step
- rate = 0 if pqv <= 0 else np.exp(np.log(pqv) / (1 - accept_param))
- assert_allclose(rate, accept_rate)
- @pytest.mark.fail_slow(10)
- def test_bounds_class(self):
- # test that result does not depend on the bounds type
- def func(x):
- f = np.sum(x * x - 10 * np.cos(2 * np.pi * x)) + 10 * np.size(x)
- return f
- lw = [-5.12] * 5
- up = [5.12] * 5
- # Unbounded global minimum is all zeros. Most bounds below will force
- # a DV away from unbounded minimum and be active at solution.
- up[0] = -2.0
- up[1] = -1.0
- lw[3] = 1.0
- lw[4] = 2.0
- # run optimizations
- bounds = Bounds(lw, up)
- ret_bounds_class = dual_annealing(func, bounds=bounds, rng=1234)
- bounds_old = list(zip(lw, up))
- ret_bounds_list = dual_annealing(func, bounds=bounds_old, rng=1234)
- # test that found minima, function evaluations and iterations match
- assert_allclose(ret_bounds_class.x, ret_bounds_list.x, atol=1e-8)
- assert_allclose(ret_bounds_class.x, np.arange(-2, 3), atol=1e-7)
- assert_allclose(ret_bounds_list.fun, ret_bounds_class.fun, atol=1e-9)
- assert ret_bounds_list.nfev == ret_bounds_class.nfev
- @pytest.mark.fail_slow(10)
- def test_callable_jac_hess_with_args_gh11052(self):
- # dual_annealing used to fail when `jac` was callable and `args` were
- # used; check that this is resolved. Example is from gh-11052.
- # extended to hess as part of closing gh20614
- rng = np.random.default_rng(94253637693657847462)
- def f(x, power):
- return np.sum(np.exp(x ** power))
- def jac(x, power):
- return np.exp(x ** power) * power * x ** (power - 1)
- def hess(x, power):
- # calculated using WolframAlpha as d^2/dx^2 e^(x^p)
- return np.diag(
- power * np.exp(x ** power) * x ** (power - 2) *
- (power * x ** power + power - 1)
- )
- def hessp(x, p, power):
- return hess(x, power) @ p
- res1 = dual_annealing(f, args=(2, ), bounds=[[0, 1], [0, 1]], rng=rng,
- minimizer_kwargs=dict(method='L-BFGS-B'))
- res2 = dual_annealing(f, args=(2, ), bounds=[[0, 1], [0, 1]], rng=rng,
- minimizer_kwargs=dict(method='L-BFGS-B',
- jac=jac))
- res3 = dual_annealing(f, args=(2, ), bounds=[[0, 1], [0, 1]], rng=rng,
- minimizer_kwargs=dict(method='newton-cg',
- jac=jac, hess=hess))
- res4 = dual_annealing(f, args=(2, ), bounds=[[0, 1], [0, 1]], rng=rng,
- minimizer_kwargs=dict(method='newton-cg',
- jac=jac, hessp=hessp))
- assert_allclose(res1.fun, res2.fun, rtol=1e-6)
- assert_allclose(res3.fun, res2.fun, rtol=1e-6)
- assert_allclose(res4.fun, res2.fun, rtol=1e-6)
|