@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mat3ra/made",
3
- "version": "2024.4.16-0",
3
+ "version": "2024.5.3-0",
4
4
  "description": "MAterials DEsign library",
5
5
  "scripts": {
6
6
  "lint": "eslint --cache src/js tests/js && prettier --write src/js tests/js",
package/pyproject.toml CHANGED
@@ -17,6 +17,7 @@ classifiers = [
17
17
  dependencies = [
18
18
  # add requirements here
19
19
  "numpy",
20
+ "mat3ra-utils",
20
21
  "mat3ra-esse",
21
22
  "mat3ra-code",
22
23
  ]
@@ -1,11 +1,11 @@
1
1
  import numpy as np
2
2
  from ase import Atoms
3
3
 
4
- from .convert import convert_material_args_kwargs_to_atoms
4
+ from .convert import decorator_convert_material_args_kwargs_to_atoms
5
5
 
6
6
 
7
- @convert_material_args_kwargs_to_atoms
8
- def calculate_average_interlayer_distance(
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 .convert import convert_material_args_kwargs_to_atoms
4
+ from .analyze import get_surface_area
5
+ from .convert import decorator_convert_material_args_kwargs_to_atoms
5
6
 
6
7
 
7
- @convert_material_args_kwargs_to_atoms
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 convert_material_args_kwargs_to_atoms(func: Callable) -> Callable:
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 convert_material_args_kwargs_to_atoms
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
- @convert_material_args_kwargs_to_atoms
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 calculate_average_interlayer_distance
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
- substrate = bulk("Si", cubic=True)
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 calculate_total_energy
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