Source code for popcor.utils.common

"""Common utilities for symmetric matrix vectorization and sparsity handling."""

import itertools
from typing import Any, Dict, List, Sequence, Tuple

import numpy as np
import scipy.sparse as sp


[docs] def upper_triangular(p: np.ndarray) -> np.ndarray: """Return the vectorized upper-triangular part of the outer product of p.""" return np.outer(p, p)[np.triu_indices(len(p))]
[docs] def diag_indices(n: int) -> np.ndarray: """Return indices of diagonal elements in the vectorized upper-triangular matrix.""" z = np.empty((n, n)) z[np.triu_indices(n)] = range(int(n * (n + 1) / 2)) return np.diag(z).astype(int)
[docs] def get_aggregate_sparsity(matrix_list_sparse: Sequence[sp.spmatrix]) -> sp.csr_matrix: """Aggregate sparsity pattern from a list of sparse matrices.""" agg_ii: List[int] = [] agg_jj: List[int] = [] for A_sparse in matrix_list_sparse: assert isinstance(A_sparse, sp.spmatrix) ii, jj = A_sparse.nonzero() # type: ignore agg_ii += list(ii) agg_jj += list(jj) return sp.csr_matrix(([1.0] * len(agg_ii), (agg_ii, agg_jj)), A_sparse.shape)
[docs] def unravel_multi_index_triu( flat_indices: Sequence[int], shape: Tuple[int, int] ) -> Tuple[np.ndarray, np.ndarray]: """Convert flat indices to (i, j) indices for upper-triangular part of a matrix.""" i_upper: List[int] = [] j_upper: List[int] = [] cutoffs = np.cumsum(list(range(1, shape[0] + 1))[::-1]) for idx in flat_indices: i = np.where(idx < cutoffs)[0][0] if i == 0: j = idx else: j = idx - cutoffs[i - 1] + i i_upper.append(i) j_upper.append(j) return np.array(i_upper), np.array(j_upper)
[docs] def ravel_multi_index_triu( index_tuple: Tuple[np.ndarray, np.ndarray], shape: Tuple[int, int] ) -> List[int]: """Convert (i, j) indices to flat indices for upper-triangular part of a matrix.""" ii, jj = index_tuple triu_mask = jj >= ii i_upper = ii[triu_mask] j_upper = jj[triu_mask] flat_indices: List[int] = [] for i, j in zip(i_upper, j_upper): idx = np.sum(range(shape[0] - i, shape[0])) + j flat_indices.append(idx) return flat_indices
[docs] def create_symmetric( vec: np.ndarray | sp.csr_matrix | sp.csc_matrix, eps_sparse: float, correct: bool = False, sparse: bool = False, ) -> np.ndarray | sp.csr_array: """Create a symmetric matrix from the vectorized upper-triangular elements.""" def get_dim_x(len_vec: int) -> int: return int(0.5 * (-1 + np.sqrt(1 + 8 * len_vec))) if isinstance(vec, np.ndarray): len_vec = len(vec) dim_x = get_dim_x(len_vec) triu = np.triu_indices(n=dim_x) mask = np.abs(vec) > eps_sparse triu_i_nnz = triu[0][mask] triu_j_nnz = triu[1][mask] vec_nnz = vec[mask] else: len_vec = vec.shape[1] dim_x = get_dim_x(len_vec) vec.data[np.abs(vec.data) < eps_sparse] = 0 vec.eliminate_zeros() ii, jj = vec.nonzero() triu_i_nnz, triu_j_nnz = unravel_multi_index_triu(jj, (dim_x, dim_x)) vec_nnz = np.array(vec[ii, jj]).flatten() if sparse: offdiag = triu_i_nnz != triu_j_nnz diag = triu_i_nnz == triu_j_nnz triu_i = triu_i_nnz[offdiag] triu_j = triu_j_nnz[offdiag] diag_i = triu_i_nnz[diag] if correct: vec_nnz_off = vec_nnz[offdiag] / np.sqrt(2) else: vec_nnz_off = vec_nnz[offdiag] vec_nnz_diag = vec_nnz[diag] Ai = sp.csr_array( ( np.r_[vec_nnz_diag, vec_nnz_off, vec_nnz_off], (np.r_[diag_i, triu_i, triu_j], np.r_[diag_i, triu_j, triu_i]), ), (dim_x, dim_x), dtype=float, ) else: Ai = np.zeros((dim_x, dim_x)) if correct: Ai[triu_i_nnz, triu_j_nnz] = vec_nnz / np.sqrt(2) Ai[triu_j_nnz, triu_i_nnz] = vec_nnz / np.sqrt(2) Ai[range(dim_x), range(dim_x)] *= np.sqrt(2) else: Ai[triu_i_nnz, triu_j_nnz] = vec_nnz Ai[triu_j_nnz, triu_i_nnz] = vec_nnz return Ai
[docs] def get_vec( mat: np.ndarray | sp.csc_matrix | sp.csr_matrix, correct: bool = True, sparse: bool = False, ) -> np.ndarray | sp.csr_matrix: """Convert NxN symmetric matrix to vectorized upper-triangular form preserving inner product.""" from copy import deepcopy mat = deepcopy(mat) if correct: if isinstance(mat, (sp.csc_matrix, sp.csr_matrix)): ii, jj = mat.nonzero() mat[ii, jj] *= np.sqrt(2.0) diag = ii == jj mat[ii[diag], jj[diag]] /= np.sqrt(2) # type: ignore else: mat *= np.sqrt(2.0) mat[range(mat.shape[0]), range(mat.shape[0])] /= np.sqrt(2) if sparse: assert isinstance(mat, sp.csc_matrix) ii, jj = mat.nonzero() if len(ii) == 0: # got an empty matrix -- this can happen depending on the parameter values. raise ValueError triu_mask = jj >= ii flat_indices = ravel_multi_index_triu([ii[triu_mask], jj[triu_mask]], mat.shape) # type: ignore data = np.array(mat[ii[triu_mask], jj[triu_mask]]).flatten() # type: ignore vec_size = int(mat.shape[0] * (mat.shape[0] + 1) / 2) # type: ignore return sp.csr_matrix( (data, ([0] * len(flat_indices), flat_indices)), (1, vec_size) ) else: return np.array(mat[np.triu_indices(n=mat.shape[0])]).flatten() # type: ignore
[docs] def get_labels(p: str, zi: str, zj: str, var_dict: Dict[str, int]) -> List[str]: """Generate labels for matrix/vector elements based on variable sizes.""" labels: List[str] = [] size_i = var_dict[zi] size_j = var_dict[zj] if zi == zj: key_pairs = itertools.combinations_with_replacement(range(size_i), 2) else: key_pairs = itertools.product(range(size_i), range(size_j)) for i, j in key_pairs: label = f"{p}-" label += f"{zi}:{i}." if size_i > 1 else f"{zi}." label += f"{zj}:{j}" if size_j > 1 else f"{zj}" labels.append(label) return labels