Skip to content

Commit 3946a43

Browse files
andyfaffmdhaber
andauthored
MAINT: optimize.quadratic_assignment: SPEC-007 transition to rng (scipy#21848)
* MAINT: optimize.quadratic_assignment: SPEC-007 transition to rng --------- Co-authored-by: Matt Haberland <[email protected]>
1 parent c3da43f commit 3946a43

File tree

2 files changed

+138
-81
lines changed

2 files changed

+138
-81
lines changed

scipy/optimize/_qap.py

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import numpy as np
22
import operator
3+
import warnings
4+
import numbers
35
from . import (linear_sum_assignment, OptimizeResult)
46
from ._optimize import _check_unknown_options
57

@@ -8,6 +10,7 @@
810

911
QUADRATIC_ASSIGNMENT_METHODS = ['faq', '2opt']
1012

13+
1114
def quadratic_assignment(A, B, method="faq", options=None):
1215
r"""
1316
Approximates solution to the quadratic assignment problem and
@@ -60,13 +63,24 @@ def quadratic_assignment(A, B, method="faq", options=None):
6063
``partial_match[i, 1]`` of `B`. The array has shape ``(m, 2)``,
6164
where ``m`` is not greater than the number of nodes, :math:`n`.
6265
63-
rng : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}
64-
If `seed` is None (or `np.random`), the `numpy.random.RandomState`
65-
singleton is used.
66-
If `seed` is an int, a new ``RandomState`` instance is used,
67-
seeded with `seed`.
68-
If `seed` is already a ``Generator`` or ``RandomState`` instance then
69-
that instance is used.
66+
rng : `numpy.random.Generator`, optional
67+
Pseudorandom number generator state. When `rng` is None, a new
68+
`numpy.random.Generator` is created using entropy from the
69+
operating system. Types other than `numpy.random.Generator` are
70+
passed to `numpy.random.default_rng` to instantiate a ``Generator``.
71+
72+
.. versionchanged:: 1.15.0
73+
As part of the `SPEC-007 <https://scientific-python.org/specs/spec-0007/>`_
74+
transition from use of `numpy.random.RandomState` to
75+
`numpy.random.Generator` is occurring. Supplying
76+
`np.random.RandomState` to this function will now emit a
77+
`DeprecationWarning`. In SciPy 1.17 its use will raise an exception.
78+
In addition relying on global state using `np.random.seed`
79+
will emit a `FutureWarning`. In SciPy 1.17 the global random number
80+
generator will no longer be used.
81+
Use of an int-like seed will raise a `FutureWarning`, in SciPy 1.17 it
82+
will be normalized via `np.random.default_rng` rather than
83+
`np.random.RandomState`.
7084
7185
For method-specific options, see
7286
:func:`show_options('quadratic_assignment') <show_options>`.
@@ -112,11 +126,12 @@ def quadratic_assignment(A, B, method="faq", options=None):
112126
--------
113127
>>> import numpy as np
114128
>>> from scipy.optimize import quadratic_assignment
129+
>>> rng = np.random.default_rng()
115130
>>> A = np.array([[0, 80, 150, 170], [80, 0, 130, 100],
116131
... [150, 130, 0, 120], [170, 100, 120, 0]])
117132
>>> B = np.array([[0, 5, 2, 7], [0, 0, 3, 8],
118133
... [0, 0, 0, 3], [0, 0, 0, 0]])
119-
>>> res = quadratic_assignment(A, B)
134+
>>> res = quadratic_assignment(A, B, options={'rng': rng})
120135
>>> print(res)
121136
fun: 3260
122137
col_ind: [0 3 2 1]
@@ -159,7 +174,7 @@ def quadratic_assignment(A, B, method="faq", options=None):
159174
... [8, 5, 0, 2], [6, 1, 2, 0]])
160175
>>> B = np.array([[0, 1, 8, 4], [1, 0, 5, 2],
161176
... [8, 5, 0, 5], [4, 2, 5, 0]])
162-
>>> res = quadratic_assignment(A, B)
177+
>>> res = quadratic_assignment(A, B, options={'rng': rng})
163178
>>> print(res)
164179
fun: 178
165180
col_ind: [1 0 3 2]
@@ -170,7 +185,7 @@ def quadratic_assignment(A, B, method="faq", options=None):
170185
171186
>>> guess = np.array([np.arange(len(A)), res.col_ind]).T
172187
>>> res = quadratic_assignment(A, B, method="2opt",
173-
... options = {'partial_guess': guess})
188+
... options = {'rng': rng, 'partial_guess': guess})
174189
>>> print(res)
175190
fun: 176
176191
col_ind: [1 2 3 0]
@@ -186,10 +201,38 @@ def quadratic_assignment(A, B, method="faq", options=None):
186201
"2opt": _quadratic_assignment_2opt}
187202
if method not in methods:
188203
raise ValueError(f"method {method} must be in {methods}.")
204+
205+
_spec007_transition(options.get("rng", None))
189206
res = methods[method](A, B, **options)
190207
return res
191208

192209

210+
def _spec007_transition(rng):
211+
if isinstance(rng, np.random.RandomState):
212+
warnings.warn(
213+
"Use of `RandomState` with `quadratic_assignment` is deprecated"
214+
" and will result in an exception in SciPy 1.17",
215+
DeprecationWarning,
216+
stacklevel=2
217+
)
218+
if ((rng is None or rng is np.random) and
219+
np.random.mtrand._rand._bit_generator._seed_seq is None):
220+
warnings.warn(
221+
"The NumPy global RNG was seeded by calling `np.random.seed`."
222+
" From SciPy 1.17, this function will no longer use the global RNG.",
223+
FutureWarning,
224+
stacklevel=2
225+
)
226+
if isinstance(rng, numbers.Integral | np.integer):
227+
warnings.warn(
228+
"The behavior when the rng option is an integer is changing: the value"
229+
" will be normalized using np.random.default_rng beginning in SciPy 1.17,"
230+
" and the resulting Generator will be used to generate random numbers.",
231+
FutureWarning,
232+
stacklevel=2
233+
)
234+
235+
193236
def _calc_score(A, B, perm):
194237
# equivalent to objective function but avoids matmul
195238
return np.sum(A * B[perm][:, perm])
@@ -284,13 +327,8 @@ def _quadratic_assignment_faq(A, B,
284327
``partial_match[i, 1]`` of `B`. The array has shape ``(m, 2)``, where
285328
``m`` is not greater than the number of nodes, :math:`n`.
286329
287-
rng : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional
288-
If `seed` is None (or `np.random`), the `numpy.random.RandomState`
289-
singleton is used.
290-
If `seed` is an int, a new ``RandomState`` instance is used,
291-
seeded with `seed`.
292-
If `seed` is already a ``Generator`` or ``RandomState`` instance then
293-
that instance is used.
330+
rng : {None, int, `numpy.random.Generator`}, optional
331+
Pseudorandom number generator state. See `quadratic_assignment` for details.
294332
P0 : 2-D array, "barycenter", or "randomized" (default: "barycenter")
295333
Initial position. Must be a doubly-stochastic matrix [3]_.
296334
@@ -312,7 +350,7 @@ def _quadratic_assignment_faq(A, B,
312350
Integer specifying the max number of Frank-Wolfe iterations performed.
313351
tol : float (default: 0.03)
314352
Tolerance for termination. Frank-Wolfe iteration terminates when
315-
:math:`\frac{||P_{i}-P_{i+1}||_F}{\sqrt{m')}} \leq tol`,
353+
:math:`\frac{||P_{i}-P_{i+1}||_F}{\sqrt{m'}} \leq tol`,
316354
where :math:`i` is the iteration number.
317355
318356
Returns
@@ -343,34 +381,36 @@ def _quadratic_assignment_faq(A, B,
343381
As mentioned above, a barycenter initialization often results in a better
344382
solution than a single random initialization.
345383
346-
>>> from numpy.random import default_rng
347-
>>> rng = default_rng()
384+
>>> from scipy.optimize import quadratic_assignment
385+
>>> import numpy as np
386+
>>> rng = np.random.default_rng()
348387
>>> n = 15
349388
>>> A = rng.random((n, n))
350389
>>> B = rng.random((n, n))
351-
>>> res = quadratic_assignment(A, B) # FAQ is default method
390+
>>> options = {"rng": rng}
391+
>>> res = quadratic_assignment(A, B, options=options) # FAQ is default method
352392
>>> print(res.fun)
353-
46.871483385480545 # may vary
393+
47.797048706380636 # may vary
354394
355-
>>> options = {"P0": "randomized"} # use randomized initialization
395+
>>> options = {"rng": rng, "P0": "randomized"} # use randomized initialization
356396
>>> res = quadratic_assignment(A, B, options=options)
357397
>>> print(res.fun)
358-
47.224831071310625 # may vary
398+
47.37287069769966 # may vary
359399
360400
However, consider running from several randomized initializations and
361401
keeping the best result.
362402
363403
>>> res = min([quadratic_assignment(A, B, options=options)
364404
... for i in range(30)], key=lambda x: x.fun)
365405
>>> print(res.fun)
366-
46.671852533681516 # may vary
406+
46.55974835248574 # may vary
367407
368-
The '2-opt' method can be used to further refine the results.
408+
The '2-opt' method can be used to attempt to refine the results.
369409
370-
>>> options = {"partial_guess": np.array([np.arange(n), res.col_ind]).T}
410+
>>> options = {"partial_guess": np.array([np.arange(n), res.col_ind]).T, "rng": rng}
371411
>>> res = quadratic_assignment(A, B, method="2opt", options=options)
372412
>>> print(res.fun)
373-
46.47160735721583 # may vary
413+
46.55974835248574 # may vary
374414
375415
References
376416
----------
@@ -578,13 +618,8 @@ def _quadratic_assignment_2opt(A, B, maximize=False, rng=None,
578618
-------
579619
maximize : bool (default: False)
580620
Maximizes the objective function if ``True``.
581-
rng : {None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional
582-
If `seed` is None (or `np.random`), the `numpy.random.RandomState`
583-
singleton is used.
584-
If `seed` is an int, a new ``RandomState`` instance is used,
585-
seeded with `seed`.
586-
If `seed` is already a ``Generator`` or ``RandomState`` instance then
587-
that instance is used.
621+
rng : {None, int, `numpy.random.Generator`}, optional
622+
Pseudorandom number generator state. See `quadratic_assignment` for details.
588623
partial_match : 2-D array of integers, optional (default: None)
589624
Fixes part of the matching. Also known as a "seed" [2]_.
590625

0 commit comments

Comments
 (0)