├── .github └── workflows │ ├── ci.yml │ ├── docs.yml │ └── publish-on-pypi.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── aiida_mlip ├── __init__.py ├── calculations │ ├── __init__.py │ ├── base.py │ ├── descriptors.py │ ├── geomopt.py │ ├── md.py │ ├── singlepoint.py │ └── train.py ├── data │ ├── __init__.py │ ├── config.py │ └── model.py ├── helpers │ ├── __init__.py │ ├── converters.py │ └── help_load.py ├── parsers │ ├── __init__.py │ ├── base_parser.py │ ├── descriptors_parser.py │ ├── md_parser.py │ ├── opt_parser.py │ ├── sp_parser.py │ └── train_parser.py └── workflows │ ├── __init__.py │ └── ht_workgraph.py ├── docs ├── .gitignore ├── Makefile └── source │ ├── apidoc │ ├── aiida_mlip.calculations.rst │ ├── aiida_mlip.data.rst │ ├── aiida_mlip.helpers.rst │ ├── aiida_mlip.parsers.rst │ ├── aiida_mlip.rst │ └── aiida_mlip.workflows.rst │ ├── conf.py │ ├── developer_guide │ └── index.rst │ ├── images │ ├── AiiDA_transparent_logo.png │ ├── aiida-mlip-100.png │ ├── aiida-mlip.png │ ├── alc-100.webp │ ├── cosec-100.webp │ └── psdi-100.webp │ ├── index.rst │ └── user_guide │ ├── calculations.rst │ ├── data.rst │ ├── get_started.rst │ ├── index.rst │ ├── training.rst │ └── tutorial.rst ├── examples ├── calculations │ ├── submit_descriptors.py │ ├── submit_geomopt.py │ ├── submit_md.py │ ├── submit_md_using_config.py │ ├── submit_singlepoint.py │ ├── submit_train.py │ └── submit_using_config.py ├── tutorials │ ├── config_computer.yml │ ├── config_profile.yml │ ├── config_sp.yaml │ ├── descriptors.ipynb │ ├── geometry-optimisation.ipynb │ ├── high-throughput-screening.ipynb │ ├── high_throughput_workgraph.ipynb │ ├── setup-janus-code.ipynb │ ├── singlepoint.ipynb │ └── structures │ │ ├── qmof-00d09fe.cif │ │ ├── qmof-013ec70.cif │ │ └── qmof-ffeef76.cif └── workflows │ └── submit_ht_workgraph.py ├── pyproject.toml ├── tests ├── __init__.py ├── calculations │ ├── configs │ │ ├── config_janus.yaml │ │ ├── config_janus_md.yaml │ │ ├── config_noarch.yml │ │ ├── config_nomodel.yml │ │ ├── mlip_train.yml │ │ ├── test.model │ │ └── test_compiled.model │ ├── structures │ │ ├── NaCl.cif │ │ ├── mlip_test.xyz │ │ ├── mlip_train.xyz │ │ ├── mlip_valid.xyz │ │ └── traj.xyz │ ├── test_descriptors.py │ ├── test_geomopt.py │ ├── test_md.py │ ├── test_singlepoint.py │ └── test_train.py ├── conftest.py ├── data │ ├── input_files │ │ ├── mace │ │ │ └── mace_mp_small.model │ │ └── model_local_file.txt │ ├── test_config.py │ └── test_model.py ├── helpers │ ├── test_converters.py │ └── test_load.py └── workflows │ ├── structures │ ├── H2O.xyz │ └── methane.xyz │ └── test_ht.py └── tox.ini /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | tests: 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 30 10 | strategy: 11 | matrix: 12 | python-version: ["3.10", "3.11", "3.12"] 13 | aiida-version: ["stable"] 14 | 15 | services: 16 | postgres: 17 | image: postgres:10 18 | env: 19 | POSTGRES_DB: test_aiida 20 | POSTGRES_PASSWORD: '' 21 | POSTGRES_HOST_AUTH_METHOD: trust 22 | options: >- 23 | --health-cmd pg_isready 24 | --health-interval 10s 25 | --health-timeout 5s 26 | --health-retries 5 27 | ports: 28 | - 5432:5432 29 | rabbitmq: 30 | image: rabbitmq:latest 31 | ports: 32 | - 5672:5672 33 | 34 | steps: 35 | - uses: actions/checkout@v4 36 | 37 | - name: Install uv 38 | uses: astral-sh/setup-uv@v5 39 | with: 40 | version: "0.6.14" 41 | python-version: ${{ matrix.python-version }} 42 | 43 | - name: Install dependencies 44 | run: | 45 | uv sync --all-extras 46 | 47 | - name: Run test suite 48 | env: 49 | # show timings of tests 50 | PYTEST_ADDOPTS: "--durations=0" 51 | run: uv run pytest --cov aiida_mlip --cov-append . 52 | 53 | - name: Report coverage to Coveralls 54 | uses: coverallsapp/github-action@v2 55 | with: 56 | parallel: true 57 | flag-name: run-${{ matrix.python-version }} 58 | file: coverage.xml 59 | base-path: aiida_mlip 60 | 61 | coverage: 62 | needs: tests 63 | runs-on: ubuntu-latest 64 | steps: 65 | - name: Close parallel build 66 | uses: coverallsapp/github-action@v2 67 | with: 68 | parallel-finished: true 69 | 70 | docs: 71 | runs-on: ubuntu-latest 72 | timeout-minutes: 15 73 | steps: 74 | - uses: actions/checkout@v4 75 | 76 | - name: Install uv 77 | uses: astral-sh/setup-uv@v5 78 | with: 79 | version: "0.6.14" 80 | python-version: "3.12" 81 | 82 | - name: Install dependencies 83 | run: uv sync 84 | 85 | - name: Build docs 86 | run: uv run make html 87 | working-directory: ./docs/ 88 | 89 | pre-commit: 90 | runs-on: ubuntu-latest 91 | timeout-minutes: 15 92 | steps: 93 | - uses: actions/checkout@v4 94 | 95 | - name: Install uv 96 | uses: astral-sh/setup-uv@v5 97 | with: 98 | version: "0.6.14" 99 | python-version: "3.12" 100 | 101 | - name: Install dependencies 102 | run: uv sync 103 | 104 | - name: Run pre-commit 105 | run: | 106 | uv run pre-commit install 107 | uv run pre-commit run --all-files || ( git status --short ; git diff ; exit 1 ) 108 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | # Allow one concurrent deployment 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | docs-deploy: 20 | if: github.ref == 'refs/heads/main' && github.repository == 'stfc/aiida-mlip' 21 | environment: 22 | name: github-pages 23 | url: ${{ steps.deployment.outputs.page_url }} 24 | runs-on: ubuntu-latest 25 | container: sphinxdoc/sphinx 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Install uv 31 | uses: astral-sh/setup-uv@v5 32 | with: 33 | version: "0.6.14" 34 | python-version: "3.12" 35 | 36 | - name: Install dependencies 37 | run: uv sync 38 | 39 | - name: Build docs 40 | run: uv run make html 41 | working-directory: ./docs/ 42 | 43 | - name: upload 44 | uses: actions/upload-pages-artifact@v3 45 | with: 46 | # Upload entire repository 47 | path: './docs/build/html/.' 48 | 49 | - name: Deploy to GitHub Pages 50 | id: deployment 51 | uses: actions/deploy-pages@v4 52 | -------------------------------------------------------------------------------- /.github/workflows/publish-on-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish on PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | # After vMajor.Minor.Patch _anything_ is allowed (without "/") ! 7 | - v[0-9]+.[0-9]+.[0-9]+* 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | if: github.repository == 'stfc/aiida-mlip' && startsWith(github.ref, 'refs/tags/v') 13 | environment: 14 | name: release 15 | permissions: 16 | # For PyPI's trusted publishing. 17 | id-token: write 18 | # For release. 19 | contents: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Install uv 26 | uses: astral-sh/setup-uv@v5 27 | with: 28 | version: "0.6.14" 29 | python-version: "3.12" 30 | 31 | - name: Install dependencies 32 | run: uv sync 33 | 34 | - name: Build 35 | run: uv build 36 | 37 | - name: Get version from pyproject.toml 38 | run: echo "VERSION=$(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version)" >> $GITHUB_ENV 39 | 40 | - name: Check version matches tag 41 | if: ${{ ! contains(github.ref, env.VERSION) }} 42 | run: | 43 | echo "Git tag does not match version in pyproject.toml" 44 | exit 1 45 | 46 | - name: Create Release 47 | uses: ncipollo/release-action@v1 48 | with: 49 | artifacts: "dist/*" 50 | token: ${{ secrets.GITHUB_TOKEN }} 51 | draft: false 52 | prerelease: steps.check-prerelease.outputs.prerelease == 'true' 53 | skipIfReleaseExists: true 54 | 55 | - name: Publish to PyPI 56 | run: uv publish 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.lock 2 | *.pyc 3 | *.swo 4 | *.swp 5 | *.xml 6 | ~* 7 | *~ 8 | .project 9 | *.egg* 10 | .DS_Store 11 | .coverage 12 | .pytest_cache 13 | .vscode 14 | build/ 15 | dist/ 16 | pip-wheel-metadata/ 17 | .tox/ 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Install pre-commit hooks via: 2 | # pre-commit install 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.5.0 6 | hooks: 7 | - id: end-of-file-fixer 8 | - id: mixed-line-ending 9 | - id: trailing-whitespace 10 | - id: check-json 11 | 12 | - repo: https://github.com/astral-sh/ruff-pre-commit 13 | # Ruff version. 14 | rev: v0.9.2 15 | hooks: 16 | # Run the linter. 17 | - id: ruff 18 | args: [ --fix ] 19 | # Run the formatter. 20 | - id: ruff-format 21 | 22 | - repo: https://github.com/numpy/numpydoc 23 | rev: v1.6.0 24 | hooks: 25 | - id: numpydoc-validation 26 | files: ^aiida_mlip/ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, UKRI Science and Technology Facilities Council 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status][ci-badge]][ci-link] 2 | [![Coverage Status][cov-badge]][cov-link] 3 | [![Docs status][docs-badge]][docs-link] 4 | [![PyPI version][pypi-badge]][pypi-link] 5 | [![License][license-badge]][license-link] 6 | [![DOI][doi-badge]][doi-link] 7 | 8 | # aiida-mlip 9 | ![logo][logo] 10 | 11 | machine learning interatomic potentials aiida plugin 12 | 13 | ## Features (in development) 14 | 15 | - [x] Supports multiple MLIPs 16 | - MACE 17 | - M3GNET 18 | - CHGNET 19 | - [x] Single point calculations 20 | - [x] Geometry optimisation 21 | - [x] Molecular Dynamics: 22 | - NVE 23 | - NVT (Langevin(Eijnden/Ciccotti flavour) and Nosé-Hoover (Melchionna flavour)) 24 | - NPT (Nosé-Hoover (Melchiona flavour)) 25 | - [x] Training MLIPs 26 | - MACE 27 | - [x] Fine tuning MLIPs 28 | - MACE 29 | - [x] MLIP descriptors 30 | - MACE 31 | 32 | The code relies heavily on [janus-core](https://github.com/stfc/janus-core), which handles mlip calculations using ASE. 33 | 34 | 35 | # Getting Started 36 | 37 | ## Installation 38 | We suggest creating a new [virtual environment](https://docs.python.org/3/library/venv.html#creating-virtual-environments) and activating it before running the commands below to install `aiida-mlip`: 39 | 40 | ```shell 41 | pip install aiida-mlip 42 | verdi plugin list aiida.calculations 43 | ``` 44 | The last command should show a list of AiiDA pre-installed calculations and the aiida-mlip plugin calculations: 45 | ``` 46 | Registered entry points for aiida.calculations: 47 | * core.arithmetic.add 48 | * core.templatereplacer 49 | * core.transfer 50 | * mlip.opt 51 | * mlip.sp 52 | * mlip.md 53 | * mlip.train 54 | * mlip.descriptors 55 | ``` 56 | 57 | ## AiiDA Configuration 58 | 59 | Once `aiida-mlip` is installed, you have to configure AiiDA by creating a profile to store your data: 60 | 61 | 1. (Optional) Install [RabbitMQ](https://aiida.readthedocs.io/projects/aiida-core/en/stable/installation/guide_complete.html#rabbitmq) 62 | 2. Run: 63 | ```shell 64 | verdi presto #Sets up profile and broker for daemon to run 65 | ``` 66 | 3. Create a [code](https://aiida.readthedocs.io/projects/aiida-core/en/stable/howto/run_codes.html#how-to-create-a-code) for `janus-core` 67 | 68 | > [!NOTE] 69 | > Setting up a message broker like RabbitMQ is recommended to enable full functionality, particularly for production use. 70 | > If detected, `verdi presto` sets up a complete AiiDA profile, including the computer, database, and broker, but the `janus-core` code must be set up separately, as described above. 71 | 72 | Please refer to our [user guide](https://stfc.github.io/aiida-mlip/user_guide/get_started.html) for more details on installation and configuring AiiDA. 73 | 74 | ## Usage 75 | 76 | The [examples](https://github.com/stfc/aiida-mlip/tree/main/examples) folder provides scripts to submit calculations in the calculations folder, and tutorials in jupyter notebook format in the tutorials folder. 77 | 78 | A quick demo of how to submit a calculation using the provided example files: 79 | 80 | 81 | ```shell 82 | verdi daemon start # make sure the daemon is running 83 | cd examples/calculations 84 | verdi run submit_singlepoint.py "janus@localhost" --struct "path/to/structure" --architecture mace --model "/path/to/model" # run singlepoint calculation 85 | verdi run submit_geomopt.py "janus@localhost" --struct "path/to/structure" --model "path/to/model" --steps 5 --opt_cell_fully True # run geometry optimisation 86 | verdi run submit_md.py "janus@localhost" --struct "path/to/structure" --model "path/to/model" --ensemble "nve" --md_dict_str "{'temp':300,'steps':4,'traj-every':3,'stats-every':1}" # run molecular dynamics 87 | 88 | verdi process list -a # check record of calculation 89 | ``` 90 | Models can be trained by using the Train calcjob. In that case the needed inputs are a config file containig the path to train, test and validation xyz file and other optional parameters. Running 91 | ```shell 92 | verdi run submit_train.py 93 | ``` 94 | a model will be trained using the provided example config file and xyz files (can be found in the tests folder) 95 | 96 | 97 | ## Development 98 | 99 | We recommend installing ``uv`` for dependency management when developing for `aiida-mlip`, and setting up PostgreSQL, as this is currently a requirement for testing: 100 | 101 | 102 | 1. Install [uv](https://docs.astral.sh/uv/getting-started/installation) 103 | 2. Setup [PostgreSQL](https://aiida.readthedocs.io/projects/aiida-core/en/stable/installation/guide_complete.html#core-psql-dos) 104 | 3. Install `aiida-mlip` with dependencies in a virtual environment: 105 | 106 | ```shell 107 | git clone https://github.com/stfc/aiida-mlip 108 | cd aiida-mlip 109 | uv sync --extra mace # Create a virtual environment and install dependencies with mace for tests 110 | source .venv/bin/activate 111 | pre-commit install # Install pre-commit hooks 112 | pytest -v # Discover and run all tests 113 | ``` 114 | See the [developer guide](https://stfc.github.io/aiida-mlip/developer_guide/index.html) for more information. 115 | 116 | ## License 117 | 118 | [BSD 3-Clause License](LICENSE) 119 | 120 | ## Funding 121 | 122 | Contributors to this project were funded by 123 | 124 | [![PSDI](https://raw.githubusercontent.com/stfc/aiida-mlip/main/docs/source/images/psdi-100.webp)](https://www.psdi.ac.uk/) 125 | [![ALC](https://raw.githubusercontent.com/stfc/aiida-mlip/main/docs/source/images/alc-100.webp)](https://adalovelacecentre.ac.uk/) 126 | [![CoSeC](https://raw.githubusercontent.com/stfc/aiida-mlip/main/docs/source/images/cosec-100.webp)](https://www.scd.stfc.ac.uk/Pages/CoSeC.aspx) 127 | 128 | 129 | [ci-badge]: https://github.com/stfc/aiida-mlip/workflows/ci/badge.svg 130 | [ci-link]: https://github.com/stfc/aiida-mlip/actions 131 | [cov-badge]: https://coveralls.io/repos/github/stfc/aiida-mlip/badge.svg?branch=main 132 | [cov-link]: https://coveralls.io/github/stfc/aiida-mlip?branch=main 133 | [docs-badge]: https://github.com/stfc/aiida-mlip/actions/workflows/docs.yml/badge.svg 134 | [docs-link]: https://stfc.github.io/aiida-mlip/ 135 | [pypi-badge]: https://badge.fury.io/py/aiida-mlip.svg 136 | [pypi-link]: https://badge.fury.io/py/aiida-mlip 137 | [license-badge]: https://img.shields.io/badge/License-BSD_3--Clause-blue.svg 138 | [license-link]: https://opensource.org/licenses/BSD-3-Clause 139 | [doi-link]: https://zenodo.org/badge/latestdoi/750834002 140 | [doi-badge]: https://zenodo.org/badge/750834002.svg 141 | [logo]: https://raw.githubusercontent.com/stfc/aiida-mlip/main/docs/source/images/aiida-mlip-100.png 142 | -------------------------------------------------------------------------------- /aiida_mlip/__init__.py: -------------------------------------------------------------------------------- 1 | """Machine learning interatomic potentials aiida plugin.""" 2 | 3 | from __future__ import annotations 4 | 5 | __version__ = "0.2.1" 6 | -------------------------------------------------------------------------------- /aiida_mlip/calculations/__init__.py: -------------------------------------------------------------------------------- 1 | """Calculations using MLIPs.""" 2 | -------------------------------------------------------------------------------- /aiida_mlip/calculations/descriptors.py: -------------------------------------------------------------------------------- 1 | """Class to run descriptors calculations.""" 2 | 3 | from __future__ import annotations 4 | 5 | from aiida.common import datastructures 6 | import aiida.common.folders 7 | from aiida.engine import CalcJobProcessSpec 8 | import aiida.engine.processes 9 | from aiida.orm import Bool 10 | 11 | from aiida_mlip.calculations.singlepoint import Singlepoint 12 | 13 | 14 | class Descriptors(Singlepoint): # numpydoc ignore=PR01 15 | """ 16 | Calcjob implementation to calculate MLIP descriptors. 17 | 18 | Methods 19 | ------- 20 | define(spec: CalcJobProcessSpec) -> None: 21 | Define the process specification, its inputs, outputs and exit codes. 22 | prepare_for_submission(folder: Folder) -> CalcInfo: 23 | Create the input files for the `CalcJob`. 24 | """ 25 | 26 | @classmethod 27 | def define(cls, spec: CalcJobProcessSpec) -> None: 28 | """ 29 | Define the process specification, its inputs, outputs and exit codes. 30 | 31 | Parameters 32 | ---------- 33 | spec : aiida.engine.CalcJobProcessSpec 34 | The calculation job process spec to define. 35 | """ 36 | super().define(spec) 37 | 38 | # Define inputs 39 | 40 | # Remove unused singlepoint input 41 | del spec.inputs["properties"] 42 | 43 | spec.input( 44 | "invariants_only", 45 | valid_type=Bool, 46 | required=False, 47 | help="Only calculate invariant descriptors.", 48 | ) 49 | 50 | spec.input( 51 | "calc_per_element", 52 | valid_type=Bool, 53 | required=False, 54 | help="Calculate mean descriptors for each element.", 55 | ) 56 | 57 | spec.input( 58 | "calc_per_atom", 59 | valid_type=Bool, 60 | required=False, 61 | help="Calculate descriptors for each atom.", 62 | ) 63 | 64 | spec.inputs["metadata"]["options"][ 65 | "parser_name" 66 | ].default = "mlip.descriptors_parser" 67 | 68 | def prepare_for_submission( 69 | self, folder: aiida.common.folders.Folder 70 | ) -> datastructures.CalcInfo: 71 | """ 72 | Create the input files for the `Calcjob`. 73 | 74 | Parameters 75 | ---------- 76 | folder : aiida.common.folders.Folder 77 | Folder where the calculation is run. 78 | 79 | Returns 80 | ------- 81 | aiida.common.datastructures.CalcInfo 82 | An instance of `aiida.common.datastructures.CalcInfo`. 83 | """ 84 | # Call the parent class method to prepare common inputs 85 | calcinfo = super().prepare_for_submission(folder) 86 | codeinfo = calcinfo.codes_info[0] 87 | 88 | # Adding command line params for when we run janus 89 | # descriptors is overwriting the placeholder "calculation" from the base.py file 90 | codeinfo.cmdline_params[0] = "descriptors" 91 | 92 | cmdline_options = { 93 | key.replace("_", "-"): getattr(self.inputs, key).value 94 | for key in ("invariants_only", "calc_per_element", "calc_per_atom") 95 | if key in self.inputs 96 | } 97 | 98 | for flag, value in cmdline_options.items(): 99 | if isinstance(value, bool): 100 | # Add boolean flags without value if True 101 | if value: 102 | codeinfo.cmdline_params.append(f"--{flag}") 103 | else: 104 | codeinfo.cmdline_params += [f"--{flag}", value] 105 | 106 | return calcinfo 107 | -------------------------------------------------------------------------------- /aiida_mlip/calculations/geomopt.py: -------------------------------------------------------------------------------- 1 | """Class to run geom opt calculations.""" 2 | 3 | from __future__ import annotations 4 | 5 | from aiida.common import datastructures 6 | import aiida.common.folders 7 | from aiida.engine import CalcJobProcessSpec 8 | import aiida.engine.processes 9 | from aiida.orm import ( 10 | Bool, 11 | Dict, 12 | Float, 13 | Int, 14 | SinglefileData, 15 | Str, 16 | StructureData, 17 | TrajectoryData, 18 | ) 19 | 20 | from aiida_mlip.calculations.singlepoint import Singlepoint 21 | 22 | 23 | class GeomOpt(Singlepoint): # numpydoc ignore=PR01 24 | """ 25 | Calcjob implementation to run geometry optimisation calculations using mlips. 26 | 27 | Methods 28 | ------- 29 | define(spec: CalcJobProcessSpec) -> None: 30 | Define the process specification, its inputs, outputs and exit codes. 31 | prepare_for_submission(folder: Folder) -> CalcInfo: 32 | Create the input files for the `CalcJob`. 33 | """ 34 | 35 | DEFAULT_TRAJ_FILE = "aiida-traj.xyz" 36 | 37 | @classmethod 38 | def define(cls, spec: CalcJobProcessSpec) -> None: 39 | """ 40 | Define the process specification, its inputs, outputs and exit codes. 41 | 42 | Parameters 43 | ---------- 44 | spec : `aiida.engine.CalcJobProcessSpec` 45 | The calculation job process spec to define. 46 | """ 47 | super().define(spec) 48 | 49 | # Additional inputs for geometry optimisation 50 | spec.input( 51 | "traj", 52 | valid_type=Str, 53 | required=False, 54 | default=lambda: Str(cls.DEFAULT_TRAJ_FILE), 55 | help="Path to save optimisation frames to", 56 | ) 57 | spec.input( 58 | "opt_cell_fully", 59 | valid_type=Bool, 60 | required=False, 61 | help="Fully optimise the cell vectors, angles, and atomic positions", 62 | ) 63 | spec.input( 64 | "opt_cell_lengths", 65 | valid_type=Bool, 66 | required=False, 67 | help="Optimise cell vectors, as well as atomic positions", 68 | ) 69 | spec.input( 70 | "fmax", 71 | valid_type=Float, 72 | required=False, 73 | help="Maximum force for convergence", 74 | ) 75 | 76 | spec.input( 77 | "steps", 78 | valid_type=Int, 79 | required=False, 80 | help="Number of optimisation steps", 81 | ) 82 | 83 | spec.input( 84 | "opt_kwargs", 85 | valid_type=Dict, 86 | required=False, 87 | help="Other optimisation keywords", 88 | ) 89 | 90 | spec.inputs["metadata"]["options"]["parser_name"].default = "mlip.opt_parser" 91 | 92 | spec.output("traj_file", valid_type=SinglefileData) 93 | spec.output("traj_output", valid_type=TrajectoryData) 94 | spec.output("final_structure", valid_type=StructureData) 95 | 96 | def prepare_for_submission( 97 | self, folder: aiida.common.folders.Folder 98 | ) -> datastructures.CalcInfo: 99 | """ 100 | Create the input files for the `Calcjob`. 101 | 102 | Parameters 103 | ---------- 104 | folder : aiida.common.folders.Folder 105 | Folder where the calculation is run. 106 | 107 | Returns 108 | ------- 109 | aiida.common.datastructures.CalcInfo 110 | An instance of `aiida.common.datastructures.CalcInfo`. 111 | """ 112 | # Call the parent class method to prepare common inputs 113 | calcinfo = super().prepare_for_submission(folder) 114 | codeinfo = calcinfo.codes_info[0] 115 | 116 | minimize_kwargs = ( 117 | f"{{'traj_kwargs': {{'filename': '{self.inputs.traj.value}'}}}}" 118 | ) 119 | 120 | geom_opt_cmdline = { 121 | "minimize-kwargs": minimize_kwargs, 122 | "write-traj": True, 123 | } 124 | if "opt_kwargs" in self.inputs: 125 | opt_kwargs = self.inputs.opt_kwargs.get_dict() 126 | geom_opt_cmdline["opt-kwargs"] = opt_kwargs 127 | if "opt_cell_fully" in self.inputs: 128 | geom_opt_cmdline["opt-cell-fully"] = self.inputs.opt_cell_fully.value 129 | if "opt_cell_lengths" in self.inputs: 130 | geom_opt_cmdline["opt-cell-lengths"] = self.inputs.opt_cell_lengths.value 131 | if "fmax" in self.inputs: 132 | geom_opt_cmdline["fmax"] = self.inputs.fmax.value 133 | if "steps" in self.inputs: 134 | geom_opt_cmdline["steps"] = self.inputs.steps.value 135 | 136 | # Adding command line params for when we run janus 137 | # 'geomopt' is overwriting the placeholder "calculation" from the base.py file 138 | codeinfo.cmdline_params[0] = "geomopt" 139 | 140 | for flag, value in geom_opt_cmdline.items(): 141 | if isinstance(value, bool): 142 | # Add boolean flags without value if True 143 | if value: 144 | codeinfo.cmdline_params.append(f"--{flag}") 145 | else: 146 | codeinfo.cmdline_params += [f"--{flag}", value] 147 | 148 | calcinfo.retrieve_list.append(self.inputs.traj.value) 149 | 150 | return calcinfo 151 | -------------------------------------------------------------------------------- /aiida_mlip/calculations/md.py: -------------------------------------------------------------------------------- 1 | """Class to run md calculations.""" 2 | 3 | from __future__ import annotations 4 | 5 | from aiida.common import datastructures 6 | import aiida.common.folders 7 | from aiida.engine import CalcJobProcessSpec 8 | import aiida.engine.processes 9 | from aiida.orm import Dict, SinglefileData, Str, StructureData, TrajectoryData 10 | 11 | from aiida_mlip.calculations.base import BaseJanus 12 | 13 | 14 | class MD(BaseJanus): # numpydoc ignore=PR01 15 | """ 16 | Calcjob implementation to run geometry MD calculations using mlips. 17 | 18 | Methods 19 | ------- 20 | define(spec: CalcJobProcessSpec) -> None: 21 | Define the process specification, its inputs, outputs and exit codes. 22 | prepare_for_submission(folder: Folder) -> CalcInfo: 23 | Create the input files for the `CalcJob`. 24 | """ 25 | 26 | DEFAULT_TRAJ_FILE = "aiida-traj.xyz" 27 | DEFAULT_STATS_FILE = "aiida-stats.dat" 28 | DEFAULT_SUMMARY_FILE = "md_summary.yml" 29 | 30 | @classmethod 31 | def define(cls, spec: CalcJobProcessSpec) -> None: 32 | """ 33 | Define the process specification, its inputs, outputs and exit codes. 34 | 35 | Parameters 36 | ---------- 37 | spec : `aiida.engine.CalcJobProcessSpec` 38 | The calculation job process spec to define. 39 | """ 40 | super().define(spec) 41 | 42 | # Additional inputs for molecular dynamics 43 | spec.input( 44 | "ensemble", 45 | valid_type=Str, 46 | required=False, 47 | help="Name for thermodynamic ensemble", 48 | ) 49 | 50 | spec.input( 51 | "md_kwargs", 52 | valid_type=Dict, 53 | required=False, 54 | default=lambda: Dict( 55 | { 56 | "traj-file": cls.DEFAULT_TRAJ_FILE, 57 | "stats-file": cls.DEFAULT_STATS_FILE, 58 | "summary": cls.DEFAULT_SUMMARY_FILE, 59 | } 60 | ), 61 | help="Keywords for molecular dynamics", 62 | ) 63 | 64 | spec.inputs["metadata"]["options"]["parser_name"].default = "mlip.md_parser" 65 | 66 | spec.output( 67 | "results_dict", 68 | valid_type=Dict, 69 | help="The `results_dict` output node of the successful calculation.", 70 | ) 71 | spec.output("summary", valid_type=SinglefileData) 72 | spec.output("stats_file", valid_type=SinglefileData) 73 | spec.output("traj_file", valid_type=SinglefileData) 74 | spec.output("traj_output", valid_type=TrajectoryData) 75 | spec.output("final_structure", valid_type=StructureData) 76 | 77 | spec.default_output_node = "results_dict" 78 | 79 | def prepare_for_submission( 80 | self, folder: aiida.common.folders.Folder 81 | ) -> datastructures.CalcInfo: 82 | """ 83 | Create the input files for the `Calcjob`. 84 | 85 | Parameters 86 | ---------- 87 | folder : aiida.common.folders.Folder 88 | Folder where the calculation is run. 89 | 90 | Returns 91 | ------- 92 | aiida.common.datastructures.CalcInfo 93 | An instance of `aiida.common.datastructures.CalcInfo`. 94 | """ 95 | # Call the parent class method to prepare common inputs 96 | calcinfo = super().prepare_for_submission(folder) 97 | codeinfo = calcinfo.codes_info[0] 98 | 99 | md_dictionary = self.inputs.md_kwargs.get_dict() 100 | 101 | md_dictionary.setdefault("traj-file", str(self.DEFAULT_TRAJ_FILE)) 102 | md_dictionary.setdefault("stats-file", str(self.DEFAULT_STATS_FILE)) 103 | md_dictionary.setdefault("summary", str(self.DEFAULT_SUMMARY_FILE)) 104 | 105 | if "ensemble" in self.inputs: 106 | ensemble = self.inputs.ensemble.value.lower() 107 | elif "config" in self.inputs and "ensemble" in self.inputs.config.as_dictionary: 108 | ensemble = self.inputs.config.as_dictionary["ensemble"] 109 | else: 110 | raise ValueError("'ensemble' not provided.") 111 | 112 | # md is overwriting the placeholder "calculation" from the base.py file 113 | codeinfo.cmdline_params[0] = "md" 114 | 115 | codeinfo.cmdline_params += ["--ensemble", ensemble] 116 | 117 | for flag, value in md_dictionary.items(): 118 | # Add boolean flags without value if True 119 | if isinstance(value, bool) and value: 120 | codeinfo.cmdline_params.append(f"--{flag}") 121 | else: 122 | codeinfo.cmdline_params += [f"--{flag}", value] 123 | 124 | calcinfo.retrieve_list.append(md_dictionary["traj-file"]) 125 | calcinfo.retrieve_list.append(md_dictionary["stats-file"]) 126 | calcinfo.retrieve_list.append(md_dictionary["summary"]) 127 | 128 | return calcinfo 129 | -------------------------------------------------------------------------------- /aiida_mlip/calculations/singlepoint.py: -------------------------------------------------------------------------------- 1 | """Class to run single point calculations.""" 2 | 3 | from __future__ import annotations 4 | 5 | from aiida.common import datastructures 6 | import aiida.common.folders 7 | from aiida.engine import CalcJobProcessSpec 8 | import aiida.engine.processes 9 | from aiida.orm import Dict, SinglefileData, Str 10 | 11 | from aiida_mlip.calculations.base import BaseJanus 12 | 13 | 14 | class Singlepoint(BaseJanus): # numpydoc ignore=PR01 15 | """ 16 | Calcjob implementation to run single point calculations using mlips. 17 | 18 | Attributes 19 | ---------- 20 | XYZ_OUTPUT : str 21 | Default xyz output file name. 22 | 23 | Methods 24 | ------- 25 | define(spec: CalcJobProcessSpec) -> None: 26 | Define the process specification, its inputs, outputs and exit codes. 27 | validate_inputs(value: dict, port_namespace: PortNamespace) -> Optional[str]: 28 | Check if the inputs are valid. 29 | prepare_for_submission(folder: Folder) -> CalcInfo: 30 | Create the input files for the `CalcJob`. 31 | """ 32 | 33 | XYZ_OUTPUT = "aiida-results.xyz" 34 | 35 | @classmethod 36 | def define(cls, spec: CalcJobProcessSpec) -> None: 37 | """ 38 | Define the process specification, its inputs, outputs and exit codes. 39 | 40 | Parameters 41 | ---------- 42 | spec : `aiida.engine.CalcJobProcessSpec` 43 | The calculation job process spec to define. 44 | """ 45 | super().define(spec) 46 | 47 | # Define inputs 48 | 49 | spec.input( 50 | "out", 51 | valid_type=Str, 52 | required=False, 53 | default=lambda: Str(cls.XYZ_OUTPUT), 54 | help="Name of the xyz output file", 55 | ) 56 | 57 | spec.input( 58 | "properties", 59 | valid_type=Str, 60 | required=False, 61 | help="Properties to calculate", 62 | ) 63 | 64 | spec.inputs["metadata"]["options"]["parser_name"].default = "mlip.sp_parser" 65 | 66 | # Define outputs. The default is a dictionary with the content of the xyz file 67 | spec.output( 68 | "results_dict", 69 | valid_type=Dict, 70 | help="The `results_dict` output node of the successful calculation.", 71 | ) 72 | spec.output("xyz_output", valid_type=SinglefileData) 73 | 74 | print("defining outputnode") 75 | spec.default_output_node = "results_dict" 76 | 77 | def prepare_for_submission( 78 | self, folder: aiida.common.folders.Folder 79 | ) -> datastructures.CalcInfo: 80 | """ 81 | Create the input files for the `Calcjob`. 82 | 83 | Parameters 84 | ---------- 85 | folder : aiida.common.folders.Folder 86 | Folder where the calculation is run. 87 | 88 | Returns 89 | ------- 90 | aiida.common.datastructures.CalcInfo 91 | An instance of `aiida.common.datastructures.CalcInfo`. 92 | """ 93 | # Call the parent class method to prepare common inputs 94 | calcinfo = super().prepare_for_submission(folder) 95 | codeinfo = calcinfo.codes_info[0] 96 | 97 | # Adding command line params for when we run janus 98 | # singlepoint is overwriting the placeholder "calculation" from the base.py file 99 | codeinfo.cmdline_params[0] = "singlepoint" 100 | 101 | # The inputs are saved in the node, but we want their value as a string 102 | xyz_filename = (self.inputs.out).value 103 | codeinfo.cmdline_params += ["--out", xyz_filename] 104 | 105 | if "properties" in self.inputs: 106 | properties = self.inputs.properties.value 107 | codeinfo.cmdline_params += ["--properties", properties] 108 | 109 | calcinfo.retrieve_list.append(xyz_filename) 110 | 111 | return calcinfo 112 | -------------------------------------------------------------------------------- /aiida_mlip/data/__init__.py: -------------------------------------------------------------------------------- 1 | """Data types for MLIPs calculations.""" 2 | -------------------------------------------------------------------------------- /aiida_mlip/data/config.py: -------------------------------------------------------------------------------- 1 | """Define Model Data type in AiiDA.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | from typing import Any 7 | 8 | from aiida.orm import Data, SinglefileData 9 | import yaml 10 | 11 | from aiida_mlip.helpers.converters import convert_to_nodes 12 | 13 | 14 | class JanusConfigfile(SinglefileData): 15 | """ 16 | Define config file type in AiiDA in yaml. 17 | 18 | Parameters 19 | ---------- 20 | file : Union[str, Path] 21 | Absolute path to the file. 22 | filename : Optional[str], optional 23 | Name to be used for the file (defaults to the name of provided file). 24 | 25 | Attributes 26 | ---------- 27 | filepath : str 28 | Path of the config file. 29 | 30 | Methods 31 | ------- 32 | set_file(file, filename=None, architecture=None, **kwargs) 33 | Set the file for the node. 34 | read_yaml() 35 | Reads the config file from yaml format. 36 | store_content(store_all: bool = False, skip: list = None) -> dict: 37 | Converts keys in dictionary to nodes and store them 38 | as_dictionary(self) -> dict 39 | Returns the config file as a dictionary. 40 | 41 | Other Parameters 42 | ---------------- 43 | **kwargs : Any 44 | Additional keyword arguments. 45 | """ 46 | 47 | def __init__( 48 | self, 49 | file: str | Path, 50 | filename: str | None = None, 51 | **kwargs: Any, 52 | ) -> None: 53 | """ 54 | Initialize the ModelData object. 55 | 56 | Parameters 57 | ---------- 58 | file : Union[str, Path] 59 | Absolute path to the file. 60 | filename : Optional[str], optional 61 | Name to be used for the file (defaults to the name of provided file). 62 | 63 | Other Parameters 64 | ---------------- 65 | **kwargs : Any 66 | Additional keyword arguments. 67 | """ 68 | super().__init__(file, filename, **kwargs) 69 | self.base.attributes.set("filepath", str(file)) 70 | 71 | def __contains__(self, key): 72 | """ 73 | Check if a key exists in the config file. 74 | 75 | Parameters 76 | ---------- 77 | key : str 78 | Key to check. 79 | 80 | Returns 81 | ------- 82 | bool 83 | True if the key exists in the config file, False otherwise. 84 | """ 85 | config = self.as_dictionary 86 | return key in config 87 | 88 | def set_file( 89 | self, 90 | file: str | Path, 91 | filename: str | None = None, 92 | **kwargs: Any, 93 | ) -> None: 94 | """ 95 | Set the file for the node. 96 | 97 | Parameters 98 | ---------- 99 | file : Union[str, Path] 100 | Absolute path to the file. 101 | filename : Optional[str], optional 102 | Name to be used for the file (defaults to the name of provided file). 103 | 104 | Other Parameters 105 | ---------------- 106 | **kwargs : Any 107 | Additional keyword arguments. 108 | """ 109 | super().set_file(file, filename, **kwargs) 110 | self.base.attributes.set("filepath", str(file)) 111 | 112 | def read_yaml(self) -> dict: 113 | """ 114 | Convert yaml file to dictionary. 115 | 116 | Returns 117 | ------- 118 | dict 119 | Returns the converted dictionary with the stored parameters. 120 | """ 121 | with open(self.filepath, encoding="utf-8") as handle: 122 | return yaml.safe_load(handle) 123 | 124 | def store_content(self, store_all: bool = False, skip: list = None) -> dict: 125 | """ 126 | Store the content of the config file in the database. 127 | 128 | Parameters 129 | ---------- 130 | store_all : bool 131 | Define if you want to store all the parameters or only the main ones. 132 | skip : list 133 | List of parameters that do not have to be stored. 134 | 135 | Returns 136 | ------- 137 | dict 138 | Returns the converted dictionary with the stored parameters. 139 | """ 140 | config = convert_to_nodes(self.as_dictionary, convert_all=store_all) 141 | for key, value in config.items(): 142 | if issubclass(type(value), Data) and key not in skip: 143 | value.store() 144 | return config 145 | 146 | @property 147 | def filepath(self) -> str: 148 | """ 149 | Return the filepath. 150 | 151 | Returns 152 | ------- 153 | str 154 | Path of the config file. 155 | """ 156 | return self.base.attributes.get("filepath") 157 | 158 | @property 159 | def as_dictionary(self) -> dict: 160 | """ 161 | Return the filepath. 162 | 163 | Returns 164 | ------- 165 | dict 166 | Config file as a dictionary. 167 | """ 168 | return self.read_yaml() 169 | -------------------------------------------------------------------------------- /aiida_mlip/data/model.py: -------------------------------------------------------------------------------- 1 | """Define Model Data type in AiiDA.""" 2 | 3 | from __future__ import annotations 4 | 5 | import hashlib 6 | from pathlib import Path 7 | from typing import Any 8 | from urllib import request 9 | 10 | from aiida.orm import QueryBuilder, SinglefileData, load_node 11 | 12 | 13 | class ModelData(SinglefileData): 14 | """ 15 | Define Model Data type in AiiDA. 16 | 17 | Parameters 18 | ---------- 19 | file : Union[str, Path] 20 | Absolute path to the file. 21 | architecture : str 22 | Architecture of the mlip model. 23 | filename : Optional[str], optional 24 | Name to be used for the file (defaults to the name of provided file). 25 | 26 | Attributes 27 | ---------- 28 | architecture : str 29 | Architecture of the mlip model. 30 | model_hash : str 31 | Hash of the model. 32 | 33 | Methods 34 | ------- 35 | set_file(file, filename=None, architecture=None, **kwargs) 36 | Set the file for the node. 37 | from_local(file, architecture, filename=None): 38 | Create a ModelData instance from a local file. 39 | from_uri(uri, architecture, filename=None, cache_dir=None, keep_file=False) 40 | Download a file from a URI and save it as ModelData. 41 | 42 | Other Parameters 43 | ---------------- 44 | **kwargs : Any 45 | Additional keyword arguments. 46 | """ 47 | 48 | @staticmethod 49 | def _calculate_hash(file: str | Path) -> str: 50 | """ 51 | Calculate the hash of a file. 52 | 53 | Parameters 54 | ---------- 55 | file : Union[str, Path] 56 | Path to the file for which hash needs to be calculated. 57 | 58 | Returns 59 | ------- 60 | str 61 | The SHA-256 hash of the file. 62 | """ 63 | # Calculate hash 64 | buf_size = 65536 # reading 64kB (arbitrary) at a time 65 | sha256 = hashlib.sha256() 66 | with open(file, "rb") as f: 67 | # calculating sha in chunks rather than 1 large pass 68 | while data := f.read(buf_size): 69 | sha256.update(data) 70 | return sha256.hexdigest() 71 | 72 | def __init__( 73 | self, 74 | file: str | Path, 75 | architecture: str, 76 | filename: str | None = None, 77 | **kwargs: Any, 78 | ) -> None: 79 | """ 80 | Initialize the ModelData object. 81 | 82 | Parameters 83 | ---------- 84 | file : Union[str, Path] 85 | Absolute path to the file. 86 | architecture : [str] 87 | Architecture of the mlip model. 88 | filename : Optional[str], optional 89 | Name to be used for the file (defaults to the name of provided file). 90 | 91 | Other Parameters 92 | ---------------- 93 | **kwargs : Any 94 | Additional keyword arguments. 95 | """ 96 | super().__init__(file, filename, **kwargs) 97 | self.base.attributes.set("architecture", architecture) 98 | 99 | def set_file( 100 | self, 101 | file: str | Path, 102 | filename: str | None = None, 103 | architecture: str | None = None, 104 | **kwargs: Any, 105 | ) -> None: 106 | """ 107 | Set the file for the node. 108 | 109 | Parameters 110 | ---------- 111 | file : Union[str, Path] 112 | Absolute path to the file. 113 | filename : Optional[str], optional 114 | Name to be used for the file (defaults to the name of provided file). 115 | architecture : Optional[str], optional 116 | Architecture of the mlip model. 117 | 118 | Other Parameters 119 | ---------------- 120 | **kwargs : Any 121 | Additional keyword arguments. 122 | """ 123 | super().set_file(file, filename, **kwargs) 124 | self.base.attributes.set("architecture", architecture) 125 | # here compute hash and set attribute 126 | model_hash = self._calculate_hash(file) 127 | self.base.attributes.set("model_hash", model_hash) 128 | 129 | @classmethod 130 | def from_local( 131 | cls, 132 | file: str | Path, 133 | architecture: str, 134 | filename: str | None = None, 135 | ): 136 | """ 137 | Create a ModelData instance from a local file. 138 | 139 | Parameters 140 | ---------- 141 | file : Union[str, Path] 142 | Path to the file. 143 | architecture : [str] 144 | Architecture of the mlip model. 145 | filename : Optional[str], optional 146 | Name to be used for the file (defaults to the name of provided file). 147 | 148 | Returns 149 | ------- 150 | ModelData 151 | A ModelData instance. 152 | """ 153 | file_path = Path(file).resolve() 154 | return cls(file=file_path, architecture=architecture, filename=filename) 155 | 156 | @classmethod 157 | def from_uri( 158 | cls, 159 | uri: str, 160 | architecture: str, 161 | filename: str | None = "tmp_file.model", 162 | cache_dir: str | Path | None = None, 163 | keep_file: bool | None = False, 164 | ): 165 | """ 166 | Download a file from a URI and save it as ModelData. 167 | 168 | Parameters 169 | ---------- 170 | uri : str 171 | URI of the file to download. 172 | architecture : [str] 173 | Architecture of the mlip model. 174 | filename : Optional[str], optional 175 | Name to be used for the file defaults to tmp_file.model. 176 | cache_dir : Optional[Union[str, Path]], optional 177 | Path to the folder where the file has to be saved 178 | (defaults to "~/.cache/mlips/"). 179 | keep_file : Optional[bool], optional 180 | True to keep the downloaded model, even if there are duplicates. 181 | (default: False, the file is deleted and only saved in the database). 182 | 183 | Returns 184 | ------- 185 | ModelData 186 | A ModelData instance. 187 | """ 188 | cache_dir = ( 189 | Path(cache_dir) if cache_dir else Path("~/.cache/mlips/").expanduser() 190 | ) 191 | arch_dir = (cache_dir / architecture) if architecture else cache_dir 192 | 193 | arch_path = arch_dir.resolve() 194 | arch_path.mkdir(parents=True, exist_ok=True) 195 | 196 | file = arch_path / filename 197 | 198 | # Download file 199 | request.urlretrieve(uri, file) 200 | 201 | model = cls.from_local(file=file, architecture=architecture) 202 | 203 | if keep_file: 204 | return model 205 | 206 | file.unlink(missing_ok=True) 207 | 208 | # Check if the same model was used previously 209 | qb = QueryBuilder() 210 | qb.append( 211 | ModelData, 212 | filters={ 213 | "attributes.model_hash": model.model_hash, 214 | "attributes.architecture": model.architecture, 215 | "ctime": {"!in": [model.ctime]}, 216 | }, 217 | project=["attributes", "pk", "ctime"], 218 | ) 219 | 220 | if qb.count() != 0: 221 | model = load_node( 222 | qb.first()[1] 223 | ) # This gets the pk of the first model in the query 224 | 225 | return model 226 | 227 | @property 228 | def architecture(self) -> str: 229 | """ 230 | Return the architecture. 231 | 232 | Returns 233 | ------- 234 | str 235 | Architecture of the mlip model. 236 | """ 237 | return self.base.attributes.get("architecture") 238 | 239 | @property 240 | def model_hash(self) -> str: 241 | """ 242 | Return hash of the architecture. 243 | 244 | Returns 245 | ------- 246 | str 247 | Hash of the MLIP model. 248 | """ 249 | return self.base.attributes.get("model_hash") 250 | -------------------------------------------------------------------------------- /aiida_mlip/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | """Helpers for running calculations.""" 2 | -------------------------------------------------------------------------------- /aiida_mlip/helpers/converters.py: -------------------------------------------------------------------------------- 1 | """Some helpers to convert between different formats.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | 7 | from aiida.orm import Bool, Dict, Str, StructureData, TrajectoryData, load_code 8 | from ase.io import read 9 | import numpy as np 10 | 11 | from aiida_mlip.helpers.help_load import load_model, load_structure 12 | 13 | 14 | def convert_numpy(dictionary: dict) -> dict: 15 | """ 16 | Convert numpy ndarrays in dictionary into lists. 17 | 18 | Parameters 19 | ---------- 20 | dictionary : dict 21 | A dictionary with numpy array values to be converted into lists. 22 | 23 | Returns 24 | ------- 25 | dict 26 | Converted dictionary. 27 | """ 28 | new_dict = dictionary.copy() 29 | for key, value in new_dict.items(): 30 | if isinstance(value, np.ndarray): 31 | new_dict[key] = value.tolist() 32 | return new_dict 33 | 34 | 35 | def xyz_to_aiida_traj( 36 | traj_file: str | Path, 37 | ) -> tuple[StructureData, TrajectoryData]: 38 | """ 39 | Convert xyz trajectory file to `TrajectoryData` data type. 40 | 41 | Parameters 42 | ---------- 43 | traj_file : Union[str, Path] 44 | The path to the XYZ file. 45 | 46 | Returns 47 | ------- 48 | Tuple[StructureData, TrajectoryData] 49 | A tuple containing the last structure in the trajectory and a `TrajectoryData` 50 | object containing all structures from the trajectory. 51 | """ 52 | # Read the XYZ file using ASE 53 | struct_list = read(traj_file, index=":") 54 | 55 | # Create a TrajectoryData object 56 | traj = [StructureData(ase=struct) for struct in struct_list] 57 | 58 | return traj[-1], TrajectoryData(traj) 59 | 60 | 61 | def convert_to_nodes(dictionary: dict, convert_all: bool = False) -> dict: 62 | """ 63 | Convert each key of the config file to a aiida node. 64 | 65 | Parameters 66 | ---------- 67 | dictionary : dict 68 | The dictionary obtained from the config file. 69 | convert_all : bool 70 | Define if you want to convert all the parameters or only the main ones. 71 | 72 | Returns 73 | ------- 74 | dict 75 | Returns the converted dictionary. 76 | """ 77 | new_dict = dictionary.copy() 78 | arch = new_dict["arch"] 79 | conv = { 80 | "code": load_code, 81 | "struct": load_structure, 82 | "model": lambda v: load_model(v, arch), 83 | "arch": Str, 84 | "ensemble": Str, 85 | "opt_cell_fully": Bool, 86 | } 87 | for key, value in new_dict.items(): 88 | if key in conv: 89 | value = conv[key](value) 90 | # This is only in the case in which we use the run_from_config function, in that 91 | # case the config file would be made for aiida specifically not for janus 92 | elif convert_all: 93 | if key.endswith("_kwargs") or key.endswith("-kwargs"): 94 | key = key.replace("-kwargs", "_kwargs") 95 | value = Dict(value) 96 | else: 97 | value = Str(value) 98 | else: 99 | continue 100 | new_dict[key] = value 101 | return new_dict 102 | -------------------------------------------------------------------------------- /aiida_mlip/helpers/help_load.py: -------------------------------------------------------------------------------- 1 | """Helper functions for automatically loading models and strucutres as data nodes.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | 7 | from aiida.orm import StructureData, load_node 8 | from ase.build import bulk 9 | import ase.io 10 | import click 11 | 12 | from aiida_mlip.data.model import ModelData 13 | 14 | 15 | def load_model( 16 | model: str | Path | None, 17 | architecture: str, 18 | cache_dir: str | Path | None = None, 19 | ) -> ModelData: 20 | """ 21 | Load a model from a file path or URI. 22 | 23 | If the string represents a file path, the model will be loaded from that path. 24 | If it's a URI, the model will be downloaded from the specified location. 25 | If the input model is None it returns a default model corresponding to the 26 | default used in the Calcjobs. 27 | 28 | Parameters 29 | ---------- 30 | model : Optional[Union[str, Path]] 31 | Model file path or a URI for downloading the model or None to use the default. 32 | architecture : str 33 | The architecture of the model. 34 | cache_dir : Optional[Union[str, Path]] 35 | Directory where to save the dowloaded model. 36 | 37 | Returns 38 | ------- 39 | ModelData 40 | The loaded model. 41 | """ 42 | if model is None: 43 | loaded_model = ModelData.from_uri( 44 | "https://github.com/stfc/janus-core/raw/main/tests/models/mace_mp_small.model", 45 | architecture, 46 | cache_dir=cache_dir, 47 | ) 48 | elif (file_path := Path(model)).is_file(): 49 | loaded_model = ModelData.from_local(file_path, architecture=architecture) 50 | else: 51 | loaded_model = ModelData.from_uri( 52 | model, architecture=architecture, cache_dir=cache_dir 53 | ) 54 | return loaded_model 55 | 56 | 57 | def load_structure(struct: str | Path | int | None = None) -> StructureData: 58 | """ 59 | Load a StructureData instance from the given input. 60 | 61 | The input can be either a path to a structure file, a node PK (int), 62 | or None. If the input is None, a default StructureData instance for NaCl 63 | with a rocksalt structure will be created. 64 | 65 | Parameters 66 | ---------- 67 | struct : Optional[Union[str, Path, int]] 68 | The input value representing either a path to a structure file, a node PK, 69 | or None. 70 | 71 | Returns 72 | ------- 73 | StructureData 74 | The loaded or created StructureData instance. 75 | 76 | Raises 77 | ------ 78 | click.BadParameter 79 | If the input is not a valid path to a structure file or a node PK. 80 | """ 81 | if struct is None: 82 | structure = StructureData(ase=bulk("NaCl", "rocksalt", 5.63)) 83 | elif isinstance(struct, int) or (isinstance(struct, str) and struct.isdigit()): 84 | structure_pk = int(struct) 85 | structure = load_node(structure_pk) 86 | elif Path.exists(Path(struct)): 87 | structure = StructureData(ase=ase.io.read(Path(struct))) 88 | else: 89 | raise click.BadParameter( 90 | f"Invalid input: {struct}. Must be either node PK (int) or a valid \ 91 | path to a structure file." 92 | ) 93 | return structure 94 | -------------------------------------------------------------------------------- /aiida_mlip/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | """Parsers for calculations.""" 2 | -------------------------------------------------------------------------------- /aiida_mlip/parsers/base_parser.py: -------------------------------------------------------------------------------- 1 | """Parsers provided by aiida_mlip.""" 2 | 3 | from __future__ import annotations 4 | 5 | from aiida.engine import ExitCode 6 | from aiida.orm import SinglefileData 7 | from aiida.orm.nodes.process.process import ProcessNode 8 | from aiida.parsers.parser import Parser 9 | 10 | 11 | class BaseParser(Parser): 12 | """ 13 | Parser class for parsing output of calculation. 14 | 15 | Parameters 16 | ---------- 17 | node : aiida.orm.nodes.process.process.ProcessNode 18 | ProcessNode of calculation. 19 | 20 | Methods 21 | ------- 22 | __init__(node: aiida.orm.nodes.process.process.ProcessNode) 23 | Initialize the BaseParser instance. 24 | 25 | parse(**kwargs: Any) -> int: 26 | Parse outputs, store results in the database. 27 | 28 | Returns 29 | ------- 30 | int 31 | An exit code. 32 | 33 | Raises 34 | ------ 35 | exceptions.ParsingError 36 | If the ProcessNode being passed was not produced by a `Base` Calcjob. 37 | """ 38 | 39 | def __init__(self, node: ProcessNode): 40 | """ 41 | Check that the ProcessNode being passed was produced by a `Base` Calcjob. 42 | 43 | Parameters 44 | ---------- 45 | node : aiida.orm.nodes.process.process.ProcessNode 46 | ProcessNode of calculation. 47 | """ 48 | super().__init__(node) 49 | 50 | def parse(self, **kwargs) -> int: 51 | """ 52 | Parse outputs, store results in the database. 53 | 54 | Parameters 55 | ---------- 56 | **kwargs : Any 57 | Any keyword arguments. 58 | 59 | Returns 60 | ------- 61 | int 62 | An exit code. 63 | """ 64 | output_filename = self.node.get_option("output_filename") 65 | log_output = (self.node.inputs.log_filename).value 66 | 67 | # Check that folder content is as expected 68 | files_retrieved = self.retrieved.list_object_names() 69 | 70 | files_expected = {output_filename, log_output} 71 | if not files_expected.issubset(files_retrieved): 72 | self.logger.error( 73 | f"Found files '{files_retrieved}', expected to find '{files_expected}'" 74 | ) 75 | return self.exit_codes.ERROR_MISSING_OUTPUT_FILES 76 | 77 | # Add output file to the outputs 78 | 79 | with ( 80 | self.retrieved.open(log_output, "rb") as log, 81 | self.retrieved.open(output_filename, "rb") as output, 82 | ): 83 | self.out("log_output", SinglefileData(file=log, filename=log_output)) 84 | self.out( 85 | "std_output", SinglefileData(file=output, filename=output_filename) 86 | ) 87 | 88 | return ExitCode(0) 89 | -------------------------------------------------------------------------------- /aiida_mlip/parsers/descriptors_parser.py: -------------------------------------------------------------------------------- 1 | """Parsers provided by aiida_mlip.""" 2 | 3 | from __future__ import annotations 4 | 5 | from aiida.common import exceptions 6 | from aiida.orm.nodes.process.process import ProcessNode 7 | from aiida.plugins import CalculationFactory 8 | 9 | from aiida_mlip.parsers.sp_parser import SPParser 10 | 11 | DescriptorsCalc = CalculationFactory("mlip.descriptors") 12 | 13 | 14 | class DescriptorsParser(SPParser): 15 | """ 16 | Parser class for parsing output of descriptors calculation. 17 | 18 | Inherits from SPParser. 19 | 20 | Parameters 21 | ---------- 22 | node : aiida.orm.nodes.process.process.ProcessNode 23 | ProcessNode of calculation. 24 | 25 | Raises 26 | ------ 27 | exceptions.ParsingError 28 | If the ProcessNode being passed was not produced by a DescriptorsCalc. 29 | """ 30 | 31 | def __init__(self, node: ProcessNode): 32 | """ 33 | Check that the ProcessNode being passed was produced by a `Descriptors`. 34 | 35 | Parameters 36 | ---------- 37 | node : aiida.orm.nodes.process.process.ProcessNode 38 | ProcessNode of calculation. 39 | """ 40 | super().__init__(node) 41 | 42 | if not issubclass(node.process_class, DescriptorsCalc): 43 | raise exceptions.ParsingError("Can only parse `Descriptors` calculations") 44 | -------------------------------------------------------------------------------- /aiida_mlip/parsers/md_parser.py: -------------------------------------------------------------------------------- 1 | """MD parser.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | 7 | from aiida.common import exceptions 8 | from aiida.engine import ExitCode 9 | from aiida.orm import Dict, SinglefileData 10 | from aiida.orm.nodes.process.process import ProcessNode 11 | from aiida.plugins import CalculationFactory 12 | import yaml 13 | 14 | from aiida_mlip.calculations.md import MD 15 | from aiida_mlip.helpers.converters import xyz_to_aiida_traj 16 | from aiida_mlip.parsers.base_parser import BaseParser 17 | 18 | MDCalculation = CalculationFactory("mlip.md") 19 | 20 | 21 | class MDParser(BaseParser): 22 | """ 23 | Parser class for parsing output of molecular dynamics simulation. 24 | 25 | Inherits from SPParser. 26 | 27 | Parameters 28 | ---------- 29 | node : aiida.orm.nodes.process.process.ProcessNode 30 | ProcessNode of calculation. 31 | 32 | Methods 33 | ------- 34 | parse(**kwargs: Any) -> int: 35 | Parse outputs, store results in the database. 36 | 37 | Returns 38 | ------- 39 | int 40 | An exit code. 41 | 42 | Raises 43 | ------ 44 | exceptions.ParsingError 45 | If the ProcessNode being passed was not produced by a `MD`. 46 | """ 47 | 48 | def __init__(self, node: ProcessNode): 49 | """ 50 | Check that the ProcessNode being passed was produced by a `MD`. 51 | 52 | Parameters 53 | ---------- 54 | node : aiida.orm.nodes.process.process.ProcessNode 55 | ProcessNode of calculation. 56 | """ 57 | super().__init__(node) 58 | 59 | if not issubclass(node.process_class, MDCalculation): 60 | raise exceptions.ParsingError("Can only parse `MD` calculations") 61 | 62 | def parse(self, **kwargs) -> ExitCode: 63 | """ 64 | Parse outputs, store results in the database. 65 | 66 | Parameters 67 | ---------- 68 | **kwargs : Any 69 | Any keyword arguments. 70 | 71 | Returns 72 | ------- 73 | int 74 | An exit code. 75 | """ 76 | # Call the parent parse method to handle common parsing logic 77 | exit_code = super().parse(**kwargs) 78 | 79 | if exit_code != ExitCode(0): 80 | return exit_code 81 | 82 | md_dictionary = self.node.inputs.md_kwargs.get_dict() 83 | 84 | # Process trajectory file saving both the file and trajectory as aiida data 85 | traj_filepath = md_dictionary.get("traj-file", MD.DEFAULT_TRAJ_FILE) 86 | with self.retrieved.open(traj_filepath, "rb") as handle: 87 | self.out("traj_file", SinglefileData(file=handle, filename=traj_filepath)) 88 | final_str, traj_output = xyz_to_aiida_traj( 89 | Path(self.node.get_remote_workdir(), traj_filepath) 90 | ) 91 | self.out("traj_output", traj_output) 92 | self.out("final_structure", final_str) 93 | 94 | # Process stats file as singlefiledata 95 | stats_filepath = md_dictionary.get("stats-file", MD.DEFAULT_STATS_FILE) 96 | with self.retrieved.open(stats_filepath, "rb") as handle: 97 | self.out("stats_file", SinglefileData(file=handle, filename=stats_filepath)) 98 | 99 | # Process summary as both singlefiledata and results dictionary 100 | summary_filepath = md_dictionary.get("summary", MD.DEFAULT_SUMMARY_FILE) 101 | print(self.node.get_remote_workdir(), summary_filepath) 102 | with self.retrieved.open(summary_filepath, "rb") as handle: 103 | self.out("summary", SinglefileData(file=handle, filename=summary_filepath)) 104 | 105 | with self.retrieved.open(summary_filepath, "r") as handle: 106 | try: 107 | res_dict = yaml.safe_load(handle.read()) 108 | except yaml.YAMLError as exc: 109 | print("Error loading YAML:", exc) 110 | if res_dict is None: 111 | self.logger.error("Results dictionary empty") 112 | return self.exit_codes.ERROR_MISSING_OUTPUT_FILES 113 | results_node = Dict(res_dict) 114 | self.out("results_dict", results_node) 115 | return ExitCode(0) 116 | -------------------------------------------------------------------------------- /aiida_mlip/parsers/opt_parser.py: -------------------------------------------------------------------------------- 1 | """Geom optimisation parser.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | 7 | from aiida.common import exceptions 8 | from aiida.engine import ExitCode 9 | from aiida.orm import SinglefileData 10 | from aiida.orm.nodes.process.process import ProcessNode 11 | from aiida.plugins import CalculationFactory 12 | 13 | from aiida_mlip.helpers.converters import xyz_to_aiida_traj 14 | from aiida_mlip.parsers.sp_parser import SPParser 15 | 16 | GeomoptCalc = CalculationFactory("mlip.opt") 17 | 18 | 19 | class GeomOptParser(SPParser): 20 | """ 21 | Parser class for parsing output of geometry optimisation calculation. 22 | 23 | Inherits from SPParser. 24 | 25 | Parameters 26 | ---------- 27 | node : aiida.orm.nodes.process.process.ProcessNode 28 | ProcessNode of calculation. 29 | 30 | Methods 31 | ------- 32 | parse(**kwargs: Any) -> int: 33 | Parse outputs, store results in the database. 34 | 35 | Returns 36 | ------- 37 | int 38 | An exit code. 39 | 40 | Raises 41 | ------ 42 | exceptions.ParsingError 43 | If the ProcessNode being passed was not produced by a `GeomOpt`. 44 | """ 45 | 46 | def __init__(self, node: ProcessNode): 47 | """ 48 | Check that the ProcessNode being passed was produced by a `GeomOpt`. 49 | 50 | Parameters 51 | ---------- 52 | node : aiida.orm.nodes.process.process.ProcessNode 53 | ProcessNode of calculation. 54 | """ 55 | super().__init__(node) 56 | 57 | if not issubclass(node.process_class, GeomoptCalc): 58 | raise exceptions.ParsingError("Can only parse `GeomOpt` calculations") 59 | 60 | def parse(self, **kwargs) -> ExitCode: 61 | """ 62 | Parse outputs, store results in the database. 63 | 64 | Parameters 65 | ---------- 66 | **kwargs : Any 67 | Any keyword arguments. 68 | 69 | Returns 70 | ------- 71 | int 72 | An exit code. 73 | """ 74 | # Call the parent parse method to handle common parsing logic 75 | exit_code = super().parse(**kwargs) 76 | 77 | if exit_code == ExitCode(0): 78 | traj_file = (self.node.inputs.traj).value 79 | 80 | # Parse the trajectory file and save it as `SingleFileData` 81 | with self.retrieved.open(traj_file, "rb") as handle: 82 | self.out("traj_file", SinglefileData(file=handle, filename=traj_file)) 83 | # Parse trajectory and save it as `TrajectoryData` 84 | opt, traj_output = xyz_to_aiida_traj( 85 | Path(self.node.get_remote_workdir(), traj_file) 86 | ) 87 | self.out("traj_output", traj_output) 88 | 89 | # Parse the final structure of the trajectory to obtain the opt structure 90 | self.out("final_structure", opt) 91 | 92 | return exit_code 93 | -------------------------------------------------------------------------------- /aiida_mlip/parsers/sp_parser.py: -------------------------------------------------------------------------------- 1 | """Parsers provided by aiida_mlip.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | 7 | from aiida.common import exceptions 8 | from aiida.engine import ExitCode 9 | from aiida.orm import Dict, SinglefileData 10 | from aiida.orm.nodes.process.process import ProcessNode 11 | from aiida.plugins import CalculationFactory 12 | from ase.io import read 13 | 14 | from aiida_mlip.helpers.converters import convert_numpy 15 | from aiida_mlip.parsers.base_parser import BaseParser 16 | 17 | SinglepointCalc = CalculationFactory("mlip.sp") 18 | 19 | 20 | class SPParser(BaseParser): 21 | """ 22 | Parser class for parsing output of calculation. 23 | 24 | Parameters 25 | ---------- 26 | node : aiida.orm.nodes.process.process.ProcessNode 27 | ProcessNode of calculation. 28 | 29 | Methods 30 | ------- 31 | __init__(node: aiida.orm.nodes.process.process.ProcessNode) 32 | Initialize the SPParser instance. 33 | 34 | parse(**kwargs: Any) -> int: 35 | Parse outputs, store results in the database. 36 | 37 | Returns 38 | ------- 39 | int 40 | An exit code. 41 | 42 | Raises 43 | ------ 44 | exceptions.ParsingError 45 | If the ProcessNode being passed was not produced by a SinglepointCalc. 46 | """ 47 | 48 | def __init__(self, node: ProcessNode): 49 | """ 50 | Check that the ProcessNode being passed was produced by a `Singlepoint`. 51 | 52 | Parameters 53 | ---------- 54 | node : aiida.orm.nodes.process.process.ProcessNode 55 | ProcessNode of calculation. 56 | """ 57 | super().__init__(node) 58 | 59 | if not issubclass(node.process_class, SinglepointCalc): 60 | raise exceptions.ParsingError("Can only parse `Singlepoint` calculations") 61 | 62 | def parse(self, **kwargs) -> int: 63 | """ 64 | Parse outputs, store results in the database. 65 | 66 | Parameters 67 | ---------- 68 | **kwargs : Any 69 | Any keyword arguments. 70 | 71 | Returns 72 | ------- 73 | int 74 | An exit code. 75 | """ 76 | exit_code = super().parse(**kwargs) 77 | 78 | if exit_code != ExitCode(0): 79 | return exit_code 80 | 81 | xyz_output = (self.node.inputs.out).value 82 | 83 | # Check that folder content is as expected 84 | files_retrieved = self.retrieved.list_object_names() 85 | 86 | files_expected = {xyz_output} 87 | if not files_expected.issubset(files_retrieved): 88 | self.logger.error( 89 | f"Found files '{files_retrieved}', expected to find '{files_expected}'" 90 | ) 91 | return self.exit_codes.ERROR_MISSING_OUTPUT_FILES 92 | 93 | # Add output file to the outputs 94 | self.logger.info(f"Parsing '{xyz_output}'") 95 | 96 | with self.retrieved.open(xyz_output, "rb") as handle: 97 | self.out("xyz_output", SinglefileData(file=handle, filename=xyz_output)) 98 | 99 | content = read( 100 | Path(self.node.get_remote_workdir(), xyz_output), format="extxyz" 101 | ) 102 | results = convert_numpy(content.todict()) 103 | results_node = Dict(results) 104 | self.out("results_dict", results_node) 105 | 106 | return ExitCode(0) 107 | -------------------------------------------------------------------------------- /aiida_mlip/parsers/train_parser.py: -------------------------------------------------------------------------------- 1 | """Parser for mlip train.""" 2 | 3 | from __future__ import annotations 4 | 5 | import json 6 | from pathlib import Path 7 | from typing import Any 8 | 9 | from aiida.engine import ExitCode 10 | from aiida.orm import Dict, FolderData 11 | from aiida.orm.nodes.process.process import ProcessNode 12 | from aiida.parsers.parser import Parser 13 | 14 | from aiida_mlip.data.model import ModelData 15 | 16 | 17 | class TrainParser(Parser): 18 | """ 19 | Parser class for parsing output of calculation. 20 | 21 | Parameters 22 | ---------- 23 | node : aiida.orm.nodes.process.process.ProcessNode 24 | ProcessNode of calculation. 25 | 26 | Methods 27 | ------- 28 | __init__(node: aiida.orm.nodes.process.process.ProcessNode) 29 | Initialize the TrainParser instance. 30 | 31 | parse(**kwargs: Any) -> int: 32 | Parse outputs, store results in the database. 33 | 34 | _get_remote_dirs(mlip_dict: [str, Any]) -> [str, Path]: 35 | Get the remote directories based on mlip config file. 36 | 37 | _validate_retrieved_files(output_filename: str, model_name: str) -> bool: 38 | Validate that the expected files have been retrieved. 39 | 40 | _save_models(model_output: Path, compiled_model_output: Path) -> None: 41 | Save model and compiled model as outputs. 42 | 43 | _parse_results(result_name: Path) -> None: 44 | Parse the results file and store the results dictionary. 45 | 46 | _save_folders(remote_dirs: [str, Path]) -> None: 47 | Save log and checkpoint folders as outputs. 48 | 49 | Returns 50 | ------- 51 | int 52 | An exit code. 53 | 54 | Raises 55 | ------ 56 | exceptions.ParsingError 57 | If the ProcessNode being passed was not produced by a `Train` Calcjob. 58 | """ 59 | 60 | def __init__(self, node: ProcessNode): 61 | """ 62 | Initialize the TrainParser instance. 63 | 64 | Parameters 65 | ---------- 66 | node : aiida.orm.nodes.process.process.ProcessNode 67 | ProcessNode of calculation. 68 | """ 69 | super().__init__(node) 70 | 71 | def parse(self, **kwargs: Any) -> int: 72 | """ 73 | Parse outputs and store results in the database. 74 | 75 | Parameters 76 | ---------- 77 | **kwargs : Any 78 | Any keyword arguments. 79 | 80 | Returns 81 | ------- 82 | int 83 | An exit code. 84 | """ 85 | mlip_dict = self.node.inputs.mlip_config.as_dictionary 86 | output_filename = self.node.get_option("output_filename") 87 | remote_dirs = self._get_remote_dirs(mlip_dict) 88 | 89 | model_output = remote_dirs["model"] / f"{mlip_dict['name']}.model" 90 | compiled_model_output = ( 91 | remote_dirs["model"] / f"{mlip_dict['name']}_compiled.model" 92 | ) 93 | result_name = remote_dirs["results"] / f"{mlip_dict['name']}_run-123_train.txt" 94 | 95 | if not self._validate_retrieved_files(output_filename, mlip_dict["name"]): 96 | return self.exit_codes.ERROR_MISSING_OUTPUT_FILES 97 | 98 | self._save_models(model_output, compiled_model_output) 99 | self._parse_results(result_name) 100 | self._save_folders(remote_dirs) 101 | 102 | return ExitCode(0) 103 | 104 | def _get_remote_dirs(self, mlip_dict: dict) -> dict: 105 | """ 106 | Get the remote directories based on mlip config file. 107 | 108 | Parameters 109 | ---------- 110 | mlip_dict : dict 111 | Dictionary containing mlip config file. 112 | 113 | Returns 114 | ------- 115 | dict 116 | Dictionary of remote directories. 117 | """ 118 | rem_dir = Path(self.node.get_remote_workdir()) 119 | return { 120 | typ: rem_dir / mlip_dict.get(f"{typ}_dir", default) 121 | for typ, default in ( 122 | ("log", "logs"), 123 | ("checkpoint", "checkpoints"), 124 | ("results", "results"), 125 | ("model", ""), 126 | ) 127 | } 128 | 129 | def _validate_retrieved_files(self, output_filename: str, model_name: str) -> bool: 130 | """ 131 | Validate that the expected files have been retrieved. 132 | 133 | Parameters 134 | ---------- 135 | output_filename : str 136 | The expected output filename. 137 | model_name : str 138 | The name of the model as found in the config file key `name`. 139 | 140 | Returns 141 | ------- 142 | bool 143 | True if the expected files are retrieved, False otherwise. 144 | """ 145 | files_retrieved = self.retrieved.list_object_names() 146 | files_expected = {output_filename, f"{model_name}.model"} 147 | 148 | if not files_expected.issubset(files_retrieved): 149 | self.logger.error( 150 | f"Found files '{files_retrieved}', expected to find '{files_expected}'" 151 | ) 152 | return False 153 | return True 154 | 155 | def _save_models(self, model_output: Path, compiled_model_output: Path) -> None: 156 | """ 157 | Save model and compiled model as outputs. 158 | 159 | Parameters 160 | ---------- 161 | model_output : Path 162 | Path to the model output file. 163 | compiled_model_output : Path 164 | Path to the compiled model output file. 165 | """ 166 | architecture = "mace_mp" 167 | model = ModelData.from_local(model_output, architecture=architecture) 168 | compiled_model = ModelData.from_local( 169 | compiled_model_output, architecture=architecture 170 | ) 171 | 172 | self.out("model", model) 173 | self.out("compiled_model", compiled_model) 174 | 175 | def _parse_results(self, result_name: Path) -> None: 176 | """ 177 | Parse the results file and store the results dictionary. 178 | 179 | Parameters 180 | ---------- 181 | result_name : Path 182 | Path to the result file. 183 | """ 184 | with open(result_name, encoding="utf-8") as file: 185 | last_dict_str = None 186 | for line in file: 187 | try: 188 | last_dict_str = json.loads(line.strip()) 189 | except json.JSONDecodeError: 190 | continue 191 | 192 | if last_dict_str is not None: 193 | results_node = Dict(last_dict_str) 194 | self.out("results_dict", results_node) 195 | else: 196 | raise ValueError("No valid dictionary in the file") 197 | 198 | def _save_folders(self, remote_dirs: dict) -> None: 199 | """ 200 | Save log and checkpoint folders as outputs. 201 | 202 | Parameters 203 | ---------- 204 | remote_dirs : dict 205 | Dictionary of remote folders. 206 | """ 207 | log_node = FolderData(tree=remote_dirs["log"]) 208 | self.out("logs", log_node) 209 | 210 | checkpoint_node = FolderData(tree=remote_dirs["checkpoint"]) 211 | self.out("checkpoints", checkpoint_node) 212 | -------------------------------------------------------------------------------- /aiida_mlip/workflows/__init__.py: -------------------------------------------------------------------------------- 1 | """Workflows for aiida-mlip.""" 2 | -------------------------------------------------------------------------------- /aiida_mlip/workflows/ht_workgraph.py: -------------------------------------------------------------------------------- 1 | """Workgraph to run high-throughput calculations.""" 2 | 3 | from __future__ import annotations 4 | 5 | from collections.abc import Callable 6 | from pathlib import Path 7 | 8 | from aiida.engine import CalcJob, WorkChain 9 | from aiida.orm import Str 10 | from aiida_workgraph import WorkGraph, task 11 | from ase.io import read 12 | 13 | from aiida_mlip.helpers.help_load import load_structure 14 | 15 | 16 | @task.graph_builder(outputs=[{"name": "final_structures", "from": "context.structs"}]) 17 | def build_ht_calc( 18 | calc: CalcJob | Callable | WorkChain | WorkGraph, 19 | folder: Path | str | Str, 20 | calc_inputs: dict, 21 | input_struct_key: str = "struct", 22 | final_struct_key: str = "final_structure", 23 | recursive: bool = True, 24 | ) -> WorkGraph: 25 | """ 26 | Build high throughput calculation WorkGraph. 27 | 28 | The `calc` must take a structure, by default `struct`, as one of its inputs. 29 | Tasks will then be created to carry out the calculation for each structure file in 30 | `folder`. 31 | 32 | Parameters 33 | ---------- 34 | calc : Union[CalcJob, Callable, WorkChain, WorkGraph] 35 | Calculation to be performed on all structures. 36 | folder : Union[Path, str, Str] 37 | Path to the folder containing input structure files. 38 | calc_inputs : dict 39 | Dictionary of inputs, shared by all the calculations. Must not contain 40 | `struct_key`. 41 | input_struct_key : str 42 | Keyword for input structure for `calc`. Default is "struct". 43 | final_struct_key : str 44 | Key for final structure output from `calc`. Default is "final_structure". 45 | recursive : bool 46 | Whether to search `folder` recursively. Default is True. 47 | 48 | Returns 49 | ------- 50 | WorkGraph 51 | The workgraph with calculation tasks for each structure. 52 | 53 | Raises 54 | ------ 55 | FileNotFoundError 56 | If `folder` has no valid structure files. 57 | """ 58 | wg = WorkGraph() 59 | structure = None 60 | 61 | if isinstance(folder, Str): 62 | folder = Path(folder.value) 63 | if isinstance(folder, str): 64 | folder = Path(folder) 65 | 66 | pattern = "**/*" if recursive else "*" 67 | for file in filter(Path.is_file, folder.glob(pattern)): 68 | try: 69 | read(file) 70 | except Exception: 71 | continue 72 | structure = load_structure(file) 73 | calc_inputs[input_struct_key] = structure 74 | calc_task = wg.add_task( 75 | calc, 76 | name=f"calc_{file.stem}", 77 | **calc_inputs, 78 | ) 79 | calc_task.set_context({f"structs.{file.stem}": final_struct_key}) 80 | 81 | if structure is None: 82 | raise FileNotFoundError( 83 | f"{folder} is empty or has no readable structure files." 84 | ) 85 | 86 | return wg 87 | 88 | 89 | def get_ht_workgraph( 90 | calc: CalcJob | Callable | WorkChain | WorkGraph, 91 | folder: Path | str | Str, 92 | calc_inputs: dict, 93 | input_struct_key: str = "struct", 94 | final_struct_key: str = "final_structure", 95 | recursive: bool = True, 96 | max_number_jobs: int = 10, 97 | ) -> WorkGraph: 98 | """ 99 | Get WorkGraph to carry out calculation on all structures in a directory. 100 | 101 | Parameters 102 | ---------- 103 | calc : Union[CalcJob, Callable, WorkChain, WorkGraph] 104 | Calculation to be performed on all structures. 105 | folder : Union[Path, str, Str] 106 | Path to the folder containing input structure files. 107 | calc_inputs : dict 108 | Dictionary of inputs, shared by all the calculations. Must not contain 109 | `struct_key`. 110 | input_struct_key : str 111 | Keyword for input structure for `calc`. Default is "struct". 112 | final_struct_key : str 113 | Key for final structure output from `calc`. Default is "final_structure". 114 | recursive : bool 115 | Whether to search `folder` recursively. Default is True. 116 | max_number_jobs : int 117 | Max number of subprocesses running within the WorkGraph. Default is 10. 118 | 119 | Returns 120 | ------- 121 | WorkGraph 122 | The workgraph ready to be submitted. 123 | """ 124 | wg = WorkGraph("ht_calculation") 125 | 126 | wg.add_task( 127 | build_ht_calc, 128 | name="ht_calc", 129 | calc=calc, 130 | folder=folder, 131 | calc_inputs=calc_inputs, 132 | input_struct_key=input_struct_key, 133 | final_struct_key=final_struct_key, 134 | recursive=recursive, 135 | ) 136 | 137 | wg.group_outputs = [ 138 | {"name": "final_structures", "from": "ht_calc.final_structures"} 139 | ] 140 | wg.max_number_jobs = max_number_jobs 141 | 142 | return wg 143 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -n -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: all help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext customdefault 23 | 24 | ## Runs nit-picky and converting warnings into errors to 25 | ## make sure the documentation is properly written 26 | customdefault: 27 | $(SPHINXBUILD) -b html -nW --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html 28 | 29 | all: html 30 | 31 | clean: 32 | rm -r $(BUILDDIR) 33 | 34 | html: 35 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 36 | @echo 37 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 38 | 39 | 40 | view: 41 | xdg-open $(BUILDDIR)/html/index.html 42 | -------------------------------------------------------------------------------- /docs/source/apidoc/aiida_mlip.calculations.rst: -------------------------------------------------------------------------------- 1 | aiida\_mlip.calculations package 2 | ================================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | aiida\_mlip.calculations.base module 8 | ------------------------------------ 9 | 10 | .. automodule:: aiida_mlip.calculations.base 11 | :members: 12 | :special-members: 13 | :private-members: 14 | :undoc-members: 15 | :show-inheritance: 16 | 17 | aiida\_mlip.calculations.descriptors module 18 | ------------------------------------------- 19 | 20 | .. automodule:: aiida_mlip.calculations.descriptors 21 | :members: 22 | :special-members: 23 | :private-members: 24 | :undoc-members: 25 | :show-inheritance: 26 | 27 | aiida\_mlip.calculations.geomopt module 28 | --------------------------------------- 29 | 30 | .. automodule:: aiida_mlip.calculations.geomopt 31 | :members: 32 | :special-members: 33 | :private-members: 34 | :undoc-members: 35 | :show-inheritance: 36 | 37 | aiida\_mlip.calculations.md module 38 | ---------------------------------- 39 | 40 | .. automodule:: aiida_mlip.calculations.md 41 | :members: 42 | :special-members: 43 | :private-members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | aiida\_mlip.calculations.singlepoint module 48 | ------------------------------------------- 49 | 50 | .. automodule:: aiida_mlip.calculations.singlepoint 51 | :members: 52 | :special-members: 53 | :private-members: 54 | :undoc-members: 55 | :show-inheritance: 56 | 57 | aiida\_mlip.calculations.train module 58 | ------------------------------------- 59 | 60 | .. automodule:: aiida_mlip.calculations.train 61 | :members: 62 | :special-members: 63 | :private-members: 64 | :undoc-members: 65 | :show-inheritance: 66 | 67 | Module contents 68 | --------------- 69 | 70 | .. automodule:: aiida_mlip.calculations 71 | :members: 72 | :special-members: 73 | :private-members: 74 | :undoc-members: 75 | :show-inheritance: 76 | -------------------------------------------------------------------------------- /docs/source/apidoc/aiida_mlip.data.rst: -------------------------------------------------------------------------------- 1 | aiida\_mlip.data package 2 | ======================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | aiida\_mlip.data.config module 8 | ------------------------------ 9 | 10 | .. automodule:: aiida_mlip.data.config 11 | :members: 12 | :special-members: 13 | :private-members: 14 | :undoc-members: 15 | :show-inheritance: 16 | 17 | aiida\_mlip.data.model module 18 | ----------------------------- 19 | 20 | .. automodule:: aiida_mlip.data.model 21 | :members: 22 | :special-members: 23 | :private-members: 24 | :undoc-members: 25 | :show-inheritance: 26 | 27 | Module contents 28 | --------------- 29 | 30 | .. automodule:: aiida_mlip.data 31 | :members: 32 | :special-members: 33 | :private-members: 34 | :undoc-members: 35 | :show-inheritance: 36 | -------------------------------------------------------------------------------- /docs/source/apidoc/aiida_mlip.helpers.rst: -------------------------------------------------------------------------------- 1 | aiida\_mlip.helpers package 2 | =========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | aiida\_mlip.helpers.converters module 8 | ------------------------------------- 9 | 10 | .. automodule:: aiida_mlip.helpers.converters 11 | :members: 12 | :special-members: 13 | :private-members: 14 | :undoc-members: 15 | :show-inheritance: 16 | 17 | aiida\_mlip.helpers.help\_load module 18 | ------------------------------------- 19 | 20 | .. automodule:: aiida_mlip.helpers.help_load 21 | :members: 22 | :special-members: 23 | :private-members: 24 | :undoc-members: 25 | :show-inheritance: 26 | 27 | Module contents 28 | --------------- 29 | 30 | .. automodule:: aiida_mlip.helpers 31 | :members: 32 | :special-members: 33 | :private-members: 34 | :undoc-members: 35 | :show-inheritance: 36 | -------------------------------------------------------------------------------- /docs/source/apidoc/aiida_mlip.parsers.rst: -------------------------------------------------------------------------------- 1 | aiida\_mlip.parsers package 2 | =========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | aiida\_mlip.parsers.base\_parser module 8 | --------------------------------------- 9 | 10 | .. automodule:: aiida_mlip.parsers.base_parser 11 | :members: 12 | :special-members: 13 | :private-members: 14 | :undoc-members: 15 | :show-inheritance: 16 | 17 | aiida\_mlip.parsers.descriptors\_parser module 18 | ---------------------------------------------- 19 | 20 | .. automodule:: aiida_mlip.parsers.descriptors_parser 21 | :members: 22 | :special-members: 23 | :private-members: 24 | :undoc-members: 25 | :show-inheritance: 26 | 27 | aiida\_mlip.parsers.md\_parser module 28 | ------------------------------------- 29 | 30 | .. automodule:: aiida_mlip.parsers.md_parser 31 | :members: 32 | :special-members: 33 | :private-members: 34 | :undoc-members: 35 | :show-inheritance: 36 | 37 | aiida\_mlip.parsers.opt\_parser module 38 | -------------------------------------- 39 | 40 | .. automodule:: aiida_mlip.parsers.opt_parser 41 | :members: 42 | :special-members: 43 | :private-members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | aiida\_mlip.parsers.sp\_parser module 48 | ------------------------------------- 49 | 50 | .. automodule:: aiida_mlip.parsers.sp_parser 51 | :members: 52 | :special-members: 53 | :private-members: 54 | :undoc-members: 55 | :show-inheritance: 56 | 57 | aiida\_mlip.parsers.train\_parser module 58 | ---------------------------------------- 59 | 60 | .. automodule:: aiida_mlip.parsers.train_parser 61 | :members: 62 | :special-members: 63 | :private-members: 64 | :undoc-members: 65 | :show-inheritance: 66 | 67 | Module contents 68 | --------------- 69 | 70 | .. automodule:: aiida_mlip.parsers 71 | :members: 72 | :special-members: 73 | :private-members: 74 | :undoc-members: 75 | :show-inheritance: 76 | -------------------------------------------------------------------------------- /docs/source/apidoc/aiida_mlip.rst: -------------------------------------------------------------------------------- 1 | aiida\_mlip package 2 | =================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | aiida_mlip.calculations 11 | aiida_mlip.data 12 | aiida_mlip.helpers 13 | aiida_mlip.parsers 14 | aiida_mlip.workflows 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: aiida_mlip 20 | :members: 21 | :special-members: 22 | :private-members: 23 | :undoc-members: 24 | :show-inheritance: 25 | -------------------------------------------------------------------------------- /docs/source/apidoc/aiida_mlip.workflows.rst: -------------------------------------------------------------------------------- 1 | aiida\_mlip.workflows package 2 | ============================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | aiida\_mlip.workflows.ht\_workgraph module 8 | ------------------------------------------ 9 | 10 | .. automodule:: aiida_mlip.workflows.ht_workgraph 11 | :members: 12 | :special-members: 13 | :private-members: 14 | :undoc-members: 15 | :show-inheritance: 16 | 17 | Module contents 18 | --------------- 19 | 20 | .. automodule:: aiida_mlip.workflows 21 | :members: 22 | :special-members: 23 | :private-members: 24 | :undoc-members: 25 | :show-inheritance: 26 | -------------------------------------------------------------------------------- /docs/source/developer_guide/index.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Developer guide 3 | =============== 4 | 5 | Getting started 6 | +++++++++++++++ 7 | 8 | We recommend `installing uv `_ 9 | for dependency management when developing for ``aiida-mlip``. 10 | 11 | This provides a number of useful features, including: 12 | 13 | - `Dependency management `_ (``uv [add,remove]`` etc.) and organization (`groups `_) 14 | 15 | - Storing the versions of all installations in a `uv.lock `_ file, for reproducible builds 16 | 17 | - Improved `dependency resolution `_ 18 | 19 | - Virtual environment management 20 | 21 | - `Building and publishing `_ tools 22 | 23 | * Currently, an external build backend, such as `pdm `_, is required 24 | 25 | 26 | After cloning the repository, dependencies useful for development can then be installed by running:: 27 | 28 | uv sync -p 3.12 --extra mace -U 29 | source .venv/bin/activate 30 | 31 | 32 | Using uv 33 | ++++++++ 34 | 35 | ``uv`` manages a `persistent environment `_ 36 | with the project and its dependencies in a ``.venv`` directory, adjacent to ``pyproject.toml``. This will be created automatically as needed. 37 | 38 | ``uv`` provides two separate APIs for managing your Python project and environment. 39 | 40 | ``uv pip`` is designed to resemble the ``pip`` CLI, with similar commands (``uv pip install``, ``uv pip list``, ``uv pip tree``, etc.), 41 | and is slightly lower level. `Compared with pip `_, 42 | ``uv`` tends to be stricter, but in most cases ``uv pip`` could be used in place of ``pip``. 43 | 44 | ``uv add``, ``uv run``, ``uv sync``, and ``uv lock`` are known as "project APIs", and are slightly higher level. 45 | These commands interact with (and require) ``pyproject.toml``, and ``uv`` will ensure your environment is in-sync when they are called, 46 | including creating or updating a `lockfile `_, 47 | a universal resolution that is `portable across platforms `_. 48 | 49 | When developing for ``aiida-mlip``, it is usually recommended to use project commands, as described in `Getting started`_ 50 | rather than using ``uv pip install`` to modify the project environment manually. 51 | 52 | .. tip:: 53 | 54 | ``uv`` will detect and use Python versions available on your system, 55 | but can also be used to `install Python automtically `_. 56 | The desired Python version can be specified when running project commands with the ``--python``/``-p`` option. 57 | 58 | 59 | For further information, please refer to the `documentation `_. 60 | 61 | Setting up PostgreSQL 62 | +++++++++++++++++++++ 63 | 64 | ``aiida-mlip`` requires a PostgreSQL database to be set up for the tests to run successfully. 65 | 66 | PostgreSQL can be installed outside the virtual environment:: 67 | 68 | sudo apt install postgresql 69 | 70 | The `Ubuntu Server `_ docs go over installing PostgreSQL on Ubuntu. 71 | For other operating systems, please refer to the `PostgreSQL documentation `_. 72 | 73 | Then for specific instructions on setting up PostgreSQL for AiiDA, please refer to the `AiiDA documentation `_. 74 | 75 | 76 | Running the tests 77 | +++++++++++++++++ 78 | 79 | Packages in the ``dev`` dependency group allow tests to be run locally using ``pytest``, by running:: 80 | 81 | pytest -v 82 | 83 | .. note:: 84 | 85 | MACE must be installed for tests to run successfully. PostgreSQL must also be installed and running. 86 | 87 | 88 | Alternatively, tests can be run in separate virtual environments using ``tox``:: 89 | 90 | tox run -e ALL 91 | 92 | This will run all unit tests for multiple versions of Python, in addition to testing that the pre-commit passes, and that documentation builds, mirroring the automated tests on GitHub. 93 | 94 | Individual components of the ``tox`` test suite can also be run separately, such as running only running the unit tests with Python 3.12:: 95 | 96 | tox run -e py312 97 | 98 | See the `tox documentation `_ for further options. 99 | 100 | 101 | Automatic coding style checks 102 | +++++++++++++++++++++++++++++ 103 | 104 | Packages in the ``pre-commit`` dependency group allow automatic code formatting and linting on every commit. 105 | 106 | To set this up, run:: 107 | 108 | pre-commit install 109 | 110 | After this, the `ruff linter `_, `ruff formatter `_, and `numpydoc `_ (docstring style validator), will run before every commit. 111 | 112 | Rules enforced by ruff are currently set up to be comparable to: 113 | 114 | - `black `_ (code formatter) 115 | - `pylint `_ (linter) 116 | - `pyupgrade `_ (syntax upgrader) 117 | - `isort `_ (import sorter) 118 | - `flake8-bugbear `_ (bug finder) 119 | 120 | The full set of `ruff rules `_ are specified by the ``[tool.ruff]`` sections of `pyproject.toml `_. 121 | 122 | If you ever need to skip these pre-commit hooks, just use:: 123 | 124 | git commit -n 125 | 126 | You should also keep the pre-commit hooks up to date periodically, with:: 127 | 128 | pre-commit autoupdate 129 | 130 | Or consider using `pre-commit.ci `_. 131 | 132 | 133 | Building the documentation 134 | ++++++++++++++++++++++++++ 135 | 136 | Packages in the ``docs`` dependency group install `Sphinx `_ 137 | and other Python packages required to build ``aiida-mlip``'s documentation. 138 | 139 | Individual individual documentation pages can be edited directly:: 140 | 141 | docs/source/index.rst 142 | docs/source/developer_guide/index.rst 143 | docs/source/user_guide/index.rst 144 | docs/source/user_guide/get_started.rst 145 | docs/source/user_guide/tutorial.rst 146 | 147 | 148 | ``Sphinx`` can then be used to generate the html documentation:: 149 | 150 | cd docs 151 | make clean; make html 152 | 153 | 154 | Check the result by opening ``build/html/index.html`` in your browser. 155 | 156 | 157 | Continuous integration 158 | ++++++++++++++++++++++ 159 | 160 | ``aiida-mlip`` comes with a ``.github`` folder that contains continuous integration tests 161 | on every commit using `GitHub Actions `_. It will: 162 | 163 | #. Run all tests 164 | #. Build the documentation 165 | #. Check coding style 166 | -------------------------------------------------------------------------------- /docs/source/images/AiiDA_transparent_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stfc/aiida-mlip/e358063de55985d345dbf56df3ea9963ce22e8de/docs/source/images/AiiDA_transparent_logo.png -------------------------------------------------------------------------------- /docs/source/images/aiida-mlip-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stfc/aiida-mlip/e358063de55985d345dbf56df3ea9963ce22e8de/docs/source/images/aiida-mlip-100.png -------------------------------------------------------------------------------- /docs/source/images/aiida-mlip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stfc/aiida-mlip/e358063de55985d345dbf56df3ea9963ce22e8de/docs/source/images/aiida-mlip.png -------------------------------------------------------------------------------- /docs/source/images/alc-100.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stfc/aiida-mlip/e358063de55985d345dbf56df3ea9963ce22e8de/docs/source/images/alc-100.webp -------------------------------------------------------------------------------- /docs/source/images/cosec-100.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stfc/aiida-mlip/e358063de55985d345dbf56df3ea9963ce22e8de/docs/source/images/cosec-100.webp -------------------------------------------------------------------------------- /docs/source/images/psdi-100.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stfc/aiida-mlip/e358063de55985d345dbf56df3ea9963ce22e8de/docs/source/images/psdi-100.webp -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | The aiida-mlip plugin for `AiiDA`_ 2 | ===================================================== 3 | 4 | ``aiida-mlip`` is available at http://github.com/aiidateam/aiida-mlip 5 | 6 | .. image:: images/aiida-mlip.png 7 | :height: 298px 8 | :target: http://github.com/aiidateam/aiida-mlip 9 | 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | user_guide/index 15 | developer_guide/index 16 | API documentation 17 | AiiDA Documentation 18 | 19 | If you use this plugin for your research, please cite the following work: 20 | 21 | .. highlights:: Author Name1, Author Name2, *Paper title*, Jornal Name XXX, YYYY (Year). 22 | 23 | If you use AiiDA for your research, please cite the following work: 24 | 25 | .. highlights:: Giovanni Pizzi, Andrea Cepellotti, Riccardo Sabatini, Nicola Marzari, 26 | and Boris Kozinsky, *AiiDA: automated interactive infrastructure and database 27 | for computational science*, Comp. Mat. Sci 111, 218-230 (2016); 28 | https://doi.org/10.1016/j.commatsci.2015.09.013; http://www.aiida.net. 29 | 30 | ``aiida-mlip`` is released under the `BSD 3-Clause license `_. 31 | 32 | Funding 33 | ======= 34 | 35 | Contributors to ``aiida-mlip`` were supported by 36 | 37 | .. image:: images/psdi-100.webp 38 | :height: 100px 39 | :target: https://www.psdi.ac.uk/ 40 | 41 | .. image:: images/alc-100.webp 42 | :height: 100px 43 | :target: https://adalovelacecentre.ac.uk/ 44 | 45 | .. image:: images/cosec-100.webp 46 | :height: 100px 47 | :target: https://www.scd.stfc.ac.uk/Pages/CoSeC.aspx 48 | 49 | 50 | 51 | 52 | Indices and tables 53 | ================== 54 | 55 | * :ref:`genindex` 56 | * :ref:`modindex` 57 | * :ref:`search` 58 | 59 | .. _AiiDA: http://www.aiida.net 60 | -------------------------------------------------------------------------------- /docs/source/user_guide/calculations.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Calculations 3 | ============================== 4 | 5 | In these examples, we will assume that the `janus-core `_ package is installed and saved in the AiiDA database as an `InstalledCode` instance named 'janus@localhost'. 6 | 7 | The structure should be a path to a file. Here, the structure file is specified as `path/to/structure`. 8 | 9 | .. note:: 10 | Any format that `ASE `_ can read is a valid structure file for a calculation. 11 | 12 | The model file determines the specific MLIP to be used. It can be a local file or a URI to a file to download. In these examples, it is assumed to be a local file located at `path/to/model`. 13 | 14 | 15 | SinglePoint Calculation 16 | ----------------------- 17 | 18 | A `Singlepoint` Calculation represents a `Calcjob` object within the AiiDA framework. 19 | 20 | 21 | Usage 22 | ^^^^^ 23 | 24 | This calculation can be executed using either the `run` or `submit` AiiDA commands. 25 | Below is a usage example with the minimum required parameters. These parameters must be AiiDA data types. 26 | 27 | 28 | .. code-block:: python 29 | 30 | SinglePointCalculation = CalculationFactory("mlip.sp") 31 | submit(SinglePointCalculation, code=InstalledCode, structure=StructureData, metadata={"options": {"resources": {"num_machines": 1}}}) 32 | 33 | The inputs can be grouped into a dictionary: 34 | 35 | .. code-block:: python 36 | 37 | inputs = { 38 | "metadata": {"options": {"resources": {"num_machines": 1}}}, 39 | "code": InstalledCode, 40 | "architecture": Str, 41 | "structure": StructureData, 42 | "model": ModelData, 43 | "precision": Str, 44 | "device": Str, 45 | } 46 | SinglePointCalculation = CalculationFactory("mlip.sp") 47 | submit(SinglePointCalculation, **inputs) 48 | 49 | 50 | Or they can be passed as a config file. The config file has to be structured as it would be for a janus calculation (refer to `janus documentation `_ ) and passed as an AiiDA data type itself. 51 | The config file contains the parameters in yaml format: 52 | 53 | .. code-block:: yaml 54 | 55 | properties: 56 | - "energy" 57 | arch: "mace_mp" 58 | device: "cpu" 59 | struct: "path/to/structure.cif" 60 | model: "path/to/model.model" 61 | 62 | And it is used as shown below. Note that some parameters, which are specific to AiiDA, need to be given individually. 63 | 64 | .. code-block:: python 65 | 66 | # Add the required inputs for AiiDA 67 | metadata = {"options": {"resources": {"num_machines": 1}}} 68 | code = load_code("janus@localhost") 69 | 70 | # All the other parameters are fetched from the config file 71 | # We want to pass it as an AiiDA data type for provenance 72 | config = JanusConfigfile("path/to/config.yaml") 73 | 74 | # Define calculation to run 75 | SinglePointCalculation = CalculationFactory("mlip.sp") 76 | 77 | # Run calculation 78 | result, node = run_get_node( 79 | SinglePointCalculation, 80 | code=code, 81 | metadata=metadata, 82 | config=config, 83 | ) 84 | 85 | If a parameter is defined twice, in the config file and manually, the manually defined one will overwrite the config one. 86 | If for example the same config file as before is used, but this time the parameter "struct" is added to the launch function, the code would look like this: 87 | 88 | .. code-block:: python 89 | 90 | # Run calculation 91 | result, node = run_get_node( 92 | SinglePointCalculation, 93 | code=code, 94 | struct=StructureData(ase=read("path/to/structure2.xyz")) 95 | metadata=metadata, 96 | config=config, 97 | ) 98 | 99 | In this case the structure used is going to be "path/to/structure2.xyz" rather than ""path/to/structure.cif", which was defined in the config file. 100 | 101 | Refer to the API documentation for additional parameters that can be passed. 102 | Some parameters are not required and don't have a default value set in aiida-mlip. In that case the default values will be the same as `janus `_ 103 | The only default parameters defined in aiida-mlip are the names of the input and output files, as they do not affect the results of the calculation itself, and are needed in AiiDA to parse the results. 104 | For example in the code above the parameter "precision" is never defined, neither in the config nor in the run_get_node function. 105 | The parameter will default to the janus default, which is "float64" 106 | 107 | 108 | Submission 109 | ^^^^^^^^^^ 110 | 111 | To facilitate the submission process and prepare inputs as AiiDA data types, example scripts are provided. 112 | The submit_singlepoint.py script can be used as is, submitted to verdi, and the parameters passed as strings to the CLI. 113 | They will be converted to AiiDA data types by the script itself. 114 | .. note:: 115 | 116 | 117 | The example files are set up with default values, ensuring that calculations runs even if no input is provided via the cli. 118 | However, the aiida-mlip code itself does require certain parameters, (e.g. the structure on which to perform the calculation). 119 | 120 | 121 | .. code-block:: python 122 | 123 | verdi run submit_singlepoint.py "janus@localhost" --structure "path/to/structure" --model "path/to/model" --precision "float64" --device "cpu" 124 | 125 | The submit_using_config.py script can be used to facilitate submission using a config file. 126 | 127 | Geometry Optimisation calculation 128 | --------------------------------- 129 | 130 | A `GeomOpt` Calculation represents a `Calcjob` object within the AiiDA framework. 131 | 132 | 133 | Usage 134 | ^^^^^ 135 | 136 | This calculation can be executed using either the `run` or `submit` AiiDA commands. 137 | Below is a usage example with some additional geometry optimisation parameters. These parameters must be AiiDA data types. 138 | 139 | 140 | .. code-block:: python 141 | 142 | 143 | GeomOptCalculation = CalculationFactory("mlip.opt") 144 | submit(GeomOptCalculation, code=InstalledCode, structure=StructureData, max_force=Float(0.1), opt_cell_lengths=Bool(True)) 145 | 146 | 147 | .. note:: 148 | 149 | As per the singlepoint calculation, the parameters can be provided as a dictionary or config file. 150 | 151 | Submission 152 | ^^^^^^^^^^ 153 | 154 | To facilitate the submission process and prepare inputs as AiiDA data types, an example script is provided. 155 | This script can be used as is, submitted to verdi, and the parameters passed as strings to the CLI. 156 | They will be converted to AiiDA data types by the script itself. 157 | 158 | .. code-block:: python 159 | 160 | verdi run submit_geomopt.py "janus@localhost" --structure "path/to/structure" --model "path/to/model" --precision "float64" --device "cpu" 161 | 162 | 163 | 164 | Molecular Dynamics calculation 165 | ------------------------------ 166 | 167 | An `MD` Calculation represents a `Calcjob` object within the AiiDA framework. 168 | 169 | 170 | Usage 171 | ^^^^^ 172 | 173 | This calculation can be executed using either the `run` or `submit` AiiDA commands. 174 | Below is a usage example with some additional geometry optimisation parameters. These parameters must be AiiDA data types. 175 | 176 | 177 | .. code-block:: python 178 | 179 | 180 | MDCalculation = CalculationFactory("mlip.md") 181 | submit(MDCalculation, code=InstalledCode, structure=StructureData, ensemble=Str("nve"), md_dict=Dict({'temp':300,'steps': 4,'traj-every':3,'stats-every':1})) 182 | 183 | .. note:: 184 | 185 | As per the singlepoint calculation, the parameters can be provided as a dictionary or config file. 186 | 187 | Submission 188 | ^^^^^^^^^^ 189 | 190 | To facilitate the submission process and prepare inputs as AiiDA data types, an example script is provided. 191 | This script can be used as is, submitted to verdi, and the parameters passed as strings to the CLI. 192 | They will be converted to AiiDA data types by the script itself. 193 | 194 | .. code-block:: python 195 | 196 | verdi run submit_md.py "janus@localhost" --structure "path/to/structure" --model "path/to/model" --ensemble "nve" --md_dict_str "{'temp':300,'steps':4,'traj-every':3,'stats-every':1}" 197 | -------------------------------------------------------------------------------- /docs/source/user_guide/data.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Data types 3 | ============================== 4 | 5 | ModelData 6 | --------- 7 | Defines a custom data type called `ModelData` in AiiDA, which is a subclass of the `SinglefileData` type. `ModelData` is used to handle model files and provides functionalities for handling local files and downloading files from URIs. 8 | Additional features compared to `SinglefileData`: 9 | 10 | - It can take a relative path as an argument 11 | 12 | - It takes the argument "architecture" which is specifically related to the mlip model and it is added to the node attributes. 13 | 14 | - Download functionality: 15 | - When provided with a URI, `ModelData` automatically downloads the file. 16 | - Saves the downloaded file in a specified folder (default: `./cache/mlips`), creating a subfolder if the architecture, and stores it as an AiiDA data type. 17 | - Handles duplicate files: if the file is downloaded twice, duplicates within the same folder are canceled, unless `force_download=True` is stated. 18 | 19 | Usage 20 | ^^^^^ 21 | 22 | - To create a `ModelData` object from a local file: 23 | 24 | .. code-block:: python 25 | 26 | model = ModelData.from_local('/path/to/file', filename='model', architecture='mace') 27 | 28 | - To download a file and save it as a `ModelData` object: 29 | 30 | .. code-block:: python 31 | 32 | model = ModelData.from_uri('http://yoururl.test/model', architecture='mace', filename='model', cache_dir='/home/mlip/', force_download=False) 33 | 34 | - The architecture of the model file can be accessed using the `architecture` property: 35 | 36 | .. code-block:: python 37 | 38 | model_arch = model.architecture 39 | 40 | As for a `SinglefileData`, the content of the model file can be accessed using the function `get_content()` 41 | 42 | 43 | JanusConfigfile 44 | --------------- 45 | 46 | The `JanusConfigfile` class is designed to handle config files written for janus-core in YAML format within the AiiDA framework. 47 | This class inherits from `SinglefileData` in the AiiDA, and extends it to support YAML config files. 48 | It provides methods for reading, storing, and accessing the content of the config file. 49 | 50 | Usage 51 | ^^^^^ 52 | 53 | - To create a `JanusConfigfile` object: 54 | 55 | .. code-block:: python 56 | 57 | config_file = JanusConfigfile('/path/to/config.yml') 58 | 59 | 60 | - To read the content of the config file as a dictionary, you can use the `read_yaml()` method: 61 | 62 | .. code-block:: python 63 | 64 | config_dict = config_file.read_yaml() 65 | 66 | 67 | - To store the content of the config file in the AiiDA database, you can use the `store_content()` method: 68 | 69 | .. code-block:: python 70 | 71 | config_file.store_content(store_all=False, skip=[]) 72 | 73 | The `store_content()` method accepts the following parameters: 74 | 75 | - `store_all` (bool): 76 | Determines whether to store all parameters or only specific ones. 77 | By default, it's set to `False`. 78 | When set to `False`, only the key parameters relevant for the provenance graph are stored: `code`, `structure`, `model`, `architecture`, `opt_cell_fully` (for GeomOpt), and `ensemble` (for MD). 79 | However, all inputs can be accessed in the config file at any time (just the config file will appear in the provenance graph as JanusConfigfile). 80 | If `store_all` is set to `True`, all inputs are stored, either as specific data types (e.g. the input 'struct' is recognised as a StructureData type) or as Str. 81 | 82 | - `skip` (list): 83 | Specifies a list of parameters that should not be stored. 84 | In the source code of the calcjobs, when the same parameter is provided both as an AiiDA input and within the config file, the parameter from the config file is ignored and not stored. 85 | These parameters are added to the `skip` list to ensure they are excluded from storage. 86 | 87 | 88 | - The filepath of the config file can be accessed using the `filepath` property: 89 | 90 | .. code-block:: python 91 | 92 | file_path = config_file.filepath 93 | 94 | .. warning:: 95 | 96 | When sharing data, using the ``filepath`` could point to a location inaccessible on another computer. 97 | So if you are using data from someone else, for both the modeldata and the configfile, consider using the ``get_content()`` method to create a new file with identical content. 98 | Then, use the filepath of the newly created file for running calculation. 99 | A more robust solution to this problem is going to be implemented. 100 | 101 | 102 | - The content of the config file can be accessed as a dictionary using the `as_dictionary` property: 103 | 104 | .. code-block:: python 105 | 106 | config_dict = config_file.as_dictionary 107 | -------------------------------------------------------------------------------- /docs/source/user_guide/get_started.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Getting started 3 | =============== 4 | 5 | Installation 6 | ++++++++++++ 7 | 8 | We suggest creating a new `virtual environment `_ and activating it before running the commands below. 9 | 10 | The latest stable release of ``aiida-mlip``, including its dependencies, can be installed from PyPI by running: 11 | 12 | .. code-block:: bash 13 | 14 | python3 -m pip install aiida-mlip 15 | 16 | To get all the latest changes, ``aiida-mlip`` can also be installed from GitHub: 17 | 18 | .. code-block:: bash 19 | 20 | python3 -m pip install git+https://github.com/stfc/aiida-mlip.git 21 | 22 | By default, no machine learnt interatomic potentials (MLIPs) will be installed with ``aiida-mlip``. 23 | However, ``aiida-mlip`` currently provides an ``extra``, allowing MACE to be installed: 24 | 25 | .. code-block:: bash 26 | 27 | python3 -m pip install aiida-mlip[mace] 28 | 29 | For additional MLIPs, it is recommended that the ``extra`` dependencies provided by ``janus-core`` are used. 30 | For example, to install CHGNet and SevenNet, run: 31 | 32 | .. code-block:: bash 33 | 34 | python3 -m pip install janus-core[chgnet,sevennet] 35 | 36 | Please refer to the ``janus-core`` `documentation `_ for further details. 37 | 38 | Once ``aiida-mlip`` and the desired MLIP calculators are installed, run:: 39 | 40 | verdi presto # better to set up a new profile 41 | verdi plugin list aiida.calculations # should now show your calculation plugins 42 | 43 | Then, use ``verdi code setup`` with the ``janus`` input plugin 44 | to set up an AiiDA code for aiida-mlip. The `aiida docs `_ go over how to create a code. 45 | 46 | 47 | 48 | .. note:: 49 | Configuring a message broker like RabbitMQ is optional, but highly recommended to avoid errors and enable `full functionality `_ of AiiDA. 50 | If you have not set up RabbitMQ, you will still be able to ``run`` processes (as shown in the `tutorial notebooks `_) but not be able to ``submit`` them. 51 | If a broker is detected, the ``verdi presto`` command can automatically configure a presto profile, including the computer, database, and broker. 52 | You’ll also need to set up a code for ``janus-core`` so it can be recognised by AiiDA. Note that PostgreSQL is not configured by default. 53 | Refer to the `AiiDA complete installation guide `_ for full setup details. 54 | 55 | 56 | Usage 57 | +++++ 58 | 59 | A quick demo of how to submit a calculation (these require a broker to be setup for daemon to start):: 60 | 61 | verdi daemon start # make sure the daemon is running 62 | cd examples/calculations 63 | verdi run submit_train.py # submit calculation 64 | verdi calculation list -a # check status of calculation 65 | 66 | If you have already set up your own aiida_mlip code using 67 | ``verdi code setup``, you may want to try the following command:: 68 | 69 | mlip-submit # uses aiida_mlip.cli 70 | 71 | Available calculations 72 | ++++++++++++++++++++++ 73 | 74 | These are the available calculations 75 | 76 | * Descriptors 77 | * GeomOpt 78 | * MD 79 | * Singlepoint 80 | * Train 81 | 82 | For more details on the calculations, please refer to the `calculations section `_. 83 | -------------------------------------------------------------------------------- /docs/source/user_guide/index.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | User guide 3 | ========== 4 | 5 | .. toctree:: 6 | :maxdepth: 3 7 | 8 | get_started 9 | tutorial 10 | data 11 | calculations 12 | training 13 | -------------------------------------------------------------------------------- /docs/source/user_guide/training.rst: -------------------------------------------------------------------------------- 1 | ================================ 2 | Training machine learning models 3 | ================================ 4 | 5 | The `Train` class represents a `CalcJob` object within the AiiDA framework, designed for training machine learning models. 6 | 7 | Usage 8 | ^^^^^ 9 | 10 | This calculation can be executed using either the `run` or `submit` AiiDA commands. 11 | Below is a usage example with some additional training parameters. These parameters must be AiiDA data types. 12 | 13 | .. code-block:: python 14 | 15 | TrainCalculation = CalculationFactory("mlip.train") 16 | submit(TrainCalculation, code=InstalledCode, mlip_config=JanusConfigfile, metadata=Dict({'options': {'output_filename': 'aiida-stdout.txt'}})) 17 | 18 | 19 | The parameters are provided in a config file. Tha mandatory parameters are: 20 | 21 | .. code-block:: yaml 22 | 23 | name: 'test' 24 | train_file: "./tests/calculations/structures/mlip_train.xyz" 25 | valid_file: "./tests/calculations/structures/mlip_valid.xyz" 26 | test_file: "./tests/calculations/structures/mlip_test.xyz" 27 | 28 | while the other parameters are optional. Here is an example (can be found in the tests folder) of a config file with more parameters: 29 | 30 | .. code-block:: yaml 31 | 32 | name: 'test' 33 | train_file: "./tests/calculations/structures/mlip_train.xyz" 34 | valid_file: "./tests/calculations/structures/mlip_valid.xyz" 35 | test_file: "./tests/calculations/structures/mlip_test.xyz" 36 | # Optional parameters: 37 | model: ScaleShiftMACE 38 | loss: 'universal' 39 | energy_weight: 1 40 | forces_weight: 10 41 | stress_weight: 100 42 | compute_stress: True 43 | energy_key: 'dft_energy' 44 | forces_key: 'dft_forces' 45 | stress_key: 'dft_stress' 46 | eval_interval: 2 47 | error_table: PerAtomRMSE 48 | # main model params 49 | interaction_first: "RealAgnosticResidualInteractionBlock" 50 | interaction: "RealAgnosticResidualInteractionBlock" 51 | num_interactions: 2 52 | correlation: 3 53 | max_ell: 3 54 | r_max: 4.0 55 | max_L: 0 56 | num_channels: 16 57 | num_radial_basis: 6 58 | MLP_irreps: '16x0e' 59 | # end model params 60 | scaling: 'rms_forces_scaling' 61 | lr: 0.005 62 | weight_decay: 1e-8 63 | ema: True 64 | ema_decay: 0.995 65 | scheduler_patience: 5 66 | batch_size: 4 67 | valid_batch_size: 4 68 | max_num_epochs: 1 69 | patience: 50 70 | amsgrad: True 71 | default_dtype: float32 72 | device: cpu 73 | distributed: False 74 | clip_grad: 100 75 | keep_checkpoints: False 76 | keep_isolated_atoms: True 77 | save_cpu: True 78 | 79 | It is also possible to fine-tune models using the same type of `Calcjob`. 80 | In that case some additional parameters must be used: foundation_model and fine_tune. 81 | 82 | 83 | .. code-block:: python 84 | 85 | inputs = { 86 | code=InstalledCode, 87 | mlip_config=JanusConfigfile, 88 | metadata=Dict({'options': {'output_filename': 'aiida-stdout.txt'}}), 89 | fine_tune=Bool(True), 90 | foundation_model=ModelData 91 | } 92 | 93 | TrainCalculation = CalculationFactory("mlip.train") 94 | submit(TrainCalculation,inputs) 95 | 96 | A model to fine-tune has to be provided as an input, either as a `ModelData` type (in which case it has to be a model file), or in the config file at the keyword `foundation_model`. 97 | If the keyword `fine_tune` is True but no model is given either way, it will return an error. 98 | 99 | .. note:: 100 | 101 | The keyword 'model' and 'foundation_model' refer to two different things. 102 | 'foundation_model' is the path to the model to fine-tune (or a shortcut like 'small', etc). 103 | 'model' refers to the model-type (see `MACE `_ documentation) 104 | 105 | 106 | Submission 107 | ^^^^^^^^^^ 108 | 109 | To facilitate the submission process and prepare inputs as AiiDA data types, an example script is provided. 110 | This script can be used as is or by changing, in the file, the path to the config file, then submitted to `verdi` as shown 111 | 112 | .. code-block:: python 113 | 114 | verdi run submit_train.py 115 | -------------------------------------------------------------------------------- /examples/calculations/submit_descriptors.py: -------------------------------------------------------------------------------- 1 | """Example code for submitting descriptors calculation.""" 2 | 3 | from __future__ import annotations 4 | 5 | from aiida.common import NotExistent 6 | from aiida.engine import run_get_node 7 | from aiida.orm import Bool, Str, load_code 8 | from aiida.plugins import CalculationFactory 9 | import click 10 | 11 | from aiida_mlip.helpers.help_load import load_model, load_structure 12 | 13 | 14 | def descriptors(params: dict) -> None: 15 | """ 16 | Prepare inputs and run a descriptors calculation. 17 | 18 | Parameters 19 | ---------- 20 | params : dict 21 | A dictionary containing the input parameters for the calculations 22 | 23 | Returns 24 | ------- 25 | None 26 | """ 27 | structure = load_structure(params["struct"]) 28 | 29 | # Select model to use 30 | model = load_model(params["model"], params["arch"]) 31 | 32 | # Select calculation to use 33 | DescriptorsCalc = CalculationFactory("mlip.descriptors") 34 | 35 | # Define inputs 36 | inputs = { 37 | "metadata": {"options": {"resources": {"num_machines": 1}}}, 38 | "code": params["code"], 39 | "arch": Str(params["arch"]), 40 | "struct": structure, 41 | "model": model, 42 | "precision": Str(params["precision"]), 43 | "device": Str(params["device"]), 44 | "invariants_only": Bool(params["invariants_only"]), 45 | "calc_per_element": Bool(params["calc_per_element"]), 46 | "calc_per_atom": Bool(params["calc_per_atom"]), 47 | } 48 | 49 | # Run calculation 50 | result, node = run_get_node(DescriptorsCalc, **inputs) 51 | print(f"Printing results from calculation: {result}") 52 | print(f"Printing node of calculation: {node}") 53 | 54 | 55 | # Arguments and options to give to the cli when running the script 56 | @click.command("cli") 57 | @click.argument("codelabel", type=str) 58 | @click.option( 59 | "--struct", 60 | default=None, 61 | type=str, 62 | help="Specify the structure (aiida node or path to a structure file)", 63 | ) 64 | @click.option( 65 | "--model", 66 | default=None, 67 | type=str, 68 | help="Specify path or URI of the model to use", 69 | ) 70 | @click.option( 71 | "--arch", 72 | default="mace_mp", 73 | type=str, 74 | help="MLIP architecture to use for calculations.", 75 | ) 76 | @click.option( 77 | "--device", default="cpu", type=str, help="Device to run calculations on." 78 | ) 79 | @click.option( 80 | "--precision", default="float64", type=str, help="Chosen level of precision." 81 | ) 82 | @click.option( 83 | "--invariants-only", 84 | default=False, 85 | type=bool, 86 | help="Only calculate invariant descriptors.", 87 | ) 88 | @click.option( 89 | "--calc-per-element", 90 | default=False, 91 | type=bool, 92 | help="Calculate mean descriptors for each element.", 93 | ) 94 | @click.option( 95 | "--calc-per-atom", 96 | default=False, 97 | type=bool, 98 | help="Calculate descriptors for each atom.", 99 | ) 100 | def cli( 101 | codelabel, 102 | struct, 103 | model, 104 | arch, 105 | device, 106 | precision, 107 | invariants_only, 108 | calc_per_element, 109 | calc_per_atom, 110 | ) -> None: 111 | """Click interface.""" 112 | try: 113 | code = load_code(codelabel) 114 | except NotExistent as exc: 115 | print(f"The code '{codelabel}' does not exist.") 116 | raise SystemExit from exc 117 | 118 | params = { 119 | "code": code, 120 | "struct": struct, 121 | "model": model, 122 | "arch": arch, 123 | "device": device, 124 | "precision": precision, 125 | "invariants_only": invariants_only, 126 | "calc_per_element": calc_per_element, 127 | "calc_per_atom": calc_per_atom, 128 | } 129 | 130 | # Submit descriptors 131 | descriptors(params) 132 | 133 | 134 | if __name__ == "__main__": 135 | cli() 136 | -------------------------------------------------------------------------------- /examples/calculations/submit_geomopt.py: -------------------------------------------------------------------------------- 1 | """Example code for submitting geometry optimisation calculation.""" 2 | 3 | from __future__ import annotations 4 | 5 | from aiida.common import NotExistent 6 | from aiida.engine import run_get_node 7 | from aiida.orm import Bool, Float, Int, Str, load_code 8 | from aiida.plugins import CalculationFactory 9 | import click 10 | 11 | from aiida_mlip.helpers.help_load import load_model, load_structure 12 | 13 | 14 | def geomopt(params: dict) -> None: 15 | """ 16 | Prepare inputs and run a geometry optimisation calculation. 17 | 18 | Parameters 19 | ---------- 20 | params : dict 21 | A dictionary containing the input parameters for the calculations 22 | 23 | Returns 24 | ------- 25 | None 26 | """ 27 | structure = load_structure(params["struct"]) 28 | 29 | # Select model to use 30 | model = load_model(params["model"], params["arch"]) 31 | 32 | # Select calculation to use 33 | GeomoptCalc = CalculationFactory("mlip.opt") 34 | 35 | # Define inputs 36 | inputs = { 37 | "metadata": {"options": {"resources": {"num_machines": 1}}}, 38 | "code": params["code"], 39 | "arch": Str(params["arch"]), 40 | "struct": structure, 41 | "model": model, 42 | "precision": Str(params["precision"]), 43 | "device": Str(params["device"]), 44 | "fmax": Float(params["fmax"]), 45 | "opt_cell_lengths": Bool(params["opt_cell_lengths"]), 46 | "opt_cell_fully": Bool(params["opt_cell_fully"]), 47 | # "opt_kwargs": Dict({"restart": "rest.pkl"}), 48 | "steps": Int(params["steps"]), 49 | } 50 | 51 | # Run calculation 52 | result, node = run_get_node(GeomoptCalc, **inputs) 53 | print(f"Printing results from calculation: {result}") 54 | print(f"Printing node of calculation: {node}") 55 | 56 | 57 | # Arguments and options to give to the cli when running the script 58 | @click.command("cli") 59 | @click.argument("codelabel", type=str) 60 | @click.option( 61 | "--struct", 62 | default=None, 63 | type=str, 64 | help="Specify the structure (aiida node or path to a structure file)", 65 | ) 66 | @click.option( 67 | "--model", 68 | default=None, 69 | type=str, 70 | help="Specify path or URI of the model to use", 71 | ) 72 | @click.option( 73 | "--arch", 74 | default="mace_mp", 75 | type=str, 76 | help="MLIP architecture to use for calculations.", 77 | ) 78 | @click.option( 79 | "--device", default="cpu", type=str, help="Device to run calculations on." 80 | ) 81 | @click.option( 82 | "--precision", default="float64", type=str, help="Chosen level of precision." 83 | ) 84 | @click.option("--fmax", default=0.1, type=float, help="Maximum force for convergence.") 85 | @click.option( 86 | "--opt_cell_lengths", 87 | default=False, 88 | type=bool, 89 | help="Optimise cell vectors, as well as atomic positions.", 90 | ) 91 | @click.option( 92 | "--opt_cell_fully", 93 | default=False, 94 | type=bool, 95 | help="Fully optimise the cell vectors, angles, and atomic positions.", 96 | ) 97 | @click.option( 98 | "--steps", default=1000, type=int, help="Maximum number of optimisation steps." 99 | ) 100 | def cli( 101 | codelabel, 102 | struct, 103 | model, 104 | arch, 105 | device, 106 | precision, 107 | fmax, 108 | opt_cell_lengths, 109 | opt_cell_fully, 110 | steps, 111 | ) -> None: 112 | """Click interface.""" 113 | try: 114 | code = load_code(codelabel) 115 | except NotExistent as exc: 116 | print(f"The code '{codelabel}' does not exist.") 117 | raise SystemExit from exc 118 | 119 | params = { 120 | "code": code, 121 | "struct": struct, 122 | "model": model, 123 | "arch": arch, 124 | "device": device, 125 | "precision": precision, 126 | "fmax": fmax, 127 | "opt_cell_lengths": opt_cell_lengths, 128 | "opt_cell_fully": opt_cell_fully, 129 | "steps": steps, 130 | } 131 | 132 | # Submit single point 133 | geomopt(params) 134 | 135 | 136 | if __name__ == "__main__": 137 | cli() 138 | -------------------------------------------------------------------------------- /examples/calculations/submit_md.py: -------------------------------------------------------------------------------- 1 | """Example code for submitting a molecular dynamics simulation.""" 2 | 3 | from __future__ import annotations 4 | 5 | import ast 6 | 7 | from aiida.common import NotExistent 8 | from aiida.engine import run_get_node 9 | from aiida.orm import Dict, Str, load_code 10 | from aiida.plugins import CalculationFactory 11 | import click 12 | 13 | from aiida_mlip.helpers.help_load import load_model, load_structure 14 | 15 | 16 | def md(params: dict) -> None: 17 | """ 18 | Prepare inputs and run a molecular dynamics simulation. 19 | 20 | Parameters 21 | ---------- 22 | params : dict 23 | A dictionary containing the input parameters for the calculations 24 | 25 | Returns 26 | ------- 27 | None 28 | """ 29 | structure = load_structure(params["struct"]) 30 | 31 | # Select model to use 32 | model = load_model(params["model"], params["arch"]) 33 | 34 | # Select calculation to use 35 | MDCalc = CalculationFactory("mlip.md") 36 | 37 | # Define inputs 38 | inputs = { 39 | "metadata": {"options": {"resources": {"num_machines": 1}}}, 40 | "code": params["code"], 41 | "arch": Str(params["arch"]), 42 | "struct": structure, 43 | "model": model, 44 | "precision": Str(params["precision"]), 45 | "device": Str(params["device"]), 46 | "ensemble": Str(params["ensemble"]), 47 | "md_kwargs": Dict(params["md_dict"]), 48 | } 49 | 50 | # Run calculation 51 | result, node = run_get_node(MDCalc, **inputs) 52 | print(f"Printing results from calculation: {result}") 53 | print(f"Printing node of calculation: {node}") 54 | 55 | 56 | # Arguments and options to give to the cli when running the script 57 | @click.command("cli") 58 | @click.argument("codelabel", type=str) 59 | @click.option( 60 | "--struct", 61 | default=None, 62 | type=str, 63 | help="Specify the structure (aiida node or path to a structure file)", 64 | ) 65 | @click.option( 66 | "--model", 67 | default=None, 68 | type=str, 69 | help="Specify path or URI of the model to use", 70 | ) 71 | @click.option( 72 | "--arch", 73 | default="mace_mp", 74 | type=str, 75 | help="MLIP architecture to use for calculations.", 76 | ) 77 | @click.option( 78 | "--device", default="cpu", type=str, help="Device to run calculations on." 79 | ) 80 | @click.option( 81 | "--precision", default="float64", type=str, help="Chosen level of precision." 82 | ) 83 | @click.option( 84 | "--ensemble", default="nve", type=str, help="Name of thermodynamic ensemble." 85 | ) 86 | @click.option( 87 | "--md_dict_str", 88 | default="{}", 89 | type=str, 90 | help="String containing a dictionary with other md parameters", 91 | ) 92 | def cli( 93 | codelabel, struct, model, arch, device, precision, ensemble, md_dict_str 94 | ) -> None: 95 | """Click interface.""" 96 | md_dict = ast.literal_eval(md_dict_str) 97 | try: 98 | code = load_code(codelabel) 99 | except NotExistent as exc: 100 | print(f"The code '{codelabel}' does not exist.") 101 | raise SystemExit from exc 102 | 103 | params = { 104 | "code": code, 105 | "struct": struct, 106 | "model": model, 107 | "arch": arch, 108 | "device": device, 109 | "precision": precision, 110 | "ensemble": ensemble, 111 | "md_dict": md_dict, 112 | } 113 | 114 | # Submit MD 115 | md(params) 116 | 117 | 118 | if __name__ == "__main__": 119 | cli() 120 | -------------------------------------------------------------------------------- /examples/calculations/submit_md_using_config.py: -------------------------------------------------------------------------------- 1 | """Example code for submitting single point calculation.""" 2 | 3 | from __future__ import annotations 4 | 5 | from aiida.engine import run_get_node 6 | from aiida.orm import load_code 7 | from aiida.plugins import CalculationFactory 8 | 9 | from aiida_mlip.data.config import JanusConfigfile 10 | from aiida_mlip.helpers.help_load import load_structure 11 | 12 | # And the required inputs for aiida 13 | metadata = {"options": {"resources": {"num_machines": 1}}} 14 | code = load_code("janus@localhost") 15 | 16 | # This structure will overwrite the one in the config file if present 17 | structure = load_structure() 18 | 19 | # All the other paramenters we want them from the config file 20 | # We want to pass it as a AiiDA data type for the provenance 21 | config = JanusConfigfile( 22 | "/home/federica/aiida-mlip/tests/calculations/configs/config_janus_md.yaml" 23 | ) 24 | 25 | # Define calculation to run 26 | MDCalculation = CalculationFactory("mlip.md") 27 | 28 | # Run calculation 29 | result, node = run_get_node( 30 | MDCalculation, code=code, struct=structure, metadata=metadata, config=config 31 | ) 32 | print(f"Printing results from calculation: {result}") 33 | print(f"Printing node of calculation: {node}") 34 | -------------------------------------------------------------------------------- /examples/calculations/submit_singlepoint.py: -------------------------------------------------------------------------------- 1 | """Example code for submitting single point calculation.""" 2 | 3 | from __future__ import annotations 4 | 5 | from aiida.common import NotExistent 6 | from aiida.engine import run_get_node 7 | from aiida.orm import Str, load_code 8 | from aiida.plugins import CalculationFactory 9 | import click 10 | 11 | from aiida_mlip.helpers.help_load import load_model, load_structure 12 | 13 | 14 | def singlepoint(params: dict) -> None: 15 | """ 16 | Prepare inputs and run a single point calculation. 17 | 18 | Parameters 19 | ---------- 20 | params : dict 21 | A dictionary containing the input parameters for the calculations 22 | 23 | Returns 24 | ------- 25 | None 26 | """ 27 | structure = load_structure(params["struct"]) 28 | 29 | # Select model to use 30 | model = load_model(params["model"], params["arch"]) 31 | 32 | # Select calculation to use 33 | SinglepointCalc = CalculationFactory("mlip.sp") 34 | 35 | # Define inputs 36 | inputs = { 37 | "metadata": {"options": {"resources": {"num_machines": 1}}}, 38 | "code": params["code"], 39 | "arch": Str(params["arch"]), 40 | "struct": structure, 41 | "model": model, 42 | "precision": Str(params["precision"]), 43 | "device": Str(params["device"]), 44 | } 45 | 46 | # Run calculation 47 | result, node = run_get_node(SinglepointCalc, **inputs) 48 | print(f"Printing results from calculation: {result}") 49 | print(f"Printing node of calculation: {node}") 50 | 51 | 52 | # Arguments and options to give to the cli when running the script 53 | @click.command("cli") 54 | @click.argument("codelabel", type=str) 55 | @click.option( 56 | "--struct", 57 | default=None, 58 | type=str, 59 | help="Specify the structure (aiida node or path to a structure file)", 60 | ) 61 | @click.option( 62 | "--model", 63 | default=None, 64 | type=str, 65 | help="Specify path or URI of the model to use", 66 | ) 67 | @click.option( 68 | "--arch", 69 | default="mace_mp", 70 | type=str, 71 | help="MLIP architecture to use for calculations.", 72 | ) 73 | @click.option( 74 | "--device", default="cpu", type=str, help="Device to run calculations on." 75 | ) 76 | @click.option( 77 | "--precision", default="float64", type=str, help="Chosen level of precision." 78 | ) 79 | def cli(codelabel, struct, model, arch, device, precision) -> None: 80 | """Click interface.""" 81 | try: 82 | code = load_code(codelabel) 83 | except NotExistent as exc: 84 | print(f"The code '{codelabel}' does not exist.") 85 | raise SystemExit from exc 86 | 87 | params = { 88 | "code": code, 89 | "struct": struct, 90 | "model": model, 91 | "arch": arch, 92 | "device": device, 93 | "precision": precision, 94 | } 95 | 96 | # Submit single point 97 | singlepoint(params) 98 | 99 | 100 | if __name__ == "__main__": 101 | cli() 102 | -------------------------------------------------------------------------------- /examples/calculations/submit_train.py: -------------------------------------------------------------------------------- 1 | """Example code for submitting training calculation.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | 7 | from aiida.engine import run_get_node 8 | from aiida.orm import load_code 9 | from aiida.plugins import CalculationFactory 10 | 11 | from aiida_mlip.data.config import JanusConfigfile 12 | 13 | # Add the required inputs for aiida 14 | metadata = {"options": {"resources": {"num_machines": 1}}} 15 | code = load_code("janus@localhost") 16 | 17 | # All the other parameters we want them from the config file 18 | # We want to pass it as a AiiDA data type for the provenance 19 | mlip_config = JanusConfigfile( 20 | Path("~/aiida-mlip/tests/calculations/configs/mlip_train.yml") 21 | .expanduser() 22 | .resolve() 23 | ) 24 | 25 | # Define calculation to run 26 | TrainCalc = CalculationFactory("mlip.train") 27 | 28 | # Run calculation 29 | result, node = run_get_node( 30 | TrainCalc, 31 | code=code, 32 | metadata=metadata, 33 | mlip_config=mlip_config, 34 | ) 35 | print(f"Printing results from calculation: {result}") 36 | print(f"Printing node of calculation: {node}") 37 | -------------------------------------------------------------------------------- /examples/calculations/submit_using_config.py: -------------------------------------------------------------------------------- 1 | """Example code for submitting single point calculation.""" 2 | 3 | from __future__ import annotations 4 | 5 | from aiida.engine import run_get_node 6 | from aiida.orm import load_code 7 | from aiida.plugins import CalculationFactory 8 | 9 | from aiida_mlip.data.config import JanusConfigfile 10 | from aiida_mlip.helpers.help_load import load_structure 11 | 12 | # Add the required inputs for aiida 13 | metadata = {"options": {"resources": {"num_machines": 1}}} 14 | code = load_code("janus@localhost") 15 | 16 | # This structure will overwrite the one in the config file if present 17 | structure = load_structure("../tests/calculations/structures/NaCl.cif") 18 | 19 | # All the other paramenters we want them from the config file 20 | # We want to pass it as a AiiDA data type for the provenance 21 | config = JanusConfigfile("../tests/calculations/configs/config_janus.yaml") 22 | 23 | # Define calculation to run 24 | SinglepointCalc = CalculationFactory("mlip.sp") 25 | 26 | # Run calculation 27 | result, node = run_get_node( 28 | SinglepointCalc, 29 | code=code, 30 | struct=structure, 31 | metadata=metadata, 32 | config=config, 33 | ) 34 | print(f"Printing results from calculation: {result}") 35 | print(f"Printing node of calculation: {node}") 36 | -------------------------------------------------------------------------------- /examples/tutorials/config_computer.yml: -------------------------------------------------------------------------------- 1 | label: localhost 2 | description: localhost computer 3 | hostname: localhost 4 | transport: core.local 5 | scheduler: core.slurm 6 | shebang: '#!/bin/bash' 7 | work_dir: /home/work_dir 8 | mpirun_command: srun 9 | mpiprocs_per_machine: 32 10 | -------------------------------------------------------------------------------- /examples/tutorials/config_profile.yml: -------------------------------------------------------------------------------- 1 | profile: tutorial 2 | email: email@email.space 3 | first_name: tutorial 4 | last_name: tutorial 5 | institution: dl 6 | db_engine: postgresql_psycopg2 7 | db_backend: core.psql_dos 8 | db_host: localhost 9 | db_name: aiida-mlip-tutorial 10 | db_username: default 11 | db_password: password 12 | db_port: 5432 13 | broker_protocol: amqp 14 | broker_username: guest 15 | broker_password: guest 16 | broker_host: localhost 17 | broker_port: 5672 18 | broker_virtual_host: "" 19 | repository: /home/tutorial 20 | -------------------------------------------------------------------------------- /examples/tutorials/config_sp.yaml: -------------------------------------------------------------------------------- 1 | arch: mace_mp 2 | calc-kwargs: 3 | calc_kwargs: 4 | dispersion: True 5 | properties: [energy] 6 | -------------------------------------------------------------------------------- /examples/tutorials/setup-janus-code.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "tags": [] 7 | }, 8 | "source": [ 9 | "# Setting up janus code" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": { 15 | "id": "_VQOcUDx26EH" 16 | }, 17 | "source": [ 18 | "To run anything, we need to make sure a profile, computer and code are set up in AiiDA.\n", 19 | "The command \"verdi\" represents the command line interface of AiiDA and it is used to set it up and interact with the database.\n", 20 | "If you run verdi status you can see what the status of the database is" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": { 27 | "id": "FySKV9vIH2EZ" 28 | }, 29 | "outputs": [], 30 | "source": [ 31 | "! verdi status" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "Then we can see that there is already a default profile setup" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "! verdi profile list" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "We can also see that the localhost computer is set up" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "metadata": { 61 | "id": "swIorHevLooP" 62 | }, 63 | "outputs": [], 64 | "source": [ 65 | "! verdi computer list" 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "metadata": { 71 | "id": "fT5TWbmOPuJe" 72 | }, 73 | "source": [ 74 | "Then we need to check if the code is already set up or not.\n" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "! verdi code list" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "metadata": {}, 89 | "source": [ 90 | "If it's not, let's set it up. Otherwise we are good to go.\n", 91 | "\n", 92 | "The code we are using is janus, that manages the mlips submission. \n", 93 | "We need to know the executable path" 94 | ] 95 | }, 96 | { 97 | "cell_type": "code", 98 | "execution_count": null, 99 | "metadata": { 100 | "id": "fT5TWbmOPuJe" 101 | }, 102 | "outputs": [], 103 | "source": [ 104 | "! which janus" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "As you can see in the config file for the code that we are going to write, we use this path." 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "metadata": { 118 | "id": "fT5TWbmOPuJe" 119 | }, 120 | "outputs": [], 121 | "source": [ 122 | "%%writefile config_code.yml\n", 123 | "append_text: ''\n", 124 | "computer: localhost\n", 125 | "default_calc_job_plugin: mlip.sp\n", 126 | "description: janus-core\n", 127 | "filepath_executable: /opt/conda/bin/janus\n", 128 | "label: janus\n", 129 | "prepend_text: ''\n", 130 | "use_double_quotes: 'False'" 131 | ] 132 | }, 133 | { 134 | "cell_type": "markdown", 135 | "metadata": {}, 136 | "source": [ 137 | "Let's create the code. We create it as a `InstalledCode` instance of the AiiDA data type." 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "metadata": { 144 | "id": "fT5TWbmOPuJe" 145 | }, 146 | "outputs": [], 147 | "source": [ 148 | "! verdi code create core.code.installed --config config_code.yml" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": null, 154 | "metadata": {}, 155 | "outputs": [], 156 | "source": [ 157 | "! verdi code list" 158 | ] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": {}, 163 | "source": [ 164 | "The code is saved in the database with a PK (1 in my case but could be different for other people). \n", 165 | "We can see the details for the code." 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": null, 171 | "metadata": {}, 172 | "outputs": [], 173 | "source": [ 174 | "! verdi code show 1" 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": null, 180 | "metadata": {}, 181 | "outputs": [], 182 | "source": [] 183 | } 184 | ], 185 | "metadata": { 186 | "colab": { 187 | "private_outputs": true, 188 | "provenance": [] 189 | }, 190 | "kernelspec": { 191 | "display_name": "Python 3 (ipykernel)", 192 | "language": "python", 193 | "name": "python3" 194 | }, 195 | "language_info": { 196 | "codemirror_mode": { 197 | "name": "ipython", 198 | "version": 3 199 | }, 200 | "file_extension": ".py", 201 | "mimetype": "text/x-python", 202 | "name": "python", 203 | "nbconvert_exporter": "python", 204 | "pygments_lexer": "ipython3", 205 | "version": "3.9.13" 206 | } 207 | }, 208 | "nbformat": 4, 209 | "nbformat_minor": 4 210 | } 211 | -------------------------------------------------------------------------------- /examples/tutorials/structures/qmof-00d09fe.cif: -------------------------------------------------------------------------------- 1 | # generated using pymatgen 2 | data_ZnH3C9NO5F2 3 | _symmetry_space_group_name_H-M P-1 4 | _cell_length_a 9.03441890 5 | _cell_length_b 9.68049828 6 | _cell_length_c 11.02010857 7 | _cell_angle_alpha 82.69493753 8 | _cell_angle_beta 79.46180697 9 | _cell_angle_gamma 83.12868301 10 | _symmetry_Int_Tables_number 2 11 | _chemical_formula_structural ZnH3C9NO5F2 12 | _chemical_formula_sum 'Zn2 H6 C18 N2 O10 F4' 13 | _cell_volume 935.24386623 14 | _cell_formula_units_Z 2 15 | loop_ 16 | _symmetry_equiv_pos_site_id 17 | _symmetry_equiv_pos_as_xyz 18 | 1 'x, y, z' 19 | 2 '-x, -y, -z' 20 | loop_ 21 | _atom_site_type_symbol 22 | _atom_site_label 23 | _atom_site_symmetry_multiplicity 24 | _atom_site_fract_x 25 | _atom_site_fract_y 26 | _atom_site_fract_z 27 | _atom_site_occupancy 28 | Zn Zn0 2 0.48916614 0.64435457 0.48500493 1 29 | H H1 2 0.01466694 0.67382881 0.67901556 1 30 | H H2 2 0.33261448 0.85455002 0.64927852 1 31 | H H3 2 0.37439730 0.69940762 0.88046311 1 32 | C C4 2 0.03541303 0.52618730 0.53995789 1 33 | C C5 2 0.04811691 0.40489236 0.35052850 1 34 | C C6 2 0.20472894 0.52135262 0.52588088 1 35 | C C7 2 0.40623881 0.91985789 0.58267005 1 36 | C C8 2 0.41672859 0.05968135 0.59487017 1 37 | C C9 2 0.42975706 0.61133246 0.93249275 1 38 | C C10 2 0.43045244 0.61492865 0.05767978 1 39 | C C11 2 0.49474369 0.49663311 0.73653261 1 40 | C C12 2 0.49916721 0.49590206 0.87157654 1 41 | N N13 2 0.49173020 0.86170916 0.48588866 1 42 | O O14 2 0.17238917 0.43243931 0.29406209 1 43 | O O15 2 0.25429625 0.64061357 0.51454108 1 44 | O O16 2 0.27925496 0.40211032 0.53439848 1 45 | O O17 2 0.47287786 0.61602493 0.67491944 1 46 | O O18 2 0.49003589 0.62105734 0.30619696 1 47 | F F19 2 0.33321190 0.11943679 0.69042423 1 48 | F F20 2 0.35752066 0.72922546 0.11006942 1 49 | -------------------------------------------------------------------------------- /examples/tutorials/structures/qmof-013ec70.cif: -------------------------------------------------------------------------------- 1 | # generated using pymatgen 2 | data_Ag2H10C7(N2O)4 3 | _symmetry_space_group_name_H-M R-3m 4 | _cell_length_a 22.17318243 5 | _cell_length_b 22.17318243 6 | _cell_length_c 16.98033986 7 | _cell_angle_alpha 90.00000000 8 | _cell_angle_beta 90.00000000 9 | _cell_angle_gamma 120.00000000 10 | _symmetry_Int_Tables_number 166 11 | _chemical_formula_structural Ag2H10C7(N2O)4 12 | _chemical_formula_sum 'Ag36 H180 C126 N144 O72' 13 | _cell_volume 7229.91298867 14 | _cell_formula_units_Z 18 15 | loop_ 16 | _symmetry_equiv_pos_site_id 17 | _symmetry_equiv_pos_as_xyz 18 | 1 'x, y, z' 19 | 2 '-x, -y, -z' 20 | 3 '-y, x-y, z' 21 | 4 'y, -x+y, -z' 22 | 5 '-x+y, -x, z' 23 | 6 'x-y, x, -z' 24 | 7 'y, x, -z' 25 | 8 '-y, -x, z' 26 | 9 'x-y, -y, -z' 27 | 10 '-x+y, y, z' 28 | 11 '-x, -x+y, -z' 29 | 12 'x, x-y, z' 30 | 13 'x+2/3, y+1/3, z+1/3' 31 | 14 '-x+2/3, -y+1/3, -z+1/3' 32 | 15 '-y+2/3, x-y+1/3, z+1/3' 33 | 16 'y+2/3, -x+y+1/3, -z+1/3' 34 | 17 '-x+y+2/3, -x+1/3, z+1/3' 35 | 18 'x-y+2/3, x+1/3, -z+1/3' 36 | 19 'y+2/3, x+1/3, -z+1/3' 37 | 20 '-y+2/3, -x+1/3, z+1/3' 38 | 21 'x-y+2/3, -y+1/3, -z+1/3' 39 | 22 '-x+y+2/3, y+1/3, z+1/3' 40 | 23 '-x+2/3, -x+y+1/3, -z+1/3' 41 | 24 'x+2/3, x-y+1/3, z+1/3' 42 | 25 'x+1/3, y+2/3, z+2/3' 43 | 26 '-x+1/3, -y+2/3, -z+2/3' 44 | 27 '-y+1/3, x-y+2/3, z+2/3' 45 | 28 'y+1/3, -x+y+2/3, -z+2/3' 46 | 29 '-x+y+1/3, -x+2/3, z+2/3' 47 | 30 'x-y+1/3, x+2/3, -z+2/3' 48 | 31 'y+1/3, x+2/3, -z+2/3' 49 | 32 '-y+1/3, -x+2/3, z+2/3' 50 | 33 'x-y+1/3, -y+2/3, -z+2/3' 51 | 34 '-x+y+1/3, y+2/3, z+2/3' 52 | 35 '-x+1/3, -x+y+2/3, -z+2/3' 53 | 36 'x+1/3, x-y+2/3, z+2/3' 54 | loop_ 55 | _atom_site_type_symbol 56 | _atom_site_label 57 | _atom_site_symmetry_multiplicity 58 | _atom_site_fract_x 59 | _atom_site_fract_y 60 | _atom_site_fract_z 61 | _atom_site_occupancy 62 | Ag Ag0 18 0.00000000 0.27445857 0.00000000 1 63 | Ag Ag1 18 0.08728960 0.17457920 0.70166607 1 64 | H H2 36 0.01418654 0.27341841 0.16579261 1 65 | H H3 36 0.01501259 0.23964930 0.74656777 1 66 | H H4 36 0.02419070 0.47248413 0.72758088 1 67 | H H5 36 0.04288913 0.16647004 0.35473543 1 68 | H H6 36 0.04427283 0.19326520 0.48730781 1 69 | C C7 36 0.01790143 0.24671397 0.57323858 1 70 | C C8 36 0.07295761 0.25083530 0.84561270 1 71 | C C9 18 0.00000000 0.20110330 0.50000000 1 72 | C C10 18 0.03495096 0.06990192 0.35425991 1 73 | C C11 18 0.05669019 0.52834510 0.62564342 1 74 | N N12 36 0.01772652 0.24464481 0.80689903 1 75 | N N13 36 0.06455478 0.47839111 0.58923582 1 76 | N N14 18 0.03599172 0.07198344 0.64599098 1 77 | N N15 18 0.03997134 0.51998567 0.70204205 1 78 | N N16 18 0.06958479 0.13916957 0.35391415 1 79 | N N17 18 0.09446872 0.54723436 0.47242210 1 80 | O O18 36 0.00195857 0.21653014 0.63998922 1 81 | O O19 36 0.02098024 0.40485653 0.22979562 1 82 | -------------------------------------------------------------------------------- /examples/tutorials/structures/qmof-ffeef76.cif: -------------------------------------------------------------------------------- 1 | # generated using pymatgen 2 | data_CdH8C14(SN3)2 3 | _symmetry_space_group_name_H-M P2_1/c 4 | _cell_length_a 5.67308267 5 | _cell_length_b 17.73132708 6 | _cell_length_c 8.52672766 7 | _cell_angle_alpha 90.00000000 8 | _cell_angle_beta 105.90132259 9 | _cell_angle_gamma 90.00000000 10 | _symmetry_Int_Tables_number 14 11 | _chemical_formula_structural CdH8C14(SN3)2 12 | _chemical_formula_sum 'Cd2 H16 C28 S4 N12' 13 | _cell_volume 824.89402978 14 | _cell_formula_units_Z 2 15 | loop_ 16 | _symmetry_equiv_pos_site_id 17 | _symmetry_equiv_pos_as_xyz 18 | 1 'x, y, z' 19 | 2 '-x, -y, -z' 20 | 3 '-x, y+1/2, -z+1/2' 21 | 4 'x, -y+1/2, z+1/2' 22 | loop_ 23 | _atom_site_type_symbol 24 | _atom_site_label 25 | _atom_site_symmetry_multiplicity 26 | _atom_site_fract_x 27 | _atom_site_fract_y 28 | _atom_site_fract_z 29 | _atom_site_occupancy 30 | Cd Cd0 2 0.50000000 0.00000000 0.00000000 1 31 | H H1 4 0.03568349 0.50590130 0.78230705 1 32 | H H2 4 0.26635355 0.58699068 0.01378965 1 33 | H H3 4 0.38723875 0.22505230 0.56570024 1 34 | H H4 4 0.41184158 0.63834904 0.70692879 1 35 | C C5 4 0.04159043 0.09166054 0.08583880 1 36 | C C6 4 0.04421711 0.16190551 0.50693828 1 37 | C C7 4 0.05141384 0.05463648 0.67923797 1 38 | C C8 4 0.07664597 0.59972079 0.94940786 1 39 | C C9 4 0.07922862 0.70978205 0.12415909 1 40 | C C10 4 0.28769667 0.17673494 0.59375278 1 41 | C C11 4 0.39998718 0.12857129 0.72071885 1 42 | S S12 4 0.32793603 0.11234352 0.17038636 1 43 | N N13 4 0.16664689 0.57693451 0.47678391 1 44 | N N14 4 0.18042178 0.74831766 0.23255087 1 45 | N N15 4 0.28531082 0.06842238 0.76226298 1 46 | -------------------------------------------------------------------------------- /examples/workflows/submit_ht_workgraph.py: -------------------------------------------------------------------------------- 1 | """Example submission for high throughput workgraph.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | 7 | from aiida.orm import load_code 8 | from aiida.plugins import CalculationFactory 9 | 10 | from aiida_mlip.data.model import ModelData 11 | from aiida_mlip.workflows.ht_workgraph import get_ht_workgraph 12 | 13 | SinglepointCalc = CalculationFactory("mlip.sp") 14 | 15 | inputs = { 16 | "model": ModelData.from_local( 17 | "./tests/calculations/configs/test.model", 18 | architecture="mace_mp", 19 | ), 20 | "metadata": {"options": {"resources": {"num_machines": 1}}}, 21 | "code": load_code("janus@localhost"), 22 | } 23 | 24 | wg = get_ht_workgraph( 25 | calc=SinglepointCalc, 26 | folder=Path("./tests/workflows/structures/"), 27 | calc_inputs=inputs, 28 | final_struct_key="xyz_output", 29 | max_number_jobs=10, 30 | ) 31 | 32 | wg.submit() 33 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "aiida-mlip" 3 | version = "0.2.1" 4 | description = "Machine learning interatomic potentials AiiDA plugin" 5 | authors = [ 6 | { name = "Federica Zanca" }, 7 | { name = "Elliott Kasoar" }, 8 | { name = "Jacob Wilkins" }, 9 | { name = "Alin M. Elena" }, 10 | ] 11 | requires-python = ">=3.10" 12 | classifiers = [ 13 | "Programming Language :: Python", 14 | "Intended Audience :: Science/Research", 15 | "License :: OSI Approved :: BSD License", 16 | "Natural Language :: English", 17 | "Development Status :: 5 - Production/Stable", 18 | "Framework :: AiiDA" 19 | ] 20 | readme = "README.md" 21 | keywords = ["aiida", "plugin"] 22 | 23 | dependencies = [ 24 | "aiida-core<3,>=2.6.3", 25 | "ase<4.0,>=3.24", 26 | "voluptuous<1,>=0.15.2", 27 | "janus-core<0.8,>=0.7.5", 28 | "aiida-workgraph==0.4.10", 29 | ] 30 | 31 | [project.optional-dependencies] 32 | mace = [ 33 | "janus-core[mace]" 34 | ] 35 | 36 | [project.urls] 37 | repository = "https://github.com/stfc/aiida-mlip/" 38 | documentation = "https://stfc.github.io/aiida-mlip/" 39 | source = "https://github.com/aiidateam/aiida-mlip" 40 | 41 | [dependency-groups] 42 | dev = [ 43 | "coverage[toml]<8.0.0,>=7.4.1", 44 | "pgtest<2.0.0,>=1.3.2", 45 | "pytest<9.0,>=8.0", 46 | "pytest-cov<5.0.0,>=4.1.0", 47 | "tox-uv<2.0,>=1.25.0", 48 | "wheel<1.0,>=0.42", 49 | ] 50 | 51 | docs = [ 52 | "furo<2025.0.0,>=2024.1.29", 53 | "markupsafe<2.1", 54 | "numpydoc<2.0.0,>=1.6.0", 55 | "sphinx<8.0.0,>=7.2.6", 56 | "sphinxcontrib-contentui<1.0.0,>=0.2.5", 57 | "sphinxcontrib-details-directive<1.0,>=0.1", 58 | "sphinx-copybutton<1.0.0,>=0.5.2", 59 | ] 60 | 61 | pre-commit = [ 62 | "pre-commit<4.0.0,>=3.6.0", 63 | "ruff<1.0.0,>=0.9.2", 64 | ] 65 | 66 | [build-system] 67 | requires = ["pdm-backend"] 68 | build-backend = "pdm.backend" 69 | 70 | [project.entry-points."aiida.data"] 71 | "mlip.modeldata" = "aiida_mlip.data.model:ModelData" 72 | "mlip.config" = "aiida_mlip.data.config:JanusConfigfile" 73 | 74 | [project.entry-points."aiida.calculations"] 75 | "mlip.sp" = "aiida_mlip.calculations.singlepoint:Singlepoint" 76 | "mlip.opt" = "aiida_mlip.calculations.geomopt:GeomOpt" 77 | "mlip.md" = "aiida_mlip.calculations.md:MD" 78 | "mlip.train" = "aiida_mlip.calculations.train:Train" 79 | "mlip.descriptors" = "aiida_mlip.calculations.descriptors:Descriptors" 80 | 81 | [project.entry-points."aiida.parsers"] 82 | "mlip.sp_parser" = "aiida_mlip.parsers.sp_parser:SPParser" 83 | "mlip.opt_parser" = "aiida_mlip.parsers.opt_parser:GeomOptParser" 84 | "mlip.md_parser" = "aiida_mlip.parsers.md_parser:MDParser" 85 | "mlip.train_parser" = "aiida_mlip.parsers.train_parser:TrainParser" 86 | "mlip.descriptors_parser" = "aiida_mlip.parsers.descriptors_parser:DescriptorsParser" 87 | 88 | [tool.pytest.ini_options] 89 | # Configuration for [pytest](https://docs.pytest.org) 90 | python_files = "test_*.py example_*.py" 91 | addopts = '--cov-report xml' 92 | filterwarnings = [ 93 | "ignore::DeprecationWarning:aiida:", 94 | "ignore:Creating AiiDA configuration folder:", 95 | "ignore::DeprecationWarning:plumpy:", 96 | "ignore::DeprecationWarning:yaml:", 97 | ] 98 | pythonpath = ["."] 99 | 100 | [tool.coverage.run] 101 | # Configuration of [coverage.py](https://coverage.readthedocs.io) 102 | # reporting which lines of your plugin are covered by tests 103 | source=["aiida_mlip"] 104 | 105 | [tool.numpydoc_validation] 106 | # report on all checks, except the below 107 | checks = [ 108 | "all", 109 | "EX01", 110 | "SA01", 111 | "ES01", 112 | ] 113 | # Don't report on objects that match any of these regex 114 | exclude = [ 115 | ".__weakref__$", 116 | ".__repr__$", 117 | ] 118 | 119 | [tool.ruff] 120 | extend-exclude = ["conf.py", "*.ipynb"] 121 | target-version = "py310" 122 | 123 | [tool.ruff.lint] 124 | # Ignore complexity, non-lowercase 125 | ignore = ["C901", "N806"] 126 | select = [ 127 | # flake8-bugbear 128 | "B", 129 | # pylint 130 | "C", "R", 131 | # pydocstyle 132 | "D", 133 | # pycodestyle 134 | "E", "W", 135 | # Pyflakes 136 | "F", 137 | # pyupgrade 138 | "I", 139 | # pep8-naming 140 | "N", 141 | # isort 142 | "UP", 143 | ] 144 | 145 | [tool.ruff.lint.isort] 146 | force-sort-within-sections = true 147 | required-imports = ["from __future__ import annotations"] 148 | 149 | [tool.ruff.lint.pydocstyle] 150 | convention = "numpy" 151 | 152 | [tool.ruff.lint.pylint] 153 | max-args = 10 154 | 155 | [tool.ruff.lint.pyupgrade] 156 | keep-runtime-typing = false 157 | 158 | [tool.uv] 159 | default-groups = [ 160 | "dev", 161 | "docs", 162 | "pre-commit", 163 | ] 164 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for the plugin. 2 | 3 | Includes both tests written in unittest style (test_cli.py) and tests written 4 | in pytest style (test_calculations.py). 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | import os 10 | 11 | TEST_DIR = os.path.dirname(os.path.realpath(__file__)) 12 | -------------------------------------------------------------------------------- /tests/calculations/configs/config_janus.yaml: -------------------------------------------------------------------------------- 1 | properties: 2 | - "energy" 3 | arch: mace_mp 4 | -------------------------------------------------------------------------------- /tests/calculations/configs/config_janus_md.yaml: -------------------------------------------------------------------------------- 1 | properties: 2 | - "energy" 3 | arch: mace_mp 4 | struct: "NaCl.cif" 5 | ensemble: "nvt" 6 | temp: 200 7 | minimize-kwargs: 8 | filter-kwargs: 9 | hydrostatic-strain: True 10 | -------------------------------------------------------------------------------- /tests/calculations/configs/config_noarch.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | - "energy" 3 | model: "small" 4 | -------------------------------------------------------------------------------- /tests/calculations/configs/config_nomodel.yml: -------------------------------------------------------------------------------- 1 | properties: 2 | - "energy" 3 | arch: "mace_mp" 4 | -------------------------------------------------------------------------------- /tests/calculations/configs/mlip_train.yml: -------------------------------------------------------------------------------- 1 | name: 'test' 2 | train_file: "./tests/calculations/structures/mlip_train.xyz" 3 | valid_file: "./tests/calculations/structures/mlip_valid.xyz" 4 | test_file: "./tests/calculations/structures/mlip_test.xyz" 5 | # Optional parameters: 6 | model: ScaleShiftMACE 7 | loss: 'universal' 8 | energy_weight: 1 9 | forces_weight: 10 10 | stress_weight: 100 11 | compute_stress: True 12 | energy_key: 'dft_energy' 13 | forces_key: 'dft_forces' 14 | stress_key: 'dft_stress' 15 | eval_interval: 2 16 | error_table: PerAtomRMSE 17 | # main model params 18 | interaction_first: "RealAgnosticResidualInteractionBlock" 19 | interaction: "RealAgnosticResidualInteractionBlock" 20 | num_interactions: 2 21 | correlation: 3 22 | max_ell: 3 23 | r_max: 4.0 24 | max_L: 0 25 | num_channels: 16 26 | num_radial_basis: 6 27 | MLP_irreps: '16x0e' 28 | # end model params 29 | scaling: 'rms_forces_scaling' 30 | lr: 0.005 31 | weight_decay: 1e-8 32 | ema: True 33 | ema_decay: 0.995 34 | scheduler_patience: 5 35 | batch_size: 4 36 | valid_batch_size: 4 37 | max_num_epochs: 1 38 | patience: 50 39 | amsgrad: True 40 | default_dtype: float32 41 | device: cpu 42 | distributed: False 43 | clip_grad: 100 44 | keep_checkpoints: False 45 | keep_isolated_atoms: True 46 | save_cpu: True 47 | -------------------------------------------------------------------------------- /tests/calculations/configs/test.model: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stfc/aiida-mlip/e358063de55985d345dbf56df3ea9963ce22e8de/tests/calculations/configs/test.model -------------------------------------------------------------------------------- /tests/calculations/configs/test_compiled.model: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stfc/aiida-mlip/e358063de55985d345dbf56df3ea9963ce22e8de/tests/calculations/configs/test_compiled.model -------------------------------------------------------------------------------- /tests/calculations/structures/NaCl.cif: -------------------------------------------------------------------------------- 1 | data_image0 2 | _chemical_formula_structural NaClNaClNaClNaCl 3 | _chemical_formula_sum "Na4 Cl4" 4 | _cell_length_a 5.64 5 | _cell_length_b 5.64 6 | _cell_length_c 5.64 7 | _cell_angle_alpha 90.0 8 | _cell_angle_beta 90.0 9 | _cell_angle_gamma 90.0 10 | 11 | _space_group_name_H-M_alt "P 1" 12 | _space_group_IT_number 1 13 | 14 | loop_ 15 | _space_group_symop_operation_xyz 16 | 'x, y, z' 17 | 18 | loop_ 19 | _atom_site_type_symbol 20 | _atom_site_label 21 | _atom_site_symmetry_multiplicity 22 | _atom_site_fract_x 23 | _atom_site_fract_y 24 | _atom_site_fract_z 25 | _atom_site_occupancy 26 | Na Na1 1.0 0.0 0.0 0.0 1.0000 27 | Cl Cl1 1.0 0.5 0.0 0.0 1.0000 28 | Na Na2 1.0 0.0 0.5 0.5 1.0000 29 | Cl Cl2 1.0 0.5 0.5 0.5 1.0000 30 | Na Na3 1.0 0.5 0.0 0.5 1.0000 31 | Cl Cl3 1.0 0.0 0.0 0.5 1.0000 32 | Na Na4 1.0 0.5 0.5 0.0 1.0000 33 | Cl Cl4 1.0 0.0 0.5 0.0 1.0000 34 | -------------------------------------------------------------------------------- /tests/calculations/structures/traj.xyz: -------------------------------------------------------------------------------- 1 | 2 2 | Lattice="3.9810111780802626 0.0 0.0 1.9905055890401309 3.4476568129673235 0.0 1.9905055890401309 1.1492189376557744 3.2504820155375933" Properties=species:S:1:pos:R:3:momenta:R:3:masses:R:1:forces:R:3 time_fs=0.0 step=0 density=2.1751659192965453 energy=-6.757520383972924 stress="-0.005816546985101064 -4.4613856654998464e-18 1.1302736597257334e-17 -4.4613856654998464e-18 -0.005816546985101082 -3.246661380676002e-17 1.1302736597257334e-17 -3.246661380676002e-17 -0.005816546985101042" free_energy=-6.757520383972924 pbc="T T T" 3 | Na 0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 22.98976928 0.00000000 0.00000001 0.00000000 4 | Cl 3.98101118 2.29843788 1.62524101 0.00000000 0.00000000 0.00000000 35.45000000 -0.00000000 -0.00000001 -0.00000000 5 | 2 6 | Lattice="3.9810111780802626 0.0 0.0 1.9905055890401309 3.4476568129673235 0.0 1.9905055890401309 1.1492189376557744 3.2504820155375933" Properties=species:S:1:pos:R:3:momenta:R:3:masses:R:1:forces:R:3 time_fs=0.0 step=0 density=2.1751659192965453 energy=-6.757520383972924 stress="-0.005816546985101041 1.2639959343079134e-18 -2.400313821922842e-17 1.2639959343079134e-18 -0.005816546985101073 -3.0649443338796896e-18 -2.400313821922842e-17 -3.0649443338796896e-18 -0.005816546985101047" free_energy=-6.757520383972924 pbc="T T T" 7 | Na 0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 22.98976928 0.00000000 0.00000001 0.00000000 8 | Cl 3.98101118 2.29843788 1.62524101 -0.00000000 -0.00000000 -0.00000000 35.45000000 -0.00000000 -0.00000001 -0.00000000 9 | 2 10 | Lattice="3.9810111780802626 0.0 0.0 1.9905055890401309 3.4476568129673235 0.0 1.9905055890401309 1.1492189376557744 3.2504820155375933" Properties=species:S:1:pos:R:3:momenta:R:3:masses:R:1:forces:R:3 time_fs=0.0 step=0 density=2.1751659192965453 energy=-6.757520383972924 stress="-0.005816546985101053 -1.0405609336862454e-17 2.5727819486459403e-18 -1.0405609336862454e-17 -0.00581654698510107 1.6615267631424653e-17 2.5727819486459403e-18 1.6615267631424653e-17 -0.0058165469851010195" free_energy=-6.757520383972924 pbc="T T T" 11 | Na 0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 22.98976928 0.00000000 0.00000001 0.00000000 12 | Cl 3.98101118 2.29843788 1.62524101 -0.00000000 -0.00000000 -0.00000000 35.45000000 -0.00000000 -0.00000001 -0.00000000 13 | 2 14 | Lattice="3.9810111780802626 0.0 0.0 1.9905055890401309 3.4476568129673235 0.0 1.9905055890401309 1.1492189376557744 3.2504820155375933" Properties=species:S:1:pos:R:3:momenta:R:3:masses:R:1:forces:R:3 time_fs=0.0 step=0 density=2.1751659192965453 energy=-6.757520383972924 stress="-0.005816546985101031 -1.4695329582264732e-17 4.2293731749837194e-18 -1.4695329582264732e-17 -0.0058165469851010265 -1.729640558874249e-17 4.2293731749837194e-18 -1.729640558874249e-17 -0.00581654698510103" free_energy=-6.757520383972924 pbc="T T T" 15 | Na 0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 22.98976928 0.00000000 0.00000001 0.00000000 16 | Cl 3.98101118 2.29843788 1.62524101 -0.00000000 -0.00000000 -0.00000000 35.45000000 -0.00000000 -0.00000001 -0.00000000 17 | 2 18 | Lattice="3.9810111780802626 0.0 0.0 1.9905055890401309 3.4476568129673235 0.0 1.9905055890401309 1.1492189376557744 3.2504820155375933" Properties=species:S:1:pos:R:3:momenta:R:3:masses:R:1:forces:R:3 time_fs=0.0 step=0 density=2.1751659192965453 energy=-6.757520383972925 stress="-0.005816546985101071 -1.2203943313608905e-17 -1.5843114591637865e-17 -1.2203943313608905e-17 -0.005816546985101065 7.610946636720814e-18 -1.5843114591637865e-17 7.610946636720814e-18 -0.0058165469851010395" free_energy=-6.757520383972925 pbc="T T T" 19 | Na 0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 0.00000000 22.98976928 0.00000000 0.00000001 0.00000000 20 | Cl 3.98101118 2.29843788 1.62524101 -0.00000000 -0.00000000 -0.00000000 35.45000000 -0.00000000 -0.00000001 -0.00000000 21 | -------------------------------------------------------------------------------- /tests/calculations/test_descriptors.py: -------------------------------------------------------------------------------- 1 | """Tests for descriptors calculation.""" 2 | 3 | from __future__ import annotations 4 | 5 | import subprocess 6 | 7 | from aiida.common import datastructures 8 | from aiida.engine import run 9 | from aiida.orm import Bool, Str, StructureData 10 | from aiida.plugins import CalculationFactory 11 | from ase.build import bulk 12 | import pytest 13 | 14 | from aiida_mlip.data.model import ModelData 15 | 16 | 17 | def test_descriptors(fixture_sandbox, generate_calc_job, janus_code, model_folder): 18 | """Test generating descriptors calculation job.""" 19 | entry_point_name = "mlip.descriptors" 20 | model_file = model_folder / "mace_mp_small.model" 21 | inputs = { 22 | "metadata": {"options": {"resources": {"num_machines": 1}}}, 23 | "code": janus_code, 24 | "arch": Str("mace"), 25 | "precision": Str("float64"), 26 | "struct": StructureData(ase=bulk("NaCl", "rocksalt", 5.63)), 27 | "model": ModelData.from_local(model_file, architecture="mace"), 28 | "device": Str("cpu"), 29 | "invariants_only": Bool(True), 30 | } 31 | 32 | calc_info = generate_calc_job(fixture_sandbox, entry_point_name, inputs) 33 | 34 | cmdline_params = [ 35 | "descriptors", 36 | "--arch", 37 | "mace", 38 | "--struct", 39 | "aiida.xyz", 40 | "--device", 41 | "cpu", 42 | "--log", 43 | "aiida.log", 44 | "--out", 45 | "aiida-results.xyz", 46 | "--calc-kwargs", 47 | "{'default_dtype': 'float64', 'model': 'mlff.model'}", 48 | "--invariants-only", 49 | ] 50 | 51 | retrieve_list = [ 52 | calc_info.uuid, 53 | "aiida.log", 54 | "aiida-results.xyz", 55 | "aiida-stdout.txt", 56 | ] 57 | 58 | # Check the attributes of the returned `CalcInfo` 59 | assert sorted(fixture_sandbox.get_content_list()) == sorted( 60 | ["aiida.xyz", "mlff.model"] 61 | ) 62 | assert isinstance(calc_info, datastructures.CalcInfo) 63 | assert isinstance(calc_info.codes_info[0], datastructures.CodeInfo) 64 | assert len(calc_info.codes_info[0].cmdline_params) == len(cmdline_params) 65 | assert sorted(map(str, calc_info.codes_info[0].cmdline_params)) == sorted( 66 | map(str, cmdline_params) 67 | ) 68 | assert sorted(calc_info.retrieve_list) == sorted(retrieve_list) 69 | 70 | 71 | def test_run_descriptors(model_folder, janus_code): 72 | """Test running descriptors calculation.""" 73 | model_file = model_folder / "mace_mp_small.model" 74 | inputs = { 75 | "metadata": {"options": {"resources": {"num_machines": 1}}}, 76 | "code": janus_code, 77 | "arch": Str("mace"), 78 | "precision": Str("float64"), 79 | "struct": StructureData(ase=bulk("NaCl", "rocksalt", 5.63)), 80 | "model": ModelData.from_local(model_file, architecture="mace"), 81 | "device": Str("cpu"), 82 | "invariants_only": Bool(False), 83 | "calc_per_element": Bool(True), 84 | "calc_per_atom": Bool(True), 85 | } 86 | 87 | DescriptorsCalc = CalculationFactory("mlip.descriptors") 88 | result = run(DescriptorsCalc, **inputs) 89 | 90 | assert "xyz_output" in result 91 | assert result["xyz_output"].filename == "aiida-results.xyz" 92 | 93 | assert "results_dict" in result 94 | obtained_res = result["results_dict"].get_dict() 95 | assert obtained_res["info"]["mace_descriptor"] == pytest.approx(-0.0056343183) 96 | assert obtained_res["info"]["mace_Cl_descriptor"] == pytest.approx(-0.0091900828) 97 | assert obtained_res["info"]["mace_Na_descriptor"] == pytest.approx(-0.0020785538) 98 | assert obtained_res["mace_descriptors"] == pytest.approx([-0.00207855, -0.00919008]) 99 | 100 | 101 | def test_example_descriptors(example_path, janus_code): 102 | """Test running descriptors calculation using the example file provided.""" 103 | example_file_path = example_path / "submit_descriptors.py" 104 | command = [ 105 | "verdi", 106 | "run", 107 | example_file_path, 108 | f"{janus_code.label}@{janus_code.computer.label}", 109 | ] 110 | 111 | # Execute the command 112 | result = subprocess.run(command, capture_output=True, text=True, check=False) 113 | assert result.stderr == "" 114 | assert result.returncode == 0 115 | assert "results from calculation:" in result.stdout 116 | assert "'results_dict': None: 14 | """Test high throughput singlepoint calculation.""" 15 | SinglepointCalc = CalculationFactory("mlip.sp") 16 | 17 | model_file = model_folder / "mace_mp_small.model" 18 | inputs = { 19 | "model": ModelData.from_local(model_file, architecture="mace"), 20 | "metadata": {"options": {"resources": {"num_machines": 1}}}, 21 | "code": janus_code, 22 | } 23 | 24 | wg = get_ht_workgraph( 25 | calc=SinglepointCalc, 26 | folder=workflow_structure_folder, 27 | calc_inputs=inputs, 28 | final_struct_key="xyz_output", 29 | ) 30 | 31 | wg.run() 32 | 33 | assert wg.state == "FINISHED" 34 | 35 | assert isinstance(wg.process.outputs.final_structures.H2O, SinglefileData) 36 | assert isinstance(wg.process.outputs.final_structures.methane, SinglefileData) 37 | 38 | 39 | def test_ht_invalid_path(janus_code, workflow_invalid_folder, model_folder) -> None: 40 | """Test invalid path for high throughput calculation.""" 41 | SinglepointCalc = CalculationFactory("mlip.sp") 42 | 43 | model_file = model_folder / "mace_mp_small.model" 44 | inputs = { 45 | "model": ModelData.from_local(model_file, architecture="mace"), 46 | "metadata": {"options": {"resources": {"num_machines": 1}}}, 47 | "code": janus_code, 48 | } 49 | 50 | wg = get_ht_workgraph( 51 | calc=SinglepointCalc, 52 | folder=workflow_invalid_folder, 53 | calc_inputs=inputs, 54 | final_struct_key="xyz_output", 55 | ) 56 | 57 | with pytest.raises(FileNotFoundError): 58 | wg.run() 59 | 60 | 61 | def test_ht_geomopt(janus_code, workflow_structure_folder, model_folder) -> None: 62 | """Test high throughput geometry optimisation.""" 63 | GeomoptCalc = CalculationFactory("mlip.opt") 64 | 65 | model_file = model_folder / "mace_mp_small.model" 66 | inputs = { 67 | "model": ModelData.from_local(model_file, architecture="mace"), 68 | "metadata": {"options": {"resources": {"num_machines": 1}}}, 69 | "code": janus_code, 70 | } 71 | 72 | wg = get_ht_workgraph( 73 | calc=GeomoptCalc, 74 | folder=workflow_structure_folder, 75 | calc_inputs=inputs, 76 | ) 77 | 78 | wg.run() 79 | 80 | assert wg.state == "FINISHED" 81 | 82 | assert isinstance(wg.process.outputs.final_structures.H2O, StructureData) 83 | assert isinstance(wg.process.outputs.final_structures.methane, StructureData) 84 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py311 3 | 4 | [testenv] 5 | usedevelop=True 6 | 7 | [testenv:py{310,311,312}] 8 | description = Run the test suite against Python versions 9 | runner = uv-venv-lock-runner 10 | with_dev = True 11 | extras = mace 12 | commands = pytest {posargs} --cov aiida_mlip --import-mode importlib 13 | 14 | [testenv:pre-commit] 15 | runner = uv-venv-lock-runner 16 | with_dev = True 17 | description = Run the pre-commit checks 18 | commands = pre-commit run {posargs} --all-files 19 | 20 | [testenv:docs] 21 | runner = uv-venv-lock-runner 22 | with_dev = True 23 | description = Build the documentation 24 | commands = sphinx-build -nW --keep-going -b html {posargs} docs/source docs/build/html 25 | --------------------------------------------------------------------------------