From 13425a3646b5fa339e834a6f18a949edfca041b9 Mon Sep 17 00:00:00 2001 From: Jayaram Kancherla Date: Fri, 25 Oct 2024 09:42:24 -0700 Subject: [PATCH 1/2] Add Named Matrices --- src/biocutils/NamedDenseMatrix.py | 190 +++++++++++++++++ src/biocutils/NamedSparseMatrix.py | 317 +++++++++++++++++++++++++++++ src/biocutils/__init__.py | 3 + tests/test_NamedDenseMatrix.py | 109 ++++++++++ tests/test_NamedSparseMatrix.py | 161 +++++++++++++++ 5 files changed, 780 insertions(+) create mode 100644 src/biocutils/NamedDenseMatrix.py create mode 100644 src/biocutils/NamedSparseMatrix.py create mode 100644 tests/test_NamedDenseMatrix.py create mode 100644 tests/test_NamedSparseMatrix.py diff --git a/src/biocutils/NamedDenseMatrix.py b/src/biocutils/NamedDenseMatrix.py new file mode 100644 index 0000000..9ce9de9 --- /dev/null +++ b/src/biocutils/NamedDenseMatrix.py @@ -0,0 +1,190 @@ +import numpy as np +from typing import Optional, List, Union + + +class NamedDenseMatrix(np.matrix): + """ + A matrix class that extends numpy.matrix to include named rows and columns. + All numpy.matrix operations are preserved while adding the ability to reference + and operate on the matrix using row and column names. + """ + + def __new__(cls, input_array, row_names=None, column_names=None): + obj = np.asarray(input_array).view(cls) + + obj.row_names = list(row_names) if row_names is not None else None + obj.column_names = list(column_names) if column_names is not None else None + + # Validate dimensions if names are provided + if obj.row_names is not None and len(obj.row_names) != obj.shape[0]: + raise ValueError("Number of row names must match number of rows") + + if obj.column_names is not None and len(obj.column_names) != obj.shape[1]: + raise ValueError("Number of column names must match number of columns") + + return obj + + def __array_finalize__(self, obj): + if obj is None: + return + self.row_names = getattr(obj, "row_names", None) + self.column_names = getattr(obj, "column_names", None) + + def __getitem__(self, key): + # Handle string-based indexing when names exist + if isinstance(key, tuple): + row_key, col_key = key + row_idx = self._process_key(row_key, self.row_names) + col_idx = self._process_key(col_key, self.column_names) + result = super().__getitem__((row_idx, col_idx)) + else: + row_idx = self._process_key(key, self.row_names) + result = super().__getitem__(row_idx) + col_idx = slice(None) # Full slice for columns when only row indexing + + # Preserve names for slice operations + if isinstance(result, NamedDenseMatrix): + # Handle row names + if self.row_names is not None: + if isinstance(row_idx, slice): + result.row_names = self.row_names[row_idx] + elif isinstance(row_idx, list): + result.row_names = [self.row_names[i] for i in row_idx] + + # Handle column names + if self.column_names is not None: + if isinstance(col_idx, slice): + result.column_names = self.column_names[col_idx] + elif isinstance(col_idx, list): + if isinstance(col_idx[0], str): + # If we used string names for indexing, use those directly + result.column_names = col_idx + else: + # If we used integer indices, get the corresponding names + result.column_names = [self.column_names[i] for i in col_idx] + + return result + + def _process_key(self, key, names): + if names is None or not isinstance(key, (str, list)): + return key + + if isinstance(key, str): + try: + return names.index(key) + except ValueError: + raise KeyError(f"Name '{key}' not found") + elif isinstance(key, list) and all(isinstance(k, str) for k in key): + return [names.index(k) for k in key] + + return key + + def get_value(self, row_key: Union[int, str], col_key: Union[int, str]): + """Get a single value using row and column keys (names or indices). + + Args: + row_key: + Row name or index to access. + + col_key: + Column name or index to access. + + Returns: + A slice of the ndarray for the given row and column. + """ + row_idx = self._process_key(row_key, self.row_names) if isinstance(row_key, str) else row_key + col_idx = self._process_key(col_key, self.column_names) if isinstance(col_key, str) else col_key + return self[row_idx, col_idx] + + def set_value(self, row_key, col_key, value): + """Set a single value using row and column keys (names or indices). + + Args: + row_key: + Row name or index to set. + + col_key: + Column name or index to set. + + value: + The value to set. + """ + row_idx = self._process_key(row_key, self.row_names) if isinstance(row_key, str) else row_key + col_idx = self._process_key(col_key, self.column_names) if isinstance(col_key, str) else col_key + self[row_idx, col_idx] = value + + def __str__(self): + """Pretty print the matrix with row and column names if they exist.""" + # Calculate column widths for values + val_widths = [[len(f"{val:.6g}") for val in row] for row in self.A] + + # Calculate name widths if names exist + col_name_widths = ( + [len(str(name)) for name in self.column_names] if self.column_names is not None else [0] * self.shape[1] + ) + row_name_width = ( + max(len(str(name)) for name in self.row_names) + if self.row_names is not None + else len(str(self.shape[0] - 1)) + ) + + # Get maximum width for each column + col_widths = [ + max(name_width, max(col_widths)) for name_width, col_widths in zip(col_name_widths, zip(*val_widths)) + ] + + # Build the string representation + lines = [] + + # Header with column names or indices + header = " " * row_name_width + " | " + if self.column_names is not None: + header += " ".join(f"{name:<{width}}" for name, width in zip(self.column_names, col_widths)) + else: + header += " ".join(f"{i:<{width}}" for i, width in enumerate(col_widths)) + lines.append(header) + + # Separator line + separator = "-" * row_name_width + "-+-" + "-".join("-" * width for width in col_widths) + lines.append(separator) + + # Data rows + for i, row in enumerate(self.A): + row_name = self.row_names[i] if self.row_names is not None else str(i) + line = f"{str(row_name):<{row_name_width}} | " + line += " ".join(f"{val:>{width}.6g}" for val, width in zip(row, col_widths)) + lines.append(line) + + return "\n".join(lines) + + def set_rows(self, names: Optional[List]): + """Set row names. + + Args: + names: + List of names to set for rows. + Pass None to remove row names. + """ + if names is not None and len(names) != self.shape[0]: + raise ValueError("Number of names must match number of rows") + self.row_names = list(names) if names is not None else None + + def set_columns(self, names: Optional[List]): + """Set column names. + + Args: + names: + List of names to set for columns. + Pass None to remove column names. + """ + if names is not None and len(names) != self.shape[1]: + raise ValueError("Number of names must match number of columns") + self.column_names = list(names) if names is not None else None + + def get_rows(self): + """Get row names""" + return self.row_names + + def get_columns(self): + """Get column names.""" + return self.column_names diff --git a/src/biocutils/NamedSparseMatrix.py b/src/biocutils/NamedSparseMatrix.py new file mode 100644 index 0000000..eb22d59 --- /dev/null +++ b/src/biocutils/NamedSparseMatrix.py @@ -0,0 +1,317 @@ +from typing import List, Optional, Tuple, Union, Dict + +import numpy as np +import scipy.sparse as sp +from scipy.sparse import spmatrix + + +class NamedSparseMatrix: + """ + A wrapper class for scipy sparse matrices that adds optional row and column naming + capabilities while preserving all sparse matrix operations and format-specific optimizations. + """ + + def __init__( + self, + matrix: spmatrix, + row_names: Optional[List[str]] = None, + column_names: Optional[List[str]] = None, + ): + """ + Initialize with a scipy sparse matrix and optional row/column names. + + Args: + matrix: + Any scipy sparse matrix. + + row_names: + List of row names. If None, will use indices. + + column_names: + List of column names. If None, will use indices. + """ + self._matrix = matrix + self.row_names = list(row_names) if row_names is not None else None + self.column_names = list(column_names) if column_names is not None else None + + # Validate dimensions if names are provided + if self.row_names is not None and len(self.row_names) != matrix.shape[0]: + raise ValueError("Number of row names must match number of rows") + + if self.column_names is not None and len(self.column_names) != matrix.shape[1]: + raise ValueError("Number of column names must match number of columns") + + self._row_to_idx: Dict[str, int] = ( + {name: idx for idx, name in enumerate(self.row_names)} + if self.row_names is not None + else {} + ) + self._col_to_idx: Dict[str, int] = ( + {name: idx for idx, name in enumerate(self.column_names)} + if self.column_names is not None + else {} + ) + + @property + def shape(self) -> Tuple[int, int]: + return self._matrix.shape + + @property + def nnz(self) -> int: + return self._matrix.nnz + + @property + def dtype(self): + return self._matrix.dtype + + def _process_key(self, key, name_to_idx): + """Convert string keys to integer indices when names exist.""" + if not name_to_idx or not isinstance(key, (str, list)): + return key + + if isinstance(key, str): + return name_to_idx[key] + elif isinstance(key, list) and all(isinstance(k, str) for k in key): + return [name_to_idx[k] for k in key] + + return key + + def __getitem__(self, key): + """Support both integer and name-based indexing.""" + if isinstance(key, tuple): + row_key, col_key = key + row_idx = self._process_key(row_key, self._row_to_idx) + col_idx = self._process_key(col_key, self._col_to_idx) + result = self._matrix[row_idx, col_idx] + else: + row_idx = self._process_key(key, self._row_to_idx) + result = self._matrix[row_idx] + + # If result is a matrix, wrap it with names + if sp.issparse(result): + # Get the new names based on the indices + new_row_names = None + new_column_names = None + + if self.row_names is not None: + if isinstance(row_idx, slice): + new_row_names = self.row_names[row_idx] + elif isinstance(row_idx, list): + new_row_names = [self.row_names[i] for i in row_idx] + + if self.column_names is not None: + if isinstance(col_idx, slice): + new_column_names = self.column_names[col_idx] + elif isinstance(col_idx, list): + new_column_names = [self.column_names[i] for i in col_idx] + + return NamedSparseMatrix(result, new_row_names, new_column_names) + + return result + + def get_value(self, row_key, col_key): + """Get a single value using row and column keys (names or indices). + + Args: + row_key: + Row name or index to access. + + col_key: + Column name or index to access. + + Returns: + A slice of the ndarray for the given row and column. + """ + row_idx = ( + self._process_key(row_key, self._row_to_idx) + if isinstance(row_key, str) + else row_key + ) + col_idx = ( + self._process_key(col_key, self._col_to_idx) + if isinstance(col_key, str) + else col_key + ) + return self._matrix[row_idx, col_idx] + + def set_value(self, row_key, col_key, value): + """Set a single value using row and column keys (names or indices). + + Args: + row_key: + Row name or index to set. + + col_key: + Column name or index to set. + """ + row_idx = ( + self._process_key(row_key, self._row_to_idx) + if isinstance(row_key, str) + else row_key + ) + col_idx = ( + self._process_key(col_key, self._col_to_idx) + if isinstance(col_key, str) + else col_key + ) + self._matrix[row_idx, col_idx] = value + + def set_rows(self, names: Optional[List[str]]): + """Set row names. + + Args: + names: + List of names to set for columns. + Pass None to remove column names. + """ + if names is not None and len(names) != self.shape[0]: + raise ValueError("Number of names must match number of rows") + + self.row_names = list(names) if names is not None else None + self._row_to_idx = ( + {name: idx for idx, name in enumerate(self.row_names)} + if names is not None + else {} + ) + + def set_columns(self, names: Optional[List[str]]): + """Set column names. + + Args: + names: + List of names to set for columns. + Pass None to remove column names. + """ + if names is not None and len(names) != self.shape[1]: + raise ValueError("Number of names must match number of columns") + + self.column_names = list(names) if names is not None else None + self._col_to_idx = ( + {name: idx for idx, name in enumerate(self.column_names)} + if names is not None + else {} + ) + + def __str__(self): + """Pretty print the sparse matrix with row and column names if they exist.""" + # Convert to dense for small matrices, otherwise show summary + if self.shape[0] * self.shape[1] < 1000: # arbitrary threshold + dense = self._matrix.todense() + + # Calculate column widths + val_widths = [[len(f"{val:.6g}") for val in row] for row in dense.A] + + # Calculate name widths if names exist + col_name_widths = ( + [len(str(name)) for name in self.column_names] + if self.column_names is not None + else [len(str(i)) for i in range(self.shape[1])] + ) + row_name_width = ( + max(len(str(name)) for name in self.row_names) + if self.row_names is not None + else len(str(self.shape[0] - 1)) + ) + + # Get maximum width for each column + col_widths = [ + max(name_width, max(col_widths)) + for name_width, col_widths in zip(col_name_widths, zip(*val_widths)) + ] + + # Build the string representation + lines = [] + + # Header with column names or indices + header = " " * row_name_width + " | " + names = ( + self.column_names + if self.column_names is not None + else range(self.shape[1]) + ) + header += " ".join( + f"{name:<{width}}" for name, width in zip(names, col_widths) + ) + lines.append(header) + + # Separator line + separator = ( + "-" * row_name_width + + "-+-" + + "-".join("-" * width for width in col_widths) + ) + lines.append(separator) + + # Data rows + for i, row in enumerate(dense.A): + row_name = self.row_names[i] if self.row_names is not None else str(i) + line = f"{str(row_name):<{row_name_width}} | " + line += " ".join( + f"{val:>{width}.6g}" for val, width in zip(row, col_widths) + ) + lines.append(line) + + return "\n".join(lines) + else: + return ( + f"" + ) + + def __repr__(self): + return self.__str__() + + # Delegate all common matrix operations to the underlying sparse matrix + def tocsr(self): + return NamedSparseMatrix( + self._matrix.tocsr(), self.row_names, self.column_names + ) + + def tocsc(self): + return NamedSparseMatrix( + self._matrix.tocsc(), self.row_names, self.column_names + ) + + def tocoo(self): + return NamedSparseMatrix( + self._matrix.tocoo(), self.row_names, self.column_names + ) + + def todense(self): + return self._matrix.todense() + + def toarray(self): + return self._matrix.toarray() + + def transpose(self): + return NamedSparseMatrix( + self._matrix.transpose(), self.column_names, self.row_names + ) + + def __add__(self, other): + if isinstance(other, NamedSparseMatrix): + other = other._matrix + return NamedSparseMatrix( + self._matrix + other, self.row_names, self.column_names + ) + + def __mul__(self, other): + if isinstance(other, NamedSparseMatrix): + other = other._matrix + + result = self._matrix * other + if sp.issparse(result): + return NamedSparseMatrix(result, self.row_names, self.column_names) + return result + + def __rmul__(self, other): + result = other * self._matrix + if sp.issparse(result): + return NamedSparseMatrix(result, self.row_names, self.column_names) + return result + + @classmethod + def from_coo(cls, row, col, data, shape=None, row_names=None, column_names=None): + """Create a NamedSparseMatrix from COO format data.""" + matrix = sp.coo_matrix((data, (row, col)), shape=shape) + return cls(matrix, row_names, column_names) diff --git a/src/biocutils/__init__.py b/src/biocutils/__init__.py index ec4921b..53c964a 100644 --- a/src/biocutils/__init__.py +++ b/src/biocutils/__init__.py @@ -58,3 +58,6 @@ from .get_height import get_height from .is_high_dimensional import is_high_dimensional + +from .NamedDenseMatrix import NamedDenseMatrix +from .NamedSparseMatrix import NamedSparseMatrix \ No newline at end of file diff --git a/tests/test_NamedDenseMatrix.py b/tests/test_NamedDenseMatrix.py new file mode 100644 index 0000000..1dc038c --- /dev/null +++ b/tests/test_NamedDenseMatrix.py @@ -0,0 +1,109 @@ +import pytest +import numpy as np +import scipy.sparse as sp + +from biocutils import NamedDenseMatrix, NamedSparseMatrix + + +def test_initialization(): + """Test different initialization scenarios.""" + # Basic initialization + data = [[1, 2], [3, 4]] + mat = NamedDenseMatrix(data) + assert mat.shape == (2, 2) + assert mat.row_names is None + assert mat.column_names is None + + # With names + mat = NamedDenseMatrix(data, row_names=["r1", "r2"], column_names=["c1", "c2"]) + assert mat.row_names == ["r1", "r2"] + assert mat.column_names == ["c1", "c2"] + + # Mixed naming + mat = NamedDenseMatrix(data, row_names=["r1", "r2"]) + assert mat.row_names == ["r1", "r2"] + assert mat.column_names is None + + # Invalid dimensions + with pytest.raises(ValueError): + NamedDenseMatrix(data, row_names=["r1"]) + with pytest.raises(ValueError): + NamedDenseMatrix(data, column_names=["c1"]) + + +def test_indexing(): + """Test different indexing methods.""" + data = [[1, 2, 3], [4, 5, 6]] + mat = NamedDenseMatrix(data, row_names=["r1", "r2"], column_names=["c1", "c2", "c3"]) + + # Integer indexing + assert mat[0, 0] == 1 + assert mat[1, 2] == 6 + + # Name indexing + assert mat["r1", "c1"] == 1 + assert mat["r2", "c3"] == 6 + + # Mixed indexing + assert mat[0, "c2"] == 2 + assert mat["r2", 1] == 5 + + # Invalid names + with pytest.raises(KeyError): + mat["invalid", "c1"] + + # Slicing + sub_mat = mat[0:2, ["c1", "c2"]] + # assert isinstance(sub_mat, NamedDenseMatrix) + assert sub_mat.shape == (2, 2) + assert sub_mat.row_names == ["r1", "r2"] + assert sub_mat.column_names == ["c1", "c2"] + + +# def test_operations(): +# """Test mathematical operations.""" +# data = [[1, 2], [3, 4]] +# mat = NamedDenseMatrix(data, row_names=["r1", "r2"], column_names=["c1", "c2"]) + +# # Multiplication +# result = mat * 2 +# assert isinstance(result, NamedDenseMatrix) +# assert result.row_names == mat.row_names +# assert result.column_names == mat.column_names +# assert np.array_equal(result.A, np.array(data) * 2) + +# # Matrix multiplication +# result = mat @ mat +# assert isinstance(result, NamedDenseMatrix) +# assert result.row_names == mat.row_names +# assert result.column_names == mat.column_names + +# # Addition +# result = mat + mat +# assert isinstance(result, NamedDenseMatrix) +# assert result.row_names == mat.row_names +# assert result.column_names == mat.column_names +# assert np.array_equal(result.A, np.array(data) * 2) + + +# def test_rename(): +# """Test renaming functionality.""" +# mat = NamedDenseMatrix([[1, 2], [3, 4]]) + +# # Add names +# mat.set_rows(["r1", "r2"]) +# mat.set_columns(["c1", "c2"]) +# assert mat.row_names == ["r1", "r2"] +# assert mat.column_names == ["c1", "c2"] + +# # Remove names +# mat.set_rows(None) +# mat.set_columns(None) +# assert mat.row_names is None +# assert mat.column_names is None + +# # Invalid renaming +# with pytest.raises(ValueError): +# mat.set_rows(["single"]) +# with pytest.raises(ValueError): +# mat.set_columns(["single"]) diff --git a/tests/test_NamedSparseMatrix.py b/tests/test_NamedSparseMatrix.py new file mode 100644 index 0000000..00ccb2a --- /dev/null +++ b/tests/test_NamedSparseMatrix.py @@ -0,0 +1,161 @@ +import pytest +import numpy as np +import scipy.sparse as sp + +from biocutils import NamedSparseMatrix + + +def sparse_data(): + """Create sample sparse matrix data.""" + return sp.csr_matrix([[1, 0, 2], [0, 3, 0], [4, 0, 5]]) + + +def test_initialization(): + """Test different initialization scenarios.""" + # Basic initialization + mat = NamedSparseMatrix(sparse_data()) + assert mat.shape == (3, 3) + assert mat.row_names is None + assert mat.column_names is None + + # With names + mat = NamedSparseMatrix( + sparse_data(), row_names=["r1", "r2", "r3"], column_names=["c1", "c2", "c3"] + ) + assert mat.row_names == ["r1", "r2", "r3"] + assert mat.column_names == ["c1", "c2", "c3"] + + # Mixed naming + mat = NamedSparseMatrix(sparse_data(), row_names=["r1", "r2", "r3"]) + assert mat.row_names == ["r1", "r2", "r3"] + assert mat.column_names is None + + # Invalid dimensions + with pytest.raises(ValueError): + NamedSparseMatrix(sparse_data(), row_names=["r1"]) + with pytest.raises(ValueError): + NamedSparseMatrix(sparse_data(), column_names=["c1"]) + + +def test_format_conversion(): + """Test conversion between sparse formats.""" + mat = NamedSparseMatrix( + sparse_data(), row_names=["r1", "r2", "r3"], column_names=["c1", "c2", "c3"] + ) + + # Test CSR conversion + csr_mat = mat.tocsr() + assert isinstance(csr_mat, NamedSparseMatrix) + assert csr_mat.row_names == mat.row_names + assert csr_mat.column_names == mat.column_names + + # Test CSC conversion + csc_mat = mat.tocsc() + assert isinstance(csc_mat, NamedSparseMatrix) + assert csc_mat.row_names == mat.row_names + assert csc_mat.column_names == mat.column_names + + # Test COO conversion + coo_mat = mat.tocoo() + assert isinstance(coo_mat, NamedSparseMatrix) + assert coo_mat.row_names == mat.row_names + assert coo_mat.column_names == mat.column_names + + +def test_indexing(): + """Test different indexing methods.""" + mat = NamedSparseMatrix( + sparse_data(), row_names=["r1", "r2", "r3"], column_names=["c1", "c2", "c3"] + ) + + # Integer indexing + assert mat[0, 0] == 1 + assert mat[1, 1] == 3 + + # Name indexing + assert mat["r1", "c1"] == 1 + assert mat["r2", "c2"] == 3 + + # Mixed indexing + assert mat[0, "c2"] == 0 + assert mat["r2", 1] == 3 + + # Invalid names + with pytest.raises(KeyError): + mat["invalid", "c1"] + + # Slicing + sub_mat = mat[0:2, ["c1", "c2"]] + assert isinstance(sub_mat, NamedSparseMatrix) + assert sub_mat.shape == (2, 2) + assert sub_mat.row_names == ["r1", "r2"] + assert sub_mat.column_names == ["c1", "c2"] + + +def test_operations(): + """Test mathematical operations.""" + mat = NamedSparseMatrix( + sparse_data(), row_names=["r1", "r2", "r3"], column_names=["c1", "c2", "c3"] + ) + + # Multiplication by scalar + result = mat * 2 + assert isinstance(result, NamedSparseMatrix) + assert result.row_names == mat.row_names + assert result.column_names == mat.column_names + assert not np.allclose(result._matrix.data, mat._matrix.data) + + # Matrix multiplication + result = mat * mat + assert isinstance(result, NamedSparseMatrix) + assert result.row_names == mat.row_names + assert result.column_names == mat.column_names + + # Addition + result = mat + mat + assert isinstance(result, NamedSparseMatrix) + assert result.row_names == mat.row_names + assert result.column_names == mat.column_names + + +def test_get_set_value(): + """Test getting and setting values.""" + mat = NamedSparseMatrix( + sparse_data(), row_names=["r1", "r2", "r3"], column_names=["c1", "c2", "c3"] + ) + + # Get values + assert mat.get_value("r1", "c1") == 1 + assert mat.get_value("r2", "c2") == 3 + assert mat.get_value("r1", "c2") == 0 + + # Set values + mat.set_value("r1", "c2", 7) + assert mat.get_value("r1", "c2") == 7 + + # Set using indices + mat.set_value(0, 1, 8) + assert mat.get_value("r1", "c2") == 8 + + +def test_rename(): + """Test renaming functionality.""" + mat = NamedSparseMatrix(sparse_data()) + + # Add names + mat.set_rows(["r1", "r2", "r3"]) + mat.set_columns(["c1", "c2", "c3"]) + assert mat.row_names == ["r1", "r2", "r3"] + assert mat.column_names == ["c1", "c2", "c3"] + + # Remove names + mat.set_rows(None) + mat.set_columns(None) + assert mat.row_names is None + assert mat.column_names is None + + # Invalid renaming + with pytest.raises(ValueError): + mat.set_rows(["single"]) + with pytest.raises(ValueError): + mat.set_columns(["single"]) From 11901f9d6dde7675fa2a1fa8b62138a4d4dda489 Mon Sep 17 00:00:00 2001 From: Jayaram Kancherla Date: Fri, 25 Oct 2024 09:47:16 -0700 Subject: [PATCH 2/2] uncoment rest of the tests --- tests/test_NamedDenseMatrix.py | 94 +++++++++++++++++----------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/tests/test_NamedDenseMatrix.py b/tests/test_NamedDenseMatrix.py index 1dc038c..785e314 100644 --- a/tests/test_NamedDenseMatrix.py +++ b/tests/test_NamedDenseMatrix.py @@ -60,50 +60,50 @@ def test_indexing(): assert sub_mat.column_names == ["c1", "c2"] -# def test_operations(): -# """Test mathematical operations.""" -# data = [[1, 2], [3, 4]] -# mat = NamedDenseMatrix(data, row_names=["r1", "r2"], column_names=["c1", "c2"]) - -# # Multiplication -# result = mat * 2 -# assert isinstance(result, NamedDenseMatrix) -# assert result.row_names == mat.row_names -# assert result.column_names == mat.column_names -# assert np.array_equal(result.A, np.array(data) * 2) - -# # Matrix multiplication -# result = mat @ mat -# assert isinstance(result, NamedDenseMatrix) -# assert result.row_names == mat.row_names -# assert result.column_names == mat.column_names - -# # Addition -# result = mat + mat -# assert isinstance(result, NamedDenseMatrix) -# assert result.row_names == mat.row_names -# assert result.column_names == mat.column_names -# assert np.array_equal(result.A, np.array(data) * 2) - - -# def test_rename(): -# """Test renaming functionality.""" -# mat = NamedDenseMatrix([[1, 2], [3, 4]]) - -# # Add names -# mat.set_rows(["r1", "r2"]) -# mat.set_columns(["c1", "c2"]) -# assert mat.row_names == ["r1", "r2"] -# assert mat.column_names == ["c1", "c2"] - -# # Remove names -# mat.set_rows(None) -# mat.set_columns(None) -# assert mat.row_names is None -# assert mat.column_names is None - -# # Invalid renaming -# with pytest.raises(ValueError): -# mat.set_rows(["single"]) -# with pytest.raises(ValueError): -# mat.set_columns(["single"]) +def test_operations(): + """Test mathematical operations.""" + data = [[1, 2], [3, 4]] + mat = NamedDenseMatrix(data, row_names=["r1", "r2"], column_names=["c1", "c2"]) + + # Multiplication + result = mat * 2 + assert isinstance(result, NamedDenseMatrix) + assert result.row_names == mat.row_names + assert result.column_names == mat.column_names + assert np.array_equal(result.A, np.array(data) * 2) + + # Matrix multiplication + result = mat @ mat + assert isinstance(result, NamedDenseMatrix) + assert result.row_names == mat.row_names + assert result.column_names == mat.column_names + + # Addition + result = mat + mat + assert isinstance(result, NamedDenseMatrix) + assert result.row_names == mat.row_names + assert result.column_names == mat.column_names + assert np.array_equal(result.A, np.array(data) * 2) + + +def test_rename(): + """Test renaming functionality.""" + mat = NamedDenseMatrix([[1, 2], [3, 4]]) + + # Add names + mat.set_rows(["r1", "r2"]) + mat.set_columns(["c1", "c2"]) + assert mat.row_names == ["r1", "r2"] + assert mat.column_names == ["c1", "c2"] + + # Remove names + mat.set_rows(None) + mat.set_columns(None) + assert mat.row_names is None + assert mat.column_names is None + + # Invalid renaming + with pytest.raises(ValueError): + mat.set_rows(["single"]) + with pytest.raises(ValueError): + mat.set_columns(["single"])