Skip to content

Commit e5a0727

Browse files
andyfaffmdhaber
andauthored
API: optimize.dual_annealing: adopt SPEC007 (scipy#21823)
--------- Co-authored-by: Matt Haberland <[email protected]>
1 parent ea24d2c commit e5a0727

File tree

4 files changed

+85
-91
lines changed

4 files changed

+85
-91
lines changed

scipy/_lib/tests/test__util.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
from scipy._lib._util import (_aligned_zeros, check_random_state, MapWrapper,
1818
getfullargspec_no_self, FullArgSpec,
1919
rng_integers, _validate_int, _rename_parameter,
20-
_contains_nan, _rng_html_rewrite, _lazywhere)
20+
_contains_nan, _rng_html_rewrite, _lazywhere,
21+
_transition_to_rng)
2122

2223
skip_xp_backends = pytest.mark.skip_xp_backends
2324

@@ -388,6 +389,26 @@ def mock_str():
388389
assert res == ref
389390

390391

392+
@_transition_to_rng("seed", position_num=1, replace_doc=False)
393+
def _f_seed(o, rng=None):
394+
rg = check_random_state(rng)
395+
return rg.uniform(size=o)
396+
397+
398+
def test__transition_to_rng():
399+
# SPEC-007 changes
400+
_f_seed(1, rng=1)
401+
_f_seed(1, rng=np.random.default_rng())
402+
_f_seed(1, seed=1)
403+
_f_seed(1, seed=np.random.RandomState())
404+
with assert_raises(TypeError):
405+
# can't pass both seed and rng
406+
_f_seed(1, seed=1234, rng=1234)
407+
with assert_raises(TypeError):
408+
# use of rng=RandomState should give rise to an error.
409+
_f_seed(rng=np.random.RandomState())
410+
411+
391412
class TestLazywhere:
392413
n_arrays = strategies.integers(min_value=1, max_value=3)
393414
rng_seed = strategies.integers(min_value=1000000000, max_value=9999999999)

scipy/optimize/_dual_annealing.py

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from scipy.optimize import OptimizeResult
1212
from scipy.optimize import minimize, Bounds
1313
from scipy.special import gammaln
14-
from scipy._lib._util import check_random_state
14+
from scipy._lib._util import check_random_state, _transition_to_rng
1515
from scipy.optimize._constraints import new_bounds_to_old
1616

