@mat3ra/made 2025.8.8-0 → 2025.8.9-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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/src/py/mat3ra/made/tools/analyze/interface/simple.py +86 -2
  3. package/src/py/mat3ra/made/tools/analyze/other.py +1 -1
  4. package/src/py/mat3ra/made/tools/build/compound_pristine_structures/two_dimensional/heterostructure/__init__.py +7 -0
  5. package/src/py/mat3ra/made/tools/build/compound_pristine_structures/two_dimensional/heterostructure/helpers.py +101 -0
  6. package/src/py/mat3ra/made/tools/build/compound_pristine_structures/two_dimensional/heterostructure/types.py +26 -0
  7. package/src/py/mat3ra/made/tools/build/defective_structures/two_dimensional/adatom/helpers.py +15 -15
  8. package/src/py/mat3ra/made/tools/build/defective_structures/two_dimensional/adatom/types.py +22 -0
  9. package/src/py/mat3ra/made/tools/build/defective_structures/zero_dimensional/point_defect/helpers.py +15 -18
  10. package/src/py/mat3ra/made/tools/build/defective_structures/zero_dimensional/point_defect/types.py +24 -0
  11. package/src/py/mat3ra/made/tools/build/pristine_structures/two_dimensional/nanoribbon/__init__.py +2 -2
  12. package/src/py/mat3ra/made/tools/build/pristine_structures/two_dimensional/nanoribbon/helpers.py +2 -2
  13. package/src/py/mat3ra/made/tools/build/pristine_structures/two_dimensional/nanotape/builders.py +3 -3
  14. package/src/py/mat3ra/made/tools/build/pristine_structures/two_dimensional/nanotape/helpers.py +2 -2
  15. package/src/py/mat3ra/made/tools/build/pristine_structures/two_dimensional/slab/builder.py +1 -1
  16. package/src/py/mat3ra/made/tools/build/pristine_structures/two_dimensional/slab/configuration.py +1 -1
  17. package/src/py/mat3ra/made/tools/build/pristine_structures/two_dimensional/slab/utils.py +13 -1
  18. package/src/py/mat3ra/made/tools/build_components/__init__.py +0 -1
  19. package/src/py/mat3ra/made/tools/build_components/entities/reusable/one_dimensional/crystal_lattice_lines/builder.py +1 -1
  20. package/src/py/mat3ra/made/tools/build_components/entities/reusable/one_dimensional/crystal_lattice_lines/edge_types.py +4 -4
  21. package/src/py/mat3ra/made/tools/build_components/entities/reusable/one_dimensional/crystal_lattice_lines/helpers.py +3 -3
  22. package/src/py/mat3ra/made/tools/build_components/operations/core/combinations/stack/configuration.py +0 -1
  23. package/src/py/mat3ra/made/tools/build_components/utils.py +2 -57
  24. package/src/py/mat3ra/made/tools/entities/coordinate/box_coordinate_condition.py +2 -1
  25. package/src/py/mat3ra/made/tools/entities/coordinate/cylinder_coordinate_condition.py +2 -1
  26. package/src/py/mat3ra/made/tools/entities/coordinate/plane_coordinate_condition.py +3 -1
  27. package/src/py/mat3ra/made/tools/entities/coordinate/sphere_coordinate_condition.py +2 -1
  28. package/src/py/mat3ra/made/tools/entities/coordinate/triangular_prism_coordinate_condition.py +3 -1
  29. package/src/py/mat3ra/made/tools/helpers.py +24 -0
  30. package/src/py/mat3ra/made/tools/modify.py +1 -1
  31. package/src/py/mat3ra/made/tools/operations/core/unary.py +1 -1
  32. package/tests/py/unit/fixtures/bulk.py +5 -0
  33. package/tests/py/unit/test_tools_analyze_interface.py +53 -1
  34. package/tests/py/unit/test_tools_analyze_interface_zsl.py +3 -4
  35. package/tests/py/unit/test_tools_build_defect/test_adatom.py +11 -4
  36. package/tests/py/unit/test_tools_build_defect/test_point_defect.py +4 -1
  37. package/tests/py/unit/test_tools_build_heterostructure.py +48 -0
  38. package/tests/py/unit/test_tools_build_interface.py +2 -3
  39. package/tests/py/unit/test_tools_build_interface_zsl.py +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mat3ra/made",
3
- "version": "2025.8.8-0",
3
+ "version": "2025.8.9-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",
@@ -1,5 +1,5 @@
1
1
  from functools import cached_property
2
- from typing import Optional
2
+ from typing import Optional, Tuple
3
3
 
4
4
  import numpy as np
5
5
  from mat3ra.code.entity import InMemoryEntityPydantic
@@ -14,7 +14,10 @@ from mat3ra.made.tools.build.pristine_structures.two_dimensional.slab.configurat
14
14
  from ...build.pristine_structures.two_dimensional.slab_strained_supercell.configuration import (
15
15
  SlabStrainedSupercellConfiguration,
16
16
  )
17
+ from ...operations.core.unary import supercell
18
+ from ...utils import unwrap
17
19
  from ..interface.utils.holders import MatchedSubstrateFilmConfigurationHolder
20
+ from ..utils import calculate_von_mises_strain
18
21
 
19
22
 
20
23
  class InterfaceAnalyzer(InMemoryEntityPydantic):
@@ -30,6 +33,7 @@ class InterfaceAnalyzer(InMemoryEntityPydantic):
30
33
  film_slab_configuration: SlabConfiguration
31
34
  substrate_build_parameters: Optional[SlabBuilderParameters] = None
32
35
  film_build_parameters: Optional[SlabBuilderParameters] = None
36
+ optimize_film_supercell: bool = False
33
37
 
34
38
  def get_component_material(self, configuration: SlabConfiguration):
35
39
  return SlabBuilder().get_material(configuration)
@@ -87,6 +91,83 @@ class InterfaceAnalyzer(InMemoryEntityPydantic):
87
91
  def get_substrate_strain_matrix(self) -> Matrix3x3Schema:
88
92
  return self._no_strain_matrix
89
93
 
