diff --git a/CodeEntropy/levels.py b/CodeEntropy/levels.py index 5a8ed4b..a4be74d 100644 --- a/CodeEntropy/levels.py +++ b/CodeEntropy/levels.py @@ -441,14 +441,17 @@ def get_sphCoord_axes(self, arg_r): if x2y2 == 0.0: raise ValueError("x2y2 is zero, cannot compute sin_phi and cos_phi.") + # These conditions are mathematically unreachable for real-valued vectors. + # Marked as no cover to avoid false negatives in coverage reports. + # Check for non-negative values inside the square root - if x2y2 / r2 < 0: + if x2y2 / r2 < 0: # pragma: no cover raise ValueError( f"Negative value encountered for sin_theta calculation: {x2y2 / r2}. " f"Cannot take square root." ) - if x2y2 < 0: + if x2y2 < 0: # pragma: no cover raise ValueError( f"Negative value encountered for sin_phi and cos_phi " f"calculation: {x2y2}. " @@ -462,7 +465,7 @@ def get_sphCoord_axes(self, arg_r): sin_phi = arg_r[1] / np.sqrt(x2y2) cos_phi = arg_r[0] / np.sqrt(x2y2) - else: + else: # pragma: no cover sin_theta = 0.0 cos_theta = 1 diff --git a/tests/test_CodeEntropy/test_levels.py b/tests/test_CodeEntropy/test_levels.py new file mode 100644 index 0000000..81166bd --- /dev/null +++ b/tests/test_CodeEntropy/test_levels.py @@ -0,0 +1,754 @@ +import os +import shutil +import tempfile +import unittest +from unittest.mock import MagicMock, patch + +import numpy as np + +from CodeEntropy.levels import LevelManager +from CodeEntropy.main import main + + +class TestLevels(unittest.TestCase): + """ + Unit tests for the functionality of Levels. + """ + + def setUp(self): + """ + Set up test environment. + """ + self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_") + self.code_entropy = main + + # Change to test directory + self._orig_dir = os.getcwd() + os.chdir(self.test_dir) + + def tearDown(self): + """ + Clean up after each test. + """ + os.chdir(self._orig_dir) + if os.path.exists(self.test_dir): + shutil.rmtree(self.test_dir) + + def test_select_levels(self): + """ + Test `select_levels` with a mocked data container containing two molecules: + - The first molecule has 2 atoms and 1 residue (should return 'united_atom' and + 'residue'). + - The second molecule has 3 atoms and 2 residues (should return all three + levels). + + Asserts that the number of molecules and the levels list match expected values. + """ + # Create a mock data_container + data_container = MagicMock() + + # Mock fragments (2 molecules) + fragment1 = MagicMock() + fragment2 = MagicMock() + + # Mock select_atoms return values + atoms1 = MagicMock() + atoms1.__len__.return_value = 2 + atoms1.residues = [1] # 1 residue + + atoms2 = MagicMock() + atoms2.__len__.return_value = 3 + atoms2.residues = [1, 2] # 2 residues + + fragment1.select_atoms.return_value = atoms1 + fragment2.select_atoms.return_value = atoms2 + + data_container.atoms.fragments = [fragment1, fragment2] + + # Import the class and call the method + level_manager = LevelManager() + number_molecules, levels = level_manager.select_levels(data_container) + + # Assertions + self.assertEqual(number_molecules, 2) + self.assertEqual( + levels, [["united_atom", "residue"], ["united_atom", "residue", "polymer"]] + ) + + def test_get_matrices(self): + """ + Test `get_matrices` with mocked internal methods and a simple trajectory. + Ensures that the method returns correctly shaped matrices after filtering. + """ + + # Create a mock LevelManager instance + level_manager = LevelManager() + + # Mock internal methods + level_manager.get_beads = MagicMock(return_value=["bead1", "bead2"]) + level_manager.get_axes = MagicMock(return_value=("trans_axes", "rot_axes")) + level_manager.get_weighted_forces = MagicMock( + return_value=np.array([1.0, 2.0, 3.0]) + ) + level_manager.get_weighted_torques = MagicMock( + return_value=np.array([0.5, 1.5, 2.5]) + ) + level_manager.create_submatrix = MagicMock(return_value=np.identity(3)) + level_manager.filter_zero_rows_columns = MagicMock(side_effect=lambda x: x) + + # Mock data_container and trajectory + data_container = MagicMock() + timestep1 = MagicMock() + timestep1.frame = 0 + timestep2 = MagicMock() + timestep2.frame = 1 + data_container.trajectory.__getitem__.return_value = [timestep1, timestep2] + + # Call the method + force_matrix, torque_matrix = level_manager.get_matrices( + data_container=data_container, + level="residue", + start=0, + end=2, + step=1, + number_frames=2, + highest_level="polymer", + ) + + # Assertions + self.assertTrue(isinstance(force_matrix, np.ndarray)) + self.assertTrue(isinstance(torque_matrix, np.ndarray)) + self.assertEqual(force_matrix.shape, (6, 6)) # 2 beads × 3D + self.assertEqual(torque_matrix.shape, (6, 6)) + + # Check that internal methods were called + self.assertEqual(level_manager.get_beads.call_count, 1) + self.assertEqual(level_manager.get_axes.call_count, 4) # 2 beads × 2 frames + self.assertEqual( + level_manager.create_submatrix.call_count, 6 + ) # 3 force + 3 torque + + def test_get_dihedrals_united_atom(self): + """ + Test `get_dihedrals` for 'united_atom' level. + Ensures it returns the dihedrals directly from the data container. + """ + level_manager = LevelManager() + + data_container = MagicMock() + mock_dihedrals = ["d1", "d2", "d3"] + data_container.dihedrals = mock_dihedrals + + result = level_manager.get_dihedrals(data_container, level="united_atom") + self.assertEqual(result, mock_dihedrals) + + def test_get_dihedrals_residue(self): + """ + Test `get_dihedrals` for 'residue' level with 5 residues. + Mocks bonded atom selections and verifies that dihedrals are constructed. + """ + level_manager = LevelManager() + + data_container = MagicMock() + data_container.residues = [0, 1, 2, 3, 4] # 5 residues + + # Mock select_atoms to return atom groups with .dihedral + mock_dihedral = MagicMock() + mock_atom_group = MagicMock() + mock_atom_group.__add__.return_value = mock_atom_group + mock_atom_group.dihedral = mock_dihedral + data_container.select_atoms.return_value = mock_atom_group + + result = level_manager.get_dihedrals(data_container, level="residue") + + # Should create 2 dihedrals for 5 residues (residues 0–3 and 1–4) + self.assertEqual(len(result), 2) + self.assertTrue(all(d == mock_dihedral for d in result)) + + def test_get_dihedrals_no_residue(self): + """ + Test `get_dihedrals` for 'residue' level with 3 residues. + Mocks bonded atom selections and verifies that dihedrals are constructed. + """ + level_manager = LevelManager() + + data_container = MagicMock() + data_container.residues = [0, 1, 2] # 3 residues + + # Mock select_atoms to return atom groups with .dihedral + mock_dihedral = MagicMock() + mock_atom_group = MagicMock() + mock_atom_group.__add__.return_value = mock_atom_group + mock_atom_group.dihedral = mock_dihedral + data_container.select_atoms.return_value = mock_atom_group + + result = level_manager.get_dihedrals(data_container, level="residue") + + # Should result in no resdies + self.assertEqual(result, []) + + def test_get_beads_polymer_level(self): + """ + Test `get_beads` for 'polymer' level. + Should return a single atom group representing the whole system. + """ + level_manager = LevelManager() + + data_container = MagicMock() + mock_atom_group = MagicMock() + + data_container.select_atoms.return_value = mock_atom_group + + result = level_manager.get_beads(data_container, level="polymer") + + self.assertEqual(len(result), 1) + self.assertEqual(result[0], mock_atom_group) + data_container.select_atoms.assert_called_once_with("all") + + def test_get_beads_residue_level(self): + """ + Test `get_beads` for 'residue' level. + Should return one atom group per residue. + """ + level_manager = LevelManager() + + data_container = MagicMock() + data_container.residues = [0, 1, 2] # 3 residues + mock_atom_group = MagicMock() + data_container.select_atoms.return_value = mock_atom_group + + result = level_manager.get_beads(data_container, level="residue") + + self.assertEqual(len(result), 3) + self.assertTrue(all(bead == mock_atom_group for bead in result)) + self.assertEqual(data_container.select_atoms.call_count, 3) + + def test_get_beads_united_atom_level(self): + """ + Test `get_beads` for 'united_atom' level. + Should return one bead per heavy atom, including bonded hydrogens. + """ + level_manager = LevelManager() + + data_container = MagicMock() + heavy_atoms = [MagicMock(index=i) for i in range(3)] + data_container.select_atoms.side_effect = [ + heavy_atoms, + "bead0", + "bead1", + "bead2", + ] + + result = level_manager.get_beads(data_container, level="united_atom") + + self.assertEqual(len(result), 3) + self.assertEqual(result, ["bead0", "bead1", "bead2"]) + self.assertEqual( + data_container.select_atoms.call_count, 4 + ) # 1 for heavy_atoms + 3 beads + + def test_get_axes_polymer_level(self): + """ + Test `get_axes` for 'polymer' level. + Should return principal axes of the full system for both + translation and rotation. + """ + level_manager = LevelManager() + + data_container = MagicMock() + principal_axes = np.identity(3) + data_container.atoms.principal_axes.return_value = principal_axes + + trans_axes, rot_axes = level_manager.get_axes(data_container, level="polymer") + + self.assertTrue((trans_axes == principal_axes).all()) + self.assertTrue((rot_axes == principal_axes).all()) + + def test_get_axes_residue_level_with_bonds(self): + """ + Test `get_axes` for 'residue' level with bonded neighbors. + Should use spherical coordinate axes for rotation. + """ + level_manager = LevelManager() + + data_container = MagicMock() + data_container.atoms.principal_axes.return_value = "trans_axes" + + atom_set = MagicMock() + atom_set.__len__.return_value = 1 + + residue = MagicMock() + residue.atoms.center_of_mass.return_value = "center" + residue.atoms.principal_axes.return_value = "fallback_rot_axes" + + data_container.select_atoms.side_effect = [atom_set, residue] + + level_manager.get_avg_pos = MagicMock(return_value="vector") + level_manager.get_sphCoord_axes = MagicMock(return_value="rot_axes") + + trans_axes, rot_axes = level_manager.get_axes( + data_container, level="residue", index=2 + ) + + self.assertEqual(trans_axes, "trans_axes") + self.assertEqual(rot_axes, "rot_axes") + + def test_get_axes_residue_level_without_bonds(self): + """ + Test `get_axes` for 'residue' level with no bonded neighbors. + Should use principal axes of the residue for rotation. + """ + level_manager = LevelManager() + + data_container = MagicMock() + data_container.atoms.principal_axes.return_value = "trans_axes" + + empty_atom_set = [] + residue = MagicMock() + residue.atoms.principal_axes.return_value = "rot_axes" + + data_container.select_atoms.side_effect = [empty_atom_set, residue] + + trans_axes, rot_axes = level_manager.get_axes( + data_container, level="residue", index=2 + ) + + self.assertEqual(trans_axes, "trans_axes") + self.assertEqual(rot_axes, "rot_axes") + + def test_get_axes_united_atom_level(self): + """ + Test `get_axes` for 'united_atom' level. + Should use residue principal axes for translation and spherical + axes for rotation. + """ + level_manager = LevelManager() + + data_container = MagicMock() + data_container.residues.principal_axes.return_value = "trans_axes" + + atom_set = MagicMock() + atom_group = MagicMock() + atom_group.positions = [[1.0, 2.0, 3.0]] + + data_container.select_atoms.side_effect = [atom_set, atom_group] + + level_manager.get_avg_pos = MagicMock(return_value="vector") + level_manager.get_sphCoord_axes = MagicMock(return_value="rot_axes") + + trans_axes, rot_axes = level_manager.get_axes( + data_container, level="united_atom", index=5 + ) + + self.assertEqual(trans_axes, "trans_axes") + self.assertEqual(rot_axes, "rot_axes") + + def test_get_avg_pos_with_atoms(self): + """ + Test `get_avg_pos` with a non-empty atom set. + Should return the average of atom positions minus the center. + """ + level_manager = LevelManager() + + atom1 = MagicMock() + atom1.position = np.array([1.0, 2.0, 3.0]) + atom2 = MagicMock() + atom2.position = np.array([4.0, 5.0, 6.0]) + + atom_set = MagicMock() + atom_set.names = ["A", "B"] + atom_set.atoms = [atom1, atom2] + + center = np.array([1.0, 1.0, 1.0]) + expected_avg = ((atom1.position + atom2.position) / 2) - center + + result = level_manager.get_avg_pos(atom_set, center) + np.testing.assert_array_almost_equal(result, expected_avg) + + @patch("numpy.random.random") + def test_get_avg_pos_empty(self, mock_random): + """ + Test `get_avg_pos` with an empty atom set. + Should return a random vector minus the center. + """ + level_manager = LevelManager() + + atom_set = MagicMock() + atom_set.names = [] + atom_set.atoms = [] + + center = np.array([1.0, 1.0, 1.0]) + mock_random.return_value = np.array([0.5, 0.5, 0.5]) + + result = level_manager.get_avg_pos(atom_set, center) + expected = np.array([0.5, 0.5, 0.5]) - center + + np.testing.assert_array_almost_equal(result, expected) + + def test_get_sphCoord_axes_valid_vector(self): + """ + Test with a valid non-zero vector. + Should return a 3x3 orthonormal basis matrix. + """ + level_manager = LevelManager() + + vector = np.array([1.0, 1.0, 1.0]) + result = level_manager.get_sphCoord_axes(vector) + + self.assertEqual(result.shape, (3, 3)) + self.assertTrue(np.all(np.isfinite(result))) + + def test_get_sphCoord_axes_vector_on_z_axis_raises(self): + """ + Test with a vector along the z-axis (x2y2 == 0). + Should raise ValueError due to undefined phi. + """ + level_manager = LevelManager() + + vector = np.array([0.0, 0.0, 1.0]) + with self.assertRaises(ValueError): + level_manager.get_sphCoord_axes(vector) + + def test_get_sphCoord_axes_negative_x2y2_div_r2(self): + """ + Test with a vector that would cause x2y2 / r2 < 0. + """ + level_manager = LevelManager() + + vector = np.array([1e-10, 1e-10, 1e10]) # x2y2 is tiny, r2 is huge + result = level_manager.get_sphCoord_axes(vector) + self.assertEqual(result.shape, (3, 3)) + + def test_get_sphCoord_axes_zero_vector_raises(self): + """ + Test with a zero vector. + Should raise ValueError due to r2 == 0. + """ + level_manager = LevelManager() + + vector = np.array([0.0, 0.0, 0.0]) + with self.assertRaises(ValueError) as context: + level_manager.get_sphCoord_axes(vector) + self.assertIn("r2 is zero", str(context.exception)) + + def test_get_sphCoord_axes_x2y2_zero_raises(self): + """ + Test with a vector along the z-axis (x2y2 == 0, r2 != 0). + Should raise ValueError due to undefined phi. + """ + level_manager = LevelManager() + + vector = np.array([0.0, 0.0, 1.0]) # r2 = 1.0, x2y2 = 0.0 + with self.assertRaises(ValueError) as context: + level_manager.get_sphCoord_axes(vector) + self.assertIn("x2y2 is zero", str(context.exception)) + + def test_get_weighted_force_with_partitioning(self): + """ + Test correct weighted force calculation with partitioning enabled. + """ + level_manager = LevelManager() + + atom = MagicMock() + atom.index = 0 + + bead = MagicMock() + bead.atoms = [atom] + bead.total_mass.return_value = 4.0 + + data_container = MagicMock() + data_container.atoms.__getitem__.return_value.force = np.array([2.0, 0.0, 0.0]) + + trans_axes = np.identity(3) + + result = level_manager.get_weighted_forces( + data_container, bead, trans_axes, highest_level=True + ) + + expected = (0.5 * np.array([2.0, 0.0, 0.0])) / np.sqrt(4.0) + np.testing.assert_array_almost_equal(result, expected) + + def test_get_weighted_force_without_partitioning(self): + """ + Test correct weighted force calculation with partitioning disabled. + """ + level_manager = LevelManager() + + atom = MagicMock() + atom.index = 0 + + bead = MagicMock() + bead.atoms = [atom] + bead.total_mass.return_value = 1.0 + + data_container = MagicMock() + data_container.atoms.__getitem__.return_value.force = np.array([3.0, 0.0, 0.0]) + + trans_axes = np.identity(3) + + result = level_manager.get_weighted_forces( + data_container, bead, trans_axes, highest_level=False + ) + + expected = np.array([3.0, 0.0, 0.0]) / np.sqrt(1.0) + np.testing.assert_array_almost_equal(result, expected) + + def test_get_weighted_forces_zero_mass_raises_value_error(self): + """ + Test that a zero mass raises a ValueError. + """ + level_manager = LevelManager() + + atom = MagicMock() + atom.index = 0 + + bead = MagicMock() + bead.atoms = [atom] + bead.total_mass.return_value = 0.0 + + data_container = MagicMock() + data_container.atoms.__getitem__.return_value.force = np.array([1.0, 0.0, 0.0]) + + trans_axes = np.identity(3) + + with self.assertRaises(ValueError): + level_manager.get_weighted_forces( + data_container, bead, trans_axes, highest_level=True + ) + + def test_get_weighted_forces_negative_mass_raises_value_error(self): + """ + Test that a negative mass raises a ValueError. + """ + level_manager = LevelManager() + + atom = MagicMock() + atom.index = 0 + + bead = MagicMock() + bead.atoms = [atom] + bead.total_mass.return_value = -1.0 + + data_container = MagicMock() + data_container.atoms.__getitem__.return_value.force = np.array([1.0, 0.0, 0.0]) + + trans_axes = np.identity(3) + + with self.assertRaises(ValueError): + level_manager.get_weighted_forces( + data_container, bead, trans_axes, highest_level=True + ) + + def test_get_weighted_torques_weighted_torque_basic(self): + """ + Test basic torque calculation with non-zero moment of inertia and torques. + """ + level_manager = LevelManager() + + # Mock atom + atom = MagicMock() + atom.index = 0 + + # Mock bead + bead = MagicMock() + bead.atoms = [atom] + bead.center_of_mass.return_value = np.array([0.0, 0.0, 0.0]) + bead.moment_of_inertia.return_value = np.identity(3) + + # Mock data_container + data_container = MagicMock() + data_container.atoms.__getitem__.return_value.position = np.array( + [1.0, 0.0, 0.0] + ) + data_container.atoms.__getitem__.return_value.force = np.array([0.0, 1.0, 0.0]) + + # Rotation axes (identity matrix) + rot_axes = np.identity(3) + + result = level_manager.get_weighted_torques(data_container, bead, rot_axes) + + np.testing.assert_array_almost_equal(result, np.array([0.0, 0.0, 0.5])) + + def test_get_weighted_torques_zero_torque_skips_division(self): + """ + Test that zero torque components skip division and remain zero. + """ + level_manager = LevelManager() + + atom = MagicMock() + atom.index = 0 + + bead = MagicMock() + bead.atoms = [atom] + bead.center_of_mass.return_value = np.array([0.0, 0.0, 0.0]) + bead.moment_of_inertia.return_value = np.identity(3) + + data_container = MagicMock() + data_container.atoms.__getitem__.return_value.position = np.array( + [0.0, 0.0, 0.0] + ) + data_container.atoms.__getitem__.return_value.force = np.array([0.0, 0.0, 0.0]) + + rot_axes = np.identity(3) + + result = level_manager.get_weighted_torques(data_container, bead, rot_axes) + np.testing.assert_array_almost_equal(result, np.zeros(3)) + + def test_get_weighted_torques_zero_moi_raises(self): + """ + Should raise ZeroDivisionError when moment of inertia is zero in a dimension + and torque in that dimension is non-zero. + """ + level_manager = LevelManager() + + atom = MagicMock() + atom.index = 0 + + bead = MagicMock() + bead.atoms = [atom] + bead.center_of_mass.return_value = np.array([0.0, 0.0, 0.0]) + + # Set moment of inertia with zero in dimension 2 + moi = np.identity(3) + moi[2, 2] = 0.0 + bead.moment_of_inertia.return_value = moi + + data_container = MagicMock() + # Position and force that will produce a non-zero torque in z (dimension 2) + data_container.atoms.__getitem__.return_value.position = np.array( + [1.0, 0.0, 0.0] + ) + data_container.atoms.__getitem__.return_value.force = np.array([0.0, 1.0, 0.0]) + + rot_axes = np.identity(3) + + with self.assertRaises(ZeroDivisionError): + level_manager.get_weighted_torques(data_container, bead, rot_axes) + + def test_get_weighted_torques_negative_moi_raises(self): + """ + Should raise ValueError when moment of inertia is negative in a dimension + and torque in that dimension is non-zero. + """ + level_manager = LevelManager() + + atom = MagicMock() + atom.index = 0 + + bead = MagicMock() + bead.atoms = [atom] + bead.center_of_mass.return_value = np.array([0.0, 0.0, 0.0]) + + # Set moment of inertia with negative value in dimension 2 + moi = np.identity(3) + moi[2, 2] = -1.0 + bead.moment_of_inertia.return_value = moi + + data_container = MagicMock() + # Position and force that will produce a non-zero torque in z (dimension 2) + data_container.atoms.__getitem__.return_value.position = np.array( + [1.0, 0.0, 0.0] + ) + data_container.atoms.__getitem__.return_value.force = np.array([0.0, 1.0, 0.0]) + + rot_axes = np.identity(3) + + with self.assertRaises(ValueError) as context: + level_manager.get_weighted_torques(data_container, bead, rot_axes) + + self.assertIn( + "Negative value encountered for moment of inertia", str(context.exception) + ) + + def test_create_submatrix_basic_outer_product_average(self): + """ + Test with known vectors to verify correct average outer product. + """ + level_manager = LevelManager() + + data_i = [np.array([1, 0, 0]), np.array([0, 1, 0])] + data_j = [np.array([0, 1, 0]), np.array([1, 0, 0])] + number_frames = 2 + + expected = (np.outer(data_i[0], data_j[0]) + np.outer(data_i[1], data_j[1])) / 2 + + result = level_manager.create_submatrix(data_i, data_j, number_frames) + np.testing.assert_array_almost_equal(result, expected) + + def test_create_submatrix_zero_vectors_returns_zero_matrix(self): + """ + Test that all-zero input vectors should return a zero matrix. + """ + level_manager = LevelManager() + + data_i = [np.zeros(3) for _ in range(3)] + data_j = [np.zeros(3) for _ in range(3)] + result = level_manager.create_submatrix(data_i, data_j, 3) + np.testing.assert_array_equal(result, np.zeros((3, 3))) + + def test_create_submatrix_single_frame(self): + """ + Test that one frame should return the outer product of the single pair of + vectors. + """ + level_manager = LevelManager() + + vec_i = np.array([1, 2, 3]) + vec_j = np.array([4, 5, 6]) + expected = np.outer(vec_i, vec_j) + + result = level_manager.create_submatrix([vec_i], [vec_j], 1) + np.testing.assert_array_almost_equal(result, expected) + + def test_create_submatrix_symmetric_result_when_data_equal(self): + """ + Test that if data_i == data_j, the result should be symmetric. + """ + level_manager = LevelManager() + + data = [np.array([1, 2, 3]), np.array([4, 5, 6])] + result = level_manager.create_submatrix(data, data, 2) + self.assertTrue(np.allclose(result, result.T)) # Check symmetry + + def test_filter_zero_rows_columns_no_zeros(self): + """ + Test that matrix with no zero-only rows or columns should return unchanged. + """ + level_manager = LevelManager() + + matrix = np.array([[1, 2], [3, 4]]) + result = level_manager.filter_zero_rows_columns(matrix) + np.testing.assert_array_equal(result, matrix) + + def test_filter_zero_rows_columns_remove_rows_and_columns(self): + """ + Test that matrix with zero-only rows and columns should return reduced matrix. + """ + level_manager = LevelManager() + + matrix = np.array([[0, 0, 0], [0, 5, 0], [0, 0, 0]]) + expected = np.array([[5]]) + result = level_manager.filter_zero_rows_columns(matrix) + np.testing.assert_array_equal(result, expected) + + def test_filter_zero_rows_columns_all_zeros(self): + """ + Test that matrix with all zeros should return an empty matrix. + """ + level_manager = LevelManager() + + matrix = np.zeros((3, 3)) + result = level_manager.filter_zero_rows_columns(matrix) + self.assertEqual(result.size, 0) + self.assertEqual(result.shape, (0, 0)) + + def test_filter_zero_rows_columns_partial_zero_removal(self): + """ + Matrix with zeros in specific rows/columns should remove only those. + """ + level_manager = LevelManager() + + matrix = np.array([[0, 0, 0], [1, 2, 3], [0, 0, 0]]) + expected = np.array([[1, 2, 3]]) + result = level_manager.filter_zero_rows_columns(matrix) + np.testing.assert_array_equal(result, expected)