1717
__all__ = ['dual_annealing']
@@ -38,21 +38,22 @@ class VisitingDistribution:
3838
makes the algorithm jump to a more distant region.
3939
The value range is (1, 3]. Its value is fixed for the life of the
4040
object.
41-
rand_gen : {`~numpy.random.RandomState`, `~numpy.random.Generator`}
42-
A `~numpy.random.RandomState`, `~numpy.random.Generator` object
43-
for using the current state of the created random generator container.
41+
rng_gen : {`~numpy.random.Generator`}
42+
A `~numpy.random.Generator` object for generating new locations.
43+
(can be a `~numpy.random.RandomState` object until SPEC007 transition
44+
is fully complete).
4445
4546
"""
4647
TAIL_LIMIT = 1.e8
4748
MIN_VISIT_BOUND = 1.e-10
4849

49-
def __init__(self, lb, ub, visiting_param, rand_gen):
50+
def __init__(self, lb, ub, visiting_param, rng_gen):
5051
# if you wish to make _visiting_param adjustable during the life of
5152
# the object then _factor2, _factor3, _factor5, _d1, _factor6 will
5253
# have to be dynamically calculated in `visit_fn`. They're factored
5354
# out here so they don't need to be recalculated all the time.
5455
self._visiting_param = visiting_param
55-
self.rand_gen = rand_gen
56+
self.rng_gen = rng_gen
5657
self.lower = lb
5758
self.upper = ub
5859
self.bound_range = ub - lb
@@ -79,7 +80,7 @@ def visiting(self, x, step, temperature):
7980
if step < dim:
8081
# Changing all coordinates with a new visiting value
8182
visits = self.visit_fn(temperature, dim)
82-
upper_sample, lower_sample = self.rand_gen.uniform(size=2)
83+
upper_sample, lower_sample = self.rng_gen.uniform(size=2)
8384
visits[visits > self.TAIL_LIMIT] = self.TAIL_LIMIT * upper_sample
8485
visits[visits < -self.TAIL_LIMIT] = -self.TAIL_LIMIT * lower_sample
8586
x_visit = visits + x
@@ -94,9 +95,9 @@ def visiting(self, x, step, temperature):
9495
x_visit = np.copy(x)
9596
visit = self.visit_fn(temperature, 1)[0]
9697
if visit > self.TAIL_LIMIT:
97-
visit = self.TAIL_LIMIT * self.rand_gen.uniform()
98+
visit = self.TAIL_LIMIT * self.rng_gen.uniform()
9899
elif visit < -self.TAIL_LIMIT:
99-
visit = -self.TAIL_LIMIT * self.rand_gen.uniform()
100+
visit = -self.TAIL_LIMIT * self.rng_gen.uniform()
100101
index = step - dim
101102
x_visit[index] = visit + x[index]
102103
a = x_visit[index] - self.lower[index]
@@ -110,7 +111,7 @@ def visiting(self, x, step, temperature):
110111

111112
def visit_fn(self, temperature, dim):
112113
""" Formula Visita from p. 405 of reference [2] """
113-
x, y = self.rand_gen.normal(size=(dim, 2)).T
114+
x, y = self.rng_gen.normal(size=(dim, 2)).T
114115

115116
factor1 = np.exp(np.log(temperature) / (self._visiting_param - 1.0))
116117
factor4 = self._factor4_p * factor1
@@ -156,14 +157,14 @@ def __init__(self, lower, upper, callback=None):
156157
self.upper = upper
157158
self.callback = callback
158159

159-
def reset(self, func_wrapper, rand_gen, x0=None):
160+
def reset(self, func_wrapper, rng_gen, x0=None):
160161
"""
161162
Initialize current location is the search domain. If `x0` is not
162163
provided, a random location within the bounds is generated.
163164
"""
164165
if x0 is None:
165-
self.current_location = rand_gen.uniform(self.lower, self.upper,
166-
size=len(self.lower))
166+
self.current_location = rng_gen.uniform(self.lower, self.upper,
167+
size=len(self.lower))
167168
else:
168169
self.current_location = np.copy(x0)
169170
init_error = True
@@ -181,9 +182,9 @@ def reset(self, func_wrapper, rand_gen, x0=None):
181182
'trying new random parameters'
182183
)
183184
raise ValueError(message)
184-
self.current_location = rand_gen.uniform(self.lower,
185-
self.upper,
186-
size=self.lower.size)
185+
self.current_location = rng_gen.uniform(self.lower,
186+
self.upper,
187+
size=self.lower.size)
187188
reinit_counter += 1
188189
else:
189190
init_error = False
@@ -448,10 +449,11 @@ def local_search(self, x, e):
448449
return e, x_tmp
449450

450451

452+
@_transition_to_rng("seed", position_num=10)
451453
def dual_annealing(func, bounds, args=(), maxiter=1000,
452454
minimizer_kwargs=None, initial_temp=5230.,
453455
restart_temp_ratio=2.e-5, visit=2.62, accept=-5.0,
454-
maxfun=1e7, seed=None, no_local_search=False,
456+
maxfun=1e7, rng=None, no_local_search=False,
455457
callback=None, x0=None):
456458
"""
457459
Find the global minimum of a function using Dual Annealing.
@@ -507,15 +509,14 @@ def dual_annealing(func, bounds, args=(), maxiter=1000,
507509
algorithm is in the middle of a local search, this number will be
508510
exceeded, the algorithm will stop just after the local search is
509511
done. Default value is 1e7.
510-
seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional
511-
If `seed` is None (or `np.random`), the `numpy.random.RandomState`
512-
singleton is used.
513-
If `seed` is an int, a new ``RandomState`` instance is used,
514-
seeded with `seed`.
515-
If `seed` is already a ``Generator`` or ``RandomState`` instance then
516-
that instance is used.
517-
Specify `seed` for repeatable minimizations. The random numbers
518-
generated with this seed only affect the visiting distribution function
512+
rng : `numpy.random.Generator`, optional
513+
Pseudorandom number generator state. When `rng` is None, a new
514+
`numpy.random.Generator` is created using entropy from the
515+
operating system. Types other than `numpy.random.Generator` are
516+
passed to `numpy.random.default_rng` to instantiate a `Generator`.
517+
518+
Specify `rng` for repeatable minimizations. The random numbers
519+
generated only affect the visiting distribution function
519520
and new coordinates generation.
520521
no_local_search : bool, optional
521522
If `no_local_search` is set to True, a traditional Generalized
@@ -666,19 +667,19 @@ def dual_annealing(func, bounds, args=(), maxiter=1000,
666667
minimizer_wrapper = LocalSearchWrapper(
667668
bounds, func_wrapper, *args, **minimizer_kwargs)
668669

669-
# Initialization of random Generator for reproducible runs if seed provided
670-
rand_state = check_random_state(seed)
670+
# Initialization of random Generator for reproducible runs if rng provided
671+
rng_gen = check_random_state(rng)
671672
# Initialization of the energy state
672673
energy_state = EnergyState(lower, upper, callback)
673-
energy_state.reset(func_wrapper, rand_state, x0)
674+
energy_state.reset(func_wrapper, rng_gen, x0)
674675
# Minimum value of annealing temperature reached to perform
675676
# re-annealing
676677
temperature_restart = initial_temp * restart_temp_ratio
677678
# VisitingDistribution instance
678-
visit_dist = VisitingDistribution(lower, upper, visit, rand_state)
679+
visit_dist = VisitingDistribution(lower, upper, visit, rng_gen)
679680
# Strategy chain instance
680681
strategy_chain = StrategyChain(accept, visit_dist, func_wrapper,
681-
minimizer_wrapper, rand_state, energy_state)
682+
minimizer_wrapper, rng_gen, energy_state)
682683
need_to_stop = False
683684
iteration = 0
684685
message = []
@@ -701,7 +702,7 @@ def dual_annealing(func, bounds, args=(), maxiter=1000,
701702
break
702703
# Need a re-annealing process?
703704
if temperature < temperature_restart:
704-
energy_state.reset(func_wrapper, rand_state)
705+
energy_state.reset(func_wrapper, rng_gen)
705706
break
706707
# starting strategy chain
707708
val = strategy_chain.run(i, temperature)

scipy/optimize/tests/test__differential_evolution.py

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,8 @@ def test_quadratic_from_diff_ev(self):
493493
# test the quadratic function from differential_evolution function
494494
differential_evolution(self.quadratic,
495495
[(-100, 100)],
496-
tol=0.02)
496+
tol=0.02,
497+
seed=1)
497498

498499
def test_rng_gives_repeatability(self):
499500
result = differential_evolution(self.quadratic,
@@ -523,35 +524,6 @@ def test_random_generator(self):
523524
tol=0.5,
524525
init=init)
525526

526-
def test_rng_seed_spec007(self):
527-
# spec007 involves the transition of RandomState-->Generator
528-
# rng is the new name
529-
differential_evolution(self.quadratic,
530-
[(-100, 100)],
531-
polish=False,
532-
rng=1,
533-
tol=0.5)
534-
# should still be allowed to pass `seed`
535-
differential_evolution(self.quadratic,
536-
[(-100, 100)],
537-
polish=False,
538-
seed=1,
539-
tol=0.5)
540-
with assert_raises(TypeError):
541-
# can't pass both seed and rng
542-
differential_evolution(self.quadratic,
543-
[(-100, 100)],
544-
polish=False,
545-
seed=1,
546-
rng=1,
547-
tol=0.5)
548-
# use of rng=RandomState should give rise to an error.
549-
differential_evolution(self.quadratic,
550-
[(-100, 100)],
551-
polish=False,
552-
rng=np.random.RandomState(),
553-
tol=0.5)
554-
555527
def test_exp_runs(self):
556528
# test whether exponential mutation loop runs
557529
solver = DifferentialEvolutionSolver(rosen,

0 commit comments

Comments
 (0)