Native code export for LAMMPS and Python/ASE #78
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |