From 2e461189e1d208ca340380d3e3a05936f4e17fd1 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Fri, 2 Oct 2026 18:25:09 +0000 Subject: [PATCH 1/2] mdanalysis: Add version 2.10.0 --- .github/workflows/build-mdanalysis.yml | 158 +++++++++++++++++++++++++ docs/packages/mdanalysis.yaml | 5 + 2 files changed, 163 insertions(+) create mode 100644 .github/workflows/build-mdanalysis.yml create mode 100644 docs/packages/mdanalysis.yaml diff --git a/.github/workflows/build-mdanalysis.yml b/.github/workflows/build-mdanalysis.yml new file mode 100644 index 00000000000..e90410b482e --- /dev/null +++ b/.github/workflows/build-mdanalysis.yml @@ -0,0 +1,158 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on: https://github.com/MDAnalysis/mdanalysis/blob/release-2.10.0/.github/workflows/deploy.yaml +# and the sdist_check_and_build test job of: https://github.com/MDAnalysis/mdanalysis/blob/release-2.10.0/.github/workflows/gh-ci.yaml +name: Build mdanalysis wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/mdanalysis.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-mdanalysis.yml' + - 'docs/packages/mdanalysis.yaml' + push: + branches: [main] + paths: + - '.github/workflows/build-mdanalysis.yml' + - 'docs/packages/mdanalysis.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: mdanalysis + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build mdanalysis ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 300 + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + python: ["cp312", "cp313", "cp314"] + + env: + MDANALYSIS_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout mdanalysis ${{ env.MDANALYSIS_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: MDAnalysis/mdanalysis + ref: release-${{ env.MDANALYSIS_VERSION }} + fetch-depth: 0 + persist-credentials: false + + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + package-dir: package + output-dir: wheelhouse/ + only: ${{ matrix.python }}-manylinux_riscv64 + env: + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + CIBW_ENVIRONMENT: >- + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + PIP_ONLY_BINARY=numpy,scipy,matplotlib,contourpy,kiwisolver,pillow,fonttools,msgpack,hypothesis + # Upstream's wheel job runs no tests; run the suite of its sdist install check + # against the wheel, with MDAnalysisTests installed from the same checkout. + CIBW_BEFORE_TEST: pip install --no-deps {project}/testsuite + CIBW_TEST_REQUIRES: pytest hypothesis pytest-xdist pytest-timeout + CIBW_TEST_COMMAND: python -m pytest --timeout=200 -n logical --pyargs MDAnalysisTests + + - name: Check the wheel ships the licences and the compiled extensions + run: | + python3 - wheelhouse/*.whl <<'EOF' + import sys, zipfile + + names = zipfile.ZipFile(sys.argv[1]).namelist() + exts = {n.split("/")[-1].split(".")[0] for n in names if n.endswith(".so") and ".libs/" not in n} + expected = { + "affinityprop", "cutils", "stochasticproxembed", "timestep", "_augment", "_cutil", + "_transformations", "c_distances", "c_distances_openmp", "cython_util", "libdcd", + "libmdaxdr", "nsgrid", "qcprot", + } + assert exts == expected, exts + licences = {n.rsplit("/", 1)[-1] for n in names if ".dist-info/licenses/" in n} - {""} + assert licences == {"LICENSE", "AUTHORS"}, licences + # -fopenmp makes auditwheel vendor the image's libgomp, hence the gpl_sources job. + assert any(n.startswith("mdanalysis.libs/libgomp") for n in names), [n for n in names if ".libs/" in n] + EOF + + - name: Store wheels + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: mdanalysis-${{ env.MDANALYSIS_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: ./wheelhouse/*.whl + if-no-files-found: error + + gpl_sources: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + name: Collect GPL sources + runs-on: ubuntu-24.04-riscv + + env: + MDANALYSIS_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: ./actions/collect-gpl-sources + with: + image: ${{ env.MANYLINUX_RISCV64_IMAGE }} + packages: gcc + output: gpl-sources.tar + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: mdanalysis-${{ env.MDANALYSIS_VERSION }}-gpl-sources + path: gpl-sources.tar + if-no-files-found: error + + publish: + name: Publish mdanalysis ${{ matrix.version }} + needs: [setup, build_wheels, gpl_sources] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + secrets: + app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }} + with: + artifact-pattern: mdanalysis-${{ matrix.version }}-*-manylinux_riscv64 + gpl-sources-artifact: mdanalysis-${{ matrix.version }}-gpl-sources + gpl-sources-description: gcc diff --git a/docs/packages/mdanalysis.yaml b/docs/packages/mdanalysis.yaml new file mode 100644 index 00000000000..59a2d8e8a5d --- /dev/null +++ b/docs/packages/mdanalysis.yaml @@ -0,0 +1,5 @@ +package-name: mdanalysis +source-code: https://github.com/MDAnalysis/mdanalysis +license: LGPL-3.0-or-later +versions: +- version: 2.10.0 From 90357593ef110b84d84ab3735fcc45e016f639fa Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Fri, 2 Oct 2026 20:02:12 +0000 Subject: [PATCH 2/2] mdanalysis: Backport the numpy 2.5 and pytest 9.1 test fixes The released 2.10.0 fails its own suite with today's dependencies on every architecture: numpy 2.5's linalg.eig returns complex eigenvectors, which breaks AtomGroup.align_principal_axis(), and pytest 9.1 rejects a trailing comma in a parametrize argnames string, which fails collection of TestAtomGroupToTopology. Both are fixed upstream for 2.11.0. --- .github/workflows/build-mdanalysis.yml | 11 ++ docs/packages/mdanalysis.yaml | 1 + ...0001-Fixed-typo-in-test_atomgroup.py.patch | 31 ++++ ...002-Fix-compatibility-with-numpy-2.5.patch | 172 ++++++++++++++++++ 4 files changed, 215 insertions(+) create mode 100644 patches/mdanalysis/2.10.0/0001-Fixed-typo-in-test_atomgroup.py.patch create mode 100644 patches/mdanalysis/2.10.0/0002-Fix-compatibility-with-numpy-2.5.patch diff --git a/.github/workflows/build-mdanalysis.yml b/.github/workflows/build-mdanalysis.yml index e90410b482e..b1bc635dd02 100644 --- a/.github/workflows/build-mdanalysis.yml +++ b/.github/workflows/build-mdanalysis.yml @@ -17,11 +17,13 @@ on: paths: - '.github/workflows/build-mdanalysis.yml' - 'docs/packages/mdanalysis.yaml' + - 'patches/mdanalysis/**' push: branches: [main] paths: - '.github/workflows/build-mdanalysis.yml' - 'docs/packages/mdanalysis.yaml' + - 'patches/mdanalysis/**' concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -64,6 +66,15 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Apply patches + run: git apply python-wheels/patches/mdanalysis/${{ env.MDANALYSIS_VERSION }}/*.patch + - name: Build wheels uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 with: diff --git a/docs/packages/mdanalysis.yaml b/docs/packages/mdanalysis.yaml index 59a2d8e8a5d..ffe0ff140fc 100644 --- a/docs/packages/mdanalysis.yaml +++ b/docs/packages/mdanalysis.yaml @@ -3,3 +3,4 @@ source-code: https://github.com/MDAnalysis/mdanalysis license: LGPL-3.0-or-later versions: - version: 2.10.0 + patched: true diff --git a/patches/mdanalysis/2.10.0/0001-Fixed-typo-in-test_atomgroup.py.patch b/patches/mdanalysis/2.10.0/0001-Fixed-typo-in-test_atomgroup.py.patch new file mode 100644 index 00000000000..1b11fcde718 --- /dev/null +++ b/patches/mdanalysis/2.10.0/0001-Fixed-typo-in-test_atomgroup.py.patch @@ -0,0 +1,31 @@ +From 8783f8d94033e82c1a9016d42e70566e4300db61 Mon Sep 17 00:00:00 2001 +From: Sai Udayagiri +Date: Mon, 22 Jun 2026 14:01:47 +0530 +Subject: [PATCH] Fixed typo in `test_atomgroup.py` + +* fixed typo in test_atomgroup.py + +pytest 9.1 reads a trailing comma in a parametrize argnames string as a +one-element tuple of names, so it unpacks every value: "btype," with +"bond" fails collection of TestAtomGroupToTopology ("the number of names +(1) must be equal to the number of values (4)"), and with it the whole +pytest run, on every architecture. Older pytest stripped the comma. + +Upstream-Status: Backport [https://github.com/MDAnalysis/mdanalysis/commit/8783f8d94033e82c1a9016d42e70566e4300db61] +--- + testsuite/MDAnalysisTests/core/test_atomgroup.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/testsuite/MDAnalysisTests/core/test_atomgroup.py b/testsuite/MDAnalysisTests/core/test_atomgroup.py +index 38432e6..b285a35 100644 +--- a/testsuite/MDAnalysisTests/core/test_atomgroup.py ++++ b/testsuite/MDAnalysisTests/core/test_atomgroup.py +@@ -88,7 +88,7 @@ class TestAtomGroupToTopology(object): + assert isinstance(imp, ImproperDihedral) + + @pytest.mark.parametrize( +- "btype,", ["bond", "angle", "dihedral", "improper"] ++ "btype", ["bond", "angle", "dihedral", "improper"] + ) + def test_VE(self, btype, u): + ag = u.atoms[:10] diff --git a/patches/mdanalysis/2.10.0/0002-Fix-compatibility-with-numpy-2.5.patch b/patches/mdanalysis/2.10.0/0002-Fix-compatibility-with-numpy-2.5.patch new file mode 100644 index 00000000000..c6da755c04c --- /dev/null +++ b/patches/mdanalysis/2.10.0/0002-Fix-compatibility-with-numpy-2.5.patch @@ -0,0 +1,172 @@ +From f765ecd77c8d51d79bec87c9780a0fab80c7d43c Mon Sep 17 00:00:00 2001 +From: Irfan Alibay +Date: Tue, 23 Jun 2026 08:24:58 +0100 +Subject: [PATCH] Fix compatibility with numpy 2.5 (#5404) + +numpy 2.5's np.linalg.eig returns complex eigenvectors even for the real +symmetric moment of inertia tensor, so Masses.principal_axes() hands +complex vectors on and align_principal_axis() raises "ufunc 'degrees' +not supported for the input types" with the current numpy on every +architecture. Use np.linalg.eigh, which is meant for symmetric matrices +and returns real results, and update the reference values of the tests +whose axis signs change with it (upstream issue #5403). The CHANGELOG +hunk is left out. + +Upstream-Status: Backport [https://github.com/MDAnalysis/mdanalysis/pull/5404] +--- + package/MDAnalysis/core/topologyattrs.py | 2 +- + .../MDAnalysisTests/core/test_atomgroup.py | 39 ++++++++++++------- + testsuite/MDAnalysisTests/core/test_groups.py | 18 ++++----- + .../core/test_topologyattrs.py | 23 ++++++----- + 4 files changed, 50 insertions(+), 32 deletions(-) + +diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py +index 359bd20..63a637c 100644 +--- a/package/MDAnalysis/core/topologyattrs.py ++++ b/package/MDAnalysis/core/topologyattrs.py +@@ -2084,7 +2084,7 @@ class Masses(AtomAttr): + 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 b285a35..81f7572 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], + ] + ), +@@ -1355,6 +1355,14 @@ class TestPBCFlag(object): + 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": ++ # See PR #5404 ++ # 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) + else: + assert_almost_equal(result, ref[method_name], self.prec) + +@@ -1620,16 +1628,21 @@ class TestAtomGroup(object): + ) + + 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], +- ] +- ), ++ 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 ++ # 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) + + 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 7cd890b..c455b96 100644 +--- a/testsuite/MDAnalysisTests/core/test_groups.py ++++ b/testsuite/MDAnalysisTests/core/test_groups.py +@@ -243,16 +243,16 @@ class TestEmptyAtomGroup(object): + 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( +- ag.principal_axes(), +- 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 5155933..064b08c 100644 +--- a/testsuite/MDAnalysisTests/core/test_topologyattrs.py ++++ b/testsuite/MDAnalysisTests/core/test_topologyattrs.py +@@ -417,16 +417,21 @@ class TestAttr(object): + return universe.atoms # prototypical AtomGroup + + 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], +- ] +- ), ++ 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 ++ # 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) + + @pytest.fixture() + def universe_pa(self):