Skip to content

Native code export for LAMMPS and Python/ASE #78

Native code export for LAMMPS and Python/ASE

Native code export for LAMMPS and Python/ASE #78

Workflow file for this run

name: Export CI
on:
workflow_dispatch:
push:
paths:
- 'export/**'
- '.github/workflows/export-ci.yml'
pull_request:
paths:
- 'export/**'
- '.github/workflows/export-ci.yml'
env:
JULIA_NUM_THREADS: 2
jobs:
build-lammps:
runs-on: ubuntu-latest
outputs:
lammps-version: ${{ steps.lammps-version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
cmake \
libfftw3-dev \
libopenmpi-dev \
mpi-default-bin \
mpi-default-dev
- name: Get LAMMPS version hash
id: lammps-version
run: |
LAMMPS_VERSION=$(git ls-remote https://github.com/lammps/lammps.git refs/heads/stable | awk '{print $1}' | cut -c1-8)
echo "version=$LAMMPS_VERSION" >> $GITHUB_OUTPUT
echo "LAMMPS_VERSION=$LAMMPS_VERSION" >> $GITHUB_ENV
- name: Cache LAMMPS
id: lammps-cache
uses: actions/cache@v4
with:
path: lammps
key: ${{ runner.os }}-lammps-${{ steps.lammps-version.outputs.version }}-plugin-v1
- name: Clone LAMMPS
if: steps.lammps-cache.outputs.cache-hit != 'true'
run: |
git clone -b stable --depth 1 https://github.com/lammps/lammps.git
- name: Build LAMMPS
if: steps.lammps-cache.outputs.cache-hit != 'true'
run: |
cd lammps
mkdir -p build && cd build
cmake ../cmake \
-D PKG_PLUGIN=on \
-D BUILD_SHARED_LIBS=on \
-D BUILD_MPI=on \
-D BUILD_OMP=on \
-D CMAKE_BUILD_TYPE=Release
cmake --build . -j $(nproc)
- name: Upload LAMMPS artifact
uses: actions/upload-artifact@v4
with:
name: lammps-build
path: |
lammps/build/lmp
lammps/build/liblammps.so*
lammps/src/*.h
lammps/src/STUBS/
lammps/src/fmt/
lammps/src/nlohmann/
retention-days: 1
test-etace-export:
name: ETACE Export Tests
runs-on: ubuntu-latest
outputs:
library-built: ${{ steps.compile.outputs.library-built }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Julia
uses: julia-actions/setup-julia@v2
with:
version: '1.12'
- name: Cache Julia packages
uses: actions/cache@v4
with:
path: |
~/.julia/packages
~/.julia/artifacts
~/.julia/compiled
key: ${{ runner.os }}-julia-export-${{ hashFiles('export/Project.toml') }}
restore-keys: |
${{ runner.os }}-julia-export-
- name: Install Julia dependencies
run: |
julia --project=export -e '
using Pkg
Pkg.instantiate()
Pkg.precompile()
'
- name: Run ETACE export tests (polynomial, hermite, multispecies)
run: |
julia --project=export export/test/runtests.jl etace hermite multispecies
- name: Compile ETACE model to shared library
id: compile
run: |
# Use test_etace_model.jl which has all C interface functions (energy, forces, virial)
# Other models like test_etace_energy.jl only have energy functions
cd export/test/build
MODEL_FILE="test_etace_model.jl"
if [ ! -f "$MODEL_FILE" ]; then
echo "No exported model found, creating one..."
julia --project=../../ -e '
include("../../src/export_ace_model.jl")
using ACEpotentials
using ACEpotentials.Models
using ACEpotentials.ETModels
using Lux, LuxCore, Random
# Create minimal ETACE model for testing
elements = (:Si,)
rcut = 5.5
rin0cuts = Models._default_rin0cuts(elements)
rin0cuts = (x -> (rin = x.rin, r0 = x.r0, rcut = rcut)).(rin0cuts)
ace_model = Models.ace_model(;
elements = elements, order = 2, Ytype = :solid,
level = Models.TotalDegree(), max_level = 6, maxl = 2,
pair_maxn = 6, rin0cuts = rin0cuts,
init_WB = :glorot_normal, init_Wpair = :glorot_normal
)
ps, st = Lux.setup(Random.MersenneTwister(1234), ace_model)
et_model = ETModels.convert2et(ace_model)
et_ps, et_st = LuxCore.setup(Random.MersenneTwister(1234), et_model)
# Copy parameters
et_ps.rembed.post.W[:, :, 1] .= ps.rbasis.Wnlq[:, :, 1, 1]
et_ps.readout.W[1, :, 1] .= ps.WB[:, 1]
et_calc = ETModels.ETACEPotential(et_model, et_ps, et_st, rcut)
export_ace_model(et_calc, "test_etace_model.jl"; for_library=true)
'
MODEL_FILE="test_etace_model.jl"
fi
echo "Compiling $MODEL_FILE using JuliaC.jl..."
# Use JuliaC.jl package for compilation (more portable than bundled juliac.jl)
# JuliaC requires: ImageRecipe -> LinkRecipe -> compile_products -> link_products
julia --startup-file=no --project=../../ -e "
using Pkg
Pkg.add(\"JuliaC\")
using JuliaC
# Create image recipe for shared library with ccallable exports
img = ImageRecipe(;
file=\"$MODEL_FILE\",
output_type=\"sharedlib\",
trim_mode=\"safe\",
add_ccallables=true,
verbose=true
)
# Create link recipe to produce the final .so file
link = LinkRecipe(;
image_recipe=img,
outname=\"libace_test.so\"
)
# Compile and link
compile_products(img)
link_products(link)
"
if [ ! -f "libace_test.so" ]; then
echo "::error::Failed to compile shared library"
exit 1
fi
ls -la libace_test.so
echo "library-built=true" >> $GITHUB_OUTPUT
- name: Upload compiled library
if: steps.compile.outputs.library-built == 'true'
uses: actions/upload-artifact@v4
with:
name: ace-library
path: export/test/build/libace_test.so
retention-days: 1
test-python:
runs-on: ubuntu-latest
needs: test-etace-export
if: needs.test-etace-export.outputs.library-built == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download compiled library
uses: actions/download-artifact@v4
with:
name: ace-library
path: export/test/build
- name: Setup Julia
uses: julia-actions/setup-julia@v2
with:
version: '1.12'
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Install Python dependencies
run: |
cd export
uv sync
# Install ase-ace package into the venv for Python calculator tests
uv pip install -e ase-ace
- name: Cache Julia packages
uses: actions/cache@v4
with:
path: |
~/.julia/packages
~/.julia/artifacts
~/.julia/compiled
key: ${{ runner.os }}-julia-export-${{ hashFiles('export/Project.toml') }}
restore-keys: |
${{ runner.os }}-julia-export-
- name: Install Julia dependencies
run: |
julia --project=export -e '
using Pkg
Pkg.instantiate()
'
- name: Run Python tests
run: |
export PATH="${{ github.workspace }}/export/.venv/bin:$PATH"
export VIRTUAL_ENV="${{ github.workspace }}/export/.venv"
julia --project=export export/test/runtests.jl python
test-lammps-serial:
runs-on: ubuntu-latest
needs: [build-lammps, test-etace-export]
if: needs.test-etace-export.outputs.library-built == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download compiled library
uses: actions/download-artifact@v4
with:
name: ace-library
path: export/test/build
- name: Install MPI headers
run: |
sudo apt-get update
sudo apt-get install -y libopenmpi-dev
- name: Setup Julia
uses: julia-actions/setup-julia@v2
with:
version: '1.12'
- name: Download LAMMPS
uses: actions/download-artifact@v4
with:
name: lammps-build
path: lammps
- name: Setup LAMMPS
run: |
chmod +x lammps/build/lmp
echo "$GITHUB_WORKSPACE/lammps/build" >> $GITHUB_PATH
echo "LD_LIBRARY_PATH=$GITHUB_WORKSPACE/lammps/build:$LD_LIBRARY_PATH" >> $GITHUB_ENV
- name: Cache Julia packages
uses: actions/cache@v4
with:
path: |
~/.julia/packages
~/.julia/artifacts
~/.julia/compiled
key: ${{ runner.os }}-julia-export-${{ hashFiles('export/Project.toml') }}
restore-keys: |
${{ runner.os }}-julia-export-
- name: Install Julia dependencies
run: |
julia --project=export -e '
using Pkg
Pkg.instantiate()
'
- name: Build ACE LAMMPS plugin
run: |
cd export/lammps/plugin
mkdir -p build && cd build
cmake ../cmake -DLAMMPS_HEADER_DIR=$GITHUB_WORKSPACE/lammps/src
make -j $(nproc)
- name: Run LAMMPS tests (serial)
env:
LAMMPS_SRC: ${{ github.workspace }}/lammps/src
run: |
julia --project=export export/test/runtests.jl lammps
test-mpi:
runs-on: ubuntu-latest
needs: [build-lammps, test-lammps-serial, test-etace-export]
if: needs.test-etace-export.outputs.library-built == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download compiled library
uses: actions/download-artifact@v4
with:
name: ace-library
path: export/test/build
- name: Setup Julia
uses: julia-actions/setup-julia@v2
with:
version: '1.12'
- name: Install MPI
run: |
sudo apt-get update
sudo apt-get install -y libopenmpi-dev mpi-default-bin
- name: Download LAMMPS
uses: actions/download-artifact@v4
with:
name: lammps-build
path: lammps
- name: Setup LAMMPS
run: |
chmod +x lammps/build/lmp
echo "$GITHUB_WORKSPACE/lammps/build" >> $GITHUB_PATH
echo "LD_LIBRARY_PATH=$GITHUB_WORKSPACE/lammps/build:$LD_LIBRARY_PATH" >> $GITHUB_ENV
- name: Cache Julia packages
uses: actions/cache@v4
with:
path: |
~/.julia/packages
~/.julia/artifacts
~/.julia/compiled
key: ${{ runner.os }}-julia-export-${{ hashFiles('export/Project.toml') }}
restore-keys: |
${{ runner.os }}-julia-export-
- name: Install Julia dependencies
run: |
julia --project=export -e '
using Pkg
Pkg.instantiate()
'
- name: Build ACE LAMMPS plugin
run: |
cd export/lammps/plugin
mkdir -p build && cd build
cmake ../cmake -DLAMMPS_HEADER_DIR=$GITHUB_WORKSPACE/lammps/src
make -j $(nproc)
- name: Run MPI tests
env:
LAMMPS_SRC: ${{ github.workspace }}/lammps/src
OMPI_ALLOW_RUN_AS_ROOT: 1
OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: 1
run: |
julia --project=export export/test/runtests.jl mpi
test-ase-ace-imports:
name: ase-ace (imports and utils)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Julia
uses: julia-actions/setup-julia@v2
with:
version: '1.12'
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install ase-ace package
run: |
cd export/ase-ace
pip install -e ".[dev]"
- name: Run import and utility tests
run: |
cd export/ase-ace
pytest -v tests/test_calculator.py::TestImports tests/test_calculator.py::TestUtilities tests/test_calculator.py::TestCalculatorInit -m "not requires_julia"
test-ase-ace-library:
name: ase-ace (library calculator)
runs-on: ubuntu-latest
needs: test-etace-export
if: needs.test-etace-export.outputs.library-built == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download compiled library
uses: actions/download-artifact@v4
with:
name: ace-library
path: export/test/build
- name: Setup Julia
uses: julia-actions/setup-julia@v2
with:
version: '1.12'
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Cache Julia packages
uses: actions/cache@v4
with:
path: |
~/.julia/packages
~/.julia/artifacts
~/.julia/compiled
key: ${{ runner.os }}-julia-export-${{ hashFiles('export/Project.toml') }}
restore-keys: |
${{ runner.os }}-julia-export-
- name: Install Julia dependencies
run: |
julia --project=export -e '
using Pkg
Pkg.instantiate()
'
- name: Install ase-ace package with library support
run: |
cd export/ase-ace
pip install -e ".[dev,lib]"
- name: Setup library path
run: |
chmod +x ${{ github.workspace }}/export/test/build/libace_test.so || true
echo "LD_LIBRARY_PATH=$(dirname $(julia -e 'print(Sys.BINDIR)'))/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV
- name: Run ase-ace library calculator tests
env:
ACE_TEST_LIBRARY: ${{ github.workspace }}/export/test/build/libace_test.so
run: |
cd export/ase-ace
pytest -v tests/test_library_calculator.py
test-ase-ace-julia:
name: ase-ace (julia calculator)
runs-on: ubuntu-latest
needs: test-etace-export
if: needs.test-etace-export.outputs.library-built == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download compiled library
uses: actions/download-artifact@v4
with:
name: ace-library
path: export/test/build
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Cache Julia packages (juliapkg)
uses: actions/cache@v4
with:
path: |
~/.julia/packages
~/.julia/artifacts
~/.julia/compiled
~/.julia/environments/pyjuliapkg
key: ${{ runner.os }}-juliapkg-${{ hashFiles('export/ase-ace/src/ase_ace/juliapkg.json') }}
restore-keys: |
${{ runner.os }}-juliapkg-
- name: Install ase-ace package with JuliaCall support
run: |
cd export/ase-ace
pip install -e ".[dev,julia,lib]"
- name: Create test model
env:
JULIA_NUM_THREADS: 2
PYTHON_JULIACALL_HANDLE_SIGNALS: yes
working-directory: export/ase-ace
run: |
# Create fixtures directory and a FITTED test model using ase_ace module
# This ensures juliapkg finds the correct juliapkg.json
python3 -c "
import os
os.makedirs('tests/fixtures', exist_ok=True)
# Import ase_ace to trigger juliapkg resolution with our dependencies
from ase_ace.julia_calculator import ACEJuliaCalculator
# Access the Julia runtime to create and fit a test model
jl = ACEJuliaCalculator._init_julia(2)
jl.seval('using ACEfit')
jl.seval('''
# Load training data and fit model (same as export tests)
dataset = ACEpotentials.example_dataset(\\\"Si_tiny\\\")
data = dataset.train
model = ace1_model(elements=[:Si], order=2, totaldegree=6, rcut=5.5)
data_keys = (
energy_key = \\\"dft_energy\\\",
force_key = \\\"dft_force\\\",
virial_key = \\\"dft_virial\\\",
)
weights = Dict(\\\"default\\\" => Dict(\\\"E\\\" => 30.0, \\\"F\\\" => 1.0, \\\"V\\\" => 1.0))
ACEpotentials.acefit!(data, model; data_keys..., weights=weights, solver=ACEfit.BLR())
ACEpotentials.save_model(model, \\\"tests/fixtures/test_model.json\\\")
''')
print('Test model created and fitted successfully')
"
- name: Run ase-ace julia calculator tests
env:
JULIA_NUM_THREADS: 2
PYTHON_JULIACALL_HANDLE_SIGNALS: yes
ACE_TEST_MODEL: ${{ github.workspace }}/export/ase-ace/tests/fixtures/test_model.json
ACE_TEST_LIBRARY: ${{ github.workspace }}/export/test/build/libace_test.so
working-directory: export/ase-ace
run: |
pytest -v tests/test_julia_calculator.py -x