├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
├── THIRD-PARTY-LICENSES
├── alchemical_mace
├── __init__.py
├── calculator.py
├── model.py
├── optimize.py
└── utils.py
├── data
├── perovskite_dataset.json
└── structures
│ ├── AlN_hex.cif
│ ├── BiSBr.cif
│ ├── CeO2.cif
│ ├── CsPbI3_alpha.cif
│ ├── CsPbI3_delta.cif
│ ├── CsSnI3_alpha.cif
│ ├── CsSnI3_delta.cif
│ ├── Fe.cif
│ ├── GaN_hex.cif
│ └── NaCl.cif
├── notebooks
├── 1_solid_solution.ipynb
├── 2_compositional_optimization.ipynb
├── 3_disorder_energy.ipynb
├── 4_vacancy_analysis.ipynb
└── 5_perovskite_analysis.ipynb
├── pyproject.toml
├── requirements.txt
└── scripts
├── perovskite_alchemy.py
├── perovskite_frenkel_ladd.py
└── vacancy_frenkel_ladd.py
/.gitignore:
--------------------------------------------------------------------------------
1 | archived/
2 |
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | share/python-wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 | MANIFEST
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .nox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | *.py,cover
52 | .hypothesis/
53 | .pytest_cache/
54 | cover/
55 |
56 | # Translations
57 | *.mo
58 | *.pot
59 |
60 | # Django stuff:
61 | *.log
62 | local_settings.py
63 | db.sqlite3
64 | db.sqlite3-journal
65 |
66 | # Flask stuff:
67 | instance/
68 | .webassets-cache
69 |
70 | # Scrapy stuff:
71 | .scrapy
72 |
73 | # Sphinx documentation
74 | docs/_build/
75 |
76 | # PyBuilder
77 | .pybuilder/
78 | target/
79 |
80 | # Jupyter Notebook
81 | .ipynb_checkpoints
82 |
83 | # IPython
84 | profile_default/
85 | ipython_config.py
86 |
87 | # pyenv
88 | # For a library or package, you might want to ignore these files since the code is
89 | # intended to run in multiple environments; otherwise, check them in:
90 | # .python-version
91 |
92 | # pipenv
93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
96 | # install all needed dependencies.
97 | #Pipfile.lock
98 |
99 | # poetry
100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
101 | # This is especially recommended for binary packages to ensure reproducibility, and is more
102 | # commonly ignored for libraries.
103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
104 | #poetry.lock
105 |
106 | # pdm
107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
108 | #pdm.lock
109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
110 | # in version control.
111 | # https://pdm.fming.dev/#use-with-ide
112 | .pdm.toml
113 |
114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115 | __pypackages__/
116 |
117 | # Celery stuff
118 | celerybeat-schedule
119 | celerybeat.pid
120 |
121 | # SageMath parsed files
122 | *.sage.py
123 |
124 | # Environments
125 | .env
126 | .venv
127 | env/
128 | venv/
129 | ENV/
130 | env.bak/
131 | venv.bak/
132 |
133 | # Spyder project settings
134 | .spyderproject
135 | .spyproject
136 |
137 | # Rope project settings
138 | .ropeproject
139 |
140 | # mkdocs documentation
141 | /site
142 |
143 | # mypy
144 | .mypy_cache/
145 | .dmypy.json
146 | dmypy.json
147 |
148 | # Pyre type checker
149 | .pyre/
150 |
151 | # pytype static type analyzer
152 | .pytype/
153 |
154 | # Cython debug symbols
155 | cython_debug/
156 |
157 | # PyCharm
158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160 | # and can be added to the global gitignore or merged into this file. For a more nuclear
161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162 | #.idea/
163 |
164 | # Results directory
165 | results/
166 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.6.0
4 | hooks:
5 | - id: check-added-large-files
6 | - id: check-yaml
7 | - id: debug-statements
8 | - id: end-of-file-fixer
9 | - id: trailing-whitespace
10 | - repo: https://github.com/astral-sh/ruff-pre-commit
11 | rev: v0.4.2
12 | hooks:
13 | - id: ruff
14 | types_or: [ python, pyi, jupyter ]
15 | args: [ --fix ]
16 | - id: ruff-format
17 | types_or: [ python, pyi, jupyter ]
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Juno Nam and Rafael Gomez-Bombarelli
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Alchemical MLIP
2 | [](https://arxiv.org/abs/2404.10746)
3 | [](https://zenodo.org/doi/10.5281/zenodo.11081492)
4 | [](https://opensource.org/license/mit)
5 |
6 | This repository contains the code to modify machine learning interatomic potentials (MLIPs) to enable continuous and differentiable alchemical transformations.
7 | Currently, we provide the alchemical modification for the [MACE](https://github.com/ACEsuit/mace) model.
8 | The details of the method are described in the paper: [Interpolation and differentiation of alchemical degrees of freedom in machine learning interatomic potentials](https://arxiv.org/abs/2404.10746).
9 |
10 | ## Installation
11 | We tested the code with Python 3.10 and the packages in `requirements.txt`.
12 | For example, you can create a conda environment and install the required packages as follows (assuming CUDA 11.8):
13 | ```bash
14 | conda create -n alchemical-mlip python=3.10
15 | conda activate alchemical-mlip
16 | pip install torch==2.0.1 --index-url https://download.pytorch.org/whl/cu118
17 | pip install -r requirements.txt
18 | pip install -e .
19 | ```
20 |
21 | ## Static calculations
22 | We provide the jupyter notebooks for the lattice parameter calculations (Fig. 2 in the paper) and the compositional optimization (Fig. 3) in the `notebook` directory.
23 | ```
24 | notebook/
25 | ├── 1_solid_solution.ipynb
26 | └── 2_compositional_optimization.ipynb
27 | ```
28 |
29 | ## Free energy calculations
30 | We provide the scripts for the free energy calculations for the vacancy (Fig. 4) and perovskites (Fig. 5) in the `scripts` directory.
31 | ```
32 | scripts/
33 | ├── vacancy_frenkel_ladd.py
34 | ├── perovskite_frenkel_ladd.py
35 | └── perovskite_alchemy.py
36 | ```
37 |
38 | The arguments for the scripts are as follows:
39 | ```bash
40 | # Vacancy Frenkel-Ladd calculation
41 | python vacancy_frenkel_ladd.py \
42 | --structure-file data/structures/Fe.cif \
43 | --supercell 5 5 5 \
44 | --temperature 100 \
45 | --output-dir data/results/vacancy/Fe_5x5x5_100K/0
46 |
47 | # Perovskite Frenkel-Ladd calculation (alpha phase)
48 | python perovskite_frenkel_ladd.py \
49 | --structure-file data/structures/CsPbI3_alpha.cif \
50 | --supercell 6 6 6 \
51 | --temperature 400 \
52 | --output-dir data/results/perovskite/frenkel_ladd/CsPbI3_alpha_6x6x6_400K/0
53 |
54 | # Perovskite Frenkel-Ladd calculation (delta phase)
55 | python perovskite_frenkel_ladd.py \
56 | --structure-file data/structures/CsPbI3_delta.cif \
57 | --supercell 6 3 3 \
58 | --temperature 400 \
59 | --output-dir data/results/perovskite/frenkel_ladd/CsPbI3_delta_6x3x3_400K/0
60 |
61 | # Perovskite alchemy calculation (alpha phase)
62 | python -u perovskite_alchemy.py \
63 | --structure-file data/structures/CsPbI3_alpha.cif \
64 | --supercell 6 6 6 \
65 | --switch-pair Pb Sn \
66 | --temperature 400 \
67 | --output-dir data/results/perovskite/alchemy/CsPbI3_CsSnI3_alpha_400K/0
68 |
69 | # Perovskite alchemy calculation (delta phase)
70 | python -u perovskite_alchemy.py \
71 | --structure-file data/structures/CsPbI3_delta.cif \
72 | --supercell 6 3 3 \
73 | --switch-pair Pb Sn \
74 | --temperature 400 \
75 | --output-dir data/results/perovskite/alchemy/CsPbI3_CsSnI3_delta_400K/0
76 | ```
77 |
78 | The result files are large and not included in the repository.
79 | If you want to reproduce the results without running the calculations, the result files are uploaded in the [Zenodo repository](https://zenodo.org/doi/10.5281/zenodo.11081395).
80 | Please download the files and place them in the `data/results` directory.
81 |
82 | The post-processing scripts for the free energy calculations are provided in the `notebook` directory.
83 | ```
84 | notebook/
85 | ├── 3_vacancy_analysis.ipynb
86 | └── 4_perovskite_analysis.ipynb
87 | ```
88 |
89 | ## Citation
90 | ```
91 | @misc{nam2024interpolation,
92 | title={Interpolation and differentiation of alchemical degrees of freedom in machine learning interatomic potentials},
93 | author={Juno Nam and Rafael G{\'o}mez-Bombarelli},
94 | year={2024},
95 | eprint={2404.10746},
96 | archivePrefix={arXiv},
97 | primaryClass={cond-mat.mtrl-sci}
98 | }
99 | ```
100 |
--------------------------------------------------------------------------------
/THIRD-PARTY-LICENSES:
--------------------------------------------------------------------------------
1 | Code in alchemical_mace/{calculator,model}.py is adapted from
2 | https://github.com/ACEsuit/mace
3 |
4 | MIT License
5 |
6 | Copyright (c) 2022 ACEsuit/mace
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
11 |
12 | -------------------------------------------------------------------------------
13 |
14 | Code in alchemical_mace/utils.py is adapted from
15 | https://github.com/CederGroupHub/chgnet
16 |
17 | Crystal Hamiltonian Graph neural Network (CHGNet) Copyright (c) 2023, The Regents
18 | of the University of California, through Lawrence Berkeley National
19 | Laboratory (subject to receipt of any required approvals from the U.S.
20 | Dept. of Energy) and the University of California, Berkeley. All rights reserved.
21 |
22 | Redistribution and use in source and binary forms, with or without
23 | modification, are permitted provided that the following conditions are met:
24 |
25 | (1) Redistributions of source code must retain the above copyright notice,
26 | this list of conditions and the following disclaimer.
27 |
28 | (2) Redistributions in binary form must reproduce the above copyright
29 | notice, this list of conditions and the following disclaimer in the
30 | documentation and/or other materials provided with the distribution.
31 |
32 | (3) Neither the name of the University of California, Lawrence Berkeley
33 | National Laboratory, U.S. Dept. of Energy, University of California,
34 | Berkeley nor the names of its contributors may be used to endorse or
35 | promote products derived from this software without specific prior written
36 | permission.
37 |
38 |
39 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
40 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
41 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
42 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
43 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
44 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
45 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
46 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
47 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
48 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
49 | POSSIBILITY OF SUCH DAMAGE.
50 |
51 | You are under no obligation whatsoever to provide any bug fixes, patches,
52 | or upgrades to the features, functionality or performance of the source
53 | code ("Enhancements") to anyone; however, if you choose to make your
54 | Enhancements available either publicly, or directly to Lawrence Berkeley
55 | National Laboratory, without imposing a separate written license agreement
56 | for such Enhancements, then you hereby grant the following license: a
57 | non-exclusive, royalty-free perpetual license to install, use, modify,
58 | prepare derivative works, incorporate into other computer software,
59 | distribute, and sublicense such enhancements or derivative works thereof,
60 | in binary and source code form.
61 |
--------------------------------------------------------------------------------
/alchemical_mace/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/learningmatter-mit/alchemical-mlip/b556b850e9e89a4c2e1f5e9c27f03a3b1b393864/alchemical_mace/__init__.py
--------------------------------------------------------------------------------
/alchemical_mace/calculator.py:
--------------------------------------------------------------------------------
1 | from typing import Sequence, Tuple
2 |
3 | import ase
4 | import numpy as np
5 | import torch
6 | import torch.nn.functional as F
7 | from ase.calculators.calculator import Calculator, all_changes
8 | from ase.constraints import ExpCellFilter
9 | from ase.optimize import FIRE
10 | from ase.stress import full_3x3_to_voigt_6_stress
11 | from mace import data
12 | from mace.calculators import mace_mp
13 | from mace.tools import torch_geometric
14 |
15 | from alchemical_mace.model import (
16 | AlchemicalPair,
17 | AlchemyManager,
18 | alchemical_mace_mp,
19 | get_z_table_and_r_max,
20 | )
21 |
22 | ################################################################################
23 | # Alchemical MACE calculator
24 | ################################################################################
25 |
26 |
27 | class AlchemicalMACECalculator(Calculator):
28 | """
29 | Alchemical MACE calculator for ASE.
30 | """
31 |
32 | def __init__(
33 | self,
34 | atoms: ase.Atoms,
35 | alchemical_pairs: Sequence[Sequence[Tuple[int, int]]],
36 | alchemical_weights: Sequence[float],
37 | device: str = "cpu",
38 | model: str = "medium",
39 | ):
40 | """
41 | Initialize the Alchemical MACE calculator.
42 |
43 | Args:
44 | atoms (ase.Atoms): Atoms object.
45 | alchemical_pairs (Sequence[Sequence[Tuple[int, int]]]): List of
46 | alchemical pairs. Each pair is a tuple of the atom index and
47 | atomic number of an alchemical atom.
48 | alchemical_weights (Sequence[float]): List of alchemical weights.
49 | device (str): Device to run the calculations on.
50 | model (str): Model to use for the MACE calculator.
51 | """
52 | Calculator.__init__(self)
53 | self.results = {}
54 | self.implemented_properties = ["energy", "free_energy", "forces", "stress"]
55 |
56 | # Build the alchemical MACE model
57 | self.device = device
58 | self.model = alchemical_mace_mp(
59 | model=model, device=device, default_dtype="float32"
60 | )
61 | for param in self.model.parameters():
62 | param.requires_grad = False
63 |
64 | # Set AlchemyManager
65 | z_table, r_max = get_z_table_and_r_max(self.model)
66 | alchemical_weights = torch.tensor(alchemical_weights, dtype=torch.float32)
67 | self.alchemy_manager = AlchemyManager(
68 | atoms=atoms,
69 | alchemical_pairs=alchemical_pairs,
70 | alchemical_weights=alchemical_weights,
71 | z_table=z_table,
72 | r_max=r_max,
73 | ).to(self.device)
74 |
75 | # Disable alchemical weights gradients by default
76 | self.alchemy_manager.alchemical_weights.requires_grad = False
77 | self.calculate_alchemical_grad = False
78 |
79 | self.num_atoms = len(atoms)
80 |
81 | def set_alchemical_weights(self, alchemical_weights: Sequence[float]):
82 | alchemical_weights = torch.tensor(
83 | alchemical_weights,
84 | dtype=torch.float32,
85 | device=self.device,
86 | )
87 | self.alchemy_manager.alchemical_weights.data = alchemical_weights
88 |
89 | def get_alchemical_atomic_masses(self) -> np.ndarray:
90 | # Get atomic masses for alchemical atoms
91 | node_masses = ase.data.atomic_masses[self.alchemy_manager.atomic_numbers]
92 | weights = self.alchemy_manager.alchemical_weights.data
93 | weights = F.pad(weights, (1, 0), "constant", 1.0).cpu().numpy()
94 | node_weights = weights[self.alchemy_manager.weight_indices]
95 |
96 | # Scatter sum to get the atomic masses
97 | atom_masses = np.zeros(self.num_atoms, dtype=np.float32)
98 | np.add.at(
99 | atom_masses, self.alchemy_manager.atom_indices, node_masses * node_weights
100 | )
101 | return atom_masses
102 |
103 | # pylint: disable=dangerous-default-value
104 | def calculate(self, atoms=None, properties=None, system_changes=all_changes):
105 | # call to base-class to set atoms attribute
106 | Calculator.calculate(self, atoms)
107 |
108 | # prepare data
109 | tensor_kwargs = {"dtype": torch.float32, "device": self.device}
110 | positions = torch.tensor(atoms.get_positions(), **tensor_kwargs)
111 | cell = torch.tensor(atoms.get_cell().array, **tensor_kwargs)
112 | if self.calculate_alchemical_grad:
113 | self.alchemy_manager.alchemical_weights.requires_grad = True
114 | batch = self.alchemy_manager(positions, cell).to(self.device)
115 |
116 | # get outputs
117 | if self.calculate_alchemical_grad:
118 | out = self.model(batch, compute_stress=True, compute_alchemical_grad=True)
119 | (grad,) = torch.autograd.grad(
120 | outputs=[batch["node_weights"], batch["edge_weights"]],
121 | inputs=[self.alchemy_manager.alchemical_weights],
122 | grad_outputs=[out["node_grad"], out["edge_grad"]],
123 | retain_graph=False,
124 | create_graph=False,
125 | )
126 | grad = grad.cpu().numpy()
127 | self.alchemy_manager.alchemical_weights.requires_grad = False
128 | else:
129 | out = self.model(batch, retain_graph=False, compute_stress=True)
130 | grad = np.zeros(
131 | self.alchemy_manager.alchemical_weights.shape[0], dtype=np.float32
132 | )
133 |
134 | # store results
135 | self.results = {}
136 | self.results["energy"] = out["energy"][0].item()
137 | self.results["free_energy"] = self.results["energy"]
138 | self.results["forces"] = out["forces"].detach().cpu().numpy()
139 | self.results["stress"] = full_3x3_to_voigt_6_stress(
140 | out["stress"][0].detach().cpu().numpy()
141 | )
142 | self.results["alchemical_grad"] = grad
143 |
144 |
145 | class NVTMACECalculator(Calculator):
146 | def __init__(self, model: str = "medium", device: str = "cuda"):
147 | Calculator.__init__(self)
148 | self.results = {}
149 | self.implemented_properties = ["energy", "free_energy", "forces", "stress"]
150 | self.device = device
151 | self.model = mace_mp(
152 | model=model, device=device, default_dtype="float32"
153 | ).models[0]
154 | self.z_table, self.r_max = get_z_table_and_r_max(self.model)
155 | for param in self.model.parameters():
156 | param.requires_grad = False
157 |
158 | # pylint: disable=dangerous-default-value
159 | def calculate(self, atoms=None, properties=None, system_changes=all_changes):
160 | # call to base-class to set atoms attribute
161 | Calculator.calculate(self, atoms)
162 |
163 | # prepare data
164 | config = data.config_from_atoms(atoms)
165 | atomic_data = data.AtomicData.from_config(
166 | config, z_table=self.z_table, cutoff=self.r_max
167 | )
168 | data_loader = torch_geometric.dataloader.DataLoader(
169 | dataset=[atomic_data],
170 | batch_size=1,
171 | shuffle=False,
172 | drop_last=False,
173 | )
174 | batch = next(iter(data_loader)).to(self.device)
175 |
176 | out = self.model(batch, compute_stress=False)
177 | self.results = {}
178 | self.results["energy"] = out["energy"][0].item()
179 | self.results["free_energy"] = self.results["energy"]
180 | self.results["forces"] = out["forces"].detach().cpu().numpy()
181 |
182 |
183 | class FrenkelLaddCalculator(Calculator):
184 | """
185 | Frenkel-Ladd calculator for ASE.
186 | """
187 |
188 | def __init__(
189 | self,
190 | spring_constants: np.ndarray,
191 | initial_positions: np.ndarray,
192 | device: str,
193 | model: str = "medium",
194 | ):
195 | """
196 | Initialize the Frenkel-Ladd calculator.
197 |
198 | Args:
199 | spring_constants (np.ndarray): Spring constants for each atom.
200 | initial_positions (np.ndarray): Initial positions of the atoms.
201 | device (str): Device to run the calculations on.
202 | model (str): Model to use for the MACE calculator.
203 | """
204 | Calculator.__init__(self)
205 | self.results = {}
206 | self.implemented_properties = ["energy", "free_energy", "forces"]
207 | self.device = device
208 | self.model = mace_mp(
209 | model=model, device=device, default_dtype="float32"
210 | ).models[0]
211 | self.z_table, self.r_max = get_z_table_and_r_max(self.model)
212 | for param in self.model.parameters():
213 | param.requires_grad = False
214 |
215 | # Spring constants
216 | self.spring_constants = spring_constants
217 | self.initial_positions = initial_positions
218 |
219 | # Reversible scaling factor
220 | self.weights = [1.0, 0.0]
221 | self.compute_mace = True
222 |
223 | def set_weights(self, lambda_value: float):
224 | self.weights = [1.0 - lambda_value, lambda_value]
225 |
226 | # pylint: disable=dangerous-default-value
227 | def calculate(self, atoms=None, properties=None, system_changes=all_changes):
228 | # call to base-class to set atoms attribute
229 | Calculator.calculate(self, atoms)
230 |
231 | # Get MACE results if needed
232 | if self.compute_mace:
233 | config = data.config_from_atoms(atoms)
234 | atomic_data = data.AtomicData.from_config(
235 | config, z_table=self.z_table, cutoff=self.r_max
236 | )
237 | data_loader = torch_geometric.dataloader.DataLoader(
238 | dataset=[atomic_data],
239 | batch_size=1,
240 | shuffle=False,
241 | drop_last=False,
242 | )
243 | batch = next(iter(data_loader)).to(self.device)
244 | out = self.model(batch, compute_stress=False) # Frenkel-Ladd is NVT
245 | mace_energy = out["energy"][0].item()
246 | mace_forces = out["forces"].detach().cpu().numpy()
247 | else:
248 | mace_energy = 0.0
249 | mace_forces = np.zeros((len(atoms), 3), dtype=np.float32)
250 |
251 | # Get spring energy and forces
252 | displacement = atoms.get_positions() - self.initial_positions
253 | spring_energy = 0.5 * np.sum(
254 | self.spring_constants * np.sum(displacement**2, axis=1)
255 | )
256 | spring_forces = -self.spring_constants[:, None] * displacement
257 |
258 | # Combine energies and forces
259 | total_energy = self.weights[0] * spring_energy + self.weights[1] * mace_energy
260 | total_forces = self.weights[0] * spring_forces + self.weights[1] * mace_forces
261 | if self.compute_mace:
262 | energy_diff = mace_energy - spring_energy
263 | else:
264 | energy_diff = 0.0
265 |
266 | self.results = {}
267 | self.results["energy"] = total_energy
268 | self.results["free_energy"] = total_energy
269 | self.results["forces"] = total_forces
270 | self.results["energy_diff"] = energy_diff
271 |
272 |
273 | class DefectFrenkelLaddCalculator(Calculator):
274 | """
275 | Frenkel-Ladd calculator for ASE, for a crystal with a defect.
276 | """
277 |
278 | def __init__(
279 | self,
280 | atoms: ase.Atoms,
281 | spring_constant: float,
282 | defect_index: int,
283 | device: str = "cpu",
284 | model: str = "medium",
285 | ):
286 | """
287 | Initialize the Frenkel-Ladd calculator.
288 |
289 | Args:
290 | atoms (ase.Atoms): Atoms object.
291 | spring_constant (float): Spring constant for the defect atom.
292 | defect_index (int): Index of the defect atom.
293 | device (str): Device to run the calculations on.
294 | model (str): Model to use for the MACE calculator.
295 | """
296 | Calculator.__init__(self)
297 | self.results = {}
298 | self.implemented_properties = ["energy", "free_energy", "forces", "stress"]
299 |
300 | # Build the alchemical MACE model
301 | self.device = device
302 | self.model = alchemical_mace_mp(
303 | model=model, device=device, default_dtype="float32"
304 | )
305 | for param in self.model.parameters():
306 | param.requires_grad = False
307 |
308 | # Set AlchemyManager
309 | z_table, r_max = get_z_table_and_r_max(self.model)
310 | alchemical_weights = torch.tensor([1.0], dtype=torch.float32)
311 | atomic_number = atoms.get_atomic_numbers()[defect_index]
312 | alchemical_pairs = [[AlchemicalPair(defect_index, atomic_number)]]
313 | self.alchemy_manager = AlchemyManager(
314 | atoms=atoms,
315 | alchemical_pairs=alchemical_pairs,
316 | alchemical_weights=alchemical_weights,
317 | z_table=z_table,
318 | r_max=r_max,
319 | ).to(self.device)
320 |
321 | # Disable alchemical weights gradients by default
322 | self.alchemy_manager.alchemical_weights.requires_grad = False
323 | self.calculate_alchemical_grad = False
324 |
325 | self.num_atoms = len(atoms)
326 |
327 | # Switching
328 | self.defect_index = defect_index
329 | self.spring_constant = spring_constant
330 |
331 | def set_alchemical_weight(self, alchemical_weight: float):
332 | # Set alchemical weights
333 | alchemical_weights = torch.tensor(
334 | [1.0 - alchemical_weight], # initial = original atoms = 1 - 0
335 | dtype=torch.float32,
336 | device=self.device,
337 | )
338 | self.alchemy_manager.alchemical_weights.data = alchemical_weights
339 |
340 | # pylint: disable=dangerous-default-value
341 | def calculate(self, atoms=None, properties=None, system_changes=all_changes):
342 | # call to base-class to set atoms attribute
343 | Calculator.calculate(self, atoms)
344 |
345 | # prepare data
346 | tensor_kwargs = {"dtype": torch.float32, "device": self.device}
347 | positions = torch.tensor(atoms.get_positions(), **tensor_kwargs)
348 | cell = torch.tensor(atoms.get_cell().array, **tensor_kwargs)
349 | if self.calculate_alchemical_grad:
350 | self.alchemy_manager.alchemical_weights.requires_grad = True
351 | batch = self.alchemy_manager(positions, cell).to(self.device)
352 |
353 | # get outputs
354 | if self.calculate_alchemical_grad:
355 | out = self.model(batch, retain_graph=True, compute_stress=True)
356 | out["energy"].backward()
357 | grad = self.alchemy_manager.alchemical_weights.grad.item()
358 | self.alchemy_manager.alchemical_weights.grad.zero_()
359 | self.alchemy_manager.alchemical_weights.requires_grad = False
360 | else:
361 | out = self.model(batch, retain_graph=False, compute_stress=True)
362 | grad = 0.0
363 | mace_energy = out["energy"][0].item()
364 | mace_forces = out["forces"].detach().cpu().numpy()
365 | mace_stress = out["stress"][0].detach().cpu().numpy()
366 |
367 | # Get spring energy and forces
368 | cell_center = np.array([0.5, 0.5, 0.5]) @ atoms.get_cell().array
369 | displacement = atoms.get_positions()[self.defect_index] - cell_center
370 | spring_energy = 0.5 * self.spring_constant * np.sum(displacement**2)
371 | spring_forces = -self.spring_constant * displacement
372 |
373 | # Combine energies and forces
374 | # Note: weight here is 1 - lambda, and we're not weighting the mace
375 | # energy because it's already weighted by the alchemical weight
376 | weight = self.alchemy_manager.alchemical_weights.item()
377 | total_energy = mace_energy + (1 - weight) * spring_energy
378 | total_forces = mace_forces
379 | total_forces[self.defect_index] += (1 - weight) * spring_forces
380 | if self.calculate_alchemical_grad:
381 | # H(lambda) = E(1 - lambda) + lambda * spring_energy
382 | # dH/d(lambda) = -dE/d(1 - lambda) + spring_energy
383 | grad = -grad + spring_energy
384 |
385 | # store results
386 | self.results = {}
387 | self.results["energy"] = total_energy
388 | self.results["free_energy"] = total_energy
389 | self.results["forces"] = total_forces
390 | self.results["stress"] = full_3x3_to_voigt_6_stress(mace_stress)
391 | self.results["alchemical_grad"] = grad
392 |
393 |
394 | def get_alchemical_optimized_cellpar(
395 | atoms: ase.Atoms,
396 | alchemical_pairs: Sequence[Sequence[Tuple[int, int]]],
397 | alchemical_weights: Sequence[float],
398 | model: str = "medium",
399 | device: str = "cpu",
400 | **kwargs,
401 | ):
402 | """
403 | Optimize the cell parameters of a crystal with alchemical atoms using the
404 | Alchemical MACE calculator.
405 |
406 | Args:
407 | atoms (ase.Atoms): Atoms object.
408 | alchemical_pairs (Sequence[Sequence[Tuple[int, int]]]): List of
409 | alchemical pairs. Each pair is a tuple of the atom index and
410 | atomic number of an alchemical atom.
411 | alchemical_weights (Sequence[float]): List of alchemical weights.
412 | model (str): Model to use for the MACE calculator.
413 | device (str): Device to run the calculations on.
414 |
415 | Returns:
416 | np.ndarray: Optimized cell parameters.
417 | """
418 | # Make a copy of the atoms object
419 | atoms = atoms.copy()
420 |
421 | # Load Alchemical MACE calculator and relax the structure
422 | calc = AlchemicalMACECalculator(
423 | atoms, alchemical_pairs, alchemical_weights, device=device, model=model
424 | )
425 | atoms.set_calculator(calc)
426 | atoms.set_masses(calc.get_alchemical_atomic_masses())
427 | atoms = ExpCellFilter(atoms)
428 | optimizer = FIRE(atoms)
429 | optimizer.run(fmax=kwargs.get("fmax", 0.01), steps=kwargs.get("steps", 500))
430 |
431 | # Return the optimized cell parameters
432 | return atoms.atoms.get_cell().cellpar()
433 |
--------------------------------------------------------------------------------
/alchemical_mace/model.py:
--------------------------------------------------------------------------------
1 | import ast
2 | from collections import namedtuple
3 | from typing import Dict, List, Optional, Sequence, Tuple
4 |
5 | import ase
6 | import numpy as np
7 | import torch
8 | import torch.nn.functional as F
9 | from e3nn import o3
10 | from e3nn.util.jit import compile_mode
11 | from mace import modules, tools
12 | from mace.calculators import mace_mp
13 | from mace.data.neighborhood import get_neighborhood
14 | from mace.modules import RealAgnosticResidualInteractionBlock, ScaleShiftMACE
15 | from mace.modules.utils import get_edge_vectors_and_lengths, get_symmetric_displacement
16 | from mace.tools import (
17 | AtomicNumberTable,
18 | atomic_numbers_to_indices,
19 | to_one_hot,
20 | torch_geometric,
21 | utils,
22 | )
23 | from mace.tools.scatter import scatter_sum
24 |
25 | ################################################################################
26 | # Alchemy manager class for handling alchemical weights
27 | ################################################################################
28 |
29 | AlchemicalPair = namedtuple("AlchemicalPair", ["atom_index", "atomic_number"])
30 |
31 |
32 | class AlchemyManager(torch.nn.Module):
33 | """
34 | Class for managing alchemical weights and building alchemical graphs for MACE.
35 | """
36 |
37 | def __init__(
38 | self,
39 | atoms: ase.Atoms,
40 | alchemical_pairs: Sequence[Sequence[Tuple[int, int]]],
41 | alchemical_weights: torch.Tensor,
42 | z_table: AtomicNumberTable,
43 | r_max: float,
44 | ):
45 | """
46 | Initialize the alchemy manager.
47 |
48 | Args:
49 | atoms: ASE atoms object
50 | alchemical_pairs: List of lists of tuples, where each tuple contains
51 | the atom index and atomic number of an alchemical atom
52 | alchemical_weights: Tensor of alchemical weights
53 | z_table: Atomic number table
54 | r_max: Maximum cutoff radius for the alchemical graph
55 | """
56 | super().__init__()
57 | self.alchemical_weights = torch.nn.Parameter(alchemical_weights)
58 | self.r_max = r_max
59 |
60 | # Process alchemical pairs into atom indices and atomic numbers
61 | # Alchemical weights are 1-indexed, 0 is reserved for non-alchemical atoms
62 | alchemical_atom_indices = []
63 | alchemical_atomic_numbers = []
64 | alchemical_weight_indices = []
65 |
66 | for weight_idx, pairs in enumerate(alchemical_pairs):
67 | for pair in pairs:
68 | alchemical_atom_indices.append(pair.atom_index)
69 | alchemical_atomic_numbers.append(pair.atomic_number)
70 | alchemical_weight_indices.append(weight_idx + 1)
71 |
72 | non_alchemical_atom_indices = [
73 | i for i in range(len(atoms)) if i not in alchemical_atom_indices
74 | ]
75 | non_alchemical_atomic_numbers = atoms.get_atomic_numbers()[
76 | non_alchemical_atom_indices
77 | ].tolist()
78 | non_alchemical_weight_indices = [0] * len(non_alchemical_atom_indices)
79 |
80 | self.atom_indices = alchemical_atom_indices + non_alchemical_atom_indices
81 | self.atomic_numbers = alchemical_atomic_numbers + non_alchemical_atomic_numbers
82 | self.weight_indices = alchemical_weight_indices + non_alchemical_weight_indices
83 |
84 | self.atom_indices = np.array(self.atom_indices)
85 | self.atomic_numbers = np.array(self.atomic_numbers)
86 | self.weight_indices = np.array(self.weight_indices)
87 |
88 | sort_idx = np.argsort(self.atom_indices)
89 | self.atom_indices = self.atom_indices[sort_idx]
90 | self.atomic_numbers = self.atomic_numbers[sort_idx]
91 | self.weight_indices = self.weight_indices[sort_idx]
92 |
93 | # Array to map original atom indices to alchemical indices
94 | # -1 means the atom does not have a corresponding alchemical atom
95 | # [n_atoms, n_weights + 1]
96 | self.original_to_alchemical_index = np.full(
97 | (len(atoms), len(alchemical_pairs) + 1), -1
98 | )
99 | for i, (atom_idx, weight_idx) in enumerate(
100 | zip(self.atom_indices, self.weight_indices)
101 | ):
102 | self.original_to_alchemical_index[atom_idx, weight_idx] = i
103 |
104 | self.is_original_atom_alchemical = np.any(
105 | self.original_to_alchemical_index[:, 1:] != -1, axis=1
106 | )
107 |
108 | # Extract common node features
109 | z_indices = atomic_numbers_to_indices(self.atomic_numbers, z_table=z_table)
110 | node_attrs = to_one_hot(
111 | torch.tensor(z_indices, dtype=torch.long).unsqueeze(-1),
112 | num_classes=len(z_table),
113 | )
114 | self.register_buffer("node_attrs", node_attrs)
115 | self.pbc = atoms.get_pbc()
116 |
117 | def forward(
118 | self,
119 | positions: torch.Tensor,
120 | cell: torch.Tensor,
121 | ) -> Dict[str, torch.Tensor]:
122 | """
123 | Build an alchemical graph for the given positions and cell.
124 |
125 | Args:
126 | positions: Tensor of atomic positions
127 | cell: Tensor of cell vectors
128 |
129 | Returns:
130 | Dictionary containing the alchemical graph data
131 | """
132 |
133 | # Build original atom graph
134 | orig_edge_index, shifts, unit_shifts = get_neighborhood(
135 | positions=positions.detach().cpu().numpy(),
136 | cutoff=self.r_max,
137 | pbc=self.pbc,
138 | cell=cell.detach().cpu().numpy(),
139 | )
140 |
141 | # Extend edges to alchemical pairs
142 | edge_index = []
143 | orig_edge_loc = []
144 | edge_weight_indices = []
145 |
146 | is_alchemical = self.is_original_atom_alchemical[orig_edge_index]
147 | src_non_dst_non = ~is_alchemical[0] & ~is_alchemical[1]
148 | src_non_dst_alch = ~is_alchemical[0] & is_alchemical[1]
149 | src_alch_dst_non = is_alchemical[0] & ~is_alchemical[1]
150 | src_alch_dst_alch = is_alchemical[0] & is_alchemical[1]
151 |
152 | # Both non-alchemical: keep as is
153 | _orig_edge_index = orig_edge_index[:, src_non_dst_non]
154 | edge_index.append(self.original_to_alchemical_index[_orig_edge_index, 0])
155 | orig_edge_loc.append(np.where(src_non_dst_non)[0])
156 | edge_weight_indices.append(np.zeros_like(_orig_edge_index[0]))
157 |
158 | # Source non-alchemical, destination alchemical: pair all, weights are 1
159 | _src, _dst = orig_edge_index[:, src_non_dst_alch]
160 | _orig_edge_loc = np.where(src_non_dst_alch)[0]
161 | _src = self.original_to_alchemical_index[_src, 0]
162 | _dst = self.original_to_alchemical_index[_dst, :]
163 | _dst_mask = _dst != -1
164 | _dst = _dst[_dst_mask]
165 | _repeat = _dst_mask.sum(axis=1)
166 | _src = np.repeat(_src, _repeat)
167 | edge_index.append(np.stack((_src, _dst), axis=0))
168 | orig_edge_loc.append(np.repeat(_orig_edge_loc, _repeat))
169 | edge_weight_indices.append(np.zeros_like(_src))
170 |
171 | # Source alchemical, destination non-alchemical: pair all, follow src weights
172 | _src, _dst = orig_edge_index[:, src_alch_dst_non]
173 | _orig_edge_loc = np.where(src_alch_dst_non)[0]
174 | _src = self.original_to_alchemical_index[_src, :]
175 | _dst = self.original_to_alchemical_index[_dst, 0]
176 | _src_mask = _src != -1
177 | _src = _src[_src_mask]
178 | _repeat = _src_mask.sum(axis=1)
179 | _dst = np.repeat(_dst, _repeat)
180 | edge_index.append(np.stack((_src, _dst), axis=0))
181 | orig_edge_loc.append(np.repeat(_orig_edge_loc, _repeat))
182 | edge_weight_indices.append(np.where(_src_mask)[1])
183 |
184 | # Both alchemical: pair according to alchemical indices, weights are 1
185 | _orig_edge_index = orig_edge_index[:, src_alch_dst_alch]
186 | _orig_edge_loc = np.where(src_alch_dst_alch)[0]
187 | _alch_edge_index = self.original_to_alchemical_index[_orig_edge_index, :]
188 | _idx = np.where((_alch_edge_index != -1).all(axis=0))
189 | edge_index.append(_alch_edge_index[:, _idx[0], _idx[1]])
190 | orig_edge_loc.append(_orig_edge_loc[_idx[0]])
191 | edge_weight_indices.append(np.zeros_like(_idx[0]))
192 |
193 | # Collect all edges
194 | edge_index = np.concatenate(edge_index, axis=1)
195 | orig_edge_loc = np.concatenate(orig_edge_loc)
196 | edge_weight_indices = np.concatenate(edge_weight_indices)
197 |
198 | # Convert to torch tensors
199 | edge_index = torch.tensor(edge_index, dtype=torch.long)
200 | shifts = torch.tensor(shifts[orig_edge_loc], dtype=torch.float32)
201 | unit_shifts = torch.tensor(unit_shifts[orig_edge_loc], dtype=torch.float32)
202 |
203 | # Alchemical weights for nodes and edges
204 | weights = F.pad(self.alchemical_weights, (1, 0), "constant", 1.0)
205 | node_weights = weights[self.weight_indices]
206 | edge_weights = weights[edge_weight_indices]
207 |
208 | # Build data batch
209 | atomic_data = torch_geometric.data.Data(
210 | num_nodes=len(self.atom_indices),
211 | edge_index=edge_index,
212 | node_attrs=self.node_attrs,
213 | positions=positions[self.atom_indices],
214 | shifts=shifts,
215 | unit_shifts=unit_shifts,
216 | cell=cell,
217 | node_weights=node_weights,
218 | edge_weights=edge_weights,
219 | node_atom_indices=torch.tensor(self.atom_indices, dtype=torch.long),
220 | )
221 | data_loader = torch_geometric.dataloader.DataLoader(
222 | dataset=[atomic_data],
223 | batch_size=1,
224 | shuffle=False,
225 | drop_last=False,
226 | )
227 | batch = next(iter(data_loader))
228 |
229 | return batch
230 |
231 |
232 | ################################################################################
233 | # Alchemical MACE model
234 | ################################################################################
235 |
236 | # get_outputs function from mace.modules.utils is modified to calculate also
237 | # the alchemical gradients
238 |
239 |
240 | def get_outputs(
241 | energy: torch.Tensor,
242 | positions: torch.Tensor,
243 | displacement: torch.Tensor,
244 | cell: torch.Tensor,
245 | node_weights: torch.Tensor,
246 | edge_weights: torch.Tensor,
247 | retain_graph: bool = False,
248 | create_graph: bool = False,
249 | compute_force: bool = True,
250 | compute_stress: bool = False,
251 | compute_alchemical_grad: bool = False,
252 | ) -> Tuple[
253 | Optional[torch.Tensor],
254 | Optional[torch.Tensor],
255 | Optional[torch.Tensor],
256 | Optional[torch.Tensor],
257 | Optional[torch.Tensor],
258 | ]:
259 | grad_outputs: List[Optional[torch.Tensor]] = [torch.ones_like(energy)]
260 | if not compute_force:
261 | return None, None, None, None, None
262 | inputs = [positions]
263 | if compute_stress:
264 | inputs.append(displacement)
265 | if compute_alchemical_grad:
266 | inputs.extend([node_weights, edge_weights])
267 | gradients = torch.autograd.grad(
268 | outputs=[energy],
269 | inputs=inputs,
270 | grad_outputs=grad_outputs,
271 | retain_graph=retain_graph,
272 | create_graph=create_graph,
273 | allow_unused=True,
274 | )
275 |
276 | forces = gradients[0]
277 | stress = torch.zeros_like(displacement)
278 | virials = gradients[1] if compute_stress else None
279 | if compute_alchemical_grad:
280 | node_grad, edge_grad = gradients[-2], gradients[-1]
281 | else:
282 | node_grad, edge_grad = None, None
283 | if compute_stress and virials is not None:
284 | cell = cell.view(-1, 3, 3)
285 | volume = torch.einsum(
286 | "zi,zi->z",
287 | cell[:, 0, :],
288 | torch.cross(cell[:, 1, :], cell[:, 2, :], dim=1),
289 | ).unsqueeze(-1)
290 | stress = virials / volume.view(-1, 1, 1)
291 |
292 | if forces is not None:
293 | forces = -1 * forces
294 | if virials is not None:
295 | virials = -1 * virials
296 | return forces, virials, stress, node_grad, edge_grad
297 |
298 |
299 | @compile_mode("script")
300 | class AlchemicalResidualInteractionBlock(RealAgnosticResidualInteractionBlock):
301 | def forward(
302 | self,
303 | node_attrs: torch.Tensor,
304 | node_feats: torch.Tensor,
305 | edge_attrs: torch.Tensor,
306 | edge_feats: torch.Tensor,
307 | edge_index: torch.Tensor,
308 | edge_weights: torch.Tensor, # alchemy
309 | ) -> Tuple[torch.Tensor, torch.Tensor]:
310 | sender = edge_index[0]
311 | receiver = edge_index[1]
312 | num_nodes = node_feats.shape[0]
313 | sc = self.skip_tp(node_feats, node_attrs)
314 | node_feats = self.linear_up(node_feats)
315 | tp_weights = self.conv_tp_weights(edge_feats)
316 | tp_weights = tp_weights * edge_weights[:, None] # alchemy
317 | mji = self.conv_tp(
318 | node_feats[sender], edge_attrs, tp_weights
319 | ) # [n_edges, irreps]
320 | message = scatter_sum(
321 | src=mji, index=receiver, dim=0, dim_size=num_nodes
322 | ) # [n_nodes, irreps]
323 | message = self.linear(message) / self.avg_num_neighbors
324 | return (
325 | self.reshape(message),
326 | sc,
327 | ) # [n_nodes, channels, (lmax + 1)**2]
328 |
329 |
330 | @compile_mode("script")
331 | class AlchemicalMACE(ScaleShiftMACE):
332 | def forward(
333 | self,
334 | data: Dict[str, torch.Tensor],
335 | retain_graph: bool = False, # alchemy
336 | create_graph: bool = False, # alchemy
337 | compute_force: bool = True,
338 | compute_stress: bool = False,
339 | compute_displacement: bool = False,
340 | compute_alchemical_grad: bool = False, # alchemy
341 | map_to_original_atoms: bool = True, # alchemy
342 | ) -> Dict[str, Optional[torch.Tensor]]:
343 | # Setup
344 | data["positions"].requires_grad_(True)
345 | data["node_attrs"].requires_grad_(True)
346 | num_graphs = data["ptr"].numel() - 1
347 | displacement = torch.zeros(
348 | (num_graphs, 3, 3),
349 | dtype=data["positions"].dtype,
350 | device=data["positions"].device,
351 | )
352 | if compute_stress or compute_displacement:
353 | (
354 | data["positions"],
355 | data["shifts"],
356 | displacement,
357 | ) = get_symmetric_displacement(
358 | positions=data["positions"],
359 | unit_shifts=data["unit_shifts"],
360 | cell=data["cell"],
361 | edge_index=data["edge_index"],
362 | num_graphs=num_graphs,
363 | batch=data["batch"],
364 | )
365 |
366 | # Atomic energies
367 | node_e0 = self.atomic_energies_fn(data["node_attrs"])
368 | node_e0 = node_e0 * data["node_weights"] # alchemy
369 | e0 = scatter_sum(
370 | src=node_e0, index=data["batch"], dim=-1, dim_size=num_graphs
371 | ) # [n_graphs,]
372 |
373 | # Embeddings
374 | node_feats = self.node_embedding(data["node_attrs"])
375 | vectors, lengths = get_edge_vectors_and_lengths(
376 | positions=data["positions"],
377 | edge_index=data["edge_index"],
378 | shifts=data["shifts"],
379 | )
380 | edge_attrs = self.spherical_harmonics(vectors)
381 | edge_feats = self.radial_embedding(lengths)
382 |
383 | # Interactions
384 | node_es_list = []
385 | node_feats_list = []
386 | for interaction, product, readout in zip(
387 | self.interactions, self.products, self.readouts
388 | ):
389 | node_feats, sc = interaction(
390 | node_attrs=data["node_attrs"],
391 | node_feats=node_feats,
392 | edge_attrs=edge_attrs,
393 | edge_feats=edge_feats,
394 | edge_index=data["edge_index"],
395 | edge_weights=data["edge_weights"], # alchemy
396 | )
397 | node_feats = product(
398 | node_feats=node_feats, sc=sc, node_attrs=data["node_attrs"]
399 | )
400 | node_feats_list.append(node_feats)
401 | node_es_list.append(readout(node_feats).squeeze(-1)) # {[n_nodes, ], }
402 |
403 | # Concatenate node features
404 | # node_feats_out = torch.cat(node_feats_list, dim=-1)
405 |
406 | # Sum over interactions
407 | node_inter_es = torch.sum(
408 | torch.stack(node_es_list, dim=0), dim=0
409 | ) # [n_nodes, ]
410 | node_inter_es = self.scale_shift(node_inter_es)
411 | node_inter_es = node_inter_es * data["node_weights"] # alchemy
412 |
413 | # Sum over nodes in graph
414 | inter_e = scatter_sum(
415 | src=node_inter_es, index=data["batch"], dim=-1, dim_size=num_graphs
416 | ) # [n_graphs,]
417 |
418 | # Add E_0 and (scaled) interaction energy
419 | total_energy = e0 + inter_e
420 | node_energy = node_e0 + node_inter_es
421 |
422 | forces, virials, stress, node_grad, edge_grad = get_outputs(
423 | energy=total_energy, # alchemy
424 | positions=data["positions"],
425 | displacement=displacement,
426 | cell=data["cell"],
427 | node_weights=data["node_weights"], # alchemy
428 | edge_weights=data["edge_weights"], # alchemy
429 | retain_graph=retain_graph, # alchemy
430 | create_graph=create_graph, # alchemy
431 | compute_force=compute_force,
432 | # compute_virials=compute_virials, # alchemy
433 | compute_stress=compute_stress,
434 | compute_alchemical_grad=compute_alchemical_grad, # alchemy
435 | )
436 |
437 | # Map to original atoms (node energies and forces): alchemy
438 | if map_to_original_atoms:
439 | # Note: we're not giving the dim_size, as we assume that all
440 | # original atoms are present in the batch
441 | node_index = data["node_atom_indices"]
442 | node_energy = scatter_sum(src=node_energy, dim=0, index=node_index)
443 | if compute_force:
444 | forces = scatter_sum(src=forces, dim=0, index=node_index)
445 |
446 | output = {
447 | "energy": total_energy,
448 | "node_energy": node_energy,
449 | "interaction_energy": inter_e,
450 | "forces": forces,
451 | "virials": virials,
452 | "stress": stress,
453 | "displacement": displacement,
454 | "node_grad": node_grad,
455 | "edge_grad": edge_grad,
456 | }
457 |
458 | return output
459 |
460 |
461 | ################################################################################
462 | # Alchemical MACE universal model
463 | ################################################################################
464 |
465 |
466 | def alchemical_mace_mp(
467 | model: str,
468 | device: str,
469 | default_dtype: str = "float32",
470 | ):
471 | """
472 | Load a pre-trained alchemical MACE model.
473 |
474 | Args:
475 | model: Model size (small, medium)
476 | device: Device to load the model onto
477 | default_dtype: Default data type for the model
478 |
479 | Returns:
480 | Alchemical MACE model
481 | """
482 |
483 | # Load foundation MACE model and extract initial parameters
484 | assert model in ("small", "medium") # TODO: support large model
485 | mace = mace_mp(model=model, device=device, default_dtype=default_dtype).models[0]
486 | atomic_energies = mace.atomic_energies_fn.atomic_energies.detach().clone()
487 | z_table = utils.AtomicNumberTable([int(z) for z in mace.atomic_numbers])
488 | atomic_inter_scale = mace.scale_shift.scale.detach().clone()
489 | atomic_inter_shift = mace.scale_shift.shift.detach().clone()
490 |
491 | # Prepare arguments for building the model
492 | placeholder_args = ["--name", "None", "--train_file", "None"]
493 | args = tools.build_default_arg_parser().parse_args(placeholder_args)
494 | args.max_L = {"small": 0, "medium": 1, "large": 2}[model]
495 | args.num_channels = 128
496 | args.hidden_irreps = o3.Irreps(
497 | (args.num_channels * o3.Irreps.spherical_harmonics(args.max_L))
498 | .sort()
499 | .irreps.simplify()
500 | )
501 |
502 | # Build the alchemical MACE model
503 | model = AlchemicalMACE(
504 | r_max=6.0,
505 | num_bessel=10,
506 | num_polynomial_cutoff=5,
507 | max_ell=3,
508 | interaction_cls=AlchemicalResidualInteractionBlock,
509 | interaction_cls_first=AlchemicalResidualInteractionBlock,
510 | num_interactions=2,
511 | num_elements=len(z_table),
512 | hidden_irreps=o3.Irreps(args.hidden_irreps),
513 | MLP_irreps=o3.Irreps(args.MLP_irreps),
514 | atomic_energies=atomic_energies,
515 | avg_num_neighbors=args.avg_num_neighbors,
516 | atomic_numbers=z_table.zs,
517 | correlation=args.correlation,
518 | gate=modules.gate_dict[args.gate],
519 | radial_MLP=ast.literal_eval(args.radial_MLP),
520 | radial_type=args.radial_type,
521 | atomic_inter_scale=atomic_inter_scale,
522 | atomic_inter_shift=atomic_inter_shift,
523 | )
524 |
525 | # Load foundation model parameters
526 | model.load_state_dict(mace.state_dict(), strict=True)
527 | for i in range(int(model.num_interactions)):
528 | model.interactions[i].avg_num_neighbors = mace.interactions[i].avg_num_neighbors
529 | model = model.to(device)
530 | return model
531 |
532 |
533 | def get_z_table_and_r_max(model: ScaleShiftMACE) -> Tuple[AtomicNumberTable, float]:
534 | """Extract the atomic number table and maximum cutoff radius from a MACE model."""
535 | z_table = AtomicNumberTable([int(z) for z in model.atomic_numbers])
536 | r_max = model.r_max.item()
537 | return z_table, r_max
538 |
--------------------------------------------------------------------------------
/alchemical_mace/optimize.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from typing import Union, Iterable, Dict, Any
3 |
4 |
5 | class ExponentiatedGradientDescent(torch.optim.Optimizer):
6 | """
7 | Implements Exponentiated Gradient Descent.
8 |
9 | Args:
10 | params (iterable of torch.Tensor or dict): iterable of parameters to optimize or
11 | dicts defining parameter groups.
12 | lr (float, optional): learning rate. Defaults to 1e-3.
13 | eps (float, optional): small constant for numerical stability. Defaults to 1e-8.
14 | """
15 |
16 | def __init__(
17 | self,
18 | params: Union[Iterable[torch.Tensor], Iterable[Dict[str, Any]]],
19 | lr: float = 1e-3,
20 | eps: float = 1e-8,
21 | ):
22 | super().__init__(params, defaults=dict(lr=lr, eps=eps))
23 |
24 | def step(self):
25 | for group in self.param_groups:
26 | for p in group["params"]:
27 | if p.grad is None:
28 | continue
29 | p.data.mul_(torch.exp(-group["lr"] * p.grad))
30 | p.data.div_(p.data.sum() + group["eps"])
31 |
--------------------------------------------------------------------------------
/alchemical_mace/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | from contextlib import ExitStack, contextmanager, redirect_stderr, redirect_stdout
3 |
4 | from ase import Atoms
5 |
6 |
7 | @contextmanager
8 | def suppress_print(out: bool = True, err: bool = False):
9 | """Suppress stdout and/or stderr."""
10 |
11 | with ExitStack() as stack:
12 | devnull = stack.enter_context(open(os.devnull, "w"))
13 | if out:
14 | stack.enter_context(redirect_stdout(devnull))
15 | if err:
16 | stack.enter_context(redirect_stderr(devnull))
17 | yield
18 |
19 |
20 | # From CHGNet
21 | def upper_triangular_cell(atoms: Atoms):
22 | """Transform to upper-triangular cell."""
23 | import numpy as np
24 | from ase.md.npt import NPT
25 |
26 | if NPT._isuppertriangular(atoms.get_cell()):
27 | return
28 |
29 | a, b, c, alpha, beta, gamma = atoms.cell.cellpar()
30 | angles = np.radians((alpha, beta, gamma))
31 | sin_a, sin_b, _sin_g = np.sin(angles)
32 | cos_a, cos_b, cos_g = np.cos(angles)
33 | cos_p = (cos_g - cos_a * cos_b) / (sin_a * sin_b)
34 | cos_p = np.clip(cos_p, -1, 1)
35 | sin_p = (1 - cos_p**2) ** 0.5
36 |
37 | new_basis = [
38 | (a * sin_b * sin_p, a * sin_b * cos_p, a * cos_b),
39 | (0, b * sin_a, b * cos_a),
40 | (0, 0, c),
41 | ]
42 |
43 | atoms.set_cell(new_basis, scale_atoms=True)
44 |
--------------------------------------------------------------------------------
/data/structures/AlN_hex.cif:
--------------------------------------------------------------------------------
1 | # generated using pymatgen
2 | data_AlN
3 | _symmetry_space_group_name_H-M 'P 1'
4 | _cell_length_a 3.12664153
5 | _cell_length_b 3.12664143
6 | _cell_length_c 5.00715332
7 | _cell_angle_alpha 90.00000043
8 | _cell_angle_beta 89.99999986
9 | _cell_angle_gamma 119.99999965
10 | _symmetry_Int_Tables_number 1
11 | _chemical_formula_structural AlN
12 | _chemical_formula_sum 'Al2 N2'
13 | _cell_volume 42.39139351
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 | loop_
20 | _atom_site_type_symbol
21 | _atom_site_label
22 | _atom_site_symmetry_multiplicity
23 | _atom_site_fract_x
24 | _atom_site_fract_y
25 | _atom_site_fract_z
26 | _atom_site_occupancy
27 | Al Al0 1 0.66666667 0.33333333 0.49932157 1
28 | Al Al1 1 0.33333333 0.66666667 0.99932156 1
29 | N N2 1 0.66666667 0.33333333 0.88067843 1
30 | N N3 1 0.33333333 0.66666667 0.38067844 1
31 |
--------------------------------------------------------------------------------
/data/structures/BiSBr.cif:
--------------------------------------------------------------------------------
1 | # generated using pymatgen
2 | data_BiSBr
3 | _symmetry_space_group_name_H-M 'P 1'
4 | _cell_length_a 4.10679210
5 | _cell_length_b 8.22484633
6 | _cell_length_c 11.05701800
7 | _cell_angle_alpha 89.99999573
8 | _cell_angle_beta 90.00000347
9 | _cell_angle_gamma 90.00001971
10 | _symmetry_Int_Tables_number 1
11 | _chemical_formula_structural BiSBr
12 | _chemical_formula_sum 'Bi4 S4 Br4'
13 | _cell_volume 373.48101173
14 | _cell_formula_units_Z 4
15 | loop_
16 | _symmetry_equiv_pos_site_id
17 | _symmetry_equiv_pos_as_xyz
18 | 1 'x, y, z'
19 | loop_
20 | _atom_site_type_symbol
21 | _atom_site_label
22 | _atom_site_symmetry_multiplicity
23 | _atom_site_fract_x
24 | _atom_site_fract_y
25 | _atom_site_fract_z
26 | _atom_site_occupancy
27 | Bi Bi0 1 0.24999992 0.88556832 0.87177467 1
28 | Bi Bi1 1 0.75000010 0.11443170 0.12822534 1
29 | Bi Bi2 1 0.24999988 0.38556830 0.62822532 1
30 | Bi Bi3 1 0.75000009 0.61443169 0.37177466 1
31 | S S4 1 0.74999990 0.82391374 0.03167750 1
32 | S S5 1 0.25000008 0.67608625 0.53167751 1
33 | S S6 1 0.74999991 0.32391375 0.46832249 1
34 | S S7 1 0.25000010 0.17608625 0.96832251 1
35 | Br Br8 1 0.24999990 0.96479764 0.30006197 1
36 | Br Br9 1 0.75000009 0.53520235 0.80006197 1
37 | Br Br10 1 0.24999991 0.46479765 0.19993803 1
38 | Br Br11 1 0.75000013 0.03520235 0.69993804 1
39 |
--------------------------------------------------------------------------------
/data/structures/CeO2.cif:
--------------------------------------------------------------------------------
1 | # generated using pymatgen
2 | data_CeO2
3 | _symmetry_space_group_name_H-M 'P 1'
4 | _cell_length_a 5.46789061
5 | _cell_length_b 5.46789009
6 | _cell_length_c 5.46788984
7 | _cell_angle_alpha 90.00000180
8 | _cell_angle_beta 90.00000282
9 | _cell_angle_gamma 90.00000072
10 | _symmetry_Int_Tables_number 1
11 | _chemical_formula_structural CeO2
12 | _chemical_formula_sum 'Ce4 O8'
13 | _cell_volume 163.47801292
14 | _cell_formula_units_Z 4
15 | loop_
16 | _symmetry_equiv_pos_site_id
17 | _symmetry_equiv_pos_as_xyz
18 | 1 'x, y, z'
19 | loop_
20 | _atom_site_type_symbol
21 | _atom_site_label
22 | _atom_site_symmetry_multiplicity
23 | _atom_site_fract_x
24 | _atom_site_fract_y
25 | _atom_site_fract_z
26 | _atom_site_occupancy
27 | Ce Ce0 1 0.00000000 -0.00000000 -0.00000000 1
28 | Ce Ce1 1 0.00000000 0.50000000 0.50000000 1
29 | Ce Ce2 1 0.50000000 0.00000000 0.50000000 1
30 | Ce Ce3 1 0.50000000 0.50000000 0.00000000 1
31 | O O4 1 0.25000000 0.75000000 0.74999999 1
32 | O O5 1 0.74999999 0.25000000 0.25000000 1
33 | O O6 1 0.25000000 0.25000000 0.25000000 1
34 | O O7 1 0.75000000 0.75000000 0.75000000 1
35 | O O8 1 0.74999999 0.75000000 0.25000000 1
36 | O O9 1 0.25000000 0.25000000 0.74999999 1
37 | O O10 1 0.75000000 0.25000000 0.75000000 1
38 | O O11 1 0.25000000 0.75000000 0.25000000 1
39 |
--------------------------------------------------------------------------------
/data/structures/CsPbI3_alpha.cif:
--------------------------------------------------------------------------------
1 | # generated using pymatgen
2 | data_CsPbI3
3 | _symmetry_space_group_name_H-M 'P 1'
4 | _cell_length_a 6.37904246
5 | _cell_length_b 6.37904251
6 | _cell_length_c 6.37904247
7 | _cell_angle_alpha 90.00000013
8 | _cell_angle_beta 89.99999979
9 | _cell_angle_gamma 89.99999953
10 | _symmetry_Int_Tables_number 1
11 | _chemical_formula_structural CsPbI3
12 | _chemical_formula_sum 'Cs1 Pb1 I3'
13 | _cell_volume 259.57716340
14 | _cell_formula_units_Z 1
15 | loop_
16 | _symmetry_equiv_pos_site_id
17 | _symmetry_equiv_pos_as_xyz
18 | 1 'x, y, z'
19 | loop_
20 | _atom_site_type_symbol
21 | _atom_site_label
22 | _atom_site_symmetry_multiplicity
23 | _atom_site_fract_x
24 | _atom_site_fract_y
25 | _atom_site_fract_z
26 | _atom_site_occupancy
27 | Cs Cs0 1 0.00000000 0.00000000 -0.00000000 1
28 | I I1 1 0.50000000 0.50000000 -0.00000000 1
29 | I I2 1 0.50000000 -0.00000000 0.50000000 1
30 | I I3 1 0.00000000 0.50000000 0.50000000 1
31 | Pb Pb4 1 0.50000000 0.50000000 0.50000000 1
32 |
--------------------------------------------------------------------------------
/data/structures/CsPbI3_delta.cif:
--------------------------------------------------------------------------------
1 | # generated using pymatgen
2 | data_CsPbI3
3 | _symmetry_space_group_name_H-M 'P 1'
4 | _cell_length_a 4.91187765
5 | _cell_length_b 10.78602159
6 | _cell_length_c 18.12941751
7 | _cell_angle_alpha 90.00000734
8 | _cell_angle_beta 89.99999876
9 | _cell_angle_gamma 89.99999900
10 | _symmetry_Int_Tables_number 1
11 | _chemical_formula_structural CsPbI3
12 | _chemical_formula_sum 'Cs4 Pb4 I12'
13 | _cell_volume 960.48962177
14 | _cell_formula_units_Z 4
15 | loop_
16 | _symmetry_equiv_pos_site_id
17 | _symmetry_equiv_pos_as_xyz
18 | 1 'x, y, z'
19 | loop_
20 | _atom_site_type_symbol
21 | _atom_site_label
22 | _atom_site_symmetry_multiplicity
23 | _atom_site_fract_x
24 | _atom_site_fract_y
25 | _atom_site_fract_z
26 | _atom_site_occupancy
27 | Cs Cs0 1 0.75000002 0.57436836 0.17329066 1
28 | Cs Cs1 1 0.25000000 0.42563165 0.82670935 1
29 | Cs Cs2 1 0.75000002 0.07436835 0.32670935 1
30 | Cs Cs3 1 0.24999999 0.92563166 0.67329065 1
31 | I I4 1 0.74999999 0.20706041 0.71324789 1
32 | I I5 1 0.25000000 0.79293960 0.28675211 1
33 | I I6 1 0.75000001 0.97331566 0.10968932 1
34 | I I7 1 0.24999998 0.02668433 0.89031069 1
35 | I I8 1 0.74999999 0.47331567 0.39031069 1
36 | I I9 1 0.25000001 0.52668433 0.60968931 1
37 | I I10 1 0.25000000 0.66460269 0.00429655 1
38 | I I11 1 0.25000000 0.16460269 0.49570344 1
39 | I I12 1 0.25000000 0.29293960 0.21324788 1
40 | I I13 1 0.75000001 0.83539729 0.50429655 1
41 | I I14 1 0.75000000 0.33539731 0.99570345 1
42 | I I15 1 0.75000001 0.70706040 0.78675212 1
43 | Pb Pb16 1 0.74999998 0.83743203 0.94239546 1
44 | Pb Pb17 1 0.24999999 0.16256797 0.05760453 1
45 | Pb Pb18 1 0.75000002 0.33743205 0.55760454 1
46 | Pb Pb19 1 0.24999998 0.66256796 0.44239545 1
47 |
--------------------------------------------------------------------------------
/data/structures/CsSnI3_alpha.cif:
--------------------------------------------------------------------------------
1 | # generated using pymatgen
2 | data_CsSnI3
3 | _symmetry_space_group_name_H-M 'P 1'
4 | _cell_length_a 6.27081715
5 | _cell_length_b 6.27081724
6 | _cell_length_c 6.27081711
7 | _cell_angle_alpha 89.99999964
8 | _cell_angle_beta 89.99999969
9 | _cell_angle_gamma 89.99999972
10 | _symmetry_Int_Tables_number 1
11 | _chemical_formula_structural CsSnI3
12 | _chemical_formula_sum 'Cs1 Sn1 I3'
13 | _cell_volume 246.58827085
14 | _cell_formula_units_Z 1
15 | loop_
16 | _symmetry_equiv_pos_site_id
17 | _symmetry_equiv_pos_as_xyz
18 | 1 'x, y, z'
19 | loop_
20 | _atom_site_type_symbol
21 | _atom_site_label
22 | _atom_site_symmetry_multiplicity
23 | _atom_site_fract_x
24 | _atom_site_fract_y
25 | _atom_site_fract_z
26 | _atom_site_occupancy
27 | Cs Cs0 1 0.00000000 0.00000000 -0.00000000 1
28 | I I1 1 0.50000000 0.50000000 0.00000000 1
29 | I I2 1 0.50000000 -0.00000000 0.50000000 1
30 | I I3 1 -0.00000000 0.50000000 0.50000000 1
31 | Sn Sn4 1 0.50000000 0.50000000 0.50000000 1
32 |
--------------------------------------------------------------------------------
/data/structures/CsSnI3_delta.cif:
--------------------------------------------------------------------------------
1 | # generated using pymatgen
2 | data_CsSnI3
3 | _symmetry_space_group_name_H-M 'P 1'
4 | _cell_length_a 4.84918880
5 | _cell_length_b 10.69167480
6 | _cell_length_c 18.19616575
7 | _cell_angle_alpha 89.99999030
8 | _cell_angle_beta 90.00000262
9 | _cell_angle_gamma 89.99999762
10 | _symmetry_Int_Tables_number 1
11 | _chemical_formula_structural CsSnI3
12 | _chemical_formula_sum 'Cs4 Sn4 I12'
13 | _cell_volume 943.39749344
14 | _cell_formula_units_Z 4
15 | loop_
16 | _symmetry_equiv_pos_site_id
17 | _symmetry_equiv_pos_as_xyz
18 | 1 'x, y, z'
19 | loop_
20 | _atom_site_type_symbol
21 | _atom_site_label
22 | _atom_site_symmetry_multiplicity
23 | _atom_site_fract_x
24 | _atom_site_fract_y
25 | _atom_site_fract_z
26 | _atom_site_occupancy
27 | Cs Cs0 1 0.75000000 0.57552416 0.17186806 1
28 | Cs Cs1 1 0.25000001 0.42447584 0.82813194 1
29 | Cs Cs2 1 0.75000000 0.07552414 0.32813195 1
30 | Cs Cs3 1 0.25000000 0.92447584 0.67186806 1
31 | I I4 1 0.75000000 0.21397041 0.70606567 1
32 | I I5 1 0.25000000 0.78602961 0.29393431 1
33 | I I6 1 0.75000000 0.97123818 0.11227438 1
34 | I I7 1 0.25000000 0.02876181 0.88772566 1
35 | I I8 1 0.74999999 0.47123818 0.38772564 1
36 | I I9 1 0.25000001 0.52876182 0.61227437 1
37 | I I10 1 0.25000002 0.66813711 0.00020614 1
38 | I I11 1 0.24999999 0.16813710 0.49979388 1
39 | I I12 1 0.25000001 0.28602959 0.20606567 1
40 | I I13 1 0.74999999 0.83186292 0.50020614 1
41 | I I14 1 0.75000000 0.33186292 0.99979386 1
42 | I I15 1 0.74999999 0.71397042 0.79393433 1
43 | Sn Sn16 1 0.75000000 0.84420702 0.94393743 1
44 | Sn Sn17 1 0.25000000 0.15579296 0.05606252 1
45 | Sn Sn18 1 0.74999998 0.34420702 0.55606253 1
46 | Sn Sn19 1 0.24999999 0.65579297 0.44393746 1
47 |
--------------------------------------------------------------------------------
/data/structures/Fe.cif:
--------------------------------------------------------------------------------
1 | # generated using pymatgen
2 | data_Fe
3 | _symmetry_space_group_name_H-M 'P 1'
4 | _cell_length_a 2.86106543
5 | _cell_length_b 2.86106544
6 | _cell_length_c 2.86106538
7 | _cell_angle_alpha 90.00000018
8 | _cell_angle_beta 89.99999992
9 | _cell_angle_gamma 90.00000009
10 | _symmetry_Int_Tables_number 1
11 | _chemical_formula_structural Fe
12 | _chemical_formula_sum Fe2
13 | _cell_volume 23.41980977
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 | loop_
20 | _atom_site_type_symbol
21 | _atom_site_label
22 | _atom_site_symmetry_multiplicity
23 | _atom_site_fract_x
24 | _atom_site_fract_y
25 | _atom_site_fract_z
26 | _atom_site_occupancy
27 | Fe Fe0 1 0.00000000 -0.00000000 0.00000000 1
28 | Fe Fe1 1 0.50000000 0.50000000 0.50000000 1
29 |
--------------------------------------------------------------------------------
/data/structures/GaN_hex.cif:
--------------------------------------------------------------------------------
1 | # generated using pymatgen
2 | data_GaN
3 | _symmetry_space_group_name_H-M 'P 1'
4 | _cell_length_a 3.21192371
5 | _cell_length_b 3.21192377
6 | _cell_length_c 5.21628467
7 | _cell_angle_alpha 90.00000055
8 | _cell_angle_beta 90.00000024
9 | _cell_angle_gamma 119.99999991
10 | _symmetry_Int_Tables_number 1
11 | _chemical_formula_structural GaN
12 | _chemical_formula_sum 'Ga2 N2'
13 | _cell_volume 46.60391121
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 | loop_
20 | _atom_site_type_symbol
21 | _atom_site_label
22 | _atom_site_symmetry_multiplicity
23 | _atom_site_fract_x
24 | _atom_site_fract_y
25 | _atom_site_fract_z
26 | _atom_site_occupancy
27 | Ga Ga0 1 0.66666667 0.33333333 0.49900050 1
28 | Ga Ga1 1 0.33333333 0.66666667 0.99900050 1
29 | N N2 1 0.66666667 0.33333333 0.87599950 1
30 | N N3 1 0.33333333 0.66666667 0.37599950 1
31 |
--------------------------------------------------------------------------------
/data/structures/NaCl.cif:
--------------------------------------------------------------------------------
1 | # generated using pymatgen
2 | data_NaCl
3 | _symmetry_space_group_name_H-M 'P 1'
4 | _cell_length_a 5.68304678
5 | _cell_length_b 5.68304679
6 | _cell_length_c 5.68304672
7 | _cell_angle_alpha 90.00000009
8 | _cell_angle_beta 90.00000015
9 | _cell_angle_gamma 90.00000005
10 | _symmetry_Int_Tables_number 1
11 | _chemical_formula_structural NaCl
12 | _chemical_formula_sum 'Na4 Cl4'
13 | _cell_volume 183.54547783
14 | _cell_formula_units_Z 4
15 | loop_
16 | _symmetry_equiv_pos_site_id
17 | _symmetry_equiv_pos_as_xyz
18 | 1 'x, y, z'
19 | loop_
20 | _atom_site_type_symbol
21 | _atom_site_label
22 | _atom_site_symmetry_multiplicity
23 | _atom_site_fract_x
24 | _atom_site_fract_y
25 | _atom_site_fract_z
26 | _atom_site_occupancy
27 | Na Na0 1 -0.00000000 -0.00000000 -0.00000000 1
28 | Na Na1 1 -0.00000000 0.50000000 0.50000000 1
29 | Na Na2 1 0.50000000 -0.00000000 0.50000000 1
30 | Na Na3 1 0.50000000 0.50000000 -0.00000000 1
31 | Cl Cl4 1 0.00000000 0.00000000 0.50000000 1
32 | Cl Cl5 1 -0.00000000 0.50000000 0.00000000 1
33 | Cl Cl6 1 0.50000000 0.00000000 -0.00000000 1
34 | Cl Cl7 1 0.50000000 0.50000000 0.50000000 1
35 |
--------------------------------------------------------------------------------
/notebooks/1_solid_solution.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import ase\n",
10 | "import matplotlib.pyplot as plt\n",
11 | "import numpy as np\n",
12 | "from tqdm import tqdm\n",
13 | "\n",
14 | "from alchemical_mace.calculator import get_alchemical_optimized_cellpar\n",
15 | "from alchemical_mace.model import AlchemicalPair\n",
16 | "from alchemical_mace.utils import suppress_print\n",
17 | "\n",
18 | "plt.style.use(\"default\")"
19 | ]
20 | },
21 | {
22 | "cell_type": "markdown",
23 | "metadata": {},
24 | "source": [
25 | "### CeO2"
26 | ]
27 | },
28 | {
29 | "cell_type": "code",
30 | "execution_count": 2,
31 | "metadata": {},
32 | "outputs": [
33 | {
34 | "name": "stderr",
35 | "output_type": "stream",
36 | "text": [
37 | "100%|██████████| 21/21 [01:15<00:00, 3.60s/it]\n"
38 | ]
39 | }
40 | ],
41 | "source": [
42 | "# Default settings\n",
43 | "model = \"medium\"\n",
44 | "device = \"cpu\"\n",
45 | "\n",
46 | "# Load structure\n",
47 | "atoms = ase.io.read(\"../data/structures/CeO2.cif\")\n",
48 | "alch_elements = [\"Ce\", \"Sn\"]\n",
49 | "alch_idx = [i for i, atom in enumerate(atoms) if atom.symbol in alch_elements]\n",
50 | "alch_atomic_numbers = [ase.Atoms(el).numbers[0] for el in alch_elements]\n",
51 | "alchemical_pairs = [\n",
52 | " [AlchemicalPair(atom_index=idx, atomic_number=z) for idx in alch_idx]\n",
53 | " for z in alch_atomic_numbers\n",
54 | "]\n",
55 | "\n",
56 | "comp_grid = [[1 - x, x] for x in np.linspace(0, 0.5, 21)]\n",
57 | "lat_params_CeSn = []\n",
58 | "for comp in tqdm(comp_grid):\n",
59 | " with suppress_print(out=True, err=True):\n",
60 | " cellpar = get_alchemical_optimized_cellpar(\n",
61 | " atoms, alchemical_pairs, comp, model=model, device=device\n",
62 | " )\n",
63 | " lat_params_CeSn.append(cellpar[:3])"
64 | ]
65 | },
66 | {
67 | "cell_type": "code",
68 | "execution_count": 3,
69 | "metadata": {},
70 | "outputs": [
71 | {
72 | "name": "stderr",
73 | "output_type": "stream",
74 | "text": [
75 | "100%|██████████| 21/21 [01:35<00:00, 4.52s/it]\n"
76 | ]
77 | }
78 | ],
79 | "source": [
80 | "# Load structure\n",
81 | "alch_elements = [\"Ce\", \"Zr\"]\n",
82 | "alch_idx = [i for i, atom in enumerate(atoms) if atom.symbol in alch_elements]\n",
83 | "alch_atomic_numbers = [ase.Atoms(el).numbers[0] for el in alch_elements]\n",
84 | "alchemical_pairs = [\n",
85 | " [AlchemicalPair(atom_index=idx, atomic_number=z) for idx in alch_idx]\n",
86 | " for z in alch_atomic_numbers\n",
87 | "]\n",
88 | "\n",
89 | "comp_grid = [[1 - x, x] for x in np.linspace(0, 0.5, 21)]\n",
90 | "lat_params_CeZr = []\n",
91 | "for comp in tqdm(comp_grid):\n",
92 | " with suppress_print(out=True, err=True):\n",
93 | " cellpar = get_alchemical_optimized_cellpar(\n",
94 | " atoms, alchemical_pairs, comp, model=model, device=device\n",
95 | " )\n",
96 | " lat_params_CeZr.append(cellpar[:3])"
97 | ]
98 | },
99 | {
100 | "cell_type": "code",
101 | "execution_count": 4,
102 | "metadata": {},
103 | "outputs": [
104 | {
105 | "data": {
106 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAATkAAAD/CAYAAACKJ6HsAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA5pklEQVR4nO3deVxU9f7H8dcsrLKJ7IgoiuKGu/7cUlPTMrdMc8kltxYrq6u3xbqYWXrNlqtZlloulWZWZmq44JJ7aaEIrigiAqLsiKzz/f1xgkQBQQfOzPB9Ph7zuHdmzpz5cIK355zvphFCCCRJkiyUVu0CJEmSqpIMOUmSLJoMOUmSLJoMOUmSLJoMOUmSLJoMOUmSLJoMOUmSLJpe7QJMlcFgID4+HkdHRzQajdrlSJJ0CyEEmZmZ+Pj4oNWWf64mQ64M8fHx+Pn5qV2GJEnluHz5MnXr1i13GxlyZXB0dASUg+jk5KRyNZIk3SojIwM/P7/iv9PyyJArQ9ElqpOTkww5STJRFbmVJBseJEmyaDLkJEmyaDLkJEmyaDLkJEmyaDLk7lP6zXzSonYhDAa1S5EkqRSydfU+ndy0iK6n5vCDoScrXZ7Hs05t/OvYU89Vefi52lO3th22Vjq1S5WkGkmG3H0quJlBodAwTLuHJmkxPHttOjuFZ4ltNBrwcrLFz9WeALdaNPd1pqWvM0FejjL8JKmKaeT056XLyMjA2dmZ9PT0u/aTyzu3G92Pk9DdTCZX78RPDULYbWhNbMpNYpNvcCOvsNTP6bUaAj0daeHjRMu6zrTwdaaZt5MMPkm6i8r8fcqQK0NlDiIA6Vdg/Ti4chTQQI9/Q49XERotKTfyiE3JJjYlm3NXs4i4ks7JK+kk38i7Yzc6rYZADwda+DrTpp4L7f1dCfRwQKuV42clqYgMOSOodMgBFOTCtjfgj+XK80Z94LFlYO96x6ZCCBLSc4oDr+h/r2fdGXxOtnra13elnX9tOtR3Jbiuszzbk2o0GXJGcE8hV+T4OvjlJSi4CS71YMQa8Gl9148JIUjMyCEiTgm9Y5dS+Ss2jZv5JS93rXQaWvo6076+K+39a9O+viuutawrV6MkmTEZckZwXyEHkHgSvnsSUi+CzgYGfABtx1Z6N/mFBk4lZPBHTCrHLqXwR0wq1zJzS2yj0UBwXRcebOLBg0EeNPdxkpe3kkWTIWcE9x1yADfT4Kdn4OyvyvO24+Dh98HK9p7rEkJwOeUmf8SkcPRSKkdjUjiXlFViG3dHG3o1cefBIA+6BbrjYCMb0SXLIkPOCIwScgAGA+z/EHbNBQR4t4aR34Bz+XNgVcbVjBz2nEki7FQS+89fJ/uW1lwrnYZODerQK0g5y2vgVsto3ytJapEhZwRGC7ki0btgwyS4mQK13JX7dP6d73+/t8ktKOT3iynsOp3E7tNJxCRnl3g/wL0W/Zt70a+5F8F1neWsx5JZkiFnBEYPOYC0WFg7Gq5GgNYKHnkf2j9lnH2X4cK1LCXwziRx5EIKBYZ//nN7O9vyUDNP+jX3omMDV/Q6OcpPMg8y5IygSkIOIO8G/DwNIn9SnrefBP3ng77qW0czc/LZfeYa204msvtMUonL2tr2VvRuqgRe90A32UVFMmky5IygykIOQAjlPl3YO4AA/64wfBU4uBv3e8qRk1/IgfPXCT2ZyM5TV0nNzi9+z95aR88m7gxo6cODQR7YWcvAk0yLDDkjqNKQK3ImFH6YDHmZ4OynNEh4t6qa7ypHQaGBP2JS2RaZyPbIROLTc4rfs7fW0aepJwNb+fBAYzds9DLwJPXJkDOCagk5gGtnYO0oSIkGvR0MWQIthlXd992FEIKIK+lsjUhk84l44lJvFr/naKunX3MvHg32pmsjN6zkPTxJJTLkjKDaQg7gZqrS8hodpjzv9go8+CZo1T1rEkIQfjmNzScS2HwinqsZ/3RCrm1vxcMtvXk02JtODeqgk52PpWokQ84IqjXkAAyFsHM2HFykPA/sB8OWga1z1X93BRgMgj9iUth8IoGtEQklJhfwdrZlRHs/Rnb0w9vZTsUqpZpChpwRVHvIFTmxHja9AAU5UKcRjPwW3JtU3/dXQEGhgcMXUvjleDyhkYmk31QaLbQaeDDIg9Gd6tGjsYc8u5OqjAw5I1At5ACu/KmMe824AtYOMHQpNB1YvTVUUG5BIaEnE/n2SCxHLqYUv+7jbMsTHerxRAc/vJzvfRibJJWmMn+fqt85nj17NhqNpsQjKCioQp9dt24dGo2GIUOGlHh9woQJd+yzf//+VVB9FfFtC1P3gn83yMtSAi/sHeWS1sTY6HUMbu3Ld093ZucrPZjcrQEu9lbEp+fw0c6zdP3vLqasPsruM0kUGuS/p1L1M4mR282bN2fnzp3Fz/X6u5cVExPDjBkz6N69e6nv9+/fn6+++qr4uY2Nzf0XWp0c3GHcRtj+Fhz5DPYthITjyn06u9pqV1eqRh4OvPloM2b0a1J8dvd7TAo7oq6yI+oqvi52jO/iz6iO9XC0tVK7XKmGMImQ0+v1eHl5VXj7wsJCxowZw9tvv82+fftIS0u7YxsbG5tK7dMk6azg4fng0wZ+eRHO74Avein96Tybq11dmWytdAxp48uQNr6cu5rJt7/H8sOxOK6k3eS9radZHHaeUZ3q8VTX+rKhQqpyql+uApw7dw4fHx8CAgIYM2YMsbGx5W4/Z84cPDw8mDRpUpnb7NmzBw8PD5o0acKzzz5LcnJyufvMzc0lIyOjxMNktHoCJm0H53rK/HTL+8DJH9WuqkICPR0JGdic32f14b/DWtLIw4HM3AK++O0C3f+7m1e+Cycq3oSOtWRxVG94+PXXX8nKyqJJkyYkJCTw9ttvc+XKFU6ePImjo+Md2+/fv5+RI0cSHh6Om5sbEyZMIC0tjY0bNxZvs27dOuzt7WnQoAHR0dG88cYbODg4cOjQIXS60vuezZ49m7fffvuO11VpeCjLjWT4YSJc2KM87zodHvwP6EzihLxCDAbBnrNJfL73QomGiu6Bbkx9IIBujdzkzCjSXZl162paWhr+/v58+OGHd5ypZWZmEhwczKeffsrDDz8MUGrI3e7ChQs0bNiQnTt30rt371K3yc3NJTf3n86uGRkZ+Pn5mVbIARQWQNjb//SnC+gJj39V6joSpu745TSW7bvA1ogEitokmno7MfWBBjwa7CNHVEhlMuuQA+jQoQN9+vRh3rx5JV4PDw+nTZs2Jc7GDH+vXK/Vajlz5gwNGzYsdZ/u7u7MnTuXp59+ukI1qNqFpCJO/gA/Pw/52cpl7MivVRn3agyXU7JZsf8i649eLp4ZxdvZlsndAxjV0Q97a/M5U5Wqh1l1IbldVlYW0dHReHt73/FeUFAQERERhIeHFz8GDRpEr169CA8Px8/Pr9R9xsXFkZycXOo+zVaLYTB5J9SuD+mxsKIfnPhe7aruiZ+rPbMHNefgaw8ys18T3BxsSEjP4Z3NUXSdv4v/7TxHWvadq5hJUkWofiY3Y8YMBg4ciL+/P/Hx8YSEhBAeHk5UVBTu7u6MGzcOX1/fO87qitx+uZqVlcXbb7/NsGHD8PLyIjo6mn//+99kZmYSERFR4a4kJn8mV+RmqjKTyfm/u+D83zToO8es7tPdLie/kB//vMLnv0Vz6e+ZjWtZ6xjdqR6Tuwfg6SQ7F9d0ZnUmFxcXx6hRo2jSpAkjRoygTp06HD58GHd3ZW612NhYEhISKrw/nU7HiRMnGDRoEI0bN2bSpEm0a9eOffv2mV9fuYqwqw2j10P3fynPDy+BNUPgxnVVy7oftlZKoIW90oNFo9rQ1NuJG3mFLNt3ke7/3c1rP5zg4vUbapcpmQnVz+RMldmcyd0qahNsfFYZJeFUV7lP59NG7arumxCCPWeu8eme8/wRkwoo42QfbunNsz0a0sLXNCYxkKqP2Tc8mAKzDDmApNOwbrQyP53OBgb+D1qPUrsqo/kjJoXP9kSz63RS8Wu9gzz410NNaOZjRv+dpPsiQ84IzDbk4O/1Xp+Gs6HK845PQ793lREUFuJUQgaf7Ylm84n44u4nA4K9eaVvYxq6O6hbnFTlZMgZgVmHHCjrve6dD3v/qzz37wrDV4KDh6plGduFa1l8tPMcvxyPB5TL2Mfa1mV670D8XO1Vrk6qKjLkjMDsQ67I6S3w49PKOhJOvvDEGvBtp3ZVRncqIYMPtp9l56mrgLKo9qiO9Xi+VyM8ZGusxZEhZwQWE3IA187Cd2Pg+lnlPt2AD6DtWLWrqhJ/xabywfaz7D+vtC7b6LWM71KfZ3o0xLVW1S/7KFUPGXJGYFEhB5CTAT89A2e2KM+rcb1XNRyKTmbh9jMcu6S0xjrY6JnUrQFTHgjAwcZ8+xBKChlyRmBxIQfKfbp9C2H3e4AAv04wYjU4mvmUVGUo6nry/rYzRCUoM524Odjwct9Anmjvh16OjTVbMuSMwCJDrsjZ7cooidx0cPBSgq5eJ7WrqjIGg+DXk4m8v+00MX+PoGjk4cBr/YPo3dRDznpihmTIGYFFhxxAcrQyrXpSFGit4OH/QvuJYMF/8HkFBr49con/hZ0jNVtZfKdTA1dmDWhKcF0XdYuTKkWGnBFYfMgB5GbBz9MgaqPyvM2T8MgHYGXZrZEZOfl8ujuaLw9cJK9AmcVmcGsfZjzURHY7MRMy5IygRoQcgBDK3HQ7Z4MwgE9bpZuJc121K6tyV9Ju8sG2M/z41xUArHVanupan+d6NcLZznI6TlsiGXJGUGNCrkj0LtgwUZnVxN4NRqyC+t3UrqpanLySzntbT3EwWpki38Xeium9Axn7f/6yccJEyZAzghoXcgCpMcp9usQI0Oig33vQ6WmLvk9XpKgl9r2tpziXlAVAY08HZg9qTpeGbipXJ91OhpwR1MiQA8jLhs0vwYnvlOetRsOjH1n8fboiBYUG1v1xmQ+2nylunBjQ0ps3BjTF10WuLGYqZMgZQY0NOVDu0x3+DLa/CaJQma7pia9rxH26ImnZeXyw/SzfHLmEQYCtlZZpPRsx5YEAbK1KXwxJqj4y5IygRodckQt74Pun4GYK1HKH4augfle1q6pWUfEZzN4Uye8xyspifq52vDWgGX2becr+dSqSIWcEMuT+lnoJ1o2BqxGg1StDwTpMrhH36YoIIdh0PJ55W0+TmJEDwAON3QkZ2ExO66QSGXJGIEPuFnnZsOl5ZYUwqDH96W53I7eAJbvPs3zfRfIKDei1GiZ2a8D03oHUkuNhq5UMOSOQIXcbIeDgYtgZovSn822n3Kdz8lG7smp38foN3tkcVTw7sY+zLXMGt6BPM0+VK6s5ZMgZgQy5MkTvUu7T5aRBLQ9l3Kt/Z7WrUsWu01cJ2RTJ5ZSbAPRv7sXsQc3xcq5ZZ7hqMKvVuiQz0/BBmLoHPJrDjSRY9Sj8sVw506thHgzyZPtLPXi6RwA6rYbQyET6fLiX1YdiKDTUvONhquSZXBnkmdxd5N2Ajc/VuHGvZTmVkMHrP0YQfjkNgNZ+Lrw3tKVcXKeKyMtVI5AhVwFCwIGPIWzO3+Ne28CINeDip3Zlqig0CL49cokFoWfIzC1Ap9UwuVsDpvcJxN5aNkwYkww5I5AhVwklxr3WURbMafCA2lWp5mpGDrM3RfLryUQA6ta2Y+6QFvRsYlmLCKlJ3pOTqlfDB2HqXvAKhuxkWD0EDn5SI+/TAXg62fLZk+1YPq49Ps62xKXeZMJXf/DC2r9IzspVu7waR4acZBy1/WHSdggeqQwF2z4Lfpik3Lurofo082THKz2Y1K0BWg38cjyevh/9xi/H45EXUNVHXq6WQV6u3iMh4PcvYNsbYChQWmFHfg2uAWpXpqoTcWn8e8MJTidmAtCvuSfvDGmBh2PNbKi5X/JyVVKPRqNMzzT+F6UfXVIkfNETzu1QuzJVBdd1YdPz3XixdyB6rYZtkVfp++Fv/PhnnDyrq2Iy5KSq4d8Fnt4LdTtATjp8Mxz2LlBWDKuhrPVaXunbmE3Pd6O5jxPpN/N5Zf1xJq06SmJ6jtrlWSwZclLVcfKBCVuUBXIQsPtdZVLOnAy1K1NVMx8nNk7rysx+TbDWadl1Oom+H+7luz9i5VldFZD35Mog78kZ2Z9rYMsrUJgHbk1g1Fqo01DtqlR39momMzec4PjfnYi7B7ox77GW1K0tF9Qpj7wnJ5metmPhqV/B0Ruun4EvetX4+3QAjT0d+fHZLrzxSBA2ei37zl2n30e/sfZ3eVZnLDLkpOpTt73Sn86vk7Kw9TfDYd+HNbY/XRGdVsPUBxry6/TutPevzY28Ql7/MYIpq49xXfaru28y5KTq5eiptLy2HQ8ICHsbNjxVo/vTFQlwd2D9052Z9UhTrHVadp66Sr+PfmNn1FW1SzNrMuSk6qe3gUGLlAVytHqI/AlWPKSsFlbDabUapjwQwM/Pd6WJpyPJN/KYvPoor/94ghu5BWqXZ5ZkyEnqaT8Rxm9W+tNdPan0p7uwR+2qTEJTbyd+fr4rU7o3QKOBtb9fZsCiffwZm6p2aWZHhpykLv/Oyvx0Pm2UAf5rhsKhJTX+Ph2ArZWOWQOa8c3kTvg42xKTnM3jnx3kw+1nyC+suf0NK0uGnKQ+Z194KlRZ41UYlCFhPz0N+TfVrswkdGnoxq8vPcCQ1j4YBCzadZ7HPztI9LUstUszCzLkJNNgZQtDPoX+/wWNTlnc+sv+kB6ndmUmwdnOio9HtmHxqDY42eo5HpfOgEX7WHP4kuxqchcy5CTTodHA/z0D4zaCnSskhCv36S4dUrkw0zGwlQ/bXn6Aro3qkJNv4K2NJ3l6zTHSsvPULs1kyZCTTE+DB5T7dJ4t4MY1WDUQjn6pdlUmw9vZjjUTO/HmgKZY6TRsj7rKI//bx9G/F8CWSpIhJ5mmovnpmg8FQz5sfll5FMgzFlC6mkzuHsCPz3alfh174tNzeOKLw3yy65xcROc2MuQk02VdCx7/Cnr/B9AoZ3OrB0FWktqVmYyWdZ3Z/GJ3hrT2odAgWLj9LGNXHCEpQ85qUkSGnGTaNBro/i8Y/R3YOEHsIeU+3ZU/1a7MZDjY6Pnoida8/3gwdlY6DkYn8/D/9rHnjPzHACo4C8mmTZsqveO+fftiZ2d3T0WZAjkLiQm6fg7WjoLkc6C3hUGLIXiE2lWZlPNJWbyw9i9OJSjTWU19IIAZDzXBWm9Z5zNGX61Lq63cAdJoNJw7d46AAPOd8lqGnInKSYcfpsC5bcrzLi9An7dBq1O3LhOSk1/IvK2nWHXoEgCt/FxYPLIN9epYzvRNVTLVUmJiIgaDoUIPe3vLOZiSibF1Vuai6/4v5fnBxfDN48poCQlQRkq8PbgFn49th7OdFccvpzFg0T62nEhQuzRVVCjkxo8fX6lLzyeffFKe/UhVR6tTGiOGrwQre2Xd12UPQtJptSszKf2ae7H17+mbMnMLmPbtn4T8fJLcgkK1S6tWcmbgMsjLVTORGAFrR0N6LFg7wrBl0ORhtasyKQWFBj7ccZZP90QDEFzXmSWj2+Lnar5XXNU+M3BhYSEbN25k8ODBxtidJFWcV0uYuhvqd4e8TKVh4rf35QD/W+h1Wv7dP4ivJnTAxd6KE38PCdsemah2adXivkLuxIkTvPzyy/j4+DBq1Cg0Go2x6pKkiqvlBmN/gg5TAAG75sL3E+REnLfpFeTBlhe706aeCxk5BUxdc4y5m6MsfkaTu4bcjRs3KCz85xo+JSWFxYsX065dO9q1a8eiRYuYNWsW165dY+PGjZUuYPbs2Wg0mhKPoKCgCn123bp1aDQahgwZUuJ1IQT/+c9/8Pb2xs7Ojj59+nDu3LlK1yaZEZ0VDFgIA/8HWiuI2vj3RJyX1K7MpPi62PHd1M5M7tYAgOX7L/LE54eIT7PcGV/KDblly5YRGBhImzZt+Oijj3j88cfx8fFh9erVjBs3jri4OLRaLX369MHBweGei2jevDkJCQnFj/3799/1MzExMcyYMYPu3bvf8d6CBQtYtGgRS5cu5ciRI9SqVYt+/fqRkyN7gVu8dhNgwmao5f7PRJwX96ldlUmx1mt589FmLH2yHY62ev6MVVpfd1tq52FRDj8/P3H48GGRlJQkdDqdeOmll8Tp06dLbKPX60VkZGR5uylXSEiIaNWqVaU+U1BQILp06SKWL18uxo8fLwYPHlz8nsFgEF5eXuL9998vfi0tLU3Y2NiItWvXVvg70tPTBSDS09MrVZtkItIuC7H0ASFCnISYXVuII18IYTCoXZXJuXT9hhiw6Dfh/+pm4f/qZrEg9JTILyhUu6y7qszfZ7lnco0bN+b777/n66+/xsXFhWXLljFnzhxCQ0ONOofVuXPn8PHxISAggDFjxhAbG1vu9nPmzMHDw4NJkybd8d7FixdJTEykT58+xa85OzvTqVMnDh0qe8qe3NxcMjIySjwkM+ZcFyaGQsvhIAph6wz45UUokKtf3apeHXs2PNOFsf/nD8CS3dGMWX6Ea5mWc5zKDblvv/0WvV5Pbm4uV65c4cCBA3h4eDBu3Di8vb2ZPn06wH01OHTq1ImVK1cSGhrKZ599xsWLF+nevTuZmZmlbr9//35WrFjBsmXLSn0/MVFpMfL09CzxuqenZ/F7pZk3bx7Ozs7FDz8/v3v8iSSTYWUHjy2DvnMADfy5GlbJAf63s7XS8c6QFiwe1YZa1jqOXExh4OL9xQtem717OVXMz88XP/zwgxg4cKCwsrISgYGB4vXXXxfHjh27l92VkJqaKpycnMTy5cvveC8jI0PUr19fbN26tfi12y9XDxw4IAARHx9f4rPDhw8XI0aMKPN7c3JyRHp6evHj8uXL8nLVkpzdLsR7fsrl6wfNhLjyl9oVmaTzSZniwYW7hf+rm0XgrK1i/R+xapdUKqNdrpZFr9fz2GOPsWnTJi5fvsyUKVP4+eef6dChw32HrouLC40bN+b8+fN3vBcdHU1MTAwDBw5Er9ej1+tZvXo1mzZtQq/XEx0djZeXFwBXr5Zcq/Lq1avF75XGxsYGJyenEg/JggT2hSlhUKcRZMQpU6uf/FHtqkxOQ3cHNk7rSp+mnuQVGJi54QSzN0WadTeT++4M7OnpycyZM4mMjCz3nldFZWVlER0djbe39x3vBQUFERERQXh4ePFj0KBB9OrVi/DwcPz8/GjQoAFeXl6EhYUVfy4jI4MjR47QuXPn+65PMmNugTA5DBr2hoKbyqLWYe+AwXz/gKuCo60VX4xtx0t9AgFYeTCGMcuPcD3LPO/TVSjkTpw4gaECvwgdO3YEIDIykoKCii2EO2PGDPbu3UtMTAwHDx5k6NCh6HQ6Ro0aBcC4ceN4/fXXAbC1taVFixYlHi4uLjg6OtKiRQusra3RaDS89NJLzJ07l02bNhEREcG4cePw8fG5oz+dVAPZucCY76Hz88rzfQvhuycht/R7wDWVVqvhpT6N+WJsOxxs9Px+MYVBi/cTEZeudmmVVqGQa9OmDcnJyRXeaefOne/aQlokLi6OUaNG0aRJE0aMGEGdOnU4fPgw7u7uAMTGxpKQULnZE/7973/zwgsvMHXqVDp06EBWVhahoaHY2tpWaj+ShdLqoN+7MGQp6GzgzBZY3hdSLqhdmcl5qLkXG6d1JcCtFvHpOQxbepAfjpnXCmoVnk9u6tSpFZ5C6dNPPyUqKkrOJyeZvrijsG4MZCWCXW0YvgoCeqhdlcnJyMnn5XXhhJ1WWqaf6lqfNx5pipVOnck4jT5pZs+ePSvdTeTbb78t9b6auZAhV4NkJMC60RD/p7Lma//50HGKMvW6VMxgEHy88yyLdimNgp0D6vDJ6DbUcbCp9lqMHnI1kQy5Gib/JvwyXVnUGqDteHhkIeit1a3LBIWeTORf68O5kVdI3dp2rBjfgSZejtVaQ7VPtSRJZs/KDoZ+fkvH4VWwejDcuK52ZSanfwvlPl39OvbEpd5k2GcH2X3adDtYy5CTpCIaDXSdDqPX/70y2EH4ohcknlS7MpMT6OnIT8915f8CXMnKLWDSqj9Yvu+CUYd7GosMOUm6XeOHYPJOcA1QZhxe8RCc+kXtqkxO7VrWrJ7YiVEd/TAImLvlFG/8FEFegWn1O5QhJ0mlcW+idBwO6An5N5S+dHvljMO3s9ZreW9oS956tBlaDaz9/TLjvjxC6o08tUsrJkNOkspi7wpjfoCOTyvPd8+FDRMhL1vdukyMRqNhUrcGLB/fHgcbPYcvpDD00wOcT8pSuzTgPlpXo6KiiI2NJS+vZGIPGjTIKIWpTbauSiUcWwlb/gWGAvBuDSO/BWdftasyOWcSM5m06g/iUm/iaKvn0zFt6R7obvTvqdIuJBcuXGDo0KFERESg0WiKbzQW9aO7dap0cyZDTrpDzAFYPxayk6GWhxJ0fvc/KYWluZ6VyzNrjnH0Uio6rYaQgc0Y17m+Ub+jSruQTJ8+nQYNGpCUlIS9vT2RkZH89ttvtG/fnj179txrzZJk+up3hSm7waM53EiClY9A+Fq1qzI5bg42fDOlE4+19aXQIPjPz5H85+eTFKg0k0mlQ+7QoUPMmTMHNzc3tFotWq2Wbt26MW/ePF588cWqqFGSTEdtf5i0HYIehcI82PgMbH8LDJZxBWMsNnodHwxvxav9g9BoYPWhS0xadZSs3IpN3GFMlQ65wsJCHB2V3s1ubm7Ex8cD4O/vz5kzZ4xbnSSZIhsHGLEGHpipPD+4SBkWJmcyKUGj0fBsz4Z8NqYdtlZa9p69xoilh0hMr94FpSodci1atOD48eOAMnX5ggULOHDgAHPmzDHrAfmSVClaLTz4Jgxbocxkcjb07yUQY9SuzOT0b+HFuqmdcXOwJiohg6GfHuBUQvWtoVLpkHvzzTeL55abM2dO8ZoMW7duZdGiRUYvUJJMWsvH4alfwcETkqJg2YNw6aDaVZmc1n4u/PRcVxq61yIhPYfhSw/x29lr1fLdRhmgn5KSQu3ate9rQRtTI1tXpUpJvwLrRkHCcWVx60c/grZj1a7K5KRn5zN1zVGOXExBp9Xw3tAWPNGhXqX3U+0D9F1dXS0q4CSp0px94alQaDYEDPmw6XnYNks2SNzG2d6K1ZM6MrSN0vL66g8RLNx2pkrHvMoRD5JkLNb28PhX0OM15fmhT2DtSMiRa/jeykav48MRrXjxwUYAfLL7PC99F05uQdX8gyBDTpKMSauFXq8rYae3hXPbYUVfSLmodmUmRaPR8MpDTVjweDB6rYafw+MZu/x30rKNP+ZVhpwkVYUWjykNEo7ecO200iARs1/tqkzOiPZ+rHyqI442en6PSeGxzw4Sm2zcscEy5CSpqvi2VUZI+LSFmynKJJx/rla7KpPTLdCNDc92wcfZlgvXbjD00wNcvH7DaPuXISdJVcnJG57aCs0fUwb3b3pBNkiUoomXIz9N60oLXyda+DrjV9vOaPuWazyUQXYhkYxKCNgzH/bOV5437g/DloNN9a6NYOpu5BYgAAcbfbnbyTUeJMnUaDRKg8SwFUqDxNlQWNEP0iq2PnFNUctGf9eAqywZcpJUnVo+DhO2KFM1JUUqDRKXf1e7KosmQ06Sqlvd9jB1N3i2hBvXYOWjcGK92lVZLBlykqQG57owMRSaDIDCXPhxCuyaCwbTWgTGEsiQkyS12DjAE18ryyAC/PY+bJgg15AwMhlykqQmrVZZ0Hrwp8rA/qif4auHISNB7coshgw5STIFbcbA+E1g5woJ4UqDRMJxtauyCDLkJMlU+HeBKbvArTFkxsOXD8PprWpXZfZkyEmSKXFtAJN2/LOo9brRcPATuaj1fZAhJ0mmxs4FxmyAdhMAAdtnweaXoDBf3brMlAw5STJFOit49GN46F1Aoyxu/c3jcDNN3brMkAw5STJVGg10eV5ZxNqqFlzY8/fcdBfUrsysyAH6ZajoAODCwkLy8+VlhARWVlbodLqq2XnCCfj2CaVBws5VCT7/zlXzXWagMgP0ZciV4W4HUQhBYmIiaWlp1V+cZLJcXFzw8vKqmjVPMhKU6dQTwkFnDYM+gVZPGP97zEBlQs64w/1rkKKA8/DwwN7eXi7kU8MJIcjOziYpKQkAb29v439J0dx0Pz0Np36Bn6ZC8jnoNUu5tJVKJUPuHhQWFhYHXJ06ddQuRzIRdnbKRI9JSUl4eHhUzaWrdS0YvhrC3oYDHytDwVIuwuAlYGVr/O+zALLh4R4U3YOzt7dXuRLJ1BT9TlTpfVqtFvq+rVyuavVwcoMytfqN61X3nWZMhtx9kJeo0u2q9Xei7Vh48gewcYbLh2F5H7h+rvq+30zIkJMkcxbQEybvABd/SL2oBN3FfWpXZVJkyEmSuXNvApPDoG4HyEmDNUMhfK3aVZkMGXKSZAkc3GH8L9B8KBjyYeMzsOtdOeYVGXI1UmJiIi+88AIBAQHY2Njg5+fHwIEDCQsLU7u0YteuXePZZ5+lXr162NjY4OXlRb9+/Thw4IDRvuPy5ctMnDgRHx8frK2t8ff3Z/r06SQnJxvtO6qVlR0M+xK6vaI8/20B/DAZ8nPUrUtlsgtJDRMTE0PXrl1xcXHh/fffp2XLluTn57Nt2zamTZvG6dOn1S4RgGHDhpGXl8eqVasICAjg6tWrhIWFGS2ALly4QOfOnWncuDFr166lQYMGREZGMnPmTH799VcOHz6Mq6urUb6rWmm10CcEXAOUQf0nN0B6nDJColYN7e4kpFKlp6cLQKSnp9/x3s2bN0VUVJS4efOmCpXdn4cfflj4+vqKrKysO95LTU0VQghRWFgo3nvvPVG/fn1ha2srgoODxffff19tNaampgpA7Nmzp9ztevToIV544QUxc+ZMUbt2beHp6SlCQkIq9B39+/cXdevWFdnZ2SVeT0hIEPb29uKZZ565p9pN6ncjercQ7/kJEeIkxMethLh2Vu2KjKa8v8/byctVIxBCkJ1XoMpDVOKeS0pKCqGhoUybNo1atWrd8b6LiwsA8+bNY/Xq1SxdupTIyEhefvllnnzySfbu3WusQ1YuBwcHHBwc2LhxI7m5ueVuu2rVKmrVqsWRI0dYsGABc+bMYceOHeV+JiUlhW3btvHcc88Vd+At4uXlxZgxY/juu+8qdWxNUnHLa71/Wl5jjHe5by7k5aoR3MwvpNl/tqny3VFz+mFvXbH/jOfPn0cIQVBQUJnb5Obm8t5777Fz5046d1YGgAcEBLB//34+//xzevToAcDQoUPZs2cPvXv3ZsOGDff/g9xCr9ezcuVKpkyZwtKlS2nbti09evRg5MiRBAcHl9g2ODiYkJAQAAIDA/nkk08ICwujb9++Ze7/3LlzCCFo2rRpqe83bdqU1NRUrl27Rm5uLmPHjiUpKQm9Xs9bb73F8OHDjffDVjX3JjB5lzLm9cpRWDNEWU8i2Ix+hvskz+RqkIqcmZw/f57s7Gz69u1bfEbl4ODA6tWriY6OLt5u+vTprF69uspqHTZsGPHx8WzatIn+/fuzZ88e2rZty8qVK0tsd3voeXt7F48fvZuKHA+9Xs/HH39MVFQU27dv56WXXuLGjRsV/jlMgoM7TNgMTQdCYR78OFkZDmbuZ6oVJM/kjMDOSkfUnH6qfXdFBQYGotFoym1cyMrKAmDLli34+vqWeM/Gxqb4//fs2ZM9e/ZUrthSdO3alQ8//JBOnToxadIkWrRowcsvvwyAra0tffv2pW/fvrz11ltMnjyZkJAQJkyYUPx5KyurEvvTaDQYblm7tLT9jxs3Do1Gw6lTpxg6dOgdNZ06dYratWvj7u6ORqMpHmzv5eWFm5sbKSkppV7umzQrO2XM64634NAnyhqvqTHKxJw6q7t92qypfiY3e/ZsNBpNiUd5l1M//vgj7du3x8XFhVq1atG6dWvWrFlTYpsJEybcsc/+/ftX2c+g0Wiwt9ar8qjMMCJXV1f69evHkiVLSj0bSUtLo1mzZtjY2BAbG0ujRo1KPPz8/Ix52AB46623mD9/Ph9++CFarbY44ErTrFmzSp9Flbb/OnXq0LdvXz799FNu3rxZYvvExES++eYbnnjiiTuO7bFjxygsLKyS41AttFro9y48shA0Wvjra/hmOOSkq11ZlTKJM7nmzZuzc+fO4ud6fdllubq6MmvWLIKCgrC2tmbz5s089dRTeHh40K/fP2dT/fv356uvvip+futZSE22ZMkSunbtSseOHZkzZw7BwcEUFBSwY8cOPvvsM06dOsWMGTN4+eWXMRgMdOvWjfT0dA4cOICTkxPjx4+v0Pe0bt2agoKCO17fvn07Pj4+xc/79+/PrFmz2LJlC6GhoQAkJyczfPhwJk6cSHBwMI6Ojhw9epQFCxYwePDgSv28pe0f4JNPPqFLly7069ePuXPnluhC4uvry7vvvltiPykpKYwbN45ly5ZV6vtNUscp4OwHGybChd3wZX8YvR5czDS878IkQk6v1+Pl5VWhbXv27Fni+fTp01m1ahX79+8vEXJFHUilkgICAvjzzz959913+de//kVCQgLu7u60a9eOzz77DIB33nkHd3d35s2bx4ULF3BxcaFt27a88cYbFf6e8PDwCm33xx9/kJKSgr+/f/Glp4ODA506deKjjz4iOjqa/Px8/Pz8mDJlSqVqKGv/oFy6Hz16lJCQEEaMGEFKSgpeXl4MGTKEkJCQEn3kcnNzGTJkCK+99hpdunSp1PebrCb9lbnpvn0CkqKUltfR34FPa7UrM76q7MtSESEhIcLe3l54e3uLBg0aiNGjR4tLly5V6LMGg0Hs3LlT2Nvbi+3btxe/Pn78eOHs7Czc3d1F48aNxTPPPCOuX79e7r5ycnJEenp68ePy5csW2U/OmHbv3i2GDRt2z5+Pi4sTwcHB4sKFC6JNmzYiIiLCiNUZZ/8Gg0GMHDmywv3vzO53IzVWiCX/p/Slm+stxOlf1a6oQirTT071kNu6datYv369OH78uAgNDRWdO3cW9erVExkZGWV+Ji0tTdSqVUvo9XphY2MjVqxYUeL9tWvXip9//lmcOHFC/PTTT6Jp06aiQ4cOoqCgoMx9hoSECOCOhwy50vXu3Vu4ubkJOzs74evrKw4ePFipz2dnZ4vOnTuLvXv3CiGEWL9+vRgxYoTR6jPW/vft2yc0Go1o1apV8ePEiRNlbm+Wvxs304RYNUgJutkuQhz5Qu2K7sqsQu52qampwsnJSSxfvrzMbQoLC8W5c+fEX3/9JRYuXCicnZ3F7t27y9w+OjpaAGLnzp1lbiPP5CRjMNvfjYI8ITY+pwRdiJMQ22YJUViodlVlqkzImcQ9uVu5uLjQuHFjzp8/X+Y2Wq2WRo0aAcoN7lOnTjFv3rw77tcVCQgIwM3NjfPnz9O7d+9St7GxsZGNE1LNpbNSZhquXV/pXnJwMaRfgSGfmf206qp3IbldVlYW0dHRlVoIxGAwlDv8Jy4ujuTk5KpZXESSLIVGAw/MhKGfK9OqR/4IXz8G2SlqV3ZfVA+5GTNmsHfvXmJiYjh48CBDhw5Fp9MxatQoAMaNG8frr79evP28efPYsWMHFy5c4NSpU3zwwQesWbOGJ598ElBCcubMmRw+fJiYmBjCwsIYPHgwjRo1KtH6KklSGVqN/HtadSe4dAC+7Aepl9Su6p6pfrkaFxfHqFGjSE5Oxt3dnW7dunH48GHc3d0BiI2NRav9J4tv3LjBc889R1xcHHZ2dgQFBfH111/zxBPK+pM6nY4TJ06watUq0tLS8PHx4aGHHuKdd96Rl6OSVFEBPWFiqNJZ+PpZWNH37y4mbdSurNLk4tJlKG/x2pycHC5evEiDBg2wtTXv+xWScVnc70ZGvBJ0V0+CVS0YvhIaP6R2VZVaXFr1y1VJkkyYkw889SsE9IL8G8psJsdWql1VpciQkySpfLZOMOZ7aDUaRCH8Mh3C3jGbWUxkyEmSdHc6KxjyKfR4VXm+byH89AwU5KlbVwXIkJMkqWI0Guj1htKfTqODE+vgm8chJ0PtysolQ06SpMppOxbGrAdrB7i4F756BDIS1K6qTDLkaiBTX5Jwz549d8wHeOujV69e97V/i1uKUA2N+sCELVDLA65GKF1Mrp1Ru6pSyZCrYWJiYmjXrh27du3i/fffJyIigtDQUHr16sW0adPULg+ALl26kJCQcMfj888/R6PR8Nxzz5X52by88u8RXbhwgfbt23Pu3DnWrl3L+fPnWbp0KWFhYXTu3JmUFPPu3V+tfForC+XUaQTpl2HFQxB7WO2q7lTlI2nNlFySUL0lCUsTFRUlHB0dxaxZs0q83qNHDzFt2jQxffp0UadOHdGzZ89y91NVSxEWMeffjXuWdV2IZb2Vgf3veAgRtanKv9KsZyExFZUKOYNBiNwsdR4GQ4V/puTkZKHRaMR7771X7nZz584VQUFBIjQ0VERHR4uvvvpK2NjY3HUd1KqSmpoqAgMDxcCBA4Xhtp+3R48ewsHBQcycOVOcPn1anD59usz93O3nnzJliqhdu/Yd31EZNTLkhBAi94YQ3478exYT5yqfrsmsZyExS/nZ8J7P3berCm/Eg3XFFlUxlyUJb2UwGBg9ejR6vZ5vvvmm1DUtAgMDWbBgwV33VaOWIqxu1vYwYg1snQHHvlL+NyMeev9HaZVVkQy5GkRUcknCW+Xl5dGmzT/jFqdPn87EiRNZtWqV0eu81RtvvMGhQ4f4/fffcXR0LHWbdu3aVWqfFTkORUsRtm7dmsTERNq1a8cjjzxifqt0VSedHh79CJx8Yfdc2P8hZCbAwEWgt1atLBlyxmBlr5xRqfXdFWRuSxKuW7eOhQsXsmXLFgIDA8vcx+3BU9Y+GzVqVPOWIqxuGg30mAlO3rDpRTi+FrKuwojVYFP6P1JVTYacMWg0Fb5kVNOtSxK++OKLd/zB3r4kYdGlaVUqWjKwe/fuJZYkDA8PZ9KkScyfP7/SU2SVtc9blyJ8+eWXsbOzK/5M0VKERWuy3srslyJUQ5snwcET1o+H6F1KX7oxG8DRs/prqdK7g2bMUltXo6OjhZeXl2jWrJnYsGGDOHv2rIiKihL/+9//RFBQkBBCiFmzZok6deqIlStXivPnz4tjx46JRYsWiZUrV5bYV3kL2bRq1Uo0b978jseVK1fu2LZt27biwQcfFHl5eUIIIa5duyb8/f3FI488IhISEu54JCUlFX+2R48eYvr06XfdZ5GzZ88KNzc30b17d7F3714RGxsrfv31V9GiRQsRGBgokpOTS2yfnJwsmjVrJg4cOHD3gyvM+3ejSsQdFeK/AUqDxEcthbh+3ii7lQ0PUpnMYUnCLVu2cOnSJS5dulTqbM7+/v7ExMRUap9FavxShNXNt53Sl27NUEiNUfrSPbmheuelM0qsWiBLPZMzJlNcktBY+6zsUoRF5O9GGTKvCrG0u3JG966PEOfD7mt3sp+cEciQK58pLklozH1WdinCIvJ3oxw5GUKsHKgE3dt1hDhx7x3MKxNycmbgMsiZgaV7IX837qIgV5miKfJH5Xm/edC57GF6ZZEzA0uSZJr0NjBsBXR6Rnm+7XXYEVKlE3DKkJMkqXpptdB/PvQOUZ4f+Bg2PgeF+VXzdVWyV0mSpPJoNND9FRi8RJmA8/i3sG405N0w+lfJkJMkST1tnoSR34LeDs5th1WDjL6YtQy5+yDbbKTbyd+Je9CkP4zfBLYucOWosph1+hWj7V6G3D0o6mCanZ2tciWSqSn6nbi9E7J0F34dYeI2ZXC/3tao41zliId7oNPpcHFxISkpCQB7e/tSpwCSag4hBNnZ2SQlJeHi4oJOp1O7JPPjEQSTdoBWryyDaCQy5O6Rl5cXQHHQSRKAi4tL8e+GdA+cfe++TSXJkLtHRVPxeHh4kJ9fNU3fknmxsrKSZ3AmSIbcfdLpdPIXW5JMmGx4kCTJosmQkyTJosmQkyTJosl7cmUo6tSZkZGhciWSJN2u6O+yIp2vZciVITMzE0DO6y9JJiwzMxNnZ+dyt5HzyZXBYDAQHx+Po6PjXTv6ZmRk4Ofnx+XLl+86t5V0d/J4Gp+lHVMhBJmZmfj4+KDVln/XTZ7JlUGr1VK3bt1KfcbJyckifoFMhTyexmdJx/RuZ3BFZMODJEkWTYacJEkWTYacEdjY2BASElJihXnp3snjaXw1+ZjKhgdJkiyaPJOTJMmiyZCTJMmiyZCTJMmiyZCTJMmiyZCroCVLllC/fn1sbW3p1KkTv//+e7nbf//99wQFBWFra0vLli3ZunVrNVVqHipzPJctW0b37t2pXbs2tWvXpk+fPnc9/jVRZX9Hi6xbtw6NRsOQIUOqtkC1COmu1q1bJ6ytrcWXX34pIiMjxZQpU4SLi4u4evVqqdsfOHBA6HQ6sWDBAhEVFSXefPNNYWVlJSIiIqq5ctNU2eM5evRosWTJEvHXX3+JU6dOiQkTJghnZ2cRFxdXzZWbrsoe0yIXL14Uvr6+onv37mLw4MHVU2w1kyFXAR07dhTTpk0rfl5YWCh8fHzEvHnzSt1+xIgRYsCAASVe69Spk3j66aertE5zUdnjebuCggLh6OgoVq1aVVUlmp17OaYFBQWiS5cuYvny5WL8+PEWG3LycvUu8vLyOHbsGH369Cl+TavV0qdPHw4dOlTqZw4dOlRie4B+/fqVuX1Nci/H83bZ2dnk5+fj6upaVWWalXs9pnPmzMHDw4NJkyZVR5mqkQP07+L69esUFhbi6elZ4nVPT09Onz5d6mcSExNL3T4xMbHK6jQX93I8b/fqq6/i4+Nzxz8kNdW9HNP9+/ezYsUKwsPDq6FCdcmQk8zK/PnzWbduHXv27MHW1lbtcsxSZmYmY8eOZdmyZbi5ualdTpWTIXcXbm5u6HQ6rl69WuL1q1evlrm+ppeXV6W2r0nu5XgWWbhwIfPnz2fnzp0EBwdXZZlmpbLHNDo6mpiYGAYOHFj8msFgAECv13PmzBkaNmxYtUVXI3lP7i6sra1p164dYWFhxa8ZDAbCwsLo3LlzqZ/p3Llzie0BduzYUeb2Ncm9HE+ABQsW8M477xAaGkr79u2ro1SzUdljGhQUREREBOHh4cWPQYMG0atXL8LDwy1vNmy1Wz7Mwbp164SNjY1YuXKliIqKElOnThUuLi4iMTFRCCHE2LFjxWuvvVa8/YEDB4RerxcLFy4Up06dEiEhIbILyS0qezznz58vrK2txYYNG0RCQkLxIzMzU60fweRU9pjezpJbV2XIVdDixYtFvXr1hLW1tejYsaM4fPhw8Xs9evQQ48ePL7H9+vXrRePGjYW1tbVo3ry52LJlSzVXbNoqczz9/f0FcMcjJCSk+gs3YZX9Hb2VJYecnGpJkiSLJu/JSZJk0WTISZJk0WTISZJk0WTISZJk0WTISZJk0WTISZJk0WTISZJk0WTISZJk0WTISZJk0WTISZJk0WTISZJk0WTISRZt7dq12NnZkZCQUPzaU089RXBwMOnp6SpWJlUXOUBfsmhCCFq3bs0DDzzA4sWLCQkJ4csvv+Tw4cP4+vqqXZ5UDeTMwJJF02g0vPvuuzz++ON4eXmxePFi9u3bJwOuBpFnclKN0LZtWyIjI9m+fTs9evRQuxypGsl7cpLFCw0N5fTp06WuaCVZPnkmJ1m0P//8k549e/L555+zcuVKnJyc+P7779UuS6pG8p6cZLFiYmIYMGAAb7zxBqNGjSIgIIDOnTvz559/0rZtW7XLk6qJPJOTLFJKSgpdunShZ8+eLF26tPj1AQMGUFhYSGhoqIrVSdVJhpwkSRZNNjxIkmTRZMhJkmTRZMhJkmTRZMhJkmTRZMhJkmTRZMhJkmTRZMhJkmTRZMhJkmTRZMhJkmTRZMhJkmTRZMhJkmTR/h8b/i8NGq1c8AAAAABJRU5ErkJggg==",
107 | "text/plain": [
108 | ""
109 | ]
110 | },
111 | "metadata": {},
112 | "output_type": "display_data"
113 | }
114 | ],
115 | "source": [
116 | "fig, ax = plt.subplots(figsize=(3, 2.5))\n",
117 | "ax.plot(\n",
118 | " [x[1] for x in comp_grid],\n",
119 | " [x[0] for x in lat_params_CeSn],\n",
120 | " label=\"Ce$_{1-x}$Sn$_x$O$_2$\",\n",
121 | ")\n",
122 | "ax.plot(\n",
123 | " [x[1] for x in comp_grid],\n",
124 | " [x[0] for x in lat_params_CeZr],\n",
125 | " label=\"Ce$_{1-x}$Zr$_x$O$_2$\",\n",
126 | ")\n",
127 | "ax.set_xlabel(\"$x$\")\n",
128 | "ax.set_ylabel(\"a [Å]\")\n",
129 | "ax.legend()\n",
130 | "fig.show()"
131 | ]
132 | },
133 | {
134 | "cell_type": "markdown",
135 | "metadata": {},
136 | "source": [
137 | "### BiSBr"
138 | ]
139 | },
140 | {
141 | "cell_type": "code",
142 | "execution_count": 5,
143 | "metadata": {},
144 | "outputs": [],
145 | "source": [
146 | "# Load structure\n",
147 | "atoms = ase.io.read(\"../data/structures/BiSBr.cif\")\n",
148 | "halide_elements = [\"Cl\", \"Br\", \"I\"]\n",
149 | "halide_idx = [i for i, atom in enumerate(atoms) if atom.symbol in halide_elements]\n",
150 | "halide_atomic_numbers = [ase.Atoms(el).numbers[0] for el in halide_elements]\n",
151 | "alchemical_pairs = [\n",
152 | " [AlchemicalPair(atom_index=idx, atomic_number=z) for idx in halide_idx]\n",
153 | " for z in halide_atomic_numbers\n",
154 | "]"
155 | ]
156 | },
157 | {
158 | "cell_type": "code",
159 | "execution_count": 6,
160 | "metadata": {},
161 | "outputs": [
162 | {
163 | "name": "stderr",
164 | "output_type": "stream",
165 | "text": [
166 | "100%|██████████| 21/21 [08:12<00:00, 23.46s/it]\n",
167 | "100%|██████████| 21/21 [04:21<00:00, 12.45s/it]\n",
168 | "100%|██████████| 21/21 [05:35<00:00, 15.95s/it]\n"
169 | ]
170 | }
171 | ],
172 | "source": [
173 | "comp_grid = [[1 - x, x, 0] for x in np.linspace(0, 1, 21)]\n",
174 | "lat_params_ClBr = []\n",
175 | "for comp in tqdm(comp_grid):\n",
176 | " with suppress_print(out=True, err=True):\n",
177 | " cellpar = get_alchemical_optimized_cellpar(\n",
178 | " atoms, alchemical_pairs, comp, model=model, device=device\n",
179 | " )\n",
180 | " lat_params_ClBr.append(cellpar[:3])\n",
181 | "\n",
182 | "comp_grid = [[0, 1 - x, x] for x in np.linspace(0, 1, 21)]\n",
183 | "lat_params_BrI = []\n",
184 | "for comp in tqdm(comp_grid):\n",
185 | " with suppress_print(out=True, err=True):\n",
186 | " cellpar = get_alchemical_optimized_cellpar(\n",
187 | " atoms, alchemical_pairs, comp, model=model, device=device\n",
188 | " )\n",
189 | " lat_params_BrI.append(cellpar[:3])\n",
190 | "\n",
191 | "\n",
192 | "comp_grid = [[1 - x, 0, x] for x in np.linspace(0, 1, 21)]\n",
193 | "lat_params_ClI = []\n",
194 | "for comp in tqdm(comp_grid):\n",
195 | " with suppress_print(out=True, err=True):\n",
196 | " cellpar = get_alchemical_optimized_cellpar(\n",
197 | " atoms, alchemical_pairs, comp, model=model, device=device\n",
198 | " )\n",
199 | " lat_params_ClI.append(cellpar[:3])"
200 | ]
201 | },
202 | {
203 | "cell_type": "code",
204 | "execution_count": 7,
205 | "metadata": {},
206 | "outputs": [
207 | {
208 | "data": {
209 | "image/png": "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",
210 | "text/plain": [
211 | ""
212 | ]
213 | },
214 | "metadata": {},
215 | "output_type": "display_data"
216 | }
217 | ],
218 | "source": [
219 | "fig, ax = plt.subplots(figsize=(3.5, 2.5), layout=\"constrained\")\n",
220 | "comp = np.linspace(0, 1, 21)\n",
221 | "idx, param = 2, \"c\"\n",
222 | "ax.plot(comp, [lat[idx] for lat in lat_params_ClBr], label=\"BiSCl$_{1-x}$Br$_x$\")\n",
223 | "ax.plot(comp, [lat[idx] for lat in lat_params_BrI], label=\"BiSBr$_{1-x}$I$_x$\")\n",
224 | "ax.plot(comp, [lat[idx] for lat in lat_params_ClI], label=\"BiSCl$_{1-x}$I$_x$\")\n",
225 | "ax.legend()\n",
226 | "ax.set_xlabel(\"$x$\")\n",
227 | "ax.set_ylabel(f\"{param} [Å]\")\n",
228 | "fig.show()"
229 | ]
230 | }
231 | ],
232 | "metadata": {
233 | "kernelspec": {
234 | "display_name": "chgnet",
235 | "language": "python",
236 | "name": "python3"
237 | },
238 | "language_info": {
239 | "codemirror_mode": {
240 | "name": "ipython",
241 | "version": 3
242 | },
243 | "file_extension": ".py",
244 | "mimetype": "text/x-python",
245 | "name": "python",
246 | "nbconvert_exporter": "python",
247 | "pygments_lexer": "ipython3",
248 | "version": "3.10.14"
249 | }
250 | },
251 | "nbformat": 4,
252 | "nbformat_minor": 2
253 | }
254 |
--------------------------------------------------------------------------------
/notebooks/4_vacancy_analysis.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "from pathlib import Path\n",
10 | "\n",
11 | "import numpy as np\n",
12 | "import pandas as pd\n",
13 | "from ase import units\n",
14 | "from scipy.integrate import trapezoid"
15 | ]
16 | },
17 | {
18 | "cell_type": "code",
19 | "execution_count": 2,
20 | "metadata": {},
21 | "outputs": [],
22 | "source": [
23 | "def integrate_switching(\n",
24 | " df_log: pd.DataFrame,\n",
25 | " equil_time: int = 20000,\n",
26 | " switch_time: int = 30000,\n",
27 | " return_E_diss: bool = False,\n",
28 | "):\n",
29 | " fwd_start, fwd_end = equil_time, equil_time + switch_time\n",
30 | " rev_start, rev_end = 2 * equil_time + switch_time, 2 * equil_time + 2 * switch_time\n",
31 | " grad, lamda = df_log[\"lambda_grad\"], df_log[\"lambda\"]\n",
32 | " W_fwd = trapezoid(grad[fwd_start:fwd_end], lamda[fwd_start:fwd_end])\n",
33 | " W_rev = trapezoid(grad[rev_start:rev_end], lamda[rev_start:rev_end])\n",
34 | " if return_E_diss:\n",
35 | " return (W_fwd - W_rev) / 2, (W_fwd + W_rev) / 2\n",
36 | " return (W_fwd - W_rev) / 2 # free energy difference\n",
37 | "\n",
38 | "\n",
39 | "def analyze_frenkel_ladd(\n",
40 | " base_path: Path,\n",
41 | " temp: float,\n",
42 | " equil_time: int = 20000,\n",
43 | " switch_time: int = 30000,\n",
44 | " verbose: bool = False,\n",
45 | "):\n",
46 | " T = temp\n",
47 | " k = np.load(base_path / \"spring_constants.npy\")\n",
48 | "\n",
49 | " mass = np.load(base_path / \"masses.npy\")\n",
50 | " omega = np.sqrt(k / mass)\n",
51 | " n_atoms = len(mass)\n",
52 | "\n",
53 | " # 1. Perfect crystal\n",
54 | " df_log = pd.read_csv(base_path / \"observables.csv\")\n",
55 | " volume = df_log[\"volume\"].values[0]\n",
56 | " if verbose:\n",
57 | " _, E_diss_perfect = integrate_switching(\n",
58 | " df_log, equil_time, switch_time, return_E_diss=True\n",
59 | " )\n",
60 | " delta_F = integrate_switching(df_log, equil_time, switch_time)\n",
61 | " F_E = 3 * units.kB * T * np.mean(np.log(units._hbar * omega / (units.kB * T)))\n",
62 | " PV = volume * 1.01325 * units.bar\n",
63 | " G_perfect = delta_F + F_E + PV\n",
64 | "\n",
65 | " # 2. Defective crystal\n",
66 | " df_log = pd.read_csv(base_path / \"observables_defect.csv\")\n",
67 | " volume = df_log[\"volume\"].values[0]\n",
68 | " if verbose:\n",
69 | " _, E_diss_defect = integrate_switching(\n",
70 | " df_log, equil_time, switch_time, return_E_diss=True\n",
71 | " )\n",
72 | " delta_F = integrate_switching(df_log, equil_time, switch_time)\n",
73 | " F_E = 3 * units.kB * T * np.mean(np.log(units._hbar * omega / (units.kB * T)))\n",
74 | " PV = volume * 1.01325 * units.bar\n",
75 | " G_defect = delta_F + F_E + PV\n",
76 | " G_v = G_defect * (n_atoms - 1) - G_perfect * (n_atoms - 1)\n",
77 | "\n",
78 | " # 3. Partial FL\n",
79 | " df_log = pd.read_csv(base_path / \"observables_FL.csv\")\n",
80 | " if verbose:\n",
81 | " _, E_diss_FL = integrate_switching(\n",
82 | " df_log, equil_time, switch_time, return_E_diss=True\n",
83 | " )\n",
84 | " delta_G = integrate_switching(df_log, equil_time, switch_time)\n",
85 | " # delta_G * N = (G_defect * N-1 + F_E) - G_perfect * N\n",
86 | " G_defect_alt = (delta_G * n_atoms - F_E + G_perfect * n_atoms) / (n_atoms - 1)\n",
87 | " G_v_alt = G_defect_alt * (n_atoms - 1) - G_perfect * (n_atoms - 1)\n",
88 | "\n",
89 | " if verbose:\n",
90 | " return G_perfect, G_defect, delta_G, E_diss_perfect, E_diss_defect, E_diss_FL\n",
91 | " return G_v, G_v_alt"
92 | ]
93 | },
94 | {
95 | "cell_type": "code",
96 | "execution_count": 3,
97 | "metadata": {},
98 | "outputs": [
99 | {
100 | "name": "stdout",
101 | "output_type": "stream",
102 | "text": [
103 | "G_v (50 K) = 1.5513 ± 0.0063 eV\n",
104 | "G_v FL (50 K) = 1.5582 ± 0.0014 eV\n",
105 | "G_v (100 K) = 1.5475 ± 0.0060 eV\n",
106 | "G_v FL (100 K) = 1.5405 ± 0.0018 eV\n",
107 | "G_v (150 K) = 1.5232 ± 0.0276 eV\n",
108 | "G_v FL (150 K) = 1.5188 ± 0.0029 eV\n",
109 | "G_v (200 K) = 1.5110 ± 0.0283 eV\n",
110 | "G_v FL (200 K) = 1.5003 ± 0.0072 eV\n"
111 | ]
112 | }
113 | ],
114 | "source": [
115 | "result_path = Path(\"../data/results/vacancy\")\n",
116 | "temp_range = [50, 100, 150, 200]\n",
117 | "\n",
118 | "G_v_all, G_v_std_all = [], []\n",
119 | "G_v_alt_all, G_v_alt_std_all = [], []\n",
120 | "for temp in temp_range:\n",
121 | " G_v_list = []\n",
122 | " G_v_alt_list = []\n",
123 | " E_diss_perfect_list = []\n",
124 | " E_diss_defect_list = []\n",
125 | " E_diss_FL_list = []\n",
126 | " for i in range(4):\n",
127 | " base_path = result_path / f\"Fe_5x5x5_{temp}K/{i}\"\n",
128 | " G_v, G_v_alt = analyze_frenkel_ladd(base_path, temp=temp, verbose=False)\n",
129 | " G_v_list.append(G_v)\n",
130 | " G_v_alt_list.append(G_v_alt)\n",
131 | " G_v = np.mean(G_v_list)\n",
132 | " G_v_std = np.std(G_v_list)\n",
133 | " G_v_alt = np.mean(G_v_alt_list)\n",
134 | " G_v_alt_std = np.std(G_v_alt_list)\n",
135 | " print(f\"G_v ({temp} K) = {G_v:.4f} ± {G_v_std:.4f} eV\")\n",
136 | " print(f\"G_v FL ({temp} K) = {G_v_alt:.4f} ± {G_v_alt_std:.4f} eV\")\n",
137 | " G_v_all.append(G_v)\n",
138 | " G_v_std_all.append(G_v_std)\n",
139 | " G_v_alt_all.append(G_v_alt)\n",
140 | " G_v_alt_std_all.append(G_v_alt_std)"
141 | ]
142 | }
143 | ],
144 | "metadata": {
145 | "kernelspec": {
146 | "display_name": "chgnet",
147 | "language": "python",
148 | "name": "python3"
149 | },
150 | "language_info": {
151 | "codemirror_mode": {
152 | "name": "ipython",
153 | "version": 3
154 | },
155 | "file_extension": ".py",
156 | "mimetype": "text/x-python",
157 | "name": "python",
158 | "nbconvert_exporter": "python",
159 | "pygments_lexer": "ipython3",
160 | "version": "3.10.14"
161 | }
162 | },
163 | "nbformat": 4,
164 | "nbformat_minor": 2
165 | }
166 |
--------------------------------------------------------------------------------
/notebooks/5_perovskite_analysis.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "from pathlib import Path\n",
10 | "\n",
11 | "import numpy as np\n",
12 | "import pandas as pd\n",
13 | "from ase import units\n",
14 | "from scipy.integrate import trapezoid"
15 | ]
16 | },
17 | {
18 | "cell_type": "markdown",
19 | "metadata": {},
20 | "source": [
21 | "### Frenkel–Ladd path"
22 | ]
23 | },
24 | {
25 | "cell_type": "code",
26 | "execution_count": 2,
27 | "metadata": {},
28 | "outputs": [],
29 | "source": [
30 | "def integrate_switching(\n",
31 | " df_log: pd.DataFrame,\n",
32 | " equil_time: int = 20000,\n",
33 | " switch_time: int = 30000,\n",
34 | " return_E_diss: bool = False,\n",
35 | "):\n",
36 | " fwd_start, fwd_end = equil_time, equil_time + switch_time\n",
37 | " rev_start, rev_end = 2 * equil_time + switch_time, 2 * equil_time + 2 * switch_time\n",
38 | " grad, lamda = df_log[\"lambda_grad\"], df_log[\"lambda\"]\n",
39 | " W_fwd = trapezoid(grad[fwd_start:fwd_end], lamda[fwd_start:fwd_end])\n",
40 | " W_rev = trapezoid(grad[rev_start:rev_end], lamda[rev_start:rev_end])\n",
41 | " if return_E_diss:\n",
42 | " return (W_fwd - W_rev) / 2, (W_fwd + W_rev) / 2\n",
43 | " return (W_fwd - W_rev) / 2 # free energy difference\n",
44 | "\n",
45 | "\n",
46 | "def analyze_frenkel_ladd(\n",
47 | " base_path: Path,\n",
48 | " temp: float,\n",
49 | " equil_time: int = 20000,\n",
50 | " switch_time: int = 30000,\n",
51 | "):\n",
52 | " T = temp\n",
53 | " df_log = pd.read_csv(base_path / \"observables.csv\")\n",
54 | " k = np.load(base_path / \"spring_constants.npy\")\n",
55 | " mass = np.load(base_path / \"masses.npy\")\n",
56 | " omega = np.sqrt(k / mass)\n",
57 | " volume = df_log[\"volume\"].values[0]\n",
58 | "\n",
59 | " delta_F = integrate_switching(df_log, equil_time, switch_time)\n",
60 | " F_E = 3 * units.kB * T * np.mean(np.log(units._hbar * omega / (units.kB * T)))\n",
61 | " PV = volume * 1.01325 * units.bar\n",
62 | " delta_G = delta_F + F_E + PV\n",
63 | "\n",
64 | " return delta_G\n",
65 | "\n",
66 | "\n",
67 | "def analyze_alchemical_switching(\n",
68 | " base_path: Path,\n",
69 | " temp: float,\n",
70 | " equil_time: int = 20000,\n",
71 | " switch_time: int = 30000,\n",
72 | "):\n",
73 | " T = temp\n",
74 | " df_log = pd.read_csv(base_path / \"observables.csv\")\n",
75 | " mass_init = np.load(base_path / \"masses_init.npy\")\n",
76 | " mass_final = np.load(base_path / \"masses_final.npy\")\n",
77 | "\n",
78 | " work = integrate_switching(df_log, equil_time, switch_time)\n",
79 | " G_mass = 1.5 * units.kB * T * np.mean(np.log(mass_init / mass_final))\n",
80 | " delta_G = work + G_mass\n",
81 | "\n",
82 | " return delta_G"
83 | ]
84 | },
85 | {
86 | "cell_type": "code",
87 | "execution_count": 3,
88 | "metadata": {},
89 | "outputs": [
90 | {
91 | "name": "stdout",
92 | "output_type": "stream",
93 | "text": [
94 | "CsPbI3 Alpha G (300 K) = -8.8514 ± 0.0003 eV/atom\n",
95 | "CsPbI3 Alpha G (350 K) = -9.8642 ± 0.0004 eV/atom\n",
96 | "CsPbI3 Alpha G (400 K) = -10.8783 ± 0.0006 eV/atom\n",
97 | "CsPbI3 Alpha G (450 K) = -11.8943 ± 0.0003 eV/atom\n",
98 | "CsPbI3 Alpha G (500 K) = -12.9119 ± 0.0004 eV/atom\n",
99 | "CsPbI3 Delta G (300 K) = -8.8560 ± 0.0001 eV/atom\n",
100 | "CsPbI3 Delta G (350 K) = -9.8660 ± 0.0001 eV/atom\n",
101 | "CsPbI3 Delta G (400 K) = -10.8782 ± 0.0002 eV/atom\n",
102 | "CsPbI3 Delta G (450 K) = -11.8921 ± 0.0002 eV/atom\n",
103 | "CsPbI3 Delta G (500 K) = -12.9072 ± 0.0002 eV/atom\n"
104 | ]
105 | }
106 | ],
107 | "source": [
108 | "result_path = Path(\"../data/results/perovskite/frenkel_ladd\")\n",
109 | "temp_range = [300, 350, 400, 450, 500]\n",
110 | "\n",
111 | "G_alpha = []\n",
112 | "G_alpha_std = []\n",
113 | "for temp in temp_range:\n",
114 | " G_list = []\n",
115 | " for i in range(4):\n",
116 | " base_path = result_path / f\"CsPbI3_alpha_6x6x6_{temp}K/{i}\"\n",
117 | " G_list.append(analyze_frenkel_ladd(base_path, temp=temp))\n",
118 | " G = np.mean(G_list)\n",
119 | " G_std = np.std(G_list)\n",
120 | " print(f\"CsPbI3 Alpha G ({temp} K) = {G:.4f} ± {G_std:.4f} eV/atom\")\n",
121 | " G_alpha.append(G)\n",
122 | " G_alpha_std.append(G_std)\n",
123 | "\n",
124 | "G_delta = []\n",
125 | "G_delta_std = []\n",
126 | "for temp in temp_range:\n",
127 | " G_list = []\n",
128 | " for i in range(4):\n",
129 | " base_path = result_path / f\"CsPbI3_delta_6x3x3_{temp}K/{i}\"\n",
130 | " G_list.append(analyze_frenkel_ladd(base_path, temp=temp))\n",
131 | " G = np.mean(G_list)\n",
132 | " G_std = np.std(G_list)\n",
133 | " print(f\"CsPbI3 Delta G ({temp} K) = {G:.4f} ± {G_std:.4f} eV/atom\")\n",
134 | " G_delta.append(G)\n",
135 | " G_delta_std.append(G_std)"
136 | ]
137 | },
138 | {
139 | "cell_type": "code",
140 | "execution_count": 4,
141 | "metadata": {},
142 | "outputs": [
143 | {
144 | "name": "stdout",
145 | "output_type": "stream",
146 | "text": [
147 | "CsSnI3 Alpha G (300 K) = -8.8297 ± 0.0002 eV/atom\n",
148 | "CsSnI3 Alpha G (350 K) = -9.8413 ± 0.0002 eV/atom\n",
149 | "CsSnI3 Alpha G (400 K) = -10.8544 ± 0.0002 eV/atom\n",
150 | "CsSnI3 Alpha G (450 K) = -11.8695 ± 0.0003 eV/atom\n",
151 | "CsSnI3 Alpha G (500 K) = -12.8863 ± 0.0003 eV/atom\n",
152 | "CsSnI3 Delta G (300 K) = -8.8289 ± 0.0001 eV/atom\n",
153 | "CsSnI3 Delta G (350 K) = -9.8381 ± 0.0000 eV/atom\n",
154 | "CsSnI3 Delta G (400 K) = -10.8494 ± 0.0003 eV/atom\n",
155 | "CsSnI3 Delta G (450 K) = -11.8627 ± 0.0003 eV/atom\n",
156 | "CsSnI3 Delta G (500 K) = -12.8771 ± 0.0003 eV/atom\n"
157 | ]
158 | }
159 | ],
160 | "source": [
161 | "G_CsSnI3_alpha = []\n",
162 | "G_CsSnI3_alpha_std = []\n",
163 | "for temp in temp_range:\n",
164 | " G_list = []\n",
165 | " for i in range(4):\n",
166 | " base_path = result_path / f\"CsSnI3_alpha_6x6x6_{temp}K/{i}\"\n",
167 | " G_list.append(analyze_frenkel_ladd(base_path, temp=temp))\n",
168 | " G = np.mean(G_list)\n",
169 | " G_std = np.std(G_list)\n",
170 | " print(f\"CsSnI3 Alpha G ({temp} K) = {G:.4f} ± {G_std:.4f} eV/atom\")\n",
171 | " G_CsSnI3_alpha.append(G)\n",
172 | " G_CsSnI3_alpha_std.append(G_std)\n",
173 | "\n",
174 | "G_CsSnI3_delta = []\n",
175 | "G_CsSnI3_delta_std = []\n",
176 | "for temp in temp_range:\n",
177 | " G_list = []\n",
178 | " for i in range(4):\n",
179 | " base_path = result_path / f\"CsSnI3_delta_6x3x3_{temp}K/{i}\"\n",
180 | " G_list.append(analyze_frenkel_ladd(base_path, temp=temp))\n",
181 | " G = np.mean(G_list)\n",
182 | " G_std = np.std(G_list)\n",
183 | " print(f\"CsSnI3 Delta G ({temp} K) = {G:.4f} ± {G_std:.4f} eV/atom\")\n",
184 | " G_CsSnI3_delta.append(G)\n",
185 | " G_CsSnI3_delta_std.append(G_std)"
186 | ]
187 | },
188 | {
189 | "cell_type": "markdown",
190 | "metadata": {},
191 | "source": [
192 | "### Alchemical path"
193 | ]
194 | },
195 | {
196 | "cell_type": "code",
197 | "execution_count": 5,
198 | "metadata": {},
199 | "outputs": [
200 | {
201 | "name": "stdout",
202 | "output_type": "stream",
203 | "text": [
204 | "Alpha ΔG (300 K) = 0.0233 ± 0.0001 eV/atom\n",
205 | "Alpha ΔG (350 K) = 0.0236 ± 0.0001 eV/atom\n",
206 | "Alpha ΔG (400 K) = 0.0241 ± 0.0001 eV/atom\n",
207 | "Alpha ΔG (450 K) = 0.0249 ± 0.0000 eV/atom\n",
208 | "Alpha ΔG (500 K) = 0.0258 ± 0.0000 eV/atom\n",
209 | "Delta ΔG (300 K) = 0.0271 ± 0.0000 eV/atom\n",
210 | "Delta ΔG (350 K) = 0.0279 ± 0.0000 eV/atom\n",
211 | "Delta ΔG (400 K) = 0.0286 ± 0.0000 eV/atom\n",
212 | "Delta ΔG (450 K) = 0.0294 ± 0.0000 eV/atom\n",
213 | "Delta ΔG (500 K) = 0.0301 ± 0.0000 eV/atom\n"
214 | ]
215 | }
216 | ],
217 | "source": [
218 | "result_path = Path(\"../data/results/perovskite/alchemy\")\n",
219 | "\n",
220 | "G_alpha = []\n",
221 | "G_alpha_std = []\n",
222 | "for temp in temp_range:\n",
223 | " G_list = []\n",
224 | " for i in range(4):\n",
225 | " base_path = result_path / f\"CsPbI3_CsSnI3_alpha_{temp}K/{i}\"\n",
226 | " G_list.append(analyze_alchemical_switching(base_path, temp=temp))\n",
227 | " G = np.mean(G_list)\n",
228 | " G_std = np.std(G_list)\n",
229 | " print(f\"Alpha ΔG ({temp} K) = {G:.4f} ± {G_std:.4f} eV/atom\")\n",
230 | " G_alpha.append(G)\n",
231 | " G_alpha_std.append(G_std)\n",
232 | "\n",
233 | "G_delta = []\n",
234 | "G_delta_std = []\n",
235 | "for temp in temp_range:\n",
236 | " G_list = []\n",
237 | " for i in range(4):\n",
238 | " base_path = result_path / f\"CsPbI3_CsSnI3_delta_{temp}K/{i}\"\n",
239 | " G_list.append(analyze_alchemical_switching(base_path, temp=temp))\n",
240 | " G = np.mean(G_list)\n",
241 | " G_std = np.std(G_list)\n",
242 | " print(f\"Delta ΔG ({temp} K) = {G:.4f} ± {G_std:.4f} eV/atom\")\n",
243 | " G_delta.append(G)\n",
244 | " G_delta_std.append(G_std)"
245 | ]
246 | }
247 | ],
248 | "metadata": {
249 | "kernelspec": {
250 | "display_name": "chgnet",
251 | "language": "python",
252 | "name": "python3"
253 | },
254 | "language_info": {
255 | "codemirror_mode": {
256 | "name": "ipython",
257 | "version": 3
258 | },
259 | "file_extension": ".py",
260 | "mimetype": "text/x-python",
261 | "name": "python",
262 | "nbconvert_exporter": "python",
263 | "pygments_lexer": "ipython3",
264 | "version": "3.10.14"
265 | }
266 | },
267 | "nbformat": 4,
268 | "nbformat_minor": 2
269 | }
270 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "alchemical_mace"
7 | authors = [
8 | { name = "Juno Nam", email = "junonam@mit.edu" },
9 | ]
10 | description = "Alchemical MACE model"
11 | readme = "README.md"
12 | requires-python = ">=3.9"
13 | version = "0.1.0"
14 |
15 | [tool.setuptools]
16 | packages = ["alchemical_mace"]
17 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | torch==2.0.1
2 | e3nn==0.4.4
3 | mace-torch==0.3.4
4 | ase==3.22.1
5 | pymatgen==2024.3.1
6 | numpy==1.25.2
7 | scipy==1.11.2
8 | pandas==2.2.2
9 | matplotlib==3.8.0
10 | mpltern==1.0.2
11 | tqdm==4.66.3
12 | ipykernel==6.25.2
13 |
--------------------------------------------------------------------------------
/scripts/perovskite_alchemy.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | from pathlib import Path
3 |
4 | import ase
5 | import numpy as np
6 | import pandas as pd
7 | from ase import units
8 | from ase.build import make_supercell
9 | from ase.constraints import ExpCellFilter
10 | from ase.md.npt import NPT
11 | from ase.md.nptberendsen import Inhomogeneous_NPTBerendsen
12 | from ase.md.velocitydistribution import MaxwellBoltzmannDistribution, Stationary
13 | from ase.optimize import FIRE
14 | from mace.calculators import mace_mp
15 | from tqdm import tqdm
16 |
17 | from alchemical_mace.calculator import AlchemicalMACECalculator
18 | from alchemical_mace.model import AlchemicalPair
19 | from alchemical_mace.utils import upper_triangular_cell
20 |
21 |
22 | # Arguments
23 | parser = argparse.ArgumentParser()
24 |
25 | # Structure
26 | parser.add_argument("--structure-file", type=str)
27 | parser.add_argument("--supercell", type=int, nargs=3, default=[6, 6, 6])
28 |
29 | # Alchemy
30 | parser.add_argument("--switch-pair", type=str, nargs=2, default=["Pb", "Sn"])
31 |
32 | # Molecular dynamics: general
33 | parser.add_argument("--temperature", type=float, default=300.0)
34 | parser.add_argument("--pressure", type=float, default=1.0)
35 | parser.add_argument("--timestep", type=float, default=2.0)
36 | parser.add_argument("--ttime", type=float, default=25.0)
37 | parser.add_argument("--ptime", type=int, default=75.0)
38 |
39 | # Molecular dynamics: timesteps
40 | parser.add_argument("--npt-equil-stpes", type=int, default=10000)
41 | parser.add_argument("--alchemy-equil-steps", type=int, default=20000)
42 | parser.add_argument("--alchemy-switch-steps", type=int, default=30000)
43 |
44 | # Molecular dynamics: output control
45 | parser.add_argument("--output-dir", type=Path, default=Path("results"))
46 | parser.add_argument("--log-interval", type=int, default=1)
47 |
48 | # MACE model
49 | parser.add_argument("--device", type=str, default="cuda")
50 | parser.add_argument("--model", type=str, default="small")
51 |
52 | args = parser.parse_args()
53 | args.output_dir.mkdir(exist_ok=True, parents=True)
54 |
55 | # Load structure
56 | atoms = ase.io.read(args.structure_file)
57 | atoms = make_supercell(atoms, np.diag(args.supercell))
58 |
59 | # Load universal MACE calculator and relax the structure
60 | mace_calc = mace_mp(model=args.model, device=args.device, default_dtype="float32")
61 | atoms.calc = mace_calc
62 | atoms = ExpCellFilter(atoms)
63 | optimizer = FIRE(atoms)
64 | optimizer.run(fmax=0.01, steps=500)
65 | atoms = atoms.atoms # get the relaxed structure
66 | initial_atoms = atoms.copy() # save the initial structure
67 |
68 |
69 | ################################################################################
70 | # Cell volume equilibration
71 | ################################################################################
72 |
73 | atoms = initial_atoms.copy()
74 | atoms.set_calculator(mace_calc)
75 | bulk_modulus = 100.0 * units.GPa
76 |
77 | # NPT equilibration
78 | dyn = Inhomogeneous_NPTBerendsen(
79 | atoms,
80 | timestep=args.timestep * units.fs,
81 | temperature_K=args.temperature,
82 | pressure_au=args.pressure * 1.01325 * units.bar,
83 | taut=args.ttime * units.fs,
84 | taup=args.ptime * units.fs,
85 | compressibility_au=1.0 / bulk_modulus,
86 | )
87 | MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature)
88 | Stationary(atoms)
89 |
90 | # NPT equilibration and volume relaxation
91 | for step in tqdm(range(args.npt_equil_stpes), desc="NPT equil"):
92 | dyn.run(steps=1)
93 |
94 |
95 | ################################################################################
96 | # Alchemical switching
97 | ################################################################################
98 |
99 | # Define alchemical transformation
100 | src_elem, dst_elem = args.switch_pair
101 | src_Z, dst_Z = ase.data.atomic_numbers[src_elem], ase.data.atomic_numbers[dst_elem]
102 | src_idx = np.where(atoms.get_atomic_numbers() == src_Z)[0]
103 | alchemical_pairs = [
104 | [AlchemicalPair(atom_index=idx, atomic_number=Z) for idx in src_idx]
105 | for Z in [src_Z, dst_Z]
106 | ]
107 |
108 | # Set up the alchemical MACE calculator
109 | calc = AlchemicalMACECalculator(
110 | atoms=atoms,
111 | alchemical_pairs=alchemical_pairs,
112 | alchemical_weights=[1.0, 0.0],
113 | device=args.device,
114 | model=args.model,
115 | )
116 | atoms.set_calculator(calc)
117 | upper_triangular_cell(atoms) # for ASE NPT
118 |
119 | # NPT alchemical switching
120 | ptime = args.ptime * units.fs
121 | pfactor = bulk_modulus * ptime * ptime
122 |
123 | dyn = NPT(
124 | atoms,
125 | timestep=args.timestep * units.fs,
126 | temperature_K=args.temperature,
127 | externalstress=args.pressure * 1.01325 * units.bar,
128 | ttime=args.ttime * units.fs,
129 | pfactor=pfactor,
130 | )
131 |
132 | # Define alchemical path
133 | t = np.linspace(0.0, 1.0, args.alchemy_switch_steps)
134 | lambda_steps = t**5 * (70 * t**4 - 315 * t**3 + 540 * t**2 - 420 * t + 126)
135 | lambda_values = [
136 | np.zeros(args.alchemy_equil_steps),
137 | lambda_steps,
138 | np.ones(args.alchemy_equil_steps),
139 | lambda_steps[::-1],
140 | ]
141 | lambda_values = np.concatenate(lambda_values)
142 |
143 | calculate_gradients = [
144 | np.zeros(args.alchemy_equil_steps, dtype=bool),
145 | np.ones(args.alchemy_switch_steps, dtype=bool),
146 | np.zeros(args.alchemy_equil_steps, dtype=bool),
147 | np.ones(args.alchemy_switch_steps, dtype=bool),
148 | ]
149 | calculate_gradients = np.concatenate(calculate_gradients)
150 |
151 |
152 | def get_observables(dynamics, time, lambda_value):
153 | num_atoms = len(dynamics.atoms)
154 | alchemical_grad = dynamics.atoms._calc.results["alchemical_grad"]
155 | lambda_grad = (alchemical_grad[1] - alchemical_grad[0]) / num_atoms
156 | return {
157 | "time": time,
158 | "potential": dynamics.atoms.get_potential_energy() / num_atoms,
159 | "temperature": dynamics.atoms.get_temperature(),
160 | "volume": dynamics.atoms.get_volume() / num_atoms,
161 | "lambda": lambda_value,
162 | "lambda_grad": lambda_grad,
163 | }
164 |
165 |
166 | # Simulation loop
167 | total_steps = 2 * args.alchemy_equil_steps + 2 * args.alchemy_switch_steps
168 |
169 | observables = []
170 | for step in tqdm(range(total_steps), desc="Alchemical switching"):
171 | lambda_value = lambda_values[step]
172 | grad_enabled = calculate_gradients[step]
173 |
174 | # Set alchemical weights and atomic masses
175 | calc.set_alchemical_weights([1 - lambda_value, lambda_value])
176 | atoms.set_masses(calc.get_alchemical_atomic_masses())
177 | calc.calculate_alchemical_grad = grad_enabled
178 |
179 | dyn.run(steps=1)
180 | if step % args.log_interval == 0:
181 | time = (step + 1) * args.timestep
182 | observables.append(get_observables(dyn, time, lambda_value))
183 |
184 | # Save observables
185 | df = pd.DataFrame(observables)
186 | df.to_csv(args.output_dir / "observables.csv", index=False)
187 |
188 | # Save masses for post-processing
189 | calc.set_alchemical_weights([1.0, 0.0])
190 | np.save(args.output_dir / "masses_init.npy", calc.get_alchemical_atomic_masses())
191 | calc.set_alchemical_weights([0.0, 1.0])
192 | np.save(args.output_dir / "masses_final.npy", calc.get_alchemical_atomic_masses())
193 |
--------------------------------------------------------------------------------
/scripts/perovskite_frenkel_ladd.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | from pathlib import Path
3 |
4 | import ase
5 | import numpy as np
6 | import pandas as pd
7 | from ase import units
8 | from ase.build import make_supercell
9 | from ase.constraints import ExpCellFilter
10 | from ase.md.langevin import Langevin
11 | from ase.md.nptberendsen import Inhomogeneous_NPTBerendsen
12 | from ase.md.velocitydistribution import MaxwellBoltzmannDistribution, Stationary
13 | from ase.optimize import FIRE
14 | from mace.calculators import mace_mp
15 | from pymatgen.io.ase import AseAtomsAdaptor
16 | from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
17 | from tqdm import tqdm
18 |
19 | from alchemical_mace.calculator import FrenkelLaddCalculator, NVTMACECalculator
20 |
21 |
22 | # Arguments
23 | parser = argparse.ArgumentParser()
24 |
25 | # Structure
26 | parser.add_argument("--structure-file", type=str)
27 | parser.add_argument("--supercell", type=int, nargs=3, default=[6, 6, 6])
28 |
29 | # Molecular dynamics: general
30 | parser.add_argument("--temperature", type=float, default=300.0)
31 | parser.add_argument("--pressure", type=float, default=1.0)
32 | parser.add_argument("--timestep", type=float, default=2.0)
33 | parser.add_argument("--ttime", type=float, default=25.0)
34 | parser.add_argument("--ptime", type=int, default=75.0)
35 |
36 | # Molecular dynamics: timesteps
37 | parser.add_argument("--npt-equil-stpes", type=int, default=10000)
38 | parser.add_argument("--npt-prod-steps", type=int, default=20000)
39 | parser.add_argument("--nvt-equil-steps", type=int, default=20000)
40 | parser.add_argument("--nvt-prod-steps", type=int, default=30000)
41 | parser.add_argument("--alchemy-equil-steps", type=int, default=20000)
42 | parser.add_argument("--alchemy-switch-steps", type=int, default=30000)
43 |
44 | # Molecular dynamics: output control
45 | parser.add_argument("--output-dir", type=Path, default=Path("results"))
46 | parser.add_argument("--log-interval", type=int, default=1)
47 |
48 | # MACE model
49 | parser.add_argument("--device", type=str, default="cuda")
50 | parser.add_argument("--model", type=str, default="small")
51 |
52 | args = parser.parse_args()
53 | args.output_dir.mkdir(exist_ok=True, parents=True)
54 |
55 | # Load structure
56 | atoms = ase.io.read(args.structure_file)
57 | atoms = make_supercell(atoms, np.diag(args.supercell))
58 |
59 | # Load universal MACE calculator and relax the structure
60 | mace_calc = mace_mp(model=args.model, device=args.device, default_dtype="float32")
61 | atoms.calc = mace_calc
62 | atoms = ExpCellFilter(atoms)
63 | optimizer = FIRE(atoms)
64 | optimizer.run(fmax=0.01, steps=500)
65 | atoms = atoms.atoms # get the relaxed structure
66 | initial_atoms = atoms.copy() # save the initial structure
67 |
68 |
69 | ################################################################################
70 | # Cell volume equilibration
71 | ################################################################################
72 |
73 | atoms = initial_atoms.copy()
74 | atoms.set_calculator(mace_calc)
75 | bulk_modulus = 100.0 * units.GPa
76 |
77 | # Equilibration and volume calculation
78 | dyn = Inhomogeneous_NPTBerendsen(
79 | atoms,
80 | timestep=args.timestep * units.fs,
81 | temperature_K=args.temperature,
82 | pressure_au=args.pressure * 1.01325 * units.bar,
83 | taut=args.ttime * units.fs,
84 | taup=args.ptime * units.fs,
85 | compressibility_au=1.0 / bulk_modulus,
86 | )
87 | MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature)
88 | Stationary(atoms)
89 |
90 | # NPT equilibration and volume relaxation
91 | cellpar_traj = []
92 | for step in tqdm(range(args.npt_equil_stpes), desc="NPT equil"):
93 | dyn.run(steps=1)
94 | for step in tqdm(range(args.npt_prod_steps), desc="NPT prod"):
95 | dyn.run(steps=1)
96 | if step % args.log_interval == 0:
97 | cellpar_traj.append(atoms.get_cell().cellpar())
98 | abc_new = np.mean(cellpar_traj, axis=0)[:3]
99 |
100 | # Scale the initial cell to match the average volume
101 | atoms = initial_atoms
102 | atoms.set_cell(np.diag(abc_new), scale_atoms=True)
103 | atoms.set_calculator(mace_calc)
104 |
105 | # Relax the atomic positions
106 | optimizer = FIRE(atoms)
107 | optimizer.run(fmax=0.01, steps=500)
108 | initial_atoms = atoms.copy() # save the initial structure
109 |
110 |
111 | ################################################################################
112 | # MSD calculation
113 | ################################################################################
114 |
115 | initial_positions = atoms.get_positions()
116 | # Using the reversible scaling MACE calculator with fixed scale of 1.0
117 | # since we can turn off the stress calculation
118 | calc = NVTMACECalculator(device=args.device, model=args.model)
119 | atoms.set_calculator(calc)
120 |
121 | # NVT MSD calculation
122 | dyn = Langevin(
123 | atoms,
124 | timestep=args.timestep * units.fs,
125 | temperature_K=args.temperature,
126 | friction=1 / (args.ttime * units.fs),
127 | )
128 | MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature)
129 | Stationary(atoms)
130 |
131 | temperatures = []
132 | for step in tqdm(range(args.nvt_equil_steps), desc="NVT equil"):
133 | dyn.run(steps=1)
134 | squared_disp = np.zeros(len(atoms))
135 | for step in tqdm(range(args.nvt_prod_steps), desc="NVT prod"):
136 | dyn.run(steps=1)
137 | squared_disp += np.sum((atoms.get_positions() - initial_positions) ** 2, axis=1)
138 | mean_squared_disp = squared_disp / args.nvt_prod_steps
139 |
140 | # Calculate spring constants and average over symmetrically equivalent atoms
141 | spring_constants = 3.0 * units.kB * args.temperature / mean_squared_disp
142 | structure = AseAtomsAdaptor.get_structure(initial_atoms)
143 | sga = SpacegroupAnalyzer(structure)
144 | equivalent_indices = sga.get_symmetrized_structure().equivalent_indices
145 | for indices in equivalent_indices:
146 | spring_constants[indices] = np.mean(spring_constants[indices])
147 |
148 | np.save(args.output_dir / "spring_constants.npy", spring_constants)
149 | np.save(args.output_dir / "masses.npy", atoms.get_masses())
150 |
151 |
152 | ################################################################################
153 | # Frenkel-Ladd calculation
154 | ################################################################################
155 |
156 | atoms = initial_atoms.copy()
157 | calc = FrenkelLaddCalculator(
158 | spring_constants=spring_constants,
159 | initial_positions=initial_positions,
160 | device=args.device,
161 | model=args.model,
162 | )
163 | atoms.set_calculator(calc)
164 |
165 | # NVT Frenkel-Ladd calculation
166 | dyn = Langevin(
167 | atoms,
168 | timestep=args.timestep * units.fs,
169 | temperature_K=args.temperature,
170 | friction=1 / (args.ttime * units.fs),
171 | )
172 | MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature)
173 | Stationary(atoms)
174 |
175 | # Define Frenkel-Ladd path
176 | t = np.linspace(0.0, 1.0, args.alchemy_switch_steps)
177 | lambda_steps = t**5 * (70 * t**4 - 315 * t**3 + 540 * t**2 - 420 * t + 126)
178 | lambda_values = [
179 | np.zeros(args.alchemy_equil_steps),
180 | lambda_steps,
181 | np.ones(args.alchemy_equil_steps),
182 | lambda_steps[::-1],
183 | ]
184 | lambda_values = np.concatenate(lambda_values)
185 |
186 |
187 | def get_observables(dynamics, time, lambda_value):
188 | num_atoms = len(dynamics.atoms)
189 | return {
190 | "time": time,
191 | "potential": dynamics.atoms.get_potential_energy() / num_atoms,
192 | "temperature": dynamics.atoms.get_temperature(),
193 | "volume": dynamics.atoms.get_volume() / num_atoms,
194 | "lambda": lambda_value,
195 | "lambda_grad": dynamics.atoms._calc.results["energy_diff"] / num_atoms,
196 | }
197 |
198 |
199 | # Simulation loop
200 | calc.compute_mace = False
201 | total_steps = 2 * args.alchemy_equil_steps + 2 * args.alchemy_switch_steps
202 |
203 | observables = []
204 | for step in tqdm(range(total_steps), desc="Frenkel-Ladd"):
205 | if step == args.alchemy_equil_steps: # turn on MACE after spring equilibration
206 | calc.compute_mace = True
207 | lambda_value = lambda_values[step]
208 | calc.set_weights(lambda_value)
209 |
210 | dyn.run(steps=1)
211 | if step % args.log_interval == 0:
212 | time = (step + 1) * args.timestep
213 | observables.append(get_observables(dyn, time, lambda_value))
214 |
215 | # Save observables
216 | df = pd.DataFrame(observables)
217 | df.to_csv(args.output_dir / "observables.csv", index=False)
218 |
--------------------------------------------------------------------------------
/scripts/vacancy_frenkel_ladd.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | from pathlib import Path
3 |
4 | import ase
5 | import numpy as np
6 | import pandas as pd
7 | from ase import units
8 | from ase.build import make_supercell
9 | from ase.constraints import ExpCellFilter
10 | from ase.md.langevin import Langevin
11 | from ase.md.npt import NPT
12 | from ase.md.nptberendsen import Inhomogeneous_NPTBerendsen
13 | from ase.md.velocitydistribution import MaxwellBoltzmannDistribution, Stationary
14 | from ase.optimize import FIRE
15 | from mace.calculators import mace_mp
16 | from pymatgen.io.ase import AseAtomsAdaptor
17 | from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
18 | from tqdm import tqdm
19 |
20 | from alchemical_mace.calculator import (
21 | DefectFrenkelLaddCalculator,
22 | FrenkelLaddCalculator,
23 | NVTMACECalculator,
24 | )
25 | from alchemical_mace.utils import upper_triangular_cell
26 |
27 |
28 | # Arguments
29 | parser = argparse.ArgumentParser()
30 |
31 | # Structure
32 | parser.add_argument("--structure-file", type=str)
33 | parser.add_argument("--supercell", type=int, nargs=3, default=[5, 5, 5])
34 |
35 | # Molecular dynamics: general
36 | parser.add_argument("--temperature", type=float, default=300.0)
37 | parser.add_argument("--pressure", type=float, default=1.0)
38 | parser.add_argument("--timestep", type=float, default=2.0)
39 | parser.add_argument("--ttime", type=float, default=25.0)
40 | parser.add_argument("--ptime", type=int, default=75.0)
41 |
42 | # Molecular dynamics: timesteps
43 | parser.add_argument("--npt-equil-stpes", type=int, default=10000)
44 | parser.add_argument("--npt-prod-steps", type=int, default=20000)
45 | parser.add_argument("--nvt-equil-steps", type=int, default=20000)
46 | parser.add_argument("--nvt-prod-steps", type=int, default=30000)
47 | parser.add_argument("--alchemy-equil-steps", type=int, default=20000)
48 | parser.add_argument("--alchemy-switch-steps", type=int, default=30000)
49 |
50 | # Molecular dynamics: output control
51 | parser.add_argument("--output-dir", type=Path, default=Path("results"))
52 | parser.add_argument("--log-interval", type=int, default=1)
53 |
54 | # MACE model
55 | parser.add_argument("--device", type=str, default="cuda")
56 | parser.add_argument("--model", type=str, default="small")
57 |
58 | args = parser.parse_args()
59 | args.output_dir.mkdir(exist_ok=True, parents=True)
60 |
61 |
62 | ################################################################################
63 | # Energy minimization: defect-free structure
64 | ################################################################################
65 |
66 | # Load structure
67 | atoms = ase.io.read(args.structure_file)
68 | atoms = make_supercell(atoms, np.diag(args.supercell))
69 |
70 | # Load universal MACE calculator and relax the structure
71 | mace_calc = mace_mp(model=args.model, device=args.device, default_dtype="float32")
72 | atoms.calc = mace_calc
73 | atoms = ExpCellFilter(atoms)
74 | optimizer = FIRE(atoms)
75 | optimizer.run(fmax=0.01, steps=500)
76 | atoms = atoms.atoms # get the relaxed structure
77 | initial_atoms = atoms.copy() # save the initial structure
78 |
79 |
80 | ################################################################################
81 | # Cell volume equilibration: defect-free structure
82 | ################################################################################
83 |
84 | atoms = initial_atoms.copy()
85 | atoms.set_calculator(mace_calc)
86 | bulk_modulus = 100.0 * units.GPa
87 |
88 | # Equilibration and volume calculation
89 | dyn = Inhomogeneous_NPTBerendsen(
90 | atoms,
91 | timestep=args.timestep * units.fs,
92 | temperature_K=args.temperature,
93 | pressure_au=args.pressure * 1.01325 * units.bar,
94 | taut=args.ttime * units.fs,
95 | taup=args.ptime * units.fs,
96 | compressibility_au=1.0 / bulk_modulus,
97 | )
98 | MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature)
99 | Stationary(atoms)
100 |
101 | # NPT equilibration and volume relaxation
102 | cellpar_traj = []
103 | for step in tqdm(range(args.npt_equil_stpes), desc="NPT equil"):
104 | dyn.run(steps=1)
105 | for step in tqdm(range(args.npt_prod_steps), desc="NPT prod"):
106 | dyn.run(steps=1)
107 | if step % args.log_interval == 0:
108 | cellpar_traj.append(atoms.get_cell().cellpar())
109 | abc_new = np.mean(cellpar_traj, axis=0)[:3]
110 |
111 | # Scale the initial cell to match the average volume
112 | atoms = initial_atoms
113 | atoms.set_cell(np.diag(abc_new), scale_atoms=True)
114 | atoms.set_calculator(mace_calc)
115 |
116 | # Relax the atomic positions
117 | optimizer = FIRE(atoms)
118 | optimizer.run(fmax=0.01, steps=500)
119 | initial_atoms = atoms.copy() # save the initial structure
120 |
121 |
122 | ################################################################################
123 | # MSD calculation: defect-free structure
124 | ################################################################################
125 |
126 | initial_positions = atoms.get_positions()
127 | # Using the reversible scaling MACE calculator with fixed scale of 1.0
128 | # since we can turn off the stress calculation
129 | calc = NVTMACECalculator(device=args.device, model=args.model)
130 | atoms.set_calculator(calc)
131 |
132 | # NVT MSD calculation
133 | dyn = Langevin(
134 | atoms,
135 | timestep=args.timestep * units.fs,
136 | temperature_K=args.temperature,
137 | friction=1 / (args.ttime * units.fs),
138 | )
139 | MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature)
140 | Stationary(atoms)
141 |
142 | temperatures = []
143 | for step in tqdm(range(args.nvt_equil_steps), desc="NVT equil"):
144 | dyn.run(steps=1)
145 | squared_disp = np.zeros(len(atoms))
146 | for step in tqdm(range(args.nvt_prod_steps), desc="NVT prod"):
147 | dyn.run(steps=1)
148 | squared_disp += np.sum((atoms.get_positions() - initial_positions) ** 2, axis=1)
149 | mean_squared_disp = squared_disp / args.nvt_prod_steps
150 |
151 | # Calculate spring constants and average over symmetrically equivalent atoms
152 | spring_constants = 3.0 * units.kB * args.temperature / mean_squared_disp
153 | structure = AseAtomsAdaptor.get_structure(initial_atoms)
154 | sga = SpacegroupAnalyzer(structure)
155 | equivalent_indices = sga.get_symmetrized_structure().equivalent_indices
156 | for indices in equivalent_indices:
157 | spring_constants[indices] = np.mean(spring_constants[indices])
158 |
159 | np.save(args.output_dir / "spring_constants.npy", spring_constants)
160 | np.save(args.output_dir / "masses.npy", atoms.get_masses())
161 |
162 |
163 | ################################################################################
164 | # Frenkel-Ladd calculation: defect-free structure
165 | ################################################################################
166 |
167 | atoms = initial_atoms.copy()
168 | calc = FrenkelLaddCalculator(
169 | spring_constants=spring_constants,
170 | initial_positions=initial_positions,
171 | device=args.device,
172 | model=args.model,
173 | )
174 | atoms.set_calculator(calc)
175 |
176 | # NVT Frenkel-Ladd calculation
177 | dyn = Langevin(
178 | atoms,
179 | timestep=args.timestep * units.fs,
180 | temperature_K=args.temperature,
181 | friction=1 / (args.ttime * units.fs),
182 | )
183 | MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature)
184 | Stationary(atoms)
185 |
186 | # Define Frenkel-Ladd path
187 | t = np.linspace(0.0, 1.0, args.alchemy_switch_steps)
188 | lambda_steps = t**5 * (70 * t**4 - 315 * t**3 + 540 * t**2 - 420 * t + 126)
189 | lambda_values = [
190 | np.zeros(args.alchemy_equil_steps),
191 | lambda_steps,
192 | np.ones(args.alchemy_equil_steps),
193 | lambda_steps[::-1],
194 | ]
195 | lambda_values = np.concatenate(lambda_values)
196 |
197 |
198 | def get_observables(dynamics, time, lambda_value):
199 | num_atoms = len(dynamics.atoms)
200 | return {
201 | "time": time,
202 | "potential": dynamics.atoms.get_potential_energy() / num_atoms,
203 | "temperature": dynamics.atoms.get_temperature(),
204 | "volume": dynamics.atoms.get_volume() / num_atoms,
205 | "lambda": lambda_value,
206 | "lambda_grad": dynamics.atoms._calc.results["energy_diff"] / num_atoms,
207 | }
208 |
209 |
210 | # Simulation loop
211 | calc.compute_mace = False
212 | total_steps = 2 * args.alchemy_equil_steps + 2 * args.alchemy_switch_steps
213 |
214 | observables = []
215 | for step in tqdm(range(total_steps), desc="Frenkel-Ladd"):
216 | if step == args.alchemy_equil_steps: # turn on MACE after spring equilibration
217 | calc.compute_mace = True
218 | lambda_value = lambda_values[step]
219 | calc.set_weights(lambda_value)
220 |
221 | dyn.run(steps=1)
222 | if step % args.log_interval == 0:
223 | time = (step + 1) * args.timestep
224 | observables.append(get_observables(dyn, time, lambda_value))
225 |
226 | # Save observables
227 | df = pd.DataFrame(observables)
228 | df.to_csv(args.output_dir / "observables.csv", index=False)
229 |
230 |
231 | ################################################################################
232 | # Cell volume equilibration: structure with a defect
233 | ################################################################################
234 |
235 | atoms = initial_atoms.copy()
236 |
237 | # Create a vacancy at the center of the supercell
238 | vacancy_index = len(atoms) // 2
239 | atom_mask = np.ones(len(atoms), dtype=bool)
240 | atom_mask[vacancy_index] = False
241 | del atoms[vacancy_index]
242 |
243 | atoms.set_calculator(mace_calc)
244 |
245 | # Equilibration and volume calculation
246 | dyn = Inhomogeneous_NPTBerendsen(
247 | atoms,
248 | timestep=args.timestep * units.fs,
249 | temperature_K=args.temperature,
250 | pressure_au=args.pressure * 1.01325 * units.bar,
251 | taut=args.ttime * units.fs,
252 | taup=args.ptime * units.fs,
253 | compressibility_au=1.0 / bulk_modulus,
254 | )
255 | MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature)
256 | Stationary(atoms)
257 |
258 | # NPT equilibration and volume relaxation
259 | cellpar_traj = []
260 | for step in tqdm(range(args.npt_equil_stpes), desc="NPT equil"):
261 | dyn.run(steps=1)
262 | for step in tqdm(range(args.npt_prod_steps), desc="NPT prod"):
263 | dyn.run(steps=1)
264 | if step % args.log_interval == 0:
265 | cellpar_traj.append(atoms.get_cell().cellpar())
266 | abc_new = np.mean(cellpar_traj, axis=0)[:3]
267 |
268 | # Scale the initial cell to match the average volume
269 | atoms = initial_atoms.copy()
270 | atoms.set_cell(np.diag(abc_new), scale_atoms=True)
271 | del atoms[vacancy_index]
272 | atoms.set_calculator(mace_calc)
273 |
274 | # Relax the atomic positions
275 | optimizer = FIRE(atoms)
276 | optimizer.run(fmax=0.01, steps=500)
277 |
278 |
279 | ################################################################################
280 | # Frenkel-Ladd calculation: structure with a defect
281 | ################################################################################
282 |
283 | calc = FrenkelLaddCalculator(
284 | spring_constants=spring_constants[atom_mask],
285 | initial_positions=initial_positions[atom_mask],
286 | device=args.device,
287 | model=args.model,
288 | )
289 | atoms.set_calculator(calc)
290 |
291 | # NVT Frenkel-Ladd calculation
292 | dyn = Langevin(
293 | atoms,
294 | timestep=args.timestep * units.fs,
295 | temperature_K=args.temperature,
296 | friction=1 / (args.ttime * units.fs),
297 | )
298 | MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature)
299 | Stationary(atoms)
300 |
301 | # Simulation loop
302 | calc.compute_mace = False
303 | total_steps = 2 * args.alchemy_equil_steps + 2 * args.alchemy_switch_steps
304 |
305 | observables = []
306 | for step in tqdm(range(total_steps), desc="Frenkel-Ladd"):
307 | if step == args.alchemy_equil_steps: # turn on MACE after spring equilibration
308 | calc.compute_mace = True
309 | lambda_value = lambda_values[step]
310 | calc.set_weights(lambda_value)
311 |
312 | dyn.run(steps=1)
313 | if step % args.log_interval == 0:
314 | time = (step + 1) * args.timestep
315 | observables.append(get_observables(dyn, time, lambda_value))
316 |
317 | # Save observables
318 | df = pd.DataFrame(observables)
319 | df.to_csv(args.output_dir / "observables_defect.csv", index=False)
320 |
321 |
322 | ################################################################################
323 | # Cell volume equilibration: partial Frenkel-Ladd calculation
324 | ################################################################################
325 |
326 | atoms = initial_atoms.copy()
327 | atoms.set_calculator(mace_calc)
328 |
329 | # Equilibration and volume calculation
330 | dyn = Inhomogeneous_NPTBerendsen(
331 | atoms,
332 | timestep=args.timestep * units.fs,
333 | temperature_K=args.temperature,
334 | pressure_au=args.pressure * 1.01325 * units.bar,
335 | taut=args.ttime * units.fs,
336 | taup=args.ptime * units.fs,
337 | compressibility_au=1.0 / bulk_modulus,
338 | )
339 | MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature)
340 | Stationary(atoms)
341 |
342 | # NPT equilibration and volume relaxation
343 | for step in tqdm(range(args.npt_equil_stpes), desc="NPT equil"):
344 | dyn.run(steps=1)
345 |
346 |
347 | ################################################################################
348 | # Alchemical switching
349 | ################################################################################
350 |
351 | # Set up the partial Frenkel-Ladd calculation
352 | calc = DefectFrenkelLaddCalculator(
353 | atoms=atoms,
354 | spring_constant=spring_constants[vacancy_index],
355 | defect_index=vacancy_index,
356 | device=args.device,
357 | model=args.model,
358 | )
359 | atoms.set_calculator(calc)
360 | upper_triangular_cell(atoms) # for ASE NPT
361 |
362 | # NPT alchemical switching
363 | ptime = args.ptime * units.fs
364 | pfactor = bulk_modulus * ptime * ptime
365 |
366 | dyn = NPT(
367 | atoms,
368 | timestep=args.timestep * units.fs,
369 | temperature_K=args.temperature,
370 | externalstress=args.pressure * 1.01325 * units.bar,
371 | ttime=args.ttime * units.fs,
372 | pfactor=pfactor,
373 | )
374 |
375 | # Define alchemical path
376 | t = np.linspace(0.0, 1.0, args.alchemy_switch_steps)
377 | lambda_steps = t**5 * (70 * t**4 - 315 * t**3 + 540 * t**2 - 420 * t + 126)
378 | lambda_values = [
379 | np.zeros(args.alchemy_equil_steps),
380 | lambda_steps,
381 | np.ones(args.alchemy_equil_steps),
382 | lambda_steps[::-1],
383 | ]
384 | lambda_values = np.concatenate(lambda_values)
385 |
386 | calculate_gradients = [
387 | np.zeros(args.alchemy_equil_steps, dtype=bool),
388 | np.ones(args.alchemy_switch_steps, dtype=bool),
389 | np.zeros(args.alchemy_equil_steps, dtype=bool),
390 | np.ones(args.alchemy_switch_steps, dtype=bool),
391 | ]
392 | calculate_gradients = np.concatenate(calculate_gradients)
393 |
394 |
395 | def get_observables(dynamics, time, lambda_value):
396 | num_atoms = len(dynamics.atoms)
397 | alchemical_grad = dynamics.atoms._calc.results["alchemical_grad"]
398 | return {
399 | "time": time,
400 | "potential": dynamics.atoms.get_potential_energy() / num_atoms,
401 | "temperature": dynamics.atoms.get_temperature(),
402 | "volume": dynamics.atoms.get_volume() / num_atoms,
403 | "lambda": lambda_value,
404 | "lambda_grad": alchemical_grad / num_atoms,
405 | }
406 |
407 |
408 | # Simulation loop
409 | total_steps = 2 * args.alchemy_equil_steps + 2 * args.alchemy_switch_steps
410 |
411 | observables = []
412 | for step in tqdm(range(total_steps), desc="Alchemical switching"):
413 | lambda_value = lambda_values[step]
414 | grad_enabled = calculate_gradients[step]
415 |
416 | # Set alchemical weights and atomic masses
417 | calc.set_alchemical_weight(lambda_value)
418 | calc.calculate_alchemical_grad = grad_enabled
419 |
420 | dyn.run(steps=1)
421 | if step % args.log_interval == 0:
422 | time = (step + 1) * args.timestep
423 | observables.append(get_observables(dyn, time, lambda_value))
424 |
425 | # Save observables
426 | df = pd.DataFrame(observables)
427 | df.to_csv(args.output_dir / "observables_FL.csv", index=False)
428 |
--------------------------------------------------------------------------------