-
Notifications
You must be signed in to change notification settings - Fork 31
Add InterfaceCalc Class for Interface Structure and Energy Calculation
#117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
8b7e845
Add InterfaceCalc
so2koo bbd2721
fix
so2koo c0da967
pre-commit auto-fixes
pre-commit-ci[bot] 41f1eab
Add InterfaceCalc into _init_.py
rul048 4a251fd
Fix errors and Add test
so2koo 75cd8fe
pre-commit auto-fixes
pre-commit-ci[bot] 4365373
Fix lint
so2koo 3072c51
Rename fixture description from 'Si' to 'SiO2'
rul048 4080ebf
Replace m3gnet_calculator with matpes_calculator in test_interface.py
rul048 781933b
Update expected value for interfacial_energy test
rul048 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,253 @@ | ||
| """Interface structure/energy calculations.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| import numpy as np | ||
| from pymatgen.analysis.interfaces.coherent_interfaces import CoherentInterfaceBuilder | ||
| from pymatgen.analysis.structure_matcher import StructureMatcher | ||
|
|
||
| from ._base import PropCalc | ||
| from ._relaxation import RelaxCalc | ||
|
|
||
| if TYPE_CHECKING: | ||
| from ase import Atoms | ||
| from ase.calculators.calculator import Calculator | ||
| from ase.optimize.optimize import Optimizer | ||
| from pymatgen.analysis.interfaces.zsl import ZSLGenerator | ||
| from pymatgen.core import Structure | ||
|
|
||
|
|
||
| class InterfaceCalc(PropCalc): | ||
| """ | ||
| This class generates all possible coherent interfaces between two bulk structures | ||
| given their miller indices, relaxes them, and computes their interfacial energies. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| calculator: Calculator | str, | ||
| *, | ||
| relax_bulk: bool = True, | ||
| relax_interface: bool = True, | ||
| fmax: float = 0.1, | ||
| optimizer: str | Optimizer = "BFGS", | ||
| max_steps: int = 500, | ||
| relax_calc_kwargs: dict | None = None, | ||
| ) -> None: | ||
| """Initialize the instance of the class. | ||
|
|
||
| Parameters: | ||
| calculator (Calculator | str): An ASE calculator object used to perform energy and force | ||
| calculations. If string is provided, the corresponding universal calculator is loaded. | ||
| relax_bulk (bool, optional): Whether to relax the bulk structures before interface | ||
| calculations. Defaults to True. | ||
| relax_interface (bool, optional): Whether to relax the interface structures. Defaults to True. | ||
| fmax (float, optional): The maximum force tolerance for convergence. Defaults to 0.1. | ||
| optimizer (str | Optimizer, optional): The optimization algorithm to use. Defaults to "BFGS". | ||
| max_steps (int, optional): The maximum number of optimization steps. Defaults to 500. | ||
| relax_calc_kwargs: Additional keyword arguments passed to the | ||
| class:`RelaxCalc` constructor for both bulk and interface. Default is None. | ||
|
|
||
| Returns: | ||
| None | ||
| """ | ||
| self.calculator = calculator | ||
| self.relax_bulk = relax_bulk | ||
| self.relax_interface = relax_interface | ||
| self.fmax = fmax | ||
| self.optimizer = optimizer | ||
| self.max_steps = max_steps | ||
| self.relax_calc_kwargs = relax_calc_kwargs | ||
|
|
||
| def calc_interfaces( | ||
| self, | ||
| film_bulk: Structure, | ||
| substrate_bulk: Structure, | ||
| film_miller: tuple[int, int, int], | ||
| substrate_miller: tuple[int, int, int], | ||
| zslgen: ZSLGenerator | None = None, | ||
| cib_kwargs: dict | None = None, | ||
| **kwargs: Any, | ||
| ) -> list[dict[str, Any]]: | ||
| """Calculate all possible coherent interfaces between two bulk structures. | ||
|
|
||
| Parameters: | ||
| film_bulk (Structure): The bulk structure of the film material. | ||
| substrate_bulk (Structure): The bulk structure of the substrate material. | ||
| film_miller (tuple[int, int, int]): The Miller index for the film surface. | ||
| substrate_miller (tuple[int, int, int]): The Miller index for the substrate surface. | ||
| zslgen (ZSLGenerator | None, optional): An instance of ZSLGenerator to use for generating | ||
| supercells. | ||
| zsl_kwargs (dict | None, optional): Additional keyword arguments to pass to the | ||
| ZSLGenerator. | ||
| cib_kwargs (dict | None, optional): Additional keyword arguments to pass to the | ||
| CoherentInterfaceBuilder. | ||
| **kwargs (Any): Additional keyword arguments passed to calc_many. | ||
|
|
||
| Returns: | ||
| dict: A list of dictionaries containing the calculated film, substrate, interface. | ||
| """ | ||
| cib = CoherentInterfaceBuilder( | ||
| film_structure=film_bulk, | ||
| substrate_structure=substrate_bulk, | ||
| film_miller=film_miller, | ||
| substrate_miller=substrate_miller, | ||
| zslgen=zslgen, | ||
| **(cib_kwargs or {}), | ||
| ) | ||
|
|
||
| terminations = cib.terminations | ||
| all_interfaces: list = [] | ||
| for t in terminations: | ||
| interfaces_for_termination = cib.get_interfaces(termination=t) | ||
| all_interfaces.extend(list(interfaces_for_termination)) | ||
|
|
||
| if not all_interfaces: | ||
| raise ValueError( | ||
| "No interfaces found with the given parameters. Adjust the ZSL parameters to find more matches." | ||
| ) | ||
|
|
||
| # Group similar / duplicate interfaces using StructureMatcher and keep one representative per group | ||
| matcher = StructureMatcher() | ||
| groups: list[list] = [] | ||
| for i in all_interfaces: | ||
| placed = False | ||
| for g in groups: | ||
| if matcher.fit(i, g[0]): | ||
| g.append(i) | ||
| placed = True | ||
| break | ||
| if not placed: | ||
| groups.append([i]) | ||
| unique_interfaces = [g[0] for g in groups] | ||
|
|
||
| film_bulk = film_bulk.to_conventional() | ||
| substrate_bulk = substrate_bulk.to_conventional() | ||
|
|
||
| relaxer_bulk = RelaxCalc( | ||
| calculator=self.calculator, | ||
| fmax=self.fmax, | ||
| max_steps=self.max_steps, | ||
| relax_cell=self.relax_bulk, | ||
| relax_atoms=self.relax_bulk, | ||
| optimizer=self.optimizer, | ||
| **(self.relax_calc_kwargs or {}), | ||
| ) | ||
| film_opt = relaxer_bulk.calc(film_bulk) | ||
| substrate_opt = relaxer_bulk.calc(substrate_bulk) | ||
|
|
||
| interfaces = [ | ||
| { | ||
| "interface": interface, | ||
| "num_atoms": len(interface), | ||
| "film_energy_per_atom": film_opt["energy"] / len(film_bulk), | ||
| "final_film": film_opt["final_structure"], | ||
| "substrate_energy_per_atom": substrate_opt["energy"] / len(substrate_bulk), | ||
| "final_substrate": substrate_opt["final_structure"], | ||
| } | ||
| for interface in unique_interfaces | ||
| ] | ||
|
|
||
| return [r for r in self.calc_many(interfaces, **kwargs) if r is not None] | ||
|
|
||
| def calc( | ||
| self, | ||
| structure: Structure | Atoms | dict[str, Any], | ||
| ) -> dict[str, Any]: | ||
| """Calculate the interfacial energy of the given interface structures and sort by the energy. | ||
|
|
||
| Parameters: | ||
| structure : A dictionary containing the film, substrate, interface structures | ||
|
|
||
| Returns: | ||
| dict: | ||
| - "interface" (Structure): The initial interface structure. | ||
| - "final_interface" (Structure): The relaxed interface structure. | ||
| - "interface_energy_per_atom" (float): The final energy of the relaxed interface structure. | ||
| - "num_atoms" (int): The number of atoms in the interface structure. | ||
| - "interfacial_energy" (float): The calculated interfacial energy | ||
| """ | ||
| if not isinstance(structure, dict): | ||
| msg = ( | ||
| "For interface calculations, structure must be a dict in one of the following formats: " | ||
| "{'interface': interface_struct, 'film_energy_per_atom': energy, ...} from calc_interfaces or " | ||
| "{'interface': interface_struct, 'film_bulk': film_struct, 'substrate_bulk': substrate_struct}." | ||
| ) | ||
| raise TypeError(msg) | ||
|
|
||
| # Type narrowing: at this point, structure is guaranteed to be dict[str, Any] | ||
| result_dict = structure.copy() | ||
|
|
||
| if "film_energy_per_atom" in structure and "substrate_energy_per_atom" in structure: | ||
| film_energy_per_atom = structure["film_energy_per_atom"] | ||
| substrate_energy_per_atom = structure["substrate_energy_per_atom"] | ||
| film_structure = structure["final_film"] | ||
| substrate_structure = structure["final_substrate"] | ||
| else: | ||
| relaxer = RelaxCalc( | ||
| calculator=self.calculator, | ||
| fmax=self.fmax, | ||
| max_steps=self.max_steps, | ||
| relax_cell=self.relax_bulk, | ||
| relax_atoms=self.relax_bulk, | ||
| optimizer=self.optimizer, | ||
| **(self.relax_calc_kwargs or {}), | ||
| ) | ||
| film_opt = relaxer.calc(structure["film_bulk"]) | ||
| film_energy_per_atom = film_opt["energy"] / len(film_opt["final_structure"]) | ||
| film_structure = film_opt["final_structure"] | ||
| substrate_opt = relaxer.calc(structure["substrate_bulk"]) | ||
| substrate_energy_per_atom = substrate_opt["energy"] / len(substrate_opt["final_structure"]) | ||
| substrate_structure = substrate_opt["final_structure"] | ||
|
|
||
| interface = structure["interface"] | ||
| relaxer = RelaxCalc( | ||
| calculator=self.calculator, | ||
| fmax=self.fmax, | ||
| max_steps=self.max_steps, | ||
| relax_cell=False, | ||
| relax_atoms=self.relax_interface, | ||
| optimizer=self.optimizer, | ||
| **(self.relax_calc_kwargs or {}), | ||
| ) | ||
| interface_opt = relaxer.calc(interface) | ||
| final_interface = interface_opt["final_structure"] | ||
| interface_energy = interface_opt["energy"] | ||
|
|
||
| # pymatgen interface object does not include interface properties for interfacial energy | ||
| # calculation, define them here | ||
|
|
||
| matrix = interface.lattice.matrix | ||
| area = float(np.linalg.norm(np.cross(matrix[0], matrix[1]))) | ||
|
|
||
| unique_in_film = set(film_structure.symbol_set) - set(substrate_structure.symbol_set) | ||
| unique_in_substrate = set(substrate_structure.symbol_set) - set(film_structure.symbol_set) | ||
|
|
||
| if unique_in_film: | ||
| unique_element = next(iter(unique_in_film)) | ||
| count = film_structure.composition[unique_element] | ||
| film_in_interface = (interface.composition[unique_element] / count) * film_structure.num_sites | ||
| substrate_in_interface = interface.num_sites - film_in_interface | ||
| elif unique_in_substrate: | ||
| unique_element = next(iter(unique_in_substrate)) | ||
| count = substrate_structure.composition[unique_element] | ||
| substrate_in_interface = (interface.composition[unique_element] / count) * substrate_structure.num_sites | ||
| film_in_interface = interface.num_sites - substrate_in_interface | ||
| else: | ||
| msg = "No unique elements found in either structure to determine atom counts in interface." | ||
| raise ValueError(msg) | ||
|
|
||
| gamma = ( | ||
| interface_energy | ||
| - (film_in_interface * film_energy_per_atom + substrate_in_interface * substrate_energy_per_atom) | ||
| ) / (2 * area) | ||
|
|
||
| return result_dict | { | ||
| "interface": interface, | ||
| "final_interface": final_interface, | ||
| "interface_energy_per_atom": interface_energy / len(interface), | ||
| "num_atoms": len(interface), | ||
| "interfacial_energy": gamma, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| """Tests for the InterfaceCalc class.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| import pytest | ||
|
|
||
| from matcalc import InterfaceCalc | ||
|
|
||
| if TYPE_CHECKING: | ||
| from matgl.ext.ase import PESCalculator | ||
| from pymatgen.core import Structure | ||
|
|
||
|
|
||
| def test_interface_calc_basic(Si: Structure, SiO2: Structure, m3gnet_calculator: PESCalculator) -> None: | ||
| """ | ||
| Test the basic workflow: | ||
| 1) calc_interfaces on SiO2 (film) and Si (substrate) | ||
| 2) calc on the resulting interfaces | ||
| 3) Check the final results | ||
| """ | ||
| interface_calc = InterfaceCalc( | ||
| calculator=m3gnet_calculator, | ||
| relax_bulk=True, | ||
| relax_interface=True, | ||
| fmax=0.1, | ||
| max_steps=100, | ||
| ) | ||
|
|
||
| results = list( | ||
| interface_calc.calc_interfaces( | ||
| film_bulk=SiO2, | ||
| substrate_bulk=Si, | ||
| film_miller=(1, 0, 0), | ||
| substrate_miller=(1, 1, 1), | ||
| ) | ||
| ) | ||
| interface_res = results[0] | ||
| assert "final_film" in interface_res | ||
| assert "final_substrate" in interface_res | ||
| assert "final_interface" in interface_res | ||
|
|
||
| assert interface_res["film_energy_per_atom"] == pytest.approx(-7.881573994954427, rel=1e-1) | ||
| assert interface_res["substrate_energy_per_atom"] == pytest.approx(-5.419038772583008, rel=1e-1) | ||
| assert interface_res["interfacial_energy"] == pytest.approx(0.14220127996544243, rel=1e-1) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will replace m3gnet_calculator with matpes_calculator for all tests.