1
1
import numpy as np
2
2
import operator
3
+ import warnings
4
+ import numbers
3
5
from . import (linear_sum_assignment , OptimizeResult )
4
6
from ._optimize import _check_unknown_options
5
7
8
10
9
11
QUADRATIC_ASSIGNMENT_METHODS = ['faq' , '2opt' ]
10
12
13
+
11
14
def quadratic_assignment (A , B , method = "faq" , options = None ):
12
15
r"""
13
16
Approximates solution to the quadratic assignment problem and
@@ -60,13 +63,24 @@ def quadratic_assignment(A, B, method="faq", options=None):
60
63
``partial_match[i, 1]`` of `B`. The array has shape ``(m, 2)``,
61
64
where ``m`` is not greater than the number of nodes, :math:`n`.
62
65
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`.
70
84
71
85
For method-specific options, see
72
86
:func:`show_options('quadratic_assignment') <show_options>`.
@@ -112,11 +126,12 @@ def quadratic_assignment(A, B, method="faq", options=None):
112
126
--------
113
127
>>> import numpy as np
114
128
>>> from scipy.optimize import quadratic_assignment
129
+ >>> rng = np.random.default_rng()
115
130
>>> A = np.array([[0, 80, 150, 170], [80, 0, 130, 100],
116
131
... [150, 130, 0, 120], [170, 100, 120, 0]])
117
132
>>> B = np.array([[0, 5, 2, 7], [0, 0, 3, 8],
118
133
... [0, 0, 0, 3], [0, 0, 0, 0]])
119
- >>> res = quadratic_assignment(A, B)
134
+ >>> res = quadratic_assignment(A, B, options={'rng': rng} )
120
135
>>> print(res)
121
136
fun: 3260
122
137
col_ind: [0 3 2 1]
@@ -159,7 +174,7 @@ def quadratic_assignment(A, B, method="faq", options=None):
159
174
... [8, 5, 0, 2], [6, 1, 2, 0]])
160
175
>>> B = np.array([[0, 1, 8, 4], [1, 0, 5, 2],
161
176
... [8, 5, 0, 5], [4, 2, 5, 0]])
162
- >>> res = quadratic_assignment(A, B)
177
+ >>> res = quadratic_assignment(A, B, options={'rng': rng} )
163
178
>>> print(res)
164
179
fun: 178
165
180
col_ind: [1 0 3 2]
@@ -170,7 +185,7 @@ def quadratic_assignment(A, B, method="faq", options=None):
170
185
171
186
>>> guess = np.array([np.arange(len(A)), res.col_ind]).T
172
187
>>> res = quadratic_assignment(A, B, method="2opt",
173
- ... options = {'partial_guess': guess})
188
+ ... options = {'rng': rng, 'partial_guess': guess})
174
189
>>> print(res)
175
190
fun: 176
176
191
col_ind: [1 2 3 0]
@@ -186,10 +201,38 @@ def quadratic_assignment(A, B, method="faq", options=None):
186
201
"2opt" : _quadratic_assignment_2opt }
187
202
if method not in methods :
188
203
raise ValueError (f"method { method } must be in { methods } ." )
204
+
205
+ _spec007_transition (options .get ("rng" , None ))
189
206
res = methods [method ](A , B , ** options )
190
207
return res
191
208
192
209
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
+
193
236
def _calc_score (A , B , perm ):
194
237
# equivalent to objective function but avoids matmul
195
238
return np .sum (A * B [perm ][:, perm ])
@@ -284,13 +327,8 @@ def _quadratic_assignment_faq(A, B,
284
327
``partial_match[i, 1]`` of `B`. The array has shape ``(m, 2)``, where
285
328
``m`` is not greater than the number of nodes, :math:`n`.
286
329
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.
294
332
P0 : 2-D array, "barycenter", or "randomized" (default: "barycenter")
295
333
Initial position. Must be a doubly-stochastic matrix [3]_.
296
334
@@ -312,7 +350,7 @@ def _quadratic_assignment_faq(A, B,
312
350
Integer specifying the max number of Frank-Wolfe iterations performed.
313
351
tol : float (default: 0.03)
314
352
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`,
316
354
where :math:`i` is the iteration number.
317
355
318
356
Returns
@@ -343,34 +381,36 @@ def _quadratic_assignment_faq(A, B,
343
381
As mentioned above, a barycenter initialization often results in a better
344
382
solution than a single random initialization.
345
383
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()
348
387
>>> n = 15
349
388
>>> A = rng.random((n, n))
350
389
>>> 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
352
392
>>> print(res.fun)
353
- 46.871483385480545 # may vary
393
+ 47.797048706380636 # may vary
354
394
355
- >>> options = {"P0": "randomized"} # use randomized initialization
395
+ >>> options = {"rng": rng, " P0": "randomized"} # use randomized initialization
356
396
>>> res = quadratic_assignment(A, B, options=options)
357
397
>>> print(res.fun)
358
- 47.224831071310625 # may vary
398
+ 47.37287069769966 # may vary
359
399
360
400
However, consider running from several randomized initializations and
361
401
keeping the best result.
362
402
363
403
>>> res = min([quadratic_assignment(A, B, options=options)
364
404
... for i in range(30)], key=lambda x: x.fun)
365
405
>>> print(res.fun)
366
- 46.671852533681516 # may vary
406
+ 46.55974835248574 # may vary
367
407
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.
369
409
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 }
371
411
>>> res = quadratic_assignment(A, B, method="2opt", options=options)
372
412
>>> print(res.fun)
373
- 46.47160735721583 # may vary
413
+ 46.55974835248574 # may vary
374
414
375
415
References
376
416
----------
@@ -578,13 +618,8 @@ def _quadratic_assignment_2opt(A, B, maximize=False, rng=None,
578
618
-------
579
619
maximize : bool (default: False)
580
620
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.
588
623
partial_match : 2-D array of integers, optional (default: None)
589
624
Fixes part of the matching. Also known as a "seed" [2]_.
590
625
0 commit comments