Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions .github/workflows/build-mdanalysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# 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'
- '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 }}
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: 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:
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
6 changes: 6 additions & 0 deletions docs/packages/mdanalysis.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package-name: mdanalysis
source-code: https://github.com/MDAnalysis/mdanalysis
license: LGPL-3.0-or-later
versions:
- version: 2.10.0
patched: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
From 8783f8d94033e82c1a9016d42e70566e4300db61 Mon Sep 17 00:00:00 2001
From: Sai Udayagiri <saibabu.udayagiri@gmail.com>
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]
172 changes: 172 additions & 0 deletions patches/mdanalysis/2.10.0/0002-Fix-compatibility-with-numpy-2.5.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
From f765ecd77c8d51d79bec87c9780a0fab80c7d43c Mon Sep 17 00:00:00 2001
From: Irfan Alibay <IAlibay@users.noreply.github.com>
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):
Loading