diff --git a/.github/workflows/build-matscipy.yml b/.github/workflows/build-matscipy.yml new file mode 100644 index 00000000000..0afaa146933 --- /dev/null +++ b/.github/workflows/build-matscipy.yml @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on the `build` job of +# https://github.com/libAtoms/matscipy/blob/v1.2.0/.github/workflows/wheels.yml +# and the `pytest` step of +# https://github.com/libAtoms/matscipy/blob/v1.2.0/.github/workflows/tests.yml +name: Build matscipy wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/matscipy.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-matscipy.yml' + - 'docs/packages/matscipy.yaml' + - 'patches/matscipy/**' + push: + branches: [main] + paths: + - '.github/workflows/build-matscipy.yml' + - 'docs/packages/matscipy.yaml' + - 'patches/matscipy/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: matscipy + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build matscipy ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 180 + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + python: ["cp312", "cp313", "cp314"] + + env: + MATSCIPY_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout matscipy v${{ env.MATSCIPY_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: libAtoms/matscipy + ref: v${{ env.MATSCIPY_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: Patch matscipy source + run: | + git apply python-wheels/patches/matscipy/${{ env.MATSCIPY_VERSION }}/*.patch + # meson-python reads the wheel version from `git describe` on this tree; a + # dirty tree gives "1.2.0.dirty", which packaging.version.Version rejects + # outright (InvalidVersion), failing the build before compilation even + # starts. Hide just the patched files from dirty-detection instead. + git diff --name-only | xargs git update-index --skip-worktree + + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + only: ${{ matrix.python }}-manylinux_riscv64 + env: + CIBW_BUILD_FRONTEND: build + CIBW_BEFORE_BUILD: pip install meson meson-python ninja + CIBW_ENVIRONMENT: >- + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + PIP_ONLY_BINARY=numpy,scipy,matplotlib,contourpy,kiwisolver,pillow + # Upstream's wheel job only imports the extension; run its tests.yml suite instead. + # ase.constraints.UnitCellFilter (and friends), which this v1.2.0 tag's tests/ + # still imports, was a @deprecated re-export from ase.filters through 3.26.0; + # 3.27.0 drops the re-export entirely (checked gitlab.com/ase/ase directly, + # tag by tag). matscipy's pyproject.toml floor (ase>=3.26.0) doesn't cap the + # other end, so an unconstrained resolve picks today's ase and breaks 29 + # tests / errors on 12 modules. ase<3.27 is its own space-separated token + # here (no quote characters anywhere in this YAML scalar), resolved together + # with matscipy's own floor by the same `pip install` that installs the wheel. + CIBW_TEST_REQUIRES: pytest pytest-subtests pytest-timeout pytest-xdist sympy ase<3.27 + # Upstream's --timeout=600 is sized for x86; test_bop/test_dislocation cases take 350-600s here. + CIBW_TEST_COMMAND: cd {project}/tests && pytest -v --durations=20 --timeout=1800 -n auto + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: matscipy-${{ env.MATSCIPY_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: ./wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish matscipy ${{ matrix.version }} + needs: [setup, build_wheels] + 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: matscipy-${{ matrix.version }}-*-manylinux_riscv64 diff --git a/docs/packages/matscipy.yaml b/docs/packages/matscipy.yaml new file mode 100644 index 00000000000..67ee66b3565 --- /dev/null +++ b/docs/packages/matscipy.yaml @@ -0,0 +1,5 @@ +package-name: matscipy +source-code: https://github.com/libAtoms/matscipy +license: LGPL-2.1-only +versions: +- version: 1.2.0 diff --git a/patches/matscipy/1.2.0/0001-eam-io-index-1-element-arrays-before-int-float.patch b/patches/matscipy/1.2.0/0001-eam-io-index-1-element-arrays-before-int-float.patch new file mode 100644 index 00000000000..2095d3a1252 --- /dev/null +++ b/patches/matscipy/1.2.0/0001-eam-io-index-1-element-arrays-before-int-float.patch @@ -0,0 +1,33 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Thu, 01 Oct 2026 00:00:00 +0000 +Subject: [PATCH] eam/io: index 1-element arrays before int()/float() + +write_eam's "eam" (DYNAMO funcfl) branch calls int(atomic_numbers) and +float(atomic_masses)/float(lattice_parameters) directly on the +EAMParameters fields, which read_eam builds as 1-element numpy arrays +(np.array((int(words[0]),), dtype=int), etc.), not scalars - the +function's own "# FIXME: atomic numbers etc are now arrays, and not +scalars" comment already says as much. NumPy 2.x turned the implicit +conversion of a non-0-d array into a hard TypeError: "only +0-dimensional arrays can be converted to Python scalars", so +test_eam_io.py::test_eam_read_write's round-trip through this branch +fails outright. Indexing [0] before the int()/float() calls is what +the FIXME already describes as the fix; upstream made the same change +in 255201ab (released in 1.3.0). + +Upstream-Status: Backport [https://github.com/libAtoms/matscipy/commit/255201aba4d827bb97dead8964c80e7276161603] + +Signed-off-by: Ludovic Henry +--- +--- a/matscipy/calculators/eam/io.py ++++ b/matscipy/calculators/eam/io.py +@@ -548,7 +548,7 @@ + # parameters unpacked + # FIXME: atomic numbers etc are now arrays, and not scalars + crystal_structures_str = ' '.join(s for s in crystal_structures) +- atline = f"{int(atomic_numbers)} {float(atomic_masses)} {float(lattice_parameters)} {crystal_structures_str}" ++ atline = f"{int(atomic_numbers[0])} {float(atomic_masses[0])} {float(lattice_parameters[0])} {crystal_structures_str}" + parameterline = f'{int(Nrho)}\t{float(drho):.16e}\t{int(Nr)}\t{float(dr):.16e}\t{float(cutoff):.10e}' + potheader = f"# EAM potential from : # {source} \n {atline} \n {parameterline}" + # --- Writing new EAM alloy pot file --- # diff --git a/patches/matscipy/1.2.0/0002-cauchy_born-drop-removed-sqrtm-disp-keyword.patch b/patches/matscipy/1.2.0/0002-cauchy_born-drop-removed-sqrtm-disp-keyword.patch new file mode 100644 index 00000000000..e65d73032a3 --- /dev/null +++ b/patches/matscipy/1.2.0/0002-cauchy_born-drop-removed-sqrtm-disp-keyword.patch @@ -0,0 +1,38 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Thu, 01 Oct 2026 00:00:00 +0000 +Subject: [PATCH] cauchy_born: drop the removed sqrtm(disp=) keyword + +SciPy 1.18 removed the deprecated disp= keyword of scipy.linalg.sqrtm, +so CubicCauchyBorn's two sqrtm(Usqr, disp=True) calls raise +"TypeError: sqrtm() got an unexpected keyword argument 'disp'" and all +nine test_cauchy_born_corrector.py::TestPredictCauchyBornShifts tests +fail. disp=True was the default, so sqrtm(Usqr) is unchanged on older +SciPy. Upstream made the same change in 75f077ff (released in 1.3.0). + +Upstream-Status: Backport [https://github.com/libAtoms/matscipy/commit/75f077ff7de629fc65814b760d6ebd225b630d64] + +Signed-off-by: Ludovic Henry +--- +diff --git a/matscipy/cauchy_born.py b/matscipy/cauchy_born.py +index c84a2534..ad973424 100644 +--- a/matscipy/cauchy_born.py ++++ b/matscipy/cauchy_born.py +@@ -268,7 +268,7 @@ class CubicCauchyBorn: + # get U^2 + Usqr = 2 * E + np.eye(3) + # square root matrix to get U +- U = sqrtm(Usqr, disp=True) ++ U = sqrtm(Usqr) + + # this is just the symmetric stretch tensor, exactly what we need. + x = U +@@ -1330,7 +1330,7 @@ class CubicCauchyBorn: + # get U^2 + Usqr = 2 * E + np.eye(3) + # square root matrix +- U = sqrtm(Usqr, disp=True) ++ U = sqrtm(Usqr) + + # this is just the symmetric stretch tensor, exactly what we need. + x = U diff --git a/patches/matscipy/1.2.0/0003-molecules-lammpsdata-replace-ndarray-resize-for-py314.patch b/patches/matscipy/1.2.0/0003-molecules-lammpsdata-replace-ndarray-resize-for-py314.patch new file mode 100644 index 00000000000..930360d3d63 --- /dev/null +++ b/patches/matscipy/1.2.0/0003-molecules-lammpsdata-replace-ndarray-resize-for-py314.patch @@ -0,0 +1,88 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Thu, 01 Oct 2026 00:00:00 +0000 +Subject: [PATCH] molecules, io/lammpsdata: replace ndarray.resize for Python 3.14 + +From NumPy 2.4 on, ndarray.resize(refcheck=True) on Python >= 3.14 uses +PyUnstable_Object_IsUniquelyReferenced, which an attribute-held array +never satisfies, so Molecules.__init__ and LAMMPSData's setter/reader +raise "ValueError: cannot resize an array that may be referenced by +another object" on cp314 only (test_newmb.py, test_neighbours.py, +test_io.py collection errors, test_manybody_molecules.py setup errors). +Upstream replaced every such resize with a fresh np.empty of the same +dtype in libAtoms/matscipy#312 (commits 1b006eb4 and 3a1ec1dc, released +in 1.3.0); this is those two hunks unchanged. + +Upstream-Status: Backport [https://github.com/libAtoms/matscipy/pull/312] + +Signed-off-by: Ludovic Henry +--- +diff --git a/matscipy/io/lammpsdata.py b/matscipy/io/lammpsdata.py +index 4525a786..e7200f0a 100644 +--- a/matscipy/io/lammpsdata.py ++++ b/matscipy/io/lammpsdata.py +@@ -193,10 +193,15 @@ class LAMMPSData: + """Set data component.""" + if name in self._type_names: + name = self._type_names[name] +- self.__data[name].resize(len(value)) ++ old = self.__data[name] ++ new_arr = np.empty(len(value), dtype=self._dtypes[name]) ++ n = min(len(old), len(value)) ++ if n > 0: ++ new_arr[:n] = old[:n] ++ self.__data[name] = new_arr + self.__data[name]['type'] = value + elif name in self._data_names: +- self.__data[name].resize(len(value)) ++ self.__data[name] = np.empty(len(value), dtype=self._dtypes[name]) + data = self.__data[name] + try: + data[data.dtype.names[-1]] = np.array(value) +@@ -309,16 +314,15 @@ class LAMMPSData: + for linum, line in enumerate(fd): + if 'Masses' in line: + ntypes = type_counts['atom types'] +- self['masses'].resize(ntypes) +- self['masses'][:] = \ ++ self.__data['masses'] = \ + np.genfromtxt(fd, skip_header=1, +- max_rows=ntypes, usecols=(1,)) ++ max_rows=ntypes, usecols=(1,), dtype=self._dtypes['masses']) + + else: + for label in self._data_names: + if self.__headers[label] in line: + nlines = data_counts[label] +- self[label].resize(nlines) ++ self.__data[label] = np.empty(nlines, dtype=self._dtypes[label]) + dtype = self[label].dtype + + raw_dtype = np.dtype([('num', np.int32)] + [ +diff --git a/matscipy/molecules.py b/matscipy/molecules.py +index 4657da62..d9a88996 100644 +--- a/matscipy/molecules.py ++++ b/matscipy/molecules.py +@@ -72,19 +72,19 @@ class Molecules: + self.__dict__[data] = np.array([], dtype=dtype) + + if bonds_connectivity is not None: +- self.bonds.resize(len(bonds_connectivity)) ++ self.bonds = np.empty(len(bonds_connectivity), dtype=self._dtypes["bonds"]) + self.bonds["atoms"][:] = bonds_connectivity + self.bonds["type"][:] = bonds_types \ + if bonds_types is not None else default_type + + if angles_connectivity is not None: +- self.angles.resize(len(angles_connectivity)) ++ self.angles = np.empty(len(angles_connectivity), dtype=self._dtypes["angles"]) + self.angles["atoms"][:] = angles_connectivity + self.angles["type"][:] = angles_types \ + if angles_types is not None else default_type + + if dihedrals_connectivity is not None: +- self.dihedrals.resize(len(dihedrals_connectivity)) ++ self.dihedrals = np.empty(len(dihedrals_connectivity), dtype=self._dtypes["dihedrals"]) + self.dihedrals["atoms"][:] = dihedrals_connectivity + self.dihedrals["type"][:] = dihedrals_types \ + if dihedrals_types is not None else default_type