├── .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 | [](https://www.psdi.ac.uk/)
125 | [](https://adalovelacecentre.ac.uk/)
126 | [](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 |
--------------------------------------------------------------------------------