L irddlZddlZddlZddlZddlmZmZddlm Z ddl m Z ddl Z ddgZ ddZd Zd Zd Z dd Zd ZddZ ddZy)N)linear_sum_assignmentOptimizeResult)_check_unknown_options)check_random_statefaq2optc|i}|j}ttd}||vrtd|d|dt |j dd||||fi|}|S)a Approximates solution to the quadratic assignment problem and the graph matching problem. Quadratic assignment solves problems of the following form: .. math:: \min_P & \ {\ \text{trace}(A^T P B P^T)}\\ \mbox{s.t. } & {P \ \epsilon \ \mathcal{P}}\\ where :math:`\mathcal{P}` is the set of all permutation matrices, and :math:`A` and :math:`B` are square matrices. Graph matching tries to *maximize* the same objective function. This algorithm can be thought of as finding the alignment of the nodes of two graphs that minimizes the number of induced edge disagreements, or, in the case of weighted graphs, the sum of squared edge weight differences. Note that the quadratic assignment problem is NP-hard. The results given here are approximations and are not guaranteed to be optimal. Parameters ---------- A : 2-D array, square The square matrix :math:`A` in the objective function above. B : 2-D array, square The square matrix :math:`B` in the objective function above. method : str in {'faq', '2opt'} (default: 'faq') The algorithm used to solve the problem. :ref:`'faq' ` (default) and :ref:`'2opt' ` are available. options : dict, optional A dictionary of solver options. All solvers support the following: maximize : bool (default: False) Maximizes the objective function if ``True``. partial_match : 2-D array of integers, optional (default: None) Fixes part of the matching. Also known as a "seed" [2]_. Each row of `partial_match` specifies a pair of matched nodes: node ``partial_match[i, 0]`` of `A` is matched to node ``partial_match[i, 1]`` of `B`. The array has shape ``(m, 2)``, where ``m`` is not greater than the number of nodes, :math:`n`. rng : `numpy.random.Generator`, optional Pseudorandom number generator state. When `rng` is None, a new `numpy.random.Generator` is created using entropy from the operating system. Types other than `numpy.random.Generator` are passed to `numpy.random.default_rng` to instantiate a ``Generator``. .. versionchanged:: 1.15.0 As part of the `SPEC-007 `_ transition from use of `numpy.random.RandomState` to `numpy.random.Generator` is occurring. Supplying `np.random.RandomState` to this function will now emit a `DeprecationWarning`. In SciPy 1.17 its use will raise an exception. In addition relying on global state using `np.random.seed` will emit a `FutureWarning`. In SciPy 1.17 the global random number generator will no longer be used. Use of an int-like seed will raise a `FutureWarning`, in SciPy 1.17 it will be normalized via `np.random.default_rng` rather than `np.random.RandomState`. For method-specific options, see :func:`show_options('quadratic_assignment') `. Returns ------- res : OptimizeResult `OptimizeResult` containing the following fields. col_ind : 1-D array Column indices corresponding to the best permutation found of the nodes of `B`. fun : float The objective value of the solution. nit : int The number of iterations performed during optimization. Notes ----- The default method :ref:`'faq' ` uses the Fast Approximate QAP algorithm [1]_; it typically offers the best combination of speed and accuracy. Method :ref:`'2opt' ` can be computationally expensive, but may be a useful alternative, or it can be used to refine the solution returned by another method. References ---------- .. [1] J.T. Vogelstein, J.M. Conroy, V. Lyzinski, L.J. Podrazik, S.G. Kratzer, E.T. Harley, D.E. Fishkind, R.J. Vogelstein, and C.E. Priebe, "Fast approximate quadratic programming for graph matching," PLOS one, vol. 10, no. 4, p. e0121002, 2015, :doi:`10.1371/journal.pone.0121002` .. [2] D. Fishkind, S. Adali, H. Patsolic, L. Meng, D. Singh, V. Lyzinski, C. Priebe, "Seeded graph matching", Pattern Recognit. 87 (2019): 203-215, :doi:`10.1016/j.patcog.2018.09.014` .. [3] "2-opt," Wikipedia. https://en.wikipedia.org/wiki/2-opt Examples -------- >>> import numpy as np >>> from scipy.optimize import quadratic_assignment >>> rng = np.random.default_rng() >>> A = np.array([[0, 80, 150, 170], [80, 0, 130, 100], ... [150, 130, 0, 120], [170, 100, 120, 0]]) >>> B = np.array([[0, 5, 2, 7], [0, 0, 3, 8], ... [0, 0, 0, 3], [0, 0, 0, 0]]) >>> res = quadratic_assignment(A, B, options={'rng': rng}) >>> print(res) fun: 3260 col_ind: [0 3 2 1] nit: 9 The see the relationship between the returned ``col_ind`` and ``fun``, use ``col_ind`` to form the best permutation matrix found, then evaluate the objective function :math:`f(P) = trace(A^T P B P^T )`. >>> perm = res['col_ind'] >>> P = np.eye(len(A), dtype=int)[perm] >>> fun = np.trace(A.T @ P @ B @ P.T) >>> print(fun) 3260 Alternatively, to avoid constructing the permutation matrix explicitly, directly permute the rows and columns of the distance matrix. >>> fun = np.trace(A.T @ B[perm][:, perm]) >>> print(fun) 3260 Although not guaranteed in general, ``quadratic_assignment`` happens to have found the globally optimal solution. >>> from itertools import permutations >>> perm_opt, fun_opt = None, np.inf >>> for perm in permutations([0, 1, 2, 3]): ... perm = np.array(perm) ... fun = np.trace(A.T @ B[perm][:, perm]) ... if fun < fun_opt: ... fun_opt, perm_opt = fun, perm >>> print(np.array_equal(perm_opt, res['col_ind'])) True Here is an example for which the default method, :ref:`'faq' `, does not find the global optimum. >>> A = np.array([[0, 5, 8, 6], [5, 0, 5, 1], ... [8, 5, 0, 2], [6, 1, 2, 0]]) >>> B = np.array([[0, 1, 8, 4], [1, 0, 5, 2], ... [8, 5, 0, 5], [4, 2, 5, 0]]) >>> res = quadratic_assignment(A, B, options={'rng': rng}) >>> print(res) fun: 178 col_ind: [1 0 3 2] nit: 13 If accuracy is important, consider using :ref:`'2opt' ` to refine the solution. >>> guess = np.array([np.arange(len(A)), res.col_ind]).T >>> res = quadratic_assignment(A, B, method="2opt", ... options = {'rng': rng, 'partial_guess': guess}) >>> print(res) fun: 176 col_ind: [1 2 3 0] nit: 17 N)rr zmethod z must be in .rng)lower_quadratic_assignment_faq_quadratic_assignment_2opt ValueError_spec007_transitionget)ABmethodoptionsmethodsress Y/mnt/ssd/data/python-lab/Trading/venv/lib/python3.12/site-packages/scipy/optimize/_qap.pyquadratic_assignmentrsxl \\^F/13G W76(,wiqABB E401 '&/!Q *' *C Jct|tjjrt j dt d||tjurTtjjjjjt j dtdt|tjtjzrt j dtdyy)NzlUse of `RandomState` with `quadratic_assignment` is deprecated and will result in an exception in SciPy 1.17) stacklevelz~The NumPy global RNG was seeded by calling `np.random.seed`. From SciPy 1.17, this function will no longer use the global RNG.zThe behavior when the rng option is an integer is changing: the value will be normalized using np.random.default_rng beginning in SciPy 1.17, and the resulting Generator will be used to generate random numbers.) isinstancenprandom RandomStatewarningswarnDeprecationWarningmtrand_rand_bit_generator _seed_seq FutureWarningnumbersIntegralinteger)r s rrrs#ryy,,-  =   ryy( II   " " 1 1 ; ; C  Q   #w''"**45  T   6rcFtj|||dd|fzSN)r sum)rrperms r _calc_scorer2s# 66!agag&& ''rctj|}tj|}|!tjgggj}tj|j t }d}|j d|j dk7rd}n<|j d|j dk7rd}n|jdk7s|jdk7rd}n|j |j k7rd}n|j d|j dkDrd}n|j ddk7rd }n|jdk7rd }n|dkjrd }n}|t|k\jrd }n^tt|dddft|dddfk(r.tt|dddft|dddfk(sd }| t||||fS)Nrrz`A` must be squarez`B` must be squarerz,`A` and `B` must have exactly two dimensionsz*`A` and `B` matrices must be of equal sizez>`partial_match` can have only as many seeds as there are nodesz%`partial_match` must have two columnsz0`partial_match` must have exactly two dimensionsz2`partial_match` must contain only positive indicesz9`partial_match` entries must be less than number of nodesz-`partial_match` column entries must be unique) r atleast_2darrayTastypeintshapendimanylensetr)rr partial_matchmsgs r_common_input_validationr@s aA aA"b*,, MM-077| z |kr|0} n|0} t5| d\}#}1tj.tj@| |1| zf}2tjB| tD}3||2|3|<t%|||3}|3|!d}t'|S)aBSolve the quadratic assignment problem (approximately). This function solves the Quadratic Assignment Problem (QAP) and the Graph Matching Problem (GMP) using the Fast Approximate QAP Algorithm (FAQ) [1]_. Quadratic assignment solves problems of the following form: .. math:: \min_P & \ {\ \text{trace}(A^T P B P^T)}\\ \mbox{s.t. } & {P \ \epsilon \ \mathcal{P}}\\ where :math:`\mathcal{P}` is the set of all permutation matrices, and :math:`A` and :math:`B` are square matrices. Graph matching tries to *maximize* the same objective function. This algorithm can be thought of as finding the alignment of the nodes of two graphs that minimizes the number of induced edge disagreements, or, in the case of weighted graphs, the sum of squared edge weight differences. Note that the quadratic assignment problem is NP-hard. The results given here are approximations and are not guaranteed to be optimal. Parameters ---------- A : 2-D array, square The square matrix :math:`A` in the objective function above. B : 2-D array, square The square matrix :math:`B` in the objective function above. method : str in {'faq', '2opt'} (default: 'faq') The algorithm used to solve the problem. This is the method-specific documentation for 'faq'. :ref:`'2opt' ` is also available. Options ------- maximize : bool (default: False) Maximizes the objective function if ``True``. partial_match : 2-D array of integers, optional (default: None) Fixes part of the matching. Also known as a "seed" [2]_. Each row of `partial_match` specifies a pair of matched nodes: node ``partial_match[i, 0]`` of `A` is matched to node ``partial_match[i, 1]`` of `B`. The array has shape ``(m, 2)``, where ``m`` is not greater than the number of nodes, :math:`n`. rng : {None, int, `numpy.random.Generator`}, optional Pseudorandom number generator state. See `quadratic_assignment` for details. P0 : 2-D array, "barycenter", or "randomized" (default: "barycenter") Initial position. Must be a doubly-stochastic matrix [3]_. If the initial position is an array, it must be a doubly stochastic matrix of size :math:`m' \times m'` where :math:`m' = n - m`. If ``"barycenter"`` (default), the initial position is the barycenter of the Birkhoff polytope (the space of doubly stochastic matrices). This is a :math:`m' \times m'` matrix with all entries equal to :math:`1 / m'`. If ``"randomized"`` the initial search position is :math:`P_0 = (J + K) / 2`, where :math:`J` is the barycenter and :math:`K` is a random doubly stochastic matrix. shuffle_input : bool (default: False) Set to `True` to resolve degenerate gradients randomly. For non-degenerate gradients this option has no effect. maxiter : int, positive (default: 30) Integer specifying the max number of Frank-Wolfe iterations performed. tol : float (default: 0.03) Tolerance for termination. Frank-Wolfe iteration terminates when :math:`\frac{||P_{i}-P_{i+1}||_F}{\sqrt{m'}} \leq tol`, where :math:`i` is the iteration number. Returns ------- res : OptimizeResult `OptimizeResult` containing the following fields. col_ind : 1-D array Column indices corresponding to the best permutation found of the nodes of `B`. fun : float The objective value of the solution. nit : int The number of Frank-Wolfe iterations performed. Notes ----- The algorithm may be sensitive to the initial permutation matrix (or search "position") due to the possibility of several local minima within the feasible region. A barycenter initialization is more likely to result in a better solution than a single random initialization. However, calling ``quadratic_assignment`` several times with different random initializations may result in a better optimum at the cost of longer total execution time. Examples -------- As mentioned above, a barycenter initialization often results in a better solution than a single random initialization. >>> from scipy.optimize import quadratic_assignment >>> import numpy as np >>> rng = np.random.default_rng() >>> n = 15 >>> A = rng.random((n, n)) >>> B = rng.random((n, n)) >>> options = {"rng": rng} >>> res = quadratic_assignment(A, B, options=options) # FAQ is default method >>> print(res.fun) 47.797048706380636 # may vary >>> options = {"rng": rng, "P0": "randomized"} # use randomized initialization >>> res = quadratic_assignment(A, B, options=options) >>> print(res.fun) 47.37287069769966 # may vary However, consider running from several randomized initializations and keeping the best result. >>> res = min([quadratic_assignment(A, B, options=options) ... for i in range(30)], key=lambda x: x.fun) >>> print(res.fun) 46.55974835248574 # may vary The '2-opt' method can be used to attempt to refine the results. >>> options = {"partial_guess": np.array([np.arange(n), res.col_ind]).T, "rng": rng} >>> res = quadratic_assignment(A, B, method="2opt", options=options) >>> print(res.fun) 46.55974835248574 # may vary References ---------- .. [1] J.T. Vogelstein, J.M. Conroy, V. Lyzinski, L.J. Podrazik, S.G. Kratzer, E.T. Harley, D.E. Fishkind, R.J. Vogelstein, and C.E. Priebe, "Fast approximate quadratic programming for graph matching," PLOS one, vol. 10, no. 4, p. e0121002, 2015, :doi:`10.1371/journal.pone.0121002` .. [2] D. Fishkind, S. Adali, H. Patsolic, L. Meng, D. Singh, V. Lyzinski, C. Priebe, "Seeded graph matching", Pattern Recognit. 87 (2019): 203-215, :doi:`10.1016/j.patcog.2018.09.014` .. [3] "Doubly stochastic Matrix," Wikipedia. https://en.wikipedia.org/wiki/Doubly_stochastic_matrix N> barycenter randomizedzInvalid 'P0' parameter stringrz$'maxiter' must be a positive integerz'tol' must be a positive floatz1`P0` matrix must have shape m' x m', where m'=n-maxisrz%`P0` matrix must be doubly stochasticrBrC)sizercol_indfunnit)maximizeTdtype)#roperatorindexr@rstrrrr<r r4r9r;allcloser0ones_doubly_stochasticuniformr2r setdiff1drange permutation concatenate _split_matrixr6reyeargminlinalgnormsqrtarangezerosr8)4rrrLr>r P0 shuffle_inputmaxitertolunknown_optionsr?nn_seedsn_unseedJKscorerobj_func_scalar nonseed_B nonseed_Aperm_Aperm_BA11A12A21A22B11B12B21B22 const_sumPn_itergrad_fp_colsQRb21b12AR22BR22b22ab22babalphaP_i1colr1unshuffled_perms4 rrrst?+nnW%G31aGAq- C"cr)EE- A4 . o S !C AA- G7{H b#  ]]2  88(+ +ECAvllnBKKr0BA$F[[!3Q79C ?S/ ! |  WWh) *X 5 |  GGXx( )H 4 s{{80D{E F!eq[ AvAAq-1"56'1-eAFc""O U1X}QT':;IOOI.  U1X}QT':;I ^^]1a40)< =F ^^]1a40)< =F'qyF';WECc3&qyF';WECc3cee ceeck)I A719%"sQw.S@'(C4 FF8 T " Ec S %%'cee suu$))+uuqyQSSysuuT{"'')d4j %%' VVd]   ! #I t # _ q Q1"ac(%7a%7B!HEIIq1q5/"9:;EqyAI?* 99>>!d( #bggh&7 7# =A  E"L#1t 4FAs >>299W-sW}= >Dhhq,O$TlOF 1o .E%eF CC # rcj|d|||d}}|ddd|f|dd|df|ddd|f|dd|dffSr/)Xrgupperr s rrZrZ$sRRa5!AB%5E BQB<q!"uuQU|U1ab5\ AArcd}d|jdz }d||zz }|}t|D]}tj|jddz |kj r` is also available. Options ------- maximize : bool (default: False) Maximizes the objective function if ``True``. rng : {None, int, `numpy.random.Generator`}, optional Pseudorandom number generator state. See `quadratic_assignment` for details. partial_match : 2-D array of integers, optional (default: None) Fixes part of the matching. Also known as a "seed" [2]_. Each row of `partial_match` specifies a pair of matched nodes: node ``partial_match[i, 0]`` of `A` is matched to node ``partial_match[i, 1]`` of `B`. The array has shape ``(m, 2)``, where ``m`` is not greater than the number of nodes, :math:`n`. .. note:: `partial_match` must be sorted by the first column. partial_guess : 2-D array of integers, optional (default: None) A guess for the matching between the two matrices. Unlike `partial_match`, `partial_guess` does not fix the indices; they are still free to be optimized. Each row of `partial_guess` specifies a pair of matched nodes: node ``partial_guess[i, 0]`` of `A` is matched to node ``partial_guess[i, 1]`` of `B`. The array has shape ``(m, 2)``, where ``m`` is not greater than the number of nodes, :math:`n`. .. note:: `partial_guess` must be sorted by the first column. Returns ------- res : OptimizeResult `OptimizeResult` containing the following fields. col_ind : 1-D array Column indices corresponding to the best permutation found of the nodes of `B`. fun : float The objective value of the solution. nit : int The number of iterations performed during optimization. Notes ----- This is a greedy algorithm that works similarly to bubble sort: beginning with an initial permutation, it iteratively swaps pairs of indices to improve the objective function until no such improvements are possible. References ---------- .. [1] "2-opt," Wikipedia. https://en.wikipedia.org/wiki/2-opt .. [2] D. Fishkind, S. Adali, H. Patsolic, L. Meng, D. Singh, V. Lyzinski, C. Priebe, "Seeded graph matching", Pattern Recognit. 87 (2019): 203-215, https://doi.org/10.1016/j.patcog.2018.09.014 rNrrGz@`partial_guess` can have only as many entries as there are nodesrz%`partial_guess` must have two columnsz0`partial_guess` must have exactly two dimensionsz2`partial_guess` must contain only positive indicesz9`partial_guess` entries must be less than number of nodesz-`partial_guess` column entries must be uniquerMTF)rrr@r<r9r2rr r5r6r4r7r8r:r;r=rrFraboolrXr`rOgtlt itertoolscombinations_with_replacement)rrrLr r> partial_guessrfNrlrr? fixed_rows guess_rows guess_cols fixed_colsr1rgcgrfcf random_rows random_cols best_scorei_freebetterr|doneijs rrrBsD?+ S !C21aGAq- AAAv$$Q'1,Aq-1"56'1-eAFc"""b*,, MM-077rsn5-/ %vAH 4( "LGKLN"&K\B 0:>-1-1vr