@mat3ra/made 2024.4.16-0 → 2024.5.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/pyproject.toml +1 -0
- package/src/py/mat3ra/made/tools/analyze.py +19 -3
- package/src/py/mat3ra/made/tools/build/__init__.py +66 -0
- package/src/py/mat3ra/made/tools/build/interface.py +237 -0
- package/src/py/mat3ra/made/tools/calculate.py +94 -2
- package/src/py/mat3ra/made/tools/convert.py +28 -1
- package/src/py/mat3ra/made/tools/modify.py +40 -2
- package/src/py/mat3ra/made/tools/utils.py +19 -0
- package/tests/py/unit/fixtures.py +31 -0
- package/tests/py/unit/test_tools_analyze.py +10 -6
- package/tests/py/unit/test_tools_build.py +22 -0
- package/tests/py/unit/test_tools_build_interface.py +24 -0
- package/tests/py/unit/test_tools_calculate.py +55 -2
- package/tests/py/unit/utils.py +25 -0
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
from ase import Atoms
|
|
3
3
|
|
|
4
|
-
from .convert import
|
|
4
|
+
from .convert import decorator_convert_material_args_kwargs_to_atoms
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
@
|
|
8
|
-
def
|
|
7
|
+
@decorator_convert_material_args_kwargs_to_atoms
|
|
8
|
+
def get_average_interlayer_distance(
|
|
9
9
|
interface_atoms: Atoms, tag_substrate: str, tag_film: str, threshold: float = 0.5
|
|
10
10
|
) -> float:
|
|
11
11
|
"""
|
|
@@ -39,3 +39,19 @@ def calculate_average_interlayer_distance(
|
|
|
39
39
|
# Calculate the average distance between the top layer of substrate and the bottom layer of film
|
|
40
40
|
average_interlayer_distance = avg_z_bottom_film - avg_z_top_substrate
|
|
41
41
|
return abs(average_interlayer_distance)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@decorator_convert_material_args_kwargs_to_atoms
|
|
45
|
+
def get_surface_area(atoms: Atoms):
|
|
46
|
+
"""
|
|
47
|
+
Calculate the area of the surface perpendicular to the z-axis of the atoms structure.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
atoms (ase.Atoms): The Atoms object to calculate the surface area of.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
float: The surface area of the atoms.
|
|
54
|
+
"""
|
|
55
|
+
matrix = atoms.cell
|
|
56
|
+
cross_product = np.cross(matrix[0], matrix[1])
|
|
57
|
+
return np.linalg.norm(cross_product)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from ...material import Material
|
|
2
|
+
from .interface import InterfaceDataHolder
|
|
3
|
+
from .interface import InterfaceSettings as Settings
|
|
4
|
+
from .interface import interface_init_zsl_builder, interface_patch_with_mean_abs_strain
|
|
5
|
+
from ..convert import decorator_convert_material_args_kwargs_to_structure
|
|
6
|
+
from ..modify import translate_to_bottom, wrap_to_unit_cell
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@decorator_convert_material_args_kwargs_to_structure
|
|
10
|
+
def create_interfaces(
|
|
11
|
+
substrate: Material,
|
|
12
|
+
layer: Material,
|
|
13
|
+
settings: Settings,
|
|
14
|
+
sort_by_strain_and_size: bool = True,
|
|
15
|
+
remove_duplicates: bool = True,
|
|
16
|
+
is_logging_enabled: bool = True,
|
|
17
|
+
) -> InterfaceDataHolder:
|
|
18
|
+
"""
|
|
19
|
+
Create all interfaces between the substrate and layer structures using ZSL algorithm provided by pymatgen.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
substrate (Material): The substrate structure.
|
|
23
|
+
layer (Material): The layer structure.
|
|
24
|
+
settings: The settings for the interface generation.
|
|
25
|
+
sort_by_strain_and_size (bool): Whether to sort the interfaces by strain and size.
|
|
26
|
+
remove_duplicates (bool): Whether to remove duplicate interfaces.
|
|
27
|
+
is_logging_enabled (bool): Whether to enable debug print.
|
|
28
|
+
Returns:
|
|
29
|
+
InterfaceDataHolder.
|
|
30
|
+
"""
|
|
31
|
+
substrate = translate_to_bottom(substrate, settings["USE_CONVENTIONAL_CELL"])
|
|
32
|
+
layer = translate_to_bottom(layer, settings["USE_CONVENTIONAL_CELL"])
|
|
33
|
+
|
|
34
|
+
if is_logging_enabled:
|
|
35
|
+
print("Creating interfaces...")
|
|
36
|
+
|
|
37
|
+
builder = interface_init_zsl_builder(substrate, layer, settings)
|
|
38
|
+
interfaces_data = InterfaceDataHolder()
|
|
39
|
+
|
|
40
|
+
for termination in builder.terminations:
|
|
41
|
+
all_interfaces_for_termination = builder.get_interfaces(
|
|
42
|
+
termination,
|
|
43
|
+
gap=settings["INTERFACE_PARAMETERS"]["DISTANCE_Z"],
|
|
44
|
+
film_thickness=settings["LAYER_PARAMETERS"]["THICKNESS"],
|
|
45
|
+
substrate_thickness=settings["SUBSTRATE_PARAMETERS"]["THICKNESS"],
|
|
46
|
+
in_layers=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
all_interfaces_for_termination_patched_wrapped = list(
|
|
50
|
+
map(
|
|
51
|
+
lambda i: wrap_to_unit_cell(interface_patch_with_mean_abs_strain(i)),
|
|
52
|
+
all_interfaces_for_termination,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
interfaces_data.add_data_entries(
|
|
57
|
+
all_interfaces_for_termination_patched_wrapped,
|
|
58
|
+
sort_interfaces_by_strain_and_size=sort_by_strain_and_size,
|
|
59
|
+
remove_duplicates=remove_duplicates,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if is_logging_enabled:
|
|
63
|
+
unique_str = "unique" if remove_duplicates else ""
|
|
64
|
+
print(f"Found {len(interfaces_data.get_interfaces_for_termination(0))} {unique_str} interfaces.")
|
|
65
|
+
|
|
66
|
+
return interfaces_data
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import types
|
|
3
|
+
import numpy as np
|
|
4
|
+
from typing import Union, List, Tuple, Dict, TypedDict
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from mat3ra.utils import array as array_utils
|
|
7
|
+
from pymatgen.core.structure import Structure
|
|
8
|
+
from pymatgen.analysis.interfaces.coherent_interfaces import CoherentInterfaceBuilder, ZSLGenerator
|
|
9
|
+
from pymatgen.analysis.interfaces.coherent_interfaces import Interface
|
|
10
|
+
from ..convert import convert_atoms_or_structure_to_material, decorator_convert_material_args_kwargs_to_structure
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SlabParameters(TypedDict):
|
|
14
|
+
MILLER_INDICES: Tuple[int, int, int]
|
|
15
|
+
THICKNESS: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ZSLParameters(TypedDict):
|
|
19
|
+
MAX_AREA_TOL: float
|
|
20
|
+
MAX_AREA: float
|
|
21
|
+
MAX_LENGTH_TOL: float
|
|
22
|
+
MAX_ANGLE_TOL: float
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InterfaceParameters(TypedDict):
|
|
26
|
+
DISTANCE_Z: float
|
|
27
|
+
MAX_AREA: float
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InterfaceSettings(TypedDict):
|
|
31
|
+
SUBSTRATE_PARAMETERS: SlabParameters
|
|
32
|
+
LAYER_PARAMETERS: SlabParameters
|
|
33
|
+
USE_CONVENTIONAL_CELL: bool
|
|
34
|
+
ZSL_PARAMETERS: ZSLParameters
|
|
35
|
+
INTERFACE_PARAMETERS: InterfaceParameters
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class StrainModes(Enum):
|
|
39
|
+
strain = "strain"
|
|
40
|
+
von_mises_strain = "von_mises_strain"
|
|
41
|
+
mean_abs_strain = "mean_abs_strain"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def interface_patch_with_mean_abs_strain(target: Interface, tolerance: float = 10e-6):
|
|
45
|
+
def get_mean_abs_strain(target):
|
|
46
|
+
return target.interface_properties[StrainModes.mean_abs_strain]
|
|
47
|
+
|
|
48
|
+
target.get_mean_abs_strain = types.MethodType(get_mean_abs_strain, target)
|
|
49
|
+
target.interface_properties[StrainModes.mean_abs_strain] = (
|
|
50
|
+
round(np.mean(np.abs(target.interface_properties["strain"])) / tolerance) * tolerance
|
|
51
|
+
)
|
|
52
|
+
return target
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@decorator_convert_material_args_kwargs_to_structure
|
|
56
|
+
def interface_init_zsl_builder(
|
|
57
|
+
substrate: Structure, layer: Structure, settings: InterfaceSettings
|
|
58
|
+
) -> CoherentInterfaceBuilder:
|
|
59
|
+
generator: ZSLGenerator = ZSLGenerator(
|
|
60
|
+
max_area_ratio_tol=settings["ZSL_PARAMETERS"]["MAX_AREA_TOL"],
|
|
61
|
+
max_area=settings["ZSL_PARAMETERS"]["MAX_AREA"],
|
|
62
|
+
max_length_tol=settings["ZSL_PARAMETERS"]["MAX_LENGTH_TOL"],
|
|
63
|
+
max_angle_tol=settings["ZSL_PARAMETERS"]["MAX_ANGLE_TOL"],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
builder = CoherentInterfaceBuilder(
|
|
67
|
+
substrate_structure=substrate,
|
|
68
|
+
film_structure=layer,
|
|
69
|
+
substrate_miller=settings["SUBSTRATE_PARAMETERS"]["MILLER_INDICES"],
|
|
70
|
+
film_miller=settings["LAYER_PARAMETERS"]["MILLER_INDICES"],
|
|
71
|
+
zslgen=generator,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return builder
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
TerminationType = Tuple[str, str]
|
|
78
|
+
InterfacesType = List[Interface]
|
|
79
|
+
InterfacesDataType = Dict[Tuple, List[Interface]]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class InterfaceDataHolder(object):
|
|
83
|
+
"""
|
|
84
|
+
A class to hold data for interfaces generated by pymatgen.
|
|
85
|
+
Structures are stored in a dictionary with the termination as the key.
|
|
86
|
+
Example data structure:
|
|
87
|
+
{
|
|
88
|
+
"('C_P6/mmm_2', 'Si_R-3m_1')": [
|
|
89
|
+
{ ...interface for ('C_P6/mmm_2', 'Si_R-3m_1') at index 0...},
|
|
90
|
+
{ ...interface for ('C_P6/mmm_2', 'Si_R-3m_1') at index 1...},
|
|
91
|
+
...
|
|
92
|
+
],
|
|
93
|
+
"<termination at index 1>": [
|
|
94
|
+
{ ...interface for 'termination at index 1' at index 0...},
|
|
95
|
+
{ ...interface for 'termination at index 1' at index 1...},
|
|
96
|
+
...
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, entries: Union[InterfacesType, None] = None) -> None:
|
|
102
|
+
if entries is None:
|
|
103
|
+
entries = []
|
|
104
|
+
self.data: InterfacesDataType = {}
|
|
105
|
+
self.terminations: List[TerminationType] = []
|
|
106
|
+
self.add_data_entries(entries)
|
|
107
|
+
|
|
108
|
+
def __str__(self):
|
|
109
|
+
terminations_list = f"There are {len(self.terminations)} terminations:" + ", ".join(
|
|
110
|
+
f"\n{idx}: ({a}, {b})" for idx, (a, b) in enumerate(self.terminations)
|
|
111
|
+
)
|
|
112
|
+
interfaces_list = "\n".join(
|
|
113
|
+
[
|
|
114
|
+
f"There are {len(self.data[termination])} interfaces for termination {termination}:\n{idx}: "
|
|
115
|
+
+ f"{self.data[termination]}"
|
|
116
|
+
for idx, termination in enumerate(self.terminations)
|
|
117
|
+
]
|
|
118
|
+
)
|
|
119
|
+
return f"{terminations_list}\n{interfaces_list}"
|
|
120
|
+
|
|
121
|
+
def add_termination(self, termination: Tuple[str, str]):
|
|
122
|
+
if termination not in self.terminations:
|
|
123
|
+
self.terminations.append(termination)
|
|
124
|
+
self.set_interfaces_for_termination(termination, [])
|
|
125
|
+
|
|
126
|
+
def add_interfaces_for_termination(
|
|
127
|
+
self, termination: TerminationType, interfaces: Union[InterfacesType, Interface]
|
|
128
|
+
):
|
|
129
|
+
self.add_termination(termination)
|
|
130
|
+
self.set_interfaces_for_termination(termination, self.get_interfaces_for_termination(termination) + interfaces)
|
|
131
|
+
|
|
132
|
+
def add_data_entries(
|
|
133
|
+
self,
|
|
134
|
+
entries: List[Interface] = [],
|
|
135
|
+
sort_interfaces_by_strain_and_size: bool = True,
|
|
136
|
+
remove_duplicates: bool = True,
|
|
137
|
+
):
|
|
138
|
+
entries = array_utils.convert_to_array_if_not(entries)
|
|
139
|
+
all_terminations = [e.interface_properties["termination"] for e in entries]
|
|
140
|
+
unique_terminations = list(set(all_terminations))
|
|
141
|
+
for termination in unique_terminations:
|
|
142
|
+
entries_for_termination = [
|
|
143
|
+
entry for entry in entries if entry.interface_properties["termination"] == termination
|
|
144
|
+
]
|
|
145
|
+
self.add_interfaces_for_termination(termination, entries_for_termination)
|
|
146
|
+
if sort_interfaces_by_strain_and_size:
|
|
147
|
+
self.sort_interfaces_for_all_terminations_by_strain_and_size()
|
|
148
|
+
if remove_duplicates:
|
|
149
|
+
self.remove_duplicate_interfaces()
|
|
150
|
+
|
|
151
|
+
def set_interfaces_for_termination(self, termination: TerminationType, interfaces: List[Interface]):
|
|
152
|
+
self.data[termination] = interfaces
|
|
153
|
+
|
|
154
|
+
def get_termination(self, termination: Union[int, TerminationType]) -> TerminationType:
|
|
155
|
+
if isinstance(termination, int):
|
|
156
|
+
termination = self.terminations[termination]
|
|
157
|
+
return termination
|
|
158
|
+
|
|
159
|
+
def get_interfaces_for_termination_or_its_index(
|
|
160
|
+
self, termination_or_its_index: Union[int, TerminationType]
|
|
161
|
+
) -> List[Interface]:
|
|
162
|
+
termination = self.get_termination(termination_or_its_index)
|
|
163
|
+
return self.data[termination]
|
|
164
|
+
|
|
165
|
+
def get_interfaces_for_termination(
|
|
166
|
+
self,
|
|
167
|
+
termination_or_its_index: Union[int, TerminationType],
|
|
168
|
+
slice_or_index_or_indices: Union[int, slice, List[int], None] = None,
|
|
169
|
+
) -> List[Interface]:
|
|
170
|
+
interfaces = self.get_interfaces_for_termination_or_its_index(termination_or_its_index)
|
|
171
|
+
return array_utils.filter_by_slice_or_index_or_indices(interfaces, slice_or_index_or_indices)
|
|
172
|
+
|
|
173
|
+
def remove_duplicate_interfaces(self, strain_mode: StrainModes = StrainModes.mean_abs_strain):
|
|
174
|
+
for termination in self.terminations:
|
|
175
|
+
self.remove_duplicate_interfaces_for_termination(termination, strain_mode)
|
|
176
|
+
|
|
177
|
+
def remove_duplicate_interfaces_for_termination(
|
|
178
|
+
self, termination, strain_mode: StrainModes = StrainModes.mean_abs_strain
|
|
179
|
+
):
|
|
180
|
+
def are_interfaces_duplicate(interface1: Interface, interface2: Interface):
|
|
181
|
+
return interface1.num_sites == interface2.num_sites and np.allclose(
|
|
182
|
+
interface1.interface_properties[strain_mode], interface2.interface_properties[strain_mode]
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
sorted_interfaces = self.get_interfaces_for_termination_sorted_by_size(termination)
|
|
186
|
+
filtered_interfaces = [sorted_interfaces[0]] if sorted_interfaces else []
|
|
187
|
+
|
|
188
|
+
for interface in sorted_interfaces[1:]:
|
|
189
|
+
if not any(
|
|
190
|
+
are_interfaces_duplicate(interface, unique_interface) for unique_interface in filtered_interfaces
|
|
191
|
+
):
|
|
192
|
+
filtered_interfaces.append(interface)
|
|
193
|
+
|
|
194
|
+
self.set_interfaces_for_termination(termination, filtered_interfaces)
|
|
195
|
+
|
|
196
|
+
def get_interfaces_for_termination_sorted_by_strain(
|
|
197
|
+
self, termination: Union[int, TerminationType], strain_mode: StrainModes = StrainModes.mean_abs_strain
|
|
198
|
+
) -> List[Interface]:
|
|
199
|
+
return sorted(
|
|
200
|
+
self.get_interfaces_for_termination(termination),
|
|
201
|
+
key=lambda x: np.mean(np.abs(x.interface_properties[strain_mode])),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def get_interfaces_for_termination_sorted_by_size(
|
|
205
|
+
self, termination: Union[int, TerminationType]
|
|
206
|
+
) -> List[Interface]:
|
|
207
|
+
return sorted(
|
|
208
|
+
self.get_interfaces_for_termination(termination),
|
|
209
|
+
key=lambda x: x.num_sites,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def get_interfaces_for_termination_sorted_by_strain_and_size(
|
|
213
|
+
self, termination: Union[int, TerminationType], strain_mode: StrainModes = StrainModes.mean_abs_strain
|
|
214
|
+
) -> List[Interface]:
|
|
215
|
+
return sorted(
|
|
216
|
+
self.get_interfaces_for_termination_sorted_by_strain(termination, strain_mode),
|
|
217
|
+
key=lambda x: x.num_sites,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def sort_interfaces_for_all_terminations_by_strain_and_size(self):
|
|
221
|
+
for termination in self.terminations:
|
|
222
|
+
self.set_interfaces_for_termination(
|
|
223
|
+
termination, self.get_interfaces_for_termination_sorted_by_strain_and_size(termination)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def get_all_interfaces(self) -> List[Interface]:
|
|
227
|
+
return functools.reduce(lambda a, b: a + b, self.data.values())
|
|
228
|
+
|
|
229
|
+
def get_interfaces_as_materials(
|
|
230
|
+
self, termination: Union[int, TerminationType], slice_range_or_index: Union[int, slice]
|
|
231
|
+
) -> List[Interface]:
|
|
232
|
+
return list(
|
|
233
|
+
map(
|
|
234
|
+
convert_atoms_or_structure_to_material,
|
|
235
|
+
self.get_interfaces_for_termination(termination, slice_range_or_index),
|
|
236
|
+
)
|
|
237
|
+
)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from ase import Atoms
|
|
2
2
|
from ase.calculators.calculator import Calculator
|
|
3
3
|
|
|
4
|
-
from .
|
|
4
|
+
from .analyze import get_surface_area
|
|
5
|
+
from .convert import decorator_convert_material_args_kwargs_to_atoms
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
@
|
|
8
|
+
@decorator_convert_material_args_kwargs_to_atoms
|
|
8
9
|
def calculate_total_energy(atoms: Atoms, calculator: Calculator):
|
|
9
10
|
"""
|
|
10
11
|
Set calculator for ASE Atoms and calculate the total energy.
|
|
@@ -18,3 +19,94 @@ def calculate_total_energy(atoms: Atoms, calculator: Calculator):
|
|
|
18
19
|
"""
|
|
19
20
|
atoms.set_calculator(calculator)
|
|
20
21
|
return atoms.get_total_energy()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@decorator_convert_material_args_kwargs_to_atoms
|
|
25
|
+
def calculate_total_energy_per_atom(atoms: Atoms, calculator: Calculator):
|
|
26
|
+
"""
|
|
27
|
+
Set calculator for ASE Atoms and calculate the total energy per atom.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
atoms (ase.Atoms): The Atoms object to calculate the energy of.
|
|
31
|
+
calculator (ase.calculators.calculator.Calculator): The calculator to use for the energy calculation.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
float: The energy per atom of the atoms.
|
|
35
|
+
"""
|
|
36
|
+
return calculate_total_energy(atoms, calculator) / atoms.get_global_number_of_atoms()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@decorator_convert_material_args_kwargs_to_atoms
|
|
40
|
+
def calculate_surface_energy(slab: Atoms, bulk: Atoms, calculator: Calculator):
|
|
41
|
+
"""
|
|
42
|
+
Calculate the surface energy by subtracting the weighted bulk energy from the slab energy.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
slab (ase.Atoms): The slab Atoms object to calculate the surface energy of.
|
|
46
|
+
bulk (ase.Atoms): The bulk Atoms object to calculate the surface energy of.
|
|
47
|
+
calculator (ase.calculators.calculator.Calculator): The calculator to use for the energy calculation.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
float: The surface energy of the slab.
|
|
51
|
+
"""
|
|
52
|
+
number_of_atoms = slab.get_global_number_of_atoms()
|
|
53
|
+
area = get_surface_area(slab)
|
|
54
|
+
return (
|
|
55
|
+
calculate_total_energy(slab, calculator) - calculate_total_energy_per_atom(bulk, calculator) * number_of_atoms
|
|
56
|
+
) / (2 * area)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@decorator_convert_material_args_kwargs_to_atoms
|
|
60
|
+
def calculate_adhesion_energy(interface: Atoms, substrate_slab: Atoms, layer_slab: Atoms, calculator: Calculator):
|
|
61
|
+
"""
|
|
62
|
+
Calculate the adhesion energy.
|
|
63
|
+
The adhesion energy is the difference between the energy of the interface and
|
|
64
|
+
the sum of the energies of the substrate and layer.
|
|
65
|
+
According to: 10.1088/0953-8984/27/30/305004
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
interface (ase.Atoms): The interface Atoms object to calculate the adhesion energy of.
|
|
69
|
+
substrate_slab (ase.Atoms): The substrate slab Atoms object to calculate the adhesion energy of.
|
|
70
|
+
layer_slab (ase.Atoms): The layer slab Atoms object to calculate the adhesion energy of.
|
|
71
|
+
calculator (ase.calculators.calculator.Calculator): The calculator to use for the energy calculation.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
float: The adhesion energy of the interface.
|
|
75
|
+
"""
|
|
76
|
+
energy_substrate_slab = calculate_total_energy(substrate_slab, calculator)
|
|
77
|
+
energy_layer_slab = calculate_total_energy(layer_slab, calculator)
|
|
78
|
+
energy_interface = calculate_total_energy(interface, calculator)
|
|
79
|
+
area = get_surface_area(interface)
|
|
80
|
+
return (energy_substrate_slab + energy_layer_slab - energy_interface) / area
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@decorator_convert_material_args_kwargs_to_atoms
|
|
84
|
+
def calculate_interfacial_energy(
|
|
85
|
+
interface: Atoms,
|
|
86
|
+
substrate_slab: Atoms,
|
|
87
|
+
substrate_bulk: Atoms,
|
|
88
|
+
layer_slab: Atoms,
|
|
89
|
+
layer_bulk: Atoms,
|
|
90
|
+
calculator: Calculator,
|
|
91
|
+
):
|
|
92
|
+
"""
|
|
93
|
+
Calculate the interfacial energy.
|
|
94
|
+
The interfacial energy is the sum of the surface energies of the substrate and layer minus the adhesion energy.
|
|
95
|
+
According to Dupré's formula
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
interface (ase.Atoms): The interface Atoms object to calculate the interfacial energy of.
|
|
99
|
+
substrate_slab (ase.Atoms): The substrate slab Atoms object to calculate the interfacial energy of.
|
|
100
|
+
substrate_bulk (ase.Atoms): The substrate bulk Atoms object to calculate the interfacial energy of.
|
|
101
|
+
layer_slab (ase.Atoms): The layer slab Atoms object to calculate the interfacial energy of.
|
|
102
|
+
layer_bulk (ase.Atoms): The layer bulk Atoms object to calculate the interfacial energy of.
|
|
103
|
+
calculator (ase.calculators.calculator.Calculator): The calculator to use for the energy calculation.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
float: The interfacial energy of the interface.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
surface_energy_substrate = calculate_surface_energy(substrate_slab, substrate_bulk, calculator)
|
|
110
|
+
surface_energy_layer = calculate_surface_energy(layer_slab, layer_bulk, calculator)
|
|
111
|
+
adhesion_energy = calculate_adhesion_energy(interface, substrate_slab, layer_slab, calculator)
|
|
112
|
+
return surface_energy_layer + surface_energy_substrate - adhesion_energy
|
|
@@ -167,7 +167,7 @@ def from_ase(ase_atoms: Atoms) -> Dict[str, Any]:
|
|
|
167
167
|
return from_pymatgen(structure)
|
|
168
168
|
|
|
169
169
|
|
|
170
|
-
def
|
|
170
|
+
def decorator_convert_material_args_kwargs_to_atoms(func: Callable) -> Callable:
|
|
171
171
|
"""
|
|
172
172
|
Decorator that converts ESSE Material objects to ASE Atoms objects.
|
|
173
173
|
"""
|
|
@@ -184,3 +184,30 @@ def convert_material_args_kwargs_to_atoms(func: Callable) -> Callable:
|
|
|
184
184
|
return func(*new_args, **new_kwargs)
|
|
185
185
|
|
|
186
186
|
return wrapper
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def decorator_convert_material_args_kwargs_to_structure(func: Callable) -> Callable:
|
|
190
|
+
"""
|
|
191
|
+
Decorator that converts ESSE Material objects to pymatgen Structure objects.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
@wraps(func)
|
|
195
|
+
def wrapper(*args, **kwargs):
|
|
196
|
+
# Convert args if they are of type ESSE Material
|
|
197
|
+
new_args = [to_pymatgen(arg) if isinstance(arg, Material) else arg for arg in args]
|
|
198
|
+
|
|
199
|
+
# Convert kwargs if they are of type ESSE Material
|
|
200
|
+
new_kwargs = {k: to_pymatgen(v) if isinstance(v, Material) else v for k, v in kwargs.items()}
|
|
201
|
+
|
|
202
|
+
# Call the original function with the converted arguments
|
|
203
|
+
return func(*new_args, **new_kwargs)
|
|
204
|
+
|
|
205
|
+
return wrapper
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def convert_atoms_or_structure_to_material(item):
|
|
209
|
+
if isinstance(item, Structure):
|
|
210
|
+
return from_pymatgen(item)
|
|
211
|
+
elif isinstance(item, Atoms):
|
|
212
|
+
return from_ase(item)
|
|
213
|
+
return item
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
from typing import Union
|
|
2
2
|
|
|
3
3
|
from ase import Atoms
|
|
4
|
+
from pymatgen.analysis.structure_analyzer import SpacegroupAnalyzer
|
|
5
|
+
from pymatgen.core.structure import Structure
|
|
4
6
|
|
|
5
|
-
from .convert import
|
|
7
|
+
from .convert import (
|
|
8
|
+
decorator_convert_material_args_kwargs_to_atoms,
|
|
9
|
+
decorator_convert_material_args_kwargs_to_structure,
|
|
10
|
+
)
|
|
11
|
+
from .utils import translate_to_bottom_pymatgen_structure
|
|
6
12
|
|
|
7
13
|
|
|
8
|
-
@
|
|
14
|
+
@decorator_convert_material_args_kwargs_to_atoms
|
|
9
15
|
def filter_by_label(atoms: Atoms, label: Union[int, str]):
|
|
10
16
|
"""
|
|
11
17
|
Filter out only atoms corresponding to the label/tag.
|
|
@@ -18,3 +24,35 @@ def filter_by_label(atoms: Atoms, label: Union[int, str]):
|
|
|
18
24
|
ase.Atoms: The filtered Atoms object.
|
|
19
25
|
"""
|
|
20
26
|
return atoms[atoms.get_tags() == label]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@decorator_convert_material_args_kwargs_to_structure
|
|
30
|
+
def translate_to_bottom(structure: Structure, use_conventional_cell: bool = True):
|
|
31
|
+
"""
|
|
32
|
+
Translate atoms to the bottom of the cell (vacuum on top) to allow for the correct consecutive interface generation.
|
|
33
|
+
If use_conventional_cell is passed, conventional cell is used.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
structure (Structure): The pymatgen Structure object to normalize.
|
|
37
|
+
use_conventional_cell: Whether to convert to the conventional cell.
|
|
38
|
+
Returns:
|
|
39
|
+
Structure: The normalized pymatgen Structure object.
|
|
40
|
+
"""
|
|
41
|
+
if use_conventional_cell:
|
|
42
|
+
structure = SpacegroupAnalyzer(structure).get_conventional_standard_structure()
|
|
43
|
+
structure = translate_to_bottom_pymatgen_structure(structure)
|
|
44
|
+
return structure
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@decorator_convert_material_args_kwargs_to_structure
|
|
48
|
+
def wrap_to_unit_cell(structure: Structure):
|
|
49
|
+
"""
|
|
50
|
+
Wrap atoms to the cell
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
structure (Structure): The pymatgen Structure object to normalize.
|
|
54
|
+
Returns:
|
|
55
|
+
Structure: The wrapped pymatgen Structure object.
|
|
56
|
+
"""
|
|
57
|
+
structure.make_supercell((1, 1, 1), to_unit_cell=True)
|
|
58
|
+
return structure
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from pymatgen.core.structure import Structure
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# TODO: convert to accept ASE Atoms object
|
|
5
|
+
def translate_to_bottom_pymatgen_structure(structure: Structure):
|
|
6
|
+
"""
|
|
7
|
+
Translate the structure to the bottom of the cell.
|
|
8
|
+
Args:
|
|
9
|
+
structure (Structure): The pymatgen Structure object to translate.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
Structure: The translated pymatgen Structure object.
|
|
13
|
+
"""
|
|
14
|
+
min_c = min(site.c for site in structure)
|
|
15
|
+
translation_vector = [0, 0, -min_c]
|
|
16
|
+
translated_structure = structure.copy()
|
|
17
|
+
for site in translated_structure:
|
|
18
|
+
site.coords += translation_vector
|
|
19
|
+
return translated_structure
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
|
|
3
|
+
from ase.build import bulk
|
|
4
|
+
from mat3ra.made.material import Material
|
|
5
|
+
from mat3ra.made.tools.build.interface import interface_patch_with_mean_abs_strain
|
|
6
|
+
from mat3ra.made.tools.convert import from_ase
|
|
7
|
+
from pymatgen.core.interface import Interface
|
|
8
|
+
|
|
9
|
+
from .utils import atoms_to_interface_structure
|
|
10
|
+
|
|
11
|
+
# ASE Atoms fixtures
|
|
12
|
+
substrate = bulk("Si", cubic=True)
|
|
13
|
+
film = bulk("Cu", cubic=True)
|
|
14
|
+
INTERFACE_ATOMS = substrate + film
|
|
15
|
+
INTERFACE_ATOMS.set_tags([1] * len(substrate) + [2] * len(film))
|
|
16
|
+
|
|
17
|
+
# Material fixtures
|
|
18
|
+
SUBSTRATE_MATERIAL = Material(from_ase(substrate))
|
|
19
|
+
LAYER_MATERIAL = Material(from_ase(film))
|
|
20
|
+
|
|
21
|
+
# Pymatgen Interface fixtures
|
|
22
|
+
INTERFACE_TERMINATION: Tuple = ("Si_termination", "Cu_termination")
|
|
23
|
+
|
|
24
|
+
interface_structure = atoms_to_interface_structure(INTERFACE_ATOMS)
|
|
25
|
+
dict = interface_structure.as_dict()
|
|
26
|
+
|
|
27
|
+
INTERFACE_STRUCTURE = Interface.from_dict(dict)
|
|
28
|
+
# Add properties that are assigned during interface creation in ZSL algorithm
|
|
29
|
+
INTERFACE_STRUCTURE.interface_properties["termination"] = INTERFACE_TERMINATION
|
|
30
|
+
INTERFACE_STRUCTURE.interface_properties["strain"] = 0.1
|
|
31
|
+
INTERFACE_STRUCTURE = interface_patch_with_mean_abs_strain(INTERFACE_STRUCTURE)
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
from ase.build import bulk
|
|
3
|
-
from mat3ra.made.tools.analyze import
|
|
3
|
+
from mat3ra.made.tools.analyze import get_average_interlayer_distance, get_surface_area
|
|
4
|
+
|
|
5
|
+
from .fixtures import INTERFACE_ATOMS
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
def test_calculate_average_interlayer_distance():
|
|
7
|
-
|
|
8
|
-
film = bulk("Cu", cubic=True)
|
|
9
|
-
interface_atoms = substrate + film
|
|
10
|
-
interface_atoms.set_tags([1] * len(substrate) + [2] * len(film))
|
|
11
|
-
distance = calculate_average_interlayer_distance(interface_atoms, 1, 2)
|
|
9
|
+
distance = get_average_interlayer_distance(INTERFACE_ATOMS, 1, 2)
|
|
12
10
|
assert np.isclose(distance, 4.0725)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_calculate_surface_area():
|
|
14
|
+
atoms = bulk("Si", cubic=False)
|
|
15
|
+
area = get_surface_area(atoms)
|
|
16
|
+
assert np.isclose(area, 12.7673)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
|
|
3
|
+
from mat3ra.made.tools.build import create_interfaces
|
|
4
|
+
from mat3ra.made.tools.build.interface import InterfaceSettings
|
|
5
|
+
|
|
6
|
+
from .fixtures import LAYER_MATERIAL, SUBSTRATE_MATERIAL
|
|
7
|
+
|
|
8
|
+
MAX_AREA = 200
|
|
9
|
+
# pymatgen `2023.6.23` supporting py3.8 returns 1 interface instead of 2
|
|
10
|
+
EXPECTED_NUMBER_OF_INTERFACES = 1 if platform.python_version().startswith("3.8") else 2
|
|
11
|
+
settings = InterfaceSettings(
|
|
12
|
+
USE_CONVENTIONAL_CELL=True,
|
|
13
|
+
INTERFACE_PARAMETERS={"DISTANCE_Z": 3.0, "MAX_AREA": MAX_AREA},
|
|
14
|
+
ZSL_PARAMETERS={"MAX_AREA": MAX_AREA, "MAX_AREA_TOL": 0.09, "MAX_LENGTH_TOL": 0.03, "MAX_ANGLE_TOL": 0.01},
|
|
15
|
+
SUBSTRATE_PARAMETERS={"MILLER_INDICES": (1, 1, 1), "THICKNESS": 3},
|
|
16
|
+
LAYER_PARAMETERS={"MILLER_INDICES": (0, 0, 1), "THICKNESS": 1},
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_create_interfaces():
|
|
21
|
+
interfaces = create_interfaces(substrate=SUBSTRATE_MATERIAL, layer=LAYER_MATERIAL, settings=settings)
|
|
22
|
+
assert len(interfaces.get_interfaces_for_termination(0)) == EXPECTED_NUMBER_OF_INTERFACES
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from mat3ra.made.tools.build.interface import InterfaceDataHolder
|
|
2
|
+
|
|
3
|
+
from .fixtures import INTERFACE_STRUCTURE, INTERFACE_TERMINATION
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_add_data_entries():
|
|
7
|
+
interfaces_data = InterfaceDataHolder()
|
|
8
|
+
interfaces_data.add_data_entries(INTERFACE_STRUCTURE)
|
|
9
|
+
assert len(interfaces_data.get_interfaces_for_termination(0)) == 1
|
|
10
|
+
assert len(interfaces_data.get_interfaces_for_termination(INTERFACE_TERMINATION)) == 1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_get_interfaces_for_termination():
|
|
14
|
+
interfaces_data = InterfaceDataHolder()
|
|
15
|
+
interfaces_data.add_data_entries([INTERFACE_STRUCTURE])
|
|
16
|
+
assert interfaces_data.get_interfaces_for_termination(0)[0] == INTERFACE_STRUCTURE
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_remove_duplicate_interfaces():
|
|
20
|
+
interfaces_data = InterfaceDataHolder()
|
|
21
|
+
interfaces_data.add_data_entries([INTERFACE_STRUCTURE, INTERFACE_STRUCTURE], remove_duplicates=False)
|
|
22
|
+
assert len(interfaces_data.get_interfaces_for_termination(INTERFACE_TERMINATION)) == 2
|
|
23
|
+
interfaces_data.remove_duplicate_interfaces()
|
|
24
|
+
assert len(interfaces_data.get_interfaces_for_termination(INTERFACE_TERMINATION)) == 1
|
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
-
from ase.build import bulk
|
|
2
|
+
from ase.build import add_adsorbate, bulk, fcc111, graphene, surface
|
|
3
3
|
from ase.calculators import emt
|
|
4
|
-
from mat3ra.made.tools.calculate import
|
|
4
|
+
from mat3ra.made.tools.calculate import (
|
|
5
|
+
calculate_adhesion_energy,
|
|
6
|
+
calculate_interfacial_energy,
|
|
7
|
+
calculate_surface_energy,
|
|
8
|
+
calculate_total_energy,
|
|
9
|
+
calculate_total_energy_per_atom,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# Interface and its constituents structures setup
|
|
13
|
+
nickel_slab = fcc111("Ni", size=(2, 2, 3), vacuum=10, a=3.52)
|
|
14
|
+
graphene_layer = graphene(size=(1, 1, 1), vacuum=10)
|
|
15
|
+
graphene_layer.cell = nickel_slab.cell
|
|
16
|
+
interface = nickel_slab.copy()
|
|
17
|
+
add_adsorbate(interface, graphene_layer, height=2, position="ontop")
|
|
18
|
+
|
|
19
|
+
# Assign calculators
|
|
20
|
+
calculator = emt.EMT()
|
|
21
|
+
nickel_slab.set_calculator(calculator)
|
|
22
|
+
graphene_layer.set_calculator(calculator)
|
|
23
|
+
interface.set_calculator(calculator)
|
|
24
|
+
|
|
25
|
+
nickel_bulk = bulk("Ni", "fcc", a=3.52)
|
|
26
|
+
graphene_bulk = graphene_layer
|
|
5
27
|
|
|
6
28
|
|
|
7
29
|
def test_calculate_total_energy():
|
|
@@ -9,3 +31,34 @@ def test_calculate_total_energy():
|
|
|
9
31
|
calculator = emt.EMT()
|
|
10
32
|
energy = calculate_total_energy(atoms, calculator)
|
|
11
33
|
assert np.isclose(energy, 1.3612647524769237)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_calculate_total_energy_per_atom():
|
|
37
|
+
atoms = bulk("C", cubic=True)
|
|
38
|
+
calculator = emt.EMT()
|
|
39
|
+
print(atoms.get_global_number_of_atoms())
|
|
40
|
+
energy_per_atom = calculate_total_energy_per_atom(atoms, calculator)
|
|
41
|
+
assert np.isclose(energy_per_atom, 0.1701580940596)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_calculate_surface_energy():
|
|
45
|
+
atoms_slab = surface("C", (1, 1, 1), 3, vacuum=10)
|
|
46
|
+
atoms_bulk = bulk("C", cubic=True)
|
|
47
|
+
calculator = emt.EMT()
|
|
48
|
+
surface_energy = calculate_surface_energy(atoms_slab, atoms_bulk, calculator)
|
|
49
|
+
assert np.isclose(surface_energy, 0.148845)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_calculate_adhesion_energy():
|
|
53
|
+
adhesion_energy = calculate_adhesion_energy(interface, nickel_slab, graphene_layer, calculator)
|
|
54
|
+
assert np.isclose(adhesion_energy, 0.07345)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_calculate_interfacial_energy():
|
|
58
|
+
interfacial_energy = calculate_interfacial_energy(
|
|
59
|
+
interface, nickel_slab, nickel_bulk, graphene_layer, graphene_bulk, calculator
|
|
60
|
+
)
|
|
61
|
+
assert np.isclose(
|
|
62
|
+
interfacial_energy,
|
|
63
|
+
0.030331590159230523,
|
|
64
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from pymatgen.core.structure import Structure
|
|
4
|
+
from pymatgen.io.ase import AseAtomsAdaptor
|
|
5
|
+
|
|
6
|
+
ATOMS_TAGS_TO_INTERFACE_STRUCTURE_LABELS: Dict = {1: "substrate", 2: "film"}
|
|
7
|
+
INTERFACE_STRUCTURE_LABELS_TO_ATOMS_TAGS: Dict = {v: k for k, v in ATOMS_TAGS_TO_INTERFACE_STRUCTURE_LABELS.items()}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def atoms_to_interface_structure(atoms) -> Structure:
|
|
11
|
+
"""
|
|
12
|
+
Converts ASE Atoms object to pymatgen Interface object.
|
|
13
|
+
Args:
|
|
14
|
+
atoms (Atoms): The ASE Atoms object.
|
|
15
|
+
Returns:
|
|
16
|
+
Interface: The pymatgen Interface object.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
adaptor = AseAtomsAdaptor()
|
|
20
|
+
interface_structure = adaptor.get_structure(atoms)
|
|
21
|
+
interface_structure.add_site_property(
|
|
22
|
+
"interface_label",
|
|
23
|
+
[ATOMS_TAGS_TO_INTERFACE_STRUCTURE_LABELS[tag] for tag in interface_structure.site_properties["tags"]],
|
|
24
|
+
)
|
|
25
|
+
return interface_structure
|