├── .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 | [![arXiv](https://img.shields.io/badge/arXiv-2404.10746-84cc16)](https://arxiv.org/abs/2404.10746) 3 | [![Zenodo](https://img.shields.io/badge/DOI-10.5281/zenodo.11081492-14b8a6.svg)](https://zenodo.org/doi/10.5281/zenodo.11081492) 4 | [![MIT](https://img.shields.io/badge/License-MIT-3b82f6.svg)](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": "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", 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": "iVBORw0KGgoAAAANSUhEUgAAAWkAAAEFCAYAAAAhTRZvAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAABW50lEQVR4nO3dd1zV1f/A8dcdcJmCgCwFt6Kioqi4QXObaZmllTv7fdOG2tJMraw0K+2rmZYNLCu/WeqXrxmWA/cWEheKspQhiux57z2/P65ghCj7XuA8H93wfub7KPfN4XzOUAghBJIkSZJJUho7AEmSJKl0MklLkiSZMJmkJUmSTJhM0pIkSSZMJmlJkiQTJpO0JEmSCZNJWpIkyYSpjR2AqdLr9cTHx2Nra4tCoTB2OJIk1TFCCDIyMnB3d0epLL2+LJN0KeLj4/Hw8DB2GJIk1XFxcXE0adKk1P0ySZfC1tYWMPwFNmjQwMjRSJJU16Snp+Ph4VGUa0ojk3QpCps4GjRoIJO0JEnV5kHNqfLBoSRJkgmTSVqSJMmEySQtSZJkwmSSliRJMmEySUuSJJkwoyfp/fv3M2rUKNzd3VEoFGzbtq3Y/i1btjBkyBAcHR1RKBSEhYWV6/qbNm1CoVAwZsyYKotZkqT6RwjB7dzbXL59mcPxhwm6EsTXJ1bw4Y7pvPbjQOZu7Fct9zV6F7ysrCw6d+7MtGnTeOyxx+65v2/fvjzxxBPMmDGjXNeOjo7m1VdfpV+/6vnLkyTJdCVlJRF+M5zI1Ej0Qo8CBYb/DF3eFChQKBRFX/++LU+bR3JOMjdzbnIz52bRn7V6ban3UwuBPjcNpYVdlZbD6El6+PDhDB8+vNT9EydOBAwJtzx0Oh1PP/0077zzDgcOHCA1NbUSUUqSZMpytblcSLnAmeQz/JX8F2eSz5CUnVQt97LX6XC682qk0+Nk4YRTw5Y0cvVBr1RXefOE0ZN0dXn33XdxdnZm+vTpHDhw4IHH5+XlkZeXV/Q+PT29OsOTJKmChBDEZcQVJeMzN89wKeUSWlG8lqtUKGnTsA1eDl5oVBqEEAgMS7oKBIXLuxb+WegKIDcNkZOCWfYtnNISaZSVcjcha3U46sHc3Qea9YVm/cDDDyyqd7BbnUzSBw8e5Ouvvy5X+/XSpUt55513qi8oSZIqLDM/kz1xe/gz+k/CksNIzUstcYyTpROdG3Wmo1NHOjXqRAfHDliZWZW8mBCQkQhJZyHxDCSehcRwSLkCQl/8WIUK3LvcScp9ayQp/1OdS9IZGRlMnDiR9evX4+TkVObz5s+fz9y5c4veF46rlyTJOPJ1+Ry4foAdV3ew79o+8nR3f9M1V5rT3rE9HRsZEnJnp864WruWHGKt10FyhCEJJ565k5jDIfvWvW9q7Qyu3uDWGZr2BU8/0Nx/bo3qVueS9JUrV4iOjmbUqFFF2/R6w09HtVpNREQELVu2LHGeRqNBo9HUWJySJJWk0+s4mXSSHVE7+DP6TzIKMor2NWvQjBEtRtCvcT/aNmyLmcqs5AX0ekMijj4I0Qcg5hDkppU8TqEEpzbg4g2uHQ2J2aUj2LpUY+kqps4laS8vL8LDw4tte+utt8jIyODf//63rB1LkokRQnDu1jl+u/obO6N3kpyTXLTP2cqZEc1HMKL5CLwcvO5RU9bDjfN3k3L0QchNLX6MuQ24djIkYteOhsTs3A7MLKu/cFXA6Ek6MzOTyMjIovdRUVGEhYXh4OCAp6cnKSkpxMbGEh8fD0BERAQArq6uuLq6AjBp0iQaN27M0qVLsbCwwNvbu9g97O3tAUpslyTJeOLS4wi6GsSOqzuIzYgt2t7AvAFDmg1hRPMR+Lr4olT8rb+EEHDjwp2kvB+iD0FOSvELm9uAZy9DG3LzfuDaGVRGT3UVZvTIT548yYABA4reF7YLT548mcDAQIKCgpg6dWrR/vHjxwOwePFi3n77bQBiY2Pvu7KBJEmmIy0vjbV/rWXTxU3ohA4AC5UFAzwGMKLFCPq49ynZlCEEhP0Ie5ZARkLxfWbW4NnzTlLub2hPvldTSC2lEIX9UKRi0tPTsbOzIy0tTc4nLUlVQKfX8evlX1kdurqod0Yvt1480uoRBnoMvHdPDIDkS7B9DsQcNLxXWxZPyu5damVSLmuOMXpNWpKkuu9E4gmWHV/GpduXAGhp15I3erxBL/depZ9UkAsHPoGDK0FfYEjOAfOg5/Ogrj8P+WWSliSp2sRnxvPJyU/4I+YPAGzNbZnlM4sn2z6JWnmf9HNlL/w2F1KuGt63HgIjPoaGTWsgatMik7QkSVUuR5vDN2e/4duz35Kny0OpUDKuzThm+cyioUXD0k/MvAE7F0D4z4b3tm4wbBm0Hw0PWGaqrpJJWpKkKiOEIDg6mE9OflI0d0Y3l27M6zGPtg5tSz9Rr4fTG2DX4jv9mhXQ4zkY+FaNj/AzNTJJS5JUJc7fOs+Hxz/k9I3TALhbu/NKt1cY3HTw/RdbTTpneDAYd8zw3rUTjPoUGvtWf9C1gEzSkiRVSp4uj5WnVvLjhR8RCCxUFkzvOJ0pHaZgobYo/cT8bNj3IRz5DPRaQ1e6gW8ZatC1uF9zVZN/E5IkVVhMegyvhbzChduGQWbD1Y7MVTnjenY3hP0G2jzQ5hq+FuQUf6/NhTuz0uH1MAz/EOyaGK8wJkomaUmSKiQ44hfePvYBWaIAe52O95Nv0T8n9sEn/p2dhyE5e42sniDrAJmkJUkql9xbl1m+ezab7yTkrrm5fJitwtV3Jlg7Gfowqy3uvDSG/s1F2/7x1doJlCojl8i0ySQtSVLZxIcRdfBDXk0L5ZK5GQoheLZAw8we81B3HAdqc2NHWCfJJC1JUumEgMhdcHgV22+c5F0nB3LMzXBAydIOM+jdbVa97b9cU2SSliSpJG0ehP8Ch1eTe/Miyxwb8quzYRGNbg3b8eGgz3C2cjZykPWDTNKSVJ9o8wyrkvz9lXWr5LYbFyDrBlfN1LzS2J1IMxUKFDzX6Tn+1flf9x/SLVUp+TctSXXV7Rg4/1+4FAxp1wzJNz+zzKcHOTXmvQYacoQWRwtHlvZbev8JkaRqIZO0JNUlKVGGxHx+G8SH3vsYhQqsHO++rB2Lvc+xaMAHiSFsSzoKQoufqx/L+i/DybLsa4ZKVUcmaUmq7W5duZuYE/66u12hhKZ9DJMTufmAlYMhEVvYlfqwLyU3hRd2v0D4zXAUKHi+8/M81+k5VLKbnNHIJC1JtdHNSENSPr/NsPp1IYUSmvUzJOZ2o8Cm7A/34jLieH7X88Skx2CnseMT/0/wc/Or8tCl8pFJWpJqi6ybcPo7OPurYUXsQgqVYYWSwsRsXf5miXO3zjFz10xSclNwt3Zn7eC1tLBrUYXBSxUlk3QV0wt98YUzJamyrp2C41/CuS2gyzdsU6qhuT90GANtRxralSvo0PVDzAmZQ442h7YN2/L5oM9l9zoTIpN0FckuyGb23tmcu3WOXeN2YamuHcvFSyaqIBfObTUk5/jTd7e7dwHfqYYas5VDpW8TdCWIxYcWoxVa/Nz8+DTgU2zMbSp9XanqGL3Kt3//fkaNGoW7uzsKhYJt27YV279lyxaGDBmCo6MjCoWCsLCwB15zy5YtdOvWDXt7e6ytrfHx8eH777+vngLcYam25GraVdLz0zmTfKZa7yXVYalxsOsdWNketv3LkKBV5tBpPDy7B54LAd/JlU7QQgi+Cv+KBQcXoBVaRjQfwdqH1soEbYKMXpPOysqic+fOTJs2jccee+ye+/v27csTTzzBjBkzynRNBwcHFixYgJeXF+bm5mzfvp2pU6fi7OzM0KFDq7oIACgUCuyVXiSRxNH4E/KBi1R2QkDUfkOtOWIHCL1he4Mm0H0adJkENo2q7HY6vY6lx5fyn4j/ADC1w1Rm+86WzXQmyuhJevjw4QwfPrzU/RMnTgQgOjq6zNcMCAgo9v7ll19mw4YNHDx4sNqSdFaelogoJ2gEX53YRWbiQJ7p2ZSmjtbVcj+pDshNgzM/w/H1cDPi7vbm/Q0T37cZXuWT3+dqc5l3YB67Y3ejQMHr3V/nmfbPVOk9pKpl9CRd3YQQ7Nmzh4iICD788MNSj8vLyyMvL6/ofXp6ernv9bTPAH64/itCE8P6g5f56mAUAW0aMal3M/xbN0KplBPR1HvZKYba8vkguLr37oNAM2vwmQDdnwXndtVy67S8NF7c8yKhN0IxU5rxQb8PGNZsWLXcS6o6dTZJp6Wl0bhxY/Ly8lCpVHz++ecMHjy41OOXLl3KO++8U+H7WWvUvPFQf37/2YGU3BR8W2dy6pI9eyOS2RuRTFNHKyb2bMo4Xw/srMwqfB+pFspMhovbDQNOog8Ylooq1MgLuk2DzuMNg0yqSUJmAv/a9S+upl3F1syWfw/8N91du1fb/aSqU2eTtK2tLWFhYWRmZrJ7927mzp1LixYtSjSFFJo/fz5z584tep+eno6Hh0e57qlQKPB18eXPmD8Z1i2bjx8Zw8ajMfx8Mo6YW9m899sFPv4jgjE+jZnYqykd3KvvQykZWXrC3cQcc+huOzOAi/edPs2PgLNXtYcSkRLBzF0zuZFzA2crZ9YNWkfrhq2r/b5S1aizSVqpVNKqVSsAfHx8uHDhAkuXLi01SWs0GjQaTaXvW5ikTyWdYkanGSx8uD2vDGnDf8Pi2XA4mouJGWw6EcemE3F0a9qQSb2bMayDK+Zq+dCm1kuNgwv/MyTmuGMUrd8Hhq5z7R4xJGfHljUWUuiNUGbumklmQSat7FuxdtBaXK1da+z+UuXV2ST9T3q9vlibc3Xp5tINMHw4tHotaqUaK3M1E3p4Mr67Byeib/PdkWiCzyZyMuY2J2Nu42yrYXLvZjzt54m9lVzdotYQAm6ch4u/GV4JYcX3N+kB7R8xJOeGTWs8vBOJJ5i1exY52hy6Ondl1cBV2Gnkb2+1jdGTdGZmJpGRkUXvo6KiCAsLw8HBAU9PT1JSUoiNjSU+Ph6AiAjDU3BXV1dcXQ01gkmTJtG4cWOWLl0KGNqXu3XrRsuWLcnLy2PHjh18//33rF27ttrL07pha2zNbcnIz+BiykW8nbyL9ikUCno0d6BHcwdupOfy4/FYfjwWy42MPD7aGcFneyJ5olsTpvVtLnuFmCqdFuKOwsUdhuaM1Ji/7VRA096G2rLXw2DX2GhhHks4xgu7XyBXl0tPt56sGrhKDrCqpYyepE+ePMmAAQOK3he2C0+ePJnAwECCgoKYOnVq0f7x48cDsHjxYt5++20AYmNjUSrvNhdkZWUxc+ZMrl27hqWlJV5eXmzcuJEnn3yy2sujVCjxdfYl5FoIp5JOFUvSf+fcwILZg9owM6AV//srnvUHrnIxMYMNR2L47mgMQ9q7MKNfC3ybNkQhlycyrvxsuLLHUFu+FAw5KXf3qS2gxQDwGgFthpVrQqPqcvj6YV7a+xJ5ujz6NO7Dvwf8G42q8k15knEohBDiwYfVP+np6djZ2ZGWlkaDBg3KdW7g2UA+OfUJAR4BrB64ukznCCE4FHmLrw5eJSQiuWi7j4c9z/ZrzrAOrqhVst26xmTdhIjfDd3lruwBbe7dfZYNDQnZayS0HAjmpvNbz/5r+5mzdw75+nz8m/izImAF5irZhGaKyppjjF6Trou6uRrapU8nnS7zhEsKhYK+rZ3o29qJS0kZfH0giq1h1wmLS+WFH0Np0tCSqX2a82R3D2w08p+t2qQnwIFP4FQg6Avubrf3NDRheI0Ej55VPsikKoTEhTA3ZC4F+gIGegzkY/+PMVPJ7p61naxJl6IyNWmtXkvvn3qTo83hl1G/0NahbYViSM7I4/ujMWw8GkNKlmHQg61GzQQ/Tyb1akqThlYVuq50D1k34eBKOPHV3Vqza6e7idmlg0mvir07Zjev7nsVrdAyuOlgPuz/IWZKmaBNWVlzjEzSpahMkgb4vz//j8Pxh5nfYz5PtXuqUrHkFuj49fQ1vj4YxdXkLACUChjo5cKkXk3p28pJjmasqJzbcPgzOLoWCgx/t3j0hIFvQfN+xo2tjHZG7+SN/W+gEzqGNxvOB/0+kAvF1gKyucPIurl043D8YU4mnax0krYwU/G0X1MmdPdkb8QNvjkUxaHIW+y6kMSuC0k0d7LmaT9POZqxPPIy4Og6OLwa8tIM29x8YOBCaPWQSdea/27H1R28efBNdELHwy0eZkmfJTJB1zGyJl2KytakTyedZnLwZBwsHAh5IqTKe2hE3shk49EYfj11jYw8wzBjCzMlY3wa80zPpng3lv1h7yk/29CkcXDl3V4azu1hwAJDs0YtSc4A/7vyP9469BZ6oWd0y9G80/sduRZhLSKbOyqpskk6X5dP7596k6fLI2hMEM3tmldDlIbZ97aFXef7IzFcTMwo2t7V055JvZoxvKMrGrX84KLNg1Mb4MDHkJlk2ObYCgLmQ4fHQFm7es5svbyVxYcXIxCMbT2WRb0WyalGaxnZ3GFk5ipzOjXqxInEE5xMOlltSdpao+Zpv6Y81cOTE9G3+f5oDL+HJ3A6NpXTsWEs2W7O+B4ePOXXlMb29WwwgxBw8xJE7jK0OafFGbbbe4L/POj0pEn20niQzZc28+6RdwF4su2TvOn3pkzQdVjt+w6tRXxdfDmReIJTSacY12Zctd6r2GjGh9ux6XgcPx6LJTE9lzV7r7Bu31We7duclwe1xsq8Dv+zZ96AqyFwZa/ha0b83X22btD/NegyEdS1s+/wpoubeP/Y+wA80+4ZXu/+uhzsVMfV4U+r8fm6+AJwMvEkQoga+zA521rw0kOtmRnQkl0XkthwOIYjV2/xxf6r/BaewJIx3gxoa/yRcVUiPxtiDhvmZr4aUnwVbQCVBpr2MizW2nUimNXe3ya2XN5SlKCndJjCXN+5MkHXAzJJV6POjTqjVqhJyk7ieuZ1mtg2qdH7q1VKhnm7MczbjT0Xk1i47RzXbucw9dsTPNzJjUUPt8e5gUWNxlRper1hIqOrew215bhjdyfOL+Ta0TBUu+UA8OxVqxNzoeCoYN4+/DYAk9tPlgm6HpFJuhpZqi3p4NSBv5L/4lTSqRpP0n830MuFnnMdWfnnJb45FM32Mwnsu5TMG8O8eKqHZ+3oZ33tJPz2SsnZ5ho0gZYBhsTcIgCsnYwQXPXZF7eP+QfmIxA80eYJXun2ikzQ9YhM0tXM18W3KEmPbjXaqLFYmatZMLI9o30a8+bWcM5cS+OtbWfZGnqdDx7tSFtXW6PGV6qsm7DrbQi9s+K7mbUhGbe8k5QdW9WqrnPlcTzhOHND5qIVWka2GMmCngtkgq5n5CPhalY4v/TJpJNGjuQu78Z2bJ3Zh8Wj2mNtruJUzG1GrjrA8uCL5BbojB3eXXqdYZHW1b53E3Tnp+DlMJjwI/SYAU6t62yC/iv5L17Y8wL5+nwGeAxgSZ8lshdHPST/xauZj7MPSoWSuIw4krKSjB1OEZVSwdQ+zdn1ij9D2rug1Qs+D7nCkJX7OXA5+cEXqG5xJ2D9ANjxKuSmGtqZp/0Bj641ielAq1tESgTP73qeHG0OPd168pH/R3IujnpKJulqZmtuS9uGhgmWTt84beRoSnKzs+TLSd34YqIvrg0siE3JZuLXx3l5Uyg3M6t/JZsSsm7Cf2fB14Mg4S/Q2MHwj2BGCHj61Xw8RhCVFsVzfz5HRn4GPo185HzQ9ZxM0jWgcOrSk4mm0+TxT0M7uLLrFX+m9G6GQgH/DYtn8Ip97LtUQ7XqoqaNrhC60bDN5xl48RT4PVcrB51URHxmPDP+mEFKbgrtHNqxZtAarMzkbIf1mUzSNaCwv/SppFNGjuT+bDRq3n6kA9tm9qGdWwNuZxcw5dvj/HvXZfT6apw9IO44fBlwp2kjzdC0Mf1PGLMGbBpV331NTHJ2Ms/+8SxJ2Uk0t2vOusHraGBe/ikJpLpFJuka0NW5KwBX0q6QkpvygKONr7OHPVtn9mZCD0+EgJW7LjFtwwlSs/MffHJ5ZCbDtlnw9WBIPAMWdjDiY3huH3j0qNp7mbjU3FSe+/M54jLiaGzTmPWD1+Ng4WDssCQTIJN0DWho0ZBW9q0Aw+x4tYGFmYqlj3Xko8c7oVErCYlI5uHVBwm/llb5i+u0cOwLQ6+NsDtNG12egRdOGXps1LOZ3DLzM3l+1/NEpkbSyLIR64esx8XaxdhhSSZCJukaUluaPP5pXDcPtszsjaeDFddu5zB23WE2HY+t+AWjD8EX/eH31w3zOLt1NjRtjK5fTRuFcrQ5vLDnBc7eOou9xp71Q9bjYeth7LAkEyKTdA0p7C9d25I0QAd3O/73Yl8GtXMmX6tn3pZwXv/lr/L1qU5PgF9nQOAIuHHOsJjrwythxt5617RRqEBXwJyQOZxKOoWNmQ3rBq+jpX1LY4clmRijJ+n9+/czatQo3N3dUSgUbNu2rdj+LVu2MGTIEBwdHVEoFISFhT3wmuvXr6dfv340bNiQhg0bMmjQII4fP149BSijwpr0xZSLpOenGzWWirCzNOPLid14bWhblAr4+eQ1Hvv8MLG3su9/ojYfDq2Cz7pB+M+AArpNgxdPG77Ws6aNQjq9jnkH5nHo+iEsVBaseWgNHRw7GDssyQQZPUlnZWXRuXNn1qxZU+r+vn378uGHH5b5miEhIUyYMIG9e/dy5MgRPDw8GDJkCNevX6+qsMutkVUjmjZoikAQdiPMaHFUhlKpYNaAVnw/3Q9Ha3POJ6Tz8OoD7L5QyiCdK3tgXR/4cyHkZ0KT7vBciKEGbVV/H4oJIfjwxIf8EfMHaqWafw/4N11duho7LMlEmdTKLAqFgq1btzJmzJgS+6Kjo2nevDmhoaH4+PiU67o6nY6GDRvy2WefMWnSpHsek5eXR17e3cEb6enpeHh4VHhllntZfHgxWy5vYar3VOb6zq2SaxpLQloOM384TWhsKgAvDGjFnMFtUCkVkBoLOxfAhSDDwdaNYPC70Gl8rVsBpTp8Ff4V/z79bwA+6v8Rw5oPM3JEkjGUdWWWevGJyc7OpqCgAAeH0mtvS5cuxc7Orujl4VH1D2+K2qUTa1+79D+52Vnyn+d6MaV3MwA+2xvJ9K8PkP3nUvishyFBK1TQc6ZhQIrPUzJBA9sitxUl6De6vyETtPRA9WIY1xtvvIG7uzuDBg0q9Zj58+czd+7d2m1hTboqFbZLn791nuyC7Fo/ksxcreTtRzrQxcOOPVu+Ym7cD1hdvwGAaNoHxYiPwaW9kaM0Hfuv7S+aE3qq91Seaf+McQOSaoU6n6SXLVvGpk2bCAkJwcKi9AnuNRoNGk31zo/gbuOOm7UbCVkJ/JX8F73ce1Xr/WpE7DFGn3qL0SrDg9kE4cD7BU8TkTKIhWmN6C+7+wJwJvkMr+57FZ3QMarFKGZ3nW3skKRaok7//vnxxx+zbNky/vjjDzp16mTscADTnLq0Qm5dgZ8nwTdD4NpxMLNC138eIYN3cMiiP5eTs5j0zXGmB57ganKmsaM1qui0aGbtnkWONoc+7n14p887cspRqczqbE16+fLlvP/+++zcuZNu3boZO5wivi6+/O/q/2plf2kAslNg33I48RXoC0ChNIwWHLAAla0rE4ARXVuxas9lNhyOZvfFG+y7lMzk3s146aHW2FnWr+k2k7OT+deuf5Gal0oHxw6sCFghpxyVysXoSTozM5PIyMii91FRUYSFheHg4ICnpycpKSnExsYSH29Y9TkiIgIAV1dXXF1dAZg0aRKNGzdm6dKlAHz44YcsWrSIH3/8kWbNmpGYmAiAjY0NNjY2NVm8EgrbpcOTw8nT5dWeKSgLcuH4F7D/E8NIQYBWgw29Nv7R7mxnZcbCh9vzlJ8n7/92gT0Xb/D1wSi2hl5nzuA2TOjugVpV92uSGfkZPL/rea5nXsfT1pM1D8kZ7aQKEEa2d+9eAZR4TZ48WQghxLfffnvP/YsXLy66hr+/f9HxQgjRtGnTB57zIGlpaQIQaWlpVVPQO/R6vQj4T4DwDvQWJxJOVOm1q4VOJ8SZzUKs8BZicQPD6/M+QkTuLvMlQiJuiEGfhIimb2wXTd/YLoas2CcOXEquxqCNL0+bJ6YFTxPegd6i/6b+IjY91tghSSamrDnGpPpJm5Ky9mGsiFf3vcrO6J284PMC/9f5/6r02lUq+iD88RbEhxre27rDQwuh05PlHimo1en58XgsK/68RGp2AQCD2rmwYGQ7mjtZV3XkRqUXel7f/zo7o3dipbbi22Hf0t5R9nKRipP9pE2YyU+2dOsK/PQUBI40JGhzGxj41t/6O5d/KLdapWRSr2aEvBrAlN7NUCkV7LqQxJCV+/hi3xXqSl1BCMHyE8vZGb0TtVLNpwM+lQlaqhSjt0nXR4U9PMKSwyjQF5jOg6Sc24aHgse/BL3WMBjFdwoEzKuydQXtrcx5+5EOPNPTk/d+u0BIRDJLf7/IhYR0lo3thIVZ7Z7L45uz3/DDhR8AeL/P+3Wjm6VkVLImbQQt7Vtip7EjR5vDhVsXjB0O6AoM8zuv6gJHPzck6FaD4fnD8PCKaln4tZWzLd9O6c6S0R1QKRVsC4vniS+OkJiWW+X3qin/jfwvn57+FIDXur3GiBYjjBuQVCeUqSYdFBRU7gsPHjwYS0vLcp9XHygVSro6d2Vv3F5OJp2kUyMj9eEWAi7/YWh3vnnJsK1ROxj6HrQqfXRmVVEoFEzs1YyWzjbM+uE0Z66lMeqzg3wx0Zeung2r/f5V6Uj8ERYfXgzAlA5TmNTh3nPESFJ5lenBobKccy4oFAouX75MixYtKhyYsVXng0OA7859x0cnP6J/k/6seejeMwBWq6TzsPNNuLrX8N7KEQYsgK6TjbLoa1xKNs9uOElEUgbmKiUfPNaRx32b1HgcFZGcnczj/3uclNwURrYYyQd9P5CDVaQHqvIHh4mJiej1+jK9rKxkX9AH8XU1PDwMTQpFpy/H5PmVlZkM/5ttmEL06l5QmUPvl+ClUOg+3Wircns4WPHrzN4Mae9Cvk7Pq5v/4r3t59Hq9EaJp6wK54VOyU2hTcM2vNNbjiaUqlaZvpsmT55crqaLZ555plpqn3VJ24ZtsTazJqMgg0u3L1X/DbV5cPBTWN0VTn0LQg/tHoFZx2DIEsMisEZmo1Gz7hlfXhpoWA/yq4NRTNtwkrQ7XfZM0ZfhX3I88TiWaks+9v+49gxOkmqNMiXpb7/9Fltb2zJfdO3atTg5OVU4qPpArVTTxbkLUANd8S7ugDU9YNdiyEs3rCs4ZQc8+T04mFaTlFKpYO6Qtqx5qiuWZir2X0pmzOeHiLxhevN/nEg8wbq/1gGwsOdCmts1N3JEUl1UJb+X6XQ6tm3bxujRo6vicvVGtfeXTrsOm56GTRPgdjTYusGYtTAjBJr1qZ57VpGRndz45fleNLa3JOpmFo+uOcTeizeMHVaRlNwU5u2fh17oGd1yNKNajjJ2SFIdVakkfebMGebMmYO7uzsTJkxAoVBUVVz1QmF/6dM3TlftYA69Do6uM9SeL24HpRr6zql1k+93cLfjvy/0oUczBzLytEzbcIJ1JjDwRS/0LDi4gBs5N2hu15w3/d40ajxS3fbAp0RZWVlYWFigUhkGGaSkpPDDDz8QGBjImTNn0Ov1rFy5kmnTphl98qLapp1jO9RKNSm5KcRnxdPYpnHlL5rwF/zv5btDuZv0gFGfgkvtXOTUyUbDxmf9WBx0jp+Ox7Ls94tcTEjnvUc7YqMxzkPODec2cPD6QTQqDR/7fywnTZKq1X2rVOvXr6d169Z06dKFlStX8vjjj+Pu7s53333HpEmTuHbtGkqlkkGDBskEXQEalYa2DdsCEH4zvHIXy8s0rCv4ZYAhQWvsYOQKmLaz1iboQuZqJR886l1s4MvgFfv441xijccSdiOMVadXATCvxzzaNGxT4zFI9ct9qyJLlixh69attGjRAjc3N1588UX++usv2rZtW1Px1XneTt6cu3WOs8lnGdasguvdRQTDjlchLc7wvsOjMGwZ2LpWXaBGVjjwpbWLLa//cobYlGye+/4UQzu48M4j3rjalb7qTlVJy0vj9f2voxVahjcbztjWY6v9npJ035p0mzZt2Lx5Mxs3bsTe3p7169fz7rvvEhwcbPR2wbqio1NHoII16fQE+M9E+OlJQ4K284SnNsO4wDqVoP+uZwtHds7uz/MBLVEpFew8l8SgFfv47kg0On31fU8KIVh0aBEJWQl42HqwqNci+QxGqhH3TdI//vgjarWavLw8rl+/zqFDh3B2dmbSpEm4ubnx8ssvA8hv1kro2MiQpM/fOo9Wry3bSXodHF8Pn3W/uyp375dg1lFoM6QaozUNluYq3hjmxfYX++LjYU9mnpZF/z3H2LWHuZCQXi33/OniT+yJ24NaqeYj/4+wMZfNe1LNqNB80lqtlqCgIAIDAwkODqZZs2Y8/vjjPP7443Tt2rU64qxx1T0svJBe6OnzUx8yCzL5ZdQvtHV4QFPSrSuw5Tm4fmeNxMa+MOrf4Nqx2mI0ZTq94IdjMSwPjiAzT4taqeDZfi14+aHWWJpXzYx652+d55kdz1CgL2Bej3k83e7pKrmuVL9V63zSarWaxx57jKCgIOLi4pgxYwb//e9/6d69e4UDrq+UCiUdnAwP9h7Y5BERDF8OMCRoc1sY8TFM/7PeJmgAlVLBpF7N2DXXn2EdXNHqBev2XWHop/vZfym50tfPzM/ktX2vUaAvYIDHAJ7yeqoKopaksqt0h1kXFxdee+01zp07x5EjR6oipnrnge3Sej2EfGhoe85LAw8/w3DuHjMqNAF/XeRqZ8G6ib6sn9QNNzsLYlOymfTNcV7eFMrNzLwKXVMIwbtH3iU2IxY3azeW9Fkim/akGlemJF3YH/pBevToAcC5c+fQasvYvirdP0nnpsF/noaQDwzvuz8Lk7eDXRX0qa6DBrd34c+5/kzp3QyFAv4bFs9Dn+zj+6Mx5OSXbyKrLZe38Hv076gUKpb3X46dxvjzm0j1T5mSdJcuXbh161aZL9qrVy9iY2MrHFR9U5ikr6ReIbsg++6OGxdh/UCI2AEqDYz+HEZ+AmpzI0VaO9ho1Lz9SAe2zexDe7cGpOUUsHDbWfw+2MWS7ee5mvzgeUAu377M0uOG1edf6voSPs4+1Ry1JN1bmYZsCSFYuHBhmacgzc/PL3MA+/fv56OPPuLUqVMkJCSwdetWxowZU7R/y5YtrFu3jlOnTpGSkkJoaCg+Pj73vea5c+dYtGgRp06dIiYmhpUrVzJ79uwyx1TTGlk1wsXKhaTsJM7dOkd31+5w/r+wbSbkZ0KDJobJkBrXjYeyNaWzhz1BL/Th+6MxfH0wimu3c/j6YBRfH4yibysnnunpyaB2LqhVxesqOdocXtv3Gnm6PPo07sOUDlOMUwBJooxJun///kRERJT5or169Srz1KZZWVl07tyZadOm8dhjj91zf9++fXniiSeYMWNGma6ZnZ1NixYtGDduHHPmzClz3MbU0akjSbFJnE0+Q/ezO+DgCsOOZv0M/Z6t5ayCFaFWKZnapzmTejVj/6VkNh6NYU/EDQ5G3uRg5E1cG1gwvocHE3p44tLAMCDmq/CvuJJ2hUaWjeQE/pLRlSlJh4SEVFsAw4cPZ/jw4aXunzhxIgDR0dFlvmb37t2LeprMmzevUvHVlI6NOrIrdhfhp7+CqPOGjb1egEHvGG0i/rpEpVQwwMuZAV7OxKVk8+PxWH4+EUdiei6f7rrM6j2RDGnvwtDOar49+y0AC/wW4GDhYOTIpfpOfvrvyMvLIy/vbi+A9PTqGRRRmo5KawDOalNBbQmjP4OOj9doDPWFh4MVbwzzYvag1gSfTWTj0RhORN/m97OJhKQGorYtwNOyC3bCh7C4VNRKBUqFArVKgUqpQKUwfFWr/vZnpRKNmbLWr3YumR6ZpO9YunQp77zzjnFufmYz7f83G0VjRxLUam5O3IxT037GiaUe0ahVjPZpzGifxlxMTOfjA1s5ln0RIVScPzuQx08fLdf1FAoY2t6VV4e2oZVz2RfJkKT7kY1td8yfP5+0tLSiV1xcXPXfVKeF4Ddhy7NYF2TTUmFYeilcIbsv1rTmjTRcV24CoKfjo/i4tqapoxWN7S1xbWBBI1sNDtbm2FmaYaNRY2GmxFylRPm3btNCQPC5RIas3M+rm//i2u3sUu4mSWUna9J3aDQaNJoaXJ8uJxV+mQpX9hje93uFjpoCIq9sI/xmOAM8B9RcLBLfnv2Wa5nXcLZyZtWw18o1R7QQAp1eEJmcyYo/LvHH+SR+OXWNoLB4nvLz5IWBrXCykWsfShUja9LGcOsKfDXIkKDNrOCJ7+ChRXjfmWzp7M2zRg6wfrmeeZ2vwr8C4LVu5UvQYJhgTK1S4uXagC8ndWPrzN70auFIvk5P4OFo+i/fyyd/RJCea7oL6kqmq9w16aVLl+Li4sK0adOKbf/mm29ITk7mjTfeKNf1MjMziYyMLHofFRVFWFgYDg4OeHp6kpKSQmxsLPHx8QBFXQFdXV1xdTVMxzlp0iQaN27M0qWGwQf5+fmcP3++6M/Xr18nLCwMGxsbWrVqVd4iV62rIfDzZMhNNfR/nvATuHUC7g5qOXvzLHqhl12/ashHJz4iT5dHD9ceDG02tNLX6+LZkB9n+HEo8hbLd17kzLU0Vu+J5PujMTzv35LJvZvJB4xS2Ylyatq0qTh06FCJ7UePHhXNmjUr7+XE3r17BVDiNXnyZCGEEN9+++099y9evLjoGv7+/kXHCyFEVFTUPc/x9/cvc1xpaWkCEGlpaeUuU6mOfSnE2w2FWNxAiPUPCZGeWGx3vi5f+H7vK7wDvcXV1KtVd1+pVAevHRTegd6i84bO4nLK5Sq/vl6vF7+Hx4uHPgkRTd/YLpq+sV30eP9PsfFotMjX6h54fl6BTiSm5Yhz19PEgUvJYlvoNXHwcrIoKMO5kmkra44p91SlFhYWXLhwgebNiy9ff/XqVdq3b09ubm5lfmaYjCqdqlRXAMHz4IThV2o6PQmjVoFZydVEJv0+idAboXzQ9wO5AnU1y9fl81jQY8SkxzCx/URe7/56td1Lq9OzNfQ6n+66zPXUHACaOVoxuXczdHrBrax8UjLzDV+z8kjJMvw5I/feD5EdrM0Z5u3Kw53c8GvuiEopJ36qbcqaY8rd3OHh4cGhQ4dKJOlDhw7h7u5e/kjrupzbhuaNqH2AAh5aZFi5u5TZ1LydvAm9EUr4zXCZpKvZd+e/IyY9BkcLR2Z2nlmt91KrlIzr5sEjPu78eCyWz/ZEEn0rm3f+d/6B5yoV0NDKHAdrcxpamxN5I5OUrHx+PBbLj8diaWSrYYS3Kw93dsfXsyHKSiTs21n5XEhI53pqDj1bOOLhIBfZNbZyJ+kZM2Ywe/ZsCgoKGDhwIAC7d+/m9ddf55VXXqnyAGu15Evw03hIuQJm1jB2PXiNvO8pRTPiJVdyYVrpvhKzEvnyzJcAvNLtlSpbaUUIgVarRacrfca9Cb5ujO7YiK2h1wm/loaNRo29lRn2lmbYFX01dPeztzTD1sKsWOLV6vSExqay71IyBy4nk5mnZdfZa+w6ew0nGw392zQioG0j2rs1KHVqVb1eEJ+WQ+SNTK4mZxGZnMnVG5kk/2NaV+/Gdgxq54x/m0Y0sJQTe5WHSqVCrVZXenrbcjd3CCGYN28eq1atKppIycLCgjfeeINFixZVKhhTUunmjsjdsHmqYf5nOw+YsAlcvR94WlxGHCO2jECtVHPsqWOYq+QHozq8uu9VdkbvpKtzVwKHBVbJPNH5+fkkJCSQnV1z/aOFEORp9WTn68gt0PH3ZR7VSgWW5ios7zykzNfp0er05OsEWp2e0paEVCsVKJUKCrR6Cg9RKMBCrcTK3NBHXM6rXTZWVla4ublhbl7yc1zWHFOh5bPA0CvjwoULWFpa0rp165rtY1wDKpykhYBjX8DO+SD04NETntwINo3KeLrA/z/+3M67zY8jfixaA1GqOscSjvHsH8+iVCj5+eGfH7xkWRno9XouX76MSqWiUaNGmJub13gi0+sFWflaMnK0ZOZrH7hYtEKhQKNW3nmp0JgZ/qxSGnoVFWj1ZOQVkJajJV979zcDpVKBrUaNraUZVmaqcpdTpzf8kNDq9QhhuJ4SBUolKBWGHxDKWv5DQAhBfn4+ycnJ6HQ6WrdujVJZvLdWtbVJF7KxsZHLZf2TrgB2vAqnAg3vfZ6Gh1eCuuw/wBQKBd5O3hy4foDwm+EySVexAn0BS48Zumo+2fbJKknQYKhF6/V6PDw8yjylb3WwsoJG9oaEnZFbQGpOARm5WpRKBZZmKizMlHe+qtCo718jtgBsbcAdyCnQkZqdT2p2AQU6PekFkF6gxVylNzTVWJljYaZCCEGBTlCg0xe98nWCAu3d99oyrOquUChQKShK2Ko7yVulAI2ZCqs7vyH8c5pZU2JpaYmZmRkxMTHk5+djYVGyo0BZyBGHVSU7BX6eBNEHAAUMWWKYxa4CNYKOTh2LkrRUtX688CNX0q7QUNOQWT6zqvz6/6wtGYtSqTC0a1uZI4SodK3e0kyFpZ1hiHxWnpbU7ALScgrI1+m5kZHHjYw81EolOr1A8OAkrFIoMFMrUQB6AToh0OsF+ju1fyEEWnFn5z/l3B0UpFHfSdjmhq8WZqpy1cINP1T05Gn15BXoydPqyNMafpAU/iIiENz57845f9tu+AMCw0e9g3vx1Xuq4vtBJumqolBCRqJhgdjHv4Y2FR8U4e1kaLuWIw+rVnJ2Mmv/WgvAbN/Z9WY5rKpsdlEoFNhYmGFjYYa7XpCeW0BqtqG2rr2zxJ4CBWYqBWYqJWZqJeaFf/7be1UpyUsIcSdhg/7OcHv9nQSuE4amktwCHdn5WkNi1erI0+oonCZFoTD8xmBlfvdlpjL88DAcryf/TiIufFWwxbfk3w3V00Qjk3RVsbSHp/4DunxwblepSxX28IhOjyYtL63eJJPqtuLUCrIKsujo1JExrcYYO5xaT6lUYG9ljr2V+Z0HknrMlErUKkWFfzAoFArUCkWZJqzQ6vRkF+jIydeRnW9I3Dq9IDtfS3b+3f7lSoWiqIZe2j3vts0rMVerMFcpQHE37Sru/E9x593fi1d8X9WTSboqObasksvYW9jjYetBXEYc526do7d77yq5bn12KukU269uR4GCBX4L5JD7KqZWKWu8fVitUtJApaSBhRlw52HdnZ4u2Xdq27n5+qIEbaZS3vNBqZnKtHuryCRtorydvInLiCM8OVwm6UrS6rV8cMyw2vrYNmPp4NTByBHVTgEBAfj4+PDpp5+a5HUVCgUaMxUaMxUN72zT64Whhq9S1tpRmbI6YaL+PtmSVDn/ifgPl25fwk5jx0tdXjJ2OCZpypQpKBSKopejoyPDhg3jzJkzRcds2bKFJUuWFL1PTk7m+eefx9PTE41Gg6urK0OHDuXQoUPFrp2YmMiLL75IixYt0Gg0eHh4MGrUKHbv3l3t5VIqFViYqUok6LKU11TIJG2iikYe3gyvsgcb9VFUWhSrQ1cD8FKXl2ho0fABZ9Rfw4YNIyEhgYSEBHbv3o1arebhhx8u2u/g4ICt7d0VZ8aOHUtoaCgbNmzg0qVLBAUFERAQwK1bt4qOiY6OxtfXlz179vDRRx8RHh5OcHAwAwYMYNasqu9dUx4PKu8/FQ7eq2myucNEeTl4oVaouZV7i8SsRNxs3IwdUq2TkZ/BS3teIqsgi67OXRnbeqyxQzJphbVhMEwFPG/ePPr160dycjKNGjUq1iyRmprKgQMHCAkJwd/fH4CmTZvSo0ePYtecOXMmCoWC48ePY21tXbS9Q4cOJaY7rmllKa+3tzdqtZqNGzfSsWNH9u7dW+NxyiRtoizUFrRu2JoLKRc4c/OMTNLlpNPrmHdgHtHp0bhYufBJwCeolDU7h7MQgpyC0ufwqE6WFRgJ+HeZmZls3LiRVq1a4ejoWGK/jY0NNjY2bNu2jZ49e95zxHFKSgrBwcG8//77xRJ0IXt7+wrHV9VKK++GDRt4/vnnSzTh1CSZpE1YR6eOXEi5wNmbZ6tkMvr6ZE3YGvZf249GpeHfA/+Nk6VTjceQU6Cj/aKdNX5fgPPvDsXKvHwf7+3bt2NjY5hoKisrCzc3N7Zv337PARlqtZrAwEBmzJjBunXr6Nq1K/7+/owfP55OnQyLWERGRiKEwMvLq1xxPProo4SEhPDQQw/xyy+/lOvc8ihLeVu3bs3y5curLYaykG3SJqxwUIsceVg+wdHBrA9fD8A7vd+hg6PszVEWAwYMICwsjLCwMI4fP87QoUMZPnw4MTEx9zx+7NixxMfHExQUxLBhwwgJCaFr164EBgYCVPhZyssvv8x3331X0WKUWVnK6+vrW+1xPIisSZuwTo0MNZLzt86j1WtRK+U/14NEpESw6JBhNsYpHaYwssX9p4atTpZmKs6/a5zfgCwrsDyXtbV1seXlvvrqK+zs7Fi/fj3vvffePc+xsLBg8ODBDB48mIULF/Lss8+yePFipkyZQuvWrVEoFFy8eLFccQQEBBASElLu+P+pT58+rFixAj8/P6ZPn463tzdz5swp2l+W8t6rmeZB161q8lNvwpo1aIa1mTVZBVlcSb1SZZMB1VW3c2/z0p6XyNHm0Nu9N7O7zjZqPAqFotxNDqZEoVCgVCrJyckp8znt27dn27ZtgKE3yNChQ1mzZg0vvfRSiYSXmppare3SCxcuZNmyZfTr1w+lUvnARFrW8pb3upUlmztMmEqpKvpVXfaXvr8CfQGv7nuV+Kx4PGw9WN5/eY0/KKzt8vLySExMJDExkQsXLvDiiy+SmZnJqFElVwi6desWAwcOZOPGjZw5c4aoqCg2b97M8uXLGT16dNFxa9asQafT0aNHD3799VcuX77MhQsXWLVqFb169apQnD4+Pnh7e5d4FS5WXWjYsGHExsby22+/8fnnn1eqvOW5blWrvT/m6wlvJ2+OJx4n/GY4Y9vILmSl+eTkJxxPPI6V2opVA1bJ+U4qIDg4GDc3Qy8iW1tbvLy82Lx5MwEBASWOtbGxwc/Pj5UrV3LlyhUKCgrw8PBgxowZvPnmm0XHtWjRgtOnT/P+++/zyiuvkJCQQKNGjfD19WXt2rUVijMsLKxMx504cYKUlBSaNm2KmZlZpcpbnutWuapeAbe89u3bJx5++GHh5uYmALF169Zi+3/99VcxePBg4eDgIAARGhpapuv+/PPPom3btkKj0Qhvb2/x22+/lSuualktvAJ2Re8S3oHeYux/xxo1DlO25dIW4R3oLbwDvcXumN1GiSEnJ0ecP39e5OTkGOX+dc3evXvF2LEV/56/du2a6NSpk7h69aro0qWLCA8Pr5K4ynvd+31flDXHGL25Iysri86dO7NmzZpS9/ft25cPP/ywzNc8fPgwEyZMYPr06YSGhjJmzBjGjBnD2bO1r8mgsIfH5dTLZBfU3LJMtcVfyX+x5KhhqPLMzjMZ6DnQyBFJlTVo0CDGjRvHjh07aNKkCUeOHCnX+Tk5OYwbN47Vq1fTvHlz5s+fX2w4e0VV13UfpMLLZ1UHhULB1q1bGTNmTIl90dHRNG/enNDQUHx8fO57nSeffJKsrCy2b99etK1nz574+Piwbt26MsVS6TUOq9BDPz/EjZwbBA4LxNfF+F2CTMWN7BuM3z6e5JxkBnoMZOWAlUab3S43N5eoqCiaN29e4RU4pLrnft8XZc0xRq9JV4cjR44waNCgYtuGDh1635/IeXl5pKenF3uZCrkIQEn5unzmhMwhOSeZVvat+KDfB3L6UalOqpPf1YmJibi4uBTb5uLiQmJiYqnnLF26FDs7u6KXh4dHdYdZZoXrHMpBLQZCCJYcXcKZ5DM0MG/AqgGrsDYr2Z9VkuqCOpmkK2L+/PmkpaUVveLi4owdUpGiGfGSZZIG+PHij2yL3IZSoeQj/4/waGA6P1AlqarVyS54rq6uJCUlFduWlJRUNOPVvWg0mntOEmMK2ju2R4GC+Kx4bubcNMo8FKbiROIJPjrxEQBzfefKBRGkOq9O1qR79epVYkLxP//8s8Kd543N1tyW5nbNATh385yRozGe9Px05h2Yh07oeLjFw0xqP8nYIUlStTN6TTozM5PIyMii91FRUYSFheHg4ICnpycpKSnExsYWjSaKiIgADLXlwprxpEmTaNy4MUuXLgUME7T4+/vzySefMHLkSDZt2sTJkyf58ssva7h0VaejU0eupl0l/GY4/h7+xg7HKD4+8TE3sm/QtEFTFvVaZNLr0klSVTF6TfrkyZN06dKFLl26ADB37ly6dOnCokWGSXKCgoLo0qULI0caJsoZP348Xbp0KdaVLjY2loSEhKL3vXv35scff+TLL7+kc+fO/PLLL2zbtg1vb+8aLFnV+vtKLfXRoeuH2Bq5FQUK3u39LpZqS2OHJEk1wqT6SZsSU+onDXDu1jnGbx+Prbkth8Yfqle1yMz8TB4NepTErESeafcMb/R4w9ghlSD7SUv3IvtJ1yNt7NtgrjQnIz+D2IxYY4dTo1acWkFiViJNbJrwYpcXjR2OJNUomaRrCTOVGe0c2wH1q8njaMJRNl/aDMC7fd7FyszKyBFJUs2SSboWqW/9pbMLsnn78NsAPNn2Sbq7djduQJJkBDJJ1yL1bXj4ylMruZ55HXdrd+b6zjV2OPVeQEAAs2fPNnYY9Y5M0rVIYU36QsoFCnQFRo6mep1IPMGmiE0AvN37bdnMUc2mTJmCQqEoejk6OjJs2DDOnDlTdMyWLVuKzfqWnJzM888/j6enJxqNBldXV4YOHVpsZe2yXNdYpkyZcs/J3EyNTNK1iIetB3YaOwr0BUTcjjB2ONUmuyCbxYcXA/B4m8fp5V47ByHVNsOGDSMhIYGEhAR2796NWq3m4YcfLtrv4OCAra1t0fuxY8cSGhrKhg0buHTpEkFBQQQEBHDr1q1yXfef8vPzq75wtZjRB7NIZadQKPB28ubQ9UOE3wwvav6oa1aHriYuIw5Xa1de8X3F2OHUG4W1YTAMFps3bx79+vUjOTmZRo0aERAQgI+PD59++impqakcOHCAkJAQ/P0Ng6uaNm1Kjx49KnRdb29v1Go1GzdupGPHjuzdu7fmCm7iZE26lils8jgcf9jIkVSP0Buh/HDhBwDe7vU2NuY2Ro6oEoSA/CzjvCo5/CEzM5ONGzfSqlUrHB0dS+y3sbHBxsaGbdu2kZeXV+nrbtiwAXNzcw4dOlTmOd/rC1mTrmWGNRvGur/WsS9uH1FpUUVzetQFudpcFh1ahEAwptUY+jTuY+yQKqcgGz5wN86934wH8/JN37p9+3ZsbAw/FLOysnBzc2P79u0olSXrcmq1msDAQGbMmMG6devo2rUr/v7+jB8/nk6dOpX7uq1bt2b58uXFznv00UcJCQnhoYce4pdffilXWeoSWZOuZVratySgSQACwYZzG4wdTpVaE7aG6PRonC2dea37a8YOp94ZMGAAYWFhhIWFcfz4cYYOHcrw4cOJiYm55/Fjx44lPj6eoKAghg0bRkhICF27diUwMLDc1/X1Lbni0Msvv8x3331XpWWsjWRNuhaa1nEaIddCCLoSxCyfWTSyamTskCrtr+S/+O684QO5uPdiGpgbfyh+pZlZGWq0xrp3OVlbW9OqVaui91999RV2dnasX7+e9957757nWFhYMHjwYAYPHszChQt59tlnWbx4MVOmTCnXda2tS9b6AwICCAkJKXc5/qlPnz6sWLECPz8/pk+fjre3N3PmzKn0sTVFJulaqItzF7o4dyH0RijfX/i+1vchztPlsfDQQvRCz6gWo+jfpL+xQ6oaCkW5mxxMiUKhQKlUkpOTU+Zz2rdvz7Zt26r8upWxcOFCli1bRr9+/VAqlfdNuuU5tqbIJF1LTe0wldAboWyO2MyMjjOwNbd98Ekmam3YWqLSonCydDLJyZPqi7y8vKIl5m7fvs1nn31GZmYmo0aNKnHsrVu3GDduHNOmTaNTp07Y2tpy8uRJli9fzujRoyt83fLw8fFBq9WW2P7HH3/g7n73WcCwYcNYsGABv/32G8HBwfe9ZnmOrSkySddS/h7+tLRryZW0K2y+tJlp3tOMHVKFnL15lm/PfQvAWz3fwk5jZ+SI6q/g4GDc3NwAsLW1xcvLi82bNxMQEFDiWBsbG/z8/Fi5ciVXrlyhoKAADw8PZsyYwZtvvlnh65ZHWFhYmY47ceIEKSkpNG3aFDMzsyo7tsYI6Z7S0tIEINLS0owdSqm2Xt4qvAO9xYD/DBB52jxjh1Nuedo8MWbbGOEd6C1eC3nN2OFUSk5Ojjh//rzIyckxdih1yt69e8XYsWMrfP61a9dEp06dxNWrV0WXLl1EeHh4lRxbVvf7vihrjpG9O2qxkc1H4mLlQnJOMv+78j9jh1Nun4V9RmRqJA4WDsz3m2/scCQTM2jQIMaNG8eOHTto0qQJR44cKdf5OTk5jBs3jtWrV9O8eXPmz59fbFh7RY+taXLS/1KY2qT/pdlwbgMfn/yYZg2asW30NlRKlbFDKpPD1w/zf7v+D4CVASsZ1HSQkSOqHDnpv3QvctJ/icfbPI6tuS3R6dGExIUYO5wyuZlzkzcPGtotn2jzRK1P0JJUnWSSruWszawZ33Y8AN+c/QZT/8VIL/QsOLiAW7m3aGXfSg5akaQHkEm6Dniq3VOYK805c/MMJ5NOGjuc+9pwbgOH4w9jobLgo/4fYaGWTQOSdD9GT9L79+9n1KhRuLu7o1AoSnSEF0KwaNEi3NzcsLS0ZNCgQVy+fPm+18zIyGD27Nk0bdoUS0tLevfuzYkTJ6qxFMblZOnEmFZjAENt2lSFJ4ez6vQqAF7v8TqtGrZ6wBmSJBk9SWdlZdG5c2fWrFlzz/3Lly9n1apVrFu3jmPHjmFtbc3QoUPJzc0t9ZrPPvssf/75J99//z3h4eEMGTKEQYMGcf369eoqhtFN7jAZpULJwesHiUgxvbmmM/MzeX3/62iFliFNh/B468eNHZIk1Q6V7ghYhQCxdevWovd6vV64urqKjz76qGhbamqq0Gg04qeffrrnNbKzs4VKpRLbt28vtr1r165iwYIFZY6lNvST/qe5e+cK70BvMW//PGOHUoxerxev7XtNeAd6iyGbh4i0vNrzd1pWsp+0dC91vp90VFQUiYmJDBp09+m/nZ0dfn5+pfaZ1Gq16HS6Et1dLC0tOXjwYKn3ysvLIz09vdirtpnW0TDq8Peo34nPNNLEPvewLXIbv0f9jkqh4sP+H9aNyZMkqYaYdJIuHO/v4uJSbLuLi0vRvn+ytbWlV69eLFmyhPj4eHQ6HRs3buTIkSMkJCSUeq+lS5diZ2dX9PLw8Ki6gtSQDo4d8HPzQyd0RTPKGdvVtKssPb4UgBe6vICPs49xA5KkWsakk3RFff/99wghaNy4MRqNhlWrVjFhwoR7Tl5eaP78+aSlpRW94uLiajDiqlM4h8eWy1tIzU01aix5ujxe3/c6Odoc/Nz8au38IpJkTCadpAvXRUtKSiq2PSkpqWjfvbRs2ZJ9+/aRmZlJXFwcx48fp6CggBYtWpR6jkajoUGDBsVetVEvt160c2hHjjaHnyJ+Mmosn5z8hIjbEThYOLC071KUCpP+dpMkk2TSn5rmzZvj6urK7t27i7alp6dz7NgxevV68ArS1tbWuLm5cfv2bXbu3FliCsW6SKFQMNV7KgA/XfiJHG3NzNn7T3ti9/DTRcMPiff6vFcnFiao7wICApg9e3atuW5dYfQknZmZWbS0DhgeFoaFhREbG4tCoWD27Nm89957BAUFER4ezqRJk3B3d2fMmDFF13jooYf47LPPit7v3LmT4OBgoqKi+PPPPxkwYABeXl5MnTq1hktnHIObDqaxTWNu591m6+WtNX7/xKxEFh1eBMCk9pPo16Rfjccglc+UKVNQKBRFL0dHR4YNG8aZM2eKjtmyZUuxSYeSk5N5/vnn8fT0LFoRfOjQoRw6dKjYtRMTE3nxxRdp0aIFGo0GDw8PRo0aVazyVdOmTJlSLIeYMqMn6ZMnT9KlSxe6dOkCwNy5c+nSpQuLFhk+5K+//jovvvgizz33HN27dyczM5Pg4OBivTeuXLnCzZs3i96npaUxa9YsvLy8mDRpEn379mXnzp2mMz9sNVMr1UzpMAWA785/h1ZfcmL06qLT65h3YB5peWm0d2zP7K6za+zeUuUMGzaMhIQEEhIS2L17N2q1mocffrhov4ODA7a2dxeXGDt2LKGhoWzYsIFLly4RFBREQEAAt27dKjomOjoaX19f9uzZw0cffUR4eDjBwcEMGDCAWbNm1Wj5aq1q6h5Y69XGftJ/l1OQI/pv6i+8A73Fb1d+q7H7fh76ufAO9BY9NvYQMWkxNXZfY6vt/aQnT54sRo8eXWzbgQMHBCBu3LghhBDC399fvPzyy0IIIW7fvi0AERISct/rDh8+XDRu3FhkZmaW2Hf79u0S160p9ypvdaiKftJyZZY6ykJtwVNeT/FZ2Gd8c/YbhjcfjkKhqNZ7nkw8yboz6wDDKiueDTyr9X6mTghhtGcClmrLSv17Z2ZmsnHjRlq1aoWjo2OJ/TY2NtjY2LBt2zZ69uyJRqMpcUxKSgrBwcG8//7791xo1t7evsLx1ScySddh473G8/XZr4m4HcHh+MP0adyn2u6VlJXEvAPz0As9j7R8hFEtK7d+XV2Qo83B70c/o9z72FPHsCrniuHbt2/HxsYGMEzX4Obmxvbt2+/ZdVWtVhMYGMiMGTNYt24dXbt2xd/fn/Hjx9OpUycAIiMjEULg5eVVrjgeffRRQkJCeOihh/jll1/KdW5dZPQ2aan62GnsGNt6LFC9Ey8lZiUydedUkrKTaNagGQv8FlTbvaTqM2DAgKKH+MePH2fo0KEMHz6cmJiYex4/duxY4uPjCQoKYtiwYYSEhNC1a1cCAwMBKjxt7ssvv8x335nGYCxTIGvSddzkDpPZdHETxxOPsytmV5VPsH898zrTd07neuZ1Gts05ovBX5S7BldXWaotOfbUMaPdu7ysra1p1eruzIRfffUVdnZ2rF+/nvfee++e51hYWDB48GAGDx7MwoULefbZZ1m8eDFTpkyhdevWKBQKLl68WK44AgICCAkJKXf8/9SnTx9WrFiBn58f06dPx9vbmzlz5lT62Joma9J1nKu1K4+2fhSAuSFz+fbst1W2MEBcRhxTg6dyPfM6nraeBA4LxN3GvUquXRcoFAqszKyM8qqK5w8KhQKlUklOTtnb1du3b09WVhZg6A0ydOhQ1qxZU7Tt71JTUysd4/0sXLiQZcuWsWLFCpRK5X2TbnmOrWkySdcD8/3mM67NOASCFadW8Naht8jX5VfqmjHpMUwNnkpCVgLNGjTjm6Hf4Gpd+ihQyfTl5eWRmJhIYmIiFy5c4MUXXyQzM5NRo0o+X7h16xYDBw5k48aNnDlzhqioKDZv3szy5cuLDRpbs2YNOp2OHj168Ouvv3L58mUuXLjAqlWryjQg7V58fHzw9vYu8YqPLz6p2LBhw4iNjeW3337j888/v+81y3NsTZPNHfWAmdKMhT0X0tK+JctPLCfoShBxGXGsDFiJo2XJJ/cPEpUWxfSd00nOSaaFXQu+Hvo1TpZO1RC5VJOCg4Nxc3MDDBOVeXl5sXnzZgICAkoca2Njg5+fHytXruTKlSsUFBTg4eHBjBkzePPNN4uOa9GiBadPn+b999/nlVdeISEhgUaNGuHr68vatWsrFGfhwLcHOXHiBCkpKTRt2vSBYyTKc2yNq46+gXVBbe8nXZpD1w6JXj/0Kprb+eKti+U6P/J2pPDf5C+8A73FmG1jxM3sm9UUae1S2/tJm5q9e/eKsWPHVvj8a9euiU6dOomrV6+KLl26iPDw8Co5trzq/HzSUtXr3bg3G0duxNPWk/iseCb9Pom9sXvLdO6l25eYtnMat3Jv0bZhW74Z+k2FauKSdD+DBg1i3Lhx7NixgyZNmpQ6d3xpcnJyGDduHKtXr6Z58+bMnz+/2HD2ih5rLAohTHx5aSNJT0/Hzs6OtLS0Wjsj3v2k5aXxSsgrHEs8hgIFs31nM7XD1FIfOF1MuciMP2aQmpdKO4d2rB+yHjuNXQ1Hbbpyc3OJioqiefPmJRackOqv+31flDXHyJp0PWWnsWPt4LU82fZJBIKVp1aW+kDx3K1zTN85ndS8VLwdvWWClqQaJJN0PWamNOOtnm/xpt+bqBQqgq4EMX3ndG7l3J0g50zyGWbsnEF6fjqdG3XmyyFfygQtSTVIJmmJCV4T+HzQ59ia2xKWHMaE3yYQkRJB2I0wnvvzOTIKMujq3JUvBn+Brbntgy8oSVKVkV3wJAB6u/fmhxE/8OKeF4lJj2Hi7xNRoCBbm0131+58NvAzOZJQkoxA1qSlIs3tmvPDiB/o6daTHG0O2dps/Nz8WPPQGpmgy0g+h5f+riq+H2RNWirGTmPH54M+56vwr0jLS2N219lYqGVvhQcpHACRnZ2NpWX5582Q6qbs7GyASg2QkUlaKsFMacbznZ83dhi1ikqlwt7enhs3bgBgZVU182dItZMQguzsbG7cuIG9vT0qlarC15JJWpKqSOEK9oWJWpLs7e2Lvi8qSiZpSaoiCoUCNzc3nJ2dKSgoMHY4kpGZmZlVqgZdSCZpSapiKpWqSj6ckgSyd4ckSZJJk0lakiTJhMkkLUmSZMJkm3QpCjuhp6enGzkSSZLqosLc8qABLzJJlyIjIwMADw8PI0ciSVJdlpGRgZ1d6ZOWyfmkS6HX64mPj8fW1rbMgxLS09Px8PAgLi6u1s9BLctimmRZTFNFyiKEICMjA3d3d5TK0lueZU26FEqlkiZNmlTo3AYNGtT6b7pCsiymSZbFNJW3LPerQReSDw4lSZJMmEzSkiRJJkwm6Sqk0WhYvHgxGo3G2KFUmiyLaZJlMU3VWRb54FCSJMmEyZq0JEmSCZNJWpIkyYTJJC1JkmTCZJKWJEkyYTJJl9OaNWto1qwZFhYW+Pn5cfz48fsev3nzZry8vLCwsKBjx47s2LGjhiJ9sPKUZf369fTr14+GDRvSsGFDBg0a9MCy16Ty/rsU2rRpEwqFgjFjxlRvgOVQ3rKkpqYya9Ys3Nzc0Gg0tGnTxmS+z8pblk8//ZS2bdtiaWmJh4cHc+bMITc3t4aivbf9+/czatQo3N3dUSgUbNu27YHnhISE0LVrVzQaDa1atSIwMLDiAQipzDZt2iTMzc3FN998I86dOydmzJgh7O3tRVJS0j2PP3TokFCpVGL58uXi/Pnz4q233hJmZmYiPDy8hiMvqbxleeqpp8SaNWtEaGiouHDhgpgyZYqws7MT165dq+HISypvWQpFRUWJxo0bi379+onRo0fXTLAPUN6y5OXliW7duokRI0aIgwcPiqioKBESEiLCwsJqOPKSyluWH374QWg0GvHDDz+IqKgosXPnTuHm5ibmzJlTw5EXt2PHDrFgwQKxZcsWAYitW7fe9/irV68KKysrMXfuXHH+/HmxevVqoVKpRHBwcIXuL5N0OfTo0UPMmjWr6L1OpxPu7u5i6dKl9zz+iSeeECNHjiy2zc/PT/zf//1ftcZZFuUtyz9ptVpha2srNmzYUF0hlllFyqLVakXv3r3FV199JSZPnmwySbq8ZVm7dq1o0aKFyM/Pr6kQy6y8ZZk1a5YYOHBgsW1z584Vffr0qdY4y6MsSfr1118XHTp0KLbtySefFEOHDq3QPWVzRxnl5+dz6tQpBg0aVLRNqVQyaNAgjhw5cs9zjhw5Uux4gKFDh5Z6fE2pSFn+KTs7m4KCAhwcHKorzDKpaFneffddnJ2dmT59ek2EWSYVKUtQUBC9evVi1qxZuLi44O3tzQcffIBOp6upsO+pImXp3bs3p06dKmoSuXr1Kjt27GDEiBE1EnNVqerPvZxgqYxu3ryJTqfDxcWl2HYXFxcuXrx4z3MSExPveXxiYmK1xVkWFSnLP73xxhu4u7uX+GasaRUpy8GDB/n6668JCwurgQjLriJluXr1Knv27OHpp59mx44dREZGMnPmTAoKCli8eHFNhH1PFSnLU089xc2bN+nbty9CCLRaLf/617948803ayLkKlPa5z49PZ2cnBwsLS3LdT1Zk5bKbdmyZWzatImtW7diYWFh7HDKJSMjg4kTJ7J+/XqcnJyMHU6l6fV6nJ2d+fLLL/H19eXJJ59kwYIFrFu3ztihlVtISAgffPABn3/+OadPn2bLli389ttvLFmyxNihGZWsSZeRk5MTKpWKpKSkYtuTkpJwdXW95zmurq7lOr6mVKQshT7++GOWLVvGrl276NSpU3WGWSblLcuVK1eIjo5m1KhRRdv0ej0AarWaiIgIWrZsWb1Bl6Ii/y5ubm6YmZkVW528Xbt2JCYmkp+fj7m5ebXGXJqKlGXhwoVMnDiRZ599FoCOHTuSlZXFc889x4IFC+4757IpKe1z36BBg3LXokHWpMvM3NwcX19fdu/eXbRNr9eze/duevXqdc9zevXqVex4gD///LPU42tKRcoCsHz5cpYsWUJwcDDdunWriVAfqLxl8fLyIjw8nLCwsKLXI488woABAwgLCzPqSjwV+Xfp06cPkZGRRT9oAC5duoSbm5vREjRUrCzZ2dklEnHhDx9Ri6YYqvLPfYUeN9ZTmzZtEhqNRgQGBorz58+L5557Ttjb24vExEQhhBATJ04U8+bNKzr+0KFDQq1Wi48//lhcuHBBLF682KS64JWnLMuWLRPm5ubil19+EQkJCUWvjIwMYxWhSHnL8k+m1LujvGWJjY0Vtra24oUXXhARERFi+/btwtnZWbz33nvGKkKR8pZl8eLFwtbWVvz000/i6tWr4o8//hAtW7YUTzzxhLGKIIQQIiMjQ4SGhorQ0FABiBUrVojQ0FARExMjhBBi3rx5YuLEiUXHF3bBe+2118SFCxfEmjVrZBe8mrR69Wrh6ekpzM3NRY8ePcTRo0eL9vn7+4vJkycXO/7nn38Wbdq0Eebm5qJDhw7it99+q+GIS1eesjRt2lQAJV6LFy+u+cDvobz/Ln9nSklaiPKX5fDhw8LPz09oNBrRokUL8f777wutVlvDUd9becpSUFAg3n77bdGyZUthYWEhPDw8xMyZM8Xt27drPvC/2bt37z2/9wtjnzx5svD39y9xjo+PjzA3NxctWrQQ3377bYXvL6cqlSRJMmGyTVqSJMmEySQtSZJkwmSSliRJMmEySUuSJJkwmaQlSZJMmEzSkiRJJkwmaUmSJBMmk7QkSZIJk0lakiTJhMkkLUmSZMJkkpYkSTJhMklLUiX89NNPWFpakpCQULRt6tSpdOrUibS0NCNGJtUVcoIlSaoEIQQ+Pj7079+f1atXs3jxYr755huOHj1K48aNjR2eVAfIlVkkqRIUCgXvv/8+jz/+OK6urqxevZoDBw7IBC1VGVmTlqQq0LVrV86dO8cff/yBv7+/scOR6hDZJi1JlRQcHMzFixfvuTq2JFWWrElLUiWcPn2agIAAvvjiCwIDA2nQoAGbN282dlhSHSLbpCWpgqKjoxk5ciRvvvkmEyZMoEWLFvTq1YvTp0/TtWtXY4cn1RGyJi1JFZCSkkLv3r0JCAhg3bp1RdtHjhyJTqcjODjYiNFJdYlM0pIkSSZMPjiUJEkyYTJJS5IkmTCZpCVJkkyYTNKSJEkmTCZpSZIkEyaTtCRJkgmTSVqSJMmEySQtSZJkwmSSliRJMmEySUuSJJkwmaQlSZJM2P8D9n5IHdgSHegAAAAASUVORK5CYII=", 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 | --------------------------------------------------------------------------------