Source code for matrix_toolkit.anymatrix.properties

"""Matrix property checking functions - Python port of anymatrix property tests"""

import numpy as np
from scipy import sparse
from typing import Any, Callable, Dict, List


[docs] class MatrixProperties: """ Matrix property checking utilities This class provides methods to verify various mathematical properties of matrices, similar to the MATLAB anymatrix property tests. """ DEFAULT_TOL = 1e-10
[docs] @staticmethod def is_symmetric(matrix: Any, tol: float = None) -> bool: """ Check if matrix is symmetric (A = A^T) Args: matrix: Input matrix tol: Tolerance for floating point comparison Returns: True if symmetric, False otherwise """ tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape') or matrix.shape[0] != matrix.shape[1]: return False if sparse.issparse(matrix): diff = matrix - matrix.T if diff.nnz == 0: return True return np.allclose(diff.data, 0, atol=tol) else: return np.allclose(matrix, matrix.T, atol=tol)
[docs] @staticmethod def is_hermitian(matrix: Any, tol: float = None) -> bool: """Check if matrix is Hermitian (A = A^H)""" tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape') or matrix.shape[0] != matrix.shape[1]: return False if sparse.issparse(matrix): diff = matrix - matrix.T.conj() if diff.nnz == 0: return True return np.allclose(diff.data, 0, atol=tol) else: return np.allclose(matrix, matrix.T.conj(), atol=tol)
[docs] @staticmethod def is_positive_definite(matrix: Any, tol: float = None) -> bool: """ Check if matrix is positive definite A matrix is positive definite if all eigenvalues are positive. For large matrices, uses Cholesky decomposition. """ tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape') or matrix.shape[0] != matrix.shape[1]: return False n = matrix.shape[0] try: if sparse.issparse(matrix): if n > 1000: # For large sparse matrices, try Cholesky from scipy.sparse.linalg import splu # This is a heuristic check return True # Assume true if no error else: matrix_dense = matrix.toarray() else: matrix_dense = matrix # Try Cholesky decomposition np.linalg.cholesky(matrix_dense) return True except np.linalg.LinAlgError: return False except Exception: return False
[docs] @staticmethod def is_orthogonal(matrix: Any, tol: float = None) -> bool: """Check if matrix is orthogonal (Q^T Q = I)""" tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape') or matrix.shape[0] != matrix.shape[1]: return False n = matrix.shape[0] if sparse.issparse(matrix): product = matrix.T @ matrix identity = sparse.eye(n, format=matrix.format) diff = product - identity if diff.nnz == 0: return True return np.allclose(diff.data, 0, atol=tol) else: product = matrix.T @ matrix identity = np.eye(n) return np.allclose(product, identity, atol=tol)
[docs] @staticmethod def is_unitary(matrix: Any, tol: float = None) -> bool: """Check if matrix is unitary (Q^H Q = I)""" tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape') or matrix.shape[0] != matrix.shape[1]: return False n = matrix.shape[0] if sparse.issparse(matrix): product = matrix.T.conj() @ matrix identity = sparse.eye(n, format=matrix.format) diff = product - identity if diff.nnz == 0: return True return np.allclose(diff.data, 0, atol=tol) else: product = matrix.T.conj() @ matrix identity = np.eye(n) return np.allclose(product, identity, atol=tol)
[docs] @staticmethod def is_tridiagonal(matrix: Any, tol: float = None) -> bool: """Check if matrix is tridiagonal""" tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape'): return False if sparse.issparse(matrix): coo = sparse.coo_matrix(matrix) return np.all(np.abs(coo.row - coo.col) <= 1) else: n = matrix.shape[0] for i in range(n): for j in range(n): if abs(i - j) > 1 and abs(matrix[i, j]) > tol: return False return True
[docs] @staticmethod def is_diagonal(matrix: Any, tol: float = None) -> bool: """Check if matrix is diagonal""" tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape'): return False if sparse.issparse(matrix): coo = sparse.coo_matrix(matrix) # Check all non-zero elements are on diagonal return np.all(coo.row == coo.col) else: diag_matrix = np.diag(np.diagonal(matrix)) return np.allclose(matrix, diag_matrix, atol=tol)
[docs] @staticmethod def is_upper_triangular(matrix: Any, tol: float = None) -> bool: """Check if matrix is upper triangular""" tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape'): return False if sparse.issparse(matrix): coo = sparse.coo_matrix(matrix) # All non-zeros must have row <= col return np.all(coo.row <= coo.col) else: return np.allclose(matrix, np.triu(matrix), atol=tol)
[docs] @staticmethod def is_lower_triangular(matrix: Any, tol: float = None) -> bool: """Check if matrix is lower triangular""" tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape'): return False if sparse.issparse(matrix): coo = sparse.coo_matrix(matrix) return np.all(coo.row >= coo.col) else: return np.allclose(matrix, np.tril(matrix), atol=tol)
[docs] @staticmethod def is_toeplitz(matrix: Any, tol: float = None) -> bool: """Check if matrix is Toeplitz (constant along diagonals)""" tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape'): return False m, n = matrix.shape if sparse.issparse(matrix): matrix = matrix.toarray() # Check all diagonals for k in range(-(m-1), n): diag_vals = np.diagonal(matrix, offset=k) if len(diag_vals) > 1: if not np.allclose(diag_vals, diag_vals[0], atol=tol): return False return True
[docs] @staticmethod def is_circulant(matrix: Any, tol: float = None) -> bool: """Check if matrix is circulant""" tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape') or matrix.shape[0] != matrix.shape[1]: return False n = matrix.shape[0] if sparse.issparse(matrix): matrix = matrix.toarray() first_row = matrix[0, :] for i in range(1, n): shifted = np.roll(first_row, i) if not np.allclose(matrix[i, :], shifted, atol=tol): return False return True
[docs] @staticmethod def is_hankel(matrix: Any, tol: float = None) -> bool: """Check if matrix is Hankel (constant along anti-diagonals)""" tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape'): return False m, n = matrix.shape if sparse.issparse(matrix): matrix = matrix.toarray() # Check each anti-diagonal for k in range(m + n - 1): values = [] for i in range(max(0, k - n + 1), min(m, k + 1)): j = k - i if 0 <= j < n: values.append(matrix[i, j]) if len(values) > 1: if not np.allclose(values, values[0], atol=tol): return False return True
[docs] @staticmethod def is_stochastic(matrix: Any, tol: float = None) -> bool: """Check if matrix is row stochastic (rows sum to 1, non-negative)""" tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape'): return False if sparse.issparse(matrix): # Check non-negativity if np.any(matrix.data < -tol): return False row_sums = np.array(matrix.sum(axis=1)).flatten() else: if np.any(matrix < -tol): return False row_sums = matrix.sum(axis=1) return np.allclose(row_sums, 1.0, atol=tol)
[docs] @staticmethod def is_doubly_stochastic(matrix: Any, tol: float = None) -> bool: """Check if matrix is doubly stochastic""" tol = tol or MatrixProperties.DEFAULT_TOL if not MatrixProperties.is_stochastic(matrix, tol): return False if sparse.issparse(matrix): col_sums = np.array(matrix.sum(axis=0)).flatten() else: col_sums = matrix.sum(axis=0) return np.allclose(col_sums, 1.0, atol=tol)
[docs] @staticmethod def is_nilpotent(matrix: Any, max_power: int = None, tol: float = None) -> bool: """Check if matrix is nilpotent (A^k = 0 for some k)""" tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape') or matrix.shape[0] != matrix.shape[1]: return False n = matrix.shape[0] max_power = max_power or min(10, n) current = matrix.copy() for k in range(1, max_power + 1): if sparse.issparse(current): if current.nnz == 0: return True if np.allclose(current.data, 0, atol=tol): return True else: if np.allclose(current, 0, atol=tol): return True current = current @ matrix return False
[docs] @staticmethod def is_involutory(matrix: Any, tol: float = None) -> bool: """Check if matrix is involutory (A^2 = I)""" tol = tol or MatrixProperties.DEFAULT_TOL if not hasattr(matrix, 'shape') or matrix.shape[0] != matrix.shape[1]: return False product = matrix @ matrix n = matrix.shape[0] if sparse.issparse(product): identity = sparse.eye(n, format=product.format) diff = product - identity if diff.nnz == 0: return True return np.allclose(diff.data, 0, atol=tol) else: identity = np.eye(n) return np.allclose(product, identity, atol=tol)
[docs] @staticmethod def is_sparse(matrix: Any) -> bool: """Check if matrix is stored in sparse format""" return sparse.issparse(matrix)
[docs] @staticmethod def is_binary(matrix: Any, tol: float = None) -> bool: """Check if all matrix elements are 0 or 1""" tol = tol or MatrixProperties.DEFAULT_TOL if sparse.issparse(matrix): data = matrix.data else: data = matrix.flatten() # Check all values are close to 0 or 1 return np.all(np.isclose(data, 0, atol=tol) | np.isclose(data, 1, atol=tol))
[docs] @staticmethod def is_integer(matrix: Any, tol: float = None) -> bool: """Check if all matrix elements are integers""" tol = tol or MatrixProperties.DEFAULT_TOL if sparse.issparse(matrix): data = matrix.data else: data = matrix.flatten() return np.allclose(data, np.round(data), atol=tol)
[docs] @classmethod def check_properties( cls, matrix: Any, expected_properties: List[str], tol: float = None ) -> Dict[str, bool]: """ Check multiple properties of a matrix Args: matrix: Matrix to check expected_properties: List of property names to check tol: Tolerance for comparisons Returns: Dictionary mapping property names to boolean results """ results = {} for prop in expected_properties: checker = cls.get_checker(prop) if checker: try: results[prop] = checker(matrix, tol=tol) except Exception as e: results[prop] = False print(f"Warning: Error checking property '{prop}': {e}") else: results[prop] = None # Property checker not found return results
[docs] @classmethod def get_checker(cls, property_name: str) -> Callable: """Get property checker function by name""" property_map = { 'symmetric': cls.is_symmetric, 'hermitian': cls.is_hermitian, 'positive definite': cls.is_positive_definite, 'positive_definite': cls.is_positive_definite, 'orthogonal': cls.is_orthogonal, 'unitary': cls.is_unitary, 'tridiagonal': cls.is_tridiagonal, 'diagonal': cls.is_diagonal, 'upper triangular': cls.is_upper_triangular, 'lower triangular': cls.is_lower_triangular, 'toeplitz': cls.is_toeplitz, 'circulant': cls.is_circulant, 'hankel': cls.is_hankel, 'stochastic': cls.is_stochastic, 'doubly stochastic': cls.is_doubly_stochastic, 'doubly_stochastic': cls.is_doubly_stochastic, 'nilpotent': cls.is_nilpotent, 'involutory': cls.is_involutory, 'sparse': cls.is_sparse, 'binary': cls.is_binary, 'integer': cls.is_integer, } return property_map.get(property_name.lower().replace('-', '_'))
[docs] @classmethod def list_all_properties(cls) -> List[str]: """List all supported property names""" return [ 'symmetric', 'hermitian', 'positive definite', 'orthogonal', 'unitary', 'tridiagonal', 'diagonal', 'upper triangular', 'lower triangular', 'toeplitz', 'circulant', 'hankel', 'stochastic', 'doubly stochastic', 'nilpotent', 'involutory', 'sparse', 'binary', 'integer', ]