Source code for CodeEntropy.levels.linalg

"""Matrix utilities used across covariance and entropy calculations.

This module contains small, focused helpers for matrix construction and cleanup.
All functions are pure (no side effects beyond logging) and operate on NumPy
arrays.

Key behaviors:
- `create_submatrix` computes a 3x3 outer-product block for two 3-vectors.
- `filter_zero_rows_columns` removes rows/columns that are all (near) zero.
"""

from __future__ import annotations

import logging

import numpy as np

logger = logging.getLogger(__name__)


[docs] class MatrixUtils: """Utility operations for small matrix manipulations."""
[docs] def create_submatrix(self, data_i: np.ndarray, data_j: np.ndarray) -> np.ndarray: """Create a 3x3 covariance-style submatrix from two 3-vectors. This computes the outer product of `data_i` and `data_j`: submatrix = outer(data_i, data_j) Args: data_i: Vector of shape (3,) representing bead i values. data_j: Vector of shape (3,) representing bead j values. Returns: A (3, 3) NumPy array corresponding to the outer product. Raises: ValueError: If either input cannot be reshaped to (3,). """ v_i = np.asarray(data_i, dtype=float).reshape(-1) v_j = np.asarray(data_j, dtype=float).reshape(-1) if v_i.shape[0] != 3 or v_j.shape[0] != 3: raise ValueError( f"Expected 3-vectors for outer product, got {v_i.shape} " f"and {v_j.shape}." ) submatrix = np.outer(v_i, v_j) logger.debug(f"Submatrix: {submatrix}") return submatrix
[docs] def filter_zero_rows_columns( self, matrix: np.ndarray, atol: float = 0.0 ) -> np.ndarray: """Remove rows and columns that are entirely (near) zero. A row (or column) is removed if all entries are close to zero according to `np.isclose(..., atol=atol)`. Args: matrix: Input 2D array. atol: Absolute tolerance used to determine "zero". Defaults to 0.0. Returns: A new matrix with all-zero rows and columns removed. If no such rows or columns exist, returns a view/copy of the original with consistent NumPy typing. Raises: ValueError: If `matrix` is not 2D. """ mat = np.asarray(matrix, dtype=float) if mat.ndim != 2: raise ValueError(f"Expected a 2D matrix, got ndim={mat.ndim}.") init_shape = mat.shape row_mask = self._nonzero_row_mask(mat, atol=atol) mat = mat[row_mask, :] col_mask = self._nonzero_col_mask(mat, atol=atol) mat = mat[:, col_mask] final_shape = mat.shape if init_shape != final_shape: logger.debug( f"Matrix shape changed {init_shape}" f"-> {final_shape} after removing zero rows/cols." ) logger.debug(f"Filtered matrix: {mat}") return mat
@staticmethod def _nonzero_row_mask(matrix: np.ndarray, atol: float) -> np.ndarray: """Return a boolean mask selecting rows that are not all (near) zero.""" is_zero_row = np.all(np.isclose(matrix, 0.0, atol=atol), axis=1) return ~is_zero_row @staticmethod def _nonzero_col_mask(matrix: np.ndarray, atol: float) -> np.ndarray: """Return a boolean mask selecting columns that are not all (near) zero.""" is_zero_col = np.all(np.isclose(matrix, 0.0, atol=atol), axis=0) return ~is_zero_col