def rotational( a: np.ndarray, b: np.ndarray, pad: bool = True, translate: bool = False, scale: bool = False, unpad_col: bool = False, unpad_row: bool = False, check_finite: bool = True, weight: Optional[np.ndarray] = None, lapack_driver: str = "gesvd", ) -> ProcrustesResult: r"""Perform rotational Procrustes. Given a matrix :math:`\mathbf{A}_{m \times n}` and a reference matrix :math:`\mathbf{B}_{m \times n}`, find the rotational transformation matrix :math:`\mathbf{R}_{n \times n}` that makes :math:`\mathbf{A}` as close as possible to :math:`\mathbf{B}`. In other words, .. math:: \underbrace{\min}_{\left\{\mathbf{R} \left| {\mathbf{R}^{-1} = {\mathbf{R}}^\dagger \atop \left| \mathbf{R} \right| = 1} \right. \right\}} \|\mathbf{A}\mathbf{R} - \mathbf{B}\|_{F}^2 This Procrustes method requires the :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices to have the same shape, which is gauranteed with the default ``pad`` argument for any given :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices. In preparing the :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices, the (optional) order of operations is: **1)** unpad zero rows/columns, **2)** translate the matrices to the origin, **3)** weight entries of :math:`\mathbf{A}`, **4)** scale the matrices to have unit norm, **5)** pad matrices with zero rows/columns so they have the same shape. Parameters ---------- a : ndarray The 2D-array :math:`\mathbf{A}` which is going to be transformed. b : ndarray The 2D-array :math:`\mathbf{B}` representing the reference matrix. pad : bool, optional Add zero rows (at the bottom) and/or columns (to the right-hand side) of matrices :math:`\mathbf{A}` and :math:`\mathbf{B}` so that they have the same shape. translate : bool, optional If True, both arrays are centered at origin (columns of the arrays will have mean zero). scale : bool, optional If True, both arrays are normalized with respect to the Frobenius norm, i.e., :math:`\text{Tr}\left[\mathbf{A}^\dagger\mathbf{A}\right] = 1` and :math:`\text{Tr}\left[\mathbf{B}^\dagger\mathbf{B}\right] = 1`. unpad_col : bool, optional If True, zero columns (with values less than 1.0e-8) on the right-hand side of the intial :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices are removed. unpad_row : bool, optional If True, zero rows (with values less than 1.0e-8) at the bottom of the intial :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices are removed. check_finite : bool, optional If True, convert the input to an array, checking for NaNs or Infs. weight : ndarray, optional The 1D-array representing the weights of each row of :math:`\mathbf{A}`. This defines the elements of the diagonal matrix :math:`\mathbf{W}` that is multiplied by :math:`\mathbf{A}` matrix, i.e., :math:`\mathbf{A} \rightarrow \mathbf{WA}`. lapack_driver : {'gesvd', 'gesdd'}, optional Whether to use the more efficient divide-and-conquer approach ('gesdd') or the more robust general rectangular approach ('gesvd') to compute the singular-value decomposition with `scipy.linalg.svd`. Returns ------- res : ProcrustesResult The Procrustes result represented as a class:`utils.ProcrustesResult` object. Notes ----- The optimal rotational matrix is obtained by, .. math:: \mathbf{R}_{\text{opt}} = \arg \underbrace{\min}_{\left\{\mathbf{R} \left| {\mathbf{R}^{-1} = {\mathbf{R}}^\dagger \atop \left| \mathbf{R} \right| = 1} \right. \right\}} \|\mathbf{A}\mathbf{R} - \mathbf{B}\|_{F}^2 = \arg \underbrace{\max}_{\left\{\mathbf{R} \left| {\mathbf{R}^{-1} = {\mathbf{R}}^\dagger \atop \left| \mathbf{R} \right| = 1} \right. \right\}} \text{Tr}\left[\mathbf{R}^\dagger {\mathbf{A}}^\dagger \mathbf{B} \right] The solution is obtained by taking the singular value decomposition (SVD) of the :math:`\mathbf{A}^\dagger \mathbf{B}` matrix, .. math:: \mathbf{A}^\dagger \mathbf{B} &= \tilde{\mathbf{U}} \tilde{\mathbf{\Sigma}} \tilde{\mathbf{V}}^{\dagger} \\ \mathbf{R}_{\text{opt}} &= \tilde{\mathbf{U}} \tilde{\mathbf{S}} \tilde{\mathbf{V}}^{\dagger} where :math:`\tilde{\mathbf{S}}_{n \times m}` is almost an identity matrix, .. math:: \tilde{\mathbf{S}}_{m \times n} \equiv \begin{bmatrix} 1 & 0 & \cdots & 0 & 0 \\ 0 & 1 & \ddots & \vdots &0 \\ 0 & \ddots &\ddots & 0 &\vdots \\ \vdots&0 & 0 & 1 &0 \\ 0 & 0 & 0 \cdots &0 &\operatorname{sgn} \left(\left|\mathbf{U}\mathbf{V}^\dagger\right|\right) \end{bmatrix} in which the smallest singular value is replaced by .. math:: \operatorname{sgn} \left(\left|\tilde{\mathbf{U}} \tilde{\mathbf{V}}^\dagger\right|\right) = \begin{cases} +1 \qquad \left|\tilde{\mathbf{U}} \tilde{\mathbf{V}}^\dagger\right| \geq 0 \\ -1 \qquad \left|\tilde{\mathbf{U}} \tilde{\mathbf{V}}^\dagger\right| < 0 \end{cases} Examples -------- >>> import numpy as np >>> array_a = np.array([[1.5, 7.4], [8.5, 4.5]]) >>> array_b = np.array([[6.29325035, 4.17193001, 0., 0,], ... [9.19238816, -2.82842712, 0., 0.], ... [0., 0., 0., 0.]]) >>> res = rotational(array_a,array_b,translate=False,scale=False) >>> res.t # rotational transformation array([[ 0.70710678, -0.70710678], [ 0.70710678, 0.70710678]]) >>> res.error # one-sided Procrustes error 1.483808210011695e-17 """ # check inputs new_a, new_b = setup_input_arrays( a, b, unpad_col, unpad_row, pad, translate, scale, check_finite, weight, ) if new_a.shape != new_b.shape: raise ValueError( f"Shape of A and B does not match: {new_a.shape} != {new_b.shape} " "Check pad, unpad_col, and unpad_row arguments.") # compute SVD of A.T * B u, _, vt = scipy.linalg.svd(np.dot(new_a.T, new_b), lapack_driver=lapack_driver) # construct S: an identity matrix with the smallest singular value replaced by sgn(|U*V^t|) s = np.eye(new_a.shape[1]) s[-1, -1] = np.sign(np.linalg.det(np.dot(u, vt))) # compute optimal rotational transformation r_opt = np.dot(np.dot(u, s), vt) # compute one-sided error error = compute_error(new_a, new_b, r_opt) return ProcrustesResult(error=error, new_a=new_a, new_b=new_b, t=r_opt, s=None)
def permutation_2sided( a, b, single=True, method="kopt", guess_p1=None, guess_p2=None, pad=False, unpad_col=False, unpad_row=False, translate=False, scale=False, check_finite=True, options=None, weight=None, lapack_driver="gesvd", ): r"""Perform two-sided permutation Procrustes. Parameters ---------- a : ndarray The 2d-array :math:`\mathbf{A}` which is going to be transformed. b : ndarray The 2d-array :math:`\mathbf{B}` representing the reference matrix. single : bool, optional If `True`, the single-transformation Procrustes is performed to obtain :math:`\mathbf{P}`. If `False`, the two-transformations Procrustes is performed to obtain :math:`\mathbf{P}_1` and :math:`\mathbf{P}_2`. method : str, optional The method to solve for permutation matrices. For `single=False`, these include "flip-flop" and "k-opt" methods. For `single=True`, these include "approx-normal1", "approx-normal2", "approx-umeyama", "approx-umeyama-svd", "k-opt", "soft-assign", and "nmf". guess_p1 : np.ndarray, optional Guess for :math:`\mathbf{P}_1` matrix given as a 2D-array. This is only required for the two-transformations case specified by setting `single=False`. guess_p2 : np.ndarray, optional Guess for :math:`\mathbf{P}_2` matrix given as a 2D-array. pad : bool, optional Add zero rows (at the bottom) and/or columns (to the right-hand side) of matrices :math:`\mathbf{A}` and :math:`\mathbf{B}` so that they have the same shape. unpad_col : bool, optional If True, zero columns (with values less than 1.0e-8) on the right-hand side are removed. unpad_row : bool, optional If True, zero rows (with values less than 1.0e-8) at the bottom are removed. translate : bool, optional If True, both arrays are centered at origin (columns of the arrays will have mean zero). scale : bool, optional If True, both arrays are normalized with respect to the Frobenius norm, i.e., :math:`\text{Tr}\left[\mathbf{A}^\dagger\mathbf{A}\right] = 1` and :math:`\text{Tr}\left[\mathbf{B}^\dagger\mathbf{B}\right] = 1`. check_finite : bool, optional If True, convert the input to an array, checking for NaNs or Infs. options : dict, optional A dictionary of method options. weight : ndarray, optional The 1D-array representing the weights of each row of :math:`\mathbf{A}`. This defines the elements of the diagonal matrix :math:`\mathbf{W}` that is multiplied by :math:`\mathbf{A}` matrix, i.e., :math:`\mathbf{A} \rightarrow \mathbf{WA}`. lapack_driver : {'gesvd', 'gesdd'}, optional Whether to use the more efficient divide-and-conquer approach ('gesdd') or the more robust general rectangular approach ('gesvd') to compute the singular-value decomposition with `scipy.linalg.svd`. Returns ------- res : ProcrustesResult The Procrustes result represented as a class:`utils.ProcrustesResult` object. Notes ----- Given matrix :math:`\mathbf{A}_{n \times n}` and a reference :math:`\mathbf{B}_{n \times n}`, find a permutation of rows/columns of :math:`\mathbf{A}_{n \times n}` that makes it as close as possible to :math:`\mathbf{B}_{n \times n}`. I.e., .. math:: &\underbrace{\text{min}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \|\mathbf{P}^\dagger \mathbf{A} \mathbf{P} - \mathbf{B}\|_{F}^2\\ = &\underbrace{\text{min}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\left(\mathbf{P}^\dagger\mathbf{A}\mathbf{P} - \mathbf{B} \right)^\dagger \left(\mathbf{P}^\dagger\mathbf{A}\mathbf{P} - \mathbf{B} \right)\right] \\ = &\underbrace{\text{max}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger\mathbf{A}^\dagger\mathbf{P}\mathbf{B} \right]\\ Here, :math:`\mathbf{P}_{n \times n}` is the permutation matrix. Given an intial guess, the best local minimum can be obtained by the iterative procedure, .. math:: p_{ij}^{(n + 1)} = p_{ij}^{(n)} \sqrt{ \frac{2\left[\mathbf{T}^{(n)}\right]_{ij}}{\left[ \mathbf{P}^{(n)} \left( \left(\mathbf{P}^{(n)}\right)^T \mathbf{T} + \left( \left(\mathbf{P}^{(n)}\right)^T \mathbf{T} \right)^T \right) \right]_{ij}} } where, .. math:: \mathbf{T}^{(n)} = \mathbf{A} \mathbf{P}^{(n)} \mathbf{B} Using an initial guess, the iteration can stops when the change in :math:`d` is below the specified threshold, .. math:: d = \text{Tr} \left[\left(\mathbf{P}^{(n+1)} -\mathbf{P}^{(n)} \right)^T \left(\mathbf{P}^{(n+1)} -\mathbf{P}^{(n)} \right)\right] The outcome of the iterative procedure :math:`\mathbf{P}^{(\infty)}` is not a permutation matrix. So, the closest permutation can be found by setting ``refinement=True``. This uses :class:`procrustes.permutation.PermutationProcrustes` to find the closest permutation; that is, .. math:: \underbrace{\text{min}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \|\mathbf{P} - \mathbf{P}^{(\infty)}\|_{F}^2 = \underbrace{\text{max}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger\mathbf{P}^{(\infty)} \right] The answer to this problem is a heuristic solution for the matrix-matching problem that seems to be relatively accurate. **Initial Guess:** Two possible initial guesses are inferred from the Umeyama procedure. One can find either the closest permutation matrix to :math:`\mathbf{U}_\text{Umeyama}` or to :math:`\mathbf{U}_\text{Umeyama}^\text{approx.}`. Considering the :class:`procrustes.permutation.PermutationProcrustes`, the resulting permutation matrix can be specified as initial guess through ``guess=umeyama`` and ``guess=umeyama_approx``, which solves: .. math:: \underbrace{\text{max}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger\mathbf{U}_\text{Umeyama} \right] \\ \underbrace{\text{max}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger\mathbf{U}_\text{Umeyama}^\text{approx.} \right] Another choice is to start by solving a normal permutation Procrustes problem. In other words, write new matrices, :math:`\mathbf{A}^0` and :math:`\mathbf{B}^0`, with columns like, .. math:: \begin{bmatrix} a_{ii} \\ p \cdot \text{sgn}\left( a_{ij_\text{max}} \right) \underbrace{\text{max}}_{1 \le j \le n} \left(\left|a_{ij}\right|\right)\\ p^2 \cdot \text{sgn}\left( a_{ij_{\text{max}-1}} \right) \underbrace{\text{max}-1}_{1 \le j \le n} \left(\left|a_{ij}\right|\right)\\ \vdots \end{bmatrix} Here, :math:`\text{max}-1` denotes the second-largest absolute value of elements, :math:`\text{max}-2` is the third-largest abosule value of elements, etc. The matrices :math:`\mathbf{A}^0` and :math:`\mathbf{B}^0` have the diagonal elements of :math:`\mathbf{A}` and :math:`\mathbf{B}` in the first row, and below the first row has the largest off-diagonal element in row :math:`i`, the second-largest off-diagonal element, etc. The elements are weighted by a factor :math:`0 < p < 1`, so that the smaller elements are considered less important for matching. The matrices can be truncated after a few terms; for example, after the size of elements falls below some threshold. A reasonable choice would be to stop after :math:`\lfloor \frac{-2\ln 10}{\ln p} +1\rfloor` rows; this ensures that the size of the elements in the last row is less than 1% of those in the first off-diagonal row. There are obviously many different ways to construct the matrices :math:`\mathbf{A}^0` and :math:`\mathbf{B}^0`. Another, even better, method would be to try to encode not only what the off-diagonal elements are, but which element in the matrix they correspond to. One could do that by not only listing the diagonal elements, but also listing the associated off-diagonal element. I.e., the columns of :math:`\mathbf{A}^0` and :math:`\mathbf{B}^0` would be, .. math:: \begin{bmatrix} a_{ii} \\ p \cdot a_{j_\text{max} j_\text{max}} \\ p \cdot \text{sgn}\left( a_{ij_\text{max}} \right) \underbrace{\text{max}}_{1 \le j \le n} \left(\left|a_{ij}\right|\right)\\ p^2 \cdot a_{j_{\text{max}-1} j_{\text{max}-1}} \\ p^2 \cdot \text{sgn}\left( a_{ij_{\text{max}-1}} \right) \underbrace{\text{max}-1}_{1 \le j \le n} \left(\left|a_{ij}\right|\right)\\ \vdots \end{bmatrix} In this case, you would stop the procedure after :math:`m = \left\lfloor {\frac{{ - 4\ln 10}}{{\ln p}} + 1} \right \rfloor` rows. Then one uses the :class:`procrustes.permutation.PermutationProcrustes` to match the constructed matrices :math:`\mathbf{A}^0` and :math:`\mathbf{B}^0` instead of :math:`\mathbf{A}` and :math:`\mathbf{B}`. I.e., .. math:: \underbrace{\text{max}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger \left(\mathbf{A^0}^\dagger\mathbf{B^0}\right)\right] Please note that the "umeyama_approx" might give inaccurate permutation matrix. More specificity, this is a approximated Umeyama method. One example we can give is that when we compute the permutation matrix that transforms :math:`A` to :math:`B`, the "umeyama_approx" method can not give the exact permutation transformation matrix while "umeyama", "normal1" and "normal2" do. .. math:: A = \begin{bmatrix} 4 & 5 & -3 & 3 \\ 5 & 7 & 3 & -5 \\ -3 & 3 & 2 & 2 \\ 3 & -5 & 2 & 5 \\ \end{bmatrix} \\ B = \begin{bmatrix} 73 & 100 & 73 & -62 \\ 100 & 208 & -116 & 154 \\ 73 & -116 & 154 & 100 \\ -62 & 154 & 100 & 127 \\ \end{bmatrix} \\ References ---------- [1] C. Ding, T. Li and M. I. Jordan, "Nonnegative Matrix Factorization for Combinatorial Optimization: Spectral Clustering, Graph Matching, and Clique Finding," 2008 Eighth IEEE International Conference on Data Mining, Pisa, Italy, 2008, pp. 183-192, doi: 10.1109/ICDM.2008.130. [2] Papadimitriou, Pythagoras. "Parallel solution of SVD-related problems, with applications." PhD diss., University of Manchester, 1993. [3] S. Umeyama. An eigendecomposition approach toweighted graph matching problems. IEEE Trans. on Pattern Analysis and Machine Intelligence, 10:695 –703, 1988. """ # check single argument if not isinstance(single, bool): raise TypeError( f"Argument single is not a boolean! Given type={type(single)}") # check inputs new_a, new_b = setup_input_arrays(a, b, unpad_col, unpad_row, pad, translate, scale, check_finite, weight) # check that A & B are square in case of single transformation if single and new_a.shape[0] != new_a.shape[1]: raise ValueError( f"For single={single}, matrix A should be square but A.shape={new_a.shape}" "Check pad, unpad_col, and unpad_row arguments.") if single and new_b.shape[0] != new_b.shape[1]: raise ValueError( f"For single={single}, matrix B should be square but B.shape={new_b.shape}" "Check pad, unpad_col, and unpad_row arguments.") # print a statement if user-specified guess is not used if method.startswith("approx") and guess_p1 is not None: print( f"Method={method} does not use an initial guess, so guess_p1 is ignored!" ) if method.startswith("approx") and guess_p2 is not None: print( f"Method={method} does not use an initial guess, so guess_p2 is ignored!" ) # get the number of rows & columns of matrix A m, n = new_a.shape # assign & check initial guess for P1 if single and guess_p1 is not None: raise ValueError( f"For single={single}, P1 is transpose of P2, so guess_p1 should be None." ) if not single: if guess_p1 is None: guess_p1 = np.eye(m) if guess_p1.shape != (m, m): raise ValueError( f"Argument guess_p1 should be either None or a ({m}, {m}) array." ) # assign & check initial guess for P2 if guess_p2 is None: guess_p2 = np.eye(n) if guess_p2.shape != (n, n): raise ValueError( f"Argument guess_p2 should be either None or a ({n}, {n}) array.") # check options dictionary & assign default keys defaults = {"tol": 1.0e-8, "maxiter": 500, "k": 3} if options is not None: if not isinstance(options, dict): raise ValueError( f"Argument options should be a dictionary. Given type={type(options)}" ) if not all(k in defaults.keys() for k in options.keys()): raise ValueError( f"Argument options should only have {defaults.keys()} keys. " f"Given options contains {options.keys()} keys!") # update defaults dictionary to use the specified options defaults.update(options) # 2-sided permutation Procrustes with two transformations # ------------------------------------------------------- if not single: if method == "flip-flop": # compute permutations using flip-flop algorithm perm1, perm2, error = _permutation_2sided_2trans_flipflop( new_a, new_b, defaults["tol"], defaults["maxiter"], guess_p1, guess_p2) elif method == "k-opt": # compute permutations using k-opt heuristic search fun_error = lambda p1, p2: compute_error(new_a, new_b, p2, p1.T) perm1, perm2, error = kopt_heuristic_double(fun_error, p1=guess_p1, p2=guess_p2, k=defaults["k"]) else: raise ValueError( f"Method={method} not supported for single={single} transformation!" ) return ProcrustesResult(error=error, new_a=new_a, new_b=new_b, t=perm2, s=perm1) # 2-sided permutation Procrustes with one transformation # ------------------------------------------------------ # The (un)directed iterative procedure for finding the permutation matrix takes the square # root of the matrix entries, which can result in complex numbers if the entries are # negative. To avoid this, all matrix entries are shifted (by the smallest amount) to be # positive. This causes no change to the objective function, as it's a constant value # being added to all entries of a and b. shift = 1.0e-6 if np.min(new_a) < 0 or np.min(new_b) < 0: shift += abs(min(np.min(new_a), np.min(new_b))) # shift is a float, so even if new_a or new_b are ints, the positive matrices are floats # default shift is not zero to avoid division by zero later in the algorithm pos_a = new_a + shift pos_b = new_b + shift if method == "approx-normal1": tmp_a = _approx_permutation_2sided_1trans_normal1(a) tmp_b = _approx_permutation_2sided_1trans_normal1(b) perm = permutation(tmp_a, tmp_b).t elif method == "approx-normal2": tmp_a = _approx_permutation_2sided_1trans_normal2(a) tmp_b = _approx_permutation_2sided_1trans_normal2(b) perm = permutation(tmp_a, tmp_b).t elif method == "approx-umeyama": perm = _approx_permutation_2sided_1trans_umeyama(pos_a, pos_b) elif method == "approx-umeyama-svd": perm = _approx_permutation_2sided_1trans_umeyama_svd( a, b, lapack_driver) elif method == "k-opt": fun_error = lambda p: compute_error(pos_a, pos_b, p, p.T) perm, error = kopt_heuristic_single(fun_error, p0=guess_p2, k=defaults["k"]) elif method == "soft-assign": raise NotImplementedError elif method == "nmf": # check whether A & B are symmetric (within a relative & absolute tolerance) is_pos_a_symmetric = np.allclose(pos_a, pos_a.T, rtol=1.0e-05, atol=1.0e-08) is_pos_b_symmetric = np.allclose(pos_b, pos_b.T, rtol=1.0e-05, atol=1.0e-08) if is_pos_a_symmetric and is_pos_b_symmetric: # undirected graph matching problem (iterative procedure) perm = _permutation_2sided_1trans_undirected( pos_a, pos_b, guess_p2, defaults['tol'], defaults['maxiter']) else: # directed graph matching problem (iterative procedure) perm = _permutation_2sided_1trans_directed(pos_a, pos_b, guess_p2, defaults['tol'], defaults['maxiter']) else: raise ValueError( f"Method={method} not supported for single={single} transformation!" ) # some of the methods for 2-sided-1-transformation permutation procrustes does not produce a # permutation matrix. So, their output is treated like a guess, and the closest permutation # matrix is found using 1-sided permutation procrustes (where A=I & B=perm) # Even though this step is not needed for ALL methods (e.g. k-opt, normal1, & normal2), to # make the code simple, this step is performed for all methods as its cost is negligible. perm = permutation( np.eye(perm.shape[0]), perm, translate=False, scale=False, unpad_col=False, unpad_row=False, check_finite=True, ).t # compute error error = compute_error(new_a, new_b, t=perm, s=perm.T) return ProcrustesResult(error=error, new_a=new_a, new_b=new_b, t=perm, s=perm.T)
def permutation( a, b, pad=True, translate=False, scale=False, unpad_col=False, unpad_row=False, check_finite=True, weight=None, ): r"""Perform one-sided permutation Procrustes. Given matrix :math:`\mathbf{A}_{m \times n}` and a reference matrix :math:`\mathbf{B}_{m \times n}`, find the permutation transformation matrix :math:`\mathbf{P}_{n \times n}` that makes :math:`\mathbf{AP}` as close as possible to :math:`\mathbf{B}`. In other words, .. math:: \underbrace{\text{min}}_{\left\{\mathbf{P} \left| {[\mathbf{P}]_{ij} \in \{0, 1\} \atop \sum_{i=1}^n [\mathbf{P}]_{ij} = \sum_{j=1}^n [\mathbf{P}]_{ij} = 1} \right. \right\}} \|\mathbf{A} \mathbf{P} - \mathbf{B}\|_{F}^2 This Procrustes method requires the :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices to have the same shape, which is guaranteed with the default ``pad=True`` argument for any given :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices. In preparing the :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices, the (optional) order of operations is: **1)** unpad zero rows/columns, **2)** translate the matrices to the origin, **3)** weight entries of :math:`\mathbf{A}`, **4)** scale the matrices to have unit norm, **5)** pad matrices with zero rows/columns so they have the same shape. Parameters ---------- a : ndarray The 2d-array :math:`\mathbf{A}` which is going to be transformed. b : ndarray The 2d-array :math:`\mathbf{B}` representing the reference matrix. pad : bool, optional Add zero rows (at the bottom) and/or columns (to the right-hand side) of matrices :math:`\mathbf{A}` and :math:`\mathbf{B}` so that they have the same shape. translate : bool, optional If True, both arrays are centered at origin (columns of the arrays will have mean zero). scale : bool, optional If True, both arrays are normalized with respect to the Frobenius norm, i.e., :math:`\text{Tr}\left[\mathbf{A}^\dagger\mathbf{A}\right] = 1` and :math:`\text{Tr}\left[\mathbf{B}^\dagger\mathbf{B}\right] = 1`. unpad_col : bool, optional If True, zero columns (with values less than 1.0e-8) on the right-hand side are removed. unpad_row : bool, optional If True, zero rows (with values less than 1.0e-8) at the bottom are removed. check_finite : bool, optional If True, convert the input to an array, checking for NaNs or Infs. weight : ndarray, optional The 1D-array representing the weights of each row of :math:`\mathbf{A}`. This defines the elements of the diagonal matrix :math:`\mathbf{W}` that is multiplied by :math:`\mathbf{A}` matrix, i.e., :math:`\mathbf{A} \rightarrow \mathbf{WA}`. Returns ------- res : ProcrustesResult The Procrustes result represented as a class:`utils.ProcrustesResult` object. Notes ----- The optimal :math:`n \times n` permutation matrix is obtained by, .. math:: \mathbf{P}^{\text{opt}} = \arg \underbrace{\text{min}}_{\left\{\mathbf{P} \left| {[\mathbf{P}]_{ij} \in \{0, 1\} \atop \sum_{i=1}^n [\mathbf{P}]_{ij} = \sum_{j=1}^n [\mathbf{P}]_{ij} = 1} \right. \right\}} \|\mathbf{A} \mathbf{P} - \mathbf{B}\|_{F}^2 = \underbrace{\text{max}}_{\left\{\mathbf{P} \left| {[\mathbf{P}]_{ij} \in \{0, 1\} \atop \sum_{i=1}^n [\mathbf{P}]_{ij} = \sum_{j=1}^n [\mathbf{P}]_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger\mathbf{A}^\dagger\mathbf{B} \right] The solution is found by relaxing the problem into a linear programming problem. The solution to a linear programming problem is always at the boundary of the allowed region. So, .. math:: \underbrace{\text{max}}_{\left\{\mathbf{P} \left| {[\mathbf{P}]_{ij} \in \{0, 1\} \atop \sum_{i=1}^n [\mathbf{P}]_{ij} = \sum_{j=1}^n [\mathbf{P}]_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger\mathbf{A}^\dagger\mathbf{B} \right] = \underbrace{\text{max}}_{\left\{\mathbf{P} \left| {[\mathbf{P}]_{ij} \geq 0 \atop \sum_{i=1}^n [\mathbf{P}]_{ij} = \sum_{j=1}^n [\mathbf{P}]_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger\left(\mathbf{A}^\dagger\mathbf{B}\right) \right] This is a matching problem and can be solved by the Hungarian algorithm. The cost matrix is defined as :math:`\mathbf{A}^\dagger\mathbf{B}` and the `scipy.optimize.linear_sum_assignment` is used to solve for the permutation that maximizes the linear sum assignment problem. """ # check inputs new_a, new_b = setup_input_arrays( a, b, unpad_col, unpad_row, pad, translate, scale, check_finite, weight, ) # if number of rows is less than column, the arrays are made square if (new_a.shape[0] < new_a.shape[1]) or (new_b.shape[0] < new_b.shape[1]): new_a, new_b = _zero_padding(new_a, new_b, "square") # compute cost matrix C = A.T B c = np.dot(new_a.T, new_b) # compute permutation matrix using Hungarian algorithm p = _compute_permutation_hungarian(c) # compute one-sided permutation error error = compute_error(new_a, new_b, p) return ProcrustesResult(new_a=new_a, new_b=new_b, t=p, error=error)
def orthogonal( a: np.ndarray, b: np.ndarray, pad: bool = True, translate: bool = False, scale: bool = False, unpad_col: bool = False, unpad_row: bool = False, check_finite: bool = True, weight: Optional[np.ndarray] = None, lapack_driver: str = "gesvd", ) -> ProcrustesResult: r"""Perform orthogonal Procrustes. Given a matrix :math:`\mathbf{A}_{m \times n}` and a reference matrix :math:`\mathbf{B}_{m \times n}`, find the orthogonal transformation matrix :math:`\mathbf{Q}_{n \times n}` that makes :math:`\mathbf{AQ}` as close as possible to :math:`\mathbf{B}`. In other words, .. math:: \underbrace{\min}_{\left\{\mathbf{Q} | \mathbf{Q}^{-1} = {\mathbf{Q}}^\dagger \right\}} \|\mathbf{A}\mathbf{Q} - \mathbf{B}\|_{F}^2 This Procrustes method requires the :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices to have the same shape, which is gauranteed with the default ``pad`` argument for any given :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices. In preparing the :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices, the (optional) order of operations is: **1)** unpad zero rows/columns, **2)** translate the matrices to the origin, **3)** weight entries of :math:`\mathbf{A}`, **4)** scale the matrices to have unit norm, **5)** pad matrices with zero rows/columns so they have the same shape. Parameters ---------- a : ndarray The 2D-array :math:`\mathbf{A}` which is going to be transformed. b : ndarray The 2D-array :math:`\mathbf{B}` representing the reference matrix. pad : bool, optional Add zero rows (at the bottom) and/or columns (to the right-hand side) of matrices :math:`\mathbf{A}` and :math:`\mathbf{B}` so that they have the same shape. translate : bool, optional If True, both arrays are centered at origin (columns of the arrays will have mean zero). scale : bool, optional If True, both arrays are normalized with respect to the Frobenius norm, i.e., :math:`\text{Tr}\left[\mathbf{A}^\dagger\mathbf{A}\right] = 1` and :math:`\text{Tr}\left[\mathbf{B}^\dagger\mathbf{B}\right] = 1`. unpad_col : bool, optional If True, zero columns (with values less than 1.0e-8) on the right-hand side of the intial :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices are removed. unpad_row : bool, optional If True, zero rows (with values less than 1.0e-8) at the bottom of the intial :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices are removed. check_finite : bool, optional If True, convert the input to an array, checking for NaNs or Infs. weight : ndarray, optional The 1D-array representing the weights of each row of :math:`\mathbf{A}`. This defines the elements of the diagonal matrix :math:`\mathbf{W}` that is multiplied by :math:`\mathbf{A}` matrix, i.e., :math:`\mathbf{A} \rightarrow \mathbf{WA}`. lapack_driver : {'gesvd', 'gesdd'}, optional Whether to use the more efficient divide-and-conquer approach ('gesdd') or the more robust general rectangular approach ('gesvd') to compute the singular-value decomposition with `scipy.linalg.svd`. Returns ------- res : ProcrustesResult The Procrustes result represented as a class:`utils.ProcrustesResult` object. Notes ----- The optimal orthogonal matrix is obtained by, .. math:: \mathbf{Q}^{\text{opt}} = \arg \underbrace{\min}_{\left\{\mathbf{Q} \left| {\mathbf{Q}^{-1} = {\mathbf{Q}}^\dagger} \right. \right\}} \|\mathbf{A}\mathbf{Q} - \mathbf{B}\|_{F}^2 = \arg \underbrace{\max}_{\left\{\mathbf{Q} \left| {\mathbf{Q}^{-1} = {\mathbf{Q}}^\dagger} \right. \right\}} \text{Tr}\left[\mathbf{Q^\dagger}\mathbf{A^\dagger}\mathbf{B}\right] The solution is obtained using the singular value decomposition (SVD) of the :math:`\mathbf{A}^\dagger \mathbf{B}` matrix, .. math:: \mathbf{A}^\dagger \mathbf{B} &= \tilde{\mathbf{U}} \tilde{\mathbf{\Sigma}} \tilde{\mathbf{V}}^{\dagger} \\ \mathbf{Q}^{\text{opt}} &= \tilde{\mathbf{U}} \tilde{\mathbf{V}}^{\dagger} The singular values are always listed in decreasing order, with the smallest singular value in the bottom-right-hand corner of :math:`\tilde{\mathbf{\Sigma}}`. Examples -------- >>> import numpy as np >>> from scipy.stats import ortho_group >>> from procrustes import orthogonal >>> a = np.random.rand(5, 3) # random input matrix >>> q = ortho_group.rvs(3) # random orthogonal transformation >>> b = np.dot(a, q) + np.random.rand(1, 3) # random target matrix >>> result = orthogonal(a, b, translate=True, scale=False) >>> print(result.error) # error (should be zero) >>> print(result.t) # transformation matrix (same as q) >>> print(result.new_a) # translated array a >>> print(result.new_b) # translated array b """ # check inputs new_a, new_b = setup_input_arrays( a, b, unpad_col, unpad_row, pad, translate, scale, check_finite, weight, ) if new_a.shape != new_b.shape: raise ValueError( f"Shape of A and B does not match: {new_a.shape} != {new_b.shape} " "Check pad, unpad_col, and unpad_row arguments." ) # calculate SVD of A.T * B u, _, vt = scipy.linalg.svd(np.dot(new_a.T, new_b), lapack_driver=lapack_driver) # compute optimal orthogonal transformation u_opt = np.dot(u, vt) # compute one-sided error error = compute_error(new_a, new_b, u_opt) return ProcrustesResult(error=error, new_a=new_a, new_b=new_b, t=u_opt, s=None)
def softassign( a: np.ndarray, b: np.ndarray, pad: bool = True, translate: bool = False, scale: bool = False, unpad_col: bool = False, unpad_row: bool = False, check_finite: bool = True, weight: Optional[np.ndarray] = None, iteration_soft: int = 50, iteration_sink: int = 200, beta_r: float = 1.10, beta_f: float = 1.0e5, epsilon: float = 0.05, epsilon_soft: float = 1.0e-3, epsilon_sink: float = 1.0e-3, k: float = 0.15, gamma_scaler: float = 1.01, n_stop: int = 3, adapted: bool = True, beta_0: Optional[float] = None, m_guess: Optional[float] = None, iteration_anneal: Optional[int] = None, kopt: bool = False, kopt_k: int = 3, ) -> ProcrustesResult: r""" Find the transformation matrix for 2-sided permutation Procrustes with softassign algorithm. Parameters ---------- a : ndarray The 2D-array :math:`\mathbf{A}_{m \times n}` which is going to be transformed. b : ndarray The 2D-array :math:`\mathbf{B}_{m \times n}` representing the reference. pad : bool, optional Add zero rows (at the bottom) and/or columns (to the right-hand side) of matrices :math:`\mathbf{A}` and :math:`\mathbf{B}` so that they have the same shape. translate : bool, optional If True, both arrays are centered at origin (columns of the arrays will have mean zero). scale : bool, optional If True, both arrays are normalized with respect to the Frobenius norm, i.e., :math:`\text{Tr}\left[\mathbf{A}^\dagger\mathbf{A}\right] = 1` and :math:`\text{Tr}\left[\mathbf{B}^\dagger\mathbf{B}\right] = 1`. unpad_col : bool, optional If True, zero columns (with values less than 1.0e-8) on the right-hand side of the intial :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices are removed. unpad_row : bool, optional If True, zero rows (with values less than 1.0e-8) at the bottom of the intial :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices are removed. check_finite : bool, optional If true, convert the input to an array, checking for NaNs or Infs. Default=True. weight : ndarray, optional The 1D-array representing the weights of each row of :math:`\mathbf{A}`. This defines the elements of the diagonal matrix :math:`\mathbf{W}` that is multiplied by :math:`\mathbf{A}` matrix, i.e., :math:`\mathbf{A} \rightarrow \mathbf{WA}`. iteration_soft : int, optional Number of iterations for softassign loop. iteration_sink : int, optional Number of iterations for Sinkhorn loop. beta_r : float, optional Annealing rate which should greater than 1. beta_f : float, optional The final inverse temperature. epsilon : float, optional The tolerance value for annealing loop. epsilon_soft : float, optional The tolerance value used for softassign. epsilon_sink : float, optional The tolerance value used for Sinkhorn loop. If adapted version is used, it will use the adapted tolerance value for Sinkhorn instead. k : float, optional This parameter controls how much tighter the coverage threshold for the interior loop should be than the coverage threshold for the loops outside. It has be be within the integral :math:`(0,1)`. gamma_scaler : float, optional This parameter ensures the quadratic cost function including self-amplification positive define. n_stop : int, optional Number of running steps after the calculation converges in the relaxation procedure. adapted : bool, optional If adapted, this function will use the tighter covergence threshold for the interior loops. beta_0 : float, optional Initial inverse temperature. beta_f : float, optional Final inverse temperature. m_guess : ndarray, optional The initial guess of the doubly-stochastic matrix. iteration_anneal : int, optional Number of iterations for annealing loop. kopt : bool, optional If True, the k_opt heuristic search will be performed. kopt_k : int, optional Defines the oder of k-opt heuristic local search. For example, kopt_k=3 leads to a local search of 3 items and kopt_k=2 only searches for two items locally. weight : ndarray, optional The weighting matrix. Returns ------- res : ProcrustesResult The Procrustes result represented as a class:`utils.ProcrustesResult` object. Notes ----- Quadratic assignment problem (QAP) has played a very special but fundamental role in combinatorial optimization problems. The problem can be defined as a optimization problem to minimize the cost to assign a set of facilities to a set of locations. The cost is a function of the flow between the facilities and the geographical distances among various facilities. The objective function (also named loss function in machine learning) is defined as .. math:: E_{qap}(M, \mu, \nu) = - \frac{1}{2}\Sigma_{aibj}C_{ai;bj}M_{ai}M_{bj} + \Sigma_{a}{\mu}_a (\Sigma_i M_{ai} -1) \\ + \Sigma_i {\nu}_i (\Sigma_i M_{ai} -1) - \frac{\gamma}{2}\Sigma_{ai} {M_{ai}}^2 + \frac{1}{\beta} \Sigma_{ai} M_{ai}\log{M_{ai}} where :math:`C_{ai,bj}` is the benefit matrix, :math:`M` is the desired :math:`N \times N` permutation matrix. :math:`E` is the energy function which comes along with a self-amplification term with `\gamma`, two Lagrange parameters :math:`\mu` and :math:`\nu` for constrained optimization and :math:`M_{ai} \log{M_{ai}}` servers as a barrier function which ensures positivity of :math:`M_{ai}`. The inverse temperature :math:`\beta` is a deterministic annealing control parameter. Examples -------- >>> import numpy as np >>> array_a = np.array([[4, 5, 3, 3], [5, 7, 3, 5], ... [3, 3, 2, 2], [3, 5, 2, 5]]) # define a random matrix >>> perm = np.array([[0., 0., 1., 0.], [1., 0., 0., 0.], ... [0., 0., 0., 1.], [0., 1., 0., 0.]]) # define b by permuting array_a >>> b = np.dot(perm.T, np.dot(a, perm)) >>> new_a, new_b, M_ai, error = softassign(a,b,unpad_col=False,unpad_row=False) >>> M_ai # the permutation matrix array([[0., 0., 1., 0.], [1., 0., 0., 0.], [0., 0., 0., 1.], [0., 1., 0., 0.]]) >>> error # the error 0.0 """ # pylint: disable-msg=too-many-arguments # pylint: disable-msg=too-many-branches # todo: add linear_cost_func with default value 0 # Check beta_r if beta_r <= 1: raise ValueError("Argument beta_r cannot be less than 1.") new_a, new_b = setup_input_arrays(a, b, unpad_col, unpad_row, pad, translate, scale, check_finite, weight) # Check that A & B are square and that they match each other. if new_a.shape[0] != new_a.shape[1]: raise ValueError(f"Matrix A should be square but A.shape={new_a.shape}" "Check pad, unpad_col, and unpad_row arguments.") if new_b.shape[0] != new_b.shape[1]: raise ValueError(f"Matrix B should be square but B.shape={new_b.shape}" "Check pad, unpad_col, and unpad_row arguments.") if new_a.shape != new_b.shape: raise ValueError(f"New matrix A {new_a.shape} should match the new" f" matrix B shape {new_b.shape}.") # Initialization # Compute the benefit matrix array_c = np.kron(new_a, new_b) # Get the shape of A (B and the permutation matrix as well) row_num = new_a.shape[0] c_tensor = array_c.reshape(row_num, row_num, row_num, row_num) # Compute the beta_0 gamma = _compute_gamma(array_c, row_num, gamma_scaler) if beta_0 is None: c_gamma = array_c + gamma * (np.identity(row_num * row_num)) eival_gamma = np.amax(np.abs(np.linalg.eigvalsh(c_gamma))) beta_0 = gamma_scaler * max(1.0e-10, eival_gamma / row_num) beta_0 = 1 / beta_0 else: beta_0 *= row_num beta = beta_0 # We will use iteration_anneal if provided even if the final inverse temperature is specified # iteration_anneal is not None, beta_f can be None or not if iteration_anneal is not None: beta_f = beta_0 * np.power(beta_r, iteration_anneal) * row_num # iteration_anneal is None and beta_f is not None elif iteration_anneal is None and beta_f is not None: beta_f *= row_num # Both iteration_anneal and beta_f are None else: raise ValueError( "We must specify at least one of iteration_anneal and beta_f and " "specify only one is recommended.") # Initialization of m_ai # check shape of m_guess if m_guess is not None: if np.any(m_guess < 0): raise ValueError( "The initial guess of permutation matrix cannot contain any negative values." ) if m_guess.shape[0] == row_num and m_guess.shape[1] == row_num: array_m = m_guess else: warnings.warn( f"The shape of m_guess does not match ({row_num}, {row_num})." "Use random initial guess instead.") array_m = np.abs( np.random.normal(loc=1.0, scale=0.1, size=(row_num, row_num))) else: # m_relax_old = 1 / N + np.random.rand(N, N) array_m = np.abs( np.random.normal(loc=1.0, scale=0.1, size=(row_num, row_num))) array_m[array_m < 0] = 0 array_m = array_m / row_num nochange = 0 if adapted: epsilon_sink = epsilon_soft * k while beta < beta_f: # relaxation m_old_beta = deepcopy(array_m) # softassign loop for _ in np.arange(iteration_soft): m_old_soft = deepcopy(array_m) # Compute Z in relaxation step # C_gamma_tensor = C_gamma.reshape(N, N, N, N) # Z = -np.einsum('ijkl,jl->ik', C_gamma_tensor, M) # Z -= linear_cost_func array_z = np.einsum("aibj,bj->ai", c_tensor, array_m) array_z += gamma * array_m # soft_assign array_m = np.exp(beta * array_z) # Sinkhorn loop for _ in np.arange(iteration_sink): # Row normalization array_m = array_m / array_m.sum(axis=1, keepdims=1) # Column normalization array_m = array_m / array_m.sum(axis=0, keepdims=1) # Compute the delata_M_sink if np.amax(np.abs(array_m.sum(axis=1, keepdims=1) - 1)) < epsilon_sink: array_m = array_m / array_m.sum(axis=1, keepdims=1) break change_soft = np.amax(np.abs(array_m - m_old_soft)) # pylint: disable-msg=no-else-break if change_soft < epsilon_soft: break else: if adapted: epsilon_sink = change_soft * k else: continue change_annealing = np.amax(np.abs(array_m - m_old_beta)) if change_annealing < epsilon: nochange += 1 if nochange > n_stop: break else: nochange = 0 beta *= beta_r if adapted: epsilon_soft = change_soft * k epsilon_sink = epsilon_soft * k # Compute the error array_m = permutation(np.eye(array_m.shape[0]), array_m)["t"] # k-opt heuristic if kopt: fun_error = lambda p: compute_error(new_a, new_b, p, p.T) array_m, error = kopt_heuristic_single(fun_error, p0=array_m, k=kopt_k) else: error = compute_error(new_a, new_b, array_m, array_m.T) return ProcrustesResult(error=error, new_a=new_a, new_b=new_b, t=array_m, s=None)
def symmetric(a, b, pad=True, translate=False, scale=False, unpad_col=False, unpad_row=False, check_finite=True, weight=None, lapack_driver="gesvd"): r"""Perform symmetric Procrustes. Given a matrix :math:`\mathbf{A}_{m \times n}` and a reference matrix :math:`\mathbf{B}_{m \times n}` with :math:`m \geqslant n`, find the symmetrix transformation matrix :math:`\mathbf{X}_{n \times n}` that makes :math:`\mathbf{AX}` as close as possible to :math:`\mathbf{B}`. In other words, .. math:: \underbrace{\text{min}}_{\left\{\mathbf{X} \left| \mathbf{X} = \mathbf{X}^\dagger \right. \right\}} \|\mathbf{A} \mathbf{X} - \mathbf{B}\|_{F}^2 This Procrustes method requires the :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices to have the same shape with :math:`m \geqslant n`, which is guaranteed with the default ``pad`` argument for any given :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices. In preparing the :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices, the (optional) order of operations is: **1)** unpad zero rows/columns, **2)** translate the matrices to the origin, **3)** weight entries of :math:`\mathbf{A}`, **4)** scale the matrices to have unit norm, **5)** pad matrices with zero rows/columns so they have the same shape. Parameters ---------- a : ndarray The 2D-array :math:`\mathbf{A}` which is going to be transformed. b : ndarray The 2D-array :math:`\mathbf{B}` representing the reference matrix. pad : bool, optional Add zero rows (at the bottom) and/or columns (to the right-hand side) of matrices :math:`\mathbf{A}` and :math:`\mathbf{B}` so that they have the same shape. translate : bool, optional If True, both arrays are centered at origin (columns of the arrays will have mean zero). scale : bool, optional If True, both arrays are normalized with respect to the Frobenius norm, i.e., :math:`\text{Tr}\left[\mathbf{A}^\dagger\mathbf{A}\right] = 1` and :math:`\text{Tr}\left[\mathbf{B}^\dagger\mathbf{B}\right] = 1`. unpad_col : bool, optional If True, zero columns (with values less than 1.0e-8) on the right-hand side of the intial :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices are removed. unpad_row : bool, optional If True, zero rows (with values less than 1.0e-8) at the bottom of the intial :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices are removed. check_finite : bool, optional If True, convert the input to an array, checking for NaNs or Infs. weight : ndarray, optional The 1D-array representing the weights of each row of :math:`\mathbf{A}`. This defines the elements of the diagonal matrix :math:`\mathbf{W}` that is multiplied by :math:`\mathbf{A}` matrix, i.e., :math:`\mathbf{A} \rightarrow \mathbf{WA}`. lapack_driver : {'gesvd', 'gesdd'}, optional Whether to use the more efficient divide-and-conquer approach ('gesdd') or the more robust general rectangular approach ('gesvd') to compute the singular-value decomposition with `scipy.linalg.svd`. Returns ------- res : ProcrustesResult The Procrustes result represented as a class:`utils.ProcrustesResult` object. Notes ----- The optimal symmetrix matrix is obtained by, .. math:: \mathbf{X}_{\text{opt}} = \arg \underbrace{\text{min}}_{\left\{\mathbf{X} \left| \mathbf{X} = \mathbf{X}^\dagger \right. \right\}} \|\mathbf{A} \mathbf{X} - \mathbf{B}\|_{F}^2 = \underbrace{\text{min}}_{\left\{\mathbf{X} \left| \mathbf{X} = \mathbf{X}^\dagger \right. \right\}} \text{Tr}\left[\left(\mathbf{A}\mathbf{X} - \mathbf{B} \right)^\dagger \left(\mathbf{A}\mathbf{X} - \mathbf{B} \right)\right] Considering the singular value decomposition of :math:`\mathbf{A}`, .. math:: \mathbf{A}_{m \times n} = \mathbf{U}_{m \times m} \mathbf{\Sigma}_{m \times n} \mathbf{V}_{n \times n}^\dagger where :math:`\mathbf{\Sigma}_{m \times n}` is a rectangular diagonal matrix with non-negative singular values :math:`\sigma_i = [\mathbf{\Sigma}]_{ii}` listed in descending order, define .. math:: \mathbf{C}_{m \times n} = \mathbf{U}_{m \times m}^\dagger \mathbf{B}_{m \times n} \mathbf{V}_{n \times n} with elements denoted by :math:`c_{ij}`. Then we compute the symmetric matrix :math:`\mathbf{Y}_{n \times n}` with .. math:: [\mathbf{Y}]_{ij} = \begin{cases} 0 && i \text{ and } j > \text{rank} \left(\mathbf{A}\right) \\ \frac{\sigma_i c_{ij} + \sigma_j c_{ji}}{\sigma_i^2 + \sigma_j^2} && \text{otherwise} \end{cases} It is worth noting that the first part of this definition only applies in the unusual case where :math:`\mathbf{A}` has rank less than :math:`n`. The :math:`\mathbf{X}_\text{opt}` is given by .. math:: \mathbf{X}_\text{opt} = \mathbf{V Y V}^{\dagger} Examples -------- >>> import numpy as np >>> a = np.array([[5., 2., 8.], ... [2., 2., 3.], ... [1., 5., 6.], ... [7., 3., 2.]]) >>> b = np.array([[ 52284.5, 209138. , 470560.5], ... [ 22788.5, 91154. , 205096.5], ... [ 46139.5, 184558. , 415255.5], ... [ 22788.5, 91154. , 205096.5]]) >>> res = symmetric(a, b, pad=True, translate=True, scale=True) >>> res.t # symmetric transformation array array([[0.0166352 , 0.06654081, 0.14971682], [0.06654081, 0.26616324, 0.59886729], [0.14971682, 0.59886729, 1.34745141]]) >>> res.error # error 4.483083428047388e-31 """ # check inputs new_a, new_b = setup_input_arrays( a, b, unpad_col, unpad_row, pad, translate, scale, check_finite, weight, ) # if number of rows is less than column, the arrays are made square if (new_a.shape[0] < new_a.shape[1]) or (new_b.shape[0] < new_b.shape[1]): new_a, new_b = _zero_padding(new_a, new_b, "square") # if new_a.shape[0] < new_a.shape[1]: # raise ValueError(f"Shape of A {new_a.shape}=(m, n) needs to satisfy m >= n.") # # if new_b.shape[0] < new_b.shape[1]: # raise ValueError(f"Shape of B {new_b.shape}=(m, n) needs to satisfy m >= n.") # compute SVD of A & matrix C u, s, vt = scipy.linalg.svd(new_a, lapack_driver=lapack_driver) c = np.dot(np.dot(u.T, new_b), vt.T) # compute intermediate matrix Y n = new_a.shape[1] y = np.zeros((n, n)) for i in range(n): for j in range(n): if s[i]**2 + s[j]**2 != 0: y[i, j] = (s[i] * c[i, j] + s[j] * c[j, i]) / (s[i]**2 + s[j]**2) # compute optimum symmetric transformation matrix X x = np.dot(np.dot(vt.T, y), vt) error = compute_error(new_a, new_b, x) return ProcrustesResult(error=error, new_a=new_a, new_b=new_b, t=x, s=None)
def orthogonal_2sided( a: np.ndarray, b: np.ndarray, single: bool = True, pad: bool = True, translate: bool = False, scale: bool = False, unpad_col: bool = False, unpad_row: bool = False, check_finite: bool = True, weight: Optional[np.ndarray] = None, lapack_driver: str = "gesvd", ) -> ProcrustesResult: r"""Perform two-sided orthogonal Procrustes with one- or two-transformations. **Two Transformations:** Given a matrix :math:`\mathbf{A}_{m \times n}` and a reference matrix :math:`\mathbf{B}_{m \times n}`, find two :math:`n \times n` orthogonal transformation matrices :math:`\mathbf{Q}_1^\dagger` and :math:`\mathbf{Q}_2` that makes :math:`\mathbf{Q}_1^\dagger\mathbf{A}\mathbf{Q}_2` as close as possible to :math:`\mathbf{B}`. In other words, .. math:: \underbrace{\text{min}}_{\left\{ {\mathbf{Q}_1 \atop \mathbf{Q}_2} \left| {\mathbf{Q}_1^{-1} = \mathbf{Q}_1^\dagger \atop \mathbf{Q}_2^{-1} = \mathbf{Q}_2^\dagger} \right. \right\}} \|\mathbf{Q}_1^\dagger \mathbf{A} \mathbf{Q}_2 - \mathbf{B}\|_{F}^2 **Single Transformations:** Given a **symmetric** matrix :math:`\mathbf{A}_{n \times n}` and a reference :math:`\mathbf{B}_{n \times n}`, find one orthogonal transformation matrix :math:`\mathbf{Q}_{n \times n}` that makes :math:`\mathbf{A}` as close as possible to :math:`\mathbf{B}`. In other words, .. math:: \underbrace{\min}_{\left\{\mathbf{Q} | \mathbf{Q}^{-1} = {\mathbf{Q}}^\dagger \right\}} \|\mathbf{Q}^\dagger\mathbf{A}\mathbf{Q} - \mathbf{B}\|_{F}^2 This Procrustes method requires the :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices to have the same shape, which is gauranteed with the default ``pad`` argument for any given :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices. In preparing the :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices, the (optional) order of operations is: **1)** unpad zero rows/columns, **2)** translate the matrices to the origin, **3)** weight entries of :math:`\mathbf{A}`, **4)** scale the matrices to have unit norm, **5)** pad matrices with zero rows/columns so they have the same shape. Parameters ---------- a : ndarray The 2D-array :math:`\mathbf{A}` which is going to be transformed. b : ndarray The 2D-array :math:`\mathbf{B}` representing the reference matrix. single : bool, optional If True, single transformation is used (i.e., :math:`\mathbf{Q}_1=\mathbf{Q}_2=\mathbf{Q}`), otherwise, two transformations are used. pad : bool, optional Add zero rows (at the bottom) and/or columns (to the right-hand side) of matrices :math:`\mathbf{A}` and :math:`\mathbf{B}` so that they have the same shape. translate : bool, optional If True, both arrays are centered at origin (columns of the arrays will have mean zero). scale : bool, optional If True, both arrays are normalized with respect to the Frobenius norm, i.e., :math:`\text{Tr}\left[\mathbf{A}^\dagger\mathbf{A}\right] = 1` and :math:`\text{Tr}\left[\mathbf{B}^\dagger\mathbf{B}\right] = 1`. unpad_col : bool, optional If True, zero columns (with values less than 1.0e-8) on the right-hand side of the intial :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices are removed. unpad_row : bool, optional If True, zero rows (with values less than 1.0e-8) at the bottom of the intial :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices are removed. check_finite : bool, optional If True, convert the input to an array, checking for NaNs or Infs. weight : ndarray, optional The 1D-array representing the weights of each row of :math:`\mathbf{A}`. This defines the elements of the diagonal matrix :math:`\mathbf{W}` that is multiplied by :math:`\mathbf{A}` matrix, i.e., :math:`\mathbf{A} \rightarrow \mathbf{WA}`. lapack_driver : {"gesvd", "gesdd"}, optional Used in the singular value decomposition function from SciPy. Only allowed two options, with "gesvd" being less-efficient than "gesdd" but is more robust. Default is "gesvd". Returns ------- res : ProcrustesResult The Procrustes result represented as a class:`utils.ProcrustesResult` object. Notes ----- **Two-Sided Orthogonal Procrustes with Two Transformations:** The optimal orthogonal transformations are obtained by: .. math:: \mathbf{Q}_{1}^{\text{opt}}, \mathbf{Q}_{2}^{\text{opt}} = \arg \underbrace{\text{min}}_{\left\{ {\mathbf{Q}_1 \atop \mathbf{Q}_2} \left| {\mathbf{Q}_1^{-1} = \mathbf{Q}_1^\dagger \atop \mathbf{Q}_2^{-1} = \mathbf{Q}_2^\dagger} \right. \right\}} \|\mathbf{Q}_1^\dagger \mathbf{A} \mathbf{Q}_2 - \mathbf{B}\|_{F}^2 = \arg \underbrace{\text{max}}_{\left\{ {\mathbf{Q}_1 \atop \mathbf{Q}_2} \left| {\mathbf{Q}_1^{-1} = \mathbf{Q}_1^\dagger \atop \mathbf{Q}_2^{-1} = \mathbf{Q}_2^\dagger} \right. \right\}} \text{Tr}\left[\mathbf{Q}_2^\dagger\mathbf{A}^\dagger\mathbf{Q}_1\mathbf{B} \right] This is solved by taking the singular value decomposition (SVD) of :math:`\mathbf{A}` and :math:`\mathbf{B}`, .. math:: \mathbf{A} = \mathbf{U}_A \mathbf{\Sigma}_A \mathbf{V}_A^\dagger \\ \mathbf{B} = \mathbf{U}_B \mathbf{\Sigma}_B \mathbf{V}_B^\dagger Then the two optimal orthogonal matrices are given by, .. math:: \mathbf{Q}_1^{\text{opt}} = \mathbf{U}_A \mathbf{U}_B^\dagger \\ \mathbf{Q}_2^{\text{opt}} = \mathbf{V}_A \mathbf{V}_B^\dagger **Two-Sided Orthogonal Procrustes with Single-Transformation:** The optimal orthogonal transformation is obtained by: .. math:: \mathbf{Q}^{\text{opt}} = \arg \underbrace{\min}_{\left\{\mathbf{Q} | \mathbf{Q}^{-1} = {\mathbf{Q}}^\dagger \right\}} \|\mathbf{Q}^\dagger\mathbf{A}\mathbf{Q} - \mathbf{B}\|_{F}^2 = \arg \underbrace{\text{max}}_{\left\{\mathbf{Q} | \mathbf{Q}^{-1} = {\mathbf{Q}}^\dagger\right\}} \text{Tr}\left[\mathbf{Q}^\dagger\mathbf{A}^\dagger\mathbf{Q}\mathbf{B} \right] Using the singular value decomposition (SVD) of :math:`\mathbf{A}` and :math:`\mathbf{B}`, .. math:: \mathbf{A} = \mathbf{U}_A \mathbf{\Lambda}_A \mathbf{U}_A^\dagger \\ \mathbf{B} = \mathbf{U}_B \mathbf{\Lambda}_B \mathbf{U}_B^\dagger The optimal orthogonal matrix :math:`\mathbf{Q}^\text{opt}` is obtained through, .. math:: \mathbf{Q}^\text{opt} = \mathbf{U}_A \mathbf{S} \mathbf{U}_B^\dagger where :math:`\mathbf{S}` is a diagonal matrix with :math:`\pm{1}` elements, .. math:: \mathbf{S} = \begin{bmatrix} { \pm 1} & 0 &\cdots &0 \\ 0 &{ \pm 1} &\ddots &\vdots \\ \vdots &\ddots &\ddots &0\\ 0 &\cdots &0 &{ \pm 1} \end{bmatrix} The matrix :math:`\mathbf{S}` is chosen to be the identity matrix. Examples -------- >>> import numpy as np >>> a = np.array([[30, 33, 20], [33, 53, 43], [20, 43, 46]]) >>> b = np.array([[ 22.78131838, -0.58896768,-43.00635291, 0., 0.], ... [ -0.58896768, 16.77132475, 0.24289990, 0., 0.], ... [-43.00635291, 0.2428999 , 89.44735687, 0., 0.], ... [ 0. , 0. , 0. , 0., 0.]]) >>> res = orthogonal_2sided(a, b, single=True, pad=True, unpad_col=True) >>> res.t array([[ 0.25116633, 0.76371527, 0.59468855], [-0.95144277, 0.08183302, 0.29674906], [ 0.17796663, -0.64034549, 0.74718507]]) >>> res.error 1.9646186414076689e-26 """ # if translate: # warnings.warn( # "The translation matrix was not well defined. \ # Two sided rotation and translation don't commute.", # stacklevel=2, # ) # Check inputs new_a, new_b = setup_input_arrays( a, b, unpad_col, unpad_row, pad, translate, scale, check_finite, weight, ) # check symmetry if single_transform=True if single: if not np.allclose(new_a.T, new_a): raise ValueError( f"Array A with {new_a.shape} shape is not symmetric. " "Check pad, unpad_col, and unpad_row arguments." ) if not np.allclose(new_b.T, new_b): raise ValueError( f"Array B with {new_b.shape} shape is not symmetric. " "Check pad, unpad_col, and unpad_row arguments." ) # two-sided orthogonal Procrustes with one-transformations if single: _, ua = np.linalg.eigh(new_a) _, ub = np.linalg.eigh(new_b) u_opt = np.dot(ua, ub.T) # compute one-sided error error = compute_error(new_a, new_b, u_opt, u_opt.T) return ProcrustesResult(error=error, new_a=new_a, new_b=new_b, t=u_opt, s=u_opt.T) # two-sided orthogonal Procrustes with two-transformations ua, _, vta = scipy.linalg.svd(new_a, lapack_driver=lapack_driver) ub, _, vtb = scipy.linalg.svd(new_b, lapack_driver=lapack_driver) u_opt1 = np.dot(ua, ub.T) u_opt2 = np.dot(vta.T, vtb) error = compute_error(new_a, new_b, u_opt2, u_opt1.T) return ProcrustesResult(error=error, new_a=new_a, new_b=new_b, t=u_opt2, s=u_opt1.T)
def rotational(array_a, array_b, remove_zero_col=True, remove_zero_row=True, pad_mode="row-col", translate=False, scale=False, check_finite=True, weight=None): r""" Compute optimal rotational transformation array. The Procrustes analysis requires two 2d-arrays with the same number of rows, so the array with the smaller number of rows will automatically be padded with zero rows. Parameters ---------- array_a : ndarray The 2d-array :math:`\mathbf{A}_{m \times n}` which is going to be transformed. array_b : ndarray The 2d-array :math:`\mathbf{B}_{m \times n}` representing the reference array. remove_zero_col : bool, optional If True, zero columns (values less than 1e-8) on the right side will be removed. Default= True. remove_zero_row : bool, optional If True, zero rows (values less than 1e-8) on the bottom will be removed. Default= True. pad_mode : str, optional Specifying how to pad the arrays, listed below. Default="row-col". - "row" The array with fewer rows is padded with zero rows so that both have the same number of rows. - "col" The array with fewer columns is padded with zero columns so that both have the same number of columns. - "row-col" The array with fewer rows is padded with zero rows, and the array with fewer columns is padded with zero columns, so that both have the same dimensions. This does not necessarily result in square arrays. - "square" The arrays are padded with zero rows and zero columns so that they are both squared arrays. The dimension of square array is specified based on the highest dimension, i.e. :math:`\text{max}(n_a, m_a, n_b, m_b)`. translate : bool, optional If True, both arrays are translated to be centered at origin, ie columns of the arrays will have mean zero. Default=False. scale : bool, optional If True, both arrays are normalized to one with respect to the Frobenius norm, ie :math:`Tr(A^T A) = 1`. Default=False. check_finite : bool, optional If true, convert the input to an array, checking for NaNs or Infs. weight : ndarray The weighting matrix. Default=None. Returns ------- res : ProcrustesResult Procrustes analysis result object. Attributes ---------- new_a : ndarray The transformed ndarray :math:`A`. new_b : ndarray The transformed ndarray :math:`B`. array_u : ndarray The optimum rotational transformation matrix. error : float One-sided orthogonal Procrustes error. Notes ----- Given matrix :math:`\mathbf{A}_{m \times n}` and a reference :math:`\mathbf{B}_{m \times n}`, find the transformation of :math:`\mathbf{A}_{m \times n}` that makes it as close as possible to :math:`\mathbf{B}_{m \times n}`. I.e., .. math:: \underbrace{\min}_{\left\{\mathbf{U} \left| {\mathbf{U}^{-1} = {\mathbf{U}}^\dagger \atop \left| \mathbf{U} \right| = 1} \right. \right\}} \|\mathbf{A}\mathbf{U} - \mathbf{B}\|_{F}^2 &= \underbrace{\min}_{\left\{\mathbf{U} \left| {\mathbf{U}^{-1} = {\mathbf{U}}^\dagger \atop \left| \mathbf{U} \right| = 1} \right. \right\}} \text{Tr}\left[\left(\mathbf{A}\mathbf{U} - \mathbf{B} \right)^\dagger \left(\mathbf{A}\mathbf{U} - \mathbf{B} \right)\right] \\ &= \underbrace{\max}_{\left\{\mathbf{U} \left| {\mathbf{U}^{-1} = {\mathbf{U}}^\dagger \atop \left| \mathbf{U} \right| = 1} \right. \right\}} \text{Tr}\left[\mathbf{U}^\dagger {\mathbf{A}}^\dagger \mathbf{B} \right] Here, :math:`\mathbf{U}_{n \times n}` is the permutation matrix. The solution is obtained by taking the singular value decomposition (SVD) of the product of the matrix, .. math:: \mathbf{A}^\dagger \mathbf{B} &= \tilde{\mathbf{U}} \tilde{\mathbf{\Sigma}} \tilde{\mathbf{V}}^{\dagger} \\ \mathbf{U}_{\text{optimum}} &= \tilde{\mathbf{U}} \tilde{\mathbf{S}} \tilde{\mathbf{V}}^{\dagger} Where :math:`\tilde{\mathbf{S}}_{n \times m}` is almost an identity matrix, .. math:: \tilde{\mathbf{S}}_{m \times n} \equiv \begin{bmatrix} 1 & 0 & \cdots & 0 & 0 \\ 0 & 1 & \ddots & \vdots &0 \\ 0 & \ddots &\ddots & 0 &\vdots \\ \vdots&0 & 0 & 1 &0 \\ 0 & 0 & 0 \cdots &0 &\operatorname{sgn} \left(\left|\mathbf{U}\mathbf{V}^\dagger\right|\right) \end{bmatrix} I.e. the smallest singular value is replaced by .. math:: \operatorname{sgn} \left(\left|\tilde{\mathbf{U}} \tilde{\mathbf{V}}^\dagger\right|\right) = \begin{cases} +1 \qquad \left|\tilde{\mathbf{U}} \tilde{\mathbf{V}}^\dagger\right| \geq 0 \\ -1 \qquad \left|\tilde{\mathbf{U}} \tilde{\mathbf{V}}^\dagger\right| < 0 \end{cases} Examples -------- >>> import numpy as np >>> array_a = np.array([[1.5, 7.4], [8.5, 4.5]]) >>> array_b = np.array([[6.29325035, 4.17193001, 0., 0,], ... [9.19238816, -2.82842712, 0., 0.], ... [0., 0., 0., 0.]]) >>> res = rotational(array_a, array_b, translate=False, scale=False) >>> res['array_u'] # rotational array array([[ 0.70710678, -0.70710678], [ 0.70710678, 0.70710678]]) >>> res['error'] # error 1.483808210011695e-17 """ # check inputs new_a, new_b = setup_input_arrays(array_a, array_b, remove_zero_col, remove_zero_row, pad_mode, translate, scale, check_finite, weight) # compute SVD of A.T * A array_u, _, array_vt = np.linalg.svd(np.dot(new_a.T, new_b)) # construct S which is an identity matrix with the smallest # singular value replaced by sgn(|U*V^t|). s_value = np.eye(new_a.shape[1]) s_value[-1, -1] = np.sign(np.linalg.det(np.dot(array_u, array_vt))) # compute optimum rotation matrix u_opt = np.dot(np.dot(array_u, s_value), array_vt) # compute single-sided error error error = compute_error(new_a, new_b, u_opt) return ProcrustesResult(new_a=new_a, new_b=new_b, array_u=u_opt, error=error)
def generic( a: np.ndarray, b: np.ndarray, pad: bool = True, translate: bool = False, scale: bool = False, unpad_col: bool = False, unpad_row: bool = False, check_finite: bool = True, weight: Optional[np.ndarray] = None, use_svd: bool = False, ) -> ProcrustesResult: r"""Perform generic one-sided Procrustes. Given matrix :math:`\mathbf{A}_{m \times n}` and a reference matrix :math:`\mathbf{B}_{m \times n}`, find the transformation matrix :math:`\mathbf{T}_{n \times n}` that makes :math:`\mathbf{AT}` as close as possible to :math:`\mathbf{B}`. In other words, .. math:: \underbrace{\text{min}}_{\mathbf{T}} \quad \|\mathbf{A} \mathbf{T} - \mathbf{B}\|_{F}^2 This Procrustes method requires the :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices to have the same shape, which is gauranteed with the default ``pad`` argument for any given :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices. In preparing the :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices, the (optional) order of operations is: **1)** unpad zero rows/columns, **2)** translate the matrices to the origin, **3)** weight entries of :math:`\mathbf{A}`, **4)** scale the matrices to have unit norm, **5)** pad matrices with zero rows/columns so they have the same shape. Parameters ---------- a : ndarray The 2D-array :math:`\mathbf{A}` which is going to be transformed. b : ndarray The 2D-array :math:`\mathbf{B}` representing the reference matrix. pad : bool, optional Add zero rows (at the bottom) and/or columns (to the right-hand side) of matrices :math:`\mathbf{A}` and :math:`\mathbf{B}` so that they have the same shape. translate : bool, optional If True, both arrays are centered at origin (columns of the arrays will have mean zero). scale : bool, optional If True, both arrays are normalized with respect to the Frobenius norm, i.e., :math:`\text{Tr}\left[\mathbf{A}^\dagger\mathbf{A}\right] = 1` and :math:`\text{Tr}\left[\mathbf{B}^\dagger\mathbf{B}\right] = 1`. unpad_col : bool, optional If True, zero columns (with values less than 1.0e-8) on the right-hand side of the intial :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices are removed. unpad_row : bool, optional If True, zero rows (with values less than 1.0e-8) at the bottom of the intial :math:`\mathbf{A}` and :math:`\mathbf{B}` matrices are removed. check_finite : bool, optional If True, convert the input to an array, checking for NaNs or Infs. weight : ndarray, optional The 1D-array representing the weights of each row of :math:`\mathbf{A}`. This defines the elements of the diagonal matrix :math:`\mathbf{W}` that is multiplied by :math:`\mathbf{A}` matrix, i.e., :math:`\mathbf{A} \rightarrow \mathbf{WA}`. use_svd : bool, optional If True, the (Moore-Penrose) pseudo-inverse is computed by singular-value decomposition (SVD) including all 'large' singular values (using `scipy.linalg.pinv2`). If False, the the (Moore-Penrose) pseudo-inverse is computed by least-squares solver (using `scipy.linalg.pinv`). The least-squares implementation is less efficient, but more robust, than the SVD implementation. Returns ------- res : ProcrustesResult The Procrustes result represented as a class:`utils.ProcrustesResult` object. Notes ----- The optimal transformation matrix is obtained by solving the least-squares equations, .. math:: \mathbf{X}_\text{opt} = {(\mathbf{A}^{\top}\mathbf{A})}^{-1} \mathbf{A}^{\top} \mathbf{B} If :math:`m < n`, the transformation matrix :math:`\mathbf{T}_\text{opt}` is not unique, because the system of equations is underdetermined (i.e., there are fewer equations than unknowns). """ if not isinstance(use_svd, bool): raise TypeError( f"The use_svd parameter {type(use_svd)} should be type bool.") # check inputs new_a, new_b = setup_input_arrays( a, b, unpad_col, unpad_row, pad, translate, scale, check_finite, weight, ) # compute the generic solution if use_svd: # Use the singular value decomposition, much faster but less robust. a_inv = pinv2(np.dot(new_a.T, new_a)) else: # Uses the least-squared method. a_inv = pinv(np.dot(new_a.T, new_a)) array_x = np.linalg.multi_dot([a_inv, new_a.T, new_b]) # compute one-sided error e_opt = compute_error(new_a, new_b, array_x) return ProcrustesResult(error=e_opt, new_a=new_a, new_b=new_b, t=array_x, s=None)
def permutation(array_a, array_b, remove_zero_col=True, remove_zero_row=True, pad_mode="row-col", translate=False, scale=False, check_finite=True, weight=None): r""" Single sided permutation Procrustes. Parameters ---------- array_a : ndarray The 2d-array :math:`\mathbf{A}_{m \times n}` which is going to be transformed. array_b : ndarray The 2d-array :math:`\mathbf{B}_{m \times n}` representing the reference. remove_zero_col : bool, optional If True, the zero columns on the right side will be removed. Default= True. remove_zero_row : bool, optional If True, the zero rows on the top will be removed. Default= True. pad_mode : str, optional Specifying how to pad the arrays, listed below. Default="row-col". - "row" The array with fewer rows is padded with zero rows so that both have the same number of rows. - "col" The array with fewer columns is padded with zero columns so that both have the same number of columns. - "row-col" The array with fewer rows is padded with zero rows, and the array with fewer columns is padded with zero columns, so that both have the same dimensions. This does not necessarily result in square arrays. - "square" The arrays are padded with zero rows and zero columns so that they are both squared arrays. The dimension of square array is specified based on the highest dimension, i.e. :math:`\text{max}(n_a, m_a, n_b, m_b)`. translate : bool, optional If True, both arrays are translated to be centered at origin, ie columns of the arrays will have mean zero. Default=False. scale : bool, optional If True, both arrays are normalized to one with respect to the Frobenius norm, ie :math:`Tr(A^T A) = 1`. Default=False. check_finite : bool, optional If true, convert the input to an array, checking for NaNs or Infs. Default=True. weight : ndarray The weighting matrix. Default=None. Returns ------- res: ProcrustesResult Procrustes analysis result object. Attributes ---------- A : ndarray The transformed ndarray A. B : ndarray The transformed ndarray B. array_u : ndarray The optimum permutation transformation matrix. error : float One-sided permutation Procrustes error. Notes ----- Given matrix :math:`\mathbf{A}_{n \times n}` and reference :math:`\mathbf{B}_{n \times n}` find a permutation of the rows and/or columns of :math:`\mathbf{A}_{n \times n}` that makes it as close as possible to :math:`\mathbf{B}_{n \times n}`. I.e., .. math:: \underbrace{\text{min}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \|\mathbf{A} \mathbf{P} - \mathbf{B}\|_{F}^2 &= \underbrace{\text{min}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\left(\mathbf{A}\mathbf{P} - \mathbf{B} \right)^\dagger \left(\mathbf{P}^\dagger\mathbf{A}\mathbf{P} - \mathbf{B} \right)\right] \\ &= \underbrace{\text{max}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger\mathbf{A}^\dagger\mathbf{B} \right] Here, :math:`\mathbf{P}_{n \times n}` is the permutation matrix. The solution is to relax the problem into a linear programming problem and note that the solution to a linear programming problem is always at the boundary of the allowed region, which means that the solution can always be written as a permutation matrix, .. math:: \underbrace{\text{max}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger\mathbf{A}^\dagger\mathbf{B} \right] = \underbrace{\text{max}}_{\left\{\mathbf{P} \left| {p_{ij} \geq 0 \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger\left(\mathbf{A}^\dagger\mathbf{B}\right) \right] This is a matching problem and can be solved by the Hungarian method. Note that if :math:`\mathbf{A}` and :math:`\mathbf{B}` have different numbers of items, you choose the larger matrix as :math:`\mathbf{B}` and then pad :math:`\mathbf{A}` with rows/columns of zeros. """ # check inputs new_a, new_b = setup_input_arrays(array_a, array_b, remove_zero_col, remove_zero_row, pad_mode, translate, scale, check_finite, weight) # compute permutation Procrustes matrix array_p = np.dot(new_a.T, new_b) array_c = np.full(array_p.shape, np.max(array_p)) array_c -= array_p array_u = np.zeros(array_p.shape) # set elements to 1 according to Hungarian algorithm (linear_sum_assignment) array_u[linear_sum_assignment(array_c)] = 1 error = compute_error(new_a, new_b, array_u) # return new_a, new_b, array_u, error return ProcrustesResult(new_a=new_a, new_b=new_b, array_u=array_u, error=error)
def permutation_2sided_explicit(array_a, array_b, remove_zero_col=True, remove_zero_row=True, pad_mode="row-col", translate=False, scale=False, check_finite=True, weight=None): r""" Two sided permutation Procrustes by explicit method. Parameters ---------- array_a : ndarray The 2d-array :math:`\mathbf{A}_{m \times n}` which is going to be transformed. array_b : ndarray The 2d-array :math:`\mathbf{B}_{m \times n}` representing the reference. remove_zero_col : bool, optional If True, near zero columns (less than 1e-8) on the right side will be removed. Default=True. remove_zero_row : bool, optional If True, near zero rows (less than 1e-8) on the bottom will be removed. Default= True. pad_mode : str, optional Specifying how to pad the arrays, listed below. Default="row-col". - "row" The array with fewer rows is padded with zero rows so that both have the same number of rows. - "col" The array with fewer columns is padded with zero columns so that both have the same number of columns. - "row-col" The array with fewer rows is padded with zero rows, and the array with fewer columns is padded with zero columns, so that both have the same dimensions. This does not necessarily result in square arrays. - "square" The arrays are padded with zero rows and zero columns so that they are both squared arrays. The dimension of square array is specified based on the highest dimension, i.e. :math:`\text{max}(n_a, m_a, n_b, m_b)`. translate : bool, optional If True, both arrays are translated to be centered at origin. Default=False. scale : bool, optional If True, both arrays are column normalized to unity. Default=False. check_finite : bool, optional If true, convert the input to an array, checking for NaNs or Infs. Default=True. weight : ndarray The weighting matrix. Default=None. Returns ------- res : ProcrustesResult Procrustes analysis result object. Attributes ---------- new_a : ndarray The transformed ndarray A. new_b : ndarray The transformed ndarray B. array_u : ndarray The optimum permutation transformation matrix. error : float Two-sided orthogonal Procrustes error. Notes ----- Given matrix :math:`\mathbf{A}_{n \times n}` and a reference :math:`\mathbf{B}_{n \times n}`, find a permutation of rows/columns of :math:`\mathbf{A}_{n \times n}` that makes it as close as possible to :math:`\mathbf{B}_{n \times n}`. But be careful that we are using a brutal way to loop over all the possible permutation matrices and return the one that gives the minimum error(distance). This method can be used as a checker for small dataset. """ print("Warning: This brute-strength method is computational expensive! \n" "But it can be used as a checker for a small dataset.") # check inputs new_a, new_b = setup_input_arrays(array_a, array_b, remove_zero_col, remove_zero_row, pad_mode, translate, scale, check_finite, weight) perm1 = np.zeros(np.shape(new_a)) perm_error1 = np.inf for comb in it.permutations(np.arange(np.shape(new_a)[0])): # Compute the permutation matrix size = np.shape(new_a)[1] perm2 = np.zeros((size, size)) perm2[np.arange(size), comb] = 1 perm_error2 = compute_error(new_a, new_b, perm2, perm2) if perm_error2 < perm_error1: perm_error1 = perm_error2 perm1 = perm2 # return new_a, new_b, perm1, perm_error1 return ProcrustesResult(new_a=new_a, new_b=new_b, array_u=perm1, error=perm_error1)
def permutation_2sided(array_a, array_b, transform_mode="single", remove_zero_col=True, remove_zero_row=True, pad_mode="row-col", translate=False, scale=False, mode="normal1", check_finite=True, iteration=500, add_noise=False, tol=1.0e-8, kopt=False, kopt_k=3, kopt_tol=1.e-8, weight=None): r"""Double sided permutation Procrustes. Parameters ---------- array_a : ndarray The 2d-array :math:`\mathbf{A}_{m \times n}` which is going to be transformed. array_b : ndarray The 2d-array :math:`\mathbf{B}_{m \times n}` representing the reference. transform_mode : str, optional When transform_mode="single", it is the two-sided permutation Procrustes with one transformation. (1). If the input matrices (adjacency matrices) are symmetric within the threshold of 1.e-5, undirected graph matching algorithm will be applied. (2). If the input matrices (adjacency matrices) are asymmetric, the directed graph matching is applied. When transform_mode="double", the problem becomes two-sided permutation Procrustes with two transformations (denoted as regular two-sided permutation Procrustes here). An flip-flop algorithm taken from *Parallel solution of SVD-related problems, with applications, Pythagoras Papadimitriou, Ph.D. Thesis, University of Manchester, 1993* is used to solve the problem. Default="single". Otherwise, transform_mode="double", the two-sided permutation Procrustes with two transformations will be performed. Default="single_undirected". remove_zero_col : bool, optional If True, zero columns (values less than 1e-8) on the right side will be removed. Default= True. remove_zero_row : bool, optional If True, zero rows (values less than 1e-8) on the bottom will be removed. Default= True. pad_mode : str, optional Specifying how to pad the arrays, listed below. Default="row-col". - "row" The array with fewer rows is padded with zero rows so that both have the same number of rows. - "col" The array with fewer columns is padded with zero columns so that both have the same number of columns. - "row-col" The array with fewer rows is padded with zero rows, and the array with fewer columns is padded with zero columns, so that both have the same dimensions. This does not necessarily result in square arrays. - "square" The arrays are padded with zero rows and zero columns so that they are both squared arrays. The dimension of square array is specified based on the highest dimension, i.e. :math:`\text{max}(n_a, m_a, n_b, m_b)`. translate : bool, optional If True, both arrays are translated to be centered at origin, ie columns of the arrays will have mean zero. Default=False. scale : bool, optional If True, both arrays are normalized to one with respect to the Frobenius norm, ie :math:`Tr(A^T A) = 1`. Default=False. mode : string, optional Option for choosing the initial guess methods, including "normal1", "normal2", "umeyama" and "umeyama_approx". "umeyama_approx" is the approximated umeyama method. check_finite : bool, optional If true, convert the input to an array, checking for NaNs or Infs. Default=True. iteration : int, optional Maximum number for iterations. Default=500. add_noise : bool, optional Add small noise if the arrays are non-diagonalizable. Default=False. tol : float, optional The tolerance value used for updating the initial guess. Default=1.e-8. kopt : bool, optional If True, the k_opt heuristic search will be performed. Default=False. kopt_k : int, optional Defines the oder of k-opt heuristic local search. For example, kopt_k=3 leads to a local search of 3 items and kopt_k=2 only searches for two items locally. Default=3. kopt_tol : float, optional Tolerance value to check if k-opt heuristic converges. Default=1.e-8. Returns ------- res : ProcrustesResult Procrustes analysis result object. Attributes ---------- new_a : ndarray The transformed ndarray A. new_b : ndarray The transformed ndarray B. array_u : ndarray The optimum permutation transformation matrix. array_p : ndarray The optimum permutation transformation matrix when using double transform mode. array_q : ndarray The optimum permutation transformation matrix when using double transform mode. error : float Two-sided permutation Procrustes error. Notes ----- Given matrix :math:`\mathbf{A}_{n \times n}` and a reference :math:`\mathbf{B}_{n \times n}`, find a permutation of rows/columns of :math:`\mathbf{A}_{n \times n}` that makes it as close as possible to :math:`\mathbf{B}_{n \times n}`. I.e., .. math:: &\underbrace{\text{min}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \|\mathbf{P}^\dagger \mathbf{A} \mathbf{P} - \mathbf{B}\|_{F}^2\\ = &\underbrace{\text{min}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\left(\mathbf{P}^\dagger\mathbf{A}\mathbf{P} - \mathbf{B} \right)^\dagger \left(\mathbf{P}^\dagger\mathbf{A}\mathbf{P} - \mathbf{B} \right)\right] \\ = &\underbrace{\text{max}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger\mathbf{A}^\dagger\mathbf{P}\mathbf{B} \right]\\ Here, :math:`\mathbf{P}_{n \times n}` is the permutation matrix. Given an intial guess, the best local minimum can be obtained by the iterative procedure, .. math:: p_{ij}^{(n + 1)} = p_{ij}^{(n)} \sqrt{ \frac{2\left[\mathbf{T}^{(n)}\right]_{ij}}{\left[ \mathbf{P}^{(n)} \left( \left(\mathbf{P}^{(n)}\right)^T \mathbf{T} + \left( \left(\mathbf{P}^{(n)}\right)^T \mathbf{T} \right)^T \right) \right]_{ij}} } where, .. math:: \mathbf{T}^{(n)} = \mathbf{A} \mathbf{P}^{(n)} \mathbf{B} Using an initial guess, the iteration can stops when the change in :math:`d` is below the specified threshold, .. math:: d = \text{Tr} \left[\left(\mathbf{P}^{(n+1)} -\mathbf{P}^{(n)} \right)^T \left(\mathbf{P}^{(n+1)} -\mathbf{P}^{(n)} \right)\right] The outcome of the iterative procedure :math:`\mathbf{P}^{(\infty)}` is not a permutation matrix. So, the closest permutation can be found by setting ``refinement=True``. This uses :class:`procrustes.permutation.PermutationProcrustes` to find the closest permutation; that is, .. math:: \underbrace{\text{min}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \|\mathbf{P} - \mathbf{P}^{(\infty)}\|_{F}^2 = \underbrace{\text{max}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger\mathbf{P}^{(\infty)} \right] The answer to this problem is a heuristic solution for the matrix-matching problem that seems to be relatively accurate. **Initial Guess:** Two possible initial guesses are inferred from the Umeyama procedure. One can find either the closest permutation matrix to :math:`\mathbf{U}_\text{Umeyama}` or to :math:`\mathbf{U}_\text{Umeyama}^\text{approx.}`. Considering the :class:`procrustes.permutation.PermutationProcrustes`, the resulting permutation matrix can be specified as initial guess through ``guess=umeyama`` and ``guess=umeyama_approx``, which solves: .. math:: \underbrace{\text{max}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger\mathbf{U}_\text{Umeyama} \right] \\ \underbrace{\text{max}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger\mathbf{U}_\text{Umeyama}^\text{approx.} \right] Another choice is to start by solving a normal permutation Procrustes problem. In other words, write new matrices, :math:`\mathbf{A}^0` and :math:`\mathbf{B}^0`, with columns like, .. math:: \begin{bmatrix} a_{ii} \\ p \cdot \text{sgn}\left( a_{ij_\text{max}} \right) \underbrace{\text{max}}_{1 \le j \le n} \left(\left|a_{ij}\right|\right)\\ p^2 \cdot \text{sgn}\left( a_{ij_{\text{max}-1}} \right) \underbrace{\text{max}-1}_{1 \le j \le n} \left(\left|a_{ij}\right|\right)\\ \vdots \end{bmatrix} Here, :math:`\text{max}-1` denotes the second-largest absolute value of elements, :math:`\text{max}-2` is the third-largest abosule value of elements, etc. The matrices :math:`\mathbf{A}^0` and :math:`\mathbf{B}^0` have the diagonal elements of :math:`\mathbf{A}` and :math:`\mathbf{B}` in the first row, and below the first row has the largest off-diagonal element in row :math:`i`, the second-largest off-diagonal element, etc. The elements are weighted by a factor :math:`0 < p < 1`, so that the smaller elements are considered less important for matching. The matrices can be truncated after a few terms; for example, after the size of elements falls below some threshold. A reasonable choice would be to stop after :math:`\lfloor \frac{-2\ln 10}{\ln p} +1\rfloor` rows; this ensures that the size of the elements in the last row is less than 1% of those in the first off-diagonal row. There are obviously many different ways to construct the matrices :math:`\mathbf{A}^0` and :math:`\mathbf{B}^0`. Another, even better, method would be to try to encode not only what the off-diagonal elements are, but which element in the matrix they correspond to. One could do that by not only listing the diagonal elements, but also listing the associated off-diagonal element. I.e., the columns of :math:`\mathbf{A}^0` and :math:`\mathbf{B}^0` would be, .. math:: \begin{bmatrix} a_{ii} \\ p \cdot a_{j_\text{max} j_\text{max}} \\ p \cdot \text{sgn}\left( a_{ij_\text{max}} \right) \underbrace{\text{max}}_{1 \le j \le n} \left(\left|a_{ij}\right|\right)\\ p^2 \cdot a_{j_{\text{max}-1} j_{\text{max}-1}} \\ p^2 \cdot \text{sgn}\left( a_{ij_{\text{max}-1}} \right) \underbrace{\text{max}-1}_{1 \le j \le n} \left(\left|a_{ij}\right|\right)\\ \vdots \end{bmatrix} In this case, you would stop the procedure after :math:`m = \left\lfloor {\frac{{ - 4\ln 10}}{{\ln p}} + 1} \right \rfloor` rows. Then one uses the :class:`procrustes.permutation.PermutationProcrustes` to match the constructed matrices :math:`\mathbf{A}^0` and :math:`\mathbf{B}^0` instead of :math:`\mathbf{A}` and :math:`\mathbf{B}`. I.e., .. math:: \underbrace{\text{max}}_{\left\{\mathbf{P} \left| {p_{ij} \in \{0, 1\} \atop \sum_{i=1}^n p_{ij} = \sum_{j=1}^n p_{ij} = 1} \right. \right\}} \text{Tr}\left[\mathbf{P}^\dagger \left(\mathbf{A^0}^\dagger\mathbf{B^0}\right)\right] Please note that the "umeyama_approx" might give inaccurate permutation matrix. More specificity, this is a approximated Umeyama method. One example we can give is that when we compute the permutation matrix that transforms :math:`A` to :math:`B`, the "umeyama_approx" method can not give the exact permutation transformation matrix while "umeyama", "normal1" and "normal2" do. .. math:: A = \begin{bmatrix} 4 & 5 & -3 & 3 \\ 5 & 7 & 3 & -5 \\ -3 & 3 & 2 & 2 \\ 3 & -5 & 2 & 5 \\ \end{bmatrix} \\ B = \begin{bmatrix} 73 & 100 & 73 & -62 \\ 100 & 208 & -116 & 154 \\ 73 & -116 & 154 & 100 \\ -62 & 154 & 100 & 127 \\ \end{bmatrix} \\ """ # check inputs new_a, new_b = setup_input_arrays(array_a, array_b, remove_zero_col, remove_zero_row, pad_mode, translate, scale, check_finite, weight) # np.power() can not handle the negatives values # Try to convert the matrices to non-negative # shift the the matrices to avoid negative values # otherwise it will cause an error in the Eq. 28 in the research notes maximum = max(np.amax(np.abs(new_a)), np.amax(np.abs(new_b))) new_a_positive = new_a.astype(np.float) + maximum new_b_positive = new_b.astype(np.float) + maximum # Do single-transformation computation if requested transform_mode = transform_mode.lower() if transform_mode == "single": # algorithm for undirected graph matching problem # check if two matrices are symmetric within a relative tolerance and absolute tolerance. if np.allclose(new_a_positive, new_a_positive.T, rtol=1.e-05, atol=1.e-08) and \ np.allclose(new_b_positive, new_b_positive.T, rtol=1.e-05, atol=1.e-08): # the initial guess guess = _guess_initial_permutation_undirected( new_a_positive, new_b_positive, mode, add_noise) # Compute the permutation matrix by iterations array_u = _compute_transform(new_a_positive, new_b_positive, guess, tol, iteration) error = compute_error(new_a_positive, new_b_positive, array_u, array_u) # k-opt heuristic if kopt: array_u, error = kopt_heuristic_single(array_a=new_a_positive, array_b=new_b_positive, ref_error=error, perm=array_u, kopt_k=kopt_k, kopt_tol=kopt_tol) # algorithm for directed graph matching problem else: # the initial guess guess = _2sided_1trans_initial_guess_directed( new_a_positive, new_b_positive) # Compute the permutation matrix by iterations array_u = _compute_transform_directed(new_a_positive, new_b_positive, guess, tol, iteration) error = compute_error(new_a_positive, new_b_positive, array_u, array_u) # k-opt heuristic if kopt: array_u, error = kopt_heuristic_single(array_a=new_a_positive, array_b=new_b_positive, ref_error=error, perm=array_u, kopt_k=kopt_k, kopt_tol=kopt_tol) return ProcrustesResult(new_a=new_a, new_b=new_b, array_u=array_u, error=error) # Do regular computation elif transform_mode == "double": array_m = new_a array_n = new_b array_p, array_q, error = _2sided_regular(array_m, array_n, tol, iteration) # perform k-opt heuristic search twice if kopt: array_p, array_q, error = kopt_heuristic_double(array_m=array_m, array_n=array_n, ref_error=error, perm_p=array_p, perm_q=array_q, kopt_k=kopt_k, kopt_tol=kopt_tol) # return array_m, array_n, array_p, array_q, error return ProcrustesResult(new_a=new_a, new_b=new_b, array_p=array_p, array_q=array_q, error=error) else: raise ValueError( """Invalid transform_mode argument, use "single"or "double".""")
def orthogonal(array_a, array_b, remove_zero_col=True, remove_zero_row=True, pad_mode='row-col', translate=False, scale=False, check_finite=True, weight=None): r""" One-sided orthogonal Procrustes. The Procrustes analysis requires two 2d-arrays with the same number of rows, so the array with the smaller number of rows will automatically be padded with zero rows. Parameters ---------- array_a : ndarray The 2d-array :math:`\mathbf{A}_{m \times n}` which is going to be transformed. array_b : ndarray The 2d-array :math:`\mathbf{B}_{m \times n}` representing the reference array. remove_zero_col : bool, optional If True, the zero columns on the right side will be removed. Default= True. remove_zero_row : bool, optional If True, the zero rows on the top will be removed. Default= True. pad_mode : str, optional Specifying how to pad the arrays, listed below. Default="row-col". - "row" The array with fewer rows is padded with zero rows so that both have the same number of rows. - "col" The array with fewer columns is padded with zero columns so that both have the same number of columns. - "row-col" The array with fewer rows is padded with zero rows, and the array with fewer columns is padded with zero columns, so that both have the same dimensions. This does not necessarily result in square arrays. - "square" The arrays are padded with zero rows and zero columns so that they are both squared arrays. The dimension of square array is specified based on the highest dimension, i.e. :math:`\text{max}(n_a, m_a, n_b, m_b)`. translate : bool, optional If True, both arrays are translated to be centered at origin, ie columns of the arrays will have mean zero. Default=False. scale : bool, optional If True, both arrays are normalized to one with respect to the Frobenius norm, ie :math:`Tr(A^T A) = 1`. Default=False. check_finite : bool, optional If true, convert the input to an array, checking for NaNs or Infs. Default=True. weight : ndarray The weighting matrix. Default=None. Returns ------- res: ProcrustesResult Procrustes analysis result object. Attributes ---------- new_a : ndarray The transformed ndarray :math:`A`. new_b : ndarray The transformed ndarray :math:`B`. array_u : ndarray The optimum orthogonal transformation matrix. error : float One-sided orthogonal Procrustes error. Notes ----- Given matrix :math:`\mathbf{A}_{m \times n}` and a reference :math:`\mathbf{B}_{m \times n}`, find the unitary/orthogonal transformation matrix :math:`\mathbf{U}_{n \times n}` that makes :math:`\mathbf{A}_{m \times n}` as close as possible to :math:`\mathbf{B}_{m \times n}`. I.e., .. math:: \underbrace{\min}_{\left\{\mathbf{U} | \mathbf{U}^{-1} = {\mathbf{U}}^\dagger \right\}} \|\mathbf{A}\mathbf{U} - \mathbf{B}\|_{F}^2 &= \underbrace{\text{min}}_{\left\{\mathbf{U} | \mathbf{U}^{-1} = {\mathbf{U}}^\dagger \right\}} \text{Tr}\left[\left(\mathbf{A}\mathbf{U} - \mathbf{B} \right)^\dagger \left(\mathbf{A}\mathbf{U} - \mathbf{B} \right)\right] \\ &= \underbrace{\text{max}}_{\left\{\mathbf{U} | \mathbf{U}^{-1} = {\mathbf{U}}^\dagger \right\}} \text{Tr}\left[\mathbf{U}^\dagger {\mathbf{A}}^\dagger \mathbf{B} \right] The solution is obtained by taking the singular value decomposition (SVD) of the product of the matrices, .. math:: \mathbf{A}^\dagger \mathbf{B} &= \tilde{\mathbf{U}} \tilde{\mathbf{\Sigma}} \tilde{\mathbf{V}}^{\dagger} \\ \mathbf{U}_{\text{optimum}} &= \tilde{\mathbf{U}} \tilde{\mathbf{V}}^{\dagger} The singular values are always listed in decreasing order, with the smallest singular value in the bottom-right-hand corner of :math:`\tilde{\mathbf{\Sigma}}`. Examples -------- >>> import numpy as np >>> array_a = np.array([[-7.3, 2.8], [-7.1, -0.2], ... [ 4. , 1.4], [ 1.3, 0. ]]) >>> array_b = np.array([[-5.90207845, -5.12791088], ... [-6.74021234, -2.24043246], ... [ 4.23759847, 0.05252849], ... [ 1.22159856, 0.44463126]]) >>> new_a, new_b, array_u, error_opt = orthogonal(array_a, array_b) >>> new_a array([[-7.3, 2.8], [-7.1, -0.2], [ 4. , 1.4], [ 1.3, 0. ]]) >>> new_b array([[-5.90207845, -5.12791088], [-6.74021234, -2.24043246], [ 4.23759847, 0.05252849], [ 1.22159856, 0.44463126]]) >>> array_u # the optimum orthogonal transformation array array([[ 0.9396912 , 0.34202404], [ 0.34202404, -0.9396912 ]]) >>> error_opt #error 1.435973366535123e-29 """ # check inputs new_a, new_b = setup_input_arrays(array_a, array_b, remove_zero_col, remove_zero_row, pad_mode, translate, scale, check_finite, weight) # calculate SVD of array_a.T * array_b array_u, _, array_vt = np.linalg.svd(np.dot(new_a.T, new_b)) # compute optimum orthogonal transformation array_u_opt = np.dot(array_u, array_vt) # compute the error error = compute_error(new_a, new_b, array_u_opt) # return new_a, new_b, array_u_opt, error return ProcrustesResult(new_a=new_a, new_b=new_b, array_u=array_u_opt, error=error)
def orthogonal_2sided(array_a, array_b, remove_zero_col=True, remove_zero_row=True, pad_mode='row-col', translate=False, scale=False, single_transform=True, check_finite=True, weight=None): r""" Two-Sided Orthogonal Procrustes. Parameters ---------- array_a : ndarray The 2d-array :math:`\mathbf{A}_{m \times n}` which is going to be transformed. array_b : ndarray The 2d-array :math:`\mathbf{B}_{m \times n}` representing the reference array. remove_zero_col : bool, optional If True, zero columns (values less than 1e-8) on the right side will be removed. Default=True. remove_zero_row : bool, optional If True, zero rows (values less than 1e-8) on the bottom will be removed. Default= True. pad_mode : str, optional Specifying how to pad the arrays, listed below. Default="row-col". - "row" The array with fewer rows is padded with zero rows so that both have the same number of rows. - "col" The array with fewer columns is padded with zero columns so that both have the same number of columns. - "row-col" The array with fewer rows is padded with zero rows, and the array with fewer columns is padded with zero columns, so that both have the same dimensions. This does not necessarily result in square arrays. - "square" The arrays are padded with zero rows and zero columns so that they are both squared arrays. The dimension of square array is specified based on the highest dimension, i.e. :math:`\text{max}(n_a, m_a, n_b, m_b)`. translate : bool, optional If True, both arrays are translated to be centered at origin. Default=False. scale : bool, optional If True, both arrays are column normalized to unity. Default=False. single_transform : bool If True, two-sided orthogonal Procrustes with one transformation will be performed. Default=False. check_finite : bool, optional If true, convert the input to an array, checking for NaNs or Infs. Default=True. weight : ndarray The weighting matrix. Default=None. Returns ------- res : ProcrustesResult Procrustes analysis result object. Attributes ---------- array_a : ndarray The transformed ndarray :math:`A`. array_b : ndarray The transformed ndarray :math:`B`. array_u : ndarray The transformation ndarray if "single_transform=False". array_p : ndarray The optimal orthogonal left-multiplying transformation ndarray if "single_transform=True". array_q : ndarray The second transformation ndarray if "single_transform=True". error : float The single- or double- sided orthogonal Procrustes error. Raises ------ ValueError When input array :math:`A` or :math:`A` is not symmetric. Notes ----- **Two-Sided Orthogonal Procrustes:** Given matrix :math:`\mathbf{A}_{m \times n}` and a reference :math:`\mathbf{B}_{m \times n}`, find two unitary/orthogonal transformation of :math:`\mathbf{A}_{m \times n}` that makes it as as close as possible to :math:`\mathbf{B}_{m \times n}`. I.e., .. math:: \underbrace{\text{min}}_{\left\{ {\mathbf{U}_1 \atop \mathbf{U}_2} \left| {\mathbf{U}_1^{-1} = \mathbf{U}_1^\dagger \atop \mathbf{U}_2^{-1} = \mathbf{U}_2^\dagger} \right. \right\}} \|\mathbf{U}_1^\dagger \mathbf{A} \mathbf{U}_2 - \mathbf{B}\|_{F}^2 &= \underbrace{\text{min}}_{\left\{ {\mathbf{U}_1 \atop \mathbf{U}_2} \left| {\mathbf{U}_1^{-1} = \mathbf{U}_1^\dagger \atop \mathbf{U}_2^{-1} = \mathbf{U}_2^\dagger} \right. \right\}} \text{Tr}\left[\left(\mathbf{U}_1^\dagger\mathbf{A}\mathbf{U}_2 - \mathbf{B} \right)^\dagger \left(\mathbf{U}_1^\dagger\mathbf{A}\mathbf{U}_2 - \mathbf{B} \right)\right] \\ &= \underbrace{\text{min}}_{\left\{ {\mathbf{U}_1 \atop \mathbf{U}_2} \left| {\mathbf{U}_1^{-1} = \mathbf{U}_1^\dagger \atop \mathbf{U}_2^{-1} = \mathbf{U}_2^\dagger} \right. \right\}} \text{Tr}\left[\mathbf{U}_2^\dagger\mathbf{A}^\dagger\mathbf{U}_1\mathbf{B} \right] We can get the solution by taking singular value decomposition of the matrices. Having, .. math:: \mathbf{A} = \mathbf{U}_A \mathbf{\Sigma}_A \mathbf{V}_A^\dagger \\ \mathbf{B} = \mathbf{U}_B \mathbf{\Sigma}_B \mathbf{V}_B^\dagger The transformation is foubd by, .. math:: \mathbf{U}_1 = \mathbf{U}_A \mathbf{U}_B^\dagger \\ \mathbf{U}_2 = \mathbf{V}_B \mathbf{V}_B^\dagger **Two-Sided Orthogonal Procrustes with Single-Transformation:** Given matrix :math:`\mathbf{A}_{n \times n}` and a reference :math:`\mathbf{B}_{n \times n}`, find one unitary/orthogonal transformation matrix :math:`\mathbf{U}_{n \times n}` that makes :math:`\mathbf{A}_{n \times n}` as close as possible to :math:`\mathbf{B}_{n \times n}`. I.e., .. math:: \underbrace{\min}_{\left\{\mathbf{U} | \mathbf{U}^{-1} = {\mathbf{U}}^\dagger \right\}} \|\mathbf{U}^\dagger\mathbf{A}\mathbf{U} - \mathbf{B}\|_{F}^2 &= \underbrace{\text{min}}_{\left\{\mathbf{U} | \mathbf{U}^{-1} = {\mathbf{U}}^\dagger \right\}} \text{Tr}\left[\left(\mathbf{U}^\dagger\mathbf{A}\mathbf{U} - \mathbf{B} \right)^\dagger \left(\mathbf{U}^\dagger\mathbf{A}\mathbf{U} - \mathbf{B} \right)\right] \\ &= \underbrace{\text{max}}_{\left\{\mathbf{U} | \mathbf{U}^{-1} = {\mathbf{U}}^\dagger \right\}} \text{Tr}\left[\mathbf{U}^\dagger\mathbf{A}^\dagger\mathbf{U}\mathbf{B} \right] Taking the eigenvalue decomposition of the matrices: .. math:: \mathbf{A} = \mathbf{U}_A \mathbf{\Lambda}_A \mathbf{U}_A^\dagger \\ \mathbf{B} = \mathbf{U}_B \mathbf{\Lambda}_B \mathbf{U}_B^\dagger the solution is obtained by, .. math:: \mathbf{U} = \mathbf{U}_A \mathbf{S} \mathbf{U}_A^\dagger where :math:`\mathbf{S}` is a diagonal matrix for which every diagonal element is :math:`\pm{1}`, .. math:: \mathbf{S} = \begin{bmatrix} { \pm 1} & 0 &\cdots &0 \\ 0 &{ \pm 1} &\ddots &\vdots \\ \vdots &\ddots &\ddots &0\\ 0 &\cdots &0 &{ \pm 1} \end{bmatrix} The matrix :math:`\mathbf{S}` is chosen to be the identity matrix. Please note that the translation operation is not well defined for two sided orthogonal procrustes since two sided rotation and translation don't commute. Therefore, please be careful when setting translate=True. Examples -------- >>> import numpy as np >>> array_a = np.array([[30, 33, 20], [33, 53, 43], [20, 43, 46]]) >>> array_b = np.array([[ 22.78131838, -0.58896768,-43.00635291, 0., 0.], ... [ -0.58896768, 16.77132475, 0.24289990, 0., 0.], ... [-43.00635291, 0.2428999 , 89.44735687, 0., 0.], ... [ 0. , 0. , 0. , 0., 0.]]) >>> new_a, new_b, array_u, error_opt = orthogonal_2sided( ... array_a, array_b, single_transform=True, ... remove_zero_col=True, remove_zero_rwo=True, mode='exact') >>> array_u array([[ 0.25116633, 0.76371527, 0.59468855], [-0.95144277, 0.08183302, 0.29674906], [ 0.17796663, -0.64034549, 0.74718507]]) >>> error_opt 1.9646186414076689e-26 """ if translate: warnings.warn("The translation matrix was not well defined. \ Two sided rotation and translation don't commute.", stacklevel=2) # Check inputs new_a, new_b = setup_input_arrays(array_a, array_b, remove_zero_col, remove_zero_row, pad_mode, translate, scale, check_finite, weight) # Check symmetry if single_transform=True if single_transform: if not np.allclose(new_a.T, new_a): raise ValueError( "array_a, after removal/padding, should be symmetric.") if not np.allclose(new_b.T, new_b): raise ValueError( "array_b, after removal/padding, should be symmetric.") # Do single-transformation computation if requested if single_transform: _, array_ua = np.linalg.eigh(new_a) _, array_ub = np.linalg.eigh(new_b) u_opt = array_ua.dot(array_ub.T) error = compute_error(new_a, new_b, u_opt, u_opt) return ProcrustesResult(new_a=new_a, new_b=new_b, array_u=u_opt, error=error) # Do regular two-sided orthogonal Procrustes calculations u_opt1, u_opt2 = _2sided(new_a, new_b) error = compute_error(new_a, new_b, u_opt1, u_opt2) return ProcrustesResult(new_a=new_a, new_b=new_b, array_p=u_opt1, array_q=u_opt2, error=error)
def generic(array_a, array_b, remove_zero_col=True, remove_zero_row=True, pad_mode='row-col', translate=False, scale=False, check_finite=True, weight=None): r""" Solve the generic right-sided Procrustes problem. The generic Procrustes solves the least squares optimization problem without any constraints. It assumed that each matrix has the same dimension, if not padding will occur. Parameters ---------- array_a : ndarray The 2d-array :math:`\mathbf{A}_{m \times n}` which is going to be transformed. array_b : ndarray The 2d-array :math:`\mathbf{B}_{m \times n}` representing the reference. remove_zero_col : bool, optional If True, the zero columns on the right side will be removed. Default=True. remove_zero_row : bool, optional If True, the zero rows on the top will be removed. Default=True. pad_mode : str, optional Specifying how to pad the arrays, listed below. Default="row-col". - "row" The array with fewer rows is padded with zero rows so that both have the same number of rows. - "col" The array with fewer columns is padded with zero columns so that both have the same number of columns. - "row-col" The array with fewer rows is padded with zero rows, and the array with fewer columns is padded with zero columns, so that both have the same dimensions. This does not necessarily result in square arrays. - "square" The arrays are padded with zero rows and zero columns so that they are both squared arrays. The dimension of square array is specified based on the highest dimension, i.e. :math:`\text{max}(n_a, m_a, n_b, m_b)`. translate : bool, optional If True, both arrays are translated to be centered at origin. Default=False. scale : bool, optional If True, both arrays are column normalized to unity. Default=False. check_finite : bool, optional If true, convert the input to an array, checking for NaNs or Infs. Default=True. weight : ndarray The weighting matrix. Default=None. Returns ------- res : ProcrustesResult Procrustes analysis result object. Attributes ---------- new_a : ndarray The transformed ndarray array_a. new_b : ndarray The transformed ndarray array_b. array_u : ndarray The optimum symmetric transformation array. error : float One-sided Procrustes error. Notes ----- Given a source matrix :math:`\mathbf{A} \in \mathbb{R}^{m \times n}` and a target matrix :math:`\mathbf{B} \in \mathbb{R}^{m \times n}`, find the transformation matrix :math:`\mathbf{X} \in \mathbb{R}^{n \times n}` for which :math:`\mathbf{AX}` is as close as possible to :math:`\mathbf{B}`. I.e., .. math:: \text{min} \quad \|\mathbf{A} \mathbf{X} - \mathbf{B}\|_{F}^2 The optimal transformation matrix :math:`\mathbf{X}` is given by .. math:: \mathbf{X} = {(\mathbf{A}^{\top}\mathbf{A})}^{-1} \mathbf{A}^{\top} \mathbf{B} References ---------- 1. Gower, J. C. Procrustes Methods. Wiley Interdisciplinary Reviews: Computational Statistics, 2(4), 503-508, 2010. """ # check inputs new_a, new_b = setup_input_arrays(array_a, array_b, remove_zero_col, remove_zero_row, pad_mode, translate, scale, check_finite, weight) # compute the generic solution a_inv = np.linalg.pinv(np.dot(new_a.T, new_a)) array_x = np.linalg.multi_dot([a_inv, new_a.T, new_b]) e_opt = compute_error(new_a, new_b, array_x) return ProcrustesResult(new_a=new_a, new_b=new_b, array_u=array_x, error=e_opt)
def softassign(array_a, array_b, iteration_soft=50, iteration_sink=200, beta_r=1.10, beta_f=1.e5, epsilon=0.05, epsilon_soft=1.e-3, epsilon_sink=1.e-3, k=0.15, gamma_scaler=1.01, n_stop=3, pad_mode='row-col', remove_zero_col=True, remove_zero_row=True, translate=False, scale=False, check_finite=True, adapted=True, beta_0=None, m_guess=None, iteration_anneal=None, weight=None): r""" Find the transformation matrix for 2-sided permutation Procrustes with softassign algorithm. Parameters ---------- array_a : ndarray The 2d-array :math:`\mathbf{A}_{m \times n}` which is going to be transformed. array_b : ndarray The 2d-array :math:`\mathbf{B}_{m \times n}` representing the reference. iteration_soft : int, optional Number of iterations for softassign loop. Default=50. iteration_sink : int, optional Number of iterations for Sinkhorn loop. Default=50. linear_cost_func : ndarray Linear cost function. Default=0. beta_r : float, optional Annealing rate which should greater than 1. Default=1.10. beta_f : float, optional The final inverse temperature. Default=1.e5. epsilon : float, optional The tolerance value for annealing loop. Default=0.05. epsilon_soft : float, optional The tolerance value used for softassign. Default=1.e-3. epsilon_sink : float, optional The tolerance value used for Sinkhorn loop. If adapted version is used, it will use the adapted tolerance value for Sinkhorn instead. Default=1.e-3. k : float, optional This parameter controls how much tighter the coverage threshold for the interior loop should be than the coverage threshold for the loops outside. It has be be within the integral :math:`\(0,1\)`. Default=0.15. gamma_scaler : float This parameter ensures the quadratic cost function including self-amplification positive define. Default=1.01. n_stop : int, optional Number of running steps after the calculation converges in the relaxation procedure. Default=10. pad_mode : str, optional Specifying how to pad the arrays, listed below. Default="row-col". - "row" The array with fewer rows is padded with zero rows so that both have the same number of rows. - "col" The array with fewer columns is padded with zero columns so that both have the same number of columns. - "row-col" The array with fewer rows is padded with zero rows, and the array with fewer columns is padded with zero columns, so that both have the same dimensions. This does not necessarily result in square arrays. - "square" The arrays are padded with zero rows and zero columns so that they are both squared arrays. The dimension of square array is specified based on the highest dimension, i.e. :math:`\text{max}(n_a, m_a, n_b, m_b)`. remove_zero_col : bool, optional If True, zero columns (values less than 1e-8) on the right side will be removed. Default=True. remove_zero_row : bool, optional If True, zero rows (values less than 1e-8) on the bottom will be removed. Default=True. translate : bool, optional If True, both arrays are translated to be centered at origin, ie columns of the arrays will have mean zero. Default=False. scale : bool, optional If True, both arrays are normalized to one with respect to the Frobenius norm, ie :math:`Tr(A^T A) = 1`. Default=False. check_finite : bool, optional If true, convert the input to an array, checking for NaNs or Infs. Default=True. adapted : bool, optional If adapted, this function will use the tighter covergence threshold for the interior loops. Default=True. beta_0 : float, optional Initial inverse temperature. Default=None. beta_f : float, optional Final inverse temperature. Default=None. m_guess : ndarray, optional The initial guess of the doubly-stochastic matrix. Default=None. iteration_anneal : int, optional Number of iterations for annealing loop. Default=None. weight : ndarray The weighting matrix. Default=None. Returns ------- res : ProcrustesResult Procrustes analysis result object. Attributes ---------- new_a : ndarray The transformed ndarray A. new_b : ndarray The transformed ndarray B. array_u : ndarray The optimum permutation transformation matrix. error : float Two-sided Procrustes error. Notes ----- Quadratic assignment problem (QAP) has played a very special but fundamental role in combinatorial optimization problems. The problem can be defined as a optimization problem to minimize the cost to assign a set of facilities to a set of locations. The cost is a function of the flow between the facilities and the geographical distances among various facilities. The objective function (also named loss function in machine learning) is defined as [1]_ .. math:: E_{qap}(M, \mu, \nu) = - \frac{1}{2}\Sigma_{aibj}C_{ai;bj}M_{ai}M_{bj} + \Sigma_{a}{\mu}_a (\Sigma_i M_{ai} -1) \\ + \Sigma_i {\nu}_i (\Sigma_i M_{ai} -1) - \frac{\gamma}{2}\Sigma_{ai} {M_{ai}}^2 + \frac{1}{\beta} \Sigma_{ai} M_{ai}\log{M_{ai}} where :math:`C_{ai,bj}` is the benefit matrix, :math:`M` is the desired :math:`N \times N` permutation matrix. :math:`E` is the energy function which comes along with a self-amplification term with `\gamma`, two Lagrange parameters :math:`\mu` and :math:`\nu` for constrained optimization and :math:`M_{ai} \log{M_{ai}}` servers as a barrier function which ensures positivity of :math:`M_{ai}`. The inverse temperature :math:`\beta` is a deterministic annealing control parameter. More detailed information about the algorithm can be referred to Rangarajan's paper and the details about the algorithm can be found at [2]_. References ---------- .. [1] Rangarajan, Anand and Yuille, Alan L and Gold, Steven and Mjolsness, Eric, "A convergence proof for the softassign quadratic assignment algorithm", Advances in Neural Information Processing Systems, page 620-626, 1997. .. [2] Stefan Roth, "Analysis of a Deterministic Annealing Method for Graph Matching and Quadratic Assignment", Ph.D. thesis, University of Mannheim, 2001 Examples -------- >>> import numpy as np >>> array_a = np.array([[4, 5, 3, 3], [5, 7, 3, 5], ... [3, 3, 2, 2], [3, 5, 2, 5]]) # define a random matrix >>> perm = np.array([[0., 0., 1., 0.], [1., 0., 0., 0.], ... [0., 0., 0., 1.], [0., 1., 0., 0.]]) # define array_b by permuting array_a >>> array_b = np.dot(perm.T, np.dot(array_a, perm)) >>> new_a, new_b, M_ai, error = softassign(array_a, array_b, ... remove_zero_col=False, ... remove_zero_row=False) >>> M_ai # the permutation matrix array([[0., 0., 1., 0.], [1., 0., 0., 0.], [0., 0., 0., 1.], [0., 1., 0., 0.]]) >>> error # the error 0.0 """ # pylint: disable-msg=too-many-arguments # pylint: disable-msg=too-many-branches # todo: add linear_cost_func with default value 0 # Check beta_r if beta_r <= 1: raise ValueError("Argument beta_r cannot be less than 1.") new_a, new_b = setup_input_arrays(array_a, array_b, remove_zero_col, remove_zero_row, pad_mode, translate, scale, check_finite, weight) # Initialization # Compute the benefit matrix array_c = np.kron(new_a, new_b) # Get the shape of A (B and the permutation matrix as well) row_num = new_a.shape[0] c_tensor = array_c.reshape(row_num, row_num, row_num, row_num) # Compute the beta_0 gamma = _compute_gamma(array_c, row_num, gamma_scaler) if beta_0 is None: c_gamma = array_c + gamma * (np.identity(row_num * row_num)) eival_gamma = np.amax(np.abs(np.linalg.eigvalsh(c_gamma))) beta_0 = gamma_scaler * max(1.e-10, eival_gamma / row_num) beta_0 = 1 / beta_0 else: beta_0 *= row_num beta = beta_0 # We will use iteration_anneal if provided even if the final inverse temperature is specified # iteration_anneal is not None, beta_f can be None or not if iteration_anneal is not None: beta_f = beta_0 * np.power(beta_r, iteration_anneal) * row_num # iteration_anneal is None and beta_f is not None elif iteration_anneal is None and beta_f is not None: beta_f *= row_num # Both iteration_anneal and beta_f are None else: raise ValueError( "We must specify at least one of iteration_anneal and beta_f and " "specify only one is recommended.") # Initialization of m_ai # check shape of m_guess if m_guess is not None: if np.any(m_guess < 0): raise ValueError( "The initial guess of permutation matrix cannot contain any negative values." ) if m_guess.shape[0] == row_num and m_guess.shape[1] == row_num: array_m = m_guess else: warnings.warn("The shape of m_guess does not match ({0}, {0})." "Use random initial guess instead.".format(row_num)) array_m = np.abs( np.random.normal(loc=1.0, scale=0.1, size=(row_num, row_num))) else: # m_relax_old = 1 / N + np.random.rand(N, N) array_m = np.abs( np.random.normal(loc=1.0, scale=0.1, size=(row_num, row_num))) array_m[array_m < 0] = 0 array_m = array_m / row_num nochange = 0 if adapted: epsilon_sink = epsilon_soft * k while beta < beta_f: # relaxation m_old_beta = deepcopy(array_m) # softassign loop for _ in np.arange(iteration_soft): m_old_soft = deepcopy(array_m) # Compute Z in relaxation step # C_gamma_tensor = C_gamma.reshape(N, N, N, N) # Z = -np.einsum('ijkl,jl->ik', C_gamma_tensor, M) # Z -= linear_cost_func array_z = np.einsum('aibj,bj->ai', c_tensor, array_m) array_z += gamma * array_m # soft_assign array_m = np.exp(beta * array_z) # Sinkhorn loop for _ in np.arange(iteration_sink): # Row normalization array_m = array_m / array_m.sum(axis=1, keepdims=1) # Column normalization array_m = array_m / array_m.sum(axis=0, keepdims=1) # Compute the delata_M_sink if np.amax(np.abs(array_m.sum(axis=1, keepdims=1) - 1)) < epsilon_sink: array_m = array_m / array_m.sum(axis=1, keepdims=1) break change_soft = np.amax(np.abs(array_m - m_old_soft)) # pylint: disable-msg=no-else-break if change_soft < epsilon_soft: break else: if adapted: epsilon_sink = change_soft * k else: continue change_annealing = np.amax(np.abs(array_m - m_old_beta)) if change_annealing < epsilon: nochange += 1 if nochange > n_stop: break else: nochange = 0 beta *= beta_r if adapted: epsilon_soft = change_soft * k epsilon_sink = epsilon_soft * k # Compute the error array_m = permutation(np.eye(array_m.shape[0]), array_m)["array_u"] error = compute_error(new_a, new_b, array_m, array_m) return ProcrustesResult(new_a=new_a, new_b=new_b, array_u=array_m, error=error)