@mat3ra/made 2024.5.15-0 → 2024.6.3-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/py/mat3ra/made/tools/analyze.py +14 -0
- package/src/py/mat3ra/made/tools/build/__init__.py +98 -77
- package/src/py/mat3ra/made/tools/build/interface/__init__.py +26 -0
- package/src/py/mat3ra/made/tools/build/interface/builders.py +185 -0
- package/src/py/mat3ra/made/tools/build/interface/configuration.py +18 -0
- package/src/py/mat3ra/made/tools/build/interface/enums.py +7 -0
- package/src/py/mat3ra/made/tools/build/interface/termination_pair.py +59 -0
- package/src/py/mat3ra/made/tools/build/interface/utils.py +36 -0
- package/src/py/mat3ra/made/tools/build/mixins.py +13 -0
- package/src/py/mat3ra/made/tools/build/slab/__init__.py +15 -0
- package/src/py/mat3ra/made/tools/build/slab/builders.py +75 -0
- package/src/py/mat3ra/made/tools/build/slab/configuration.py +56 -0
- package/src/py/mat3ra/made/tools/build/slab/termination.py +27 -0
- package/src/py/mat3ra/made/tools/build/supercell.py +25 -0
- package/src/py/mat3ra/made/tools/convert.py +9 -4
- package/src/py/mat3ra/made/tools/utils.py +17 -0
- package/tests/py/unit/fixtures.py +115 -10
- package/tests/py/unit/test_tools_build_interface.py +30 -17
- package/tests/py/unit/test_tools_build_slab.py +21 -0
- package/tests/py/unit/test_tools_build_supercell.py +15 -0
- package/src/py/mat3ra/made/tools/build/interface.py +0 -268
- package/tests/py/unit/test_tools_build.py +0 -20
package/package.json
CHANGED
|
@@ -55,3 +55,17 @@ def get_surface_area(atoms: Atoms):
|
|
|
55
55
|
matrix = atoms.cell
|
|
56
56
|
cross_product = np.cross(matrix[0], matrix[1])
|
|
57
57
|
return np.linalg.norm(cross_product)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@decorator_convert_material_args_kwargs_to_atoms
|
|
61
|
+
def get_chemical_formula(atoms: Atoms):
|
|
62
|
+
"""
|
|
63
|
+
Calculate the formula of the atoms structure.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
atoms (ase.Atoms): The Atoms object to calculate the formula of.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
str: The formula of the atoms.
|
|
70
|
+
"""
|
|
71
|
+
return atoms.get_chemical_formula()
|
|
@@ -1,80 +1,101 @@
|
|
|
1
|
+
from typing import List, Optional, Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
1
5
|
from ...material import Material
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
from ..convert import decorator_convert_material_args_kwargs_to_structure
|
|
6
|
-
from ..modify import translate_to_bottom, wrap_to_unit_cell
|
|
7
|
-
from typing import Optional
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@decorator_convert_material_args_kwargs_to_structure
|
|
11
|
-
def create_interfaces(
|
|
12
|
-
substrate: Optional[Material] = None,
|
|
13
|
-
layer: Optional[Material] = None,
|
|
14
|
-
settings: Settings = Settings(),
|
|
15
|
-
sort_by_strain_and_size: bool = True,
|
|
16
|
-
remove_duplicates: bool = True,
|
|
17
|
-
is_logging_enabled: bool = True,
|
|
18
|
-
interface_builder: Optional[CoherentInterfaceBuilder] = None,
|
|
19
|
-
termination: Optional[TerminationType] = None,
|
|
20
|
-
) -> InterfaceDataHolder:
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseBuilder(BaseModel):
|
|
21
9
|
"""
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
10
|
+
Base class for material builders.
|
|
11
|
+
This class provides an interface for generating materials and getter functions.
|
|
12
|
+
The builder is meant as a description of the process, while its functions require a
|
|
13
|
+
"Configuration" class instance to perform the generation.
|
|
14
|
+
|
|
15
|
+
The class is designed to be subclassed and the subclass should implement the following methods:
|
|
16
|
+
|
|
17
|
+
- `_generate`: Generate the material items, possibly using third-party tools/implementation for items.
|
|
18
|
+
- `_sort`: Sort the items.
|
|
19
|
+
- `_select`: Select a subset of the items.
|
|
20
|
+
- `_post_process`: Post-process the items to convert them to materials (Material class).
|
|
21
|
+
- `_finalize`: Finalize the materials.
|
|
22
|
+
|
|
23
|
+
The subclass should also define the following attributes:
|
|
24
|
+
|
|
25
|
+
- `_BuildParametersType`: The data structure model for the build parameters.
|
|
26
|
+
- `_DefaultBuildParameters`: The default build parameters.
|
|
27
|
+
- `_ConfigurationType`: The data structure model for the Configuration used during the build.
|
|
28
|
+
- `_GeneratedItemType`: The type of the generated item.
|
|
29
|
+
- `_SelectorParametersType`: The data structure model for the selector parameters.
|
|
30
|
+
- `_PostProcessParametersType`: The data structure model for the post-process parameters.
|
|
33
31
|
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@
|
|
73
|
-
def
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
) ->
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
32
|
+
|
|
33
|
+
build_parameters: Any = None
|
|
34
|
+
_BuildParametersType: Any = None
|
|
35
|
+
_DefaultBuildParameters: Any = None
|
|
36
|
+
|
|
37
|
+
_ConfigurationType: Any = Any
|
|
38
|
+
_GeneratedItemType: Any = Any
|
|
39
|
+
_SelectorParametersType: Any = None
|
|
40
|
+
_PostProcessParametersType: Any = None
|
|
41
|
+
|
|
42
|
+
def __init__(self, build_parameters: _BuildParametersType = None):
|
|
43
|
+
super().__init__(build_parameters=build_parameters)
|
|
44
|
+
self.build_parameters = build_parameters or self._DefaultBuildParameters
|
|
45
|
+
self.__generated_items: List[List[BaseBuilder._GeneratedItemType]] = []
|
|
46
|
+
self.__configurations: List[BaseBuilder._ConfigurationType] = []
|
|
47
|
+
|
|
48
|
+
def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]:
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
def _generate_or_get_from_cache(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]:
|
|
52
|
+
if configuration not in self.__configurations:
|
|
53
|
+
self.__configurations.append(configuration)
|
|
54
|
+
self.__generated_items.append(self._generate(configuration))
|
|
55
|
+
return self.__generated_items[self.__configurations.index(configuration)]
|
|
56
|
+
|
|
57
|
+
def _sort(self, items: List[_GeneratedItemType]) -> List[_GeneratedItemType]:
|
|
58
|
+
return items
|
|
59
|
+
|
|
60
|
+
def _select(
|
|
61
|
+
self, items: List[_GeneratedItemType], selector_parameters: Optional[_SelectorParametersType]
|
|
62
|
+
) -> List[_GeneratedItemType]:
|
|
63
|
+
return items
|
|
64
|
+
|
|
65
|
+
def _post_process(
|
|
66
|
+
self, items: List[_GeneratedItemType], post_process_parameters: Optional[_PostProcessParametersType]
|
|
67
|
+
) -> List[Material]:
|
|
68
|
+
return [Material(self._convert_generated_item(item)) for item in items]
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _convert_generated_item(item: _GeneratedItemType):
|
|
72
|
+
material_config = item
|
|
73
|
+
return material_config
|
|
74
|
+
|
|
75
|
+
def _finalize(self, materials: List[Material], configuration: _ConfigurationType) -> List[Material]:
|
|
76
|
+
return [self._update_material_name(material, configuration) for material in materials]
|
|
77
|
+
|
|
78
|
+
def get_materials(
|
|
79
|
+
self,
|
|
80
|
+
configuration: _ConfigurationType,
|
|
81
|
+
selector_parameters: Optional[_SelectorParametersType] = None,
|
|
82
|
+
post_process_parameters: Optional[_PostProcessParametersType] = None,
|
|
83
|
+
) -> List[Material]:
|
|
84
|
+
generated_items = self._generate_or_get_from_cache(configuration)
|
|
85
|
+
sorted_items = self._sort(generated_items)
|
|
86
|
+
selected_items = self._select(sorted_items, selector_parameters)
|
|
87
|
+
materials = self._post_process(selected_items, post_process_parameters)
|
|
88
|
+
finalized_materials = self._finalize(materials, configuration)
|
|
89
|
+
return finalized_materials
|
|
90
|
+
|
|
91
|
+
def get_material(
|
|
92
|
+
self,
|
|
93
|
+
configuration: _ConfigurationType,
|
|
94
|
+
selector_parameters: Optional[_SelectorParametersType] = None,
|
|
95
|
+
post_process_parameters: Optional[_PostProcessParametersType] = None,
|
|
96
|
+
) -> Material:
|
|
97
|
+
return self.get_materials(configuration, selector_parameters, post_process_parameters)[0]
|
|
98
|
+
|
|
99
|
+
def _update_material_name(self, material, configuration):
|
|
100
|
+
# Do nothing by default
|
|
101
|
+
return material
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Union, List, Optional
|
|
2
|
+
|
|
3
|
+
from mat3ra.made.material import Material
|
|
4
|
+
from .builders import (
|
|
5
|
+
SimpleInterfaceBuilder,
|
|
6
|
+
SimpleInterfaceBuilderParameters,
|
|
7
|
+
ZSLStrainMatchingParameters,
|
|
8
|
+
ZSLStrainMatchingInterfaceBuilder,
|
|
9
|
+
ZSLStrainMatchingInterfaceBuilderParameters,
|
|
10
|
+
)
|
|
11
|
+
from .configuration import InterfaceConfiguration
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_interfaces(
|
|
15
|
+
builder: Union[SimpleInterfaceBuilder, ZSLStrainMatchingInterfaceBuilder], configuration: InterfaceConfiguration
|
|
16
|
+
) -> List[Material]:
|
|
17
|
+
return builder.get_materials(configuration)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_interface(
|
|
21
|
+
configuration: InterfaceConfiguration,
|
|
22
|
+
builder: Optional[Union[SimpleInterfaceBuilder, ZSLStrainMatchingInterfaceBuilder]] = None,
|
|
23
|
+
) -> Material:
|
|
24
|
+
if builder is None:
|
|
25
|
+
builder = SimpleInterfaceBuilder(build_parameters=SimpleInterfaceBuilderParameters())
|
|
26
|
+
return builder.get_material(configuration)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from typing import Any, List, Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from ase.build.tools import niggli_reduce
|
|
6
|
+
from pymatgen.analysis.interfaces.coherent_interfaces import (
|
|
7
|
+
CoherentInterfaceBuilder,
|
|
8
|
+
ZSLGenerator,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from mat3ra.made.material import Material
|
|
12
|
+
from .enums import StrainModes
|
|
13
|
+
from .configuration import InterfaceConfiguration
|
|
14
|
+
from .termination_pair import TerminationPair, safely_select_termination_pair
|
|
15
|
+
from .utils import interface_patch_with_mean_abs_strain, remove_duplicate_interfaces
|
|
16
|
+
from ..mixins import (
|
|
17
|
+
ConvertGeneratedItemsASEAtomsMixin,
|
|
18
|
+
ConvertGeneratedItemsPymatgenStructureMixin,
|
|
19
|
+
)
|
|
20
|
+
from ..slab import create_slab, Termination
|
|
21
|
+
from ..slab.configuration import SlabConfiguration
|
|
22
|
+
from ...analyze import get_chemical_formula
|
|
23
|
+
from ...convert import to_ase, from_ase, to_pymatgen, PymatgenInterface, ASEAtoms
|
|
24
|
+
from ...build import BaseBuilder
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class InterfaceBuilderParameters(BaseModel):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class InterfaceBuilder(BaseBuilder):
|
|
32
|
+
_BuildParametersType = InterfaceBuilderParameters
|
|
33
|
+
_ConfigurationType: type(InterfaceConfiguration) = InterfaceConfiguration # type: ignore
|
|
34
|
+
|
|
35
|
+
def _update_material_name(self, material: Material, configuration: InterfaceConfiguration) -> Material:
|
|
36
|
+
film_formula = get_chemical_formula(configuration.film_configuration.bulk)
|
|
37
|
+
substrate_formula = get_chemical_formula(configuration.substrate_configuration.bulk)
|
|
38
|
+
film_miller_indices = "".join([str(i) for i in configuration.film_configuration.miller_indices])
|
|
39
|
+
substrate_miller_indices = "".join([str(i) for i in configuration.substrate_configuration.miller_indices])
|
|
40
|
+
new_name = f"{film_formula}({film_miller_indices})-{substrate_formula}({substrate_miller_indices}), Interface"
|
|
41
|
+
material.name = new_name
|
|
42
|
+
return material
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
########################################################################################
|
|
46
|
+
# Simple Interface Builder #
|
|
47
|
+
########################################################################################
|
|
48
|
+
class SimpleInterfaceBuilderParameters(InterfaceBuilderParameters):
|
|
49
|
+
scale_film: bool = True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SimpleInterfaceBuilder(ConvertGeneratedItemsASEAtomsMixin, InterfaceBuilder):
|
|
53
|
+
"""
|
|
54
|
+
Creates matching interface between substrate and film by straining the film to match the substrate.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
_BuildParametersType = Optional[SimpleInterfaceBuilderParameters]
|
|
58
|
+
_DefaultBuildParameters = SimpleInterfaceBuilderParameters(scale_film=True)
|
|
59
|
+
_GeneratedItemType: type(ASEAtoms) = ASEAtoms # type: ignore
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def __preprocess_slab_configuration(configuration: SlabConfiguration, termination: Termination):
|
|
63
|
+
slab = create_slab(configuration, termination)
|
|
64
|
+
ase_slab = to_ase(slab)
|
|
65
|
+
niggli_reduce(ase_slab)
|
|
66
|
+
return ase_slab
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def __combine_two_slabs_ase(substrate_slab_ase: ASEAtoms, film_slab_ase: ASEAtoms, distance_z: float) -> ASEAtoms:
|
|
70
|
+
max_z_substrate = max(substrate_slab_ase.positions[:, 2])
|
|
71
|
+
min_z_film = min(film_slab_ase.positions[:, 2])
|
|
72
|
+
shift_z = max_z_substrate - min_z_film + distance_z
|
|
73
|
+
|
|
74
|
+
film_slab_ase.translate([0, 0, shift_z])
|
|
75
|
+
|
|
76
|
+
return substrate_slab_ase + film_slab_ase
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def __add_vacuum_along_c_ase(interface_ase: ASEAtoms, vacuum: float) -> ASEAtoms:
|
|
80
|
+
cell_c_with_vacuum = max(interface_ase.positions[:, 2]) + vacuum
|
|
81
|
+
interface_ase.cell[2, 2] = cell_c_with_vacuum
|
|
82
|
+
return interface_ase
|
|
83
|
+
|
|
84
|
+
def _generate(self, configuration: InterfaceBuilder._ConfigurationType) -> List[_GeneratedItemType]: # type: ignore
|
|
85
|
+
film_slab_ase = self.__preprocess_slab_configuration(
|
|
86
|
+
configuration.film_configuration, configuration.film_termination
|
|
87
|
+
)
|
|
88
|
+
substrate_slab_ase = self.__preprocess_slab_configuration(
|
|
89
|
+
configuration.substrate_configuration, configuration.substrate_termination
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if self.build_parameters.scale_film:
|
|
93
|
+
film_slab_ase.set_cell(substrate_slab_ase.cell, scale_atoms=True)
|
|
94
|
+
film_slab_ase.wrap()
|
|
95
|
+
|
|
96
|
+
interface_ase = self.__combine_two_slabs_ase(substrate_slab_ase, film_slab_ase, configuration.distance_z)
|
|
97
|
+
interface_ase_with_vacuum = self.__add_vacuum_along_c_ase(interface_ase, configuration.vacuum)
|
|
98
|
+
|
|
99
|
+
return [interface_ase_with_vacuum]
|
|
100
|
+
|
|
101
|
+
def _post_process(self, items: List[_GeneratedItemType], post_process_parameters=None) -> List[Material]:
|
|
102
|
+
return [Material(from_ase(slab)) for slab in items]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
########################################################################################
|
|
106
|
+
# Strain Matching Interface Builders #
|
|
107
|
+
########################################################################################
|
|
108
|
+
class StrainMatchingInterfaceBuilderParameters(BaseModel):
|
|
109
|
+
strain_matching_parameters: Optional[Any] = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class StrainMatchingInterfaceBuilder(InterfaceBuilder):
|
|
113
|
+
_BuildParametersType = StrainMatchingInterfaceBuilderParameters # type: ignore
|
|
114
|
+
|
|
115
|
+
def _update_material_name(self, material: Material, configuration: InterfaceConfiguration) -> Material:
|
|
116
|
+
updated_material = super()._update_material_name(material, configuration)
|
|
117
|
+
if StrainModes.mean_abs_strain in material.metadata:
|
|
118
|
+
strain = material.metadata[StrainModes.mean_abs_strain]
|
|
119
|
+
new_name = f"{updated_material.name}, Strain {strain*100:.3f}%"
|
|
120
|
+
updated_material.name = new_name
|
|
121
|
+
return material
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ZSLStrainMatchingParameters(BaseModel):
|
|
125
|
+
max_area: float = 50.0
|
|
126
|
+
max_area_ratio_tol: float = 0.09
|
|
127
|
+
max_length_tol: float = 0.03
|
|
128
|
+
max_angle_tol: float = 0.01
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ZSLStrainMatchingInterfaceBuilderParameters(StrainMatchingInterfaceBuilderParameters):
|
|
132
|
+
strain_matching_parameters: ZSLStrainMatchingParameters
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class ZSLStrainMatchingInterfaceBuilder(ConvertGeneratedItemsPymatgenStructureMixin, StrainMatchingInterfaceBuilder):
|
|
136
|
+
"""
|
|
137
|
+
Creates matching interface between substrate and film using the ZSL algorithm.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
_BuildParametersType: type( # type: ignore
|
|
141
|
+
ZSLStrainMatchingInterfaceBuilderParameters
|
|
142
|
+
) = ZSLStrainMatchingInterfaceBuilderParameters # type: ignore
|
|
143
|
+
_GeneratedItemType: PymatgenInterface = PymatgenInterface # type: ignore
|
|
144
|
+
|
|
145
|
+
def _generate(self, configuration: InterfaceConfiguration) -> List[PymatgenInterface]:
|
|
146
|
+
generator = ZSLGenerator(**self.build_parameters.strain_matching_parameters.dict())
|
|
147
|
+
builder = CoherentInterfaceBuilder(
|
|
148
|
+
substrate_structure=to_pymatgen(configuration.substrate_configuration.bulk),
|
|
149
|
+
film_structure=to_pymatgen(configuration.film_configuration.bulk),
|
|
150
|
+
substrate_miller=configuration.substrate_configuration.miller_indices,
|
|
151
|
+
film_miller=configuration.film_configuration.miller_indices,
|
|
152
|
+
zslgen=generator,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
generated_termination_pairs = [
|
|
156
|
+
TerminationPair.from_pymatgen(pymatgen_termination) for pymatgen_termination in builder.terminations
|
|
157
|
+
]
|
|
158
|
+
termination_pair = safely_select_termination_pair(configuration.termination_pair, generated_termination_pairs)
|
|
159
|
+
interfaces = builder.get_interfaces(
|
|
160
|
+
termination=termination_pair.to_pymatgen(),
|
|
161
|
+
gap=configuration.distance_z,
|
|
162
|
+
film_thickness=configuration.film_configuration.thickness,
|
|
163
|
+
substrate_thickness=configuration.substrate_configuration.thickness,
|
|
164
|
+
in_layers=True,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return list([interface_patch_with_mean_abs_strain(interface) for interface in interfaces])
|
|
168
|
+
|
|
169
|
+
def _sort(self, items: List[_GeneratedItemType]):
|
|
170
|
+
sorted_by_num_sites = sorted(items, key=lambda x: x.num_sites)
|
|
171
|
+
sorted_by_num_sites_and_strain = sorted(
|
|
172
|
+
sorted_by_num_sites, key=lambda x: np.mean(x.interface_properties[StrainModes.mean_abs_strain])
|
|
173
|
+
)
|
|
174
|
+
unique_sorted_interfaces = remove_duplicate_interfaces(
|
|
175
|
+
sorted_by_num_sites_and_strain, strain_mode=StrainModes.mean_abs_strain
|
|
176
|
+
)
|
|
177
|
+
return unique_sorted_interfaces
|
|
178
|
+
|
|
179
|
+
def _post_process(self, items: List[_GeneratedItemType], post_process_parameters=None) -> List[Material]:
|
|
180
|
+
materials = super()._post_process(items, post_process_parameters)
|
|
181
|
+
strains = [interface.interface_properties[StrainModes.mean_abs_strain] for interface in items]
|
|
182
|
+
|
|
183
|
+
for material, strain in zip(materials, strains):
|
|
184
|
+
material.metadata["mean_abs_strain"] = strain
|
|
185
|
+
return materials
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from .termination_pair import TerminationPair
|
|
4
|
+
from ..slab import Termination
|
|
5
|
+
from ..slab.configuration import SlabConfiguration
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InterfaceConfiguration(BaseModel):
|
|
9
|
+
film_configuration: SlabConfiguration
|
|
10
|
+
substrate_configuration: SlabConfiguration
|
|
11
|
+
film_termination: Termination
|
|
12
|
+
substrate_termination: Termination
|
|
13
|
+
distance_z: float = 3.0
|
|
14
|
+
vacuum: float = 5.0
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def termination_pair(self):
|
|
18
|
+
return TerminationPair(self.film_termination, self.substrate_termination)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from typing import Tuple, List
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from ..slab.termination import Termination
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TerminationPair(BaseModel):
|
|
8
|
+
film_termination: Termination
|
|
9
|
+
substrate_termination: Termination
|
|
10
|
+
|
|
11
|
+
def __str__(self):
|
|
12
|
+
# return str(self.to_tuple_of_str())
|
|
13
|
+
return f"({self.film_termination}, {self.substrate_termination})"
|
|
14
|
+
|
|
15
|
+
def __repr__(self):
|
|
16
|
+
return self.__str__()
|
|
17
|
+
|
|
18
|
+
def __init__(self, film_termination: Termination, substrate_termination: Termination):
|
|
19
|
+
super().__init__(film_termination=film_termination, substrate_termination=substrate_termination)
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_tuple_of_str(cls, termination_pair: Tuple[str, str]):
|
|
23
|
+
film_termination = Termination.from_string(termination_pair[0])
|
|
24
|
+
substrate_termination = Termination.from_string(termination_pair[1])
|
|
25
|
+
return cls(film_termination=film_termination, substrate_termination=substrate_termination)
|
|
26
|
+
|
|
27
|
+
def to_tuple_of_str(self) -> Tuple[str, str]:
|
|
28
|
+
return str(self.film_termination), str(self.substrate_termination)
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_pymatgen(cls, termination_pair: Tuple[str, str]):
|
|
32
|
+
return cls.from_tuple_of_str(termination_pair)
|
|
33
|
+
|
|
34
|
+
def to_pymatgen(self) -> Tuple[str, str]:
|
|
35
|
+
return self.to_tuple_of_str()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def safely_select_termination_pair(
|
|
39
|
+
provided_termination_pair: TerminationPair, generated_termination_pairs: List[TerminationPair]
|
|
40
|
+
) -> TerminationPair:
|
|
41
|
+
"""
|
|
42
|
+
Attempt finding provided in generated terminations to find a complete match,
|
|
43
|
+
if match isn't found, get terminations with equivalent chemical elements.
|
|
44
|
+
"""
|
|
45
|
+
provided_film_termination = provided_termination_pair.film_termination
|
|
46
|
+
provided_substrate_termination = provided_termination_pair.substrate_termination
|
|
47
|
+
hotfix_termination_pair = provided_termination_pair
|
|
48
|
+
if provided_termination_pair not in generated_termination_pairs:
|
|
49
|
+
for termination_pair in generated_termination_pairs:
|
|
50
|
+
generated_film_termination = termination_pair.film_termination
|
|
51
|
+
generated_substrate_termination = termination_pair.substrate_termination
|
|
52
|
+
if (
|
|
53
|
+
generated_film_termination.chemical_elements == provided_film_termination.chemical_elements
|
|
54
|
+
and generated_substrate_termination.chemical_elements
|
|
55
|
+
== provided_substrate_termination.chemical_elements
|
|
56
|
+
):
|
|
57
|
+
hotfix_termination_pair = termination_pair
|
|
58
|
+
print("Interface will be built with terminations: ", hotfix_termination_pair)
|
|
59
|
+
return hotfix_termination_pair
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import types
|
|
2
|
+
from typing import List
|
|
3
|
+
import numpy as np
|
|
4
|
+
from .enums import StrainModes
|
|
5
|
+
from ...convert import PymatgenInterface
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def interface_patch_with_mean_abs_strain(target: PymatgenInterface, tolerance: float = 10e-6):
|
|
9
|
+
def get_mean_abs_strain(target):
|
|
10
|
+
return target.interface_properties[StrainModes.mean_abs_strain]
|
|
11
|
+
|
|
12
|
+
target.get_mean_abs_strain = types.MethodType(get_mean_abs_strain, target)
|
|
13
|
+
target.interface_properties[StrainModes.mean_abs_strain] = (
|
|
14
|
+
round(np.mean(np.abs(target.interface_properties["strain"])) / tolerance) * tolerance
|
|
15
|
+
)
|
|
16
|
+
return target
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def remove_duplicate_interfaces(
|
|
20
|
+
interfaces: List[PymatgenInterface], strain_mode: StrainModes = StrainModes.mean_abs_strain
|
|
21
|
+
):
|
|
22
|
+
def are_interfaces_duplicate(interface1: PymatgenInterface, interface2: PymatgenInterface):
|
|
23
|
+
are_sizes_equivalent = interface1.num_sites == interface2.num_sites and np.allclose(
|
|
24
|
+
interface1.interface_properties[strain_mode], interface2.interface_properties[strain_mode]
|
|
25
|
+
)
|
|
26
|
+
are_strains_equivalent = np.allclose(
|
|
27
|
+
interface1.interface_properties[strain_mode], interface2.interface_properties[strain_mode]
|
|
28
|
+
)
|
|
29
|
+
return are_sizes_equivalent and are_strains_equivalent
|
|
30
|
+
|
|
31
|
+
filtered_interfaces = [interfaces[0]] if interfaces else []
|
|
32
|
+
|
|
33
|
+
for interface in interfaces[1:]:
|
|
34
|
+
if not any(are_interfaces_duplicate(interface, unique_interface) for unique_interface in filtered_interfaces):
|
|
35
|
+
filtered_interfaces.append(interface)
|
|
36
|
+
return filtered_interfaces
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from ..convert import from_ase, from_pymatgen, ASEAtoms, PymatgenStructure
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ConvertGeneratedItemsASEAtomsMixin:
|
|
5
|
+
@staticmethod
|
|
6
|
+
def _convert_generated_item(item: ASEAtoms):
|
|
7
|
+
return from_ase(item)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConvertGeneratedItemsPymatgenStructureMixin:
|
|
11
|
+
@staticmethod
|
|
12
|
+
def _convert_generated_item(item: PymatgenStructure):
|
|
13
|
+
return from_pymatgen(item)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from mat3ra.made.material import Material
|
|
4
|
+
from .builders import SlabBuilder, SlabSelectorParameters
|
|
5
|
+
from .configuration import SlabConfiguration
|
|
6
|
+
from .termination import Termination
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_terminations(configuration: SlabConfiguration) -> List[Termination]:
|
|
10
|
+
return SlabBuilder().get_terminations(configuration)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_slab(configuration: SlabConfiguration, termination: Termination) -> Material:
|
|
14
|
+
builder = SlabBuilder()
|
|
15
|
+
return builder.get_material(configuration, selector_parameters=SlabSelectorParameters(termination=termination))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from pymatgen.core.surface import SlabGenerator as PymatgenSlabGenerator
|
|
2
|
+
from ...convert import label_pymatgen_slab_termination
|
|
3
|
+
from typing import List
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from mat3ra.made.material import Material
|
|
8
|
+
|
|
9
|
+
from .termination import Termination
|
|
10
|
+
from ...analyze import get_chemical_formula
|
|
11
|
+
from ...convert import to_pymatgen, PymatgenSlab
|
|
12
|
+
from ...build import BaseBuilder
|
|
13
|
+
from ...build.mixins import ConvertGeneratedItemsPymatgenStructureMixin
|
|
14
|
+
from ..supercell import create_supercell
|
|
15
|
+
from .configuration import SlabConfiguration
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SlabSelectorParameters(BaseModel):
|
|
19
|
+
termination: Termination
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SlabBuilder(ConvertGeneratedItemsPymatgenStructureMixin, BaseBuilder):
|
|
23
|
+
_ConfigurationType: type(SlabConfiguration) = SlabConfiguration # type: ignore
|
|
24
|
+
_GeneratedItemType: PymatgenSlab = PymatgenSlab # type: ignore
|
|
25
|
+
_SelectorParametersType: type(SlabSelectorParameters) = SlabSelectorParameters # type: ignore
|
|
26
|
+
__configuration: SlabConfiguration
|
|
27
|
+
|
|
28
|
+
def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]: # type: ignore
|
|
29
|
+
generator = PymatgenSlabGenerator(
|
|
30
|
+
initial_structure=to_pymatgen(configuration.bulk),
|
|
31
|
+
miller_index=configuration.miller_indices,
|
|
32
|
+
min_slab_size=configuration.thickness,
|
|
33
|
+
min_vacuum_size=configuration.vacuum,
|
|
34
|
+
in_unit_planes=True,
|
|
35
|
+
reorient_lattice=True,
|
|
36
|
+
)
|
|
37
|
+
raw_slabs = generator.get_slabs()
|
|
38
|
+
self.__configuration = configuration
|
|
39
|
+
|
|
40
|
+
return [self.__conditionally_convert_slab_to_orthogonal_pymatgen(slab) for slab in raw_slabs]
|
|
41
|
+
|
|
42
|
+
def _select(
|
|
43
|
+
self, items: List[_GeneratedItemType], selector_parameters: _SelectorParametersType
|
|
44
|
+
) -> List[_GeneratedItemType]:
|
|
45
|
+
termination = selector_parameters.termination
|
|
46
|
+
return [slab for slab in items if self.__create_termination_from_slab_pymatgen(slab) == termination]
|
|
47
|
+
|
|
48
|
+
def _post_process(self, items: List[_GeneratedItemType], post_process_parameters=None) -> List[Material]:
|
|
49
|
+
materials = super()._post_process(items, post_process_parameters)
|
|
50
|
+
materials = [create_supercell(material, self.__configuration.xy_supercell_matrix) for material in materials]
|
|
51
|
+
for idx, material in enumerate(materials):
|
|
52
|
+
material.metadata["termination"] = label_pymatgen_slab_termination(items[idx])
|
|
53
|
+
|
|
54
|
+
return materials
|
|
55
|
+
|
|
56
|
+
def get_terminations(self, configuration: _ConfigurationType) -> List[Termination]:
|
|
57
|
+
return [
|
|
58
|
+
self.__create_termination_from_slab_pymatgen(slab)
|
|
59
|
+
for slab in self._generate_or_get_from_cache(configuration)
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
def __conditionally_convert_slab_to_orthogonal_pymatgen(self, slab: PymatgenSlab) -> PymatgenSlab:
|
|
63
|
+
return slab.get_orthogonal_c_slab() if self.__configuration.use_orthogonal_z else slab
|
|
64
|
+
|
|
65
|
+
def __create_termination_from_slab_pymatgen(self, slab: PymatgenSlab) -> Termination:
|
|
66
|
+
return Termination.from_string(label_pymatgen_slab_termination(slab))
|
|
67
|
+
|
|
68
|
+
def _update_material_name(self, material: Material, configuration: SlabConfiguration) -> Material:
|
|
69
|
+
formula = get_chemical_formula(configuration.bulk)
|
|
70
|
+
miller_indices = "".join([str(i) for i in configuration.miller_indices])
|
|
71
|
+
termination = material.metadata["termination"]
|
|
72
|
+
# for example: "Si8(001), termination Si_P4/mmm_1, Slab"
|
|
73
|
+
new_name = f"{formula}({miller_indices}), termination {termination}, Slab"
|
|
74
|
+
material.name = new_name
|
|
75
|
+
return material
|