diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7526e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,163 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Results directory +results/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c7a2d66 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-added-large-files + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.2 + hooks: + - id: ruff + types_or: [ python, pyi, jupyter ] + args: [ --fix ] + - id: ruff-format + types_or: [ python, pyi, jupyter ] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5c5d1d7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Juno Nam and Rafael Gomez-Bombarelli + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 490cb2f..15b5a0b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,90 @@ # Alchemical MLIP +[![arXiv](https://img.shields.io/badge/arXiv-2404.10746-84cc16)](https://arxiv.org/abs/2404.10746) +[![Zenodo](https://img.shields.io/badge/Zenodo-10.5281/zenodo.11081395-14b8a6.svg)](https://zenodo.org/doi/10.5281/zenodo.11081395) +[![MIT](https://img.shields.io/badge/License-MIT-3b82f6.svg)](https://opensource.org/license/mit) + This repository contains the code to modify machine learning interatomic potentials (MLIPs) to enable continuous and differentiable alchemical transformations. +Currently, we provide the alchemical modification for the [MACE](https://github.com/ACEsuit/mace) model. 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). -> **Note:** The code is currently under final preparation and will be uploaded here shortly. -> We anticipate having the code ready by the end of this week. -> Please stay tuned for updates! +## Installation +We tested the code with Python 3.10 and the packages in `requirements.txt`. +For example, you can create a conda environment and install the required packages as follows (assuming CUDA 11.8): +```bash +conda create -n alchemical-mlip python=3.10 +conda activate alchemical-mlip +pip install torch==2.0.1 --index-url https://download.pytorch.org/whl/cu118 +pip install -r requirements.txt +pip install -e . +``` + +## Static calculations +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. +``` +notebook/ +├── 1_solid_solution.ipynb +└── 2_compositional_optimization.ipynb +``` + +## Free energy calculations +We provide the scripts for the free energy calculations for the vacancy (Fig. 4) and perovskites (Fig. 5) in the `scripts` directory. +``` +scripts/ +├── vacancy_frenkel_ladd.py +├── perovskite_frenkel_ladd.py +└── perovskite_alchemy.py +``` + +The arguments for the scripts are as follows: +```bash +# Vacancy Frenkel-Ladd calculation +python vacancy_frenkel_ladd.py \ + --structure-file data/structures/Fe.cif \ + --supercell 5 5 5 \ + --temperature 100 \ + --output-dir data/results/vacancy/Fe_5x5x5_100K/0 + +# Perovskite Frenkel-Ladd calculation (alpha phase) +python perovskite_frenkel_ladd.py \ + --structure-file data/structures/CsPbI3_alpha.cif \ + --supercell 6 6 6 \ + --temperature 400 \ + --output-dir data/results/perovskite/frenkel_ladd/CsPbI3_alpha_6x6x6_400K/0 + +# Perovskite Frenkel-Ladd calculation (delta phase) +python perovskite_frenkel_ladd.py \ + --structure-file data/structures/CsPbI3_delta.cif \ + --supercell 6 3 3 \ + --temperature 400 \ + --output-dir data/results/perovskite/frenkel_ladd/CsPbI3_delta_6x3x3_400K/0 + +# Perovskite alchemy calculation (alpha phase) +python -u perovskite_alchemy.py \ + --structure-file data/structures/CsPbI3_alpha.cif \ + --supercell 6 6 6 \ + --switch-pair Pb Sn \ + --temperature 400 \ + --output-dir data/results/perovskite/alchemy/CsPbI3_CsSnI3_alpha_400K/0 + +# Perovskite alchemy calculation (delta phase) +python -u perovskite_alchemy.py \ + --structure-file data/structures/CsPbI3_delta.cif \ + --supercell 6 3 3 \ + --switch-pair Pb Sn \ + --temperature 400 \ + --output-dir data/results/perovskite/alchemy/CsPbI3_CsSnI3_delta_400K/0 +``` + +The result files are large and not included in the repository. +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). +Please download the files and place them in the `data/results` directory. + +The post-processing scripts for the free energy calculations are provided in the `notebook` directory. +``` +notebook/ +├── 3_vacancy_analysis.ipynb +└── 4_perovskite_analysis.ipynb +``` ## Citation ``` diff --git a/THIRD-PARTY-LICENSES b/THIRD-PARTY-LICENSES new file mode 100644 index 0000000..859c10b --- /dev/null +++ b/THIRD-PARTY-LICENSES @@ -0,0 +1,60 @@ +Code in alchemical_mace/{calculator,model}.py is adapted from +https://github.com/ACEsuit/mace + +MIT License + +Copyright (c) 2022 ACEsuit/mace + +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: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +------------------------------------------------------------------------------- + +Code in alchemical_mace/utils.py is adapted from +https://github.com/CederGroupHub/chgnet + +Crystal Hamiltonian Graph neural Network (CHGNet) Copyright (c) 2023, The Regents +of the University of California, through Lawrence Berkeley National +Laboratory (subject to receipt of any required approvals from the U.S. +Dept. of Energy) and the University of California, Berkeley. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +(1) Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +(2) Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +(3) Neither the name of the University of California, Lawrence Berkeley +National Laboratory, U.S. Dept. of Energy, University of California, +Berkeley nor the names of its contributors may be used to endorse or +promote products derived from this software without specific prior written +permission. + + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +You are under no obligation whatsoever to provide any bug fixes, patches, +or upgrades to the features, functionality or performance of the source +code ("Enhancements") to anyone; however, if you choose to make your +Enhancements available either publicly, or directly to Lawrence Berkeley +National Laboratory, without imposing a separate written license agreement +for such Enhancements, then you hereby grant the following license: a +non-exclusive, royalty-free perpetual license to install, use, modify, +prepare derivative works, incorporate into other computer software, +distribute, and sublicense such enhancements or derivative works thereof, +in binary and source code form. diff --git a/alchemical_mace/__init__.py b/alchemical_mace/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/alchemical_mace/calculator.py b/alchemical_mace/calculator.py new file mode 100644 index 0000000..ffc06c1 --- /dev/null +++ b/alchemical_mace/calculator.py @@ -0,0 +1,432 @@ +from typing import Sequence, Tuple + +import ase +import numpy as np +import torch +import torch.nn.functional as F +from ase.calculators.calculator import Calculator, all_changes +from ase.constraints import ExpCellFilter +from ase.optimize import FIRE +from ase.stress import full_3x3_to_voigt_6_stress +from mace import data +from mace.calculators import mace_mp +from mace.tools import torch_geometric + +from alchemical_mace.model import ( + AlchemicalPair, + AlchemyManager, + alchemical_mace_mp, + get_z_table_and_r_max, +) + +################################################################################ +# Alchemical MACE calculator +################################################################################ + + +class AlchemicalMACECalculator(Calculator): + """ + Alchemical MACE calculator for ASE. + """ + + def __init__( + self, + atoms: ase.Atoms, + alchemical_pairs: Sequence[Sequence[Tuple[int, int]]], + alchemical_weights: Sequence[float], + device: str = "cpu", + model: str = "medium", + ): + """ + Initialize the Alchemical MACE calculator. + + Args: + atoms (ase.Atoms): Atoms object. + alchemical_pairs (Sequence[Sequence[Tuple[int, int]]]): List of + alchemical pairs. Each pair is a tuple of the atom index and + atomic number of an alchemical atom. + alchemical_weights (Sequence[float]): List of alchemical weights. + device (str): Device to run the calculations on. + model (str): Model to use for the MACE calculator. + """ + Calculator.__init__(self) + self.results = {} + self.implemented_properties = ["energy", "free_energy", "forces", "stress"] + + # Build the alchemical MACE model + self.device = device + self.model = alchemical_mace_mp( + model=model, device=device, default_dtype="float32" + ) + for param in self.model.parameters(): + param.requires_grad = False + + # Set AlchemyManager + z_table, r_max = get_z_table_and_r_max(self.model) + alchemical_weights = torch.tensor(alchemical_weights, dtype=torch.float32) + self.alchemy_manager = AlchemyManager( + atoms=atoms, + alchemical_pairs=alchemical_pairs, + alchemical_weights=alchemical_weights, + z_table=z_table, + r_max=r_max, + ).to(self.device) + + # Disable alchemical weights gradients by default + self.alchemy_manager.alchemical_weights.requires_grad = False + self.calculate_alchemical_grad = False + + self.num_atoms = len(atoms) + + def set_alchemical_weights(self, alchemical_weights: Sequence[float]): + alchemical_weights = torch.tensor( + alchemical_weights, + dtype=torch.float32, + device=self.device, + ) + self.alchemy_manager.alchemical_weights.data = alchemical_weights + + def get_alchemical_atomic_masses(self) -> np.ndarray: + # Get atomic masses for alchemical atoms + node_masses = ase.data.atomic_masses[self.alchemy_manager.atomic_numbers] + weights = self.alchemy_manager.alchemical_weights.data + weights = F.pad(weights, (1, 0), "constant", 1.0).cpu().numpy() + node_weights = weights[self.alchemy_manager.weight_indices] + + # Scatter sum to get the atomic masses + atom_masses = np.zeros(self.num_atoms, dtype=np.float32) + np.add.at( + atom_masses, self.alchemy_manager.atom_indices, node_masses * node_weights + ) + return atom_masses + + # pylint: disable=dangerous-default-value + def calculate(self, atoms=None, properties=None, system_changes=all_changes): + # call to base-class to set atoms attribute + Calculator.calculate(self, atoms) + + # prepare data + tensor_kwargs = {"dtype": torch.float32, "device": self.device} + positions = torch.tensor(atoms.get_positions(), **tensor_kwargs) + cell = torch.tensor(atoms.get_cell().array, **tensor_kwargs) + if self.calculate_alchemical_grad: + self.alchemy_manager.alchemical_weights.requires_grad = True + batch = self.alchemy_manager(positions, cell).to(self.device) + + # get outputs + if self.calculate_alchemical_grad: + out = self.model(batch, compute_stress=True, compute_alchemical_grad=True) + (grad,) = torch.autograd.grad( + outputs=[batch["node_weights"], batch["edge_weights"]], + inputs=[self.alchemy_manager.alchemical_weights], + grad_outputs=[out["node_grad"], out["edge_grad"]], + retain_graph=False, + create_graph=False, + ) + grad = grad.cpu().numpy() + self.alchemy_manager.alchemical_weights.requires_grad = False + else: + out = self.model(batch, retain_graph=False, compute_stress=True) + grad = np.zeros( + self.alchemy_manager.alchemical_weights.shape[0], dtype=np.float32 + ) + + # store results + self.results = {} + self.results["energy"] = out["energy"][0].item() + self.results["free_energy"] = self.results["energy"] + self.results["forces"] = out["forces"].detach().cpu().numpy() + self.results["stress"] = full_3x3_to_voigt_6_stress( + out["stress"][0].detach().cpu().numpy() + ) + self.results["alchemical_grad"] = grad + + +class NVTMACECalculator(Calculator): + def __init__(self, model: str = "medium", device: str = "cuda"): + Calculator.__init__(self) + self.results = {} + self.implemented_properties = ["energy", "free_energy", "forces", "stress"] + self.device = device + self.model = mace_mp( + model=model, device=device, default_dtype="float32" + ).models[0] + self.z_table, self.r_max = get_z_table_and_r_max(self.model) + for param in self.model.parameters(): + param.requires_grad = False + + # pylint: disable=dangerous-default-value + def calculate(self, atoms=None, properties=None, system_changes=all_changes): + # call to base-class to set atoms attribute + Calculator.calculate(self, atoms) + + # prepare data + config = data.config_from_atoms(atoms) + atomic_data = data.AtomicData.from_config( + config, z_table=self.z_table, cutoff=self.r_max + ) + data_loader = torch_geometric.dataloader.DataLoader( + dataset=[atomic_data], + batch_size=1, + shuffle=False, + drop_last=False, + ) + batch = next(iter(data_loader)).to(self.device) + + out = self.model(batch, compute_stress=False) + self.results = {} + self.results["energy"] = out["energy"][0].item() + self.results["free_energy"] = self.results["energy"] + self.results["forces"] = out["forces"].detach().cpu().numpy() + + +class FrenkelLaddCalculator(Calculator): + """ + Frenkel-Ladd calculator for ASE. + """ + + def __init__( + self, + spring_constants: np.ndarray, + initial_positions: np.ndarray, + device: str, + model: str = "medium", + ): + """ + Initialize the Frenkel-Ladd calculator. + + Args: + spring_constants (np.ndarray): Spring constants for each atom. + initial_positions (np.ndarray): Initial positions of the atoms. + device (str): Device to run the calculations on. + model (str): Model to use for the MACE calculator. + """ + Calculator.__init__(self) + self.results = {} + self.implemented_properties = ["energy", "free_energy", "forces"] + self.device = device + self.model = mace_mp( + model=model, device=device, default_dtype="float32" + ).models[0] + self.z_table, self.r_max = get_z_table_and_r_max(self.model) + for param in self.model.parameters(): + param.requires_grad = False + + # Spring constants + self.spring_constants = spring_constants + self.initial_positions = initial_positions + + # Reversible scaling factor + self.weights = [1.0, 0.0] + self.compute_mace = True + + def set_weights(self, lambda_value: float): + self.weights = [1.0 - lambda_value, lambda_value] + + # pylint: disable=dangerous-default-value + def calculate(self, atoms=None, properties=None, system_changes=all_changes): + # call to base-class to set atoms attribute + Calculator.calculate(self, atoms) + + # Get MACE results if needed + if self.compute_mace: + config = data.config_from_atoms(atoms) + atomic_data = data.AtomicData.from_config( + config, z_table=self.z_table, cutoff=self.r_max + ) + data_loader = torch_geometric.dataloader.DataLoader( + dataset=[atomic_data], + batch_size=1, + shuffle=False, + drop_last=False, + ) + batch = next(iter(data_loader)).to(self.device) + out = self.model(batch, compute_stress=False) # Frenkel-Ladd is NVT + mace_energy = out["energy"][0].item() + mace_forces = out["forces"].detach().cpu().numpy() + else: + mace_energy = 0.0 + mace_forces = np.zeros((len(atoms), 3), dtype=np.float32) + + # Get spring energy and forces + displacement = atoms.get_positions() - self.initial_positions + spring_energy = 0.5 * np.sum( + self.spring_constants * np.sum(displacement**2, axis=1) + ) + spring_forces = -self.spring_constants[:, None] * displacement + + # Combine energies and forces + total_energy = self.weights[0] * spring_energy + self.weights[1] * mace_energy + total_forces = self.weights[0] * spring_forces + self.weights[1] * mace_forces + if self.compute_mace: + energy_diff = mace_energy - spring_energy + else: + energy_diff = 0.0 + + self.results = {} + self.results["energy"] = total_energy + self.results["free_energy"] = total_energy + self.results["forces"] = total_forces + self.results["energy_diff"] = energy_diff + + +class DefectFrenkelLaddCalculator(Calculator): + """ + Frenkel-Ladd calculator for ASE, for a crystal with a defect. + """ + + def __init__( + self, + atoms: ase.Atoms, + spring_constant: float, + defect_index: int, + device: str = "cpu", + model: str = "medium", + ): + """ + Initialize the Frenkel-Ladd calculator. + + Args: + atoms (ase.Atoms): Atoms object. + spring_constant (float): Spring constant for the defect atom. + defect_index (int): Index of the defect atom. + device (str): Device to run the calculations on. + model (str): Model to use for the MACE calculator. + """ + Calculator.__init__(self) + self.results = {} + self.implemented_properties = ["energy", "free_energy", "forces", "stress"] + + # Build the alchemical MACE model + self.device = device + self.model = alchemical_mace_mp( + model=model, device=device, default_dtype="float32" + ) + for param in self.model.parameters(): + param.requires_grad = False + + # Set AlchemyManager + z_table, r_max = get_z_table_and_r_max(self.model) + alchemical_weights = torch.tensor([1.0], dtype=torch.float32) + atomic_number = atoms.get_atomic_numbers()[defect_index] + alchemical_pairs = [[AlchemicalPair(defect_index, atomic_number)]] + self.alchemy_manager = AlchemyManager( + atoms=atoms, + alchemical_pairs=alchemical_pairs, + alchemical_weights=alchemical_weights, + z_table=z_table, + r_max=r_max, + ).to(self.device) + + # Disable alchemical weights gradients by default + self.alchemy_manager.alchemical_weights.requires_grad = False + self.calculate_alchemical_grad = False + + self.num_atoms = len(atoms) + + # Switching + self.defect_index = defect_index + self.spring_constant = spring_constant + + def set_alchemical_weight(self, alchemical_weight: float): + # Set alchemical weights + alchemical_weights = torch.tensor( + [1.0 - alchemical_weight], # initial = original atoms = 1 - 0 + dtype=torch.float32, + device=self.device, + ) + self.alchemy_manager.alchemical_weights.data = alchemical_weights + + # pylint: disable=dangerous-default-value + def calculate(self, atoms=None, properties=None, system_changes=all_changes): + # call to base-class to set atoms attribute + Calculator.calculate(self, atoms) + + # prepare data + tensor_kwargs = {"dtype": torch.float32, "device": self.device} + positions = torch.tensor(atoms.get_positions(), **tensor_kwargs) + cell = torch.tensor(atoms.get_cell().array, **tensor_kwargs) + if self.calculate_alchemical_grad: + self.alchemy_manager.alchemical_weights.requires_grad = True + batch = self.alchemy_manager(positions, cell).to(self.device) + + # get outputs + if self.calculate_alchemical_grad: + out = self.model(batch, retain_graph=True, compute_stress=True) + out["energy"].backward() + grad = self.alchemy_manager.alchemical_weights.grad.item() + self.alchemy_manager.alchemical_weights.grad.zero_() + self.alchemy_manager.alchemical_weights.requires_grad = False + else: + out = self.model(batch, retain_graph=False, compute_stress=True) + grad = 0.0 + mace_energy = out["energy"][0].item() + mace_forces = out["forces"].detach().cpu().numpy() + mace_stress = out["stress"][0].detach().cpu().numpy() + + # Get spring energy and forces + cell_center = np.array([0.5, 0.5, 0.5]) @ atoms.get_cell().array + displacement = atoms.get_positions()[self.defect_index] - cell_center + spring_energy = 0.5 * self.spring_constant * np.sum(displacement**2) + spring_forces = -self.spring_constant * displacement + + # Combine energies and forces + # Note: weight here is 1 - lambda, and we're not weighting the mace + # energy because it's already weighted by the alchemical weight + weight = self.alchemy_manager.alchemical_weights.item() + total_energy = mace_energy + (1 - weight) * spring_energy + total_forces = mace_forces + total_forces[self.defect_index] += (1 - weight) * spring_forces + if self.calculate_alchemical_grad: + # H(lambda) = E(1 - lambda) + lambda * spring_energy + # dH/d(lambda) = -dE/d(1 - lambda) + spring_energy + grad = -grad + spring_energy + + # store results + self.results = {} + self.results["energy"] = total_energy + self.results["free_energy"] = total_energy + self.results["forces"] = total_forces + self.results["stress"] = full_3x3_to_voigt_6_stress(mace_stress) + self.results["alchemical_grad"] = grad + + +def get_alchemical_optimized_cellpar( + atoms: ase.Atoms, + alchemical_pairs: Sequence[Sequence[Tuple[int, int]]], + alchemical_weights: Sequence[float], + model: str = "medium", + device: str = "cpu", + **kwargs, +): + """ + Optimize the cell parameters of a crystal with alchemical atoms using the + Alchemical MACE calculator. + + Args: + atoms (ase.Atoms): Atoms object. + alchemical_pairs (Sequence[Sequence[Tuple[int, int]]]): List of + alchemical pairs. Each pair is a tuple of the atom index and + atomic number of an alchemical atom. + alchemical_weights (Sequence[float]): List of alchemical weights. + model (str): Model to use for the MACE calculator. + device (str): Device to run the calculations on. + + Returns: + np.ndarray: Optimized cell parameters. + """ + # Make a copy of the atoms object + atoms = atoms.copy() + + # Load Alchemical MACE calculator and relax the structure + calc = AlchemicalMACECalculator( + atoms, alchemical_pairs, alchemical_weights, device=device, model=model + ) + atoms.set_calculator(calc) + atoms.set_masses(calc.get_alchemical_atomic_masses()) + atoms = ExpCellFilter(atoms) + optimizer = FIRE(atoms) + optimizer.run(fmax=kwargs.get("fmax", 0.01), steps=kwargs.get("steps", 500)) + + # Return the optimized cell parameters + return atoms.atoms.get_cell().cellpar() diff --git a/alchemical_mace/model.py b/alchemical_mace/model.py new file mode 100644 index 0000000..e9e8e62 --- /dev/null +++ b/alchemical_mace/model.py @@ -0,0 +1,537 @@ +import ast +from collections import namedtuple +from typing import Dict, List, Optional, Sequence, Tuple + +import ase +import numpy as np +import torch +import torch.nn.functional as F +from e3nn import o3 +from e3nn.util.jit import compile_mode +from mace import modules, tools +from mace.calculators import mace_mp +from mace.data.neighborhood import get_neighborhood +from mace.modules import RealAgnosticResidualInteractionBlock, ScaleShiftMACE +from mace.modules.utils import get_edge_vectors_and_lengths, get_symmetric_displacement +from mace.tools import ( + AtomicNumberTable, + atomic_numbers_to_indices, + to_one_hot, + torch_geometric, + utils, +) +from mace.tools.scatter import scatter_sum + +################################################################################ +# Alchemy manager class for handling alchemical weights +################################################################################ + +AlchemicalPair = namedtuple("AlchemicalPair", ["atom_index", "atomic_number"]) + + +class AlchemyManager(torch.nn.Module): + """ + Class for managing alchemical weights and building alchemical graphs for MACE. + """ + + def __init__( + self, + atoms: ase.Atoms, + alchemical_pairs: Sequence[Sequence[Tuple[int, int]]], + alchemical_weights: torch.Tensor, + z_table: AtomicNumberTable, + r_max: float, + ): + """ + Initialize the alchemy manager. + + Args: + atoms: ASE atoms object + alchemical_pairs: List of lists of tuples, where each tuple contains + the atom index and atomic number of an alchemical atom + alchemical_weights: Tensor of alchemical weights + z_table: Atomic number table + r_max: Maximum cutoff radius for the alchemical graph + """ + super().__init__() + self.alchemical_weights = torch.nn.Parameter(alchemical_weights) + self.r_max = r_max + + # Process alchemical pairs into atom indices and atomic numbers + # Alchemical weights are 1-indexed, 0 is reserved for non-alchemical atoms + alchemical_atom_indices = [] + alchemical_atomic_numbers = [] + alchemical_weight_indices = [] + + for weight_idx, pairs in enumerate(alchemical_pairs): + for pair in pairs: + alchemical_atom_indices.append(pair.atom_index) + alchemical_atomic_numbers.append(pair.atomic_number) + alchemical_weight_indices.append(weight_idx + 1) + + non_alchemical_atom_indices = [ + i for i in range(len(atoms)) if i not in alchemical_atom_indices + ] + non_alchemical_atomic_numbers = atoms.get_atomic_numbers()[ + non_alchemical_atom_indices + ].tolist() + non_alchemical_weight_indices = [0] * len(non_alchemical_atom_indices) + + self.atom_indices = alchemical_atom_indices + non_alchemical_atom_indices + self.atomic_numbers = alchemical_atomic_numbers + non_alchemical_atomic_numbers + self.weight_indices = alchemical_weight_indices + non_alchemical_weight_indices + + self.atom_indices = np.array(self.atom_indices) + self.atomic_numbers = np.array(self.atomic_numbers) + self.weight_indices = np.array(self.weight_indices) + + sort_idx = np.argsort(self.atom_indices) + self.atom_indices = self.atom_indices[sort_idx] + self.atomic_numbers = self.atomic_numbers[sort_idx] + self.weight_indices = self.weight_indices[sort_idx] + + # Array to map original atom indices to alchemical indices + # -1 means the atom does not have a corresponding alchemical atom + # [n_atoms, n_weights + 1] + self.original_to_alchemical_index = np.full( + (len(atoms), len(alchemical_pairs) + 1), -1 + ) + for i, (atom_idx, weight_idx) in enumerate( + zip(self.atom_indices, self.weight_indices) + ): + self.original_to_alchemical_index[atom_idx, weight_idx] = i + + self.is_original_atom_alchemical = np.any( + self.original_to_alchemical_index[:, 1:] != -1, axis=1 + ) + + # Extract common node features + z_indices = atomic_numbers_to_indices(self.atomic_numbers, z_table=z_table) + node_attrs = to_one_hot( + torch.tensor(z_indices, dtype=torch.long).unsqueeze(-1), + num_classes=len(z_table), + ) + self.register_buffer("node_attrs", node_attrs) + self.pbc = atoms.get_pbc() + + def forward( + self, + positions: torch.Tensor, + cell: torch.Tensor, + ) -> Dict[str, torch.Tensor]: + """ + Build an alchemical graph for the given positions and cell. + + Args: + positions: Tensor of atomic positions + cell: Tensor of cell vectors + + Returns: + Dictionary containing the alchemical graph data + """ + + # Build original atom graph + orig_edge_index, shifts, unit_shifts = get_neighborhood( + positions=positions.detach().cpu().numpy(), + cutoff=self.r_max, + pbc=self.pbc, + cell=cell.detach().cpu().numpy(), + ) + + # Extend edges to alchemical pairs + edge_index = [] + orig_edge_loc = [] + edge_weight_indices = [] + + is_alchemical = self.is_original_atom_alchemical[orig_edge_index] + src_non_dst_non = ~is_alchemical[0] & ~is_alchemical[1] + src_non_dst_alch = ~is_alchemical[0] & is_alchemical[1] + src_alch_dst_non = is_alchemical[0] & ~is_alchemical[1] + src_alch_dst_alch = is_alchemical[0] & is_alchemical[1] + + # Both non-alchemical: keep as is + _orig_edge_index = orig_edge_index[:, src_non_dst_non] + edge_index.append(self.original_to_alchemical_index[_orig_edge_index, 0]) + orig_edge_loc.append(np.where(src_non_dst_non)[0]) + edge_weight_indices.append(np.zeros_like(_orig_edge_index[0])) + + # Source non-alchemical, destination alchemical: pair all, weights are 1 + _src, _dst = orig_edge_index[:, src_non_dst_alch] + _orig_edge_loc = np.where(src_non_dst_alch)[0] + _src = self.original_to_alchemical_index[_src, 0] + _dst = self.original_to_alchemical_index[_dst, :] + _dst_mask = _dst != -1 + _dst = _dst[_dst_mask] + _repeat = _dst_mask.sum(axis=1) + _src = np.repeat(_src, _repeat) + edge_index.append(np.stack((_src, _dst), axis=0)) + orig_edge_loc.append(np.repeat(_orig_edge_loc, _repeat)) + edge_weight_indices.append(np.zeros_like(_src)) + + # Source alchemical, destination non-alchemical: pair all, follow src weights + _src, _dst = orig_edge_index[:, src_alch_dst_non] + _orig_edge_loc = np.where(src_alch_dst_non)[0] + _src = self.original_to_alchemical_index[_src, :] + _dst = self.original_to_alchemical_index[_dst, 0] + _src_mask = _src != -1 + _src = _src[_src_mask] + _repeat = _src_mask.sum(axis=1) + _dst = np.repeat(_dst, _repeat) + edge_index.append(np.stack((_src, _dst), axis=0)) + orig_edge_loc.append(np.repeat(_orig_edge_loc, _repeat)) + edge_weight_indices.append(np.where(_src_mask)[1]) + + # Both alchemical: pair according to alchemical indices, weights are 1 + _orig_edge_index = orig_edge_index[:, src_alch_dst_alch] + _orig_edge_loc = np.where(src_alch_dst_alch)[0] + _alch_edge_index = self.original_to_alchemical_index[_orig_edge_index, :] + _idx = np.where((_alch_edge_index != -1).all(axis=0)) + edge_index.append(_alch_edge_index[:, _idx[0], _idx[1]]) + orig_edge_loc.append(_orig_edge_loc[_idx[0]]) + edge_weight_indices.append(np.zeros_like(_idx[0])) + + # Collect all edges + edge_index = np.concatenate(edge_index, axis=1) + orig_edge_loc = np.concatenate(orig_edge_loc) + edge_weight_indices = np.concatenate(edge_weight_indices) + + # Convert to torch tensors + edge_index = torch.tensor(edge_index, dtype=torch.long) + shifts = torch.tensor(shifts[orig_edge_loc], dtype=torch.float32) + unit_shifts = torch.tensor(unit_shifts[orig_edge_loc], dtype=torch.float32) + + # Alchemical weights for nodes and edges + weights = F.pad(self.alchemical_weights, (1, 0), "constant", 1.0) + node_weights = weights[self.weight_indices] + edge_weights = weights[edge_weight_indices] + + # Build data batch + atomic_data = torch_geometric.data.Data( + num_nodes=len(self.atom_indices), + edge_index=edge_index, + node_attrs=self.node_attrs, + positions=positions[self.atom_indices], + shifts=shifts, + unit_shifts=unit_shifts, + cell=cell, + node_weights=node_weights, + edge_weights=edge_weights, + node_atom_indices=torch.tensor(self.atom_indices, dtype=torch.long), + ) + data_loader = torch_geometric.dataloader.DataLoader( + dataset=[atomic_data], + batch_size=1, + shuffle=False, + drop_last=False, + ) + batch = next(iter(data_loader)) + + return batch + + +################################################################################ +# Alchemical MACE model +################################################################################ + +# get_outputs function from mace.modules.utils is modified to calculate also +# the alchemical gradients + + +def get_outputs( + energy: torch.Tensor, + positions: torch.Tensor, + displacement: torch.Tensor, + cell: torch.Tensor, + node_weights: torch.Tensor, + edge_weights: torch.Tensor, + retain_graph: bool = False, + create_graph: bool = False, + compute_force: bool = True, + compute_stress: bool = False, + compute_alchemical_grad: bool = False, +) -> Tuple[ + Optional[torch.Tensor], + Optional[torch.Tensor], + Optional[torch.Tensor], + Optional[torch.Tensor], + Optional[torch.Tensor], +]: + grad_outputs: List[Optional[torch.Tensor]] = [torch.ones_like(energy)] + if not compute_force: + return None, None, None, None, None + inputs = [positions] + if compute_stress: + inputs.append(displacement) + if compute_alchemical_grad: + inputs.extend([node_weights, edge_weights]) + gradients = torch.autograd.grad( + outputs=[energy], + inputs=inputs, + grad_outputs=grad_outputs, + retain_graph=retain_graph, + create_graph=create_graph, + allow_unused=True, + ) + + forces = gradients[0] + stress = torch.zeros_like(displacement) + virials = gradients[1] if compute_stress else None + if compute_alchemical_grad: + node_grad, edge_grad = gradients[-2], gradients[-1] + else: + node_grad, edge_grad = None, None + if compute_stress and virials is not None: + cell = cell.view(-1, 3, 3) + volume = torch.einsum( + "zi,zi->z", + cell[:, 0, :], + torch.cross(cell[:, 1, :], cell[:, 2, :], dim=1), + ).unsqueeze(-1) + stress = virials / volume.view(-1, 1, 1) + + if forces is not None: + forces = -1 * forces + if virials is not None: + virials = -1 * virials + return forces, virials, stress, node_grad, edge_grad + + +@compile_mode("script") +class AlchemicalResidualInteractionBlock(RealAgnosticResidualInteractionBlock): + def forward( + self, + node_attrs: torch.Tensor, + node_feats: torch.Tensor, + edge_attrs: torch.Tensor, + edge_feats: torch.Tensor, + edge_index: torch.Tensor, + edge_weights: torch.Tensor, # alchemy + ) -> Tuple[torch.Tensor, torch.Tensor]: + sender = edge_index[0] + receiver = edge_index[1] + num_nodes = node_feats.shape[0] + sc = self.skip_tp(node_feats, node_attrs) + node_feats = self.linear_up(node_feats) + tp_weights = self.conv_tp_weights(edge_feats) + tp_weights = tp_weights * edge_weights[:, None] # alchemy + mji = self.conv_tp( + node_feats[sender], edge_attrs, tp_weights + ) # [n_edges, irreps] + message = scatter_sum( + src=mji, index=receiver, dim=0, dim_size=num_nodes + ) # [n_nodes, irreps] + message = self.linear(message) / self.avg_num_neighbors + return ( + self.reshape(message), + sc, + ) # [n_nodes, channels, (lmax + 1)**2] + + +@compile_mode("script") +class AlchemicalMACE(ScaleShiftMACE): + def forward( + self, + data: Dict[str, torch.Tensor], + retain_graph: bool = False, # alchemy + create_graph: bool = False, # alchemy + compute_force: bool = True, + compute_stress: bool = False, + compute_displacement: bool = False, + compute_alchemical_grad: bool = False, # alchemy + map_to_original_atoms: bool = True, # alchemy + ) -> Dict[str, Optional[torch.Tensor]]: + # Setup + data["positions"].requires_grad_(True) + data["node_attrs"].requires_grad_(True) + num_graphs = data["ptr"].numel() - 1 + displacement = torch.zeros( + (num_graphs, 3, 3), + dtype=data["positions"].dtype, + device=data["positions"].device, + ) + if compute_stress or compute_displacement: + ( + data["positions"], + data["shifts"], + displacement, + ) = get_symmetric_displacement( + positions=data["positions"], + unit_shifts=data["unit_shifts"], + cell=data["cell"], + edge_index=data["edge_index"], + num_graphs=num_graphs, + batch=data["batch"], + ) + + # Atomic energies + node_e0 = self.atomic_energies_fn(data["node_attrs"]) + node_e0 = node_e0 * data["node_weights"] # alchemy + e0 = scatter_sum( + src=node_e0, index=data["batch"], dim=-1, dim_size=num_graphs + ) # [n_graphs,] + + # Embeddings + node_feats = self.node_embedding(data["node_attrs"]) + vectors, lengths = get_edge_vectors_and_lengths( + positions=data["positions"], + edge_index=data["edge_index"], + shifts=data["shifts"], + ) + edge_attrs = self.spherical_harmonics(vectors) + edge_feats = self.radial_embedding(lengths) + + # Interactions + node_es_list = [] + node_feats_list = [] + for interaction, product, readout in zip( + self.interactions, self.products, self.readouts + ): + node_feats, sc = interaction( + node_attrs=data["node_attrs"], + node_feats=node_feats, + edge_attrs=edge_attrs, + edge_feats=edge_feats, + edge_index=data["edge_index"], + edge_weights=data["edge_weights"], # alchemy + ) + node_feats = product( + node_feats=node_feats, sc=sc, node_attrs=data["node_attrs"] + ) + node_feats_list.append(node_feats) + node_es_list.append(readout(node_feats).squeeze(-1)) # {[n_nodes, ], } + + # Concatenate node features + # node_feats_out = torch.cat(node_feats_list, dim=-1) + + # Sum over interactions + node_inter_es = torch.sum( + torch.stack(node_es_list, dim=0), dim=0 + ) # [n_nodes, ] + node_inter_es = self.scale_shift(node_inter_es) + node_inter_es = node_inter_es * data["node_weights"] # alchemy + + # Sum over nodes in graph + inter_e = scatter_sum( + src=node_inter_es, index=data["batch"], dim=-1, dim_size=num_graphs + ) # [n_graphs,] + + # Add E_0 and (scaled) interaction energy + total_energy = e0 + inter_e + node_energy = node_e0 + node_inter_es + + forces, virials, stress, node_grad, edge_grad = get_outputs( + energy=total_energy, # alchemy + positions=data["positions"], + displacement=displacement, + cell=data["cell"], + node_weights=data["node_weights"], # alchemy + edge_weights=data["edge_weights"], # alchemy + retain_graph=retain_graph, # alchemy + create_graph=create_graph, # alchemy + compute_force=compute_force, + # compute_virials=compute_virials, # alchemy + compute_stress=compute_stress, + compute_alchemical_grad=compute_alchemical_grad, # alchemy + ) + + # Map to original atoms (node energies and forces): alchemy + if map_to_original_atoms: + # Note: we're not giving the dim_size, as we assume that all + # original atoms are present in the batch + node_index = data["node_atom_indices"] + node_energy = scatter_sum(src=node_energy, dim=0, index=node_index) + if compute_force: + forces = scatter_sum(src=forces, dim=0, index=node_index) + + output = { + "energy": total_energy, + "node_energy": node_energy, + "interaction_energy": inter_e, + "forces": forces, + "virials": virials, + "stress": stress, + "displacement": displacement, + "node_grad": node_grad, + "edge_grad": edge_grad, + } + + return output + + +################################################################################ +# Alchemical MACE universal model +################################################################################ + + +def alchemical_mace_mp( + model: str, + device: str, + default_dtype: str = "float32", +): + """ + Load a pre-trained alchemical MACE model. + + Args: + model: Model size (small, medium) + device: Device to load the model onto + default_dtype: Default data type for the model + + Returns: + Alchemical MACE model + """ + + # Load foundation MACE model and extract initial parameters + assert model in ("small", "medium") # TODO: support large model + mace = mace_mp(model=model, device=device, default_dtype=default_dtype).models[0] + atomic_energies = mace.atomic_energies_fn.atomic_energies.detach().clone() + z_table = utils.AtomicNumberTable([int(z) for z in mace.atomic_numbers]) + atomic_inter_scale = mace.scale_shift.scale.detach().clone() + atomic_inter_shift = mace.scale_shift.shift.detach().clone() + + # Prepare arguments for building the model + placeholder_args = ["--name", "None", "--train_file", "None"] + args = tools.build_default_arg_parser().parse_args(placeholder_args) + args.max_L = {"small": 0, "medium": 1, "large": 2}[model] + args.num_channels = 128 + args.hidden_irreps = o3.Irreps( + (args.num_channels * o3.Irreps.spherical_harmonics(args.max_L)) + .sort() + .irreps.simplify() + ) + + # Build the alchemical MACE model + model = AlchemicalMACE( + r_max=6.0, + num_bessel=10, + num_polynomial_cutoff=5, + max_ell=3, + interaction_cls=AlchemicalResidualInteractionBlock, + interaction_cls_first=AlchemicalResidualInteractionBlock, + num_interactions=2, + num_elements=len(z_table), + hidden_irreps=o3.Irreps(args.hidden_irreps), + MLP_irreps=o3.Irreps(args.MLP_irreps), + atomic_energies=atomic_energies, + avg_num_neighbors=args.avg_num_neighbors, + atomic_numbers=z_table.zs, + correlation=args.correlation, + gate=modules.gate_dict[args.gate], + radial_MLP=ast.literal_eval(args.radial_MLP), + radial_type=args.radial_type, + atomic_inter_scale=atomic_inter_scale, + atomic_inter_shift=atomic_inter_shift, + ) + + # Load foundation model parameters + model.load_state_dict(mace.state_dict(), strict=True) + for i in range(int(model.num_interactions)): + model.interactions[i].avg_num_neighbors = mace.interactions[i].avg_num_neighbors + model = model.to(device) + return model + + +def get_z_table_and_r_max(model: ScaleShiftMACE) -> Tuple[AtomicNumberTable, float]: + """Extract the atomic number table and maximum cutoff radius from a MACE model.""" + z_table = AtomicNumberTable([int(z) for z in model.atomic_numbers]) + r_max = model.r_max.item() + return z_table, r_max diff --git a/alchemical_mace/optimize.py b/alchemical_mace/optimize.py new file mode 100644 index 0000000..87171be --- /dev/null +++ b/alchemical_mace/optimize.py @@ -0,0 +1,29 @@ +import torch +from typing import Union, Iterable, Dict, Any + + +class ExponentiatedGradientDescent(torch.optim.Optimizer): + """ + Implements Exponentiated Gradient Descent. + + Args: + params (iterable of torch.Tensor or dict): iterable of parameters to optimize or + dicts defining parameter groups. + lr (float, optional): learning rate. Defaults to 1e-3. + eps (float, optional): small constant for numerical stability. Defaults to 1e-8. + """ + def __init__( + self, + params: Union[Iterable[torch.Tensor], Iterable[Dict[str, Any]]], + lr: float = 1e-3, + eps: float = 1e-8, + ): + super().__init__(params, defaults=dict(lr=lr, eps=eps)) + + def step(self): + for group in self.param_groups: + for p in group["params"]: + if p.grad is None: + continue + p.data.mul_(torch.exp(-group["lr"] * p.grad)) + p.data.div_(p.data.sum() + group["eps"]) diff --git a/alchemical_mace/utils.py b/alchemical_mace/utils.py new file mode 100644 index 0000000..d37cf9c --- /dev/null +++ b/alchemical_mace/utils.py @@ -0,0 +1,43 @@ +import os +from contextlib import ExitStack, contextmanager, redirect_stderr, redirect_stdout + +from ase import Atoms + + +@contextmanager +def suppress_print(out: bool = True, err: bool = False): + """Suppress stdout and/or stderr.""" + + with ExitStack() as stack: + devnull = stack.enter_context(open(os.devnull, "w")) + if out: + stack.enter_context(redirect_stdout(devnull)) + if err: + stack.enter_context(redirect_stderr(devnull)) + yield + + +# From CHGNet +def upper_triangular_cell(atoms: Atoms): + """Transform to upper-triangular cell.""" + import numpy as np + from ase.md.npt import NPT + + if NPT._isuppertriangular(atoms.get_cell()): + return + + a, b, c, alpha, beta, gamma = atoms.cell.cellpar() + angles = np.radians((alpha, beta, gamma)) + sin_a, sin_b, _sin_g = np.sin(angles) + cos_a, cos_b, cos_g = np.cos(angles) + cos_p = (cos_g - cos_a * cos_b) / (sin_a * sin_b) + cos_p = np.clip(cos_p, -1, 1) + sin_p = (1 - cos_p**2) ** 0.5 + + new_basis = [ + (a * sin_b * sin_p, a * sin_b * cos_p, a * cos_b), + (0, b * sin_a, b * cos_a), + (0, 0, c), + ] + + atoms.set_cell(new_basis, scale_atoms=True) diff --git a/data/structures/AlN_hex.cif b/data/structures/AlN_hex.cif new file mode 100644 index 0000000..ef9bf10 --- /dev/null +++ b/data/structures/AlN_hex.cif @@ -0,0 +1,30 @@ +# generated using pymatgen +data_AlN +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 3.12664153 +_cell_length_b 3.12664143 +_cell_length_c 5.00715332 +_cell_angle_alpha 90.00000043 +_cell_angle_beta 89.99999986 +_cell_angle_gamma 119.99999965 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural AlN +_chemical_formula_sum 'Al2 N2' +_cell_volume 42.39139351 +_cell_formula_units_Z 2 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Al Al0 1 0.66666667 0.33333333 0.49932157 1 + Al Al1 1 0.33333333 0.66666667 0.99932156 1 + N N2 1 0.66666667 0.33333333 0.88067843 1 + N N3 1 0.33333333 0.66666667 0.38067844 1 diff --git a/data/structures/BiSBr.cif b/data/structures/BiSBr.cif new file mode 100644 index 0000000..e9bbad3 --- /dev/null +++ b/data/structures/BiSBr.cif @@ -0,0 +1,38 @@ +# generated using pymatgen +data_BiSBr +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 4.10679210 +_cell_length_b 8.22484633 +_cell_length_c 11.05701800 +_cell_angle_alpha 89.99999573 +_cell_angle_beta 90.00000347 +_cell_angle_gamma 90.00001971 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural BiSBr +_chemical_formula_sum 'Bi4 S4 Br4' +_cell_volume 373.48101173 +_cell_formula_units_Z 4 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Bi Bi0 1 0.24999992 0.88556832 0.87177467 1 + Bi Bi1 1 0.75000010 0.11443170 0.12822534 1 + Bi Bi2 1 0.24999988 0.38556830 0.62822532 1 + Bi Bi3 1 0.75000009 0.61443169 0.37177466 1 + S S4 1 0.74999990 0.82391374 0.03167750 1 + S S5 1 0.25000008 0.67608625 0.53167751 1 + S S6 1 0.74999991 0.32391375 0.46832249 1 + S S7 1 0.25000010 0.17608625 0.96832251 1 + Br Br8 1 0.24999990 0.96479764 0.30006197 1 + Br Br9 1 0.75000009 0.53520235 0.80006197 1 + Br Br10 1 0.24999991 0.46479765 0.19993803 1 + Br Br11 1 0.75000013 0.03520235 0.69993804 1 diff --git a/data/structures/CeO2.cif b/data/structures/CeO2.cif new file mode 100644 index 0000000..ad4b557 --- /dev/null +++ b/data/structures/CeO2.cif @@ -0,0 +1,38 @@ +# generated using pymatgen +data_CeO2 +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 5.46789061 +_cell_length_b 5.46789009 +_cell_length_c 5.46788984 +_cell_angle_alpha 90.00000180 +_cell_angle_beta 90.00000282 +_cell_angle_gamma 90.00000072 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural CeO2 +_chemical_formula_sum 'Ce4 O8' +_cell_volume 163.47801292 +_cell_formula_units_Z 4 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Ce Ce0 1 0.00000000 -0.00000000 -0.00000000 1 + Ce Ce1 1 0.00000000 0.50000000 0.50000000 1 + Ce Ce2 1 0.50000000 0.00000000 0.50000000 1 + Ce Ce3 1 0.50000000 0.50000000 0.00000000 1 + O O4 1 0.25000000 0.75000000 0.74999999 1 + O O5 1 0.74999999 0.25000000 0.25000000 1 + O O6 1 0.25000000 0.25000000 0.25000000 1 + O O7 1 0.75000000 0.75000000 0.75000000 1 + O O8 1 0.74999999 0.75000000 0.25000000 1 + O O9 1 0.25000000 0.25000000 0.74999999 1 + O O10 1 0.75000000 0.25000000 0.75000000 1 + O O11 1 0.25000000 0.75000000 0.25000000 1 diff --git a/data/structures/CsPbI3_alpha.cif b/data/structures/CsPbI3_alpha.cif new file mode 100644 index 0000000..8f0984d --- /dev/null +++ b/data/structures/CsPbI3_alpha.cif @@ -0,0 +1,31 @@ +# generated using pymatgen +data_CsPbI3 +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 6.37904246 +_cell_length_b 6.37904251 +_cell_length_c 6.37904247 +_cell_angle_alpha 90.00000013 +_cell_angle_beta 89.99999979 +_cell_angle_gamma 89.99999953 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural CsPbI3 +_chemical_formula_sum 'Cs1 Pb1 I3' +_cell_volume 259.57716340 +_cell_formula_units_Z 1 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Cs Cs0 1 0.00000000 0.00000000 -0.00000000 1 + I I1 1 0.50000000 0.50000000 -0.00000000 1 + I I2 1 0.50000000 -0.00000000 0.50000000 1 + I I3 1 0.00000000 0.50000000 0.50000000 1 + Pb Pb4 1 0.50000000 0.50000000 0.50000000 1 diff --git a/data/structures/CsPbI3_delta.cif b/data/structures/CsPbI3_delta.cif new file mode 100644 index 0000000..1313893 --- /dev/null +++ b/data/structures/CsPbI3_delta.cif @@ -0,0 +1,46 @@ +# generated using pymatgen +data_CsPbI3 +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 4.91187765 +_cell_length_b 10.78602159 +_cell_length_c 18.12941751 +_cell_angle_alpha 90.00000734 +_cell_angle_beta 89.99999876 +_cell_angle_gamma 89.99999900 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural CsPbI3 +_chemical_formula_sum 'Cs4 Pb4 I12' +_cell_volume 960.48962177 +_cell_formula_units_Z 4 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Cs Cs0 1 0.75000002 0.57436836 0.17329066 1 + Cs Cs1 1 0.25000000 0.42563165 0.82670935 1 + Cs Cs2 1 0.75000002 0.07436835 0.32670935 1 + Cs Cs3 1 0.24999999 0.92563166 0.67329065 1 + I I4 1 0.74999999 0.20706041 0.71324789 1 + I I5 1 0.25000000 0.79293960 0.28675211 1 + I I6 1 0.75000001 0.97331566 0.10968932 1 + I I7 1 0.24999998 0.02668433 0.89031069 1 + I I8 1 0.74999999 0.47331567 0.39031069 1 + I I9 1 0.25000001 0.52668433 0.60968931 1 + I I10 1 0.25000000 0.66460269 0.00429655 1 + I I11 1 0.25000000 0.16460269 0.49570344 1 + I I12 1 0.25000000 0.29293960 0.21324788 1 + I I13 1 0.75000001 0.83539729 0.50429655 1 + I I14 1 0.75000000 0.33539731 0.99570345 1 + I I15 1 0.75000001 0.70706040 0.78675212 1 + Pb Pb16 1 0.74999998 0.83743203 0.94239546 1 + Pb Pb17 1 0.24999999 0.16256797 0.05760453 1 + Pb Pb18 1 0.75000002 0.33743205 0.55760454 1 + Pb Pb19 1 0.24999998 0.66256796 0.44239545 1 diff --git a/data/structures/CsSnI3_alpha.cif b/data/structures/CsSnI3_alpha.cif new file mode 100644 index 0000000..87f7b76 --- /dev/null +++ b/data/structures/CsSnI3_alpha.cif @@ -0,0 +1,31 @@ +# generated using pymatgen +data_CsSnI3 +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 6.27081715 +_cell_length_b 6.27081724 +_cell_length_c 6.27081711 +_cell_angle_alpha 89.99999964 +_cell_angle_beta 89.99999969 +_cell_angle_gamma 89.99999972 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural CsSnI3 +_chemical_formula_sum 'Cs1 Sn1 I3' +_cell_volume 246.58827085 +_cell_formula_units_Z 1 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Cs Cs0 1 0.00000000 0.00000000 -0.00000000 1 + I I1 1 0.50000000 0.50000000 0.00000000 1 + I I2 1 0.50000000 -0.00000000 0.50000000 1 + I I3 1 -0.00000000 0.50000000 0.50000000 1 + Sn Sn4 1 0.50000000 0.50000000 0.50000000 1 diff --git a/data/structures/CsSnI3_delta.cif b/data/structures/CsSnI3_delta.cif new file mode 100644 index 0000000..e7054a0 --- /dev/null +++ b/data/structures/CsSnI3_delta.cif @@ -0,0 +1,46 @@ +# generated using pymatgen +data_CsSnI3 +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 4.84918880 +_cell_length_b 10.69167480 +_cell_length_c 18.19616575 +_cell_angle_alpha 89.99999030 +_cell_angle_beta 90.00000262 +_cell_angle_gamma 89.99999762 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural CsSnI3 +_chemical_formula_sum 'Cs4 Sn4 I12' +_cell_volume 943.39749344 +_cell_formula_units_Z 4 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Cs Cs0 1 0.75000000 0.57552416 0.17186806 1 + Cs Cs1 1 0.25000001 0.42447584 0.82813194 1 + Cs Cs2 1 0.75000000 0.07552414 0.32813195 1 + Cs Cs3 1 0.25000000 0.92447584 0.67186806 1 + I I4 1 0.75000000 0.21397041 0.70606567 1 + I I5 1 0.25000000 0.78602961 0.29393431 1 + I I6 1 0.75000000 0.97123818 0.11227438 1 + I I7 1 0.25000000 0.02876181 0.88772566 1 + I I8 1 0.74999999 0.47123818 0.38772564 1 + I I9 1 0.25000001 0.52876182 0.61227437 1 + I I10 1 0.25000002 0.66813711 0.00020614 1 + I I11 1 0.24999999 0.16813710 0.49979388 1 + I I12 1 0.25000001 0.28602959 0.20606567 1 + I I13 1 0.74999999 0.83186292 0.50020614 1 + I I14 1 0.75000000 0.33186292 0.99979386 1 + I I15 1 0.74999999 0.71397042 0.79393433 1 + Sn Sn16 1 0.75000000 0.84420702 0.94393743 1 + Sn Sn17 1 0.25000000 0.15579296 0.05606252 1 + Sn Sn18 1 0.74999998 0.34420702 0.55606253 1 + Sn Sn19 1 0.24999999 0.65579297 0.44393746 1 diff --git a/data/structures/Fe.cif b/data/structures/Fe.cif new file mode 100644 index 0000000..18422ee --- /dev/null +++ b/data/structures/Fe.cif @@ -0,0 +1,28 @@ +# generated using pymatgen +data_Fe +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 2.86106543 +_cell_length_b 2.86106544 +_cell_length_c 2.86106538 +_cell_angle_alpha 90.00000018 +_cell_angle_beta 89.99999992 +_cell_angle_gamma 90.00000009 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural Fe +_chemical_formula_sum Fe2 +_cell_volume 23.41980977 +_cell_formula_units_Z 2 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Fe Fe0 1 0.00000000 -0.00000000 0.00000000 1 + Fe Fe1 1 0.50000000 0.50000000 0.50000000 1 diff --git a/data/structures/GaN_hex.cif b/data/structures/GaN_hex.cif new file mode 100644 index 0000000..86d63dc --- /dev/null +++ b/data/structures/GaN_hex.cif @@ -0,0 +1,30 @@ +# generated using pymatgen +data_GaN +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 3.21192371 +_cell_length_b 3.21192377 +_cell_length_c 5.21628467 +_cell_angle_alpha 90.00000055 +_cell_angle_beta 90.00000024 +_cell_angle_gamma 119.99999991 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural GaN +_chemical_formula_sum 'Ga2 N2' +_cell_volume 46.60391121 +_cell_formula_units_Z 2 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Ga Ga0 1 0.66666667 0.33333333 0.49900050 1 + Ga Ga1 1 0.33333333 0.66666667 0.99900050 1 + N N2 1 0.66666667 0.33333333 0.87599950 1 + N N3 1 0.33333333 0.66666667 0.37599950 1 diff --git a/data/structures/NaCl.cif b/data/structures/NaCl.cif new file mode 100644 index 0000000..ece9ba4 --- /dev/null +++ b/data/structures/NaCl.cif @@ -0,0 +1,34 @@ +# generated using pymatgen +data_NaCl +_symmetry_space_group_name_H-M 'P 1' +_cell_length_a 5.68304678 +_cell_length_b 5.68304679 +_cell_length_c 5.68304672 +_cell_angle_alpha 90.00000009 +_cell_angle_beta 90.00000015 +_cell_angle_gamma 90.00000005 +_symmetry_Int_Tables_number 1 +_chemical_formula_structural NaCl +_chemical_formula_sum 'Na4 Cl4' +_cell_volume 183.54547783 +_cell_formula_units_Z 4 +loop_ + _symmetry_equiv_pos_site_id + _symmetry_equiv_pos_as_xyz + 1 'x, y, z' +loop_ + _atom_site_type_symbol + _atom_site_label + _atom_site_symmetry_multiplicity + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_occupancy + Na Na0 1 -0.00000000 -0.00000000 -0.00000000 1 + Na Na1 1 -0.00000000 0.50000000 0.50000000 1 + Na Na2 1 0.50000000 -0.00000000 0.50000000 1 + Na Na3 1 0.50000000 0.50000000 -0.00000000 1 + Cl Cl4 1 0.00000000 0.00000000 0.50000000 1 + Cl Cl5 1 -0.00000000 0.50000000 0.00000000 1 + Cl Cl6 1 0.50000000 0.00000000 -0.00000000 1 + Cl Cl7 1 0.50000000 0.50000000 0.50000000 1 diff --git a/notebooks/1_solid_solution.ipynb b/notebooks/1_solid_solution.ipynb new file mode 100644 index 0000000..3c2101b --- /dev/null +++ b/notebooks/1_solid_solution.ipynb @@ -0,0 +1,253 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import ase\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from tqdm import tqdm\n", + "\n", + "from alchemical_mace.calculator import get_alchemical_optimized_cellpar\n", + "from alchemical_mace.model import AlchemicalPair\n", + "from alchemical_mace.utils import suppress_print\n", + "\n", + "plt.style.use(\"default\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### CeO2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 21/21 [01:15<00:00, 3.60s/it]\n" + ] + } + ], + "source": [ + "# Default settings\n", + "model = \"medium\"\n", + "device = \"cpu\"\n", + "\n", + "# Load structure\n", + "atoms = ase.io.read(\"../data/structures/CeO2.cif\")\n", + "alch_elements = [\"Ce\", \"Sn\"]\n", + "alch_idx = [i for i, atom in enumerate(atoms) if atom.symbol in alch_elements]\n", + "alch_atomic_numbers = [ase.Atoms(el).numbers[0] for el in alch_elements]\n", + "alchemical_pairs = [\n", + " [AlchemicalPair(atom_index=idx, atomic_number=z) for idx in alch_idx]\n", + " for z in alch_atomic_numbers\n", + "]\n", + "\n", + "comp_grid = [[1 - x, x] for x in np.linspace(0, 0.5, 21)]\n", + "lat_params_CeSn = []\n", + "for comp in tqdm(comp_grid):\n", + " with suppress_print(out=True, err=True):\n", + " cellpar = get_alchemical_optimized_cellpar(\n", + " atoms, alchemical_pairs, comp, model=model, device=device\n", + " )\n", + " lat_params_CeSn.append(cellpar[:3])" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 21/21 [01:35<00:00, 4.52s/it]\n" + ] + } + ], + "source": [ + "# Load structure\n", + "alch_elements = [\"Ce\", \"Zr\"]\n", + "alch_idx = [i for i, atom in enumerate(atoms) if atom.symbol in alch_elements]\n", + "alch_atomic_numbers = [ase.Atoms(el).numbers[0] for el in alch_elements]\n", + "alchemical_pairs = [\n", + " [AlchemicalPair(atom_index=idx, atomic_number=z) for idx in alch_idx]\n", + " for z in alch_atomic_numbers\n", + "]\n", + "\n", + "comp_grid = [[1 - x, x] for x in np.linspace(0, 0.5, 21)]\n", + "lat_params_CeZr = []\n", + "for comp in tqdm(comp_grid):\n", + " with suppress_print(out=True, err=True):\n", + " cellpar = get_alchemical_optimized_cellpar(\n", + " atoms, alchemical_pairs, comp, model=model, device=device\n", + " )\n", + " lat_params_CeZr.append(cellpar[:3])" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(figsize=(3, 2.5))\n", + "ax.plot(\n", + " [x[1] for x in comp_grid],\n", + " [x[0] for x in lat_params_CeSn],\n", + " label=\"Ce$_{1-x}$Sn$_x$O$_2$\",\n", + ")\n", + "ax.plot(\n", + " [x[1] for x in comp_grid],\n", + " [x[0] for x in lat_params_CeZr],\n", + " label=\"Ce$_{1-x}$Zr$_x$O$_2$\",\n", + ")\n", + "ax.set_xlabel(\"$x$\")\n", + "ax.set_ylabel(\"a [Å]\")\n", + "ax.legend()\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### BiSBr" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# Load structure\n", + "atoms = ase.io.read(\"../data/structures/BiSBr.cif\")\n", + "halide_elements = [\"Cl\", \"Br\", \"I\"]\n", + "halide_idx = [i for i, atom in enumerate(atoms) if atom.symbol in halide_elements]\n", + "halide_atomic_numbers = [ase.Atoms(el).numbers[0] for el in halide_elements]\n", + "alchemical_pairs = [\n", + " [AlchemicalPair(atom_index=idx, atomic_number=z) for idx in halide_idx]\n", + " for z in halide_atomic_numbers\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 21/21 [08:12<00:00, 23.46s/it]\n", + "100%|██████████| 21/21 [04:21<00:00, 12.45s/it]\n", + "100%|██████████| 21/21 [05:35<00:00, 15.95s/it]\n" + ] + } + ], + "source": [ + "comp_grid = [[1 - x, x, 0] for x in np.linspace(0, 1, 21)]\n", + "lat_params_ClBr = []\n", + "for comp in tqdm(comp_grid):\n", + " with suppress_print(out=True, err=True):\n", + " cellpar = get_alchemical_optimized_cellpar(\n", + " atoms, alchemical_pairs, comp, model=model, device=device\n", + " )\n", + " lat_params_ClBr.append(cellpar[:3])\n", + "\n", + "comp_grid = [[0, 1 - x, x] for x in np.linspace(0, 1, 21)]\n", + "lat_params_BrI = []\n", + "for comp in tqdm(comp_grid):\n", + " with suppress_print(out=True, err=True):\n", + " cellpar = get_alchemical_optimized_cellpar(\n", + " atoms, alchemical_pairs, comp, model=model, device=device\n", + " )\n", + " lat_params_BrI.append(cellpar[:3])\n", + "\n", + "\n", + "comp_grid = [[1 - x, 0, x] for x in np.linspace(0, 1, 21)]\n", + "lat_params_ClI = []\n", + "for comp in tqdm(comp_grid):\n", + " with suppress_print(out=True, err=True):\n", + " cellpar = get_alchemical_optimized_cellpar(\n", + " atoms, alchemical_pairs, comp, model=model, device=device\n", + " )\n", + " lat_params_ClI.append(cellpar[:3])" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(figsize=(3.5, 2.5), layout=\"constrained\")\n", + "comp = np.linspace(0, 1, 21)\n", + "idx, param = 2, \"c\"\n", + "ax.plot(comp, [lat[idx] for lat in lat_params_ClBr], label=\"BiSCl$_{1-x}$Br$_x$\")\n", + "ax.plot(comp, [lat[idx] for lat in lat_params_BrI], label=\"BiSBr$_{1-x}$I$_x$\")\n", + "ax.plot(comp, [lat[idx] for lat in lat_params_ClI], label=\"BiSCl$_{1-x}$I$_x$\")\n", + "ax.legend()\n", + "ax.set_xlabel(\"$x$\")\n", + "ax.set_ylabel(f\"{param} [Å]\")\n", + "fig.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "chgnet", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/2_compositional_optimization.ipynb b/notebooks/2_compositional_optimization.ipynb new file mode 100644 index 0000000..34cee6c --- /dev/null +++ b/notebooks/2_compositional_optimization.ipynb @@ -0,0 +1,368 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import ase\n", + "import matplotlib.patheffects as pe\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import torch\n", + "from mpltern.datasets import get_triangular_grid\n", + "from tqdm import tqdm\n", + "\n", + "from alchemical_mace.calculator import get_alchemical_optimized_cellpar\n", + "from alchemical_mace.model import (\n", + " AlchemicalPair,\n", + " AlchemyManager,\n", + " alchemical_mace_mp,\n", + " get_z_table_and_r_max,\n", + ")\n", + "from alchemical_mace.optimize import ExponentiatedGradientDescent\n", + "from alchemical_mace.utils import suppress_print\n", + "\n", + "plt.style.use(\"default\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Cell parameter scan" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Default settings\n", + "model = \"medium\"\n", + "device = \"cpu\"\n", + "\n", + "# Load pre-optimized structure (medium model)\n", + "atoms = ase.io.read(\"../data/structures/NaCl.cif\")\n", + "\n", + "# Construct the alchemical pairs\n", + "alkali_elements = [\"Li\", \"Na\", \"K\"]\n", + "alkali_idx = [i for i, atom in enumerate(atoms) if atom.symbol in alkali_elements]\n", + "alkali_atomic_numbers = [ase.Atoms(el).numbers[0] for el in alkali_elements]\n", + "alchemical_pairs = [\n", + " [AlchemicalPair(atom_index=idx, atomic_number=z) for idx in alkali_idx]\n", + " for z in alkali_atomic_numbers\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 105/105 [05:25<00:00, 3.10s/it]\n" + ] + } + ], + "source": [ + "# Compute lattice parameters for a triangular grid of compositions\n", + "comp_grid = np.array(get_triangular_grid(14))\n", + "lat_params = []\n", + "for comp in tqdm(comp_grid.T):\n", + " with suppress_print(out=True, err=True):\n", + " cellpar = get_alchemical_optimized_cellpar(\n", + " atoms, alchemical_pairs, comp, model=model, device=device\n", + " )\n", + " lat_params.append(np.mean(cellpar[:3])) # a = b = c" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAATcAAAD7CAYAAAAPf9NJAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAACFl0lEQVR4nO1dd1zUShc9u3QRFVAEERVBEFFBARELPntvz96xY0NFFLH33nvvnwV7QVGfvWAHFUWlqCCKiArSy2a+P3aTTbILLgpSzPEXsyTTkk3Onjtz546IEEIgQIAAAcUM4oJugAABAgTkBwRyEyBAQLGEQG4CBAgolhDITYAAAcUSArkJECCgWEIgNwECBBRLCOQmQICAYgmB3AQIEFAsIZCbAAECiiUEchOQZxCJRBCJRKhSpUpBN0WAAIHcBPwcc+bMYYjLzc2toJsjQIBKUC/oBggoPrh16xYAQFtbu4BbIkCAQG4C8hCNGjUq6CYIEMBAMEsF5BmEPjcBhQkCuQkQIKBYQiA3AQIEFEsI5CZAgIBiCYHcBAgQUCwhkJsAAQKKJQRyEyBAQLGE4OcmIFd4/Pgxpk6dqnDc29u7AFojQED2EMhNQK4QHByM4OBghePu7u4F0BoBArKHYJYKECCgWEIkrFsqQICA4ghBuQkQIKBYQiA3AQIEFEsI5CZAgIBiCYHcBAgQUCwhkJsAAQKKJQRyEyBAQLGEQG4CBAgolhDITYAAAcUSArkJECCgWEIgNwECBBRLCOQmQICAYgkhKogABsHBwXjx4gUMDAxgY2ODihUrFnSTBAj4ZQjKTQCePn2Kdu3awcnJCcuWLUPnzp3Ro0cPXLlypaCbJkDAL0Mgt78cM2bMgKOjI5KTk3H48GEcPHgQz58/R8uWLTF8+PCCbp4AAb8MIeTRX4q0tDRMmjQJp06dwsyZM9G/f3+ULFmSOR8eHo7mzZvj8OHDqF+/fgG2VICAX4PQ5/aXIiQkBCdPnsT8+fMxYMAAaGpqcs4HBgZCIpEIq8cLKLIQyO0vxZMnT1CpUiX06dOHQ2wSiQSnTp2Ct7c32rZtC2Nj4wJspQABvw6B3P5SaGlpISYmBhKJhDkWFBSE8+fP4+LFi7C3t4eXl1cBtlCAgN+D0Of2F8PW1hZmZmZwdHREZGQkYmNjERUVhcaNG8PHxweVK1cu6CYKEPDLEMjtL0ZISAh8fX1x4MAB2NrawtzcHF26dEGTJk0AAIQQiESiAm6lAAG/BoHcBCgFRVEQi8WQSCRQU1Mr6OYIEJBrCH5uAgAAV69eRXR0NAApsRFCQAhhiC0lJQXC76CAogSB3ATAz88PvXr1wr59+wCAITWRSISAgAD07dsX/fr1g7OzM9auXYukpKQCbrEAAT+HYJYKAADMnTuXmYIFAF++fIG7uzvOnj2Lli1bwtraGikpKXj69CnMzMzg6+tbwC0WUBwxePBgldLt3r37p2kEchOggAsXLmDChAnQ09ODp6cnGjduDDMzMwDAs2fP0KhRIwQEBMDW1raAWyogLS0NGRkZKqfX1NSEtrZ2Prbo96CmpoYmTZqgTJkyOaY7ceLET8sS/NwEcJCamoqtW7fCzs4O3t7esLe35wwo6OnpoUqVKkhJSSnAVgoApMRWWqccMqB6N4GxsTHevn1bqAlu9erVsLOz++1yBHITwEFAQAAuX76My5cvw8HBgXMuISEBixcvxpcvX4RwSIUAGRkZyEASGqhPhDq0fpo+C+m4G7MaGRkZhZrc8goCuQng4P3796hduzZnsjxFUXj16hV8fX1x//59LF26FCYmJgXYSgFsaIi1oC76OVmJ/rIOKIHcBHDQvn17jBkzBgcOHECnTp0QHh6OR48e4dChQwgJCcGECRPQt2/fgm6mABbE6mKIRT93fBATMaB691yRh0BuAjgwMjLC/PnzsX79egwbNgzW1tbIzMxEjRo1cPPmTVhbWwMQZi8UJojEIpW+CxEp/N/X0KFDYWhomCdlCaOlApTi3bt3CAwMhIaGBoyNjeHo6AhAPnNBQMHjx48fKF26NFqUnqGSWZpF0vBfwgIkJCSgVKlSf6CFv4Znz54hLCwMDg4OvzW/WVBuApSiSpUqCrHcCCECsRVGqKjcUASU29q1a+Hp6QkNDQ2IRCKcPXsWLVq0wLp16yCRSDBx4kSVyxKeVAEqQzBDCyfEamJpv9vPNrXC/7ovX74cq1evRlpaGsaMGYMlS5YAAOzs7FRy3GWj8F+tAAECcoRILFJ5K+yIj49Hx44dAQA9e/bEq1evAADm5uaIiIjIVVkCuQn4Y7h58yY6duyIChUqQCQS4dSpUz/Nc/36ddStWxdaWlqwtLTEnj178r2dRQ0iNZHKW2GHq6srbt++DQAwMDDAjx8/AAAREREwMDDIVVkCuQnINRYsWICwsLBc50tOToadnR02btyoUvq3b9+iffv2aNq0KYKCgjBhwgQMGzYMFy9ezHXdxRkqmaSyrbCjX79+mDZtGhYsWIBbt24hKysLx48fx+jRoxlFpyqE0VIBuQY9uTm3fSBsiEQinDx5El26dMk2jbe3N/z8/BAcHMwc6927N+Lj4+Hv7//LdRcX0KOlHSovgIb456OlmVQazr2fUahHS5XFDjQ0NETPnj2xbNkylChRQuWyhNFSAbnG9OnTUbNmTQwcOBBNmzbNt3oCAgLQokULzrHWrVtjwoQJ+VZnUYSq/WkiFH6z9Pv375y/f2eif+HXqQIKHSwtLeHq6opmzZrh/v37+PHjB7Olp6fnWT0xMTEoX74851j58uXx48cPpKam5lk9RR0isRgiNRW2IuDGU6pUKc5GE1tmZiZu3LiRq7IE5VaMEBwcjBcvXsDAwAA2Njb5NrmdEIL4+HiUKFFCYcHm2bNnY86cOflSrwDlEKuLIFZhsEBcRFx5AgIC8O7dO04op4SEBEyYMAG7du2CSCTCoEGDflqOQG7FAE+fPoWPjw+uXbuGGjVqICQkBHZ2dliwYAGaN2+e5/UdO3YMUVFRuHXzDlybNMaxY0fRoEEDANIlA/MKxsbG+Pz5M+fY58+fUapUKejo6ORZPUUdYrFIJedqcRFw4h0zZgy2bNmCkiVLcvrf6Ol+np6eIISoRG6FX6cKyBEzZsyAo6MjkpOTcfjwYRw8eBDPnz9Hy5YtMXz48DyvLzU1FV5eXpg/bwFsbWtiyuQpmDZtGnR1dVGqVKk8JTcXFxdcuXKFc+zy5ctwcXHJszqKA4qTK4ivry8uX76MhIQEfPv2jdnevHkDQgi+ffum0C+XHQRyK6KgPbh3796N9evXw8/PD507d4a1tTUsLCwwaNAgUBSFe/fu5Wm9K1asQLly5dCv3wAQQjBu3Hh8jvmskv9ZUlISgoKCEBQUBEDq6hEUFITIyEgAgI+PDwYOHMikd3d3R0REBKZMmYJXr15h06ZN8PX1zdUUnL8BxckV5Nu3b0oDVf5KoIbCf7UClCIkJAQnT57EvHnzMGTIEJQsWZJzPjAwEBKJRGF+6O/gw4cPWLp0KVauWA2RSASKIti8eRNsbW0xbdo0JCQk5Jj/0aNHqFOnDurUqQMA8PT0RJ06dTBr1iwAwKdPnxiiA6Re6X5+frh8+TLs7OywcuVK7NixA61bt86zayoOEKlJ+9x+thUF5TZ79myl7h4lS5bE7Nmzc1WW4OdWRLFz505s374dV69e5TwMEokEp06dwpQpU9C8eXNs27Ytz+rs378/JBIKu3ftRVZWFqZM8YKxSQVMnOCJDh3bwsnJEStWrMiz+gTkDNrPrZfjKmiq/7wPMiMrFUceeRZqP7fs8P37d/z777+4du2aynmEAYUiCi0tLcTExEAikTDHgoKCcP78eVy8eBH29vbw8vLKs/ru3r2LU6dOISjwOZKTkzFy5HC0a98BvXr2BiHAksXL0ay5K0aMGAErK6s8q1fAz6Fqf1pRiOd2//59zJo1S2G0VCKR4MOHDzA3Nwcg7dL4GQTlVoRha2sLMzMzODo6IjIyErGxsYiKikLjxo3h4+PzW7Gw2KAoCs7OzmjXtj2GDh2OocMGY8IETzRxlTrwShdwBiZOHIfY2BicPXc2T+oVkDNo5dav4VqVldv/7owv1MqtTp06qFKlClxdXTmjpUlJSZg5cyZWr14NAPDw8PhpWQK5FWGEhITA19cXBw4cgK2tLczNzdGlSxc0adIEALcT9nci5+7duxezZs7CiROn4TlpIpYvWwlbW1sQAtkmfYS+fImFo5MdDh8+LPSL/QHQ5Nbfdb3K5Hbg5rhCTW46OjoIDw9HhQoVOMdjY2NhbGwMiqJULksgt2KKvIqYm5iYCCsrK7i7j8b169ewdct2VKhgyiE1+jMhwJatG7B/3x48ffYUGhoav12/gOxBk9uAfzaoTG77r48t1OSmpqaG2NhYhVDjsbGxMDEx4XTD/AzCaGkxwdWrVxEdHQ1AObE9ePAA8+fPx927d3NV7oIFC1CunBHuBQTgwP5DHGJjKzd6P2TwCGRJJCpH/hDw+yhOriASiUTpGgpGRka5IjZAGFAoFvDz84Obmxs8PT3h4+OjVLFpaWkhKioKY8eOxX///adSbKzw8HCsXbsOjRo1xoEDh6ClpaVAZgCX6NTU1DF//hK4uw9B//79UbZs2Ty/XgFciMUqTr+iCv+Awt69e7M9RwiBm5sb0tPTcfjw4Z/OUhDM0mKCOXPmoE2bNqhfv362JmlsbCyGDx8OLS0t+Pr6/rTMLl264O3bd7h54w7EYrVsCY3+DBBQlPRz/wE9YGlpjs2bN+fxlQqgQZulQ9psgaaGCmZpZip2+bsXarM0px9dQgi+f/+OuLg4WFlZ4du3bzmWJZBbMQchBBKJBOrqUpEeHByMNm3a4Pr167C0tMw235UrV/Dvv/8iKDAY5cqWk5IYSDbKTX5MSm4EYWFhaNmqIR48eIBatWr9kWv920CT29D221Qmt51+Iwo1ueUlCr8RLkBl3LlzB0OHDsXOnTvx4sULZGZmQiQSMcQGSKPh/swkzcrKwoQJEzB16nSULVsOFAEoIlVlNHnRG/sYRVHSdITAvKoFBg4cBg+P8RB+P/MXauoiqKmLVdhyb5ZGR0ejf//+MDQ0hI6ODmrVqoVHjx7lmKewhIYXyK0YQVNTE7t378b06dPRtm1bNGnSBEOHDsX+/fvx8OFDPHv2DF5eXkhPT8+R4LZt24b09HSMHO4Olu0pO0t4pqj8GEUBhCIgsr3HOC8EBz9Xaa0EAb+O/Jo4//37dzRs2BAaGhq4cOECXr58iZUrV0JfXz/bPHkRGv7GjRto1qwZypYtC11dXTRs2BB+fn65ajsgmKXFDn369EFqairWr1+PBw8e4PLly/D394dYLManT5/QsGFDLFiwQCEOG41v376hWrVq2LZ1J1q1bK1ghlIMzymaqHR/G/vYvv07sWPHBrx69eqXI6oKUA7aLB3ZYxe0NH4efjs9MwVbjw5R2SydOnUq7ty5g1u3bqncpt8NDX/q1Cn06NEDffr0YcJ1Xb16FQcPHsSJEydytY6CQG7FDDExMTA1NcWNGzfQqFEjANIIIh8/foSOjg40NDRyHMEcN24cXr9+g+PHTikQlTIXEHoQgaIUz33//h0TJoxEZNQ7DB06GNOmTftDd+HvAE1uo3rvgZamCuSWkYLNh90QFRXFITctLS2loapq1KiB1q1b48OHD7hx4wZMTU0xevToHENpubq6om7dulizZg1zbPfu3ZgwYcJPAysAgIODAzp27KgQ8HTevHk4d+4cHjx48NMyaAhmaTGDsbExJk2ahGHDhgGQkoy2tjaqVq0KExOTHInt5cuX2LFjBxYtWMqYlsr27H43ioJSYouMfI9Ro93gOWk6Fi5cjcWLF+Pjx49/6jb8VVAlIgi9AYCZmRlKly7NbIsXL1ZabkREBDZv3oxq1arh4sWLGDVqFDw8PHJ01/jd0PAvX75E7969FY736tULz58//2l+NgQ/t2KIZcuW4ciRI7h06RJatWqlUh5CCCaMn4Ahg4fByspaOjIKKNnL08tHR8EhtqdPA7FkyRwsW7YBFUykTr+urs3h4zMNe/fuyZ+L/oshEqu2PgKdRplyUwaKouDo6IhFixYBkM77DA4OxpYtW1SKhPsrKFWqFDIzMxWOZ2RkKIT1+hkE5VZM4efnBzMzM5XTnzt3Dk8CAzFl8lRmBFT5RoHIPksk8mP0/sqVS1i9Zgk2bNgNE+MKkFAEEorCpEmzcPTo0VyZFQJUQ25nKPAXYcmO3ExMTFCjRg3OMRsbG07MPT5+NzS8q6srLly4oHD8/PnzcHV1/Wl+NgTlVkxRs2ZNldOmp6fD09MTM6bPQuky+jn0s8n82iBXaXITFfD3P4dzfiexYf0eaGpqSk1W2TkTk4pwcxuFsWPH4d69gDyZ9ypACrbJ+bN0uUHDhg3x+vVrzrE3b97kGG3GxcUF58+f5xzLTWj4o0ePKj3u7e2tUn42hCdMANauXQttbR0M6D9I3r+WDcFRFCCRUIxPG22u+p0/hQv+Z7BixSZoammCcAxZ6eY2eDQ+fPiAgwcPFuwFFzOotKyfbMsNJk6ciHv37mHRokUICwvDwYMHsW3bNowZM4ZJk1+h4UNDQ+Hr64vz588jKioqV3lpCOT2F2LBggUICwsDIDUZFixYgEULl0KspianomxHSgnLJUR6/Mzp4/jvv4tYtnQD1NU0OAMPFLMn0NYqgXHjpmHKFG8kJSX9Uts3btyIKlWqQFtbG87Ozj81c9esWQNra2vo6OjAzMwMEydORFpa2i/VXVghEqvo56bCws1sODk54eTJkzh06BBq1qyJ+fPnY82aNejXrx+TJq9Dw0skEgwYMADVq1dH//790aFDB1StWhXDhw/nBK9UBYIryF+IwYMHA5AO0Q8dOhRxX75h754D8gEC5KTcWORGEew/sBPPnj3F/PkroKamni0pSiTyGQ1DhnRB+/atsGDBgly1+8iRIxg4cCC2bNkCZ2dnrFmzBkePHsXr169hZGSkkP7gwYMYMmQIdu3ahQYNGuDNmzdwc3ND7969sWrVqry4lQUK2hXEa9xRaGmp4AqSnoIV63sU6ulXc+fOxb59+7Bv3z6UL18edevWxZs3b9CjRw84OzvnKoy9QG5/IcLCwlCzZk0cPnwYffv2RcCdh6hUqYqCIlNKUrLRUYlEguXLF0AkEsNzog8IREoUnrxfTsIydV8EB2HkyO54+fJlrhawcXZ2hpOTEzZs2ABAOppnZmaGcePGYerUqQrpx44di5CQEM7ygJMmTcL9+/dx+/bt376PBQ2a3KZMOAYtLd2fpk9PT8ayNd0LNblZWFhgxYoV6Nq1KyIiImBnZ4fExETcvXsXPXv2xIcPH1QuSzBL/0JYWlqid+/eGDlyJEaPGgszsyqyOaE5EBukgwMgBOnp6ZgyZRxMTc0wyVPqmMs2WWm3ELpMigJA5L5yNjZ2aNmyEzMV7MePH5wtPT1doc0ZGRl4/PgxWrRowRwTi8Vo0aIFAgIClF5ngwYN8PjxY8Z0jYiIwPnz59GuXbs8v6cFCZFYpPJW2BEdHc2sjsaGiYkJ4uPjc1WWQG5/Kezt7ZGSkoLxHpMAEFZHG5+oZOakROruEZ+QgFGjBqFNm47o3XsQhxSlE+zBOibrb1NCnCPdJ+PixYsYOXIkx6E0O6fSuLg4SCQSpQ6iMTExSq+xb9++mDdvHho1agQNDQ1YWFjgn3/+KXYzJUQquoGIikCwyrJlyyI2Nlbh+MmTJ3MdXUZwBSkkCA4OxosXL2BgYAAbGxtUrFgx3+pKSUnBypUrsXzZaujq6vLmjSqPsEsg/VWdPHksJk+egVq16ijMJZWqMyV9dLJjtFsIoQgM9I0wYMBY3Lrlh2/fvnEWA8mrVeuvX7+ORYsWYdOmTXB2dkZYWBjGjx+P+fPnY+bMmXlSR2GAWCyCWAVVpkqagkb9+vVx9epV1KtXD4BUsbds2RJ37txR6v+WEwo/lRdzPH36FO3atYOTkxOWLVuGzp07o0ePHpx+orzGsmXLYGxsgm7dekoJDYCaupri4AGjvoAXL4Lh5TUaCxasRM1adZhz8rRE5hoin81A0coPys3dnj2HICEhEUePHv2pU2nZsmWhpqam1EHU2NhY6XXOnDkTAwYMwLBhw1CrVi107doVixYtwuLFi3O10Ehhh1gkYggux+0XFwj6k5g7dy6z4nzJkiXx77//wtnZGc+fP2cWPlIVArkVIGbMmAFHR0ckJyfj8OHDOHjwIJ4/f46WLVvmODn5dxAZGYnly5dj4YKlspcCKF1GG6lp8Sipp8X4pxGpfQpCCG7fuY6lS+di3bodzMCDnLTYm4jlCyebTC/rZ+OoQkjza2hoYtQoH0ybNv2n/SmamppwcHDgkL50RsSVbB1EU1JSFJyFaYVYnMbRVCI2FdVdQcPW1hZt27YFIF034dChQ1iwYAEsLCxyXZZAbgWAtLQ0jBkzBrt378b69evh5+eHzp07w9raGhYWFhg0aBAoisK9e/fyvO4pU6agY4fOqFvHERQF6OpqYdGihahTpw5CQ0NQqpS23DeNABcvXcD+/buxceNulCljqOC7xp9+xQlcKaEgoShI2GkkFCgJPYJKwcWlBapUsVaIAqEMnp6e2L59O/bu3YuQkBCMGjUKycnJjGvLwIED4ePjw6Tv2LEjNm/ejMOHD+Pt27e4fPkyZs6ciY4dO3LM4KIONTWRytvfBKHPrQAQEhKCkydPYv78+RgwYAA0NTU55wMDAyGRSHLlJqEKbt26BT8/PwTcecwoKQlFkJqaCmdnZzRt2hTnzp1DnTpOSE/PQmaGBFZWNli9eivU1TWkZibFNUUZNcZSZ4ohkGROvby/afE0avR0jBn9L9zd3VG9evVs29+rVy98+fIFs2bNQkxMDOzt7eHv788MMkRGRnKU2owZMyASiTBjxgxER0ejXLly6NixIxYuXJin97WgIRapZnIWBbM0LyH4uRUAdu7cie3bt+Pq1asoUULufCmRSHDq1ClMmTIFzZs3x7Zt2/KsToqi4OTkhDatO2D8+EkMyWhrayDg3g3cvn0blpaWGD9+PHx9fVGmTBlUqlQJIlFJJCenM067/H42ZU6+tMMum/g4Awq8gYekpESMGdMNtrbWuHDh/M8vRgAAuZ/bwrl+0Nb+uZ9bWloyps9uX6j93PISgllaANDS0kJMTAxnHcagoCAsXboU69atg729Pby8vPK0zj179iAu7ivcR47luH2kp2eiQYMGuHnzJgYPHow9e/agR48e6N69O+Li4qCuLuYQlNzlgyYumWpjmaoMsbHMW0b1UXLVRhGC79+/Yc6csRg5chru3g1QmHQt4OfIbTy3vwWCWVoA6N+/PxYvXowePXrA0dERkZGRiI2NRVRUFBo3bgwfH58cIy/kFj9+/ICPjw+WLF4FTS0t1kgnQLII9PSkMbQSExNRo0YNGBgYID4+Hg8ePED//jb4/j1FuWMvxVdu3CghXGWnmP9LbAyWLJmMESOmoqp5dfTrNxYTJkxEixYtFEx1AdmjOLmC0Lh58yaePXuG1NRU2NnZoWXLlhDl0qwWlFsB4dixY3BxccGRI0fw48cP2NjYYNOmTdiyZQsqV66cp6N58+fPh5VVdbRt20EpyaSmSNXbwYMH0aFDB2zfvh3+/v44cuQIxGICyKZWMcpMttHuH5QyYqN4ZikvfXT0eyxe7AWPcXNQ1dwahBC0bdsLWVlgplcJUA0iFUdKi8IMhfj4eDRr1gzNmzfHypUrsWnTJrRv3x4NGjRQKUw5G0KfWyFEdosq/wpCQ0NRu3ZtBD9/BZMKJviRkKrgfFuihAYCg+6hXbt2OHToENq06YjMTAlKltREbGwSEhLSQIcU56gyVl+a3CRVNreUq/aio99j5cppmDRpCcqVq8DqgyMIDLyL5Ss8ERYWhnLlyuXJPSiuoPvcViy9CB0V+txS05Lh5d26UPe5DRs2DA8fPsTx48eZdXXDw8PRtWtXuLi4YOvWrSqXJSi3QoCrV68iOjoagHRQgZbfefG74+k5CQcPHkZqWiJOnPBF6dI6PN80guTkDLg2/gcvX75Eu3ad8OlTAj7HJiIs7Kuc2Fj+aUx/G91GQreVXbCc5Gh/OUIRfPjwDitXToOX1zKUK2fCIkJpmXZ2LqhRwxEzZsz47Wv/W6AmVtEVpAgotzNnzmDlypWcBcMtLCywZs0anD59OldlCeRWwPDz80OvXr2wb98+AFInU5rc2H0M7MEHVXHp0iVoaWnBzq4WOnbsCGNjY0goAnUNNYjFch7S1FKHuoYazM2r4uPHBNlEd5rMwFFjFPuzzNSUMH5u7MnyBJzAlxQQFfUWq1fPwJQpy1G2bHmOycooQEIwcIAX9u7dh6CgoLy4xcUexcmJNzk5mUNsNKpWrZprs1QgtwJG+/btMXbsWLRs2RKANOT39evXMWPGDPj7++PkyZN48+YN3r17h5SUFKURM5QhMzMTc+fMxZo1q9GmTRusXLkSLVq0gI6OOhISPqGknhhly+lCJBbD0LAE/v23K65evQJ9/RKsvjXWtCp6ZJSjtOj+NbCIijvQQJPd+/dhWLNmJry9l8PAoLx8AWcizytlRcDE2Azt2vYRVqtXEcWJ3CwtLfHq1SuF4y9fvkS1atVyVZYwWloIMHv2bOZzQkICBg4cCF1dXcTFxSEiIgJfvnxB9erVERAQgEqVKmH16tVwcHDIsczNmzdj9JgxWL1aOjm+TZs2ePnyJbp164aYmBhkZGRg7dq16N9/EChKSrIlSpSQLgDDGwHluoJIp1lJ03GX+ePOIZXPTX3zOhjbti2Dt/cK6OuXk87rVBjY4KrCrl2HY8LETjh+/Di6d++ez99A0UZ+raFQEFiyZInSUVE1NTUsWbIkV2UJAwqFBOxBhDlz5uB///sfQkNDmfO3b9/Gxo0bceHCBfTo0QPbt2/Ptqy4uDhYWlbDkcMn0LRpQwwdNhTv3r1DbGwsli9fjo4dOyIgIABt2rTBjRs3UMHEEjolNJCVReHz50Suwy3FJx7IRz5Zo6dKY8FRBM+fP8b//rcRU6eugq6unmz9BT5pKk6qJ4Tg6tVTOHtuJ0JDX6u0ctLfBnpAYfOGq9DR+fmyd6mpSRg1tlmhHlDISwhmaSEBe3R0zJgxyMzMxJo1a0BRFFavXo358+fj06dPGDp0KDw9PXMsa9asWXB0dIZtzTr49j0du3bthp2dHdzc3NCiRRu8e/cNDg710K5dO9y+fRtq6mJ8+vQDMTGJsjmfRDr/U0JkS/PJiExCIJFQzN8SCSX7W7ZgjGw+Kb0FBt7DwUObMW3aGujolIQkS7osoJQ9+WqQ7eQr/dy4cUdoa+th5cqV+X37izSKk1kKSKcf9u/fHw4ODnBwcED//v3x+PHjXJcjmKWFCIQQiEQilCtXDrNmzcLo0aNx69YtfPr0Cebm5hg0aNBPF1l+/vw5du/ejYsXb4EQICM9C7GxSVi9eg2ysrIQF5cMdXUxxGKCZ8+eYfz48UhPz5KFD2e7bSh36wDTz0axXD8Up2Y9fnwbJ0/ux3Sf1dDQ1JGnzcZdRPEzgQhAvz6eWLzYA4MHD4apqemf+SKKGEQqEldR8HM7efIkevbsiebNm6NLly4AgDt37sDZ2RlHjhxBt27dVC5LILdCBLqv4dq1a3j48CF0dXXx4MEDbNmyBc2aNWNMs+z84AghGD9+AgYOHAbzKpaMyZeZQRD9IQEisRilS2tDQ4PC4MGD0bBhQzg4OCEi4qucdMCbYcCaKsX3XaOncfGJ7cGDGzh37jB8fFZDU1ObWcg5+7483owGuk4KqFbNHvb2rpgyxRv/+9+BP/htFB2oicVQU2HZPrUisFbsrFmzMHv2bAVXoEWLFmH27Nm5IrfCf7V/GW7fvg0PDw+Ehoaia9euiI6ORr169aCjo8MEWMzOwff06dN4/vwZPDy8mICR9x8EYP2GlZBQQFaWBHp6Wujbty/KlSuHzZs348OHH5BIaDLhkRlLxYFlPjKhjpi+OHm6u3eu4Pz5o/CZugqamlpyhQcesSkxS5l1BUGbplJS7P7vGJw4cSLbtRL+dhSnuaWhoaHo1auXwvGePXsyy1GqCkG5FTI0atQIXbt2RceOHeHk5ITBgwdDV1f3p7MW0tLS4Ok5CV5eM6BXshQIBZw9ewJ+509j6dL1jDqKjU3CyZMnkZEhwfvIBKSlZSmJ7iEnNn6YIooAlIRLdvR28+ZF3LhxAd7eK2Trl0rTcvrVFFxK+PNSAUIoDmka6JdH29YDMG6sBx48vC+sVs9DcZpbamJigrt37yq4fdy9excmJia5Kksgt0KIefPmMZ8bNGig0oRh2uWjR89+kBAK27dtRGTkO6xZux1qamoMofz4kY4fP9KYvjX5Ggds05BFbBySkys2vkl55cpZ3Lt3DZMnL4WamgYIIUw/nqI7iXKzlDZzKQnXpYQQglYt+2H23LM4cOAAZ4VzAYCamopmaS5XnC8IeHh4YPTo0Xjx4gUaNWoEQNrntmHDBsyfPz9XZQmuIIUU9OCCKvj06ROsrKywc+dhODjUw7x501C+vAlGjPBgyuIoJ1nfGh0Rl21ucpUU1wdNOhrKIz8CnDi+B+/fh2PMmFkQi8RMPVkSSqkalBMbxWsXn2y57b7/4DJOn9mI0LA30NPTy8/bXyRAu4L878A9lCjxc1eQlJQk9Otfv9C7guzfvx/Lly/HmzdvAADVqlXD5MmTc/2jVvip/C9FbsK7+PhMwz//tEStWvYYP34E7O0dMXLkeEijeUidbgk9AUCmziQSWoXxZiEoMxdZgwhyR11p6PCdO1YgIeE7xo2bDbFYxPTDSSjahqXkFcuZFczKNHJ5xpqKpcSUJQSODs1RRt8YixYtyqe7XjRRnPrcAGDAgAFMuKPU1FQ8f/78l9S6QG5FEAsWLGA6Vx8+fIijR30xYsQ4uLsPRJ8+bujUqbsCMSjv94KMVNj9YKzBBQ6xsQcSgMysLKxeNQMGBkYYONADgIiVVrZOAotMFfb0Cll8gmVP4eJ8BkCA7l08sGbNGkRERPzSvdu4cSOqVKkCbW1tODs7Mws2Z4f4+HiMGTMGJiYm0NLSgpWVVaELqFnc/NxoiESiXMdw4+QXzNKiB3pBlF27dsHFxQXVqtkiPPwNZs5cDCsrG66Zp4TkuD5qfHOUF4uNITaKmWualpaKZUu90ahRK7i6tpX31cnySbIohTKV+rJR3M8U20xVOugg3f/v8FIYGIpx+vSpXN23I0eOYODAgdiyZQucnZ2xZs0aHD16FK9fv4aRkZFC+oyMDDRs2BBGRkaYNm0aTE1N8f79e5QpU4ZZfq4gQZulx449Utks7d7dsVCbpVWrVkVOlPT27VuVyxIGFIogpk+fjpo1a6JmzZqIiHgLQtSwevVWGBkZswiCRVY8UpGvcaBIZMoIj56dQBHpC7JooSc6dx6IOnVcWO4j8n6ybImNpw5p9w+K1Z7sp2PJ9+3bDMPchX1w9epVNGvWTOX7tmrVKgwfPpz5cdiyZQv8/Pywa9cuTJ06VSH9rl278O3bN9y9excaGhoAkOeL9uQFRCIVnXiLwAIxEyZM4PydmZmJ58+fw8/P76czc/gQyK0IwtLSEt27d8fMmTNRqZI5NmzcgzJl9LndWtmoHvasAkWFpsw0BSSygYGkpEQsWjgBvXqNhK2tgyIxyqZkZavOlIyUKkbvpfsI2STI3ULDn8LAwATjxnngwYP7CuGgtLS0FBZ2zsjIwOPHjzlL/4nFYrRo0SJb/7kzZ87AxcUFY8aMwenTp1GuXDn07dsX3t7ehWppQKnJ+fMepqJglnp4eCg9vmXLFjx8+DBXZQl9bkUUpUuXhrq6Bnx9z6NMaX3u4ivMnl60hb1nkxxLLbFHKCn5HFDaHP2REI8F8z3Qr+9Y2NZwUFRYFJc4ObHcFAYHZJ8hb5fcFCHSjbAHHOT7+/cvIPjFXYwfuxHfvyWiZ8+eKF26NGdbvHixwv2Ki4uDRCJhlgGkUb58ecTExCi9xxERETh27BgkEgnOnz+PmTNnYuXKlViwYEFefpW/DTV1kcpbUUWrVq1w9OjRXOURlFseITg4GC9evICBgQFsbGxQsWLFfKvr3bt32LNnD/73v9PQ0dFliEFKC4SzZ4MdGJJNMlyzlR7MlA8gfP0Wi6VLJmPIEC9YWNRQEt+Nq/SUm5OKZEdR8hbKj2XfT3c34BzevXuJ3j2mQCQSo31bdxw7uRxv376FgYEBUxZftf0qKIqCkZERtm3bBjU1NTg4OCA6OhrLly/nhKkqaKja8V4UzNLscPToUejr6+cqj0Buv4mnT5/Cx8cH165dQ40aNRASEgI7OzssWLAAzZs3z5c6J0+egrZtO8PW1j7H/imOQy7FNzsJE7pI/rf8PGTk8/FjFFatnIaxY2ahYkVZZy8F6SgrQ1qKzsA/M0spntKDbHoWoNysvnXrJD5+ikCPbp6AzJeuenUXVKhghVWrVmHdunU53rOyZctCTU0Nnz9/5hz//PkzjI2NleYxMTGBhoYGxwS1sbFh4uEVlhW6itMMhbp167JUvPS7j4mJQVxcHDZv3pyrsgRy+w3MmDEDS5cuRYMGDXD48GFUr14d6urq2Lt3L4YPH/7L7go54caNG7hw4QIuXQoAwCcNrvkHpoNemSJiu54pDjxQFMHbiDfYuHE+Jk1ajHJl5esd8AcoaJXHrT9ns5ROz0y+V6b0ZJ+vXT+KuK8f0e3fiZw5qSKI0L7NKKzdMBLu7u6oUaNGtvdNU1MTDg4OuHLlChNtgqIoXLlyBWPHjlWap2HDhjh48CBn6tubN29gYmJSaIgNKF7kRn83NMRiMYyMjNC0adNcR+IVXEF+AWlpaZg0aRJOnTqFmTNnon///ihZUj4UHx4ejubNm+Pw4cOoX79+ntUrkUhQt64DWrXqhOHDxykQAUMwUBxEYM8skBOZ8hFKiiJ48TIQe3avwZQpy1G6tIF8GhbHXUM24EBRsighyspUVIYUXzHmsL985X9ISoxHxw7u2ZL42XMboKX9A5cuX8zR9Dpy5AgGDRqErVu3ol69elizZg18fX3x6tUrlC9fHgMHDoSpqSnTZxcVFQVbW1sMGjQI48aNQ2hoKIYMGQIPDw9Mnz49z77XXwXtCnLpcjB0dX8+YyM5ORGtWtZU2RVkzpw5mDt3LueYtbW10jDggHThb3okmoaWlhbS0tJ+Wld+QFBuv4CQkBCcPHkS8+fPx4ABAxR+xQMDAyGRSPLcbWDXrl1ISPiBQYNGKnnR+eqIR3p0Xxyj6uQd9YyakuV59OgOjh/fjenT16KEjh4rGq+i8qNYgw+c+ul/bEVIO+4SrruKwsCBbOd/cS8yMtNZxMa9ThrNmg3EqjWD4Ofnhw4dOmR7/3r16oUvX75g1qxZiImJgb29Pfz9/ZlBhsjISM6oo5mZGS5evIiJEyeidu3aMDU1xfjx4+Ht7Z2n3+vvQiyWbqqkyy1sbW3x33//MX+rq+dMGaVKlcLr16+Zv3Pbz7djxw4kJiZi4sSJAKTv0p49e2BmZobx48czLjmqQCC3X8CTJ09QqVIl9OnTh0NsEokEp06dgre3N9q2bZttX86vID4+HtOmTcfcuSuhoaGpYN7laP5l09Gv6MoB3Lp1Cf/9dxrTp6+VxmJjSFK+ceqk5GGPuAQnV2oMcSkMaGTXLgrnzu+EWKyG9m2HMccAZXkAba2SaPbPIHh4jEerVq1yNBnHjh2brRl6/fp1hWMuLi64d+9eHnyD+QcRRBCrMqCA3Jul6urquXqORSLRbz33mzZtwpQpUwAASUlJaNmyJerUqYPTp0/j3bt3uVqwW3AF+QVoaWkhJiaG418VFBSEpUuXYt26dbC3t4eXl1ee1jlv3jxYWdmgWbPWnJdbCsLsOSTE9LXR5iDXNYRrigL+/sdw585lTJ26Cpqa2vIVqRiZRpMYpETG9JvJzvHcUPgkS4cnZ84z6eSbRELh6PG1KFGiFNq0cgNtCgOKxMa+dkeHdsjMEGPt2rV5et+LAuioIKpsuUVoaCgqVKiAqlWrol+/foiMjMwxfVJSEipXrgwzMzN07twZL168yFV9YWFhqFOnDgDgwoULMDQ0xOXLl3Hw4EGcOHEiV2UJfW6/CFtbW5iZmcHR0RGRkZGIjY1FVFQUGjduDB8fH1SuXDnP6nr9+jXs7e3h6+uPatVsFKJ3sAmK/eIz06YU1Jqi+Xry5AG8ffsGo0bNhEgkYlQZV6FxFZlEQnEm0hOm302xbYSS9s3lNACSlZWJ/x1aimqWdVHPqQ1zTkLHjwOP3HhmcHh4II4cm4vw8DAFf7biCLrP7ebNVyhZ8ud9bklJiXB1rY6oqChOn5syp2dASi5JSUmwtrbGp0+fMHfuXERHRyM4OFhpVJaAgACEhoaidu3aSEhIwIoVK3Dz5k28ePFCZdcofX193L9/H1ZWVhg5ciS0tbWxdu1aREVFwcrKCqmpqSqVAwjk9ssICQmBr68vDhw4AFtbW5ibm6NLly5o0qQJAOlLl1d+Re3atYO+vglmzFjMKLJsBwM4piiBWCxCZqYyUpHvDx3aiu/fv2Lo0CkMYVAsUlPox6MHErIkyolMiWlKu34AysktIzMD+w4sQF375qhdy1V6HgDbZSS7vPT9JgQ4eHgOXBpYY+fOnXly7wszaHK7dfu1yuTWuJG1wvHZs2djzpw5P80fHx+PypUrY9WqVRg6dOhP02dmZsLGxgZ9+vRRORZbq1atUKlSJfTs2RO9evWCr68vWrZsibt376Jfv37C3NI/ARsbG8yePVupMyftOkAT3M+i6OaECxcuICAgAH5+d7OdB8pVSDSJAFWq6MPAQAvp6QTBwTHIyOCSHEVR2L9/A7KyJBg2dArHPKTNUT6hcfrdQKupbIiTpSSVzXmlt/SMdOzdPw/1ndujho0Lx0Ulu2sGlPfbtWw+HFt3uGPMmDGoW7fur369RQpqYhHUVHDzoNMoU26qoEyZMrCyslI53LeGhgbq1KmTq/Dgy5cvR7t27bBr1y5069aNWaw8KioK7u7uKpcDCH1ueYKrV68iOjoaALfjm1ZubGLLjVDOzMzEhAkTMWbMFKk7hpKXXOn0KgqwtDDEs2cBsLa2RnT0W5QrV1Ih344dKyAWq2HAAA+WeUnJgkiyzVAuedIEyg6BRLHT0+UwRCgnNnb/IECQnp6C3XtnoXHDLrC1qS8jVEqR2ChCZ5GqS1afnfwYgX5pE9Rz6IyxY8fl6l4XZeQ25FGpUqU4m6rklpSUhPDwcJXDfUskEjx//jxX4cHt7OwQHR2Nb9++caZb9erVK9ej1AK5/Sb8/PzQq1cv7Nu3jzkmFoshEonw8uVLrFy5EosXL8aMGTMQHh6uMMk7J2zYsAGEAD16DmS91zwykR2Tii0pIejqaiAt7Tvc3Nxw/vx5VK9eHeXKlUDNmuVhYqIHQgju3buKMmUM0avXSM6IKpus5GYlGLJjxhhYpMIwVzYDCtzJ+vItJSUFO/fMQtN/eqNaNQdWvdwBD3BMT7qPjXfdLKXYwLkXQkLewNfXN4++5cKN/Irn5uXlhRs3buDdu3e4e/cuunbtCjU1NfTp0wcAMHDgQE4ggnnz5uHSpUuIiIjAkydP0L9/f7x//x7Dhg3L9TWVKVOG83dcXBzMzc1zVYZglv4m2rdvj7Fjx6JFixYApMRGd6bu2rULNjY2kEgk0NDQwH///Yd//vkHS5Ys+Wm5X758wdy5c7FixXaoqalnb6IpRNUg0NfXwbFjezBw4EDY2NggLCwMS5cuRXp6OsaMGYM6dWqBkGbIypIqJNr85M5YUFRsNENJwx/JO/Xlsw0UTWWFmQuQklJySiJ275uDNq3cUKWyrVJzVcHspAB5vxtrYAHytoIQaGrowNVlECZ5eqFjx44oUaJEPn37hQP5NUPhw4cP6NOnD75+/Ypy5cqhUaNGuHfvHsqVKwdA0S/w+/fvGD58OGJiYqCvrw8HBwfcvXs3x5kjfJw7dw6TJk3Cu3fvkJmZyTknDe0krY+2kHKCMKCQxwgKCsK0adNw8eJFAED9+vUxfPhwuLm54cqVK+jatSsuXrwIFxeXHMsZOXIkwsIisXbtbpZyUuxjo1iKiya6SpX0sX//Rnz58gWenp5o1qwZPD09ERsbi+XLl+P48eOoVs0Jr17F8siM+5lrZkJmjlJKVJnyfjFKwu7jk5+Lj/+C/QcXoUP7ETCrWJ1LimAPIsgJlN/HCHDLpCSEMV2l7adwwHcyBg/tVagmuecl6AGFJ08iVB5QqFu3aqEOVmltbY2WLVuiefPmnDm9CQkJGDRoEE6dOgUA6NSp00/LEsgtj0D3sY0cORLXrl3DkCFD0L17d1y7dg3Lli3D/Pnz0bt3b4wePRohISG4du1atmU9ffoU9evXx/ET12BWsYr8BWYIRXFhFQlrelWZMtrQUI9H48aNMWjQIFSuXBn9+g0FIcD588exZs0a3Lp1D9evhSshSgKFEVJKXr+EF1hSUa3x+wO5pPc5NhKHfVegZ/dJKFfOTCn50e1QJDW+uuMrS3AI7kN0CI6cmok3b17DzMzsDz4NfwY0uQUGvlVpwZzExETUqWNeqMlNS0sLkZGRCq48sbGxMDY2Vkmx0RD63PIIdB/bgQMHsHz5ckydOhWWlpaMaqM7R4cNG4Z27dohISFBaTmEEIwfPx59+w6VERs4fVHKZhvIVY50KtS3rykwNq6M4cOHY/Xq1YiJiYFYLMKXL8lo1qwZ4uPjoaYmkq9xwIvQwSYP+eLLMmda2Un2oAN34y5Aw27r+/ev4HtsNQb2mwmjcmacAQKwyIz9WTmxcTKCSAgz+wGM8iQwNa4OawsXeHlN+TMPQQFBJFJ9K+wwNTVVOsChpqaW6+mMQp9bHiIiIgJWVlZo3Lgx5/iLFy+YOXkWFhaoXLkySpcurbSMEydO4OXLECxevJ2niNjERnfOS/3YtLTESEnJknGCNM2rV18wa9YcEEKwfPlylC9fHq1bt4aX1xwMGzYMX7+mQD6imb3yYuqjeCGToFxNKRKTdHsZ8gA3bh2D26C50NEuqXRtB+k1yYlVGcHJy6cHNuhhBW4eQHovGjkPwM7/jcadO3fQsGHD/PrqCxTSuaWq9Ln9gcb8JrKLpGNoaJjrKDtF4HKLDurXr483b97g6dOnoCgKaWlpuH79Op49e4YGDRoAkEbQNTQ0VJo/NTUVkyZNwtixU6GrW1IeSoh2vyBc9wxtbXXUsTeGRdWSKFtWVz4SSQiSUzLx+PFH+PjMxsWLF3Hjxg30798fjo6OmDx5Cl68iOGNahJOfm6cN3Y7wIqyq0g+FEWHGperyYePLuHeg/MYLCM2ORMCYLmGSM1tljpUsskVIsXcF3qUFmxSlJ3T0zVEvbrdMGbMuFyZNEUJdLBKVbbCjhs3buQ6nHh2EJRbHqJs2bKYNm0apkyZAnV1dVStWhXHjh3DP//8g759+/40/4oVK6CrWxodO/aUrdYOTv8Xm3A0NdVgW8MIHh5joaenBx+fufj06QeHbFLTKMTEJMLSsg62b98PLS11fPmShOvXw5GSnKlEFSoOEuQ08V5hRJWlKGl1d/36UXz+EoUBfWdAJBJLSRF881dR6UHhvEhGXCKZWQxQEopLaERGdBzVR+BQsyN2+17G3r17FULyFAeIRapNnFclTUGjWbNmsLKyQkhIyG+XJQwo5AMuXLiAGzduIDo6Gh07dkTPnj1/mic6OhpWVtbYuPF/sLNz4qohRq1Jj6mri1GrljHmzJmBp0+f4vTp00hKygIApKdnISoqAfHxabC3N8Hr14GoW9cBz559QVxcMlMOv8ycPssHEZSRnOIxiczF5OLlfUjPSEP7NsMBkXzRFxA5+THTsnjlQWldctOXPTqcE7HRKu51+F3cebIHERFhhbYzPbegBxRevngPPb2fX1Ni4g/UsK1cqAcUIiMjoaGhkSvH3+wgkNsfAj2amh0GDBiIr1+TsHDhBg5xKLh/UATVq5fDrl2bcODAAdy9exc6OjrYvXs3zp07BwsLC/j4+CA2NgsWFgZo2LABevbsiT59huPxow8yslQMOKmowuTmKHdCPkEtuwqoVq0sRCIRQt98wZNHHzijqBIJhTPntkJLSwctmw9gKTXWgADdr8dRjdkRKBTJTsIiMT7Jscpk6pFQOHlxLrr1aoPly5f9oW89f0GT26uQSJXJrbpNpUJNbnkJoc8tn8B0asv2ORHbvXv3cPz4cXh4+CgSG6uvix65TEnNRKNGjfDp0yecOXMGY8aMwd69e9GmTRuEhoaiUaNGMDPTxfv38bh8+TLGjBmH0NA4DrHIWC57kqPokVQu0TRtXg2pae/Qrn0LNG/RGKnp71G/YWUmOGWWJBO+x1ahdKmyaNmCJjYueUGWlh6B5UQF5rdRGbGxyVAZsdG/14yiIxARERrWHYh169blaq5jUUBx6nPLSwjKrYBBURTq16+PunVdMWLEROYlVhxNlCsoiiKoZmmIuLh3aNu2LaytrXHp0iVERv5ARVM9NGjoggkTJqCOfQvExiYhI1OCtNRMrqnL95XjzeOk62E701azKoeSpRLQp08fbN68Ga9evcLixYsRGRmJ2zciEBYah48fYxAaFgh7u6by8qC8v45rjvJNymyOM4MZXLNTmVnK5JHI0169vw0Vq2rh7NkzBf3V/zZo5Rb6+oPKyq2adUVBuQnIPyxYsIBRDwcOHMCHDx8xYKA7QwJ8YmObhjThvHr9BaVLm+H69etYu3YtQkK+4M2bL0jPoJjYWQQESckZSEvLYk2uV7IOAst5l1ZSfGIjhMC8qgGWLFmCefPmoUplO4waNQrGxsY4dOgQ3n8IQM++tWFVvTLs7ZsqurCxNoC9l5udbAKjg2MyQTJ5c1aZyfJskqNdQsAjQ9bflcrb4fKly7h8+fIvfXcbN25ElSpVoK2tDWdnZzx48EClfIcPH4ZIJFJYACUvQIcZV2X7m/CXXW7hQHh4OBYuXIikpCR4e0+Fh8d0aGvpsEJ2c80umhX45l1o2FdkZpaCnp4pEhLSULeuKYKCHiA4OBgdO3ZEbGyyfFoS7XLBYhxlAwPcc7x+L5HUBLp16xYqVymDgwcPIiYmBs+fP8fy5cvRr18ftG5rLQt2CYWy5X1yrIi8fPXImg3ByQd+XyDLFYRvkvL73mQkHfr+Pl6E34CjbVeMHeuBrKysXH1vR44cgaenJ2bPno0nT57Azs4OrVu3RmxsbI753r17By8vLwX/x7yCYJYqh2CWFgDCwsJQs2ZNDB48GA8ePMHWrccAiBRVG+9Fl09Yl45IxsREwdjYDGKxCE2amGPFiuXYvHkzTpw4gVJ6lfH61RdIJBKlc1KlJMEzAen6KErp2gwWloaoUlUN3bt3R0pKCr5+/Yr79+9DjDLQN1RD5cqV8fDhQzy4l4jPMYkcZcUmO4pXnzJzFPwpYGzzk/+ZdYxTJ0WYfsXgN1fwIeYlWriMBKEA30tTMWP2ZIwbN07l783Z2RlOTk5MHH+KomBmZoZx48Zh6tSpSvNIJBK4urpiyJAhuHXrFuLj45n5kb8L2ix9G/FRZbPUvGqFv8YsFfzcCgCWlpbo0KEDtm/fjr17z4EmNjahKVM8tJ9bZmYmVq2cDju7+ihf3gxZWRQSEzNQp04d3Lt3D2mpWggOjuFG61BwulVGcvw1SLn70Ddx0NY2xoMHDwEQmJubw9zcHFevhKFhBTNoa2vDwMAAaWnfuIpPbkUy071AwEtDWCan4uACX6Vlq9QIuzJpXY+en0JSajxaNRgFQAyICerX6osZM2aiW7dunGUZAeVhtzMyMvD48WNOiB+xWIwWLVogICAg2+963rx5MDIywtChQ3Hr1q28eHwUIBIBqgT8KKzC7dOnTyr5gbJBCFG6oA8bArkVEH78SISVVQ1YWdXkTDniExs3vhmQmpKCpUu90KxZJzRo0JI5/uBBFCpXrov371Lx5ct3Th4i648iUEaccmUoV0SAhoYY5Y31EBUZzxkxfRr0CU+DPmLI8HrQ09ODh4cH+vbti/79feDu7o7UFDXEf0+D3CBQNjjANoP5pMRVcwwJUvx0SogNrOuQEFCUBLceHoCmZgk0cRgIQMTUXcXEHkZlLNGlSxcFj3hlYbfj4uIgkUgUJnSXL18+23U8b9++jZ07dyIoKOj3H5gcIIJqJuevrH71J5CamoqAgACMHj1apfQpKSnYtm3bT9MJ5CZDcHAwXrx4AQMDA9jY2Ki8oMWv4OrVq7h3LwBHj96gp37L/ilTVjJTjqKQkBCPxYs90bPHcNSu7cwQBQggySIID/vKiZQLxrzL2SylK6SnX2loiNG+ky3i42NQ3cYaF/xCFNrz8MEHnD17FuPHj8ewYcMwfPhwuLuPwYG9jxUUH2TXxvSj0TeCR2T0PQBrrihtjisdBc1OvVEEWVQm/ruzFcaGlqhl1ZIhNbpOUATONXrj6JUZCAgI4MQcUzUybU5ITEzEgAEDsH37dpQtW/a3y8sJqvanFeY+N01NTaxatUqltF++fMH27dt/mu6vJ7enT5/Cx8cH165dQ40aNRASEgI7OzssWLAAzZs3z/P6srKyMG7ceLi5eaBMmbLMi6vcBJX3gX37GofFiz0xbJg3LCxsuKqMZ1oSAm5oInb5lLL65MvnicUitGxtjePH/4cdO3bg9u07EItFssCW0vQikQi6uhowMTbC3j1HoKmljvfvvmP3zodISsyQExb4BM0mJvA+KzNLSY6DBgpkRxEQCUFmVjou3tqAapVdUK1SfYbgwa6LEOjrVYBNlaaYMnkqbty8luPLX7ZsWaipqeHz58+c458/f1a6Tmd4eDjevXuHjh07Msfoe6yuro7Xr1/DwsLidx4lBqpG/Cis3FapUqUcTXs+DA0NERgY+NN0f/Vo6YwZM+Do6Ijk5GQcPnwYBw8exPPnz9GyZUsMHz48X+rcsWMHkpNT0LOnm/SFJ5Rsk33mjQRSFIVPnz5g4cKJGD16JqqaV5cGgaTdNiSUdKBBNghASaQT18EQg7wsOkwRe5O7mYhAUcA/zarh3v2rWL58Oc6cOYO0NAm69bBDr7514NKwCtTU1VCvfiUk/AjDlasXkZCQhm2b78H//CsZsbFJmU1scvOSUASQSImISCDfs909KIBIACqLyCfG8/b8Y6AIMjJTcP76athaNpUSm4w0QfH30nxO1l0R+CQIZ87k7PemqakJBwcHXLlyhTlGURSuXLmiNPBo9erV8fz5cwQFBTFbp06d0LRpUwQFBeVpfLmiPlqqrq4OW1tbAFDJtUYsFqN27do/T/fbLSuCSEtLw5gxY7B7926sX78efn5+6Ny5M6ytrWFhYYFBgwaBoqg8X2n8+/fvmD59BsaOnQ51dc1sIm1wFVxE+GusXOGDSZMWo2LFqkqUHXujGGJUDEmufKN73imKQjWrcvgSF44BAwbgzJkzKF++PK5du4jOXVqjXbtmeP3mFnr0qg19fR1ER0dj9+7dMCpfktWHJu83o6UZTaB0tA++CUrY/2jCZZE+yekfh6QpJCXH4+zVlXCw7YxKJnby61f4Jy9XQ0MHDtX+xXiPiUhPT8/x+/P09JQNAu1FSEgIRo0aheTkZGYyPntNAW1tbdSsWZOzlSlTBnp6eqhZsyY0NTXz7LkqTvHcXFxcYGtri+XLlyuo5NziryS3kJAQnDx5EvPmzcOQIUMURssCAwMhkUhyHRzvZ5gzZw6srGqiYcPmTF+U9H/Wi84ituAXT7Bly2L4+KxGuXImshdfRmx8PzGZ2qEXeJH31bOUFJEvpkKPWkpDFEnPf/uWDGvr6mjcuDEmTZqEXbt2wdvbG5MmTcLo0aMxduxY/PffOaQkZ8Klfhv873+Hcf5cCCdaCTdUErsfjDaJwSVfNrnTZiajxMD0oSlTb2yzND4hBudvroGr4yCYlLPi9cspV210HdaVXJGeAqxZsybH769Xr15YsWIFZs2aBXt7ewQFBcHf358ZZIiMjMSnT5/y9JlRBUVdubHx8OFDdO7cGdu3b4eZmRk6deqEEydO5NonEfhL/dx27tyJ7du34+rVq5zFQyQSCU6dOoUpU6agefPmKo3IqIqXL1+ibl0H7N59FlWqVOP0Q/HVGiEEgYH34eu7Hd5TV0FHR5fbryZjL4bk6LIkFEOA7P46hX439mdK3mFPAJhUKIVWbaphxIjhOHPmDEJCQhD1PgMGhrq4eesM9u/fj8OHz+DA3icAIKtTTqQcFw+wQ4bLiIWumx/BgznH7mujeH1lygcUvnx9h2v3d6N1g9EoWaKsvByKNYCgZE+xzn+MC8GVp+sRHh6mtA+tMIL2c/sUHauS39qPHz9gYmpUJPzcQkJCULt2bYwePRpHjhwBRVHo168f3NzcUL16daxevRrx8fHw9PSEkZGR0jL+SuWmpaWFmJgYzjJ7QUFBWLp0KdatWwd7e3t4eXnlWX2EEEycOBGdO/dB5SrV5AqKPyggOx5w7zpOnNyDadPXooS2LkvFyF9GvlnKTJtikyRHwtEKit2vJWsDZMkoIDoqAWdOhmDHjp3w8/NDcpIG7t2NREJ8GjQ0NKCvr4+MdAlDioCIozo5Mws4/WLgkBTNgmzzmDat2e1l7+WDDcxNQ0xsKG482It2jScoITbuvVU+uCFtiolBdVQ0rImp3sqdcQszipNyo0FrrrVr1+LTp0/Yt28fYmNj0bBhQyYUeVZWVo7+cX+lcgMAW1tbmJmZwdHREZGRkYiNjUVUVBQaN24MHx8fVK5cOc/q8vPzQ//+A3Ho8DXolSytoKDYhHDj+gXcvOkPL6+lUFPTUFRc2cRco9UX1wH3J+qN4qWHnABK6mmhRo3yCAuNQ81axjAsB7i6uuJ///sfkn4Y4MnjDzxilhEXqwxGtSkbrWSZqOxzhBDORHe26uOPlkZ9eoEHz06ibWMPaGnoKpxnVG4O9QNywv2R/AUnAmbg9u1bcHJyyrPvP79AK7fPn+JUVm7lTcoWCeX28uVL2NnZcZb3e/36NbZs2YL169fj06dPiIiIQI8ePRAZGam0jL9SuQHAsWPH4OLigiNHjuDHjx+wsbHBpk2bsGXLFlSuXBl5xfkZGRmYMGEChg6dCL2SpThqS65wpNtF/+O4d+8avL2XQ11dQ34+G2KjQ25L6JFTxsRVttq7olnK6R9jqboXL+7hW1wiAu68h30dU8TEvkD9+vUxc+ZMVKhgjcDHHxjlJ1dWAFjXQlj9Wpz28wiV77jLnUvLGoRgDT8AwNvoQDwOPov2rhOgpV4CRBofXU5mbBcSQJZXtlei5AghKKljiJqVWmHsGI88+/7/BIrTgAIfnz9/xtq1a+Hk5AQ7OztERUVh+fLlaN68Obp27YqZM2dmm/evVW45gaIoiMViEJJzgElVsGLFCmzduhPbd5xlFldmv8j051On9uPdu1CMHjMLIpGYu+ZnNv1y9AsskWSvzBRVHNc045tq/109hO/fP6Nr57EQidRQqXIZtGprgXfv3qGEjgnOnn6BtLQsReKlyQqQ18P0t0GJgiOcPji5yczNw1djFEUQ+u4+XkfcRuuGY6Am0lBUZXz1RmiSldfDpIc8HwiQkZmGUw9nYfPWdczK6oUVtHL78vmrysqtXHnDQq/ckpKSsGvXLkycOJFx+3Bzc0O/fv1gYGAAQNo/npaWBl1d3WzLEcgN0hkD1tbWMDU1ZYiNjZiYGOjp6aFEiRK5IrvPnz+jWjUrzJu/CQ4ODZUSDkUR7Nq1ChRFwc3NEyIRpOsnUNmRFPuYbKSTTVJKXUCgQGQSnkOtRCLBydMboatbGi2a9WeugaIIDA11UbKUFsJDvyqQLKCogCieCanYsc8jNkqmyWROuDnNQHj2+jKiP79Ci/ojIYYa5xqyJTgFklU0cRkVCSD0YwBex/khLPxNji9PQYMmt7hY1cmtrFHhJrcuXbrg4sWLKFWqFPr27YvBgwer5NOmDH+tWUrDz88PvXr1wr59+zjHac739fXFiBEj4OjoCB8fHyQmJqpc9vTp01G3rgvq1m0gMxvlTroURZCRkYHlK6bCwNAIbm6eDFkxjrkSmZMux2GXPkZx1BFjjvL9vwhROvgAOj0hyMzMwP6Di2BsbI6WzfvLrl+u8r58SULYmziZics2e7nXxBASM1ggs0lBk4uczDiKjUhJjY69xvzjtfnO40OI//EZrVxGQUTEnHtJeHtKQknnl0pYTsyyFbMoltmu8I8QVC1fD2JJSSxZsiSPnrJ8RjGySwkhOHToED5+/IjVq1f/MrEBgnIDAMydOxdt27ZFvXr1AADJycnQ1tZGVlYWXF1dYWRkBCcnJ9y+fRv6+vo4cuTIT8sMDAxEgwYNsHuPP0xMWCurywgjNTUFS5Z4oXnzzmjQoIVMPVHKlZoys5SSKz/2alWKjsBcs5OekUCbY+npadi7fx6c67WDbY0GCqqMnjXBLhdg16HEHGUpSwUzkN7z1BSzkhVfZclU5ZWA7SinXxl21q2VmK9QKJ9vDtOjowpqDUrUGwFif0Tg0rNVeBP6Kk8Hl/IS8pBH71RWbuZVqxRq5ZaXEMiNh507d2LLli3Q19eHpaUlIiIi4Ovri1KlSuHNmzeoV68erl69irp162ZbBiEEjRu7okqVmhg+YjJDVrSCSkz8gcWLPNG9+xDUrl2fIR62Sae0z4w3f1RKCtn7yvGJkUOCBEhJTcLe/fPQtEkvWFrU4eXnEZX0wrhpoFifVBVyzT8uySmarLRPmzIzMSsrE5dub0IV0zqobt6IQ4T0AIYCcfIJjqI47eQMgIBbH/vzndA9qOVigmPHjub7c/crSEtLg7m5OWJiYlTOY2xsjLdv30JbWzsfW5Y7xMXFYebMmdi8ebNK6RMTE+Hh4YHdu3fnmE4gNxYePnwINzc3tG7dGubm5oiOjsayZcvw5MkT2Nvbg6IotG7dGm3atMGkSZOyLcfX1xdjxozDvv3/QVu7BIegvn2Lw5LFXnBzmwgrq1o89ZUDsbFUH4hctdGzC5QpNTYhUow5Jk2bnJSAPfvnok0rN1SpXFOB2OTmq+JxNqmBdYw2kznERViqjdd+Jl2WolojhCAzIw3nb66DbbWmqGrqwCFDfr+cMmJkz4TINg0U66X3KWkJOPt0Ni5dvphvUXR/F2lpacjIyFA5vaamZqEiNkC6yrydnZ3KXT6xsbEwNjZmAhFkh78+Kggbjx8/RlJSEkaOHAlra2sA0hs5cOBA7Nu3D0+fPsXz58/h7e2dbRkpKSmY5OWFYcO9oKOjy1FLnz9/xPJl3hg9ehYqVbLgkAbjEsFTV0SmThQVGSD/buXmFZfkCONAQRG52kmI/4J9/1uIzh1GwdRUPltCoSzmL3DrZqsg9jkeseWorlizLZQpu7T0ZJy/uRYONTrCrLytginKUV8KxKmo0JSao3zVxgYF6GiUQg2TNhgzeiwCg55ATU0tdw/UH4C2tnahI6tfQUpKisJA3u9CIDcWEhMTUb16dYbYAKBTp064f/8+PDw8kJCQACcnJ1SvXj3bMpYvX45SegZo2aKrvK+JInj/Phzr18/BxIkLUb58RYZ8sg1NxFJqysxSRbWnTL2x1Yv079jYDzjsuww9u3uhXLmK0uPgqjF2HihtmyK58VWbonqTHeeMVNLELatHWhpSUhPgd3MtGtr3hrGhZY5EBSXEBhZZKiU0/Jz8aK6rbtwM/i8DsHv3bgwbNixvHjQBHJiYmODkyZN5Xq5glrIQGxuLqlWr4vTp02jatCnEYjFGjRqFpKQkbNu2DUFBQXBwcMg2okNUVBSsratj+fI9qFGjLkNCr149w65dqzB58jLo65flkJZEQjFTpvhEQrHnYjJkQa9xkD3h8MlIIovF9iE6FCdPb0C/PtNRulRZrnKEPB9Fyeetglcm+PUgZ2LjmI9KXET4fW0JibG4dHsT/nEaDMPSZsxxOtoJm6gU6mORJghrkIJHfMwTr0zNsdxw6ITvvwbheewxvH0XjtKlS+fX4ycgjyGQGw8LFizA0aNHUbVqVRgZGWHfvn1Yt24dJ76bMl84AOjbty/i4lIw1Wcl8/I/fHATp07th7f3cujo6HGIguvKwVU6yhUbTYiKaaTt4puudF8bQWhYEC7/dwD9+81ECR09AMoUH9cfjk2sYJ3nEB6gYDIqN1H5ZinXBAcBvnyNxLX7u9DSxR2lSxpxzsmJSlYpeMSkjDh5ZCflcLodfEKUqz122TSx3ozYiO4D2mDVqpX58twJyHsI5KYEu3btgr+/Pz5+/IhRo0ahX79+P81z584dtGrVCrt3X4ShoTQ80eXLp/DgwXV4ei6GurqmgrqSSCQKKkshLDiP2JStJ8olKMW0z57fxv0HF9C/73RoauookinAvOAUE35IUQGCUz5bYbKIITs1xSc4xvlY+nf059cICPJF24bjoKNdSiGvQtlQQkzK0gPcellEp6y9CkpPlv9bcjT+e70Cz4OfwcrKKu8fOgF5DoHcskFWVhbU1eVdkoRkPxWLoig4OjqhTp0m6NdvDAgh8PXdgU+fouDuPgNisZirqtgExSMzivWZVl98JZY9sYFTJkURBNz3w5vQJ+jdYwprIj5NDty6FFUblP/NajPFJwieKVpSVwsmpnr4/CkR8V9TGSXEJryIqEA8fe2PNg09oKWhI7vW7P3eGBOSr7wo7l4ZobGvg993pziZXq7yQAgeR/vC3E4X5y/45dtzJyDvIAwoZAN6ZIw2QXOadrV3717ExMSie7ehyMqSYPv2ZShRoiRGj54F+WghpC8LxR8M4JGUwnEe+SkMIijb0/NED+L7t1j06TUVIpFYNnBAuIMHrN82+YvOJQDmb4a4aJJDtopNTSxC5x61UbOOEQIDA9GrdkN8eJeME/8LwrfYZCbty/BbeBv1CO1dPaEuls8TBcB0/vM7/tmbiIh4BKWEBHmDCGz1x5ynVR6f2Fj5axi1hf/1hfD390ebNm1+9dES8IcgKLffxI8fP2BpWQ2jRs2Ai0tzrF41E9Vt7NG2bU+e8pKpMp4HvwKJKfNvI7JpWZwIH9nvKYrC6bPboK6mgTat3ADQhMRKyyg3unxuYEi+KuKrOIqvqHik0rp9dYi0PmLgwIGoUKECXr16hQkTJsBrkjf2b36I0JexCA69hpi4UPzjOARi2Sr1HKWVnWqjSUfJOSqL5wwMXpk84mIcgjnlcdOwFdzr2Gv4rvEUr9+8hIaGxh94wgT8Kv76uaW/ggULFiAsLAwAsHDhQpiYVIaT0z9YtGgSnOs3Q9s2PUFYfVcUAS86rnTiukTmhMveKCq77WfEJt0kEgpHj6+Bnp4+Wrdyk45mEqKwEdamzO2DDvVNh0GSh/6m/wbALO5C5IMKsr8dXSphwoQJ8PPzw43rt/HgwQOcPn0aPXt1w4DRjjCtVBpVK9ZFU6ehyomNJvrcbPSke44i45mlcj3IUXBcJStPyyY2QggsDRvjW0wSNm7c+MvPz8aNG1GlShVoa2vD2dk5x0VRtm/fjsaNG0NfXx/6+vpo0aKFSouoCBDI7ZcQHh6OhQsXIiwsDGvXrsNgN08sXDABbVr3QAOXFgrmIecFZE/aJkThxeKQEEXPQqAYUqQoZXvp58zMLBw8vBSmFazg2qg7Lw9bvYGTj8gOMh3xCuqRS4h0EABG3XHyEaiJpcEu37x5A2NjY2xffRuBdxJx+/ZtJCYmYuzY0Rg0xgUltEtBBJZSYpEokc2zlSsp7p6wyJfeMyTNImVOXjoiMMucJiDM3yJZWibyMdOdAOb+gIhgZ9wFs2bORlxcXK6fnSNHjsDT0xOzZ8/GkydPYGdnh9atWyM2NlZp+uvXr6NPnz64du0aAgICYGZmhlatWiE6OjrXdf9tEMzSX0BYWBhq1qyJRo0aQSzWw9evX9C//1jY2NSVEwLdqc8mMkrur8U1UxXNUA7hZfHPQSFdZlYG9h1YiNq1GsHerpl8lBU8c5Qxt+Qqkj3aSZurYF2DgnnKG71UNstglFdjzF0wCbq6upgzcwnWL7qOyhYG+HdgDdjY2ODcuXN4fjcdQfejuGakLD/F83/jm5/0UyvPR3HOgyE2nnJjXwOj3OibBM69Yas2jqolwL1Pu9C2W31s2bolV8+Os7MznJycsGHDBgDS6zQzM8O4ceMwderPQ5xLJBLo6+tjw4YNGDhwYK7q/ttQLJXbr6yUkxtYWlqiSZMmuHHjBj5+jIK7+3Q5sdFKiSY2WkFw5niC2//GJjN2ekoWPpweBJDtuSYmkJaeil175sChbgvY2zVn1Q+WwuMRJqPaICcM0EpITmzyl17WLgmL2FgkxyefqxdeY/78+Th58iT2HtgKjxlNkfAtFR/epsDNzQ0XLlxApaoGXEWmMEABZq/Q50YImOi7srBGbGVGWNcEcIlNzoxgypKbs9kQG0slggA1ynTA7t178OjRI/z48YOzZbdEYEZGBh4/fowWLVowx8RiMVq0aKHyosQpKSnIzMxkgjYKyB7FitwIIbh//z62b9/+SyaDqsjKykJYWBh0dErCx2c1zMxk64ky6oFhMxmhUKBjn8lZh3DUAFeJsUxXSv5i0fNJ2XlSUpOwa88sNG7UFTVtG3JeftkbLdu4So+j3FjEqjiAwDWh5euiypWShqYaDAx0mGsCIXj1LAavnybj/PnzWLVqFabN8MQgjzqwsi2LwMBAODo6IubDD0V1xVJUYO35bVH88qGQR3qBcnNXXgf3WjnkzSJHOWmDUychBHqa5VCxZF38+283lC5dmrMtXrxY6XMTFxcHiUTCLAVIo3z58ipH9vD29kaFChU4BClAOYoVuYlEIsTGxuL06dN5unoVH1u3bsW3b/HYtvU8DA3KswYPWITAvEA0cchUFMAoLorzUnFVEN8slKsXwrzAyUkJ2LV7Jlq1GAgrSwfI1y2leP12tEoTyd95ijb96DayyRYMAXMWjJZwFQ8hBNVtjTB9YUu4ezmjYTMLDln4nwxG3ActPHnyBAkJCTA3N0eVKlVQo0YNuDZuhmcPP3BJB2D1QbKIjEUyoK+BMV/lfW38NSEY8oKc56XXzu735KaXVwaG4dg/NHQd31IjkZD2Gd++xGP//v1ISEhgNnph5rzGkiVLcPjwYZw8ebJYTJbPbxSbPreMjAxmzqevry969+6N69evw9XVNU/r+fr1Kywtq8HTcymcnJoodelQttq78rhrSj6zCI1SmGYl38cnxGHv/vno3Gk0TCtYMiqM3QnP5AG/DMjbqUTBgNVOmvCUpTW3NEDvwbXw77//wsbGBsuWrsKs8edYhE4gVhOhTZcasK5ZHqUM1CAWixH3KR2Htt/Hp6gfrPKIklWveMqKTViyp1Ye241vThLFvABXGQIKn/l10KTG7puLTQrF62/XUM+4H6KTniFW/R5Cw17/lHAyMjJQokQJHDt2DF26dGGODxo0CPHx8Th9+nS2eVesWIEFCxbgv//+g6OjY24f278SRV650f1rNLEtWLAAgwcPRq1atZCamprn9c2ePRvW1rXh5OgKwlEAtOnJUx5M/xXHxlHc6JFCmcyTLxDD34C4uI/Ys28euv87HhUrVAO7n4qpAhwRyK2KNYgAKCESJo9y1xHpcQot2lXHxIkT4ezsjPXr1wMAxs9oir7DnVDBrBTU1IDRU1xRq54BEhPSsG7OLSzzvoLVsy8jJuoHr2E8laWwFzENE9HkRcl/l7PLw/nlZhEVc89o8coeReXdD3b+DwlBCI+/g/oVBkFTrQQq6zkhI0mEVatW/fTZ0dTUhIODA65cucIcoygKV65cgYuLS7b5li1bhvnz58Pf318gtlygSCq3d+/eQSKRwMLCgjl27NgxjB8/HpmZmfD09ETv3r1RsWJFzhSq30VwcDAcHZ2wfv0JVDStqkA6Cg67ss8Ki7iw90pGS6X9WopqjRCCD9FhOHZiPfr1nooy+sbyPPTAA0t5MHnB/Zs9fxSsuhUj50pVkTKHXQ1NMeasaAsjIyPExMRAJBJhyZIluHr1KpycnDBz5ky8fBIH4ypi1KxZEzdu3EDI/UzcvxGhSCBAtqqNo8YAbjsVovey0rKuNzslKC1PSfnM9XMJP/zbbXxPi0Kd8j0ghpjJ+yU1Ao++7kZYeCgqVKiQ4zN05MgRDBo0CFu3bkW9evWwZs0a+Pr64tWrVyhfvjwGDhwIU1NTpt9u6dKlmDVrFg4ePIiGDRsy5ZQsWRIlS5bMoye7eKJIKrc9e/aga9euAIAXL16gQYMG6NOnDzp06IBLly7B09MTVapUyVNiI4Rg/PgJaNeuN0xNq7L6tFgjkjJSo1j9VJSEyBQZWJJItqdYn2Wb3D9LkdjehAbi5OlNGDxwDvT1y3PysFUhs1AM/Y9VvpSoGIkC9grvnBebTTB8nzJCQCTSPs7MzEx8//4do0aNwsePHzFp0iQ8e/YMLVu2RB0XU+jqGMLf3x8WVasj9GWsoqoErRApRikSEHCvANy+OPozvxx2Wtke9N+sQRBlSpR9n+S3QdYWQuHlF38kZ35FHaOeEEHM+TEy1DJHeR0bTJmSfRBTGr169cKKFSswa9Ys2NvbIygoCP7+/swgQ2RkJD59+sSk37x5MzIyMtC9e3eYmJgw24oVK/LkuS7OKJLKLTU1FWXLlkWNGjUQGBiIZs2aYfLkyahfvz709PTypc4zZ85g4MDB2LbtAnR1Synta2N/ptUXf92C7P3a6HPsWG1gzj99dgv3HlzAgL4zOJE9KCJb4YmIWMqNm1fOn1KHYIW+JI5a4yk6JT5s9N9jJrti4dKpoCgKjx49QmBgIM4efobmnazg4uKMxYsXIzVWqi5fPY3Bl5hEmQrilkPJRlqkTyJPrTF7ZNtG8M8r2TMhk0DfDEDfSBdt+tRGJauyiIlMwKmdj/AtJomj+ghFIejzSZTQ0IeVflNG8fHN1uTMb7j2cSVu3bkBZ2fnfHkGBeQORVK56ejoYN26dXj8+DFWrVqFAwcOoGXLlvlGbOnp6ZgwYSL69/dAiRJ62U+RYpbkk24SZlk+wlV32eWnlE/Buv/gIp4EXsOg/nOgoaHNHM+SUKCyiJwMKVqhcDfmOHt1LTaxsZUZAVelSShAQhhfMma6lYTg3NHnWLhQOpE8PDwcX79+BURAUkIm7O3tkZqaisxMCW5fCpMTG+GqQKmyBaNg5X524Ez1kveVsQiO1Z/5s01et5S4qtoaYeXJfohKuY95Kz3x8tMVLDnSC+UrlmJ+aLKyMvHg4/+gr2WGamX+kf8wcUbHpd+rjpo+qpZ0xehRY/Gz2P4C/gyKJLkBwNChQ1GpUiWEh4fDyMgoX+tas2YNxGJNtGzZjfVgZ2/aMGtpchQa4Sgh7osnfWMIy4WDznvr9imEhT9Fvz4+UFNTYylDmQlFm5fZveigzWVpmxSCOvI+86+DrSzly/FJ9xGhcQgKiMONGzdgYWGBVq1aoUTZL7j43yk8efIE7dq1Q2jw55xJh29uQm4Oyv9RrDZJrwVgmZ3KVDB/D4YXoVNCA5PXdcCQYW7w9/dHy5YtsWXLFqzbuAo9Rtdn2vUlJQxmenVQuZQTQGRRYRRtYeYHwaJkE4S9fouDBw/m6/MoQDUUSbOUxsWLF+Hj44PLly/D0NBQaRpCso/DpgpiYmJQrZoVpk1bj5q2TryXhrD6x+THOaafLA3Fz8cx+8C8uGxz9PKVg0j48RVdOo4GIOLVwSYlZG+Sys6x1V22Zin7WhRcP+RtZfJIKBCKgktTS7TrUR27d+/CmTNnYGVlBW9vbzy5+RVXz77iKqefmJcMkWZjXvIdjZGtKctTa3TwD0LQeYgjNCt+wtq1a3HR/zIeXA6HetnvGDZsGG5fe4CR/+zg5pdmk6tG8OqCXIlGpwQhSu0qIt6GCR3+BYwiTW4A8Pr1a1hbWyM0NBTVqlVjjkskkjxZrWjw4MEID/uEyZNX8QhJCVnJOq05RKYwOKD8MyWRDkZARnLnzu+ASCSWhSxiExu4JEgrOMImUGnb6TR0m/jEkiNxUFSOI5iEonDt3i5UMa0D8wp1YFiuBFyaWsDSxgjx31IQcCUcwU8+5kA+Mh816WmmfJo9COHnUSRDDuHksOcT+rilrXE/xA82Njb48FALd/3fYMu1wTAyMkJCQgKGuGxB91HOeHrnPR5djZA3i2mjcqIGpPf6UdIODBnbC4sWLfzt50/Ar6PIkxtFUZg6dSoePXqEnTt3wtzcnKPWLl++jIcPH+L79++oVKkSWrVqxVndKic8evQIjRu7Yv260zAyMmUISbnJKSOpLGWhiXImO3Z5EokEvsfXoFxZU/zj2pNXFne6FHhlyyxUXvky0pPwiUH2orLaxJTHVm0KAwyARJKF/+5uRQWj6rC1+AegiUrJi88QkxIyyImQZAVx8hOeWpXXpRqx0XtDYz1M39oFOiU14NnpAJIT0uH7cjxKly6NhIQEfAj/ilsPLqF58xZY4X4ZYU9lo5estrGJl33vQQjiMz/gwY/tePUqBObm5r//kAv4JRT5SLxisRiVK1dGVFQUtLS0AEhdFAIDAzF79myEhobC0NAQqamp0NLSwqpVq/Dw4UOULVs2x3IJIRg3zgOdOg1AOaMK8gWNKTmxyIkBcpKCcmJTvlSenLAIgPT0NPzv0GLUsm2IunVacNKwTUwoM83osmjiAqtsQjj9U/L0hNWPJj/H7ZtjtZsCsrIycPH2RliY1YNV5fpy1aJAYqy/wT8nVYZ0Gzl1gP03nUB23+lrgLxMdvvZ1ycnUbDqke7jPibCq+sBiMQiZKZlSe+ThIK6ujoyMzNx4eoJbNiwAa1atWK1U1Ye5O0i9MNCca+ztJopTLXsMMnTCydOHlfhKRaQHyjyyo0PiUSCnTt3Yt26dTA1NUW3bt3QsGFD2NraIikpCb1790apUqV+2ul76NAhjB8/EevXnYW2VglFhcZTb9JRSSWqjZ0OcvXDV23JyT+wZ/88NHHtAWsrdt8emJedM8mdEFYfGTsdlxAlCo6ufFXD/hvghA7iEV96egr8b21ALcvmqGJahznOX0IPvHZzlBW4TsFK0zCnWO1QosAAKJA8//q0tNTRdYQTTKvqY8/iG/gSnciQNiBv556Ho1HN2hwzZ87E+vXrce3aNRxdFYSrx4LlZTMN49XDv24QpGUl4nbiavhfPI9//vknLx9xASqiyI6WZoeIiAhs27YNDRs2xKJFizBixAjY2toCkHp1t2rVCklJSUhJScm2jOTkZHh5TUa/vhOgpaXDnYiuhNgUTE/2HtKXgoCe9sSaOC9zAP76LRY7ds9Eu9bDUN3KSZZJPt2ITajMNC2K/2Lx2gDC9LOBcPPRk+Lp4Ixy8iCKbiGyFzYp+TvOXVsJJ9vOqGJaV1oPJTcVmXqUEBtD6kQ2k4Jph0xZMWQsvW5CwDpPmP4s+leY2TN5FIkYBCihq4k5e7qBGEThcsARTN3UmWkjnZ+uOyMtC9ra2li5ciUuX76M05uCceVoMOtaCCfqiVz5Kr9uLXFJmGv8g9GjxkIikfzWMy3g11DsyG3+/PlIT0/H2rVr4eDgwDkXHR2NjRs3wtjYGCVKlMi2jKVLl6J06XJo3LgdRwUxLzSb2GS+Wuw4bfKoH+xZDMrIj+Djp7fY/78F6NV9MipWtOLmI3JvepkA4weHVSBT5kXjqB0AtDGXjcphm5dsgiMU8O37B1y4uQ7/OA2BcVkrRvkwdXBmL7Dr4N4z8DaFenk/FmApJT45y0lR3v/GIR6KQm8PF9x+dBGzZs1C586doa2rAZe2VqhsXZZpV70WFpi9pxu0dNQxd+5c/Pfff/hvXxj89wfJfhRYPxAcEgZjKsv7MOV7EKCSVn18iozDjh07fv/BFpBrFCtyS01NRVxcHEaMGMGJ0EAIQUREBJYsWYKSJUtizJgx2Zbx/v17rFixEm5u3tIVowhtrikjNgJmsjw7mCHnRYRcLfBIISz8GY6fXIeB/WfB0LBCNkqQJkx5rDjpOd4UK9k/aZWEUTKEjvyrzA+ORYCc0OH0PyJdT/Ta/V1o08gD+qVMWMQmmwPL/8cPoy5jAqYtTJw7Sk4G4JMD5D8cvClTVLZ5WOY4RUGsJoZrx+qYP38+Tpw4gRo1aiCTJMGqGcHEjU3hvakTajiZYviCxth9dA0glsDJtjkubA/Fqe0P5fcfUPwMlq8dTb7yHdMOMdRRVdwSU719EB8fn3cPugCVUKzITUdHB6VKlcLly5eRmZkJQGqmHjp0CJ07d8b+/fsxYcIE2NnZZVuGl9dkONdrAYuqtvIZBxR7pgH7M3v1d/rloxSUFz+2GiEEz57fweUrBzF44HyU1DXgzmKglZ6MTNnLAFKMKstuL01ESYh0ARf+edYMA3o2A2NeshUYRRAR9QSPnp9ChyaToKtdmkvaRLaXLRLD7GlTk1ZXFAGRSPfyGRMA5wdD2TXQyg7gkBdHDfLzSmSzRCgCXT1NZFEZiI6ORtWqVXH27Fk0aNAAS5cuhbW1NUI+3MGkdR1AURRevnyJqKgonNz6EJcOPpWrXXa9TDvoa5Pfa7o9ckUpJ+myatZQSzbErFlz8uehF5Atit2AwufPn+Hi4gIDAwNGvd2/fx89evTAxo0boa+vn23eW7duoU2btli7+hT09Y1Y6kzR/GP6kAg9rSmbNLz8FEXw4NFFvHhxF317+8gWSoY8Ddj52fnYjsFck44xk9j1siKRyE1ANkFw8/PXIHgVfhsRUY/RwsVdtp4o4cxuoPgLuIBdD1j1gFF6XIVFSyEwHfWcY3Q92Qw+ME+tkjoIIRAB2HF7BOq51MXSpUsxdepUXLp0CdrqpfHi9RN06NABz549Q/C1r2jUsTquHnuB/Utu8q6FVwcgv6d0smzbI/sMICnrMx6kb8Wz509RvXr133vABaiMYkduABASEgJ/f398+/YNWVlZcHNzY3zb6EWW+ZBIJHBwcERNW1d06TyEqxIYZcYnMK4ay4kEaZV05dohxMV9xL9dPACRmEdKkJl2cmKDjEAVHIc5/VRsUpH9ncUz8ZSQG+ezRPY3RfD01SXEfn2LpvWGQiQSy/OxVArdwc7vtwOgcIxLskrOyx5BPuHxXUz415hdHfTxXmPrQ71CDEaMGAFDQ0O8fPkSbk6bMP9QL3hMHoY+ffog7kkZXD7yXF6OtCFgdjKyU6iPn55NdFBsW6jkPKo11MOlyxfz/oEXoBRF3s9NGWxsbGBjY8M5RpOaMmIDgN27dyM29ivaT+7P6luhySY7wgJDbOwBA4CfVmqWnj67BRoaWujWdYIsDY/Y6Dp59TCqTaFzW05QHGJjdXTTbVFUU+CRFUAkFB48P4m09CQ0cx4GETMzgn6RiQKxQnaPmL2CmiFMk5WSgzJioz+z58FyygCTlslD0cxKc5MIp3Y+xrLjfeDu7o558+Zh69atWH9pMMSaFN68eYM6depgz/HHnLaIwCUs5osCtw6awUQ8Yuc2Vn6usqgJbt/agPPnz6Ndu3bZP7wC8gzFUrnxQUjO80sTEhJgYWGJwW4+cK7XnENKOZmlylaVUraXZGXB99galC9fGY0b/stNg5zzstsAXl3K/Nw465CyCDEns1TqniHBjYf7UEK7NBxqdJISmxJlpjgFSpGU2OSmtAwoy8MjO2X5WMTMqZet2sAlRKMKpeC9qSPuP70GT09PZorVhAkT0LReF8zsfUQelVgZ2UJZG+XXp/R6lOYhiMq8j4wKr/HqzUsmcrSA/EOxGlDIDj+bOD9//nyYmlrAyamZ3BWD7gCn/yaEe1yB2JRvWVmZ2H9wMSpVtkEjPrERotiZzjpPMcTDOifruJYTHV0ZxfrMJjZ5OoZsZJ358kGRTFy8swlly1SCo20XSOeysoQLvVdCOCKZPx73uCKJcjZ2GCOKn1d+nr9Un7wMLpmwf5/lhAPEJoUi+PVj+PQ8DL0MK0REvMW2bdvw6NEjuNbtiOVjzjLExtwjtuKFMoLlPjtytUrkCjabPBXUHBEfm8qsWSogf/FXKLec8ObNG9SubYeF8/ejUqVqcsJhk4IS9cafLcDdSz+np6dj3/55cKjbCrVqNlKeFtw8nD0lr59LHFxyBKtNCpE+GJJTQjwUQXp6KvxvrUcNi39QtaKDokJiEZp8orsyc5inXnh5OWqHpVaVqjeK7rdSVGrca1BShyxf1I8gRP0IhJNxX6iLNEEIgY6uJsxtyuFL9A98jkygLUuW2uL+nW39rHxchQeFtkmPy/N+oyIQqnESEW/D8j1U19+Ov0K58bFgwQKEhYUBACZO9MQ//3SCmZklS63JCYSr3qR7CduFgnHZYBMTQUpqMnbunoEGDTqjZs1GTF6WuKIFF1e9setljvGJE9L/WG4HhIDxoJcrOHDUG19RJack4Oy1lahj0x5VKzry2kW4xMHv02IRsoIyUyAEcOtW8ANk5aFNRLYrCYecWfXzzFH6ZNi32/ic/ArOJgMYYgMBUhIz8OL+B3yOSmCSy5kJ7JurnNhYadkkx6hN/rWDdX8g7c8zEJmjNKmEadOm//Lzu3HjRlSpUgXa2tpwdnbGgwcPckx/9OhRVK9eHdra2qhVqxbOnz//y3UXJfyV5BYeHs5EkL116xZ6dHPnPNyEyAM8sglLUckpIyngx4/v2LFzBlq1HAirao68h55NVER2mLVnvV9SKJISd3FmWTnMSlxg0suJjVWnjDQSfnyB343VaOzQH6ZGNgrqCZC/zAQkGxOSTzrKTDPw2i8vmdD/2AqITRwgnHsAfp10EhYhB8eeR1rWD9Q16gkR1MBOBlY9zJWx8svTibjqiz7PM5E5U8fo74n+2lj3it18EBEqZTbD/r37ERQUlOtn98iRI/D09MTs2bPx5MkT2NnZoXXr1oiNjVWa/u7du+jTpw+GDh2KwMBAdOnSBV26dEFwcHCu6y5q+CvN0rCwMNja2qJCBVM0ce2Otm36cl9+lsqhXzxK9pniKSy5eSn9HBf3CQcOLUa3LuNgbFyV44DLfuH5JiwhUmUHKJqXCqqHIVp5mZRElo5iEwk3H532y7f3uH5/N1rUH4lSukZcZcIjK/AVHJ8E+eTGqyu7dErr47SdtweUpJUTFUVlITDmGEppVYBlmUZysmLVz4T/5n0XbJJj6mK1Jdt0rGPc41wC5hK79Pg7XIWJI8Gdu7dyFUzV2dkZTk5OTL8dRVEwMzPDuHHjMHXqVIX0vXr1QnJyMs6dO8ccq1+/Puzt7bFlyxaV6y2KKJauID+DpaUl7O3tERz8Eo0atEVKciKjOigCuYsE52WVbvzQ22xy+/gxHKfPbkWP7hOhX6Y8UlKSOA90dqTGLUc6C4I7IZs2O+VtYpOBfMoTeATIIkdZ3pjYUDx4dhLNXUZCU1MXaZnJ8noUzD8uUSkjHTA7dv3KyFBZfj7h8clMMR33x0F6LEuSgSefj6JCyZow1a2NjKw02TctJxTpDwAregmUtItXJz3FilH18p1i2+jvmD3yCvZncI4bU454Frgbu3fvRrdu3TgEp6WlxYTvYiMjIwOPHz/mrGgvFovRokULBAQEKH3WAwIC4OnpyTnWunVrnDp1Smn6YgXyF4KiKFK9enXmt1bYhK0wbbNnz1b63EZHRxMA5O7du5zjkydPJvXq1VOaR0NDgxw8eJBzbOPGjcTIyChP3qXCjL9SuYlEIjg5OaFu3brYvHlzgbRh1KhRAPDX1l8Y2lDQ9bu7u0MikWDbtm0Kyk3A7+OvJLewsDD4+voiODgYpUqVKpD6jx8//tfWXxjaUBjqP3HiBIKDg1G6dGmV8pQtWxZqamr4/Pkz5/jnz59hbGysNI+xsXGu0hcn/JWjpQsXLkSfPn1gaWkp1F9AKOg2FMX6NTU14eDggCtXrjDHKIrClStX4OLiojSPi4sLJz0gXVcku/TFCX+lcrOwsEDv3r2F+gsQBd2Golq/p6cnBg0aBEdHR9SrVw9r1qxBcnIyBg8eDAAYOHAgTE1NsXjxYgDA+PHj0aRJE6xcuRLt27fH4cOH8ejRI2zbti1Pr6dQoqA7/QQIEJA7rF+/nlSqVIloamqSevXqkXv37jHnmjRpQgYNGsRJ7+vrS6ysrIimpiaxtbUlfn5+f7jFBYO/0s9NgAABxR9/ZZ+bAAECij/+CLkRQhAUFITbt2//ieoEABAEuRzCvfg7ke9maUxMDPbt24cDBw4gMjISHz9+zHHlqfxAdtF3izsK6rrfvXsHIyOjP/4985GWlsZZKKigQEjO8QQF5A/yjdxSUlJw6tQpHDhwALdu3UJycjIAqePipk2b8qNKAEBiYiKeP3+OHz9+IC0tDa6urjAwMADwZ172jIwMhIaGIiUlBfr6+qhateofJZiAgAA8fPgQgwYNYvyn/iTJnTt3Dvv378e0adNQu3Ztzkv9p9rx+vVrnD17Fo8fP4aVlRW8vb0Zov3TRMOu72/9kS0o5Dm5SSQS3L59G7t27cKVK1fw8eNHAFKv6/T0dBgZGeH169cqOy7mFu3atUNoaCg+f/6MqlWrIi0tDf3798eMGTPypT4+hgwZgjt37iA0NBSOjo7YuHEjnJyc/kjdgNTFICsrC87OzujZs6fCvMX8RsWKFTFs2DDMmTMHFEXh06dPePfuHRo2bMikyW+CcXJygra2NgwNDfHu3TssWrRIIbR3frfh6tWruHTpEtTV1WFkZAQPDw/mnEByfwh5OfT6+vVr4uXlRWxtbYlIJCIikYhoaWkRbW1t5u/Zs2eTzMzMvKyWwdy5c0n16tXJ06dPybdv34ifnx+ZNm0aqVKlCnFwcCBXr17Nl3rZ9deuXZtcuHCBREdHkw4dOhBzc3MSFxdHKIrK17oJISQrK4v06tWL1KtXj3Tv3p04OTmRoUOHkvfv35MZM2aQw4cPE4qi8q0t27dvJzVr1iSEEJKWlkbGjBlDzMzMiLm5OSlXrhzZsWNHvtTLxrx580itWrVISkoKSUlJIcOGDSNTp04lCxcuJB07diRbt27N9zYsWrSI1KhRg9jY2JD27duTypUrEwMDA7Jp0yYmzZ94Hv525Am5xcXFkdWrV5PGjRsTDQ0NhsgaNWpEBgwYQIyMjIhIJCI9evQg6enpeVGlAjIzM0nr1q3JihUrOMd//PhBLl68SLp160aaNm1KIiMj86X+hIQEUqFCBXL27Fnm2MePH4mVlRW5dOmSQpvyC9evXydubm4kPDycLF68mLRu3Zo4OjoSkUhEdu7cmW/1EkLIunXrSNeuXQkhhAwbNow0b96c7N27l9y6dYtMmTKFlC1blmzYsCHf6k9PTydNmjQhu3fvZo7Nnj2bmJiYkKZNm5JRo0YRPT09Mm7cOJKRkZEvBJOQkEBKlixJTpw4QQghJDExkTx9+pR4e3sTXV1d4urqSkJDQ/O8XgGK+C1yoyiK+Pr6ko4dOxJDQ0OG1ExNTUnfvn3J7t27SefOnYlIJCJlypRReMnzGgMHDiT169dXei4oKIiYmpqSsWPH5kvdJ06cIK1btyYRERGEEPkvc69evYi3tzeTzs/Pj/N3XkIikZD4+HhSp04dcuTIEUIIIS9fviQVK1Yk5cuXJ+3atSPr1q0jKSkp+VL/kSNHiJWVFYmJiSHNmjUj/v7+zLm0tDQyYsQI0rx583xTLenp6aR9+/aka9euJC0tjcTGxhJdXV2yZ88ekpWVRQiRqmt7e3uSmJiYL224ffs2qVWrFnn//j3neFpaGvnvv/+Iq6srGTp0KMnIyMiX+gXI8VuGv0gkwps3b3Du3Dl8+/YNZcqUQdu2bTFz5kzs3LkTVlZWeP78OXR0dLB06VK0bNkSAPD161esWrUKx48fzxPTmsbw4cORnJyMZcuWISYmhnPOzs4OEyZMwIcPH5CWlpZNCb8OMzMzlC5dGnp6egCkfY8A0LJlS/j6+oKiKGRlZaFfv34wNTXN8/oBaWyv0qVLY+rUqTh48CAAIDY2Ft++fcP06dNhaGiItWvX4tGjR/lSf6dOnWBiYgJPT09kZGQgMTGROaelpYW+ffsiNTUVUVFR+VK/pqYmevfujffv38Pc3Bz9+/dHxYoV0b17d6ipqQEAWrVqhaysLLx//z5f2lCpUiW8e/eOuf80tLS00Lx5c4wZMwZ79uzJNv5aQYOiF+cuDvhddkxLSyNGRkbE3t6ezJ8/nzx69IgQQkhoaCipXLkyEYlEpEWLFiQtLY1kZGSQ48ePk759+xJjY2OioaFBvn79+rtNYJCamkq8vb1J2bJlyYABA8idO3dIXFwcc97T05M0aNAgz+rjIyEhgRDC7U+JiIggJiYm5N27d2T27NnE0tIy3+qn8enTJ1KvXj0SFBRELC0tyfTp0wkhhMTGxpKjR4/mS50SiYQQQsixY8cYM7hhw4YkMDCQSTNjxgzi4OCQL/XTSExMJIcOHSI7d+4kN27cIJUrVyb3799nzs+cOTPf27Bq1SpSq1YtsmHDBvLp0yeF802bNs33LoJfxdKlS8nLly8Luhl5gjzpc7t79y7x8/PjSG0vLy+ipqZGGjduTCIjI8mrV6+Ih4cHqVmzJtHU1GRM2IULF+ZFEzg4c+YMqVy5MjExMSFDhw4l48ePJ+7u7kRfX59cu3Ytz+sjJPsO4szMTNK8eXOyfPlyoqOjQy5fvpwv9fPbMW/ePCISiUjlypVJbGzsH+3Avnv3Lmnfvj0pWbIkqVmzJmnbti3p0KEDMTY2JtevX/9j7SCEkB49ehB3d3eydetWsmjRImJkZJRvzwCNqKgoMmTIEGJtbU3c3d3J2bNnmX7WwMBAUqJECYWAk4UBAQEBRCQS5fv9+VPIl4nzmzdvZshr8ODBZP78+aRevXpET0+POV6tWjXi5eVFrl27lmcvHq0eaGzYsIF07NiRtGjRgvTo0YMcO3YsT+pRFfR1eXl5EZFIRHr37v3H6s7IyCBjx44lx48fV2hPfoFf/okTJ8jEiRNJnz59yIQJE8jNmzfztX52G+j9iRMnSJ06dYilpSWpX78+Wb9+fb63gca2bdtIrVq1iIODA6lfvz6pU6cOsbe3J3369PljbcgNateuTUQiEZk5cyYhRPF9KmrIcz+36Oho/Pvvv3j48CHKly+POnXq4NWrV3j37h0AoEyZMmjfvj26d++OBg0aoFy5cnlWN+0/lJWVBXV1aTSnzMxMAIC6unq++3vx/ZeIzJfK398fbm5uePbsWb6uVUnXT+/T09OZlc3/hK8bXa9EImH6uP60T5ey+lJTU/H69WvY2NhAU1Mz3+8F+/qTk5Nx6NAhfPnyBR8+fEDnzp3RqFGjAp+9wUd8fDyaNm2Kp0+fokGDBrh8+TJ0dHQKulm/h7xkyvj4eNK7d2+Ojxv9WU1NjbRu3Zps376dfP78mZPvV38haH+55ORkTll0efmtVH5WPxuxsbF/rH56ZDC/kZvrL6g28J+BP2me/6nvIa8watQoIhKJSNmyZUlQUFC26YqKosvzYJX0EmKamprIyMgAANjb2+Pff/9Fu3btULduXbx58wb+/v6wt7dH7dq1f6mew4cP49SpU3j58iXs7e1Ro0YNuLm5ccInP3jwAEFBQejbty8ziplXUKX++/fv48mTJ+jTp0+eKtTc1P/06VP07t07z0Npq1p/UFAQ+vTpky+hvFV9BgIDA9GvXz/o6en90dkadF3p6elFYl2EFi1aYMuWLfj69SuuXbsGOzs7jgqlQSvjZ8+e/fL7+0eQ12w5Y8YMRq1VqlSJTJo0ifj7+5OMjAxGVUydOpWUKFGCtGzZkulozc2vwdWrV0mZMmWIu7s78fHxIf/++y9xcXEhDg4OZPPmzcyv87hx44ixsTF5+PBhnl6jUH/B1l9Y2vDt2zcSExOTY5rMzEwyatQosm7dujyvPy+QlJRE4uPjCSGEfP/+nVSoUIGIRCLSuXNnhbTR0dHk2LFjZO7cucTOzo7Y2tr+4dbmDnlObhRFEQsLC9K2bVty/PhxEhAQQLZv3066dOlCateuTXbu3EkOHjxI6tWrR0QiEZk8eTIhJHfk1qhRIzJ16lTm78TEROLn50eGDx9OnJycyLx58wghhHz9+lVhWbO8gFB/wdZfWNrQtGlT8u+//5I7d+5k6xgdEhJCmjZtqhAdt7DA3d2ddOrUiVy4cIFkZWWRoUOHErFYTCwsLMi1a9fI//73P+Lh4UEaNmxI1NXVmS4mkUjEGawqjMiX0dLQ0FDy5s0bkpmZScaOHcvcFJFIRAwNDYmbmxs5c+YMc+zp06eEENX6KBITE0n79u3J3LlzFc59+PCBTJ06lRgaGpILFy7k+XUJ9Rd8/YWlDQcOHCAlS5YkVatWJbq6umTKlCkkJCRE6bzpb9++5ak/Z17hv//+IyKRiGhra5PatWsTb29v0q9fP+a9LFWqFPOZTWoikYi4uroWdPN/inxdQ2HNmjUcE1VXV5chun379hEPDw9mzmlu4OPjQ2rXrk2Cg4OVdhC3bt2aDB8+PK8uQ6i/kNVfGNowYsQIMnHiREKI1OWjdOnSpEqVKmTTpk0kOjqaECL9sZ4yZQp58OBBvrXjd+Dt7c0Z9DMwMCA1atQgampqjC8qPVe8dOnSpEaNGqRPnz7k8OHD+TZHOy+Rb+RGURRxd3dnRk1XrFhBLl26RNzc3IhIJCIWFhZk7dq1RFdXl1hbW5Pw8HCVy37+/DmxtbUlrq6u5P79+wom7fz588k///yTb9FHhPoLtv6CbkNmZiY5c+YMx9yVSCRk7NixTMCICxcukE2bNhENDQ2mT6uwISEhgezZs4e0atWK6OrqKii00qVLk169epFLly6Rjx8/5tt83PxCvio3b29vxmH31atXhBCph3bVqlWJSCQi5ubmzD63k7kjIiJIgwYNiI6ODhk/fjwJCAggYWFh5MWLF8Ta2prpc8kvCPUXbP0F3QaKopgZOexIN+Hh4aR58+aM8lFmOhc2fPjwgcybN484ODgwBCcWi0n16tXJiBEj8mUw5k8gX8nt69evxMTEhIhEIjJmzBhy4cIFEhQURFq0aMGx5QcMGPDLvjM7d+4kZmZmpEKFCqRq1aqkUqVKpFOnTnl8JUL9hbX+wtIGiUTCUYk9e/Yk1atX/6Nt+B1QFEWePHlC3N3dGdEhEomIjo4OqVu3LpkxYwZnnnBRQL6voXDt2jUcO3YM/v7++PTpE0qVKoXY2FgAgLa2Nho1aoQlS5agbt26v1XPuXPnQAiBsbExrKys8i3Sr1B/4ay/sLSBoiiEhobCxsYGR44cQY8ePf54G34HGRkZuHz5Mvbs2YPr16/j69evAKQ+ewMGDMCePXsKtoG5wB9ZtzQ4OBjt2rXD58+fkZmZCU1NTZQrVw6tW7fG+PHjUatWrfxuggABfwzPnj3DgQMHsGzZsoJuyi8jPj4ehw8fxtGjRxEQEIC0tDScP38ebdq0KeimqYw/Qm4nT55Et27doKamBj09PTg7O8Pd3R2dO3fO76oFCCgQFJd1Et6/f4+VK1eCEIL169cXdHNyhT+24nznzp0RHByMSZMmwc3NjZk4XFweAgECiisoikJmZmaRmELGxh8jt8+fPyM5ORlVq1YFIJCaAAEC8hd/jNxoEGGBWgECBPwB/HHpJBCbAAEC/gQEu1CAAAHFEgK5CRAgoFhCIDcBAgQUSwjkJkCAgGIJgdwECBBQLCGQmwABAoolBHITIEBAsYRAbgIECCiWEMhNgAABxRICuQkQIKBYQiA3AQIEFEsI5CZAgIBiif8D2N2uxUz/I+QAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig = plt.figure(figsize=(3, 3), layout=\"constrained\")\n", + "ax = plt.subplot(projection=\"ternary\")\n", + "\n", + "cs = ax.tripcolor(*comp_grid, lat_params, cmap=\"Purples\", shading=\"gouraud\")\n", + "cax = ax.inset_axes([1.1, 0.3, 0.075, 0.8], transform=ax.transAxes)\n", + "cbar = fig.colorbar(cs, cax=cax)\n", + "cbar.set_label(\"Lattice const. [Å]\", rotation=270, va=\"baseline\")\n", + "\n", + "cs = ax.tricontour(*comp_grid, lat_params, colors=\"k\", linewidths=0.5, levels=6)\n", + "clabels = ax.clabel(cs)\n", + "for txt in clabels:\n", + " txt.set_fontsize(8)\n", + " txt.set_path_effects([pe.Stroke(linewidth=1.5, foreground=\"white\"), pe.Normal()])\n", + "ax.set_tlabel(\"Li\", weight=\"bold\", fontsize=12)\n", + "ax.set_llabel(\"Na\", weight=\"bold\", fontsize=12)\n", + "ax.set_rlabel(\"K\", weight=\"bold\", fontsize=12)\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Stress gradient visualization" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "# Load alchemical MACE model\n", + "with suppress_print(out=True, err=True):\n", + " mace = alchemical_mace_mp(model=model, device=device, default_dtype=\"float32\")\n", + "for param in mace.parameters():\n", + " param.requires_grad = False\n", + "\n", + "# Set AlchemyManager\n", + "z_table, r_max = get_z_table_and_r_max(mace)\n", + "alchemical_weights = torch.ones(3, dtype=torch.float32) / 3\n", + "alchemy_manager = AlchemyManager(\n", + " atoms=atoms,\n", + " alchemical_pairs=alchemical_pairs,\n", + " alchemical_weights=alchemical_weights,\n", + " z_table=z_table,\n", + " r_max=r_max,\n", + ").to(device)\n", + "\n", + "# Common inputs\n", + "tensor_kwargs = {\"dtype\": torch.float32, \"device\": device}\n", + "positions = torch.tensor(atoms.get_positions(), **tensor_kwargs)\n", + "cell = torch.tensor(atoms.get_cell().array, **tensor_kwargs)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 105/105 [00:07<00:00, 14.57it/s]\n", + "100%|██████████| 55/55 [00:09<00:00, 5.53it/s]\n" + ] + } + ], + "source": [ + "# Calculate hydrostatic stresses for a triangular grid of compositions\n", + "comp_grid = np.array(get_triangular_grid(14))\n", + "stress_list = []\n", + "for comp in tqdm(comp_grid.T):\n", + " # Set alchemical weights\n", + " alchemical_weights = torch.tensor(comp, dtype=torch.float32)\n", + " alchemy_manager.alchemical_weights.data = alchemical_weights\n", + " batch = alchemy_manager(positions, cell).to(device)\n", + "\n", + " # Get hydrostatic stress\n", + " out = mace(batch, retain_graph=True, create_graph=True, compute_stress=True)\n", + " stress = torch.abs(torch.trace(out[\"stress\"][0])) / 3\n", + " stress_list.append(stress.item())\n", + "\n", + "# Calculate gradients for a small triangular grid of compositions\n", + "comp_grid_small = np.array(get_triangular_grid(10))\n", + "grad_list = []\n", + "for comp in tqdm(comp_grid_small.T):\n", + " # Set alchemical weights\n", + " alchemical_weights = torch.tensor(comp, dtype=torch.float32)\n", + " alchemy_manager.alchemical_weights.data = alchemical_weights\n", + " batch = alchemy_manager(positions, cell).to(device)\n", + "\n", + " # Get hydrostatic stress\n", + " out = mace(batch, retain_graph=True, create_graph=True, compute_stress=True)\n", + " stress = torch.abs(torch.trace(out[\"stress\"][0])) / 3 # hydrostatic stress\n", + " stress.backward()\n", + " grad = alchemy_manager.alchemical_weights.grad\n", + " grad_list.append(grad.clone().cpu().numpy())\n", + " grad.data.zero_()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "stress_list = np.array(stress_list)\n", + "grad_list = np.array(grad_list)\n", + "\n", + "fig = plt.figure(figsize=(3, 3), layout=\"constrained\")\n", + "ax = plt.subplot(projection=\"ternary\")\n", + "cs = ax.tripcolor(*comp_grid, stress_list * 1e3, cmap=\"Blues\", shading=\"gouraud\")\n", + "cax = ax.inset_axes([1.1, 0.3, 0.075, 0.8], transform=ax.transAxes)\n", + "cbar = fig.colorbar(cs, cax=cax)\n", + "cbar.set_label(\"Stress [meV/Å$^3$]\", rotation=270, va=\"baseline\")\n", + "ax.quiver(\n", + " *comp_grid_small,\n", + " -grad_list[:, 0],\n", + " -grad_list[:, 1],\n", + " -grad_list[:, 2],\n", + " color=\"k\",\n", + " scale=1.2,\n", + " pivot=\"mid\",\n", + " linewidth=0.3,\n", + " width=0.01,\n", + " headlength=4,\n", + " headaxislength=3.5,\n", + ")\n", + "ax.set_tlabel(\"Li\", weight=\"bold\", fontsize=12)\n", + "ax.set_llabel(\"Na\", weight=\"bold\", fontsize=12)\n", + "ax.set_rlabel(\"K\", weight=\"bold\", fontsize=12)\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Al(Sc,Y)N/GaN optimization" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# Load structure\n", + "atoms = ase.io.read(\"../data/structures/GaN_hex.cif\")\n", + "alch_elements = [\"Al\", \"Sc\"]\n", + "Ga_idx = [i for i, atom in enumerate(atoms) if atom.symbol == \"Ga\"]\n", + "alch_numbers = [ase.Atoms(el).numbers[0] for el in alch_elements]\n", + "alchemical_pairs = [\n", + " [AlchemicalPair(atom_index=idx, atomic_number=z) for idx in Ga_idx]\n", + " for z in alch_numbers\n", + "]\n", + "\n", + "# Set AlchemyManager\n", + "z_table, r_max = get_z_table_and_r_max(mace)\n", + "alchemical_weights = torch.tensor([0.999, 0.001], dtype=torch.float32)\n", + "alchemy_manager = AlchemyManager(\n", + " atoms=atoms,\n", + " alchemical_pairs=alchemical_pairs,\n", + " alchemical_weights=alchemical_weights,\n", + " z_table=z_table,\n", + " r_max=r_max,\n", + ").to(device)\n", + "\n", + "# Common inputs\n", + "tensor_kwargs = {\"dtype\": torch.float32, \"device\": device}\n", + "cell = torch.tensor(atoms.get_cell().array, **tensor_kwargs, requires_grad=True)\n", + "frac_coords = torch.tensor(\n", + " atoms.get_scaled_positions(), **tensor_kwargs, requires_grad=True\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "lr_weights = 5e-3\n", + "lr_cell = 1e-2\n", + "max_steps = 500\n", + "early_stop_loss = 5e-4\n", + "\n", + "for i in range(max_steps):\n", + " # Prepare data\n", + " positions = frac_coords @ cell\n", + " batch = alchemy_manager(positions, cell).to(device)\n", + "\n", + " # Get stress loss\n", + " out = mace(batch, retain_graph=True, create_graph=True, compute_stress=True)\n", + " stress = out[\"stress\"][0]\n", + " loss = torch.abs(stress[0, 0] + stress[1, 1])\n", + " loss.backward()\n", + "\n", + " # Gradient update\n", + " weights = alchemy_manager.alchemical_weights\n", + " weights.grad -= weights.grad.mean()\n", + " weights.data -= lr_weights * weights.grad\n", + " weights.grad.zero_()\n", + "\n", + " c_update = cell.grad[2, 2].detach().clone()\n", + " cell.grad.zero_()\n", + " cell.grad[2, 2] = c_update # only update the c component\n", + " cell.data -= lr_cell * cell.grad\n", + " cell.grad.zero_()\n", + "\n", + " if loss < early_stop_loss:\n", + " break" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.8017931 0.19820715]\n" + ] + } + ], + "source": [ + "print(weights.detach().cpu().numpy())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "chgnet", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/3_vacancy_analysis.ipynb b/notebooks/3_vacancy_analysis.ipynb new file mode 100644 index 0000000..d41610f --- /dev/null +++ b/notebooks/3_vacancy_analysis.ipynb @@ -0,0 +1,165 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "from ase import units\n", + "from scipy.integrate import trapezoid" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def integrate_switching(\n", + " df_log: pd.DataFrame,\n", + " equil_time: int = 20000,\n", + " switch_time: int = 30000,\n", + " return_E_diss: bool = False,\n", + "):\n", + " fwd_start, fwd_end = equil_time, equil_time + switch_time\n", + " rev_start, rev_end = 2 * equil_time + switch_time, 2 * equil_time + 2 * switch_time\n", + " grad, lamda = df_log[\"lambda_grad\"], df_log[\"lambda\"]\n", + " W_fwd = trapezoid(grad[fwd_start:fwd_end], lamda[fwd_start:fwd_end])\n", + " W_rev = trapezoid(grad[rev_start:rev_end], lamda[rev_start:rev_end])\n", + " if return_E_diss:\n", + " return (W_fwd - W_rev) / 2, (W_fwd + W_rev) / 2\n", + " return (W_fwd - W_rev) / 2 # free energy difference\n", + "\n", + "\n", + "def analyze_frenkel_ladd(\n", + " base_path: Path,\n", + " temp: float,\n", + " equil_time: int = 20000,\n", + " switch_time: int = 30000,\n", + " verbose: bool = False,\n", + "):\n", + " T = temp\n", + " k = np.load(base_path / \"spring_constants.npy\")\n", + "\n", + " mass = np.load(base_path / \"masses.npy\")\n", + " omega = np.sqrt(k / mass)\n", + " n_atoms = len(mass)\n", + "\n", + " # 1. Perfect crystal\n", + " df_log = pd.read_csv(base_path / \"observables.csv\")\n", + " volume = df_log[\"volume\"].values[0]\n", + " if verbose:\n", + " _, E_diss_perfect = integrate_switching(\n", + " df_log, equil_time, switch_time, return_E_diss=True\n", + " )\n", + " delta_F = integrate_switching(df_log, equil_time, switch_time)\n", + " F_E = 3 * units.kB * T * np.mean(np.log(units._hbar * omega / (units.kB * T)))\n", + " PV = volume * 1.01325 * units.bar\n", + " G_perfect = delta_F + F_E + PV\n", + "\n", + " # 2. Defective crystal\n", + " df_log = pd.read_csv(base_path / \"observables_defect.csv\")\n", + " volume = df_log[\"volume\"].values[0]\n", + " if verbose:\n", + " _, E_diss_defect = integrate_switching(\n", + " df_log, equil_time, switch_time, return_E_diss=True\n", + " )\n", + " delta_F = integrate_switching(df_log, equil_time, switch_time)\n", + " F_E = 3 * units.kB * T * np.mean(np.log(units._hbar * omega / (units.kB * T)))\n", + " PV = volume * 1.01325 * units.bar\n", + " G_defect = delta_F + F_E + PV\n", + " G_v = G_defect * (n_atoms - 1) - G_perfect * (n_atoms - 1)\n", + "\n", + " # 3. Partial FL\n", + " df_log = pd.read_csv(base_path / \"observables_FL.csv\")\n", + " if verbose:\n", + " _, E_diss_FL = integrate_switching(\n", + " df_log, equil_time, switch_time, return_E_diss=True\n", + " )\n", + " delta_G = integrate_switching(df_log, equil_time, switch_time)\n", + " # delta_G * N = (G_defect * N-1 + F_E) - G_perfect * N\n", + " G_defect_alt = (delta_G * n_atoms - F_E + G_perfect * n_atoms) / (n_atoms - 1)\n", + " G_v_alt = G_defect_alt * (n_atoms - 1) - G_perfect * (n_atoms - 1)\n", + "\n", + " if verbose:\n", + " return G_perfect, G_defect, delta_G, E_diss_perfect, E_diss_defect, E_diss_FL\n", + " return G_v, G_v_alt" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "G_v (50 K) = 1.5513 ± 0.0063 eV\n", + "G_v FL (50 K) = 1.5582 ± 0.0014 eV\n", + "G_v (100 K) = 1.5475 ± 0.0060 eV\n", + "G_v FL (100 K) = 1.5405 ± 0.0018 eV\n", + "G_v (150 K) = 1.5232 ± 0.0276 eV\n", + "G_v FL (150 K) = 1.5188 ± 0.0029 eV\n", + "G_v (200 K) = 1.5110 ± 0.0283 eV\n", + "G_v FL (200 K) = 1.5003 ± 0.0072 eV\n" + ] + } + ], + "source": [ + "result_path = Path(\"../data/results/vacancy\")\n", + "temp_range = [50, 100, 150, 200]\n", + "\n", + "G_v_all, G_v_std_all = [], []\n", + "G_v_alt_all, G_v_alt_std_all = [], []\n", + "for temp in temp_range:\n", + " G_v_list = []\n", + " G_v_alt_list = []\n", + " E_diss_perfect_list = []\n", + " E_diss_defect_list = []\n", + " E_diss_FL_list = []\n", + " for i in range(4):\n", + " base_path = result_path / f\"Fe_5x5x5_{temp}K/{i}\"\n", + " G_v, G_v_alt = analyze_frenkel_ladd(base_path, temp=temp, verbose=False)\n", + " G_v_list.append(G_v)\n", + " G_v_alt_list.append(G_v_alt)\n", + " G_v = np.mean(G_v_list)\n", + " G_v_std = np.std(G_v_list)\n", + " G_v_alt = np.mean(G_v_alt_list)\n", + " G_v_alt_std = np.std(G_v_alt_list)\n", + " print(f\"G_v ({temp} K) = {G_v:.4f} ± {G_v_std:.4f} eV\")\n", + " print(f\"G_v FL ({temp} K) = {G_v_alt:.4f} ± {G_v_alt_std:.4f} eV\")\n", + " G_v_all.append(G_v)\n", + " G_v_std_all.append(G_v_std)\n", + " G_v_alt_all.append(G_v_alt)\n", + " G_v_alt_std_all.append(G_v_alt_std)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "chgnet", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/4_perovskite_analysis.ipynb b/notebooks/4_perovskite_analysis.ipynb new file mode 100644 index 0000000..700e82c --- /dev/null +++ b/notebooks/4_perovskite_analysis.ipynb @@ -0,0 +1,269 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "from ase import units\n", + "from scipy.integrate import trapezoid" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Frenkel–Ladd path" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def integrate_switching(\n", + " df_log: pd.DataFrame,\n", + " equil_time: int = 20000,\n", + " switch_time: int = 30000,\n", + " return_E_diss: bool = False,\n", + "):\n", + " fwd_start, fwd_end = equil_time, equil_time + switch_time\n", + " rev_start, rev_end = 2 * equil_time + switch_time, 2 * equil_time + 2 * switch_time\n", + " grad, lamda = df_log[\"lambda_grad\"], df_log[\"lambda\"]\n", + " W_fwd = trapezoid(grad[fwd_start:fwd_end], lamda[fwd_start:fwd_end])\n", + " W_rev = trapezoid(grad[rev_start:rev_end], lamda[rev_start:rev_end])\n", + " if return_E_diss:\n", + " return (W_fwd - W_rev) / 2, (W_fwd + W_rev) / 2\n", + " return (W_fwd - W_rev) / 2 # free energy difference\n", + "\n", + "\n", + "def analyze_frenkel_ladd(\n", + " base_path: Path,\n", + " temp: float,\n", + " equil_time: int = 20000,\n", + " switch_time: int = 30000,\n", + "):\n", + " T = temp\n", + " df_log = pd.read_csv(base_path / \"observables.csv\")\n", + " k = np.load(base_path / \"spring_constants.npy\")\n", + " mass = np.load(base_path / \"masses.npy\")\n", + " omega = np.sqrt(k / mass)\n", + " volume = df_log[\"volume\"].values[0]\n", + "\n", + " delta_F = integrate_switching(df_log, equil_time, switch_time)\n", + " F_E = 3 * units.kB * T * np.mean(np.log(units._hbar * omega / (units.kB * T)))\n", + " PV = volume * 1.01325 * units.bar\n", + " delta_G = delta_F + F_E + PV\n", + "\n", + " return delta_G\n", + "\n", + "\n", + "def analyze_alchemical_switching(\n", + " base_path: Path,\n", + " temp: float,\n", + " equil_time: int = 20000,\n", + " switch_time: int = 30000,\n", + "):\n", + " T = temp\n", + " df_log = pd.read_csv(base_path / \"observables.csv\")\n", + " mass_init = np.load(base_path / \"masses_init.npy\")\n", + " mass_final = np.load(base_path / \"masses_final.npy\")\n", + "\n", + " work = integrate_switching(df_log, equil_time, switch_time)\n", + " G_mass = 1.5 * units.kB * T * np.mean(np.log(mass_init / mass_final))\n", + " delta_G = work + G_mass\n", + "\n", + " return delta_G" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CsPbI3 Alpha G (300 K) = -8.8514 ± 0.0003 eV/atom\n", + "CsPbI3 Alpha G (350 K) = -9.8642 ± 0.0004 eV/atom\n", + "CsPbI3 Alpha G (400 K) = -10.8783 ± 0.0006 eV/atom\n", + "CsPbI3 Alpha G (450 K) = -11.8943 ± 0.0003 eV/atom\n", + "CsPbI3 Alpha G (500 K) = -12.9119 ± 0.0004 eV/atom\n", + "CsPbI3 Delta G (300 K) = -8.8560 ± 0.0001 eV/atom\n", + "CsPbI3 Delta G (350 K) = -9.8660 ± 0.0001 eV/atom\n", + "CsPbI3 Delta G (400 K) = -10.8782 ± 0.0002 eV/atom\n", + "CsPbI3 Delta G (450 K) = -11.8921 ± 0.0002 eV/atom\n", + "CsPbI3 Delta G (500 K) = -12.9072 ± 0.0002 eV/atom\n" + ] + } + ], + "source": [ + "result_path = Path(\"../data/results/perovskite/frenkel_ladd\")\n", + "temp_range = [300, 350, 400, 450, 500]\n", + "\n", + "G_alpha = []\n", + "G_alpha_std = []\n", + "for temp in temp_range:\n", + " G_list = []\n", + " for i in range(4):\n", + " base_path = result_path / f\"CsPbI3_alpha_6x6x6_{temp}K/{i}\"\n", + " G_list.append(analyze_frenkel_ladd(base_path, temp=temp))\n", + " G = np.mean(G_list)\n", + " G_std = np.std(G_list)\n", + " print(f\"CsPbI3 Alpha G ({temp} K) = {G:.4f} ± {G_std:.4f} eV/atom\")\n", + " G_alpha.append(G)\n", + " G_alpha_std.append(G_std)\n", + "\n", + "G_delta = []\n", + "G_delta_std = []\n", + "for temp in temp_range:\n", + " G_list = []\n", + " for i in range(4):\n", + " base_path = result_path / f\"CsPbI3_delta_6x3x3_{temp}K/{i}\"\n", + " G_list.append(analyze_frenkel_ladd(base_path, temp=temp))\n", + " G = np.mean(G_list)\n", + " G_std = np.std(G_list)\n", + " print(f\"CsPbI3 Delta G ({temp} K) = {G:.4f} ± {G_std:.4f} eV/atom\")\n", + " G_delta.append(G)\n", + " G_delta_std.append(G_std)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CsSnI3 Alpha G (300 K) = -8.8297 ± 0.0002 eV/atom\n", + "CsSnI3 Alpha G (350 K) = -9.8413 ± 0.0002 eV/atom\n", + "CsSnI3 Alpha G (400 K) = -10.8544 ± 0.0002 eV/atom\n", + "CsSnI3 Alpha G (450 K) = -11.8695 ± 0.0003 eV/atom\n", + "CsSnI3 Alpha G (500 K) = -12.8863 ± 0.0003 eV/atom\n", + "CsSnI3 Delta G (300 K) = -8.8289 ± 0.0001 eV/atom\n", + "CsSnI3 Delta G (350 K) = -9.8381 ± 0.0000 eV/atom\n", + "CsSnI3 Delta G (400 K) = -10.8494 ± 0.0003 eV/atom\n", + "CsSnI3 Delta G (450 K) = -11.8627 ± 0.0003 eV/atom\n", + "CsSnI3 Delta G (500 K) = -12.8771 ± 0.0003 eV/atom\n" + ] + } + ], + "source": [ + "G_CsSnI3_alpha = []\n", + "G_CsSnI3_alpha_std = []\n", + "for temp in temp_range:\n", + " G_list = []\n", + " for i in range(4):\n", + " base_path = result_path / f\"CsSnI3_alpha_6x6x6_{temp}K/{i}\"\n", + " G_list.append(analyze_frenkel_ladd(base_path, temp=temp))\n", + " G = np.mean(G_list)\n", + " G_std = np.std(G_list)\n", + " print(f\"CsSnI3 Alpha G ({temp} K) = {G:.4f} ± {G_std:.4f} eV/atom\")\n", + " G_CsSnI3_alpha.append(G)\n", + " G_CsSnI3_alpha_std.append(G_std)\n", + "\n", + "G_CsSnI3_delta = []\n", + "G_CsSnI3_delta_std = []\n", + "for temp in temp_range:\n", + " G_list = []\n", + " for i in range(4):\n", + " base_path = result_path / f\"CsSnI3_delta_6x3x3_{temp}K/{i}\"\n", + " G_list.append(analyze_frenkel_ladd(base_path, temp=temp))\n", + " G = np.mean(G_list)\n", + " G_std = np.std(G_list)\n", + " print(f\"CsSnI3 Delta G ({temp} K) = {G:.4f} ± {G_std:.4f} eV/atom\")\n", + " G_CsSnI3_delta.append(G)\n", + " G_CsSnI3_delta_std.append(G_std)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Alchemical path" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Alpha ΔG (300 K) = 0.0233 ± 0.0001 eV/atom\n", + "Alpha ΔG (350 K) = 0.0236 ± 0.0001 eV/atom\n", + "Alpha ΔG (400 K) = 0.0241 ± 0.0001 eV/atom\n", + "Alpha ΔG (450 K) = 0.0249 ± 0.0000 eV/atom\n", + "Alpha ΔG (500 K) = 0.0258 ± 0.0000 eV/atom\n", + "Delta ΔG (300 K) = 0.0271 ± 0.0000 eV/atom\n", + "Delta ΔG (350 K) = 0.0279 ± 0.0000 eV/atom\n", + "Delta ΔG (400 K) = 0.0286 ± 0.0000 eV/atom\n", + "Delta ΔG (450 K) = 0.0294 ± 0.0000 eV/atom\n", + "Delta ΔG (500 K) = 0.0301 ± 0.0000 eV/atom\n" + ] + } + ], + "source": [ + "result_path = Path(\"../data/results/perovskite/alchemy\")\n", + "\n", + "G_alpha = []\n", + "G_alpha_std = []\n", + "for temp in temp_range:\n", + " G_list = []\n", + " for i in range(4):\n", + " base_path = result_path / f\"CsPbI3_CsSnI3_alpha_{temp}K/{i}\"\n", + " G_list.append(analyze_alchemical_switching(base_path, temp=temp))\n", + " G = np.mean(G_list)\n", + " G_std = np.std(G_list)\n", + " print(f\"Alpha ΔG ({temp} K) = {G:.4f} ± {G_std:.4f} eV/atom\")\n", + " G_alpha.append(G)\n", + " G_alpha_std.append(G_std)\n", + "\n", + "G_delta = []\n", + "G_delta_std = []\n", + "for temp in temp_range:\n", + " G_list = []\n", + " for i in range(4):\n", + " base_path = result_path / f\"CsPbI3_CsSnI3_delta_{temp}K/{i}\"\n", + " G_list.append(analyze_alchemical_switching(base_path, temp=temp))\n", + " G = np.mean(G_list)\n", + " G_std = np.std(G_list)\n", + " print(f\"Delta ΔG ({temp} K) = {G:.4f} ± {G_std:.4f} eV/atom\")\n", + " G_delta.append(G)\n", + " G_delta_std.append(G_std)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "chgnet", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9096539 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "alchemical_mace" +authors = [ + { name = "Juno Nam", email = "junonam@mit.edu" }, +] +description = "Alchemical MACE model" +readme = "README.md" +requires-python = ">=3.9" +version = "0.1.0" + +[tool.setuptools] +packages = ["alchemical_mace"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..19e8df1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +torch==2.0.1 +e3nn==0.4.4 +mace-torch==0.3.4 +ase==3.22.1 +pymatgen==2024.3.1 +numpy==1.25.2 +scipy==1.11.2 +pandas==2.2.2 +matplotlib==3.8.0 +mpltern==1.0.2 +tqdm==4.66.1 +ipykernel==6.25.2 \ No newline at end of file diff --git a/scripts/perovskite_alchemy.py b/scripts/perovskite_alchemy.py new file mode 100644 index 0000000..c58422c --- /dev/null +++ b/scripts/perovskite_alchemy.py @@ -0,0 +1,192 @@ +import argparse +from pathlib import Path + +import ase +import numpy as np +import pandas as pd +from ase import units +from ase.build import make_supercell +from ase.constraints import ExpCellFilter +from ase.md.npt import NPT +from ase.md.nptberendsen import Inhomogeneous_NPTBerendsen +from ase.md.velocitydistribution import MaxwellBoltzmannDistribution, Stationary +from ase.optimize import FIRE +from mace.calculators import mace_mp +from tqdm import tqdm + +from alchemical_mace.calculator import AlchemicalMACECalculator +from alchemical_mace.model import AlchemicalPair +from alchemical_mace.utils import upper_triangular_cell + + +# Arguments +parser = argparse.ArgumentParser() + +# Structure +parser.add_argument("--structure-file", type=str) +parser.add_argument("--supercell", type=int, nargs=3, default=[6, 6, 6]) + +# Alchemy +parser.add_argument("--switch-pair", type=str, nargs=2, default=["Pb", "Sn"]) + +# Molecular dynamics: general +parser.add_argument("--temperature", type=float, default=300.0) +parser.add_argument("--pressure", type=float, default=1.0) +parser.add_argument("--timestep", type=float, default=2.0) +parser.add_argument("--ttime", type=float, default=25.0) +parser.add_argument("--ptime", type=int, default=75.0) + +# Molecular dynamics: timesteps +parser.add_argument("--npt-equil-stpes", type=int, default=10000) +parser.add_argument("--alchemy-equil-steps", type=int, default=20000) +parser.add_argument("--alchemy-switch-steps", type=int, default=30000) + +# Molecular dynamics: output control +parser.add_argument("--output-dir", type=Path, default=Path("results")) +parser.add_argument("--log-interval", type=int, default=1) + +# MACE model +parser.add_argument("--device", type=str, default="cuda") +parser.add_argument("--model", type=str, default="small") + +args = parser.parse_args() +args.output_dir.mkdir(exist_ok=True, parents=True) + +# Load structure +atoms = ase.io.read(args.structure_file) +atoms = make_supercell(atoms, np.diag(args.supercell)) + +# Load universal MACE calculator and relax the structure +mace_calc = mace_mp(model=args.model, device=args.device, default_dtype="float32") +atoms.calc = mace_calc +atoms = ExpCellFilter(atoms) +optimizer = FIRE(atoms) +optimizer.run(fmax=0.01, steps=500) +atoms = atoms.atoms # get the relaxed structure +initial_atoms = atoms.copy() # save the initial structure + + +################################################################################ +# Cell volume equilibration +################################################################################ + +atoms = initial_atoms.copy() +atoms.set_calculator(mace_calc) +bulk_modulus = 100.0 * units.GPa + +# NPT equilibration +dyn = Inhomogeneous_NPTBerendsen( + atoms, + timestep=args.timestep * units.fs, + temperature_K=args.temperature, + pressure_au=args.pressure * 1.01325 * units.bar, + taut=args.ttime * units.fs, + taup=args.ptime * units.fs, + compressibility_au=1.0 / bulk_modulus, +) +MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature) +Stationary(atoms) + +# NPT equilibration and volume relaxation +for step in tqdm(range(args.npt_equil_stpes), desc="NPT equil"): + dyn.run(steps=1) + + +################################################################################ +# Alchemical switching +################################################################################ + +# Define alchemical transformation +src_elem, dst_elem = args.switch_pair +src_Z, dst_Z = ase.data.atomic_numbers[src_elem], ase.data.atomic_numbers[dst_elem] +src_idx = np.where(atoms.get_atomic_numbers() == src_Z)[0] +alchemical_pairs = [ + [AlchemicalPair(atom_index=idx, atomic_number=Z) for idx in src_idx] + for Z in [src_Z, dst_Z] +] + +# Set up the alchemical MACE calculator +calc = AlchemicalMACECalculator( + atoms=atoms, + alchemical_pairs=alchemical_pairs, + alchemical_weights=[1.0, 0.0], + device=args.device, + model=args.model, +) +atoms.set_calculator(calc) +upper_triangular_cell(atoms) # for ASE NPT + +# NPT alchemical switching +ptime = args.ptime * units.fs +pfactor = bulk_modulus * ptime * ptime + +dyn = NPT( + atoms, + timestep=args.timestep * units.fs, + temperature_K=args.temperature, + externalstress=args.pressure * 1.01325 * units.bar, + ttime=args.ttime * units.fs, + pfactor=pfactor, +) + +# Define alchemical path +t = np.linspace(0.0, 1.0, args.alchemy_switch_steps) +lambda_steps = t ** 5 * (70 * t ** 4 - 315 * t ** 3 + 540 * t ** 2 - 420 * t + 126) +lambda_values = [ + np.zeros(args.alchemy_equil_steps), + lambda_steps, + np.ones(args.alchemy_equil_steps), + lambda_steps[::-1], +] +lambda_values = np.concatenate(lambda_values) + +calculate_gradients = [ + np.zeros(args.alchemy_equil_steps, dtype=bool), + np.ones(args.alchemy_switch_steps, dtype=bool), + np.zeros(args.alchemy_equil_steps, dtype=bool), + np.ones(args.alchemy_switch_steps, dtype=bool), +] +calculate_gradients = np.concatenate(calculate_gradients) + + +def get_observables(dynamics, time, lambda_value): + num_atoms = len(dynamics.atoms) + alchemical_grad = dynamics.atoms._calc.results["alchemical_grad"] + lambda_grad = (alchemical_grad[1] - alchemical_grad[0]) / num_atoms + return { + "time": time, + "potential": dynamics.atoms.get_potential_energy() / num_atoms, + "temperature": dynamics.atoms.get_temperature(), + "volume": dynamics.atoms.get_volume() / num_atoms, + "lambda": lambda_value, + "lambda_grad": lambda_grad, + } + + +# Simulation loop +total_steps = 2 * args.alchemy_equil_steps + 2 * args.alchemy_switch_steps + +observables = [] +for step in (tqdm(range(total_steps), desc="Alchemical switching")): + lambda_value = lambda_values[step] + grad_enabled = calculate_gradients[step] + + # Set alchemical weights and atomic masses + calc.set_alchemical_weights([1 - lambda_value, lambda_value]) + atoms.set_masses(calc.get_alchemical_atomic_masses()) + calc.calculate_alchemical_grad = grad_enabled + + dyn.run(steps=1) + if step % args.log_interval == 0: + time = (step + 1) * args.timestep + observables.append(get_observables(dyn, time, lambda_value)) + +# Save observables +df = pd.DataFrame(observables) +df.to_csv(args.output_dir / "observables.csv", index=False) + +# Save masses for post-processing +calc.set_alchemical_weights([1.0, 0.0]) +np.save(args.output_dir / "masses_init.npy", calc.get_alchemical_atomic_masses()) +calc.set_alchemical_weights([0.0, 1.0]) +np.save(args.output_dir / "masses_final.npy", calc.get_alchemical_atomic_masses()) diff --git a/scripts/perovskite_frenkel_ladd.py b/scripts/perovskite_frenkel_ladd.py new file mode 100644 index 0000000..9d33f3f --- /dev/null +++ b/scripts/perovskite_frenkel_ladd.py @@ -0,0 +1,217 @@ +import argparse +from pathlib import Path + +import ase +import numpy as np +import pandas as pd +from ase import units +from ase.build import make_supercell +from ase.constraints import ExpCellFilter +from ase.md.langevin import Langevin +from ase.md.nptberendsen import Inhomogeneous_NPTBerendsen +from ase.md.velocitydistribution import MaxwellBoltzmannDistribution, Stationary +from ase.optimize import FIRE +from mace.calculators import mace_mp +from pymatgen.io.ase import AseAtomsAdaptor +from pymatgen.symmetry.analyzer import SpacegroupAnalyzer +from tqdm import tqdm + +from alchemical_mace.calculator import FrenkelLaddCalculator, NVTMACECalculator + + +# Arguments +parser = argparse.ArgumentParser() + +# Structure +parser.add_argument("--structure-file", type=str) +parser.add_argument("--supercell", type=int, nargs=3, default=[6, 6, 6]) + +# Molecular dynamics: general +parser.add_argument("--temperature", type=float, default=300.0) +parser.add_argument("--pressure", type=float, default=1.0) +parser.add_argument("--timestep", type=float, default=2.0) +parser.add_argument("--ttime", type=float, default=25.0) +parser.add_argument("--ptime", type=int, default=75.0) + +# Molecular dynamics: timesteps +parser.add_argument("--npt-equil-stpes", type=int, default=10000) +parser.add_argument("--npt-prod-steps", type=int, default=20000) +parser.add_argument("--nvt-equil-steps", type=int, default=20000) +parser.add_argument("--nvt-prod-steps", type=int, default=30000) +parser.add_argument("--alchemy-equil-steps", type=int, default=20000) +parser.add_argument("--alchemy-switch-steps", type=int, default=30000) + +# Molecular dynamics: output control +parser.add_argument("--output-dir", type=Path, default=Path("results")) +parser.add_argument("--log-interval", type=int, default=1) + +# MACE model +parser.add_argument("--device", type=str, default="cuda") +parser.add_argument("--model", type=str, default="small") + +args = parser.parse_args() +args.output_dir.mkdir(exist_ok=True, parents=True) + +# Load structure +atoms = ase.io.read(args.structure_file) +atoms = make_supercell(atoms, np.diag(args.supercell)) + +# Load universal MACE calculator and relax the structure +mace_calc = mace_mp(model=args.model, device=args.device, default_dtype="float32") +atoms.calc = mace_calc +atoms = ExpCellFilter(atoms) +optimizer = FIRE(atoms) +optimizer.run(fmax=0.01, steps=500) +atoms = atoms.atoms # get the relaxed structure +initial_atoms = atoms.copy() # save the initial structure + + +################################################################################ +# Cell volume equilibration +################################################################################ + +atoms = initial_atoms.copy() +atoms.set_calculator(mace_calc) +bulk_modulus = 100.0 * units.GPa + +# Equilibration and volume calculation +dyn = Inhomogeneous_NPTBerendsen( + atoms, + timestep=args.timestep * units.fs, + temperature_K=args.temperature, + pressure_au=args.pressure * 1.01325 * units.bar, + taut=args.ttime * units.fs, + taup=args.ptime * units.fs, + compressibility_au=1.0 / bulk_modulus, +) +MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature) +Stationary(atoms) + +# NPT equilibration and volume relaxation +cellpar_traj = [] +for step in tqdm(range(args.npt_equil_stpes), desc="NPT equil"): + dyn.run(steps=1) +for step in tqdm(range(args.npt_prod_steps), desc="NPT prod"): + dyn.run(steps=1) + if step % args.log_interval == 0: + cellpar_traj.append(atoms.get_cell().cellpar()) +abc_new = np.mean(cellpar_traj, axis=0)[:3] + +# Scale the initial cell to match the average volume +atoms = initial_atoms +atoms.set_cell(np.diag(abc_new), scale_atoms=True) +atoms.set_calculator(mace_calc) + +# Relax the atomic positions +optimizer = FIRE(atoms) +optimizer.run(fmax=0.01, steps=500) +initial_atoms = atoms.copy() # save the initial structure + + +################################################################################ +# MSD calculation +################################################################################ + +initial_positions = atoms.get_positions() +# Using the reversible scaling MACE calculator with fixed scale of 1.0 +# since we can turn off the stress calculation +calc = NVTMACECalculator(device=args.device, model=args.model) +atoms.set_calculator(calc) + +# NVT MSD calculation +dyn = Langevin( + atoms, + timestep=args.timestep * units.fs, + temperature_K=args.temperature, + friction=1 / (args.ttime * units.fs), +) +MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature) +Stationary(atoms) + +temperatures = [] +for step in tqdm(range(args.nvt_equil_steps), desc="NVT equil"): + dyn.run(steps=1) +squared_disp = np.zeros(len(atoms)) +for step in tqdm(range(args.nvt_prod_steps), desc="NVT prod"): + dyn.run(steps=1) + squared_disp += np.sum((atoms.get_positions() - initial_positions) ** 2, axis=1) +mean_squared_disp = squared_disp / args.nvt_prod_steps + +# Calculate spring constants and average over symmetrically equivalent atoms +spring_constants = 3.0 * units.kB * args.temperature / mean_squared_disp +structure = AseAtomsAdaptor.get_structure(initial_atoms) +sga = SpacegroupAnalyzer(structure) +equivalent_indices = sga.get_symmetrized_structure().equivalent_indices +for indices in equivalent_indices: + spring_constants[indices] = np.mean(spring_constants[indices]) + +np.save(args.output_dir / "spring_constants.npy", spring_constants) +np.save(args.output_dir / "masses.npy", atoms.get_masses()) + + +################################################################################ +# Frenkel-Ladd calculation +################################################################################ + +atoms = initial_atoms.copy() +calc = FrenkelLaddCalculator( + spring_constants=spring_constants, + initial_positions=initial_positions, + device=args.device, + model=args.model, +) +atoms.set_calculator(calc) + +# NVT Frenkel-Ladd calculation +dyn = Langevin( + atoms, + timestep=args.timestep * units.fs, + temperature_K=args.temperature, + friction=1 / (args.ttime * units.fs), +) +MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature) +Stationary(atoms) + +# Define Frenkel-Ladd path +t = np.linspace(0.0, 1.0, args.alchemy_switch_steps) +lambda_steps = t**5 * (70 * t**4 - 315 * t**3 + 540 * t**2 - 420 * t + 126) +lambda_values = [ + np.zeros(args.alchemy_equil_steps), + lambda_steps, + np.ones(args.alchemy_equil_steps), + lambda_steps[::-1], +] +lambda_values = np.concatenate(lambda_values) + + +def get_observables(dynamics, time, lambda_value): + num_atoms = len(dynamics.atoms) + return { + "time": time, + "potential": dynamics.atoms.get_potential_energy() / num_atoms, + "temperature": dynamics.atoms.get_temperature(), + "volume": dynamics.atoms.get_volume() / num_atoms, + "lambda": lambda_value, + "lambda_grad": dynamics.atoms._calc.results["energy_diff"] / num_atoms, + } + + +# Simulation loop +calc.compute_mace = False +total_steps = 2 * args.alchemy_equil_steps + 2 * args.alchemy_switch_steps + +observables = [] +for step in tqdm(range(total_steps), desc="Frenkel-Ladd"): + if step == args.alchemy_equil_steps: # turn on MACE after spring equilibration + calc.compute_mace = True + lambda_value = lambda_values[step] + calc.set_weights(lambda_value) + + dyn.run(steps=1) + if step % args.log_interval == 0: + time = (step + 1) * args.timestep + observables.append(get_observables(dyn, time, lambda_value)) + +# Save observables +df = pd.DataFrame(observables) +df.to_csv(args.output_dir / "observables.csv", index=False) diff --git a/scripts/vacancy_frenkel_ladd.py b/scripts/vacancy_frenkel_ladd.py new file mode 100644 index 0000000..ca84e46 --- /dev/null +++ b/scripts/vacancy_frenkel_ladd.py @@ -0,0 +1,427 @@ +import argparse +from pathlib import Path + +import ase +import numpy as np +import pandas as pd +from ase import units +from ase.build import make_supercell +from ase.constraints import ExpCellFilter +from ase.md.langevin import Langevin +from ase.md.npt import NPT +from ase.md.nptberendsen import Inhomogeneous_NPTBerendsen +from ase.md.velocitydistribution import MaxwellBoltzmannDistribution, Stationary +from ase.optimize import FIRE +from mace.calculators import mace_mp +from pymatgen.io.ase import AseAtomsAdaptor +from pymatgen.symmetry.analyzer import SpacegroupAnalyzer +from tqdm import tqdm + +from alchemical_mace.calculator import ( + DefectFrenkelLaddCalculator, + FrenkelLaddCalculator, + NVTMACECalculator, +) +from alchemical_mace.utils import upper_triangular_cell + + +# Arguments +parser = argparse.ArgumentParser() + +# Structure +parser.add_argument("--structure-file", type=str) +parser.add_argument("--supercell", type=int, nargs=3, default=[5, 5, 5]) + +# Molecular dynamics: general +parser.add_argument("--temperature", type=float, default=300.0) +parser.add_argument("--pressure", type=float, default=1.0) +parser.add_argument("--timestep", type=float, default=2.0) +parser.add_argument("--ttime", type=float, default=25.0) +parser.add_argument("--ptime", type=int, default=75.0) + +# Molecular dynamics: timesteps +parser.add_argument("--npt-equil-stpes", type=int, default=10000) +parser.add_argument("--npt-prod-steps", type=int, default=20000) +parser.add_argument("--nvt-equil-steps", type=int, default=20000) +parser.add_argument("--nvt-prod-steps", type=int, default=30000) +parser.add_argument("--alchemy-equil-steps", type=int, default=20000) +parser.add_argument("--alchemy-switch-steps", type=int, default=30000) + +# Molecular dynamics: output control +parser.add_argument("--output-dir", type=Path, default=Path("results")) +parser.add_argument("--log-interval", type=int, default=1) + +# MACE model +parser.add_argument("--device", type=str, default="cuda") +parser.add_argument("--model", type=str, default="small") + +args = parser.parse_args() +args.output_dir.mkdir(exist_ok=True, parents=True) + + +################################################################################ +# Energy minimization: defect-free structure +################################################################################ + +# Load structure +atoms = ase.io.read(args.structure_file) +atoms = make_supercell(atoms, np.diag(args.supercell)) + +# Load universal MACE calculator and relax the structure +mace_calc = mace_mp(model=args.model, device=args.device, default_dtype="float32") +atoms.calc = mace_calc +atoms = ExpCellFilter(atoms) +optimizer = FIRE(atoms) +optimizer.run(fmax=0.01, steps=500) +atoms = atoms.atoms # get the relaxed structure +initial_atoms = atoms.copy() # save the initial structure + + +################################################################################ +# Cell volume equilibration: defect-free structure +################################################################################ + +atoms = initial_atoms.copy() +atoms.set_calculator(mace_calc) +bulk_modulus = 100.0 * units.GPa + +# Equilibration and volume calculation +dyn = Inhomogeneous_NPTBerendsen( + atoms, + timestep=args.timestep * units.fs, + temperature_K=args.temperature, + pressure_au=args.pressure * 1.01325 * units.bar, + taut=args.ttime * units.fs, + taup=args.ptime * units.fs, + compressibility_au=1.0 / bulk_modulus, +) +MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature) +Stationary(atoms) + +# NPT equilibration and volume relaxation +cellpar_traj = [] +for step in tqdm(range(args.npt_equil_stpes), desc="NPT equil"): + dyn.run(steps=1) +for step in tqdm(range(args.npt_prod_steps), desc="NPT prod"): + dyn.run(steps=1) + if step % args.log_interval == 0: + cellpar_traj.append(atoms.get_cell().cellpar()) +abc_new = np.mean(cellpar_traj, axis=0)[:3] + +# Scale the initial cell to match the average volume +atoms = initial_atoms +atoms.set_cell(np.diag(abc_new), scale_atoms=True) +atoms.set_calculator(mace_calc) + +# Relax the atomic positions +optimizer = FIRE(atoms) +optimizer.run(fmax=0.01, steps=500) +initial_atoms = atoms.copy() # save the initial structure + + +################################################################################ +# MSD calculation: defect-free structure +################################################################################ + +initial_positions = atoms.get_positions() +# Using the reversible scaling MACE calculator with fixed scale of 1.0 +# since we can turn off the stress calculation +calc = NVTMACECalculator(device=args.device, model=args.model) +atoms.set_calculator(calc) + +# NVT MSD calculation +dyn = Langevin( + atoms, + timestep=args.timestep * units.fs, + temperature_K=args.temperature, + friction=1 / (args.ttime * units.fs), +) +MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature) +Stationary(atoms) + +temperatures = [] +for step in tqdm(range(args.nvt_equil_steps), desc="NVT equil"): + dyn.run(steps=1) +squared_disp = np.zeros(len(atoms)) +for step in tqdm(range(args.nvt_prod_steps), desc="NVT prod"): + dyn.run(steps=1) + squared_disp += np.sum((atoms.get_positions() - initial_positions) ** 2, axis=1) +mean_squared_disp = squared_disp / args.nvt_prod_steps + +# Calculate spring constants and average over symmetrically equivalent atoms +spring_constants = 3.0 * units.kB * args.temperature / mean_squared_disp +structure = AseAtomsAdaptor.get_structure(initial_atoms) +sga = SpacegroupAnalyzer(structure) +equivalent_indices = sga.get_symmetrized_structure().equivalent_indices +for indices in equivalent_indices: + spring_constants[indices] = np.mean(spring_constants[indices]) + +np.save(args.output_dir / "spring_constants.npy", spring_constants) +np.save(args.output_dir / "masses.npy", atoms.get_masses()) + + +################################################################################ +# Frenkel-Ladd calculation: defect-free structure +################################################################################ + +atoms = initial_atoms.copy() +calc = FrenkelLaddCalculator( + spring_constants=spring_constants, + initial_positions=initial_positions, + device=args.device, + model=args.model, +) +atoms.set_calculator(calc) + +# NVT Frenkel-Ladd calculation +dyn = Langevin( + atoms, + timestep=args.timestep * units.fs, + temperature_K=args.temperature, + friction=1 / (args.ttime * units.fs), +) +MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature) +Stationary(atoms) + +# Define Frenkel-Ladd path +t = np.linspace(0.0, 1.0, args.alchemy_switch_steps) +lambda_steps = t**5 * (70 * t**4 - 315 * t**3 + 540 * t**2 - 420 * t + 126) +lambda_values = [ + np.zeros(args.alchemy_equil_steps), + lambda_steps, + np.ones(args.alchemy_equil_steps), + lambda_steps[::-1], +] +lambda_values = np.concatenate(lambda_values) + + +def get_observables(dynamics, time, lambda_value): + num_atoms = len(dynamics.atoms) + return { + "time": time, + "potential": dynamics.atoms.get_potential_energy() / num_atoms, + "temperature": dynamics.atoms.get_temperature(), + "volume": dynamics.atoms.get_volume() / num_atoms, + "lambda": lambda_value, + "lambda_grad": dynamics.atoms._calc.results["energy_diff"] / num_atoms, + } + + +# Simulation loop +calc.compute_mace = False +total_steps = 2 * args.alchemy_equil_steps + 2 * args.alchemy_switch_steps + +observables = [] +for step in tqdm(range(total_steps), desc="Frenkel-Ladd"): + if step == args.alchemy_equil_steps: # turn on MACE after spring equilibration + calc.compute_mace = True + lambda_value = lambda_values[step] + calc.set_weights(lambda_value) + + dyn.run(steps=1) + if step % args.log_interval == 0: + time = (step + 1) * args.timestep + observables.append(get_observables(dyn, time, lambda_value)) + +# Save observables +df = pd.DataFrame(observables) +df.to_csv(args.output_dir / "observables.csv", index=False) + + +################################################################################ +# Cell volume equilibration: structure with a defect +################################################################################ + +atoms = initial_atoms.copy() + +# Create a vacancy at the center of the supercell +vacancy_index = len(atoms) // 2 +atom_mask = np.ones(len(atoms), dtype=bool) +atom_mask[vacancy_index] = False +del atoms[vacancy_index] + +atoms.set_calculator(mace_calc) + +# Equilibration and volume calculation +dyn = Inhomogeneous_NPTBerendsen( + atoms, + timestep=args.timestep * units.fs, + temperature_K=args.temperature, + pressure_au=args.pressure * 1.01325 * units.bar, + taut=args.ttime * units.fs, + taup=args.ptime * units.fs, + compressibility_au=1.0 / bulk_modulus, +) +MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature) +Stationary(atoms) + +# NPT equilibration and volume relaxation +cellpar_traj = [] +for step in tqdm(range(args.npt_equil_stpes), desc="NPT equil"): + dyn.run(steps=1) +for step in tqdm(range(args.npt_prod_steps), desc="NPT prod"): + dyn.run(steps=1) + if step % args.log_interval == 0: + cellpar_traj.append(atoms.get_cell().cellpar()) +abc_new = np.mean(cellpar_traj, axis=0)[:3] + +# Scale the initial cell to match the average volume +atoms = initial_atoms.copy() +atoms.set_cell(np.diag(abc_new), scale_atoms=True) +del atoms[vacancy_index] +atoms.set_calculator(mace_calc) + +# Relax the atomic positions +optimizer = FIRE(atoms) +optimizer.run(fmax=0.01, steps=500) + + +################################################################################ +# Frenkel-Ladd calculation: structure with a defect +################################################################################ + +calc = FrenkelLaddCalculator( + spring_constants=spring_constants[atom_mask], + initial_positions=initial_positions[atom_mask], + device=args.device, + model=args.model, +) +atoms.set_calculator(calc) + +# NVT Frenkel-Ladd calculation +dyn = Langevin( + atoms, + timestep=args.timestep * units.fs, + temperature_K=args.temperature, + friction=1 / (args.ttime * units.fs), +) +MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature) +Stationary(atoms) + +# Simulation loop +calc.compute_mace = False +total_steps = 2 * args.alchemy_equil_steps + 2 * args.alchemy_switch_steps + +observables = [] +for step in tqdm(range(total_steps), desc="Frenkel-Ladd"): + if step == args.alchemy_equil_steps: # turn on MACE after spring equilibration + calc.compute_mace = True + lambda_value = lambda_values[step] + calc.set_weights(lambda_value) + + dyn.run(steps=1) + if step % args.log_interval == 0: + time = (step + 1) * args.timestep + observables.append(get_observables(dyn, time, lambda_value)) + +# Save observables +df = pd.DataFrame(observables) +df.to_csv(args.output_dir / "observables_defect.csv", index=False) + + +################################################################################ +# Cell volume equilibration: partial Frenkel-Ladd calculation +################################################################################ + +atoms = initial_atoms.copy() +atoms.set_calculator(mace_calc) + +# Equilibration and volume calculation +dyn = Inhomogeneous_NPTBerendsen( + atoms, + timestep=args.timestep * units.fs, + temperature_K=args.temperature, + pressure_au=args.pressure * 1.01325 * units.bar, + taut=args.ttime * units.fs, + taup=args.ptime * units.fs, + compressibility_au=1.0 / bulk_modulus, +) +MaxwellBoltzmannDistribution(atoms, temperature_K=args.temperature) +Stationary(atoms) + +# NPT equilibration and volume relaxation +for step in tqdm(range(args.npt_equil_stpes), desc="NPT equil"): + dyn.run(steps=1) + + +################################################################################ +# Alchemical switching +################################################################################ + +# Set up the partial Frenkel-Ladd calculation +calc = DefectFrenkelLaddCalculator( + atoms=atoms, + spring_constant=spring_constants[vacancy_index], + defect_index=vacancy_index, + device=args.device, + model=args.model, +) +atoms.set_calculator(calc) +upper_triangular_cell(atoms) # for ASE NPT + +# NPT alchemical switching +ptime = args.ptime * units.fs +pfactor = bulk_modulus * ptime * ptime + +dyn = NPT( + atoms, + timestep=args.timestep * units.fs, + temperature_K=args.temperature, + externalstress=args.pressure * 1.01325 * units.bar, + ttime=args.ttime * units.fs, + pfactor=pfactor, +) + +# Define alchemical path +t = np.linspace(0.0, 1.0, args.alchemy_switch_steps) +lambda_steps = t**5 * (70 * t**4 - 315 * t**3 + 540 * t**2 - 420 * t + 126) +lambda_values = [ + np.zeros(args.alchemy_equil_steps), + lambda_steps, + np.ones(args.alchemy_equil_steps), + lambda_steps[::-1], +] +lambda_values = np.concatenate(lambda_values) + +calculate_gradients = [ + np.zeros(args.alchemy_equil_steps, dtype=bool), + np.ones(args.alchemy_switch_steps, dtype=bool), + np.zeros(args.alchemy_equil_steps, dtype=bool), + np.ones(args.alchemy_switch_steps, dtype=bool), +] +calculate_gradients = np.concatenate(calculate_gradients) + + +def get_observables(dynamics, time, lambda_value): + num_atoms = len(dynamics.atoms) + alchemical_grad = dynamics.atoms._calc.results["alchemical_grad"] + return { + "time": time, + "potential": dynamics.atoms.get_potential_energy() / num_atoms, + "temperature": dynamics.atoms.get_temperature(), + "volume": dynamics.atoms.get_volume() / num_atoms, + "lambda": lambda_value, + "lambda_grad": alchemical_grad / num_atoms, + } + + +# Simulation loop +total_steps = 2 * args.alchemy_equil_steps + 2 * args.alchemy_switch_steps + +observables = [] +for step in tqdm(range(total_steps), desc="Alchemical switching"): + lambda_value = lambda_values[step] + grad_enabled = calculate_gradients[step] + + # Set alchemical weights and atomic masses + calc.set_alchemical_weight(lambda_value) + calc.calculate_alchemical_grad = grad_enabled + + dyn.run(steps=1) + if step % args.log_interval == 0: + time = (step + 1) * args.timestep + observables.append(get_observables(dyn, time, lambda_value)) + +# Save observables +df = pd.DataFrame(observables) +df.to_csv(args.output_dir / "observables_FL.csv", index=False)