From 84345102743107f3d60bffca4c1cef2b33cb863a Mon Sep 17 00:00:00 2001 From: Mathieu Salanne Date: Thu, 22 Oct 2026 18:58:49 +0800 Subject: [PATCH 1/3] Add GitHub Actions CI for standalone LPC3D --- .github/workflows/ci.yml | 62 ++++++++++++++++++++++++ tests/ci/bulk/lattice_gas.inpt | 21 ++++++++ tests/ci/check_outputs.py | 48 ++++++++++++++++++ tests/ci/supercapacitor/lattice_gas.inpt | 37 ++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 tests/ci/bulk/lattice_gas.inpt create mode 100644 tests/ci/check_outputs.py create mode 100644 tests/ci/supercapacitor/lattice_gas.inpt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..79f7dc3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: LPC3D CI + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + lpc3d-cpu: + name: LPC3D (CPU) - ${{ matrix.case }} - Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.12"] + case: + - bulk + - supercapacitor + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pystencils numba ipython matplotlib pyevtk + pip freeze > pip-freeze.txt + + - name: Prepare run directory + run: | + mkdir -p run + cp tests/ci/${{ matrix.case }}/lattice_gas.inpt run/ + if [ "${{ matrix.case }}" = "supercapacitor" ]; then + cp examples/short-test/psd.txt examples/short-test/pore_dens_freq_2neg.txt run/ + fi + + - name: Run LPC3D + working-directory: run + env: + OMP_NUM_THREADS: "2" + NUMBA_NUM_THREADS: "2" + run: python3 ../LPC3D.py -i lattice_gas.inpt | tee log.lpc3d + + - name: Check outputs + run: | + if [ "${{ matrix.case }}" = "supercapacitor" ]; then NB=3; else NB=1; fi + python3 tests/ci/check_outputs.py run $NB + + - name: Upload outputs and environment + if: always() + uses: actions/upload-artifact@v4 + with: + name: lpc3d-${{ matrix.case }}-py${{ matrix.python-version }} + path: | + run/ + pip-freeze.txt diff --git a/tests/ci/bulk/lattice_gas.inpt b/tests/ci/bulk/lattice_gas.inpt new file mode 100644 index 0000000..bd7aacf --- /dev/null +++ b/tests/ci/bulk/lattice_gas.inpt @@ -0,0 +1,21 @@ +cpu +10 10 10 +true true true +1.0 +5e-6 +1 +298 +282 +100 +0.0 +0 +bulk +1 + +bulk +1 10 +1 10 +1 10 +0.296297 +0.000 + diff --git a/tests/ci/check_outputs.py b/tests/ci/check_outputs.py new file mode 100644 index 0000000..612d983 --- /dev/null +++ b/tests/ci/check_outputs.py @@ -0,0 +1,48 @@ +"""Check that an LPC3D run produced the expected, valid output files. + +Usage: python3 check_outputs.py +""" +import sys +from pathlib import Path + +import numpy as np + + +def main(): + run_dir = Path(sys.argv[1]) + nblocks = int(sys.argv[2]) + errors = [] + + for b in range(1, nblocks + 1): + expected = [ + f"density_{b}.txt", + f"vacf_{b}.txt", + f"Diffusion_coeff_x_{b}.txt", + f"Diffusion_coeff_y_{b}.txt", + f"Diffusion_coeff_z_{b}.txt", + f"FT_signal-1-bloc-{b}.dat", + f"Normalised_spectrum-1-bloc-{b}.dat", + ] + for name in expected: + path = run_dir / name + if not path.is_file() or path.stat().st_size == 0: + errors.append(f"missing or empty: {name}") + continue + try: + data = np.loadtxt(path, ndmin=1) + except ValueError as exc: + errors.append(f"unreadable: {name} ({exc})") + continue + if not np.all(np.isfinite(data)): + errors.append(f"NaN or Inf values in: {name}") + + if errors: + print("Output check FAILED:") + for e in errors: + print(" -", e) + sys.exit(1) + print(f"Output check passed for {nblocks} block(s) in {run_dir}") + + +if __name__ == "__main__": + main() diff --git a/tests/ci/supercapacitor/lattice_gas.inpt b/tests/ci/supercapacitor/lattice_gas.inpt new file mode 100644 index 0000000..6c9b10b --- /dev/null +++ b/tests/ci/supercapacitor/lattice_gas.inpt @@ -0,0 +1,37 @@ +cpu +10 10 30 +true true false +1.0 +5e-6 +1 +298 +282 +100 +0.0 +0 +porous_carbon +3 + +electrode +1 +1 10 +1 10 +1 10 +psd.txt +pore_dens_freq_2neg.txt + +bulk +1 10 +1 10 +11 20 +0.296297 +0.000 + +electrode +1 +1 10 +1 10 +21 30 +psd.txt +pore_dens_freq_2neg.txt + From d44deb53e09f60c0566b5e30d3f86e60420ec015 Mon Sep 17 00:00:00 2001 From: Mathieu Salanne Date: Thu, 22 Oct 2026 19:11:27 +0800 Subject: [PATCH 2/3] Fix CI: pin pystencils<2, fail on errors, support recent NumPy --- .github/workflows/ci.yml | 7 +++++-- LPC3D.py | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79f7dc3..927349d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pystencils numba ipython matplotlib pyevtk + # LPC3D uses pystencils.session, removed in pystencils 2.x + pip install "pystencils<2" numba ipython matplotlib pyevtk pip freeze > pip-freeze.txt - name: Prepare run directory @@ -45,7 +46,9 @@ jobs: env: OMP_NUM_THREADS: "2" NUMBA_NUM_THREADS: "2" - run: python3 ../LPC3D.py -i lattice_gas.inpt | tee log.lpc3d + run: | + set -o pipefail + python3 ../LPC3D.py -i lattice_gas.inpt | tee log.lpc3d - name: Check outputs run: | diff --git a/LPC3D.py b/LPC3D.py index ac43f4d..e0cc7ab 100644 --- a/LPC3D.py +++ b/LPC3D.py @@ -492,7 +492,9 @@ def fourier_transform(ReGtotsmp, ImGtotsmp, Nvalues, dwell, larmorfreq, tau, nsa minvalue = np.min(NMR_spec) NMR_spec -= minvalue - save = np.trapz(NMR_spec, NMR_freq) + # np.trapz was renamed np.trapezoid in NumPy 2.0 and later removed + trapezoid = getattr(np, "trapezoid", None) or np.trapz + save = trapezoid(NMR_spec, NMR_freq) name = f"Normalised_spectrum-{nsample}-bloc-{nb}.dat" with open(name, "w") as outFT: From 05323b4b1ddd46abe4e2e4623565bc623a03c791 Mon Sep 17 00:00:00 2001 From: Mathieu Salanne Date: Thu, 22 Oct 2026 19:13:35 +0800 Subject: [PATCH 3/3] Updated Readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 452e440..9575215 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ Here, a manual and example input files are provided. Compared to the previous version of the program, written in C and serial (https://github.com/cmerlet/LPC3D-C-serial), this code is written using pystencils (https://pypi.org/project/pystencils/), is parallel and can use CPU and GPU. +## Continuous integration + +![CI](https://github.com/multixscale/LPC3D/actions/workflows/ci.yml/badge.svg) + ## Multiscale coupling with LAMMPS The `LAMMPS-LPC3D/` folder contains a coupled version of the code, in which LPC3D drives an ensemble of independent LAMMPS simulations, one for each pore size. The pore-resolved densities obtained from molecular dynamics are transferred on the fly to the lattice gas model. The coupling is implemented in Python with mpi4py and the LAMMPS Python interface, and can be run on CPU or GPU (LAMMPS with Kokkos). Examples for both modes are provided in `LAMMPS-LPC3D/Examples/`, and the installation, input format and usage are described in the manual.