@mat3ra/made 2024.7.7-0 → 2024.7.8-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.7.7-0",
3
+ "version": "2024.7.8-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,10 +1,10 @@
1
- from typing import Callable, List, Optional, Literal
1
+ from typing import Callable, List, Literal, Optional
2
2
 
3
3
  import numpy as np
4
4
 
5
5
  from ..material import Material
6
6
  from .convert import decorator_convert_material_args_kwargs_to_atoms, to_pymatgen
7
- from .third_party import ASEAtoms, PymatgenIStructure
7
+ from .third_party import ASEAtoms, PymatgenIStructure, PymatgenVoronoiNN
8
8
 
9
9
 
10
10
  @decorator_convert_material_args_kwargs_to_atoms
@@ -231,6 +231,41 @@ def get_atom_indices_with_condition_on_coordinates(
231
231
  return selected_indices
232
232
 
233
233
 
234
+ def get_nearest_neighbors_atom_indices(
235
+ material: Material,
236
+ position: Optional[List[float]] = None,
237
+ ) -> Optional[List[int]]:
238
+ """
239
+ Returns the indices of direct neighboring atoms to a specified position in the material using Voronoi tessellation.
240
+
241
+ Args:
242
+ material (Material): The material object to find neighbors in.
243
+ position (List[float]): The position to find neighbors for.
244
+
245
+ Returns:
246
+ List[int]: A list of indices of neighboring atoms, or an empty list if no neighbors are found.
247
+ """
248
+ if position is None:
249
+ position = [0, 0, 0]
250
+ structure = to_pymatgen(material)
251
+ voronoi_nn = PymatgenVoronoiNN(
252
+ tol=0.5,
253
+ cutoff=13.0,
254
+ allow_pathological=False,
255
+ weight="solid_angle",
256
+ extra_nn_info=True,
257
+ compute_adj_neighbors=True,
258
+ )
259
+ structure.append("X", position, validate_proximity=False)
260
+ neighbors = voronoi_nn.get_nn_info(structure, len(structure.sites) - 1)
261
+ neighboring_atoms_pymatgen_ids = [n["site_index"] for n in neighbors]
262
+ structure.remove_sites([-1])
263
+
264
+ all_coordinates = material.basis.coordinates
265
+ all_coordinates.filter_by_indices(neighboring_atoms_pymatgen_ids)
266
+ return all_coordinates.ids
267
+
268
+
234
269
  def get_atomic_coordinates_extremum(
235
270
  material: Material,
236
271
  extremum: Literal["max", "min"] = "max",
@@ -65,6 +65,8 @@ class BaseBuilder(BaseModel):
65
65
  def _post_process(
66
66
  self, items: List[_GeneratedItemType], post_process_parameters: Optional[_PostProcessParametersType]
67
67
  ) -> List[Material]:
68
+ if self._GeneratedItemType == Material:
69
+ return items
68
70
  return [Material(self._convert_generated_item(item)) for item in items]
69
71
 
70
72
  @staticmethod
@@ -1,11 +1,16 @@
1
- from typing import Optional
1
+ from typing import Optional, Union
2
2
 
3
3
  from mat3ra.utils.factory import BaseFactory
4
4
  from mat3ra.made.material import Material
5
5
 
6
- from .builders import PointDefectBuilderParameters
7
- from .configuration import PointDefectConfiguration
8
- from .enums import PointDefectTypeEnum
6
+ from .builders import (
7
+ PointDefectBuilderParameters,
8
+ SlabDefectBuilderParameters,
9
+ AdatomSlabDefectBuilder,
10
+ EquidistantAdatomSlabDefectBuilder,
11
+ )
12
+ from .configuration import PointDefectConfiguration, AdatomSlabDefectConfiguration
13
+ from .enums import PointDefectTypeEnum, SlabDefectTypeEnum
9
14
 
10
15
 
11
16
  class DefectBuilderFactory(BaseFactory):
@@ -17,8 +22,8 @@ class DefectBuilderFactory(BaseFactory):
17
22
 
18
23
 
19
24
  def create_defect(
20
- configuration: PointDefectConfiguration,
21
- builder_parameters: Optional[PointDefectBuilderParameters] = None,
25
+ configuration: Union[PointDefectConfiguration, AdatomSlabDefectConfiguration],
26
+ builder_parameters: Union[PointDefectBuilderParameters, SlabDefectBuilderParameters, None] = None,
22
27
  ) -> Material:
23
28
  """
24
29
  Return a material with a selected defect added.
@@ -34,3 +39,22 @@ def create_defect(
34
39
  builder = BuilderClass(builder_parameters)
35
40
 
36
41
  return builder.get_material(configuration) if builder else configuration.crystal
42
+
43
+
44
+ def create_slab_defect(
45
+ configuration: Union[AdatomSlabDefectConfiguration],
46
+ builder: Optional[Union[AdatomSlabDefectBuilder, EquidistantAdatomSlabDefectBuilder]] = None,
47
+ ) -> Material:
48
+ """
49
+ Return a material with a selected slab defect added.
50
+
51
+ Args:
52
+ configuration: The configuration of the defect to be added.
53
+ builder: The builder to be used to create the defect.
54
+
55
+ Returns:
56
+ The material with the defect added.
57
+ """
58
+ if builder is None:
59
+ builder = AdatomSlabDefectBuilder(build_parameters=SlabDefectBuilderParameters())
60
+ return builder.get_material(configuration)
@@ -1,7 +1,10 @@
1
- from typing import List, Callable
1
+ from typing import List, Callable, Optional
2
2
 
3
- from mat3ra.made.material import Material
3
+ from mat3ra.made.tools.build.supercell import create_supercell
4
+ from mat3ra.made.tools.modify import add_vacuum
4
5
  from pydantic import BaseModel
6
+ from mat3ra.made.material import Material
7
+
5
8
 
6
9
  from ...third_party import (
7
10
  PymatgenStructure,
@@ -12,8 +15,10 @@ from ...third_party import (
12
15
  )
13
16
  from ...build import BaseBuilder
14
17
  from ...convert import to_pymatgen
18
+ from ...analyze import get_nearest_neighbors_atom_indices, get_atomic_coordinates_extremum
19
+ from ....utils import get_center_of_coordinates
15
20
  from ..mixins import ConvertGeneratedItemsPymatgenStructureMixin
16
- from .configuration import PointDefectConfiguration
21
+ from .configuration import PointDefectConfiguration, AdatomSlabDefectConfiguration
17
22
 
18
23
 
19
24
  class PointDefectBuilderParameters(BaseModel):
@@ -67,3 +72,124 @@ class SubstitutionPointDefectBuilder(PointDefectBuilder):
67
72
 
68
73
  class InterstitialPointDefectBuilder(PointDefectBuilder):
69
74
  _generator: PymatgenInterstitial = PymatgenInterstitial
75
+
76
+
77
+ class SlabDefectBuilderParameters(BaseModel):
78
+ auto_add_vacuum: bool = True
79
+ vacuum_thickness: float = 5.0
80
+
81
+
82
+ class SlabDefectBuilder(BaseBuilder):
83
+ _BuildParametersType = SlabDefectBuilderParameters
84
+ _DefaultBuildParameters = SlabDefectBuilderParameters()
85
+
86
+
87
+ class AdatomSlabDefectBuilder(SlabDefectBuilder):
88
+ _ConfigurationType: type(AdatomSlabDefectConfiguration) = AdatomSlabDefectConfiguration # type: ignore
89
+ _GeneratedItemType: Material = Material
90
+
91
+ def create_adatom(
92
+ self,
93
+ material: Material,
94
+ chemical_element: str = "Si",
95
+ position_on_surface: Optional[List[float]] = None,
96
+ distance_z: float = 2.0,
97
+ ) -> List[Material]:
98
+ """
99
+ Create an adatom at the specified position on the surface of the material.
100
+
101
+ Args:
102
+ material: The material to add the adatom to.
103
+ chemical_element: The chemical element of the adatom.
104
+ position_on_surface: The position on the surface of the material.
105
+ distance_z: The distance of the adatom from the surface.
106
+
107
+ Returns:
108
+ The material with the adatom added.
109
+ """
110
+ if position_on_surface is None:
111
+ position_on_surface = [0.5, 0.5]
112
+ new_material = material.clone()
113
+ new_basis = new_material.basis
114
+ adatom_position = self._calculate_position_from_2d(material, position_on_surface, distance_z)
115
+ new_basis.add_atom(chemical_element, adatom_position)
116
+ new_material.basis = new_basis
117
+ return [new_material]
118
+
119
+ def _calculate_position_from_2d(
120
+ self, material: Material, position_on_surface: List[float], distance_z: float
121
+ ) -> List[float]:
122
+ max_z = get_atomic_coordinates_extremum(material)
123
+ distance_z = distance_z
124
+ distance_in_crystal_units = distance_z / material.lattice.c
125
+ position = position_on_surface.copy()
126
+ position = position[:2]
127
+ position.append(max_z + distance_in_crystal_units)
128
+ return position
129
+
130
+ def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]:
131
+ return self.create_adatom(
132
+ material=configuration.crystal,
133
+ chemical_element=configuration.chemical_element,
134
+ position_on_surface=configuration.position_on_surface,
135
+ distance_z=configuration.distance_z,
136
+ )
137
+
138
+
139
+ class EquidistantAdatomSlabDefectBuilder(AdatomSlabDefectBuilder):
140
+ def create_adatom(
141
+ self,
142
+ material: Material,
143
+ chemical_element: str = "Si",
144
+ position_on_surface: Optional[List[float]] = None,
145
+ distance_z: float = 2.0,
146
+ ) -> List[Material]:
147
+ """
148
+ Create an adatom with an equidistant XY position among the nearest neighbors
149
+ at the given distance from the surface.
150
+
151
+ Args:
152
+ material: The material to add the adatom to.
153
+ chemical_element: The chemical element of the adatom.
154
+ position_on_surface: The position on the surface of the material.
155
+ distance_z: The distance of the adatom from the surface.
156
+
157
+ Returns:
158
+ The material with the adatom added.
159
+ """
160
+ if position_on_surface is None:
161
+ position_on_surface = [0.5, 0.5]
162
+ equidistant_position = self.get_equidistant_position(material, position_on_surface, distance_z)
163
+ new_material = material.clone()
164
+ if equidistant_position[2] > 1:
165
+ if self.build_parameters.auto_add_vacuum:
166
+ new_material = add_vacuum(material, self.build_parameters.vacuum_thickness)
167
+ equidistant_position = self.get_equidistant_position(new_material, position_on_surface, distance_z)
168
+ else:
169
+ raise ValueError("Not enough vacuum space to place the adatom.")
170
+
171
+ return super().create_adatom(new_material, chemical_element, equidistant_position, distance_z)
172
+
173
+ def get_equidistant_position(
174
+ self, material: Material, position_on_surface: List[float], distance_z: float = 2.0
175
+ ) -> List[float]:
176
+ new_basis = material.basis
177
+ adatom_position = self._calculate_position_from_2d(material, position_on_surface, distance_z)
178
+ neighboring_atoms_ids = get_nearest_neighbors_atom_indices(material, adatom_position)
179
+ # We need to check if neighboring atoms number is the same in pbc
180
+ supercell_material = create_supercell(material, [[3, 0, 0], [0, 3, 0], [0, 0, 1]])
181
+ # Move the coordinate to the central unit cell of the supercell (crystal coordinates)
182
+ supercell_adatom_position = [1 / 3 + adatom_position[0] / 3, 1 / 3 + adatom_position[1] / 3, adatom_position[2]]
183
+ supercell_neighboring_atoms_ids = get_nearest_neighbors_atom_indices(
184
+ supercell_material, supercell_adatom_position
185
+ )
186
+ if neighboring_atoms_ids is None or supercell_neighboring_atoms_ids is None:
187
+ raise ValueError("No neighboring atoms found. Try reducing the distance_z.")
188
+ if len(supercell_neighboring_atoms_ids) != len(neighboring_atoms_ids):
189
+ raise ValueError("Number of neighboring atoms is not the same in PBC. Try increasing the supercell size.")
190
+ neighboring_atoms_coordinates = [new_basis.coordinates.values[atom_id] for atom_id in neighboring_atoms_ids]
191
+
192
+ equidistant_position = get_center_of_coordinates(neighboring_atoms_coordinates)
193
+ equidistant_position[2] = adatom_position[2]
194
+
195
+ return equidistant_position
@@ -5,7 +5,7 @@ from mat3ra.code.entity import InMemoryEntity
5
5
  from mat3ra.made.material import Material
6
6
 
7
7
  from ...analyze import get_closest_site_id_from_position
8
- from .enums import PointDefectTypeEnum
8
+ from .enums import PointDefectTypeEnum, SlabDefectTypeEnum
9
9
 
10
10
 
11
11
  class BaseDefectConfiguration(BaseModel):
@@ -23,7 +23,7 @@ class PointDefectConfiguration(BaseDefectConfiguration, InMemoryEntity):
23
23
  cls, crystal: Material, defect_type: PointDefectTypeEnum, site_id: int, chemical_element: Optional[str] = None
24
24
  ):
25
25
  if not crystal:
26
- RuntimeError("Crystal is not defined")
26
+ raise RuntimeError("Crystal is not defined")
27
27
  position = crystal.coordinates_array[site_id]
28
28
  return cls(crystal=crystal, defect_type=defect_type, position=position, chemical_element=chemical_element)
29
29
 
@@ -36,7 +36,7 @@ class PointDefectConfiguration(BaseDefectConfiguration, InMemoryEntity):
36
36
  chemical_element: Optional[str] = None,
37
37
  ):
38
38
  if not crystal:
39
- RuntimeError("Crystal is not defined")
39
+ raise RuntimeError("Crystal is not defined")
40
40
  closest_site_id = get_closest_site_id_from_position(crystal, approximate_position)
41
41
  return cls.from_site_id(
42
42
  crystal=crystal, defect_type=defect_type, site_id=closest_site_id, chemical_element=chemical_element
@@ -50,3 +50,24 @@ class PointDefectConfiguration(BaseDefectConfiguration, InMemoryEntity):
50
50
  "position": self.position,
51
51
  "chemical_element": self.chemical_element,
52
52
  }
53
+
54
+
55
+ class SlabDefectConfiguration(BaseDefectConfiguration, InMemoryEntity):
56
+ pass
57
+
58
+
59
+ class AdatomSlabDefectConfiguration(SlabDefectConfiguration):
60
+ defect_type: SlabDefectTypeEnum = SlabDefectTypeEnum.ADATOM
61
+ position_on_surface: List[float] = [0.5, 0.5]
62
+ distance_z: float = 2.0
63
+ chemical_element: str = "Si"
64
+
65
+ @property
66
+ def _json(self):
67
+ return {
68
+ "type": "AdatomSlabDefectConfiguration",
69
+ "defect_type": self.defect_type.name,
70
+ "position_on_surface": self.position_on_surface,
71
+ "distance_z": self.distance_z,
72
+ "chemical_element": self.chemical_element,
73
+ }
@@ -5,3 +5,7 @@ class PointDefectTypeEnum(str, Enum):
5
5
  VACANCY = "vacancy"
6
6
  SUBSTITUTION = "substitution"
7
7
  INTERSTITIAL = "interstitial"
8
+
9
+
10
+ class SlabDefectTypeEnum(str, Enum):
11
+ ADATOM = "adatom"
@@ -6,6 +6,7 @@ from ase.calculators.emt import EMT as ASECalculatorEMT
6
6
  from pymatgen.analysis.defects.core import Interstitial as PymatgenInterstitial
7
7
  from pymatgen.analysis.defects.core import Substitution as PymatgenSubstitution
8
8
  from pymatgen.analysis.defects.core import Vacancy as PymatgenVacancy
9
+ from pymatgen.analysis.local_env import VoronoiNN as PymatgenVoronoiNN
9
10
  from pymatgen.core import IStructure as PymatgenIStructure
10
11
  from pymatgen.core import PeriodicSite as PymatgenPeriodicSite
11
12
  from pymatgen.core.interface import Interface as PymatgenInterface
@@ -40,4 +41,5 @@ __all__ = [
40
41
  "ase_add_vacuum",
41
42
  "PymatgenAseAtomsAdaptor",
42
43
  "PymatgenPoscar",
44
+ "PymatgenVoronoiNN",
43
45
  ]
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  from typing import Any, Callable, Dict, List, Optional, Union
3
3
 
4
+ import numpy as np
4
5
  from mat3ra.utils.array import convert_to_array_if_not
5
6
  from mat3ra.utils.mixins import RoundNumericValuesMixin
6
7
  from pydantic import BaseModel
@@ -44,6 +45,19 @@ def are_arrays_equal_by_id_value(array1: List[Dict[str, Any]], array2: List[Dict
44
45
  return map_array_with_id_value_to_array(array1) == map_array_with_id_value_to_array(array2)
45
46
 
46
47
 
48
+ def get_center_of_coordinates(coordinates: List[List[float]]) -> List[float]:
49
+ """
50
+ Calculate the center of the coordinates.
51
+
52
+ Args:
53
+ coordinates (List[List[float]]): The list of coordinates.
54
+
55
+ Returns:
56
+ List[float]: The center of the coordinates.
57
+ """
58
+ return list(np.mean(np.array(coordinates), axis=0))
59
+
60
+
47
61
  class ValueWithId(RoundNumericValuesMixin, BaseModel):
48
62
  id: int = 0
49
63
  value: Any = None
@@ -1,8 +1,28 @@
1
1
  from mat3ra.made.material import Material
2
- from mat3ra.made.tools.build.defect import PointDefectBuilderParameters, PointDefectConfiguration, create_defect
2
+ from mat3ra.made.tools.build.defect import (
3
+ AdatomSlabDefectConfiguration,
4
+ EquidistantAdatomSlabDefectBuilder,
5
+ PointDefectBuilderParameters,
6
+ PointDefectConfiguration,
7
+ create_defect,
8
+ create_slab_defect,
9
+ )
10
+ from mat3ra.made.tools.build.slab import SlabConfiguration, create_slab, get_terminations
11
+ from mat3ra.utils import assertion as assertion_utils
3
12
 
4
13
  clean_material = Material.create(Material.default_config)
5
14
 
15
+ slab_config = SlabConfiguration(
16
+ clean_material,
17
+ (1, 1, 1),
18
+ thickness=3,
19
+ vacuum=6,
20
+ use_orthogonal_z=True,
21
+ xy_supercell_matrix=[[2, 0, 0], [0, 2, 0], [0, 0, 1]],
22
+ )
23
+ t = get_terminations(slab_config)[0]
24
+ slab = create_slab(slab_config, t)
25
+
6
26
 
7
27
  def test_create_vacancy():
8
28
  # vacancy in place of 0 element
@@ -49,3 +69,26 @@ def test_create_defect_from_site_id():
49
69
  {"id": 0, "value": "Si"},
50
70
  {"id": 1, "value": "Ge"},
51
71
  ]
72
+
73
+
74
+ def test_create_adatom():
75
+ # Adatom of Si at 0.5, 0.5 position
76
+ configuration = AdatomSlabDefectConfiguration(
77
+ crystal=slab, position_on_surface=[0.5, 0.5], distance_z=2, chemical_element="Si"
78
+ )
79
+ defect = create_slab_defect(configuration=configuration, builder=None)
80
+
81
+ assert defect.basis.elements.values[-1] == "Si"
82
+ assertion_utils.assert_deep_almost_equal([0.5, 0.5, 0.389826], defect.basis.coordinates.values[-1])
83
+
84
+
85
+ def test_create_adatom_equidistant():
86
+ # Adatom of Si at approximate 0.5, 0.5 position
87
+ configuration = AdatomSlabDefectConfiguration(
88
+ crystal=slab, position_on_surface=[0.5, 0.5], distance_z=2, chemical_element="Si"
89
+ )
90
+ defect = create_slab_defect(configuration=configuration, builder=EquidistantAdatomSlabDefectBuilder())
91
+
92
+ assert defect.basis.elements.values[-1] == "Si"
93
+ # We expect adatom to shift from provided position
94
+ assertion_utils.assert_deep_almost_equal([0.4583333333, 0.541666667, 0.389826], defect.basis.coordinates.values[-1])