Skip to content

Commit ee7b6e2

Browse files
authored
pyscf: Add version 2.14.0 (#2415)
* pyscf: add version 2.14.0 riscv64 wheel build Upstream ships py3-tagged wheels that are platform-specific fat wheels: no CPython C-API extensions (no Python.h anywhere in pyscf/lib), just CMake-built shared libraries (own code, libcint, libxc, xcfun) loaded via ctypes, so one build serves every interpreter, matching upstream's own aarch64/x86_64 Linux wheel jobs. libcint's x86 SIMD file (gout2e_simd.c) is not wired into its build; pyscf's own C sources gate every x86-specific flag behind a compiler check, so nothing here is riscv64-blocking. Bundles LICENSE files for libcint (Apache-2.0), libxc/xcfun (MPL-2.0) and OpenBLAS (BSD), none of which upstream's own official wheels carry despite linking all four. ENABLE_SMD is left off (matching upstream's own macOS build), which also keeps pyscf's own GPLv3-licensed MNSOL solvent code (pyscf/lib/solvent) out of the wheel. * pyscf: cd out of the checkout before the licence check /work/pyscf still holds pyscf.egg-info from the --no-clean pip wheel build step. python -c puts cwd on sys.path[0], so importlib.metadata resolves that stale egg-info (no licenses/ subdirectory) instead of the installed wheel, and the licence assertion sees an empty set. Move the check after the cd /tmp the later import pyscf smoke test already relies on for the same reason. * pyscf: rename checkout package dir before pytest to avoid on-disk import pytest's --import-mode=importlib still imports a nested test's ancestor packages straight from the checkout's __init__.py by walking up the filesystem, bypassing the installed wheel (gotcha 616). The checkout's unrepaired .so then fails to find libopenblas.so.0, aborting pyscf/lib/__init__.py partway through and cascading into 336 unrelated-looking AttributeErrors. Fix by moving /work/pyscf/pyscf to /work/pyscf/pyscf-src right before the pytest invocation and repointing the collection target and --ignore= globs at the new path, the same approach gotcha 148 already uses. * pyscf: ignore grad/test/test_ucasci.py, which imports a test-only sibling module not part of the installed wheel grad/test/test_ucasci.py does `from pyscf.grad.test.test_casci import _check_fd_grad`, relying on pyscf.grad.test being an importable package - which it is when running against the in-place checkout, but the installed wheel (which the collected tests otherwise correctly exercise per gotcha 616) does not ship its own test suite as an importable subpackage. * pyscf: raise build job timeout to 1440 minutes and drop pytest -q Run 36691144536 attempt 2 was not superseded or cancelled by hand: the build job hit its own timeout-minutes: 720 exactly 12h after it started (17:02:49Z -> 05:03:05Z), with pytest still making steady progress at 88% of the suite. The wheel build takes 3-4h on the riscv64 runner and the test suite (upstream's linux-build-aarch64 CI selection, single-threaded) needs roughly 10h on top, so the job cannot fit in 720 minutes. 1440 is the ceiling the repo's other long single-job builds use. Also drop -q: it hides test names until the final summary, which the timeout cut off, so the ~14 failures already visible in that run could not be identified. Default verbosity prints each test file as it runs, as upstream's own pytest invocation does. * pyscf: run pytest from the checkout root, stop re-importing checkout packages, deselect 8 rounding-edge tests The first complete test run (15 failed, 2690 passed) showed three separate causes: - cd /tmp before pytest anchored pytest.ini's --ignore-glob patterns at /tmp, so upstream's own exclusions (*_slow*.py, *test_bz*, ...) matched nothing. That collected fci/test/test_dhf_slow.py and mcscf/test/test_bz.py (5 failures) plus about 3h of other excluded files. Run from /work/pyscf like upstream's CI, via the pytest script so the cwd stays off sys.path. - After the pyscf -> pyscf-src rename, pytest's Package.setup() resolved checkout package names only up to the non-identifier directory, so grad/__init__.py ran a second time as a top-level `grad`. Its rhf.py rebound the installed scf.hf.RHF.Gradients to a duplicate class, which broke both tools/test/test_finite_diff.py isinstance checks. Delete the checkout's package __init__.py files (no test directory has one). - Deselect 6 PBC XC-stress finite-difference checks, test_davidson_diag_matrix and test_weight_orth. Each one misses by a few ULP-scale rounding differences against a bound that sits at the test's own numerical noise floor.
1 parent 4c256cc commit ee7b6e2

2 files changed

Lines changed: 260 additions & 0 deletions

File tree

‎.github/workflows/build-pyscf.yml‎

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
# SPDX-FileCopyrightText: 2026 The RISE Project
2+
# SPDX-License-Identifier: MIT
3+
---
4+
# This workflow follows upstream's own release-pypi-aarch64 job:
5+
# https://github.com/pyscf/pyscf/blob/master/.github/workflows/publish.yml
6+
name: Build pyscf wheels (riscv64)
7+
8+
on:
9+
workflow_dispatch:
10+
inputs:
11+
version:
12+
description: 'Version glob to (re)build; empty builds every version of docs/packages/pyscf.yaml not released yet'
13+
required: false
14+
default: ''
15+
pull_request:
16+
branches: [main]
17+
paths:
18+
- '.github/workflows/build-pyscf.yml'
19+
- 'docs/packages/pyscf.yaml'
20+
push:
21+
branches: [main]
22+
paths:
23+
- '.github/workflows/build-pyscf.yml'
24+
- 'docs/packages/pyscf.yaml'
25+
26+
concurrency:
27+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
28+
cancel-in-progress: true
29+
30+
permissions:
31+
contents: read
32+
33+
env:
34+
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64
35+
LIBCINT_VERSION: v6.1.3
36+
LIBXC_VERSION: '7.0.0'
37+
XCFUN_REF: a89b783
38+
39+
jobs:
40+
setup:
41+
uses: $/.github/workflows/_setup.yml
42+
with:
43+
package: pyscf
44+
version: ${{ inputs.version }}
45+
46+
build_wheels:
47+
needs: [setup]
48+
if: needs.setup.outputs.versions != '[]'
49+
name: Build pyscf ${{ matrix.version }} py3-manylinux_riscv64
50+
runs-on: ubuntu-24.04-riscv
51+
timeout-minutes: 1440
52+
strategy:
53+
fail-fast: false
54+
matrix:
55+
version: ${{ fromJSON(needs.setup.outputs.versions) }}
56+
env:
57+
PYSCF_VERSION: ${{ matrix.version }}
58+
59+
steps:
60+
- name: Checkout pyscf v${{ env.PYSCF_VERSION }}
61+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
62+
with:
63+
repository: pyscf/pyscf
64+
ref: v${{ env.PYSCF_VERSION }}
65+
path: pyscf
66+
persist-credentials: false
67+
68+
- name: Build wheel
69+
run: |
70+
mkdir -p wheelhouse
71+
set -o pipefail
72+
docker run --rm -i --network=host \
73+
-v "${GITHUB_WORKSPACE}:/work" -w /work/pyscf \
74+
-e LIBCINT_VERSION -e LIBXC_VERSION -e XCFUN_REF \
75+
"${MANYLINUX_RISCV64_IMAGE}" \
76+
bash <<'SCRIPT' 2>&1 | tee build.log
77+
set -eux
78+
79+
dnf -y install openblas-devel gcc
80+
81+
# Upstream's own official Linux wheels ship no licence for any of these;
82+
# send this fix upstream too.
83+
curl -fsSLo LICENSE.libcint "https://raw.githubusercontent.com/sunqm/libcint/${LIBCINT_VERSION}/LICENSE"
84+
curl -fsSLo LICENSE.libxc "https://gitlab.com/libxc/libxc/-/raw/${LIBXC_VERSION}/COPYING"
85+
curl -fsSLo LICENSE.xcfun "https://raw.githubusercontent.com/dftlibs/xcfun/${XCFUN_REF}/LICENSE.md"
86+
cp /usr/share/licenses/openblas/LICENSE LICENSE.openblas
87+
88+
PYTHON_BIN=/opt/python/cp312-cp312/bin/python
89+
export PATH="/opt/python/cp312-cp312/bin:${PATH}"
90+
"${PYTHON_BIN}" -m pip install -q -U pip setuptools wheel auditwheel
91+
92+
export PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/
93+
export PIP_ONLY_BINARY=cmake
94+
# WITH_F12/ENABLE_SMD=OFF matches upstream's own macOS build; the Linux
95+
# jobs turn ENABLE_SMD on, which also links in pyscf's own GPLv3-licensed
96+
# MNSOL solvent code (pyscf/lib/solvent), kept out of scope here.
97+
export CMAKE_CONFIGURE_ARGS="-DWITH_F12=OFF -DENABLE_SMD=OFF"
98+
export CMAKE_BUILD_PARALLEL_LEVEL=4
99+
"${PYTHON_BIN}" -m pip wheel -v --no-deps --no-clean -w /tmp/wheelhouse .
100+
101+
auditwheel repair /tmp/wheelhouse/pyscf-*.whl --lib-sdir /lib -w /work/wheelhouse
102+
SCRIPT
103+
104+
- name: Upload build log
105+
if: failure()
106+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
107+
with:
108+
name: pyscf-${{ env.PYSCF_VERSION }}-build-log
109+
path: build.log
110+
111+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
112+
with:
113+
name: pyscf-${{ env.PYSCF_VERSION }}-py3-manylinux_riscv64
114+
path: wheelhouse/*.whl
115+
if-no-files-found: error
116+
117+
- name: Test wheel
118+
run: |
119+
docker run --rm -i --network=host \
120+
-v "${GITHUB_WORKSPACE}:/work" -w /work/pyscf \
121+
-e PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ \
122+
"${MANYLINUX_RISCV64_IMAGE}" \
123+
bash <<'SCRIPT'
124+
set -eux
125+
126+
PYTHON_BIN=/opt/python/cp312-cp312/bin/python
127+
"${PYTHON_BIN}" -m pip install -q --only-binary=:all: /work/wheelhouse/*.whl
128+
"${PYTHON_BIN}" -m pip install -q pytest
129+
130+
# cd out of /work/pyscf first (gotcha 612): the build step's --no-clean
131+
# pip wheel leaves pyscf.egg-info there, and `python -c` puts cwd on
132+
# sys.path[0], so importlib.metadata resolves that stale egg-info instead
133+
# of the installed wheel and the licence check below sees an empty set.
134+
cd /tmp
135+
licences=$("${PYTHON_BIN}" -c "
136+
import importlib.metadata as m
137+
print(sorted(p.name for p in m.files('pyscf') if '.dist-info/licenses/' in str(p)))
138+
")
139+
echo "$licences"
140+
"${PYTHON_BIN}" -c "
141+
import importlib.metadata as m
142+
names = {p.name for p in m.files('pyscf') if '.dist-info/licenses/' in str(p)}
143+
expected = {'LICENSE', 'NOTICE', 'LICENSE.libcint', 'LICENSE.libxc', 'LICENSE.xcfun', 'LICENSE.openblas'}
144+
assert expected <= names, names
145+
"
146+
147+
echo 'pbc_tools_pbc_fft_engine = "NUMPY"' > .pyscf_conf.py
148+
echo 'scf_hf_SCF_mute_chkfile = True' >> .pyscf_conf.py
149+
export PYSCF_CONFIG_FILE=/tmp/.pyscf_conf.py
150+
export OMP_NUM_THREADS=1
151+
ulimit -s 20000
152+
"${PYTHON_BIN}" -c "import pyscf; from pyscf import gto, scf
153+
mol = gto.M(atom='H 0 0 0; H 0 0 0.74', basis='sto-3g')
154+
mf = scf.RHF(mol).run()
155+
assert abs(mf.e_tot - (-1.1167)) < 1e-3, mf.e_tot
156+
print('HF energy ok:', mf.e_tot)"
157+
158+
# Rename the checkout's package dir before collection (gotcha 618): even under
159+
# pytest.ini's --import-mode=importlib, a test nested inside the package gets its
160+
# ancestor packages imported straight off disk by walking up __init__.py files, so
161+
# pyscf itself loads from this in-place build, not the installed wheel. Renaming
162+
# breaks the name match and pyscf/__init__.py's own `from pyscf import lib` then
163+
# falls through to the installed copy via normal sys.path.
164+
mv /work/pyscf/pyscf /work/pyscf/pyscf-src
165+
# pytest's Package.setup() still imports each package __init__.py it collects
166+
# through, and resolves names only up to the non-identifier `pyscf-src`, so
167+
# grad/__init__.py runs again as a top-level `grad` whose rhf.py rebinds the
168+
# installed scf.hf.RHF.Gradients to a duplicate class (tools/test/test_finite_diff.py
169+
# then fails isinstance(..., GradientsBase)). No pyscf test dir has an __init__.py,
170+
# so dropping these changes nothing collected and imports nothing from the checkout.
171+
find /work/pyscf/pyscf-src -name __init__.py -delete
172+
# Run from the checkout root like upstream's CI: pytest anchors pytest.ini's
173+
# --ignore-glob patterns (*_slow*.py, *test_bz*, ...) at the cwd, so from /tmp none
174+
# matched. The pytest script, unlike `python -m`, keeps the cwd (and gotcha 612's
175+
# stale egg-info) off sys.path.
176+
cd /work/pyscf
177+
# Deselected for riscv64 rounding at the edge of each test's own numerical noise
178+
# (gotcha 304), none is a wrong result:
179+
# - pbc/grad *_stress test_get_vxc_*: analytic XC stress vs a 1e-5 central difference
180+
# of the XC grid energy. The bound (1e-9; 1e-8 for kuks) is only 28-45 ULP of that
181+
# energy; riscv64 lands at 48-67 ULP (1.06e-9 to 1.48e-9; 2.06e-8). The full-SCF
182+
# test_{lda,gga}_vs_finite_difference stress checks in the same files pass.
183+
# - test_davidson_diag_matrix: for a = 2*I the eigenvalue is 2*dot(x, x) of a
184+
# normalised x, and assertAlmostEqual on that size-1 array passes only when it is
185+
# bit-exactly 2.0; a 1-ULP ddot difference raises TypeError (no ndarray.__round__).
186+
# - test_weight_orth: norm of (wSw)^-1/2 for a cond-6e7 random Gram matrix, asserted
187+
# to 8 places. riscv64 is 6.5e-9 off; on x86_64 a 1-ULP perturbation of S alone
188+
# moves it by up to 4.6e-9. Its orthonormality assertion passes.
189+
/opt/python/cp312-cp312/bin/pytest pyscf-src -c pytest.ini \
190+
--ignore=/work/pyscf/pyscf-src/adc \
191+
--ignore=/work/pyscf/pyscf-src/pbc/df \
192+
--ignore=/work/pyscf/pyscf-src/pbc/cc \
193+
--ignore=/work/pyscf/pyscf-src/solvent \
194+
--ignore=/work/pyscf/pyscf-src/grad/test/test_ucasci.py \
195+
--deselect pyscf-src/pbc/grad/test/test_krks_stress.py::KnownValues::test_get_vxc_gga \
196+
--deselect pyscf-src/pbc/grad/test/test_krks_stress.py::KnownValues::test_get_vxc_lda \
197+
--deselect pyscf-src/pbc/grad/test/test_kuks_stress.py::KnownValues::test_get_vxc_gga \
198+
--deselect pyscf-src/pbc/grad/test/test_rks_stress.py::KnownValues::test_get_vxc_gga \
199+
--deselect pyscf-src/pbc/grad/test/test_rks_stress.py::KnownValues::test_get_vxc_lda \
200+
--deselect pyscf-src/pbc/grad/test/test_rks_stress.py::KnownValues::test_get_vxc_mgga \
201+
--deselect pyscf-src/lib/test/test_linalg_helper.py::KnownValues::test_davidson_diag_matrix \
202+
--deselect pyscf-src/lo/test/test_orth.py::KnownValues::test_weight_orth
203+
SCRIPT
204+
205+
gpl_sources:
206+
needs: [setup]
207+
if: needs.setup.outputs.versions != '[]'
208+
strategy:
209+
fail-fast: false
210+
matrix:
211+
version: ${{ fromJSON(needs.setup.outputs.versions) }}
212+
name: Collect GPL sources
213+
runs-on: ubuntu-24.04-riscv
214+
215+
env:
216+
PYSCF_VERSION: ${{ matrix.version }}
217+
218+
steps:
219+
- name: Checkout python-wheels
220+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
221+
with:
222+
persist-credentials: false
223+
224+
# OpenBLAS is compiled with gfortran and pyscf links OpenMP, so
225+
# auditwheel vendors the image's libgfortran and libgomp.
226+
- uses: ./actions/collect-gpl-sources
227+
with:
228+
image: ${{ env.MANYLINUX_RISCV64_IMAGE }}
229+
packages: gcc
230+
output: gpl-sources.tar
231+
232+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
233+
with:
234+
name: pyscf-${{ env.PYSCF_VERSION }}-gpl-sources
235+
path: gpl-sources.tar
236+
if-no-files-found: error
237+
238+
publish:
239+
name: Publish pyscf ${{ matrix.version }}
240+
needs: [setup, build_wheels, gpl_sources]
241+
if: needs.setup.outputs.versions != '[]'
242+
strategy:
243+
fail-fast: false
244+
matrix:
245+
version: ${{ fromJSON(needs.setup.outputs.versions) }}
246+
permissions:
247+
contents: write
248+
pull-requests: write
249+
uses: $/.github/workflows/_publish-wheel.yml
250+
secrets:
251+
app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }}
252+
with:
253+
artifact-pattern: pyscf-${{ matrix.version }}-py3-manylinux_riscv64
254+
gpl-sources-artifact: pyscf-${{ matrix.version }}-gpl-sources
255+
gpl-sources-description: gcc

‎docs/packages/pyscf.yaml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package-name: pyscf
2+
source-code: https://github.com/pyscf/pyscf
3+
license: Apache-2.0
4+
versions:
5+
- version: 2.14.0

0 commit comments

Comments
 (0)