Skip to content

Commit b919b4a

Browse files
authored
MAINT: optimize.basinhopping: transition seed -> rng (scipy#21847)
* MAINT: optimize.basinhopping: transition seed -> rng per SPEC-007
1 parent 3367349 commit b919b4a

File tree

2 files changed

+38
-51
lines changed

2 files changed

+38
-51
lines changed

scipy/optimize/_basinhopping.py

Lines changed: 24 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import math
66
import inspect
77
import scipy.optimize
8-
from scipy._lib._util import check_random_state
8+
from scipy._lib._util import check_random_state, _transition_to_rng
99

1010
__all__ = ['basinhopping']
1111

@@ -264,25 +264,17 @@ class RandomDisplacement:
264264
----------
265265
stepsize : float, optional
266266
Maximum stepsize in any dimension
267-
random_gen : {None, int, `numpy.random.Generator`,
268-
`numpy.random.RandomState`}, optional
269-
270-
If `seed` is None (or `np.random`), the `numpy.random.RandomState`
271-
singleton is used.
272-
If `seed` is an int, a new ``RandomState`` instance is used,
273-
seeded with `seed`.
274-
If `seed` is already a ``Generator`` or ``RandomState`` instance then
275-
that instance is used.
276-
267+
rng : {None, int, `numpy.random.Generator`}, optional
268+
Random number generator
277269
"""
278270

279-
def __init__(self, stepsize=0.5, random_gen=None):
271+
def __init__(self, stepsize=0.5, rng=None):
280272
self.stepsize = stepsize
281-
self.random_gen = check_random_state(random_gen)
273+
self.rng = check_random_state(rng)
282274

283275
def __call__(self, x):
284-
x += self.random_gen.uniform(-self.stepsize, self.stepsize,
285-
np.shape(x))
276+
x += self.rng.uniform(-self.stepsize, self.stepsize,
277+
np.shape(x))
286278
return x
287279

288280

@@ -309,25 +301,17 @@ class Metropolis:
309301
----------
310302
T : float
311303
The "temperature" parameter for the accept or reject criterion.
312-
random_gen : {None, int, `numpy.random.Generator`,
313-
`numpy.random.RandomState`}, optional
314-
315-
If `seed` is None (or `np.random`), the `numpy.random.RandomState`
316-
singleton is used.
317-
If `seed` is an int, a new ``RandomState`` instance is used,
318-
seeded with `seed`.
319-
If `seed` is already a ``Generator`` or ``RandomState`` instance then
320-
that instance is used.
304+
rng : {None, int, `numpy.random.Generator`}, optional
321305
Random number generator used for acceptance test.
322306
323307
"""
324308

325-
def __init__(self, T, random_gen=None):
309+
def __init__(self, T, rng=None):
326310
# Avoid ZeroDivisionError since "MBH can be regarded as a special case
327311
# of the BH framework with the Metropolis criterion, where temperature
328312
# T = 0." (Reject all steps that increase energy.)
329313
self.beta = 1.0 / T if T != 0 else float('inf')
330-
self.random_gen = check_random_state(random_gen)
314+
self.rng = check_random_state(rng)
331315

332316
def accept_reject(self, res_new, res_old):
333317
"""
@@ -348,7 +332,7 @@ def accept_reject(self, res_new, res_old):
348332
prod = -(res_new.fun - res_old.fun) * self.beta
349333
w = math.exp(min(0, prod))
350334

351-
rand = self.random_gen.uniform()
335+
rand = self.rng.uniform()
352336
return w >= rand and (res_new.success or not res_old.success)
353337

354338
def __call__(self, *, res_new, res_old):
@@ -358,10 +342,11 @@ def __call__(self, *, res_new, res_old):
358342
return bool(self.accept_reject(res_new, res_old))
359343

360344

345+
@_transition_to_rng("seed", position_num=12, replace_doc=True)
361346
def basinhopping(func, x0, niter=100, T=1.0, stepsize=0.5,
362347
minimizer_kwargs=None, take_step=None, accept_test=None,
363348
callback=None, interval=50, disp=False, niter_success=None,
364-
seed=None, *, target_accept_rate=0.5, stepwise_factor=0.9):
349+
rng=None, *, target_accept_rate=0.5, stepwise_factor=0.9):
365350
"""Find the global minimum of a function using the basin-hopping algorithm.
366351
367352
Basin-hopping is a two-phase method that combines a global stepping
@@ -432,15 +417,13 @@ def basinhopping(func, x0, niter=100, T=1.0, stepsize=0.5,
432417
niter_success : integer, optional
433418
Stop the run if the global minimum candidate remains the same for this
434419
number of iterations.
435-
seed : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional
436-
If `seed` is None (or `np.random`), the `numpy.random.RandomState`
437-
singleton is used.
438-
If `seed` is an int, a new ``RandomState`` instance is used,
439-
seeded with `seed`.
440-
If `seed` is already a ``Generator`` or ``RandomState`` instance then
441-
that instance is used.
442-
Specify `seed` for repeatable minimizations. The random numbers
443-
generated with this seed only affect the default Metropolis
420+
rng : `numpy.random.Generator`, optional
421+
Pseudorandom number generator state. When `rng` is None, a new
422+
`numpy.random.Generator` is created using entropy from the
423+
operating system. Types other than `numpy.random.Generator` are
424+
passed to `numpy.random.default_rng` to instantiate a ``Generator``.
425+
426+
The random numbers generated only affect the default Metropolis
444427
`accept_test` and the default `take_step`. If you supply your own
445428
`take_step` and `accept_test`, and these functions use random
446429
number generation, then those functions are responsible for the state
@@ -641,7 +624,7 @@ def basinhopping(func, x0, niter=100, T=1.0, stepsize=0.5,
641624
642625
>>> rng = np.random.default_rng()
643626
>>> ret = basinhopping(func2d, x0, minimizer_kwargs=minimizer_kwargs,
644-
... niter=10, callback=print_fun, seed=rng)
627+
... niter=10, callback=print_fun, rng=rng)
645628
at minimum 0.4159 accepted 1
646629
at minimum -0.4317 accepted 1
647630
at minimum -1.0109 accepted 1
@@ -666,7 +649,7 @@ def basinhopping(func, x0, niter=100, T=1.0, stepsize=0.5,
666649
x0 = np.array(x0)
667650

668651
# set up the np.random generator
669-
rng = check_random_state(seed)
652+
rng = check_random_state(rng)
670653

671654
# set up minimizer
672655
if minimizer_kwargs is None:
@@ -690,7 +673,7 @@ def basinhopping(func, x0, niter=100, T=1.0, stepsize=0.5,
690673
take_step_wrapped = take_step
691674
else:
692675
# use default
693-
displace = RandomDisplacement(stepsize=stepsize, random_gen=rng)
676+
displace = RandomDisplacement(stepsize=stepsize, rng=rng)
694677
take_step_wrapped = AdaptiveStepsize(displace, interval=interval,
695678
accept_rate=target_accept_rate,
696679
factor=stepwise_factor,
@@ -704,7 +687,7 @@ def basinhopping(func, x0, niter=100, T=1.0, stepsize=0.5,
704687
accept_tests = [accept_test]
705688

706689
# use default
707-
metropolis = Metropolis(T, random_gen=rng)
690+
metropolis = Metropolis(T, rng=rng)
708691
accept_tests.append(metropolis)
709692

710693
if niter_success is None:

scipy/optimize/tests/test__basinhopping.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,6 @@ def setup_method(self):
118118
self.niter = 100
119119
self.disp = False
120120

121-
# fix random seed
122-
np.random.seed(1234)
123-
124121
self.kwargs = {"method": "L-BFGS-B", "jac": True}
125122
self.kwargs_nograd = {"method": "L-BFGS-B"}
126123

@@ -301,8 +298,8 @@ def test_niter_zero(self):
301298
basinhopping(func1d, self.x0[i], minimizer_kwargs=self.kwargs,
302299
niter=0, disp=self.disp)
303300

304-
def test_seed_reproducibility(self):
305-
# seed should ensure reproducibility between runs
301+
def test_rng_reproducibility(self):
302+
# rng should ensure reproducibility between runs
306303
minimizer_kwargs = {"method": "L-BFGS-B", "jac": True}
307304

308305
f_1 = []
@@ -311,15 +308,15 @@ def callback(x, f, accepted):
311308
f_1.append(f)
312309

313310
basinhopping(func2d, [1.0, 1.0], minimizer_kwargs=minimizer_kwargs,
314-
niter=10, callback=callback, seed=10)
311+
niter=10, callback=callback, rng=10)
315312

316313
f_2 = []
317314

318315
def callback2(x, f, accepted):
319316
f_2.append(f)
320317

321318
basinhopping(func2d, [1.0, 1.0], minimizer_kwargs=minimizer_kwargs,
322-
niter=10, callback=callback2, seed=10)
319+
niter=10, callback=callback2, rng=10)
323320
assert_equal(np.array(f_1), np.array(f_2))
324321

325322
def test_random_gen(self):
@@ -330,17 +327,18 @@ def test_random_gen(self):
330327

331328
res1 = basinhopping(func2d, [1.0, 1.0],
332329
minimizer_kwargs=minimizer_kwargs,
333-
niter=10, seed=rng)
330+
niter=10, rng=rng)
334331

335332
rng = np.random.default_rng(1)
336333
res2 = basinhopping(func2d, [1.0, 1.0],
337334
minimizer_kwargs=minimizer_kwargs,
338-
niter=10, seed=rng)
335+
niter=10, rng=rng)
339336
assert_equal(res1.x, res2.x)
340337

341338
def test_monotonic_basin_hopping(self):
342339
# test 1-D minimizations with gradient and T=0
343340
i = 0
341+
344342
res = basinhopping(func1d, self.x0[i], minimizer_kwargs=self.kwargs,
345343
niter=self.niter, disp=self.disp, T=0)
346344
assert_almost_equal(res.x, self.sol[i], self.tol)
@@ -449,7 +447,13 @@ def func(x):
449447
x0 = -4
450448
limit = 50 # Constrain to func value >= 50
451449
con = {'type': 'ineq', 'fun': lambda x: func(x) - limit},
452-
res = basinhopping(func, x0, 30, minimizer_kwargs={'constraints': con})
450+
res = basinhopping(
451+
func,
452+
x0,
453+
30,
454+
seed=np.random.RandomState(1234),
455+
minimizer_kwargs={'constraints': con}
456+
)
453457
assert res.success
454458
assert_allclose(res.fun, limit, rtol=1e-6)
455459

0 commit comments

Comments
 (0)