From 0df2fd7fcf216301ec341ac6e7a868f9ffcaa0d1 Mon Sep 17 00:00:00 2001 From: IAlibay Date: Mon, 22 Jun 2026 11:26:17 +0100 Subject: [PATCH 1/6] Fix compatibility with numpy 2.5 --- package/CHANGELOG | 4 ++++ package/MDAnalysis/core/topologyattrs.py | 2 +- testsuite/MDAnalysisTests/core/test_atomgroup.py | 12 ++++++------ testsuite/MDAnalysisTests/core/test_topologyattrs.py | 4 ++-- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 93ef2d158e4..2c84bd4e4c8 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -22,6 +22,10 @@ The rules for this file: * 2.11.0 Fixes + * The `principal_axes` method in :class:`Masses` now uses + `np.linalg.eigh` instead of `np.linalg.eig`, improving numerical + stability. This may lead to slightly different results from previous + version of MDAnalysis. * `MDAnalysis.analysis.nucleicacids.WatsonCrickDist`, `MinorPairDist`, and `MajorPairDist` now match residue names against the full resname instead of only the first character, fixing incorrect behaviour with diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index 359bd20c9c1..63a637ca97e 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -2084,7 +2084,7 @@ def principal_axes(group, wrap=False): is deprecated and will be removed in version 3.0. """ atomgroup = group.atoms - e_val, e_vec = np.linalg.eig(atomgroup.moment_of_inertia(wrap=wrap)) + e_val, e_vec = np.linalg.eigh(atomgroup.moment_of_inertia(wrap=wrap)) # Sort indices = np.argsort(e_val)[::-1] diff --git a/testsuite/MDAnalysisTests/core/test_atomgroup.py b/testsuite/MDAnalysisTests/core/test_atomgroup.py index b285a3568a5..84e80cb6c32 100644 --- a/testsuite/MDAnalysisTests/core/test_atomgroup.py +++ b/testsuite/MDAnalysisTests/core/test_atomgroup.py @@ -1280,8 +1280,8 @@ class TestPBCFlag(object): ), "principal_axes": np.array( [ - [0.78787867, 0.26771575, -0.55459488], - [-0.40611024, -0.45112859, -0.7947059], + [-0.78787867, -0.26771575, 0.55459488], + [0.40611024, 0.45112859, 0.7947059], [-0.46294889, 0.85135849, -0.24671249], ] ), @@ -1315,8 +1315,8 @@ class TestPBCFlag(object): ), "principal_axes": np.array( [ - [0.85911708, -0.19258726, -0.4741603], - [0.07520116, 0.96394227, -0.25526473], + [-0.85911708, 0.19258726, 0.4741603], + [-0.07520116, -0.96394227, 0.25526473], [0.50622389, 0.18364489, 0.84262206], ] ), @@ -1624,8 +1624,8 @@ def test_principal_axes(self, ag): ag.principal_axes(), np.array( [ - [1.53389276e-03, 4.41386224e-02, 9.99024239e-01], - [1.20986911e-02, 9.98951474e-01, -4.41539838e-02], + [-1.53389276e-03, -4.41386224e-02, -9.99024239e-01], + [-1.20986911e-02, -9.98951474e-01, 4.41539838e-02], [-9.99925632e-01, 1.21546132e-02, 9.98264877e-04], ] ), diff --git a/testsuite/MDAnalysisTests/core/test_topologyattrs.py b/testsuite/MDAnalysisTests/core/test_topologyattrs.py index 5155933c2e4..7f8f766f251 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyattrs.py +++ b/testsuite/MDAnalysisTests/core/test_topologyattrs.py @@ -421,8 +421,8 @@ def test_principal_axes(self, ag): ag.principal_axes(), np.array( [ - [1.53389276e-03, 4.41386224e-02, 9.99024239e-01], - [1.20986911e-02, 9.98951474e-01, -4.41539838e-02], + [-1.53389276e-03, -4.41386224e-02, -9.99024239e-01], + [-1.20986911e-02, -9.98951474e-01, 4.41539838e-02], [-9.99925632e-01, 1.21546132e-02, 9.98264877e-04], ] ), From 3bcf7626e4e4fbd9a6fe0fde344bdd7b4688603e Mon Sep 17 00:00:00 2001 From: IAlibay Date: Mon, 22 Jun 2026 19:22:09 +0100 Subject: [PATCH 2/6] fix CI --- .../MDAnalysisTests/core/test_atomgroup.py | 18 +++++++++++------- testsuite/MDAnalysisTests/core/test_groups.py | 15 ++++++++------- .../MDAnalysisTests/core/test_topologyattrs.py | 16 +++++++++------- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/testsuite/MDAnalysisTests/core/test_atomgroup.py b/testsuite/MDAnalysisTests/core/test_atomgroup.py index 84e80cb6c32..3fb45aad845 100644 --- a/testsuite/MDAnalysisTests/core/test_atomgroup.py +++ b/testsuite/MDAnalysisTests/core/test_atomgroup.py @@ -1355,6 +1355,8 @@ def test_wrap(self, ag, wrap, ref, method_name): if method_name == "bsphere": assert_almost_equal(result[0], ref[method_name][0], self.prec) assert_almost_equal(result[1], ref[method_name][1], self.prec) + elif method_name == "principal_axes": + assert_almost_equal(np.absolute(result), np.absolute(ref[method_name]), self.prec) else: assert_almost_equal(result, ref[method_name], self.prec) @@ -1621,13 +1623,15 @@ def test_coordinates(self, ag): def test_principal_axes(self, ag): assert_almost_equal( - ag.principal_axes(), - np.array( - [ - [-1.53389276e-03, -4.41386224e-02, -9.99024239e-01], - [-1.20986911e-02, -9.98951474e-01, 4.41539838e-02], - [-9.99925632e-01, 1.21546132e-02, 9.98264877e-04], - ] + np.absolute(ag.principal_axes()), + np.absolute( + np.array( + [ + [-1.53389276e-03, -4.41386224e-02, -9.99024239e-01], + [-1.20986911e-02, -9.98951474e-01, 4.41539838e-02], + [-9.99925632e-01, 1.21546132e-02, 9.98264877e-04], + ] + ) ), ) diff --git a/testsuite/MDAnalysisTests/core/test_groups.py b/testsuite/MDAnalysisTests/core/test_groups.py index 7cd890b046f..bab59c5875a 100644 --- a/testsuite/MDAnalysisTests/core/test_groups.py +++ b/testsuite/MDAnalysisTests/core/test_groups.py @@ -244,13 +244,14 @@ def test_passive_decorator(self, ag): assert_almost_equal(ag.shape_parameter(), 0.61460819) assert_almost_equal(ag.asphericity(), 0.4892751412) assert_almost_equal( - ag.principal_axes(), - np.array( - [ - [0.7574113, -0.113481, 0.643001], - [0.5896252, 0.5419056, -0.5988993], - [-0.2804821, 0.8327427, 0.4773566], - ] + np.absolute(ag.principal_axes()), + np.absolute(np.array( + [ + [-0.7574113, 0.113481, -0.643001], + [-0.5896252, -0.5419056, 0.5988993], + [-0.2804821, 0.8327427, 0.4773566], + ] + ) ), ) assert_almost_equal( diff --git a/testsuite/MDAnalysisTests/core/test_topologyattrs.py b/testsuite/MDAnalysisTests/core/test_topologyattrs.py index 7f8f766f251..a7474144466 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyattrs.py +++ b/testsuite/MDAnalysisTests/core/test_topologyattrs.py @@ -418,13 +418,15 @@ def ag(self): def test_principal_axes(self, ag): assert_almost_equal( - ag.principal_axes(), - np.array( - [ - [-1.53389276e-03, -4.41386224e-02, -9.99024239e-01], - [-1.20986911e-02, -9.98951474e-01, 4.41539838e-02], - [-9.99925632e-01, 1.21546132e-02, 9.98264877e-04], - ] + np.absolute(ag.principal_axes()), + np.absolute( + np.array( + [ + [-1.53389276e-03, -4.41386224e-02, -9.99024239e-01], + [-1.20986911e-02, -9.98951474e-01, 4.41539838e-02], + [-9.99925632e-01, 1.21546132e-02, 9.98264877e-04], + ] + ) ), ) From fd6fa86143bf3e909a09508a4a0cc88596f0a56a Mon Sep 17 00:00:00 2001 From: IAlibay Date: Mon, 22 Jun 2026 19:24:53 +0100 Subject: [PATCH 3/6] format two files --- testsuite/MDAnalysisTests/core/test_atomgroup.py | 4 +++- testsuite/MDAnalysisTests/core/test_groups.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/testsuite/MDAnalysisTests/core/test_atomgroup.py b/testsuite/MDAnalysisTests/core/test_atomgroup.py index 3fb45aad845..10d043b0fa0 100644 --- a/testsuite/MDAnalysisTests/core/test_atomgroup.py +++ b/testsuite/MDAnalysisTests/core/test_atomgroup.py @@ -1356,7 +1356,9 @@ def test_wrap(self, ag, wrap, ref, method_name): assert_almost_equal(result[0], ref[method_name][0], self.prec) assert_almost_equal(result[1], ref[method_name][1], self.prec) elif method_name == "principal_axes": - assert_almost_equal(np.absolute(result), np.absolute(ref[method_name]), self.prec) + assert_almost_equal( + np.absolute(result), np.absolute(ref[method_name]), self.prec + ) else: assert_almost_equal(result, ref[method_name], self.prec) diff --git a/testsuite/MDAnalysisTests/core/test_groups.py b/testsuite/MDAnalysisTests/core/test_groups.py index bab59c5875a..d8843548027 100644 --- a/testsuite/MDAnalysisTests/core/test_groups.py +++ b/testsuite/MDAnalysisTests/core/test_groups.py @@ -245,7 +245,8 @@ def test_passive_decorator(self, ag): assert_almost_equal(ag.asphericity(), 0.4892751412) assert_almost_equal( np.absolute(ag.principal_axes()), - np.absolute(np.array( + np.absolute( + np.array( [ [-0.7574113, 0.113481, -0.643001], [-0.5896252, -0.5419056, 0.5988993], From 7cecd6003bae8d2c5d344e6cf58e1a89db2c0cc3 Mon Sep 17 00:00:00 2001 From: IAlibay Date: Mon, 22 Jun 2026 23:05:19 +0100 Subject: [PATCH 4/6] Try something a little bit more clever to compare the arrays --- package/CHANGELOG | 2 +- .../MDAnalysisTests/core/test_atomgroup.py | 31 ++++++++++--------- testsuite/MDAnalysisTests/core/test_groups.py | 20 ++++++------ .../core/test_topologyattrs.py | 23 +++++++------- 4 files changed, 39 insertions(+), 37 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 2c84bd4e4c8..5cb5143f6cd 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -25,7 +25,7 @@ Fixes * The `principal_axes` method in :class:`Masses` now uses `np.linalg.eigh` instead of `np.linalg.eig`, improving numerical stability. This may lead to slightly different results from previous - version of MDAnalysis. + version of MDAnalysis (PR #5404). * `MDAnalysis.analysis.nucleicacids.WatsonCrickDist`, `MinorPairDist`, and `MajorPairDist` now match residue names against the full resname instead of only the first character, fixing incorrect behaviour with diff --git a/testsuite/MDAnalysisTests/core/test_atomgroup.py b/testsuite/MDAnalysisTests/core/test_atomgroup.py index 10d043b0fa0..ef12544b7fc 100644 --- a/testsuite/MDAnalysisTests/core/test_atomgroup.py +++ b/testsuite/MDAnalysisTests/core/test_atomgroup.py @@ -1356,9 +1356,11 @@ def test_wrap(self, ag, wrap, ref, method_name): assert_almost_equal(result[0], ref[method_name][0], self.prec) assert_almost_equal(result[1], ref[method_name][1], self.prec) elif method_name == "principal_axes": - assert_almost_equal( - np.absolute(result), np.absolute(ref[method_name]), self.prec - ) + # See PR #5404 + # Get the signs to flip any anti-parallel vectors by before + # comparing the two results arrays + signs = np.sign(np.einsum("ij,ij->i", result, ref[method_name])) + assert_almost_equal(result * signs[:, np.newaxis], ref[method_name], self.prec) else: assert_almost_equal(result, ref[method_name], self.prec) @@ -1624,18 +1626,19 @@ def test_coordinates(self, ag): ) def test_principal_axes(self, ag): - assert_almost_equal( - np.absolute(ag.principal_axes()), - np.absolute( - np.array( - [ - [-1.53389276e-03, -4.41386224e-02, -9.99024239e-01], - [-1.20986911e-02, -9.98951474e-01, 4.41539838e-02], - [-9.99925632e-01, 1.21546132e-02, 9.98264877e-04], - ] - ) - ), + ref = np.array( + [ + [-1.53389276e-03, -4.41386224e-02, -9.99024239e-01], + [-1.20986911e-02, -9.98951474e-01, 4.41539838e-02], + [-9.99925632e-01, 1.21546132e-02, 9.98264877e-04], + ] ) + result = ag.principal_axes() + # See PR #5404 + # Get the signs to flip any anti-parallel vectors by before + # comparing the two results arrays + signs = np.sign(np.einsum("ij,ij->i", result, ref)) + assert_almost_equal(result * signs[:, np.newaxis], ref) def test_principal_axes_duplicates(self, ag): ag2 = ag + ag[0] diff --git a/testsuite/MDAnalysisTests/core/test_groups.py b/testsuite/MDAnalysisTests/core/test_groups.py index d8843548027..c455b96c23e 100644 --- a/testsuite/MDAnalysisTests/core/test_groups.py +++ b/testsuite/MDAnalysisTests/core/test_groups.py @@ -243,18 +243,16 @@ def test_passive_decorator(self, ag): assert_almost_equal(ag.radius_of_gyration(), 2.400527938286) assert_almost_equal(ag.shape_parameter(), 0.61460819) assert_almost_equal(ag.asphericity(), 0.4892751412) - assert_almost_equal( - np.absolute(ag.principal_axes()), - np.absolute( - np.array( - [ - [-0.7574113, 0.113481, -0.643001], - [-0.5896252, -0.5419056, 0.5988993], - [-0.2804821, 0.8327427, 0.4773566], - ] - ) - ), + ref_pa = np.array( + [ + [-0.7574113, 0.113481, -0.643001], + [-0.5896252, -0.5419056, 0.5988993], + [-0.2804821, 0.8327427, 0.4773566], + ] ) + result_pa = ag.principal_axes() + signs_pa = np.sign(np.einsum("ij,ij->i", result_pa, ref_pa)) + assert_almost_equal(result_pa * signs_pa[:, np.newaxis], ref_pa) assert_almost_equal( ag.center_of_charge(), np.array([11.0800112, 8.8885659, -8.9886632]), diff --git a/testsuite/MDAnalysisTests/core/test_topologyattrs.py b/testsuite/MDAnalysisTests/core/test_topologyattrs.py index a7474144466..40c2b0b0ef1 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyattrs.py +++ b/testsuite/MDAnalysisTests/core/test_topologyattrs.py @@ -417,18 +417,19 @@ def ag(self): return universe.atoms # prototypical AtomGroup def test_principal_axes(self, ag): - assert_almost_equal( - np.absolute(ag.principal_axes()), - np.absolute( - np.array( - [ - [-1.53389276e-03, -4.41386224e-02, -9.99024239e-01], - [-1.20986911e-02, -9.98951474e-01, 4.41539838e-02], - [-9.99925632e-01, 1.21546132e-02, 9.98264877e-04], - ] - ) - ), + ref = np.array( + [ + [-1.53389276e-03, -4.41386224e-02, -9.99024239e-01], + [-1.20986911e-02, -9.98951474e-01, 4.41539838e-02], + [-9.99925632e-01, 1.21546132e-02, 9.98264877e-04], + ] ) + result = ag.principal_axes() + # See PR #5404 + # Get the signs to flip any anti-parallel vectors by before + # comparing the two results arrays + signs = np.sign(np.einsum("ij,ij->i", result, ref)) + assert_almost_equal(result * signs[:, np.newaxis], ref) @pytest.fixture() def universe_pa(self): From 0aebde9d83cc56b5964f9e0649c5d87f11cbfee7 Mon Sep 17 00:00:00 2001 From: IAlibay Date: Mon, 22 Jun 2026 23:07:55 +0100 Subject: [PATCH 5/6] add more comments --- testsuite/MDAnalysisTests/core/test_atomgroup.py | 8 ++++++-- testsuite/MDAnalysisTests/core/test_topologyattrs.py | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/testsuite/MDAnalysisTests/core/test_atomgroup.py b/testsuite/MDAnalysisTests/core/test_atomgroup.py index ef12544b7fc..81f757298d3 100644 --- a/testsuite/MDAnalysisTests/core/test_atomgroup.py +++ b/testsuite/MDAnalysisTests/core/test_atomgroup.py @@ -1357,7 +1357,9 @@ def test_wrap(self, ag, wrap, ref, method_name): assert_almost_equal(result[1], ref[method_name][1], self.prec) elif method_name == "principal_axes": # See PR #5404 - # Get the signs to flip any anti-parallel vectors by before + # The direction (sign) of the principal axes is dependent on the + # specific algorithm used, but the direction itself is not physically + # relevant, so we get the signs to flip any anti-parallel vectors before # comparing the two results arrays signs = np.sign(np.einsum("ij,ij->i", result, ref[method_name])) assert_almost_equal(result * signs[:, np.newaxis], ref[method_name], self.prec) @@ -1635,7 +1637,9 @@ def test_principal_axes(self, ag): ) result = ag.principal_axes() # See PR #5404 - # Get the signs to flip any anti-parallel vectors by before + # The direction (sign) of the principal axes is dependent on the + # specific algorithm used, but the direction itself is not physically + # relevant, so we get the signs to flip any anti-parallel vectors before # comparing the two results arrays signs = np.sign(np.einsum("ij,ij->i", result, ref)) assert_almost_equal(result * signs[:, np.newaxis], ref) diff --git a/testsuite/MDAnalysisTests/core/test_topologyattrs.py b/testsuite/MDAnalysisTests/core/test_topologyattrs.py index 40c2b0b0ef1..064b08c65c3 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyattrs.py +++ b/testsuite/MDAnalysisTests/core/test_topologyattrs.py @@ -426,7 +426,9 @@ def test_principal_axes(self, ag): ) result = ag.principal_axes() # See PR #5404 - # Get the signs to flip any anti-parallel vectors by before + # The direction (sign) of the principal axes is dependent on the + # specific algorithm used, but the direction itself is not physically + # relevant, so we get the signs to flip any anti-parallel vectors before # comparing the two results arrays signs = np.sign(np.einsum("ij,ij->i", result, ref)) assert_almost_equal(result * signs[:, np.newaxis], ref) From 2f46b14a840d5de363dfea703e1ab4f2d2a1d86b Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Mon, 22 Jun 2026 16:26:05 -0700 Subject: [PATCH 6/6] Update package/CHANGELOG --- package/CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 5cb5143f6cd..33b21622ab0 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -25,7 +25,7 @@ Fixes * The `principal_axes` method in :class:`Masses` now uses `np.linalg.eigh` instead of `np.linalg.eig`, improving numerical stability. This may lead to slightly different results from previous - version of MDAnalysis (PR #5404). + version of MDAnalysis (#5403, PR #5404). * `MDAnalysis.analysis.nucleicacids.WatsonCrickDist`, `MinorPairDist`, and `MajorPairDist` now match residue names against the full resname instead of only the first character, fixing incorrect behaviour with