From ac13cb716e6ec39528ee690bc533a841d723f77c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 02:42:21 +0000 Subject: [PATCH 1/6] Initial plan From d7a0ea57405fc96518bbdc567be8b4af7a4ebb93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 02:49:02 +0000 Subject: [PATCH 2/6] Implement non-orthogonal cell support for amber/md format Co-authored-by: njzjz <9496702+njzjz@users.noreply.github.com> --- dpdata/amber/md.py | 75 ++++++++-- tests/test_amber_nonorthogonal.py | 132 ++++++++++++++++++ tests/test_amber_nonorthogonal_integration.py | 58 ++++++++ 3 files changed, 256 insertions(+), 9 deletions(-) create mode 100644 tests/test_amber_nonorthogonal.py create mode 100644 tests/test_amber_nonorthogonal_integration.py diff --git a/dpdata/amber/md.py b/dpdata/amber/md.py index cb4f2d25e..b86034218 100644 --- a/dpdata/amber/md.py +++ b/dpdata/amber/md.py @@ -18,6 +18,70 @@ force_convert = energy_convert +def cell_lengths_angles_to_cell(cell_lengths, cell_angles): + """Convert cell lengths and angles to cell vectors. + + Parameters + ---------- + cell_lengths : np.ndarray + Cell lengths with shape (..., 3) where the last dimension + corresponds to [a, b, c] + cell_angles : np.ndarray + Cell angles in degrees with shape (..., 3) where the last dimension + corresponds to [alpha, beta, gamma] + + Returns + ------- + np.ndarray + Cell vectors with shape (..., 3, 3) where the last two dimensions + form the cell matrix + + Notes + ----- + Uses the standard crystallographic convention: + - v1 = [a, 0, 0] + - v2 = [b*cos(gamma), b*sin(gamma), 0] + - v3 = [c*cos(beta), c*(cos(alpha) - cos(beta)*cos(gamma))/sin(gamma), c*z] + where z = sqrt(1 - cos²(alpha) - cos²(beta) - cos²(gamma) + 2*cos(alpha)*cos(beta)*cos(gamma))/sin(gamma) + """ + # Convert to radians + alpha = np.deg2rad(cell_angles[..., 0]) # angle between b and c + beta = np.deg2rad(cell_angles[..., 1]) # angle between a and c + gamma = np.deg2rad(cell_angles[..., 2]) # angle between a and b + + a = cell_lengths[..., 0] + b = cell_lengths[..., 1] + c = cell_lengths[..., 2] + + cos_alpha = np.cos(alpha) + cos_beta = np.cos(beta) + cos_gamma = np.cos(gamma) + sin_gamma = np.sin(gamma) + + # Calculate the z-component of the third vector + z_factor = 1 - cos_alpha**2 - cos_beta**2 - cos_gamma**2 + 2*cos_alpha*cos_beta*cos_gamma + z_factor = np.maximum(z_factor, 0) # Ensure non-negative for sqrt + z = np.sqrt(z_factor) / sin_gamma + + # Build cell vectors + shape = cell_lengths.shape[:-1] + (3, 3) + cell = np.zeros(shape) + + # First vector: [a, 0, 0] + cell[..., 0, 0] = a + + # Second vector: [b*cos(gamma), b*sin(gamma), 0] + cell[..., 1, 0] = b * cos_gamma + cell[..., 1, 1] = b * sin_gamma + + # Third vector: [c*cos(beta), c*(cos(alpha) - cos(beta)*cos(gamma))/sin(gamma), c*z] + cell[..., 2, 0] = c * cos_beta + cell[..., 2, 1] = c * (cos_alpha - cos_beta * cos_gamma) / sin_gamma + cell[..., 2, 2] = c * z + + return cell + + def read_amber_traj( parm7_file, nc_file, @@ -85,15 +149,8 @@ def read_amber_traj( coords = np.array(f.variables["coordinates"][:]) cell_lengths = np.array(f.variables["cell_lengths"][:]) cell_angles = np.array(f.variables["cell_angles"][:]) - if np.all(cell_angles > 89.99) and np.all(cell_angles < 90.01): - # only support 90 - # TODO: support other angles - shape = cell_lengths.shape - cells = np.zeros((shape[0], 3, 3)) - for ii in range(3): - cells[:, ii, ii] = cell_lengths[:, ii] - else: - raise RuntimeError("Unsupported cells") + # Convert cell lengths and angles to cell vectors for all cases + cells = cell_lengths_angles_to_cell(cell_lengths, cell_angles) if labeled: with netcdf_file(mdfrc_file, "r") as f: diff --git a/tests/test_amber_nonorthogonal.py b/tests/test_amber_nonorthogonal.py new file mode 100644 index 000000000..a01769fcf --- /dev/null +++ b/tests/test_amber_nonorthogonal.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import unittest + +import numpy as np + +from dpdata.amber.md import cell_lengths_angles_to_cell + + +class TestAmberNonOrthogonalCells(unittest.TestCase): + def test_orthogonal_cell_conversion(self): + """Test that orthogonal cells (90° angles) work correctly.""" + # Test case: simple cubic cell with a=10, b=15, c=20, all angles=90° + cell_lengths = np.array([[10.0, 15.0, 20.0]]) + cell_angles = np.array([[90.0, 90.0, 90.0]]) + + expected_cell = np.array([[[10.0, 0.0, 0.0], + [0.0, 15.0, 0.0], + [0.0, 0.0, 20.0]]]) + + result_cell = cell_lengths_angles_to_cell(cell_lengths, cell_angles) + + np.testing.assert_allclose(result_cell, expected_cell, rtol=1e-12, atol=1e-14) + + def test_monoclinic_cell_conversion(self): + """Test monoclinic cell (beta != 90°, alpha=gamma=90°).""" + # Test case: monoclinic cell with a=10, b=15, c=20, alpha=90°, beta=120°, gamma=90° + cell_lengths = np.array([[10.0, 15.0, 20.0]]) + cell_angles = np.array([[90.0, 120.0, 90.0]]) + + # Expected result: + # v1 = [10, 0, 0] + # v2 = [0, 15, 0] (gamma=90°) + # v3 = [20*cos(120°), 0, 20*sin(120°)] = [-10, 0, 17.32...] + cos_120 = np.cos(np.deg2rad(120.0)) # -0.5 + sin_120 = np.sin(np.deg2rad(120.0)) # sqrt(3)/2 + + expected_cell = np.array([[[10.0, 0.0, 0.0], + [0.0, 15.0, 0.0], + [20.0 * cos_120, 0.0, 20.0 * sin_120]]]) + + result_cell = cell_lengths_angles_to_cell(cell_lengths, cell_angles) + + np.testing.assert_allclose(result_cell, expected_cell, rtol=1e-12, atol=1e-14) + + def test_hexagonal_cell_conversion(self): + """Test hexagonal cell (gamma=120°, alpha=beta=90°).""" + # Test case: hexagonal cell with a=10, b=10, c=15, alpha=90°, beta=90°, gamma=120° + cell_lengths = np.array([[10.0, 10.0, 15.0]]) + cell_angles = np.array([[90.0, 90.0, 120.0]]) + + # Expected result: + # v1 = [10, 0, 0] + # v2 = [10*cos(120°), 10*sin(120°), 0] = [-5, 8.66..., 0] + # v3 = [0, 0, 15] (alpha=beta=90°) + cos_120 = np.cos(np.deg2rad(120.0)) # -0.5 + sin_120 = np.sin(np.deg2rad(120.0)) # sqrt(3)/2 + + expected_cell = np.array([[[10.0, 0.0, 0.0], + [10.0 * cos_120, 10.0 * sin_120, 0.0], + [0.0, 0.0, 15.0]]]) + + result_cell = cell_lengths_angles_to_cell(cell_lengths, cell_angles) + + np.testing.assert_allclose(result_cell, expected_cell, rtol=1e-12, atol=1e-14) + + def test_triclinic_cell_conversion(self): + """Test triclinic cell (all angles != 90°).""" + # Test case: triclinic cell with a=8, b=10, c=12, alpha=70°, beta=80°, gamma=110° + cell_lengths = np.array([[8.0, 10.0, 12.0]]) + cell_angles = np.array([[70.0, 80.0, 110.0]]) + + result_cell = cell_lengths_angles_to_cell(cell_lengths, cell_angles) + + # Check that the result has the right shape + self.assertEqual(result_cell.shape, (1, 3, 3)) + + # Check that the cell vectors have the correct lengths + computed_lengths = np.linalg.norm(result_cell[0], axis=1) + expected_lengths = np.array([8.0, 10.0, 12.0]) + np.testing.assert_allclose(computed_lengths, expected_lengths, rtol=1e-12) + + # Check that the angles between vectors are correct + v1, v2, v3 = result_cell[0] + + # Angle between v2 and v3 should be alpha (70°) + cos_alpha = np.dot(v2, v3) / (np.linalg.norm(v2) * np.linalg.norm(v3)) + alpha_computed = np.rad2deg(np.arccos(cos_alpha)) + np.testing.assert_allclose(alpha_computed, 70.0, rtol=1e-8) + + # Angle between v1 and v3 should be beta (80°) + cos_beta = np.dot(v1, v3) / (np.linalg.norm(v1) * np.linalg.norm(v3)) + beta_computed = np.rad2deg(np.arccos(cos_beta)) + np.testing.assert_allclose(beta_computed, 80.0, rtol=1e-8) + + # Angle between v1 and v2 should be gamma (110°) + cos_gamma = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)) + gamma_computed = np.rad2deg(np.arccos(cos_gamma)) + np.testing.assert_allclose(gamma_computed, 110.0, rtol=1e-8) + + def test_multiple_frames(self): + """Test that multiple frames are handled correctly.""" + # Test case: 3 frames with different cell parameters + cell_lengths = np.array([[10.0, 10.0, 10.0], # cubic + [8.0, 12.0, 15.0], # orthorhombic + [10.0, 10.0, 12.0]]) # hexagonal-like + cell_angles = np.array([[90.0, 90.0, 90.0], + [90.0, 90.0, 90.0], + [90.0, 90.0, 120.0]]) + + result_cell = cell_lengths_angles_to_cell(cell_lengths, cell_angles) + + # Check shape + self.assertEqual(result_cell.shape, (3, 3, 3)) + + # Check first frame (cubic) + expected_frame1 = np.array([[10.0, 0.0, 0.0], + [0.0, 10.0, 0.0], + [0.0, 0.0, 10.0]]) + np.testing.assert_allclose(result_cell[0], expected_frame1, rtol=1e-12, atol=1e-14) + + # Check third frame (hexagonal-like) + cos_120 = np.cos(np.deg2rad(120.0)) # -0.5 + sin_120 = np.sin(np.deg2rad(120.0)) # sqrt(3)/2 + expected_frame3 = np.array([[10.0, 0.0, 0.0], + [10.0 * cos_120, 10.0 * sin_120, 0.0], + [0.0, 0.0, 12.0]]) + np.testing.assert_allclose(result_cell[2], expected_frame3, rtol=1e-12, atol=1e-14) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/test_amber_nonorthogonal_integration.py b/tests/test_amber_nonorthogonal_integration.py new file mode 100644 index 000000000..fdea26077 --- /dev/null +++ b/tests/test_amber_nonorthogonal_integration.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import unittest + +import numpy as np + +from dpdata.amber.md import cell_lengths_angles_to_cell + + +class TestAmberNonOrthogonalIntegration(unittest.TestCase): + def test_no_runtime_error_for_nonorthogonal_cells(self): + """Test that non-orthogonal cells no longer raise RuntimeError.""" + # Test case that would have previously raised RuntimeError + cell_lengths = np.array([[10.0, 10.0, 15.0]]) + cell_angles = np.array([[90.0, 90.0, 120.0]]) # gamma != 90 + + # This should NOT raise a RuntimeError anymore + try: + result = cell_lengths_angles_to_cell(cell_lengths, cell_angles) + # Should return a valid 3x3 cell matrix + self.assertEqual(result.shape, (1, 3, 3)) + except RuntimeError as e: + if "Unsupported cells" in str(e): + self.fail("cell_lengths_angles_to_cell should support non-orthogonal cells") + else: + raise # Re-raise if it's a different RuntimeError + + def test_extreme_angles_case(self): + """Test edge case with angles very far from 90°.""" + cell_lengths = np.array([[5.0, 8.0, 12.0]]) + cell_angles = np.array([[60.0, 70.0, 130.0]]) # all far from 90° + + # Should work without error + result = cell_lengths_angles_to_cell(cell_lengths, cell_angles) + self.assertEqual(result.shape, (1, 3, 3)) + + # Verify the lengths are preserved + computed_lengths = np.linalg.norm(result[0], axis=1) + expected_lengths = np.array([5.0, 8.0, 12.0]) + np.testing.assert_allclose(computed_lengths, expected_lengths, rtol=1e-10) + + def test_backwards_compatibility_orthogonal(self): + """Test that orthogonal cells still produce the same result as before.""" + cell_lengths = np.array([[10.0, 15.0, 20.0]]) + cell_angles = np.array([[90.0, 90.0, 90.0]]) + + result = cell_lengths_angles_to_cell(cell_lengths, cell_angles) + + # Should produce the same diagonal matrix as the old implementation + expected = np.array([[[10.0, 0.0, 0.0], + [0.0, 15.0, 0.0], + [0.0, 0.0, 20.0]]]) + + np.testing.assert_allclose(result, expected, rtol=1e-12, atol=1e-14) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From c4853008b91b7863ebaab431a538945b0a13d919 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 03:04:52 +0000 Subject: [PATCH 3/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- dpdata/amber/md.py | 38 +++--- tests/test_amber_nonorthogonal.py | 112 +++++++++++------- tests/test_amber_nonorthogonal_integration.py | 22 ++-- 3 files changed, 99 insertions(+), 73 deletions(-) diff --git a/dpdata/amber/md.py b/dpdata/amber/md.py index b86034218..756b40681 100644 --- a/dpdata/amber/md.py +++ b/dpdata/amber/md.py @@ -20,65 +20,71 @@ def cell_lengths_angles_to_cell(cell_lengths, cell_angles): """Convert cell lengths and angles to cell vectors. - + Parameters ---------- cell_lengths : np.ndarray Cell lengths with shape (..., 3) where the last dimension corresponds to [a, b, c] - cell_angles : np.ndarray + cell_angles : np.ndarray Cell angles in degrees with shape (..., 3) where the last dimension corresponds to [alpha, beta, gamma] - + Returns ------- np.ndarray Cell vectors with shape (..., 3, 3) where the last two dimensions form the cell matrix - + Notes ----- Uses the standard crystallographic convention: - v1 = [a, 0, 0] - - v2 = [b*cos(gamma), b*sin(gamma), 0] + - v2 = [b*cos(gamma), b*sin(gamma), 0] - v3 = [c*cos(beta), c*(cos(alpha) - cos(beta)*cos(gamma))/sin(gamma), c*z] where z = sqrt(1 - cos²(alpha) - cos²(beta) - cos²(gamma) + 2*cos(alpha)*cos(beta)*cos(gamma))/sin(gamma) """ # Convert to radians alpha = np.deg2rad(cell_angles[..., 0]) # angle between b and c - beta = np.deg2rad(cell_angles[..., 1]) # angle between a and c + beta = np.deg2rad(cell_angles[..., 1]) # angle between a and c gamma = np.deg2rad(cell_angles[..., 2]) # angle between a and b - + a = cell_lengths[..., 0] - b = cell_lengths[..., 1] + b = cell_lengths[..., 1] c = cell_lengths[..., 2] - + cos_alpha = np.cos(alpha) cos_beta = np.cos(beta) cos_gamma = np.cos(gamma) sin_gamma = np.sin(gamma) - + # Calculate the z-component of the third vector - z_factor = 1 - cos_alpha**2 - cos_beta**2 - cos_gamma**2 + 2*cos_alpha*cos_beta*cos_gamma + z_factor = ( + 1 + - cos_alpha**2 + - cos_beta**2 + - cos_gamma**2 + + 2 * cos_alpha * cos_beta * cos_gamma + ) z_factor = np.maximum(z_factor, 0) # Ensure non-negative for sqrt z = np.sqrt(z_factor) / sin_gamma - + # Build cell vectors shape = cell_lengths.shape[:-1] + (3, 3) cell = np.zeros(shape) - + # First vector: [a, 0, 0] cell[..., 0, 0] = a - + # Second vector: [b*cos(gamma), b*sin(gamma), 0] cell[..., 1, 0] = b * cos_gamma cell[..., 1, 1] = b * sin_gamma - + # Third vector: [c*cos(beta), c*(cos(alpha) - cos(beta)*cos(gamma))/sin(gamma), c*z] cell[..., 2, 0] = c * cos_beta cell[..., 2, 1] = c * (cos_alpha - cos_beta * cos_gamma) / sin_gamma cell[..., 2, 2] = c * z - + return cell diff --git a/tests/test_amber_nonorthogonal.py b/tests/test_amber_nonorthogonal.py index a01769fcf..f20a4ae88 100644 --- a/tests/test_amber_nonorthogonal.py +++ b/tests/test_amber_nonorthogonal.py @@ -13,13 +13,13 @@ def test_orthogonal_cell_conversion(self): # Test case: simple cubic cell with a=10, b=15, c=20, all angles=90° cell_lengths = np.array([[10.0, 15.0, 20.0]]) cell_angles = np.array([[90.0, 90.0, 90.0]]) - - expected_cell = np.array([[[10.0, 0.0, 0.0], - [0.0, 15.0, 0.0], - [0.0, 0.0, 20.0]]]) - + + expected_cell = np.array( + [[[10.0, 0.0, 0.0], [0.0, 15.0, 0.0], [0.0, 0.0, 20.0]]] + ) + result_cell = cell_lengths_angles_to_cell(cell_lengths, cell_angles) - + np.testing.assert_allclose(result_cell, expected_cell, rtol=1e-12, atol=1e-14) def test_monoclinic_cell_conversion(self): @@ -27,20 +27,26 @@ def test_monoclinic_cell_conversion(self): # Test case: monoclinic cell with a=10, b=15, c=20, alpha=90°, beta=120°, gamma=90° cell_lengths = np.array([[10.0, 15.0, 20.0]]) cell_angles = np.array([[90.0, 120.0, 90.0]]) - + # Expected result: # v1 = [10, 0, 0] # v2 = [0, 15, 0] (gamma=90°) # v3 = [20*cos(120°), 0, 20*sin(120°)] = [-10, 0, 17.32...] cos_120 = np.cos(np.deg2rad(120.0)) # -0.5 sin_120 = np.sin(np.deg2rad(120.0)) # sqrt(3)/2 - - expected_cell = np.array([[[10.0, 0.0, 0.0], - [0.0, 15.0, 0.0], - [20.0 * cos_120, 0.0, 20.0 * sin_120]]]) - + + expected_cell = np.array( + [ + [ + [10.0, 0.0, 0.0], + [0.0, 15.0, 0.0], + [20.0 * cos_120, 0.0, 20.0 * sin_120], + ] + ] + ) + result_cell = cell_lengths_angles_to_cell(cell_lengths, cell_angles) - + np.testing.assert_allclose(result_cell, expected_cell, rtol=1e-12, atol=1e-14) def test_hexagonal_cell_conversion(self): @@ -48,20 +54,26 @@ def test_hexagonal_cell_conversion(self): # Test case: hexagonal cell with a=10, b=10, c=15, alpha=90°, beta=90°, gamma=120° cell_lengths = np.array([[10.0, 10.0, 15.0]]) cell_angles = np.array([[90.0, 90.0, 120.0]]) - + # Expected result: # v1 = [10, 0, 0] # v2 = [10*cos(120°), 10*sin(120°), 0] = [-5, 8.66..., 0] # v3 = [0, 0, 15] (alpha=beta=90°) cos_120 = np.cos(np.deg2rad(120.0)) # -0.5 sin_120 = np.sin(np.deg2rad(120.0)) # sqrt(3)/2 - - expected_cell = np.array([[[10.0, 0.0, 0.0], - [10.0 * cos_120, 10.0 * sin_120, 0.0], - [0.0, 0.0, 15.0]]]) - + + expected_cell = np.array( + [ + [ + [10.0, 0.0, 0.0], + [10.0 * cos_120, 10.0 * sin_120, 0.0], + [0.0, 0.0, 15.0], + ] + ] + ) + result_cell = cell_lengths_angles_to_cell(cell_lengths, cell_angles) - + np.testing.assert_allclose(result_cell, expected_cell, rtol=1e-12, atol=1e-14) def test_triclinic_cell_conversion(self): @@ -69,30 +81,30 @@ def test_triclinic_cell_conversion(self): # Test case: triclinic cell with a=8, b=10, c=12, alpha=70°, beta=80°, gamma=110° cell_lengths = np.array([[8.0, 10.0, 12.0]]) cell_angles = np.array([[70.0, 80.0, 110.0]]) - + result_cell = cell_lengths_angles_to_cell(cell_lengths, cell_angles) - + # Check that the result has the right shape self.assertEqual(result_cell.shape, (1, 3, 3)) - + # Check that the cell vectors have the correct lengths computed_lengths = np.linalg.norm(result_cell[0], axis=1) expected_lengths = np.array([8.0, 10.0, 12.0]) np.testing.assert_allclose(computed_lengths, expected_lengths, rtol=1e-12) - + # Check that the angles between vectors are correct v1, v2, v3 = result_cell[0] - + # Angle between v2 and v3 should be alpha (70°) cos_alpha = np.dot(v2, v3) / (np.linalg.norm(v2) * np.linalg.norm(v3)) alpha_computed = np.rad2deg(np.arccos(cos_alpha)) np.testing.assert_allclose(alpha_computed, 70.0, rtol=1e-8) - + # Angle between v1 and v3 should be beta (80°) cos_beta = np.dot(v1, v3) / (np.linalg.norm(v1) * np.linalg.norm(v3)) beta_computed = np.rad2deg(np.arccos(cos_beta)) np.testing.assert_allclose(beta_computed, 80.0, rtol=1e-8) - + # Angle between v1 and v2 should be gamma (110°) cos_gamma = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)) gamma_computed = np.rad2deg(np.arccos(cos_gamma)) @@ -101,32 +113,40 @@ def test_triclinic_cell_conversion(self): def test_multiple_frames(self): """Test that multiple frames are handled correctly.""" # Test case: 3 frames with different cell parameters - cell_lengths = np.array([[10.0, 10.0, 10.0], # cubic - [8.0, 12.0, 15.0], # orthorhombic - [10.0, 10.0, 12.0]]) # hexagonal-like - cell_angles = np.array([[90.0, 90.0, 90.0], - [90.0, 90.0, 90.0], - [90.0, 90.0, 120.0]]) - + cell_lengths = np.array( + [ + [10.0, 10.0, 10.0], # cubic + [8.0, 12.0, 15.0], # orthorhombic + [10.0, 10.0, 12.0], + ] + ) # hexagonal-like + cell_angles = np.array( + [[90.0, 90.0, 90.0], [90.0, 90.0, 90.0], [90.0, 90.0, 120.0]] + ) + result_cell = cell_lengths_angles_to_cell(cell_lengths, cell_angles) - + # Check shape self.assertEqual(result_cell.shape, (3, 3, 3)) - + # Check first frame (cubic) - expected_frame1 = np.array([[10.0, 0.0, 0.0], - [0.0, 10.0, 0.0], - [0.0, 0.0, 10.0]]) - np.testing.assert_allclose(result_cell[0], expected_frame1, rtol=1e-12, atol=1e-14) - + expected_frame1 = np.array( + [[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]] + ) + np.testing.assert_allclose( + result_cell[0], expected_frame1, rtol=1e-12, atol=1e-14 + ) + # Check third frame (hexagonal-like) cos_120 = np.cos(np.deg2rad(120.0)) # -0.5 sin_120 = np.sin(np.deg2rad(120.0)) # sqrt(3)/2 - expected_frame3 = np.array([[10.0, 0.0, 0.0], - [10.0 * cos_120, 10.0 * sin_120, 0.0], - [0.0, 0.0, 12.0]]) - np.testing.assert_allclose(result_cell[2], expected_frame3, rtol=1e-12, atol=1e-14) + expected_frame3 = np.array( + [[10.0, 0.0, 0.0], [10.0 * cos_120, 10.0 * sin_120, 0.0], [0.0, 0.0, 12.0]] + ) + np.testing.assert_allclose( + result_cell[2], expected_frame3, rtol=1e-12, atol=1e-14 + ) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/test_amber_nonorthogonal_integration.py b/tests/test_amber_nonorthogonal_integration.py index fdea26077..67bf717a4 100644 --- a/tests/test_amber_nonorthogonal_integration.py +++ b/tests/test_amber_nonorthogonal_integration.py @@ -13,7 +13,7 @@ def test_no_runtime_error_for_nonorthogonal_cells(self): # Test case that would have previously raised RuntimeError cell_lengths = np.array([[10.0, 10.0, 15.0]]) cell_angles = np.array([[90.0, 90.0, 120.0]]) # gamma != 90 - + # This should NOT raise a RuntimeError anymore try: result = cell_lengths_angles_to_cell(cell_lengths, cell_angles) @@ -21,7 +21,9 @@ def test_no_runtime_error_for_nonorthogonal_cells(self): self.assertEqual(result.shape, (1, 3, 3)) except RuntimeError as e: if "Unsupported cells" in str(e): - self.fail("cell_lengths_angles_to_cell should support non-orthogonal cells") + self.fail( + "cell_lengths_angles_to_cell should support non-orthogonal cells" + ) else: raise # Re-raise if it's a different RuntimeError @@ -29,11 +31,11 @@ def test_extreme_angles_case(self): """Test edge case with angles very far from 90°.""" cell_lengths = np.array([[5.0, 8.0, 12.0]]) cell_angles = np.array([[60.0, 70.0, 130.0]]) # all far from 90° - + # Should work without error result = cell_lengths_angles_to_cell(cell_lengths, cell_angles) self.assertEqual(result.shape, (1, 3, 3)) - + # Verify the lengths are preserved computed_lengths = np.linalg.norm(result[0], axis=1) expected_lengths = np.array([5.0, 8.0, 12.0]) @@ -43,16 +45,14 @@ def test_backwards_compatibility_orthogonal(self): """Test that orthogonal cells still produce the same result as before.""" cell_lengths = np.array([[10.0, 15.0, 20.0]]) cell_angles = np.array([[90.0, 90.0, 90.0]]) - + result = cell_lengths_angles_to_cell(cell_lengths, cell_angles) - + # Should produce the same diagonal matrix as the old implementation - expected = np.array([[[10.0, 0.0, 0.0], - [0.0, 15.0, 0.0], - [0.0, 0.0, 20.0]]]) - + expected = np.array([[[10.0, 0.0, 0.0], [0.0, 15.0, 0.0], [0.0, 0.0, 20.0]]]) + np.testing.assert_allclose(result, expected, rtol=1e-12, atol=1e-14) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From 8aaea48a385d52f451c296adba15cb3e9b089f81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 05:20:08 +0000 Subject: [PATCH 4/6] Address PR review feedback: add type hints and improve integration test Co-authored-by: njzjz <9496702+njzjz@users.noreply.github.com> --- dpdata/amber/md.py | 2 +- tests/test_amber_nonorthogonal_integration.py | 36 +++++++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/dpdata/amber/md.py b/dpdata/amber/md.py index 756b40681..85217386b 100644 --- a/dpdata/amber/md.py +++ b/dpdata/amber/md.py @@ -18,7 +18,7 @@ force_convert = energy_convert -def cell_lengths_angles_to_cell(cell_lengths, cell_angles): +def cell_lengths_angles_to_cell(cell_lengths: np.ndarray, cell_angles: np.ndarray) -> np.ndarray: """Convert cell lengths and angles to cell vectors. Parameters diff --git a/tests/test_amber_nonorthogonal_integration.py b/tests/test_amber_nonorthogonal_integration.py index 67bf717a4..bb1e631c9 100644 --- a/tests/test_amber_nonorthogonal_integration.py +++ b/tests/test_amber_nonorthogonal_integration.py @@ -9,23 +9,31 @@ class TestAmberNonOrthogonalIntegration(unittest.TestCase): def test_no_runtime_error_for_nonorthogonal_cells(self): - """Test that non-orthogonal cells no longer raise RuntimeError.""" - # Test case that would have previously raised RuntimeError + """Test that the new cell conversion function handles non-orthogonal cells. + + This test verifies that cell_lengths_angles_to_cell() can handle + non-orthogonal cells that would have previously caused read_amber_traj() + to raise RuntimeError("Unsupported cells"). + """ + # Test case that would have previously raised RuntimeError in read_amber_traj cell_lengths = np.array([[10.0, 10.0, 15.0]]) cell_angles = np.array([[90.0, 90.0, 120.0]]) # gamma != 90 - # This should NOT raise a RuntimeError anymore - try: - result = cell_lengths_angles_to_cell(cell_lengths, cell_angles) - # Should return a valid 3x3 cell matrix - self.assertEqual(result.shape, (1, 3, 3)) - except RuntimeError as e: - if "Unsupported cells" in str(e): - self.fail( - "cell_lengths_angles_to_cell should support non-orthogonal cells" - ) - else: - raise # Re-raise if it's a different RuntimeError + # This function now replaces the code that raised RuntimeError + result = cell_lengths_angles_to_cell(cell_lengths, cell_angles) + + # Should return a valid 3x3 cell matrix + self.assertEqual(result.shape, (1, 3, 3)) + + # Verify the conversion worked correctly for this hexagonal case + # For hexagonal: a=b, gamma=120°, alpha=beta=90° + expected_cell = np.array([ + [[10.0, 0.0, 0.0], + [-5.0, 8.660254037844387, 0.0], # 10*cos(120°), 10*sin(120°), 0 + [0.0, 0.0, 15.0]] + ]) + + np.testing.assert_allclose(result, expected_cell, rtol=1e-10, atol=1e-14) def test_extreme_angles_case(self): """Test edge case with angles very far from 90°.""" From a8b31c46d5a432e74fab0be97ec8e0cd4b816827 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 05:46:33 +0000 Subject: [PATCH 5/6] Consolidate amber nonorthogonal test files as requested in PR review Co-authored-by: njzjz <9496702+njzjz@users.noreply.github.com> --- tests/test_amber_nonorthogonal.py | 14 ++++ tests/test_amber_nonorthogonal_integration.py | 66 ------------------- 2 files changed, 14 insertions(+), 66 deletions(-) delete mode 100644 tests/test_amber_nonorthogonal_integration.py diff --git a/tests/test_amber_nonorthogonal.py b/tests/test_amber_nonorthogonal.py index f20a4ae88..b1b111111 100644 --- a/tests/test_amber_nonorthogonal.py +++ b/tests/test_amber_nonorthogonal.py @@ -110,6 +110,20 @@ def test_triclinic_cell_conversion(self): gamma_computed = np.rad2deg(np.arccos(cos_gamma)) np.testing.assert_allclose(gamma_computed, 110.0, rtol=1e-8) + def test_extreme_angles_case(self): + """Test edge case with angles very far from 90°.""" + cell_lengths = np.array([[5.0, 8.0, 12.0]]) + cell_angles = np.array([[60.0, 70.0, 130.0]]) # all far from 90° + + # Should work without error + result = cell_lengths_angles_to_cell(cell_lengths, cell_angles) + self.assertEqual(result.shape, (1, 3, 3)) + + # Verify the lengths are preserved + computed_lengths = np.linalg.norm(result[0], axis=1) + expected_lengths = np.array([5.0, 8.0, 12.0]) + np.testing.assert_allclose(computed_lengths, expected_lengths, rtol=1e-10) + def test_multiple_frames(self): """Test that multiple frames are handled correctly.""" # Test case: 3 frames with different cell parameters diff --git a/tests/test_amber_nonorthogonal_integration.py b/tests/test_amber_nonorthogonal_integration.py deleted file mode 100644 index bb1e631c9..000000000 --- a/tests/test_amber_nonorthogonal_integration.py +++ /dev/null @@ -1,66 +0,0 @@ -from __future__ import annotations - -import unittest - -import numpy as np - -from dpdata.amber.md import cell_lengths_angles_to_cell - - -class TestAmberNonOrthogonalIntegration(unittest.TestCase): - def test_no_runtime_error_for_nonorthogonal_cells(self): - """Test that the new cell conversion function handles non-orthogonal cells. - - This test verifies that cell_lengths_angles_to_cell() can handle - non-orthogonal cells that would have previously caused read_amber_traj() - to raise RuntimeError("Unsupported cells"). - """ - # Test case that would have previously raised RuntimeError in read_amber_traj - cell_lengths = np.array([[10.0, 10.0, 15.0]]) - cell_angles = np.array([[90.0, 90.0, 120.0]]) # gamma != 90 - - # This function now replaces the code that raised RuntimeError - result = cell_lengths_angles_to_cell(cell_lengths, cell_angles) - - # Should return a valid 3x3 cell matrix - self.assertEqual(result.shape, (1, 3, 3)) - - # Verify the conversion worked correctly for this hexagonal case - # For hexagonal: a=b, gamma=120°, alpha=beta=90° - expected_cell = np.array([ - [[10.0, 0.0, 0.0], - [-5.0, 8.660254037844387, 0.0], # 10*cos(120°), 10*sin(120°), 0 - [0.0, 0.0, 15.0]] - ]) - - np.testing.assert_allclose(result, expected_cell, rtol=1e-10, atol=1e-14) - - def test_extreme_angles_case(self): - """Test edge case with angles very far from 90°.""" - cell_lengths = np.array([[5.0, 8.0, 12.0]]) - cell_angles = np.array([[60.0, 70.0, 130.0]]) # all far from 90° - - # Should work without error - result = cell_lengths_angles_to_cell(cell_lengths, cell_angles) - self.assertEqual(result.shape, (1, 3, 3)) - - # Verify the lengths are preserved - computed_lengths = np.linalg.norm(result[0], axis=1) - expected_lengths = np.array([5.0, 8.0, 12.0]) - np.testing.assert_allclose(computed_lengths, expected_lengths, rtol=1e-10) - - def test_backwards_compatibility_orthogonal(self): - """Test that orthogonal cells still produce the same result as before.""" - cell_lengths = np.array([[10.0, 15.0, 20.0]]) - cell_angles = np.array([[90.0, 90.0, 90.0]]) - - result = cell_lengths_angles_to_cell(cell_lengths, cell_angles) - - # Should produce the same diagonal matrix as the old implementation - expected = np.array([[[10.0, 0.0, 0.0], [0.0, 15.0, 0.0], [0.0, 0.0, 20.0]]]) - - np.testing.assert_allclose(result, expected, rtol=1e-12, atol=1e-14) - - -if __name__ == "__main__": - unittest.main() From 14d644713ae8167cf159d9d431dd8cec904fbf57 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 05:56:51 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- dpdata/amber/md.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dpdata/amber/md.py b/dpdata/amber/md.py index 85217386b..32396f0f7 100644 --- a/dpdata/amber/md.py +++ b/dpdata/amber/md.py @@ -18,7 +18,9 @@ force_convert = energy_convert -def cell_lengths_angles_to_cell(cell_lengths: np.ndarray, cell_angles: np.ndarray) -> np.ndarray: +def cell_lengths_angles_to_cell( + cell_lengths: np.ndarray, cell_angles: np.ndarray +) -> np.ndarray: """Convert cell lengths and angles to cell vectors. Parameters