Source code for mcnnm.core_utils

import jax
import jax.numpy as jnp
from jax import jit
from jax.numpy.linalg import norm

from .types import Array, Scalar

jax.config.update("jax_enable_x64", True)


[docs] @jit def is_positive_definite(mat: jnp.ndarray) -> bool: """ Check if a matrix is positive definite. Args: mat (jnp.ndarray): The input matrix to check. Returns: bool: True if the matrix is positive definite, False otherwise. This function checks if a matrix is positive definite by computing its eigenvalues and checking if they are all positive. """ # Compute the eigenvalues of the matrix eigenvalues = jnp.linalg.eigvalsh(mat) # Check if all eigenvalues are positive return jax.lax.cond(jnp.min(eigenvalues) > 0, lambda _: True, lambda _: False, operand=None)
[docs] @jit def mask_observed(A: Array, mask: Array) -> Array: r""" Projects the matrix A onto the observed entries specified by the binary mask. Corresponds to :math:`P_{\mathcal{O}}` in the paper. Args: A: The input matrix. mask: The binary mask matrix, where 1 indicates an observed entry and 0 indicates an unobserved entry. Returns: Array: The projected matrix. Raises: ValueError: If the shapes of A and mask do not match. .. math:: P_{\mathcal{O}}(A) = A \odot \text{mask} where :math:`\odot` denotes the element-wise product. """ if A.shape != mask.shape: raise ValueError(f"The shapes of A ({A.shape}) and mask ({mask.shape}) do not match.") return A * mask
[docs] @jit def mask_unobserved(A: Array, mask: Array) -> Array: r""" Projects the matrix A onto the unobserved entries specified by the binary mask. Corresponds to :math:`P_{\mathcal{O}}^\perp` in the paper. Args: A: The input matrix. mask: The binary mask matrix, where 1 indicates an observed entry and 0 indicates an unobserved entry. Returns: Array: The projected matrix. Raises: ValueError: If the shapes of A and mask do not match. .. math:: P_{\mathcal{O}}^\perp(A) = A \odot (\mathbf{1} - \text{mask}) where :math:`\odot` denotes the element-wise product and :math:`\mathbf{1}` is a matrix of 1s. """ if A.shape != mask.shape: raise ValueError(f"The shapes of A ({A.shape}) and mask ({mask.shape}) do not match.") return jnp.where(mask, jnp.zeros_like(A), A)
[docs] @jit def frobenius_norm(A: Array) -> Scalar: """ Computes the Frobenius norm of a matrix A. Args: A: The input matrix. Returns: Scalar: The Frobenius norm of the matrix A. Raises: ValueError: If the input is not a 2D array. """ if A.ndim != 2: raise ValueError("Input must be a 2D array.") return norm(A, ord="fro")
[docs] @jit def nuclear_norm(A: Array) -> Scalar: """ Computes the nuclear norm (sum of singular values) of a matrix A. Args: A: The input matrix. Returns: Scalar: The nuclear norm of the matrix A. Raises: ValueError: If the input is not a 2D array. """ if A.ndim != 2: raise ValueError("Input must be a 2D array.") _, s, _ = jnp.linalg.svd(A, full_matrices=False) return jnp.sum(s)
[docs] @jit def element_wise_l1_norm(A: Array) -> Scalar: """ Computes the element-wise L1 norm of a matrix A. Args: A: The input matrix. Returns: Scalar: The element-wise L1 norm of the matrix A. Raises: ValueError: If the input is not a 2D array. """ if A.ndim != 2: raise ValueError("Input must be a 2D array.") return jnp.sum(jnp.abs(A))
[docs] @jit def normalize(mat: Array) -> tuple[Array, Array]: """ Normalize the columns of the input matrix. Return the normalized matrix and the column norms. """ if mat.size == 0: col_norms = jnp.zeros(mat.shape[1]) mat_norm = jnp.zeros_like(mat) else: epsilon = 1e-10 col_norms = jnp.linalg.norm(mat, axis=0) + epsilon mat_norm = mat / col_norms return mat_norm, col_norms
[docs] @jit def normalize_back(mat: Array, row_scales: Array, col_scales: Array) -> Array: """ Rescale the rows and columns of the matrix H using the provided scales. """ mat_new = mat.copy() if row_scales.size > 0: mat_new /= row_scales[:, None] if col_scales.size > 0: mat_new /= col_scales[None, :] return mat_new