94
+ def _calculate_strain_for_film_supercell(self, film_n: int, film_m: int) -> float:
95
+ """
96
+ Calculate strain for given film supercell configuration against unchanged substrate.
97
+
98
+ Args:
99
+ film_n: Film supercell multiplier in a direction
100
+ film_m: Film supercell multiplier in b direction
101
+
102
+ Returns:
103
+ Von Mises strain percentage
104
+ """
105
+
106
+ # Apply supercell to film material
107
+ supercell_matrix = [[film_n, 0], [0, film_m]]
108
+ film_with_supercell = supercell(self.film_material, supercell_matrix)
109
+
110
+ # Use existing get_film_strain_matrix with supercelled film
111
+ strain_matrix = self.get_film_strain_matrix(
112
+ self.substrate_material.lattice.vector_arrays, film_with_supercell.lattice.vector_arrays
113
+ )
114
+
115
+ return calculate_von_mises_strain(np.array(unwrap(strain_matrix.root)))
116
+
117
+ def _find_optimal_supercell_factor_for_direction(self, substrate_length: float, film_length: float) -> int:
118
+ """
119
+ Finds a multiplier for the component of diagonal supercell matrix
120
+ that will get film supercell lattice vector length around substrate length.
121
+ The N leads to the film supercell vector length being shorter than substrate length,
122
+ and N+1 leads to the film supercell vector length being longer than substrate length.
123
+ """
124
+
125
+ # Find optimal: when n*film_length < substrate_length < (n+1)*film_length
126
+ optimal = max(1, int(substrate_length / film_length))
127
+ if (optimal + 1) * film_length - substrate_length < substrate_length - optimal * film_length:
128
+ optimal += 1
129
+
130
+ return optimal
131
+
132
+ def find_optimal_film_supercell(self) -> Tuple[int, int]:
133
+ """
134
+ Find optimal (n, m) supercell multipliers for film that minimize strain.
135
+
136
+ Returns:
137
+ Tuple of (n, m) supercell multipliers that minimize strain
138
+ """
139
+ substrate_vectors = np.array(self.substrate_material.lattice.vector_arrays[:2])
140
+ film_vectors = np.array(self.film_material.lattice.vector_arrays[:2])
141
+
142
+ substrate_lengths = [np.linalg.norm(substrate_vectors[i, :2]) for i in range(2)]
143
+ film_lengths = [np.linalg.norm(film_vectors[i, :2]) for i in range(2)]
144
+
145
+ optimal_values = []
146
+ for substrate_length, film_length in zip(substrate_lengths, film_lengths):
147
+ optimal = self._find_optimal_supercell_factor_for_direction(substrate_length, film_length)
148
+ optimal_values.append(optimal)
149
+
150
+ n_optimal, m_optimal = optimal_values
151
+
152
+ # Test neighboring values to find minimum strain
153
+ candidates = [
154
+ (n_optimal, m_optimal),
155
+ (n_optimal + 1, m_optimal),
156
+ (n_optimal, m_optimal + 1),
157
+ (n_optimal + 1, m_optimal + 1),
158
+ ]
159
+
160
+ min_strain = float("inf")
161
+ best_n, best_m = n_optimal, m_optimal
162
+
163
+ for n, m in candidates:
164
+ strain = self._calculate_strain_for_film_supercell(n, m)
165
+ if strain < min_strain:
166
+ min_strain = strain
167
+ best_n, best_m = n, m
168
+
169
+ return best_n, best_m
170
+
90
171
  def get_component_strained_configuration(
91
172
  self,
92
173
  configuration: SlabConfiguration,
@@ -146,7 +227,10 @@ class InterfaceAnalyzer(InMemoryEntityPydantic):
146
227
 
147
228
  @property
148
229
  def film_supercell_matrix(self) -> SupercellMatrix2DSchema:
149
- if self.film_build_parameters and self.film_build_parameters.xy_supercell_matrix:
230
+ if self.optimize_film_supercell:
231
+ n, m = self.find_optimal_film_supercell()
232
+ return SupercellMatrix2DSchema(root=[[n, 0], [0, m]])
233
+ elif self.film_build_parameters and self.film_build_parameters.xy_supercell_matrix:
150
234
  return SupercellMatrix2DSchema(root=self.film_build_parameters.xy_supercell_matrix)
151
235
  return self.identity_supercell
152
236
 
@@ -2,9 +2,9 @@ from typing import Callable, List, Literal, Optional
2
2
 
3
3
  import numpy as np
4
4
  from mat3ra.made.material import Material
5
- from mat3ra.made.tools.build.processed_structures.two_dimensional.passivation.enums import SurfaceTypesEnum
6
5
  from scipy.spatial import cKDTree
7
6
 
7
+ from ..build.processed_structures.two_dimensional.passivation.enums import SurfaceTypesEnum
8
8
  from ..convert import decorator_convert_material_args_kwargs_to_atoms, to_pymatgen
9
9
  from ..third_party import ASEAtoms, PymatgenIStructure
10
10
  from ..utils import decorator_convert_position_to_coordinate
@@ -0,0 +1,7 @@
1
+ from .helpers import create_heterostructure
2
+ from .types import StackComponentDict
3
+
4
+ __all__ = [
5
+ "create_heterostructure",
6
+ "StackComponentDict",
7
+ ]
@@ -0,0 +1,101 @@
1
+ from typing import List
2
+
3
+ from mat3ra.esse.models.core.reusable.axis_enum import AxisEnum
4
+
5
+ from .types import StackComponentDict
6
+ from mat3ra.made.utils import adjust_material_cell_to_set_gap_along_direction
7
+ from ....pristine_structures.two_dimensional.slab.helpers import create_slab
8
+ from ....pristine_structures.two_dimensional.slab_strained_supercell.builder import SlabStrainedSupercellBuilder
9
+ from .....analyze import BaseMaterialAnalyzer
10
+ from .....analyze.interface import InterfaceAnalyzer
11
+ from .....analyze.slab import SlabMaterialAnalyzer
12
+ from .....build_components import MaterialWithBuildMetadata
13
+ from .....operations.core.binary import stack
14
+
15
+
16
+ def create_heterostructure(
17
+ stack_component_dicts: List[StackComponentDict],
18
+ gaps: List[float],
19
+ vacuum: float = 10.0,
20
+ use_conventional_cell: bool = True,
21
+ optimize_layer_supercells: bool = True,
22
+ ) -> MaterialWithBuildMetadata:
23
+ """
24
+ Create a heterostructure by stacking multiple slabs, while applying strain to each slab relative to the first slab.
25
+
26
+ Args:
27
+ stack_component_dicts: List of validated stack component configurations
28
+ gaps: List of gaps between adjacent slabs (in Angstroms)
29
+ vacuum: Size of vacuum layer in Angstroms
30
+ use_conventional_cell: Whether to use conventional cell
31
+ optimize_layer_supercells: Whether to find optimal supercells for strained layers
32
+
33
+ Returns:
34
+ Heterostructure material with stacked strained slabs
35
+ """
36
+ if len(stack_component_dicts) < 2:
37
+ raise ValueError("At least 2 stack components are required for a heterostructure")
38
+
39
+ if len(gaps) != len(stack_component_dicts) - 1:
40
+ raise ValueError("Number of gaps must be one less than number of stack components")
41
+
42
+ slabs = []
43
+ for i, component in enumerate(stack_component_dicts):
44
+ slab = create_slab(
45
+ crystal=component.crystal,
46
+ miller_indices=component.miller_indices,
47
+ number_of_layers=component.thickness,
48
+ vacuum=0.0 if i < len(stack_component_dicts) - 1 else vacuum,
49
+ use_conventional_cell=use_conventional_cell,
50
+ xy_supercell_matrix=component.xy_supercell_matrix or [[1, 0], [0, 1]],
51
+ )
52
+ slabs.append(slab)
53
+
54
+ strained_slabs = [slabs[0]] # First slab is the substrate, not strained
55
+
56
+ for i in range(1, len(slabs)):
57
+ substrate_slab = strained_slabs[0]
58
+ film_slab = slabs[i]
59
+
60
+ substrate_analyzer = SlabMaterialAnalyzer(material=substrate_slab)
61
+ film_analyzer = SlabMaterialAnalyzer(material=film_slab)
62
+
63
+ analyzer = InterfaceAnalyzer(
64
+ substrate_slab_configuration=substrate_analyzer.build_configuration,
65
+ film_slab_configuration=film_analyzer.build_configuration,
66
+ substrate_build_parameters=substrate_analyzer.build_parameters,
67
+ film_build_parameters=film_analyzer.build_parameters,
68
+ optimize_film_supercell=optimize_layer_supercells,
69
+ )
70
+
71
+ strained_film_config = analyzer.film_strained_configuration
72
+
73
+ builder = SlabStrainedSupercellBuilder()
74
+ strained_slab = builder.get_material(strained_film_config)
75
+ strained_slabs.append(strained_slab)
76
+
77
+ stacked_materials = []
78
+ for i, slab in enumerate(strained_slabs):
79
+ if i < len(gaps):
80
+ slab_with_gap = adjust_material_cell_to_set_gap_along_direction(slab, gaps[i], AxisEnum.z)
81
+ stacked_materials.append(slab_with_gap)
82
+ else:
83
+ stacked_materials.append(slab)
84
+
85
+ heterostructure = stack(stacked_materials, AxisEnum.z)
86
+ heterostructure.name = generate_heterostructure_name(stack_component_dicts)
87
+
88
+ return heterostructure
89
+
90
+
91
+ def generate_heterostructure_name(stack_component_dicts: List[StackComponentDict]):
92
+ """Generate a descriptive name for the heterostructure."""
93
+
94
+ components = []
95
+ for component in stack_component_dicts:
96
+ analyzer = BaseMaterialAnalyzer(material=component.crystal)
97
+ formula = analyzer.formula
98
+ miller_str = "".join(map(str, component.miller_indices))
99
+ components.append(f"{formula}({miller_str})")
100
+
101
+ return f"Heterostructure [{'-'.join(components)}]"
@@ -0,0 +1,26 @@
1
+ from typing import List, Optional, Tuple, Union
2
+
3
+ from mat3ra.code.entity import InMemoryEntityPydantic
4
+ from pydantic import Field
5
+
6
+ from mat3ra.made.material import Material
7
+ from .....build_components import MaterialWithBuildMetadata
8
+
9
+
10
+ class StackComponentDict(InMemoryEntityPydantic):
11
+ """
12
+ Pydantic model for heterostructure stack component configurations.
13
+
14
+ Required fields:
15
+ crystal: The crystal material to create a slab from
16
+ miller_indices: Miller indices for the slab surface as (h, k, l)
17
+ thickness: Number of layers in the slab
18
+
19
+ Optional fields:
20
+ xy_supercell_matrix: Optional supercell matrix for the xy plane
21
+ """
22
+
23
+ crystal: Union[Material, MaterialWithBuildMetadata] = Field(..., description="Crystal material for the slab")
24
+ miller_indices: Tuple[int, int, int] = Field(..., description="Miller indices for the slab surface")
25
+ thickness: int = Field(..., gt=0, description="Number of layers in the slab")
26
+ xy_supercell_matrix: Optional[List[List[int]]] = Field(None, description="Optional xy supercell matrix")
@@ -1,4 +1,3 @@
1
- from types import SimpleNamespace
2
1
  from typing import List, Optional
3
2
 
4
3
  from mat3ra.made.material import Material
@@ -6,6 +5,7 @@ from .builder import AdatomDefectBuilder
6
5
  from .configuration import (
7
6
  AdatomDefectConfiguration,
8
7
  )
8
+ from .types import AdatomDefectDict
9
9
  from .....analyze.crystal_site.adatom_crystal_site_material_analyzer import (
10
10
  AdatomCrystalSiteMaterialAnalyzer,
11
11
  )
@@ -52,12 +52,14 @@ def create_defect_adatom(
52
52
  slab_with_adatom = create_multiple_adatom_defects(
53
53
  slab,
54
54
  defect_dicts=[
55
- {
56
- "element": element or "Si", # Default to Silicon if no element provided
57
- "coordinate": position_on_surface,
58
- "distance_z": distance_z,
59
- "use_cartesian_coordinates": use_cartesian_coordinates,
60
- }
55
+ AdatomDefectDict(
56
+ **{
57
+ "element": element or "Si", # Default to Silicon if no element provided
58
+ "coordinate_2d": position_on_surface,
59
+ "distance_z": distance_z,
60
+ "use_cartesian_coordinates": use_cartesian_coordinates,
61
+ }
62
+ )
61
63
  ],
62
64
  placement_method=placement_method,
63
65
  )
@@ -65,10 +67,10 @@ def create_defect_adatom(
65
67
 
66
68
 
67
69
  def create_multiple_adatom_defects(
68
- slab: MaterialWithBuildMetadata, defect_dicts: List[dict], placement_method: str
70
+ slab: MaterialWithBuildMetadata, defect_dicts: List[AdatomDefectDict], placement_method: str
69
71
  ) -> Material:
70
72
  """
71
- Create multiple adatom defects (at once) from a list of dictionaries.
73
+ Create multiple adatom defects from a list of AdatomDefectDict.
72
74
 
73
75
  Args:
74
76
  slab: The slab material.
@@ -99,10 +101,8 @@ def create_multiple_adatom_defects(
99
101
  last_analyzer = None
100
102
 
101
103
  for defect_dict in defect_dicts:
102
- defect_configuration = SimpleNamespace(**defect_dict)
103
-
104
- coordinate_2d = defect_configuration.coordinate
105
- use_cartesian = getattr(defect_configuration, "use_cartesian_coordinates", False)
104
+ coordinate_2d = defect_dict.coordinate_2d
105
+ use_cartesian = defect_dict.use_cartesian_coordinates
106
106
 
107
107
  if use_cartesian:
108
108
  coordinate_3d = coordinate_2d + [0.0]
@@ -112,9 +112,9 @@ def create_multiple_adatom_defects(
112
112
  analyzer = analyzer_cls(
113
113
  material=slab,
114
114
  coordinate_2d=coordinate_2d,
115
- distance_z=defect_configuration.distance_z,
115
+ distance_z=defect_dict.distance_z,
116
116
  placement_method=placement_method,
117
- element=defect_configuration.element,
117
+ element=defect_dict.element,
118
118
  )
119
119
  last_analyzer = analyzer
120
120
  all_adatom_configs.append(analyzer.added_component)
@@ -0,0 +1,22 @@
1
+ from typing import List
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class AdatomDefectDict(BaseModel):
7
+ """
8
+ Pydantic model for adatom defect configurations used with create_multiple_adatom_defects.
9
+
10
+ Required fields:
11
+ element: Chemical element for the adatom
12
+ coordinate: Position on surface as [x, y] list
13
+ distance_z: Distance above surface in Angstroms
14
+
15
+ Optional fields:
16
+ use_cartesian_coordinates: Whether coordinates are in Cartesian units
17
+ """
18
+
19
+ element: str = Field(..., description="Chemical element for the adatom")
20
+ coordinate_2d: List[float] = Field(..., min_items=2, max_items=2, description="Position on surface as [x, y]")
21
+ distance_z: float = Field(..., gt=0, description="Distance above surface in Angstroms")
22
+ use_cartesian_coordinates: bool = False
@@ -1,4 +1,3 @@
1
- from types import SimpleNamespace
2
1
  from typing import List, Union
3
2
 
4
3
  from mat3ra.made.material import Material
@@ -7,6 +6,7 @@ from .interstitial.interstitial_placement_method_enum import InterstitialPlaceme
7
6
  from .point_defect_type_enum import PointDefectTypeEnum
8
7
  from .substitutional.helpers import create_defect_point_substitution
9
8
  from .substitutional.substitution_placement_method_enum import SubstitutionPlacementMethodEnum
9
+ from .types import PointDefectDict
10
10
  from .vacancy.helpers import create_defect_point_vacancy
11
11
  from .vacancy.vacancy_placement_method_enum import VacancyPlacementMethodEnum
12
12
  from .....build_components import MaterialWithBuildMetadata
@@ -32,14 +32,14 @@ DEFECT_TYPE_MAPPING = {
32
32
 
33
33
  def create_multiple_defects(
34
34
  material: Union[Material, MaterialWithBuildMetadata],
35
- defect_dicts: List[dict],
35
+ defect_dicts: List[PointDefectDict],
36
36
  ) -> Material:
37
37
  """
38
- Create multiple point defects from a list of dictionaries.
38
+ Create multiple point defects from a list of PointDefectDict.
39
39
 
40
40
  Args:
41
41
  material (Material): The host material.
42
- defect_dicts (List[dict]): List of defect dictionaries with keys:
42
+ defect_dicts (List[PointDefectDict]): List of defect dictionaries with keys:
43
43
  - type: str ("vacancy", "substitution", "interstitial")
44
44
  - coordinate: List[float]
45
45
  - element: str (required for substitution and interstitial)
@@ -56,39 +56,36 @@ def create_multiple_defects(
56
56
  current_material = material
57
57
 
58
58
  for defect_dict in defect_dicts:
59
- defect_configuration = SimpleNamespace(**defect_dict)
60
- defect_type = defect_configuration.type
59
+ defect_type = defect_dict.type
61
60
 
62
61
  if defect_type not in [e.value for e in PointDefectTypeEnum]:
63
- raise ValueError(f"Unsupported defect type: {defect_configuration.type}")
62
+ raise ValueError(f"Unsupported defect type: {defect_dict.type}")
64
63
 
65
- use_cartesian = getattr(defect_configuration, "use_cartesian_coordinates", False)
64
+ use_cartesian = getattr(defect_dict, "use_cartesian_coordinates", False)
66
65
 
67
66
  if defect_type == "vacancy":
68
67
  current_material = create_defect_point_vacancy(
69
68
  current_material,
70
- coordinate=defect_configuration.coordinate,
71
- placement_method=defect_configuration.placement_method or VacancyPlacementMethodEnum.CLOSEST_SITE.value,
69
+ coordinate=defect_dict.coordinate,
70
+ placement_method=defect_dict.placement_method or VacancyPlacementMethodEnum.CLOSEST_SITE.value,
72
71
  use_cartesian_coordinates=use_cartesian,
73
72
  )
74
73
 
75
74
  elif defect_type == "substitution":
76
75
  current_material = create_defect_point_substitution(
77
76
  current_material,
78
- coordinate=defect_configuration.coordinate,
79
- element=defect_configuration.element,
80
- placement_method=defect_configuration.placement_method
81
- or SubstitutionPlacementMethodEnum.CLOSEST_SITE.value,
77
+ coordinate=defect_dict.coordinate,
78
+ element=defect_dict.element, # type: ignore # Pydantic validates this at creation
79
+ placement_method=defect_dict.placement_method or SubstitutionPlacementMethodEnum.CLOSEST_SITE.value,
82
80
  use_cartesian_coordinates=use_cartesian,
83
81
  )
84
82
 
85
83
  elif defect_type == "interstitial":
86
84
  current_material = create_defect_point_interstitial(
87
85
  current_material,
88
- coordinate=defect_configuration.coordinate,
89
- element=defect_configuration.element,
90
- placement_method=defect_configuration.placement_method
91
- or InterstitialPlacementMethodEnum.EXACT_COORDINATE.value,
86
+ coordinate=defect_dict.coordinate,
87
+ element=defect_dict.element, # type: ignore # Pydantic validates this at creation
88
+ placement_method=defect_dict.placement_method or InterstitialPlacementMethodEnum.EXACT_COORDINATE.value,
92
89
  use_cartesian_coordinates=use_cartesian,
93
90
  )
94
91
 
@@ -0,0 +1,24 @@
1
+ from typing import List, Literal, Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class PointDefectDict(BaseModel):
7
+ """
8
+ Pydantic model for point defect configurations used with create_multiple_defects.
9
+
10
+ Required fields:
11
+ type: The type of defect ("vacancy", "substitution", "interstitial")
12
+ coordinate: Position coordinates as [x, y, z] list
13
+
14
+ Optional fields:
15
+ element: Chemical element (required for substitution and interstitial)
16
+ placement_method: Method for placing the defect
17
+ use_cartesian_coordinates: Whether coordinates are in Cartesian units
18
+ """
19
+
20
+ type: Literal["vacancy", "substitution", "interstitial"]
21
+ coordinate: List[float] = Field(..., min_items=3, max_items=3, description="Position coordinates as [x, y, z]")
22
+ element: Optional[str] = None
23
+ placement_method: Optional[Literal["closest_site", "exact_coordinate", "voronoi_site"]] = None
24
+ use_cartesian_coordinates: bool = False
@@ -3,7 +3,7 @@ from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lin
3
3
  CrystalLatticeLinesConfiguration,
4
4
  )
5
5
  from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lines.edge_types import (
6
- EdgeTypes,
6
+ EdgeTypesEnum,
7
7
  )
8
8
  from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lines_unique_repeated import (
9
9
  CrystalLatticeLinesRepeatedBuilder,
@@ -25,5 +25,5 @@ __all__ = [
25
25
  "CrystalLatticeLinesBuilder",
26
26
  "CrystalLatticeLinesRepeatedBuilder",
27
27
  "create_nanoribbon",
28
- "EdgeTypes",
28
+ "EdgeTypesEnum",
29
29
  ]
@@ -10,7 +10,7 @@ from ..nanotape import NanoTapeConfiguration
10
10
  from .....build_components import MaterialWithBuildMetadata
11
11
  from .....build_components.entities.core.two_dimensional.vacuum.configuration import VacuumConfiguration
12
12
  from .....build_components.entities.reusable.base_builder import BaseBuilderParameters
13
- from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lines.edge_types import EdgeTypes
13
+ from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lines.edge_types import EdgeTypesEnum
14
14
  from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lines.helpers import (
15
15
  create_lattice_lines_config_and_material,
16
16
  )
@@ -21,7 +21,7 @@ P = TypeVar("P", bound=BaseBuilderParameters)
21
21
  def create_nanoribbon(
22
22
  material: Union[Material, MaterialWithBuildMetadata],
23
23
  miller_indices_2d: Optional[Tuple[int, int]] = None,
24
- edge_type: EdgeTypes = EdgeTypes.zigzag,
24
+ edge_type: EdgeTypesEnum = EdgeTypesEnum.zigzag,
25
25
  width: int = 2,
26
26
  length: int = 2,
27
27
  vacuum_width: float = 10.0,
@@ -5,7 +5,7 @@ from mat3ra.made.material import Material
5
5
  from . import NanoTapeBuilderParameters
6
6
  from .configuration import NanoTapeConfiguration
7
7
  from .....build_components import MaterialWithBuildMetadata, TypeConfiguration
8
- from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lines.edge_types import EdgeTypes
8
+ from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lines.edge_types import EdgeTypesEnum
9
9
  from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lines_unique_repeated.builder import (
10
10
  CrystalLatticeLinesRepeatedBuilder,
11
11
  )
@@ -44,9 +44,9 @@ class NanoTapeBuilder(StackNComponentsBuilder):
44
44
 
45
45
  def _get_edge_type_from_miller_indices(self, miller_indices_2d: tuple) -> str:
46
46
  if miller_indices_2d == (1, 1):
47
- return EdgeTypes.armchair.value.capitalize()
47
+ return EdgeTypesEnum.armchair.value.capitalize()
48
48
  elif miller_indices_2d == (0, 1):
49
- return EdgeTypes.zigzag.value.capitalize()
49
+ return EdgeTypesEnum.zigzag.value.capitalize()
50
50
  else:
51
51
  miller_str = f"{miller_indices_2d[0]}{miller_indices_2d[1]}"
52
52
  return f"({miller_str})"
@@ -7,7 +7,7 @@ from . import NanoTapeConfiguration
7
7
  from .builders import NanoTapeBuilder, NanoTapeBuilderParameters
8
8
  from .....build_components import MaterialWithBuildMetadata
9
9
  from .....build_components.entities.core.two_dimensional.vacuum.configuration import VacuumConfiguration
10
- from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lines.edge_types import EdgeTypes
10
+ from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lines.edge_types import EdgeTypesEnum
11
11
  from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lines.helpers import (
12
12
  create_lattice_lines_config_and_material,
13
13
  )
@@ -16,7 +16,7 @@ from .....build_components.entities.reusable.one_dimensional.crystal_lattice_lin
16
16
  def create_nanotape(
17
17
  material: Union[Material, MaterialWithBuildMetadata],
18
18
  miller_indices_2d: Optional[Tuple[int, int]] = None,
19
- edge_type: EdgeTypes = EdgeTypes.zigzag,
19
+ edge_type: EdgeTypesEnum = EdgeTypesEnum.zigzag,
20
20
  width: int = 2,
21
21
  length: int = 2,
22
22
  vacuum_width: float = 10.0,
@@ -2,6 +2,7 @@ from typing import Type, cast
2
2
 
3
3
  from .build_parameters import SlabBuilderParameters
4
4
  from .configuration import SlabConfiguration
5
+ from .utils import get_orthogonal_c_slab
5
6
  from .....build_components import MaterialWithBuildMetadata
6
7
  from .....build_components.entities.reusable.two_dimensional import (
7
8
  AtomicLayersUniqueRepeatedConfiguration,
@@ -9,7 +10,6 @@ from .....build_components.entities.reusable.two_dimensional import (
9
10
  )
10
11
 
11
12
  from .....build_components.operations.core.combinations.stack.builder import StackNComponentsBuilder
12
- from .....build_components.utils import get_orthogonal_c_slab
13
13
  from .....operations.core.unary import supercell
14
14
 
15
15
 
@@ -3,13 +3,13 @@ from typing import List, Optional, Tuple, Union
3
3
  from mat3ra.esse.models.core.reusable.axis_enum import AxisEnum
4
4
  from mat3ra.esse.models.materials_category.pristine_structures.two_dimensional.slab import SlabConfigurationSchema
5
5
  from mat3ra.made.material import Material
6
+ from .termination_utils import select_slab_termination
6
7
 
7
8
  from .....analyze.lattice_planes import CrystalLatticePlanesMaterialAnalyzer
8
9
  from .....build_components.entities.reusable.two_dimensional.atomic_layers_unique_repeated.configuration import (
9
10
  AtomicLayersUniqueRepeatedConfiguration,
10
11
  )
11
12
  from .....build_components.metadata import MaterialWithBuildMetadata
12
- from .....build_components import select_slab_termination
13
13
  from .....build_components.operations.core.combinations.stack.configuration import StackConfiguration
14
14
  from .....build_components.entities.core.two_dimensional.vacuum.configuration import VacuumConfiguration
15
15
  from .....build_components.entities.auxiliary.two_dimensional.termination import Termination
@@ -1,14 +1,26 @@
1
- from typing import Union
1
+ from typing import Union, Optional, List
2
2
 
3
3
  import numpy as np
4
4
 
5
5
  from mat3ra.made.lattice import Lattice
6
6
  from mat3ra.made.material import Material
7
7
  from .....build_components import MaterialWithBuildMetadata
8
+ from .....build_components.entities.auxiliary.two_dimensional.termination import Termination
8
9
  from .....modify import wrap_to_unit_cell
9
10
  from .....operations.core.unary import edit_cell
10
11
 
11
12
 
13
+ def select_slab_termination(terminations: List[Termination], formula: Optional[str] = None) -> Termination:
14
+ if not terminations:
15
+ raise ValueError("No terminations available.")
16
+ if formula is None:
17
+ return terminations[0]
18
+ for termination in terminations:
19
+ if termination.formula == formula:
20
+ return termination
21
+ raise ValueError(f"Termination with formula {formula} not found in available terminations: {terminations}")
22
+
23
+
12
24
  def get_orthogonal_c_slab(material: Union[Material, MaterialWithBuildMetadata]) -> Material:
13
25
  """
14
26
  Make the c-vector orthogonal to the ab plane and update the basis.
@@ -8,4 +8,3 @@ from mat3ra.made.tools.build_components.entities.reusable.base_builder import (
8
8
  )
9
9
 
10
10
  from .metadata import BuildMetadata, MaterialBuildMetadata, MaterialWithBuildMetadata
11
- from .utils import get_orthogonal_c_slab, select_slab_termination
@@ -4,10 +4,10 @@ from mat3ra.made.material import Material
4
4
  from mat3ra.made.tools.build_components.entities.reusable.base_builder import BaseSingleBuilder, TypeConfiguration
5
5
 
6
6
  from ......analyze.lattice_lines import CrystalLatticeLinesMaterialAnalyzer
7
+ from ......build.pristine_structures.two_dimensional.slab.utils import get_orthogonal_c_slab
7
8
  from ......modify import translate_to_z_level
8
9
  from ......operations.core.unary import supercell
9
10
  from ..... import MaterialWithBuildMetadata
10
- from .....utils import get_orthogonal_c_slab
11
11
  from .configuration import CrystalLatticeLinesConfiguration
12
12
 
13
13
 
@@ -2,7 +2,7 @@ from enum import Enum
2
2
  from typing import Tuple
3
3
 
4
4
 
5
- class EdgeTypes(str, Enum):
5
+ class EdgeTypesEnum(str, Enum):
6
6
  """
7
7
  Enum for nanoribbon/nanotape edge types.
8
8
  """
@@ -11,7 +11,7 @@ class EdgeTypes(str, Enum):
11
11
  armchair = "armchair"
12
12
 
13
13
 
14
- def get_miller_indices_from_edge_type(edge_type: EdgeTypes) -> Tuple[int, int]:
14
+ def get_miller_indices_from_edge_type(edge_type: EdgeTypesEnum) -> Tuple[int, int]:
15
15
  """
16
16
  Convert edge type shorthand to (u,v) Miller indices.
17
17
 
@@ -21,9 +21,9 @@ def get_miller_indices_from_edge_type(edge_type: EdgeTypes) -> Tuple[int, int]:
21
21
  Returns:
22
22
  Tuple of (u,v) Miller indices.
23
23
  """
24
- if edge_type == EdgeTypes.zigzag:
24
+ if edge_type == EdgeTypesEnum.zigzag:
25
25
  return (1, 1)
26
- elif edge_type == EdgeTypes.armchair:
26
+ elif edge_type == EdgeTypesEnum.armchair:
27
27
  return (0, 1)
28
28
  else:
29
29
  raise ValueError(f"Unknown edge type: {edge_type}. Use 'zigzag' or 'armchair'.")
@@ -3,16 +3,16 @@ from typing import Optional, Tuple, Union
3
3
  from mat3ra.made.material import Material
4
4
 
5
5
  from ......analyze.lattice_lines import CrystalLatticeLinesMaterialAnalyzer
6
+ from ......build.pristine_structures.two_dimensional.slab.termination_utils import select_slab_termination
6
7
  from ..... import MaterialWithBuildMetadata
7
- from .....utils import select_slab_termination
8
8
  from ..crystal_lattice_lines_unique_repeated.configuration import CrystalLatticeLinesUniqueRepeatedConfiguration
9
- from .edge_types import EdgeTypes, get_miller_indices_from_edge_type
9
+ from .edge_types import EdgeTypesEnum, get_miller_indices_from_edge_type
10
10
 
11
11
 
12
12
  def create_lattice_lines_config_and_material(
13
13
  material: Union[Material, MaterialWithBuildMetadata],
14
14
  miller_indices_2d: Optional[Tuple[int, int]],
15
- edge_type: Optional[EdgeTypes],
15
+ edge_type: Optional[EdgeTypesEnum],
16
16
  width: int,
17
17
  length: int,
18
18
  termination_formula: Optional[str] = None,
@@ -15,7 +15,6 @@ class StackConfiguration(StackSchema, BaseConfigurationPydantic):
15
15
  direction: AxisEnum = AxisEnum.z
16
16
 
17
17
  def __init__(self, **data):
18
- # Convert gaps to ArrayWithIds if not already
19
18
  gaps = data.get("gaps", [])
20
19
  if not isinstance(gaps, ArrayWithIds):
21
20
  data["gaps"] = ArrayWithIds.from_values(gaps)
@@ -1,67 +1,12 @@
1
- from typing import List, Optional, Union
1
+ from typing import List, Union
2
2
 
3
- import numpy as np
4
- from mat3ra.made.lattice import Lattice
5
3
  from mat3ra.made.material import Material
6
4
 
7
- from ..modify import filter_by_box, wrap_to_unit_cell
8
- from ..operations.core.unary import edit_cell
5
+ from ..modify import filter_by_box
9
6
  from . import MaterialWithBuildMetadata
10
- from .entities.auxiliary.two_dimensional.termination import Termination
11
7
  from .entities.reusable.three_dimensional.supercell.helpers import create_supercell
12
8
 
13
9
 
14
- def select_slab_termination(terminations: List[Termination], formula: Optional[str] = None) -> Termination:
15
- if not terminations:
16
- raise ValueError("No terminations available.")
17
- if formula is None:
18
- return terminations[0]
19
- for termination in terminations:
20
- if termination.formula == formula:
21
- return termination
22
- raise ValueError(f"Termination with formula {formula} not found in available terminations: {terminations}")
23
-
24
-
25
- def get_orthogonal_c_slab(material: Union[Material, MaterialWithBuildMetadata]) -> Material:
26
- """
27
- Make the c-vector orthogonal to the ab plane and update the basis.
28
-
29
- This function calculates a new c-vector that is orthogonal to the a and b vectors
30
- of the lattice. It then computes the transformation matrix between the old and new
31
- lattice vectors and applies this transformation to the atomic coordinates.
32
-
33
- A new material is returned with an updated lattice and basis, where the new
34
- lattice is defined by its parameters (a, b, c, alpha, beta, gamma) to avoid
35
- storing raw vectors, preserving a standard representation.
36
-
37
- Args:
38
- material (Material): The input material object.
39
-
40
- Returns:
41
- Material: A new material object with an orthogonalized c-vector and
42
- updated basis.
43
- """
44
- new_material = material.clone()
45
- current_vectors = np.array(new_material.lattice.vector_arrays)
46
- a_vec, b_vec, c_old_vec = current_vectors
47
-
48
- normal = np.cross(a_vec, b_vec)
49
- n_hat = normal / np.linalg.norm(normal)
50
- height = float(np.dot(c_old_vec, n_hat))
51
- c_new_vec = n_hat * height
52
-
53
- new_vectors = np.array([a_vec, b_vec, c_new_vec])
54
- transform_matrix = np.dot(current_vectors, np.linalg.inv(new_vectors))
55
-
56
- new_basis = new_material.basis.clone()
57
- new_basis.transform_by_matrix(transform_matrix)
58
- new_lattice_from_vectors = Lattice.from_vectors_array(new_vectors.tolist())
59
- new_material = edit_cell(new_material, new_lattice_from_vectors.vector_arrays)
60
- new_material.basis = new_basis
61
- new_material = wrap_to_unit_cell(new_material)
62
- return new_material
63
-
64
-
65
10
  def double_and_filter_material(
66
11
  material: Union[Material, MaterialWithBuildMetadata], start: List[float], end: List[float]
67
12
  ) -> Material:
@@ -1,12 +1,13 @@
1
1
  from typing import List
2
2
 
3
+ from mat3ra.esse.models.core.reusable.coordinate_conditions import BoxCoordinateConditionSchema
3
4
  from pydantic import Field
4
5
 
5
6
  from .coordinate_condition import CoordinateCondition
6
7
  from .coordinate_functions import is_coordinate_in_box
7
8
 
8
9
 
9
- class BoxCoordinateCondition(CoordinateCondition):
10
+ class BoxCoordinateCondition(BoxCoordinateConditionSchema, CoordinateCondition):
10
11
  min_coordinate: List[float] = Field(default_factory=lambda: [0, 0, 0])
11
12
  max_coordinate: List[float] = Field(default_factory=lambda: [1, 1, 1])
12
13
 
@@ -1,12 +1,13 @@
1
1
  from typing import List
2
2
 
3
+ from mat3ra.esse.models.core.reusable.coordinate_conditions import CylinderCoordinateConditionSchema
3
4
  from pydantic import Field
4
5
 
5
6
  from .coordinate_condition import CoordinateCondition
6
7
  from .coordinate_functions import is_coordinate_in_cylinder
7
8
 
8
9
 
9
- class CylinderCoordinateCondition(CoordinateCondition):
10
+ class CylinderCoordinateCondition(CylinderCoordinateConditionSchema, CoordinateCondition):
10
11
  center_position: List[float] = Field(default_factory=lambda: [0.5, 0.5])
11
12
  radius: float = 0.25
12
13
  min_z: float = 0
@@ -1,10 +1,12 @@
1
1
  from typing import List
2
2
 
3
+ from mat3ra.esse.models.core.reusable.coordinate_conditions import PlaneCoordinateConditionSchema
4
+
3
5
  from .coordinate_condition import CoordinateCondition
4
6
  from .coordinate_functions import is_coordinate_behind_plane
5
7
 
6
8
 
7
- class PlaneCoordinateCondition(CoordinateCondition):
9
+ class PlaneCoordinateCondition(PlaneCoordinateConditionSchema, CoordinateCondition):
8
10
  plane_normal: List[float]
9
11
  plane_point_coordinate: List[float]
10
12
 
@@ -1,12 +1,13 @@
1
1
  from typing import List
2
2
 
3
+ from mat3ra.esse.models.core.reusable.coordinate_conditions import SphereCoordinateConditionSchema
3
4
  from pydantic import Field
4
5
 
5
6
  from .coordinate_condition import CoordinateCondition
6
7
  from .coordinate_functions import is_coordinate_in_sphere
7
8
 
8
9
 
9
- class SphereCoordinateCondition(CoordinateCondition):
10
+ class SphereCoordinateCondition(SphereCoordinateConditionSchema, CoordinateCondition):
10
11
  center_coordinate: List[float] = Field(default_factory=lambda: [0.5, 0.5, 0.5])
11
12
  radius: float = 0.25
12
13
 
@@ -1,10 +1,12 @@
1
1
  from typing import List
2
2
 
3
+ from mat3ra.esse.models.core.reusable.coordinate_conditions import TriangularPrismCoordinateConditionSchema
4
+
3
5
  from .coordinate_condition import CoordinateCondition
4
6
  from .coordinate_functions import is_coordinate_in_triangular_prism
5
7
 
6
8
 
7
- class TriangularPrismCoordinateCondition(CoordinateCondition):
9
+ class TriangularPrismCoordinateCondition(TriangularPrismCoordinateConditionSchema, CoordinateCondition):
8
10
  position_on_surface_1: List[float] = [0, 0]
9
11
  position_on_surface_2: List[float] = [1, 0]
10
12
  position_on_surface_3: List[float] = [0, 1]
@@ -1,3 +1,7 @@
1
+ # Defective Structures
2
+ from .build.compound_pristine_structures.two_dimensional.heterostructure import create_heterostructure
3
+ from .build.compound_pristine_structures.two_dimensional.heterostructure.types import StackComponentDict
4
+
1
5
  # Compound Pristine Structures
2
6
  from .build.compound_pristine_structures.two_dimensional.interface.base.helpers import (
3
7
  create_interface_simple,
@@ -21,6 +25,7 @@ from .build.defective_structures.two_dimensional.adatom.helpers import (
21
25
  create_multiple_adatom_defects,
22
26
  get_adatom_defect_analyzer_cls,
23
27
  )
28
+ from .build.defective_structures.two_dimensional.adatom.types import AdatomDefectDict
24
29
  from .build.defective_structures.two_dimensional.grain_boundary_planar.helpers import create_grain_boundary_planar
25
30
  from .build.defective_structures.two_dimensional.island.helpers import create_defect_island, get_coordinate_condition
26
31
  from .build.defective_structures.two_dimensional.terrace.helpers import create_defect_terrace
@@ -32,6 +37,7 @@ from .build.defective_structures.zero_dimensional.point_defect.interstitial.help
32
37
  from .build.defective_structures.zero_dimensional.point_defect.substitutional.helpers import (
33
38
  create_defect_point_substitution,
34
39
  )
40
+ from .build.defective_structures.zero_dimensional.point_defect.types import PointDefectDict
35
41
  from .build.defective_structures.zero_dimensional.point_defect.vacancy.helpers import create_defect_point_vacancy
36
42
 
37
43
  # Pristine Structures
@@ -51,6 +57,9 @@ from .build.pristine_structures.zero_dimensional.nanoparticle.helpers import (
51
57
  create_nanoparticle_from_material,
52
58
  )
53
59
 
60
+ # Enums
61
+ from .build.processed_structures.two_dimensional.passivation.enums import SurfaceTypesEnum
62
+
54
63
  # Processed Structures
55
64
  from .build.processed_structures.two_dimensional.passivation.helpers import (
56
65
  get_coordination_numbers_distribution,
@@ -58,6 +67,7 @@ from .build.processed_structures.two_dimensional.passivation.helpers import (
58
67
  passivate_dangling_bonds,
59
68
  passivate_surface,
60
69
  )
70
+ from .build_components.entities.reusable.one_dimensional.crystal_lattice_lines.edge_types import EdgeTypesEnum
61
71
  from .build_components.entities.reusable.one_dimensional.crystal_lattice_lines.helpers import (
62
72
  create_lattice_lines_config_and_material,
63
73
  )
@@ -73,6 +83,9 @@ from .build_components.entities.reusable.two_dimensional.slab_stack.helpers impo
73
83
  )
74
84
  from .build_components.operations.core.modifications.perturb.helpers import create_perturbation
75
85
 
86
+ # Entities
87
+ from .entities.coordinate import CoordinateCondition
88
+
76
89
  __all__ = [
77
90
  # Slab and related Functions
78
91
  "create_slab",
@@ -84,6 +97,8 @@ __all__ = [
84
97
  "create_supercell",
85
98
  "create_monolayer",
86
99
  "create_perturbation",
100
+ # heterostructure Functions
101
+ "create_heterostructure",
87
102
  "create_lattice_lines_config_and_material",
88
103
  # Nanostructure Functions
89
104
  "create_nanoparticle_from_material",
@@ -125,6 +140,15 @@ __all__ = [
125
140
  "get_coordination_numbers_distribution",
126
141
  # Utility Functions
127
142
  "get_optimal_film_displacement",
143
+ # Type Definitions
144
+ "AdatomDefectDict",
145
+ "PointDefectDict",
146
+ "StackComponentDict",
147
+ # Entities
148
+ "CoordinateCondition",
149
+ # Enums
150
+ "EdgeTypesEnum",
151
+ "SurfaceTypesEnum",
128
152
  ]
129
153
 
130
154
  # Aliases
@@ -5,7 +5,7 @@ from mat3ra.made.material import Material
5
5
  from mat3ra.made.utils import get_atomic_coordinates_extremum
6
6
 
7
7
  from .analyze.other import get_atom_indices_with_condition_on_coordinates, get_atom_indices_within_radius_pbc
8
- from .build_components import MaterialWithBuildMetadata
8
+ from .build_components.metadata import MaterialWithBuildMetadata
9
9
  from .convert import from_ase, to_ase
10
10
  from .convert.interface_parts_enum import InterfacePartsEnum
11
11
  from .entities.coordinate import (
@@ -5,7 +5,7 @@ from mat3ra.code.vector import Vector3D
5
5
  from mat3ra.esse.models.core.abstract.matrix_3x3 import Matrix3x3Schema
6
6
  from mat3ra.made.material import Material
7
7
 
8
- from ...build_components import MaterialWithBuildMetadata
8
+ from ...build_components.metadata import MaterialWithBuildMetadata
9
9
  from ...build_components.operations.core.modifications.perturb import FunctionHolder
10
10
  from ...convert import from_ase, to_ase
11
11
  from ...modify import translate_by_vector, wrap_to_unit_cell
@@ -4,6 +4,11 @@ from mat3ra.standata.materials import Materials
4
4
 
5
5
  BULK_SrTiO3 = Materials.get_by_name_first_match("SrTiO3")
6
6
  BULK_GRAPHITE = Materials.get_by_name_first_match("Graphite")
7
+ BULK_SiO2 = Materials.get_by_name_first_match("SiO2")
8
+ BULK_Hf2O_MCL = Materials.get_by_name_first_match("Hafnium.*MCL")
9
+ BULK_TiN = Materials.get_by_name_first_match("TiN")
10
+ BULK_Ni_PRIMITIVE = Materials.get_by_name_first_match("Nickel")
11
+ BULK_GRAPHENE = Materials.get_by_name_first_match("Graphene")
7
12
 
8
13
  BULK_Si_PRIMITIVE: Dict[str, Any] = {
9
14
  "name": "Silicon FCC",
@@ -6,7 +6,7 @@ import pytest
6
6
  from mat3ra.made.tools.analyze.interface import InterfaceAnalyzer
7
7
  from mat3ra.made.tools.analyze.interface.commensurate import CommensurateLatticeInterfaceAnalyzer
8
8
  from mat3ra.made.tools.build.pristine_structures.two_dimensional.slab import SlabConfiguration
9
- from unit.fixtures.bulk import BULK_Ge_CONVENTIONAL, BULK_Si_CONVENTIONAL
9
+ from unit.fixtures.bulk import BULK_GRAPHENE, BULK_Ge_CONVENTIONAL, BULK_Si_CONVENTIONAL
10
10
 
11
11
  from .fixtures.monolayer import GRAPHENE
12
12
  from .utils import assert_two_entities_deep_almost_equal
@@ -35,6 +35,25 @@ EXPECTED_PROPERTIES_SI_GE_001: Final = SimpleNamespace(
35
35
  TEST_CASES = [(SUBSTRATE_SI_001, FILM_GE_001, EXPECTED_PROPERTIES_SI_GE_001)]
36
36
 
37
37
 
38
+ SUBSTRATE_SI_111: Final = SimpleNamespace(
39
+ bulk_config=BULK_Si_CONVENTIONAL,
40
+ miller_indices=(1, 1, 1),
41
+ number_of_layers=2,
42
+ vacuum=0.0,
43
+ )
44
+
45
+ FILM_GRAPHENE_001: Final = SimpleNamespace(
46
+ bulk_config=BULK_GRAPHENE,
47
+ miller_indices=(0, 0, 1),
48
+ number_of_layers=1,
49
+ vacuum=0.0,
50
+ )
51
+
52
+ OPTIMAL_SUPERCELL_TEST_CASES = [
53
+ (SUBSTRATE_SI_111, FILM_GRAPHENE_001, 4, 4), # n, m
54
+ ]
55
+
56
+
38
57
  @pytest.mark.parametrize("substrate, film, expected", TEST_CASES)
39
58
  def test_interface_analyzer(substrate, film, expected):
40
59
  substrate_slab_config = SlabConfiguration.from_parameters(
@@ -143,3 +162,36 @@ def test_commensurate_analyzer_functionality(
143
162
  # Test negative match ID
144
163
  with pytest.raises(ValueError, match="Match ID .* out of range"):
145
164
  analyzer.get_strained_configuration_by_match_id(-1)
165
+
166
+
167
+ @pytest.mark.parametrize("substrate, film, expected_n, expected_m", OPTIMAL_SUPERCELL_TEST_CASES)
168
+ def test_optimal_supercell_functions(substrate, film, expected_n, expected_m):
169
+ """Test the optimal supercell functions with Si/Ge fixtures."""
170
+ substrate_slab_config = SlabConfiguration.from_parameters(
171
+ substrate.bulk_config,
172
+ substrate.miller_indices,
173
+ substrate.number_of_layers,
174
+ vacuum=substrate.vacuum,
175
+ termination_top_formula=None,
176
+ termination_bottom_formula=None,
177
+ )
178
+ film_slab_config = SlabConfiguration.from_parameters(
179
+ film.bulk_config,
180
+ film.miller_indices,
181
+ film.number_of_layers,
182
+ vacuum=film.vacuum,
183
+ termination_top_formula=None,
184
+ termination_bottom_formula=None,
185
+ )
186
+
187
+ analyzer = InterfaceAnalyzer(
188
+ substrate_slab_configuration=substrate_slab_config,
189
+ film_slab_configuration=film_slab_config,
190
+ optimize_film_supercell=True,
191
+ )
192
+
193
+ # Test find_optimal_film_supercell
194
+ optimal_n, optimal_m = analyzer.find_optimal_film_supercell()
195
+
196
+ assert optimal_n == expected_n
197
+ assert optimal_m == expected_m
@@ -9,9 +9,8 @@ from mat3ra.made.tools.build.pristine_structures.two_dimensional.slab_strained_s
9
9
  SlabStrainedSupercellBuilder,
10
10
  )
11
11
  from mat3ra.made.tools.utils import supercell_matrix_2d_schema_to_list, unwrap
12
- from mat3ra.standata.materials import Materials
13
12
  from mat3ra.utils.matrix import convert_2x2_to_3x3
14
- from unit.fixtures.bulk import BULK_Ge_CONVENTIONAL, BULK_Si_CONVENTIONAL
13
+ from unit.fixtures.bulk import BULK_Ge_CONVENTIONAL, BULK_Ni_PRIMITIVE, BULK_Si_CONVENTIONAL
15
14
 
16
15
  from .fixtures.monolayer import GRAPHENE
17
16
  from .utils import OSPlatform, get_platform_specific_value
@@ -49,7 +48,7 @@ EXPECTED_PROPERTIES_SI_GE_001: Final = SimpleNamespace(
49
48
  ),
50
49
  (
51
50
  SimpleNamespace(
52
- bulk_config=Materials.get_by_name_first_match("Nickel"),
51
+ bulk_config=BULK_Ni_PRIMITIVE,
53
52
  miller_indices=(0, 0, 1),
54
53
  number_of_layers=2,
55
54
  vacuum=0.0,
@@ -123,7 +122,7 @@ def test_zsl_interface_analyzer(substrate, film, zsl_params, expected_matches_mi
123
122
  [
124
123
  (
125
124
  SimpleNamespace(
126
- bulk_config=Materials.get_by_name_first_match("Nickel"),
125
+ bulk_config=BULK_Ni_PRIMITIVE,
127
126
  miller_indices=(1, 1, 1),
128
127
  number_of_layers=3,
129
128
  vacuum=0.0,
@@ -5,6 +5,7 @@ from mat3ra.made.tools.build.defective_structures.two_dimensional.adatom.helpers
5
5
  create_defect_adatom,
6
6
  create_multiple_adatom_defects,
7
7
  )
8
+ from mat3ra.made.tools.build.defective_structures.two_dimensional.adatom.types import AdatomDefectDict
8
9
  from mat3ra.made.tools.build.pristine_structures.two_dimensional.slab.helpers import create_slab
9
10
  from mat3ra.made.tools.build_components.operations.core.combinations.enums import AdatomPlacementMethodEnum
10
11
  from mat3ra.utils import assertion as assertion_utils
@@ -61,9 +62,9 @@ def test_create_adatom(
61
62
  (
62
63
  SI_CONVENTIONAL_SLAB_001,
63
64
  [
64
- {"element": "Si", "coordinate": [0.5, 0.5], "distance_z": 2.0},
65
- {"element": "C", "coordinate": [0.25, 0.25], "distance_z": 1.5},
66
- {"element": "N", "coordinate": [0.75, 0.75], "distance_z": 1.0},
65
+ {"element": "Si", "coordinate_2d": [0.5, 0.5], "distance_z": 2.0},
66
+ {"element": "C", "coordinate_2d": [0.25, 0.25], "distance_z": 1.5},
67
+ {"element": "N", "coordinate_2d": [0.75, 0.75], "distance_z": 1.0},
67
68
  ],
68
69
  AdatomPlacementMethodEnum.EXACT_COORDINATE.value,
69
70
  SLAB_Si_3_ADATOMS,
@@ -72,6 +73,12 @@ def test_create_adatom(
72
73
  )
73
74
  def test_create_multiple_adatom_defects(crystal_config, adatom_dicts, placement_method, expected_material_config):
74
75
  slab = MaterialWithBuildMetadata.create(crystal_config)
75
- defects = create_multiple_adatom_defects(slab, adatom_dicts, placement_method)
76
+
77
+ defect_models = []
78
+ for adatom_data in adatom_dicts:
79
+ defect_dict = AdatomDefectDict(**adatom_data)
80
+ defect_models.append(defect_dict)
81
+
82
+ defects = create_multiple_adatom_defects(slab, defect_models, placement_method)
76
83
  defects.metadata.build = []
77
84
  assert_two_entities_deep_almost_equal(defects, expected_material_config)
@@ -14,6 +14,7 @@ from mat3ra.made.tools.build.defective_structures.zero_dimensional.point_defect.
14
14
  create_defect_point_vacancy,
15
15
  create_multiple_defects,
16
16
  )
17
+ from mat3ra.made.tools.build.defective_structures.zero_dimensional.point_defect.types import PointDefectDict
17
18
  from unit.fixtures.bulk import BULK_Si_CONVENTIONAL, BULK_Si_PRIMITIVE
18
19
  from unit.fixtures.point_defects import (
19
20
  INTERSTITIAL_DEFECT_BULK_PRIMITIVE_Si,
@@ -123,13 +124,15 @@ def test_create_multiple_defects(material_config, defect_params_list, expected_m
123
124
 
124
125
  defect_dicts = []
125
126
  for defect_params in defect_params_list:
126
- defect_dict = {
127
+ defect_data = {
127
128
  "type": defect_params.defect_type,
128
129
  "coordinate": defect_params.coordinate,
129
130
  "placement_method": defect_params.placement_method if hasattr(defect_params, "placement_method") else None,
130
131
  "element": defect_params.element if hasattr(defect_params, "element") else None,
131
132
  }
132
133
 
134
+ defect_dict = PointDefectDict(**defect_data)
135
+
133
136
  defect_dicts.append(defect_dict)
134
137
 
135
138
  defects = create_multiple_defects(
@@ -0,0 +1,48 @@
1
+ from types import SimpleNamespace
2
+
3
+ import pytest
4
+ from mat3ra.made.material import Material
5
+ from mat3ra.made.tools.helpers import StackComponentDict, create_heterostructure
6
+
7
+ from .fixtures.bulk import BULK_Hf2O_MCL, BULK_Si_CONVENTIONAL, BULK_SiO2, BULK_TiN
8
+
9
+ PRECISION = 1e-3
10
+
11
+ Si_SiO2_Hf2O_HETEROSTRUCTURE_TEST_CASE = (
12
+ [
13
+ SimpleNamespace(bulk_config=BULK_Si_CONVENTIONAL, miller_indices=(0, 0, 1), number_of_layers=4),
14
+ SimpleNamespace(bulk_config=BULK_SiO2, miller_indices=(1, 1, 1), number_of_layers=5),
15
+ SimpleNamespace(bulk_config=BULK_Hf2O_MCL, miller_indices=(0, 0, 1), number_of_layers=2),
16
+ SimpleNamespace(bulk_config=BULK_TiN, miller_indices=(1, 1, 1), number_of_layers=5),
17
+ ],
18
+ [1.5, 1.0, 1.0], # gaps
19
+ 10.0, # vacuum
20
+ )
21
+
22
+
23
+ @pytest.mark.parametrize("layers, gaps, vacuum", [Si_SiO2_Hf2O_HETEROSTRUCTURE_TEST_CASE])
24
+ def test_create_heterostructure_simple(layers, gaps, vacuum):
25
+ # Convert raw test data to Pydantic models
26
+ stack_components = []
27
+ for layer in layers:
28
+ component_data = {
29
+ "crystal": Material.create(layer.bulk_config),
30
+ "miller_indices": layer.miller_indices,
31
+ "thickness": layer.number_of_layers,
32
+ }
33
+ component = StackComponentDict(**component_data)
34
+ stack_components.append(component)
35
+
36
+ heterostructure = create_heterostructure(
37
+ stack_component_dicts=stack_components,
38
+ gaps=gaps,
39
+ vacuum=vacuum,
40
+ )
41
+
42
+ assert isinstance(heterostructure, Material)
43
+ elements = set()
44
+ for layer in layers:
45
+ elements.update(Material.create(layer.bulk_config).basis.elements.values)
46
+ assert set(heterostructure.basis.elements.values) == elements
47
+
48
+ assert len(heterostructure.basis.elements.values) > 0
@@ -23,8 +23,7 @@ from mat3ra.made.tools.build.pristine_structures.two_dimensional.nanoribbon.help
23
23
  from mat3ra.made.tools.build.pristine_structures.two_dimensional.slab import SlabBuilder, SlabConfiguration
24
24
  from mat3ra.made.tools.build.pristine_structures.two_dimensional.slab.helpers import create_slab
25
25
  from mat3ra.made.tools.helpers import create_interface_simple, create_interface_simple_between_slabs
26
- from mat3ra.standata.materials import Materials
27
- from unit.fixtures.bulk import BULK_Ge_CONVENTIONAL, BULK_Si_CONVENTIONAL
26
+ from unit.fixtures.bulk import BULK_Ge_CONVENTIONAL, BULK_Ni_PRIMITIVE, BULK_Si_CONVENTIONAL
28
27
 
29
28
  from .fixtures.interface.commensurate import INTERFACE_GRAPHENE_GRAPHENE_X, INTERFACE_GRAPHENE_GRAPHENE_Z
30
29
  from .fixtures.interface.gr_ni_111_top_hcp import (
@@ -55,7 +54,7 @@ Si_Ge_SIMPLE_INTERFACE_TEST_CASE = (
55
54
 
56
55
  GRAPHENE_NICKEL_TEST_CASE = (
57
56
  SimpleNamespace(
58
- bulk_config=Materials.get_by_name_first_match("Nickel"),
57
+ bulk_config=BULK_Ni_PRIMITIVE,
59
58
  miller_indices=(1, 1, 1),
60
59
  number_of_layers=3,
61
60
  vacuum=0.0,
@@ -12,8 +12,8 @@ from mat3ra.made.tools.build.compound_pristine_structures.two_dimensional.interf
12
12
  )
13
13
  from mat3ra.made.tools.build.pristine_structures.two_dimensional.slab import SlabBuilder, SlabConfiguration
14
14
  from mat3ra.made.tools.build_components.entities.core.two_dimensional.vacuum.configuration import VacuumConfiguration
15
- from mat3ra.standata.materials import Materials
16
15
 
16
+ from .fixtures.bulk import BULK_Ni_PRIMITIVE
17
17
  from .fixtures.interface.gr_ni_111_top_hcp import (
18
18
  GRAPHENE_NICKEL_INTERFACE_TOP_HCP,
19
19
  GRAPHENE_NICKEL_INTERFACE_TOP_HCP_GH_WF,
@@ -23,7 +23,7 @@ from .utils import OSPlatform, assert_two_entities_deep_almost_equal, get_platfo
23
23
 
24
24
  GRAPHENE_NICKEL_TEST_CASE = (
25
25
  SimpleNamespace(
26
- bulk_config=Materials.get_by_name_first_match("Nickel"),
26
+ bulk_config=BULK_Ni_PRIMITIVE,
27
27
  miller_indices=(1, 1, 1),
28
28
  number_of_layers=3,
29
29
  vacuum=0.0,