@mat3ra/made 2024.7.31-0 → 2024.7.31-1

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.31-0",
3
+ "version": "2024.7.31-1",
4
4
  "description": "MAterials DEsign library",
5
5
  "scripts": {
6
6
  "lint": "eslint --cache src/js tests/js && prettier --write src/js tests/js",
@@ -246,10 +246,12 @@ def get_atom_indices_with_condition_on_coordinates(
246
246
  List[int]: List of indices of atoms whose coordinates satisfy the condition.
247
247
  """
248
248
  new_material = material.clone()
249
+ new_basis = new_material.basis
249
250
  if use_cartesian_coordinates:
250
- new_basis = new_material.basis
251
251
  new_basis.to_cartesian()
252
- new_material.basis = new_basis
252
+ else:
253
+ new_basis.to_crystal()
254
+ new_material.basis = new_basis
253
255
  coordinates = new_material.basis.coordinates.to_array_of_values_with_ids()
254
256
 
255
257
  selected_indices = []
@@ -313,10 +315,12 @@ def get_atomic_coordinates_extremum(
313
315
  float: Minimum or maximum of coordinates along the specified axis.
314
316
  """
315
317
  new_material = material.clone()
318
+ new_basis = new_material.basis
316
319
  if use_cartesian_coordinates:
317
- new_basis = new_material.basis
318
320
  new_basis.to_cartesian()
319
- new_material.basis = new_basis
321
+ else:
322
+ new_basis.to_crystal()
323
+ new_material.basis = new_basis
320
324
  coordinates = new_material.basis.coordinates.to_array_of_values_with_ids()
321
325
  values = [coord.value[{"x": 0, "y": 1, "z": 2}[axis]] for coord in coordinates]
322
326
  return getattr(np, extremum)(values)
@@ -1,6 +1,5 @@
1
1
  from typing import Optional, Union
2
2
 
3
- from mat3ra.utils.factory import BaseFactory
4
3
  from mat3ra.made.material import Material
5
4
 
6
5
  from .builders import (
@@ -14,14 +13,7 @@ from .builders import (
14
13
  )
15
14
  from .configuration import PointDefectConfiguration, AdatomSlabPointDefectConfiguration, IslandSlabDefectConfiguration
16
15
  from .enums import PointDefectTypeEnum
17
-
18
-
19
- class DefectBuilderFactory(BaseFactory):
20
- __class_registry__ = {
21
- PointDefectTypeEnum.VACANCY: "mat3ra.made.tools.build.defect.builders.VacancyPointDefectBuilder",
22
- PointDefectTypeEnum.SUBSTITUTION: "mat3ra.made.tools.build.defect.builders.SubstitutionPointDefectBuilder",
23
- PointDefectTypeEnum.INTERSTITIAL: "mat3ra.made.tools.build.defect.builders.InterstitialPointDefectBuilder",
24
- }
16
+ from .factories import DefectBuilderFactory
25
17
 
26
18
 
27
19
  def create_defect(
@@ -1,9 +1,10 @@
1
- from typing import List, Callable, Optional
1
+ from typing import List, Callable, Optional, Union
2
2
 
3
3
  import numpy as np
4
4
  from pydantic import BaseModel
5
5
  from mat3ra.made.material import Material
6
6
 
7
+
7
8
  from ...third_party import (
8
9
  PymatgenStructure,
9
10
  PymatgenPeriodicSite,
@@ -39,7 +40,9 @@ from .configuration import (
39
40
  AdatomSlabPointDefectConfiguration,
40
41
  IslandSlabDefectConfiguration,
41
42
  TerraceSlabDefectConfiguration,
43
+ PointDefectPairConfiguration,
42
44
  )
45
+ from .factories import DefectBuilderFactory
43
46
 
44
47
 
45
48
  class PointDefectBuilderParameters(BaseModel):
@@ -47,7 +50,7 @@ class PointDefectBuilderParameters(BaseModel):
47
50
 
48
51
 
49
52
  class DefectBuilder(BaseBuilder):
50
- def create_isolated_defect(self, material: Material, defect_configuration: PointDefectConfiguration) -> Material:
53
+ def create_isolated_defect(self, defect_configuration: PointDefectConfiguration) -> Material:
51
54
  raise NotImplementedError
52
55
 
53
56
 
@@ -171,8 +174,7 @@ class AdatomSlabDefectBuilder(SlabDefectBuilder):
171
174
  def _calculate_coordinate_from_position_and_distance(
172
175
  self, material: Material, position_on_surface: List[float], distance_z: float
173
176
  ) -> List[float]:
174
- max_z = get_atomic_coordinates_extremum(material)
175
- distance_z = distance_z
177
+ max_z = get_atomic_coordinates_extremum(material, use_cartesian_coordinates=False)
176
178
  distance_in_crystal_units = distance_z / material.lattice.c
177
179
  return [position_on_surface[0], position_on_surface[1], max_z + distance_in_crystal_units]
178
180
 
@@ -272,12 +274,16 @@ class CrystalSiteAdatomSlabDefectBuilder(AdatomSlabDefectBuilder):
272
274
  )
273
275
  return approximate_adatom_coordinate_cartesian
274
276
 
275
- def create_isolated_defect(
277
+ def create_isolated_adatom(
276
278
  self,
277
- material: Material,
278
- approximate_adatom_coordinate_cartesian: List[float],
279
- chemical_element: Optional[str] = None,
279
+ original_material: Material,
280
+ configuration: AdatomSlabPointDefectConfiguration,
280
281
  ) -> Material:
282
+ material: Material = configuration.crystal
283
+ approximate_adatom_coordinate_cartesian: List[float] = self.calculate_approximate_adatom_coordinate(
284
+ original_material, configuration.position_on_surface, configuration.distance_z
285
+ )
286
+ chemical_element: Optional[str] = configuration.chemical_element or None
281
287
  if chemical_element is None:
282
288
  closest_site_id = get_closest_site_id_from_coordinate(material, approximate_adatom_coordinate_cartesian)
283
289
  else:
@@ -310,21 +316,77 @@ class CrystalSiteAdatomSlabDefectBuilder(AdatomSlabDefectBuilder):
310
316
  position_on_surface = [0.5, 0.5]
311
317
 
312
318
  new_material = material.clone()
313
-
314
- approximate_adatom_coordinate_cartesian = self.calculate_approximate_adatom_coordinate(
315
- new_material, position_on_surface, distance_z
316
- )
317
-
318
319
  material_with_additional_layer = self.create_material_with_additional_layers(new_material)
319
320
  material_with_additional_layer.to_cartesian()
320
321
 
321
- only_adatom_material = self.create_isolated_defect(
322
- material_with_additional_layer, approximate_adatom_coordinate_cartesian, chemical_element
322
+ only_adatom_material = self.create_isolated_adatom(
323
+ original_material=new_material,
324
+ configuration=AdatomSlabPointDefectConfiguration(
325
+ crystal=material_with_additional_layer,
326
+ chemical_element=chemical_element,
327
+ position_on_surface=position_on_surface,
328
+ distance_z=distance_z,
329
+ ),
323
330
  )
324
331
 
325
332
  return self.merge_slab_and_defect(new_material, only_adatom_material)
326
333
 
327
334
 
335
+ class DefectPairBuilder(DefectBuilder):
336
+ def create_defect_pair(
337
+ self,
338
+ primary_defect_configuration: Union[PointDefectConfiguration, AdatomSlabPointDefectConfiguration],
339
+ secondary_defect_configuration: Union[PointDefectConfiguration, AdatomSlabPointDefectConfiguration],
340
+ ) -> Material:
341
+ """
342
+ Create a pair of point defects in the material.
343
+
344
+ Args:
345
+ primary_defect_configuration: The configuration of the first defect.
346
+ secondary_defect_configuration: The configuration of the second defect.
347
+
348
+ Returns:
349
+ Material: The material with both defects added.
350
+ """
351
+ primary_material = self.create_isolated_defect(primary_defect_configuration)
352
+ # Remove metadata to allow for independent defect creation
353
+ primary_material.metadata["build"] = primary_defect_configuration.crystal.metadata["build"]
354
+ primary_material.name = primary_defect_configuration.crystal.name
355
+ secondary_defect_configuration.crystal = primary_material
356
+ secondary_material = self.create_isolated_defect(secondary_defect_configuration)
357
+
358
+ return secondary_material
359
+
360
+
361
+ class PointDefectPairBuilder(PointDefectBuilder, DefectPairBuilder):
362
+ _ConfigurationType: type(PointDefectPairConfiguration) = PointDefectPairConfiguration # type: ignore
363
+ _GeneratedItemType: Material = Material
364
+
365
+ def create_isolated_defect(self, defect_configuration: PointDefectConfiguration) -> Material:
366
+ key = defect_configuration.defect_type.value
367
+ if hasattr(defect_configuration, "placement_method") and defect_configuration.placement_method is not None:
368
+ key += f":{defect_configuration.placement_method.name}".lower()
369
+ builder_class = DefectBuilderFactory.get_class_by_name(key)
370
+ defect_builder = builder_class()
371
+ return defect_builder.get_material(defect_configuration)
372
+
373
+ def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]:
374
+ return [
375
+ self.create_defect_pair(
376
+ primary_defect_configuration=configuration.primary_defect_configuration,
377
+ secondary_defect_configuration=configuration.secondary_defect_configuration,
378
+ )
379
+ ]
380
+
381
+ def _update_material_name(self, material: Material, configuration: _ConfigurationType) -> Material:
382
+ updated_material = super()._update_material_name(material, configuration)
383
+ name_1 = configuration.primary_defect_configuration.defect_type.name.capitalize()
384
+ name_2 = configuration.secondary_defect_configuration.defect_type.name.capitalize()
385
+ new_name = f"{updated_material.name}, {name_1} and {name_2} Defect Pair"
386
+ updated_material.name = new_name
387
+ return updated_material
388
+
389
+
328
390
  class IslandSlabDefectBuilder(SlabDefectBuilder):
329
391
  _ConfigurationType: type(IslandSlabDefectConfiguration) = IslandSlabDefectConfiguration # type: ignore
330
392
  _GeneratedItemType: Material = Material
@@ -1,4 +1,4 @@
1
- from typing import Optional, List, Any, Callable, Dict, Tuple
1
+ from typing import Optional, List, Any, Callable, Dict, Tuple, Union
2
2
  from pydantic import BaseModel
3
3
 
4
4
  from mat3ra.code.entity import InMemoryEntity
@@ -6,7 +6,7 @@ from mat3ra.made.material import Material
6
6
 
7
7
  from ...analyze import get_closest_site_id_from_coordinate, get_atomic_coordinates_extremum
8
8
  from ...utils import CoordinateConditionBuilder
9
- from .enums import PointDefectTypeEnum, SlabDefectTypeEnum
9
+ from .enums import PointDefectTypeEnum, SlabDefectTypeEnum, AtomPlacementMethodEnum, ComplexDefectTypeEnum
10
10
 
11
11
 
12
12
  class BaseDefectConfiguration(BaseModel):
@@ -144,12 +144,15 @@ class AdatomSlabPointDefectConfiguration(SlabPointDefectConfiguration):
144
144
  """
145
145
 
146
146
  defect_type: PointDefectTypeEnum = PointDefectTypeEnum.ADATOM
147
+ placement_method: AtomPlacementMethodEnum = AtomPlacementMethodEnum.COORDINATE
147
148
 
148
149
  @property
149
150
  def _json(self):
150
151
  return {
151
152
  **super()._json,
152
153
  "type": self.get_cls_name(),
154
+ "defect_type": self.defect_type.name,
155
+ "placement_method": self.placement_method.name,
153
156
  }
154
157
 
155
158
 
@@ -212,3 +215,26 @@ class TerraceSlabDefectConfiguration(SlabDefectConfiguration):
212
215
  "use_cartesian_coordinates": self.use_cartesian_coordinates,
213
216
  "rotate_to_match_pbc": self.rotate_to_match_pbc,
214
217
  }
218
+
219
+
220
+ class PointDefectPairConfiguration(BaseDefectConfiguration, InMemoryEntity):
221
+ """
222
+ Configuration for a pair of point defects.
223
+
224
+ Args:
225
+ primary_defect_configuration: The first defect configuration.
226
+ secondary_defect_configuration: The second defect configuration. Material is used from the primary defect.
227
+ """
228
+
229
+ defect_type: ComplexDefectTypeEnum = ComplexDefectTypeEnum.PAIR
230
+ primary_defect_configuration: Union[PointDefectConfiguration, AdatomSlabPointDefectConfiguration]
231
+ secondary_defect_configuration: Union[PointDefectConfiguration, AdatomSlabPointDefectConfiguration]
232
+
233
+ @property
234
+ def _json(self):
235
+ return {
236
+ "type": self.get_cls_name(),
237
+ "defect_type": self.defect_type.name,
238
+ "primary_defect_configuration": self.primary_defect_configuration.to_json(),
239
+ "secondary_defect_configuration": self.secondary_defect_configuration.to_json(),
240
+ }
@@ -11,3 +11,18 @@ class PointDefectTypeEnum(str, Enum):
11
11
  class SlabDefectTypeEnum(str, Enum):
12
12
  ISLAND = "island"
13
13
  TERRACE = "terrace"
14
+
15
+
16
+ class ComplexDefectTypeEnum(str, Enum):
17
+ PAIR = "pair"
18
+
19
+
20
+ class AtomPlacementMethodEnum(str, Enum):
21
+ # Places the atom at the exact given coordinate.
22
+ COORDINATE = "coordinate"
23
+ # Among existing atoms, selects the closest one to the given coordinate.
24
+ CLOSEST_SITE = "closest_site"
25
+ # Places the atom at the equal distance from the closest atoms to the given coordinate.
26
+ EQUIDISTANT = "equidistant"
27
+ # Places the atom at the existing or extrapolated crystal site closest to the given coordinate.
28
+ CRYSTAL_SITE = "crystal_site"
@@ -0,0 +1,12 @@
1
+ from mat3ra.utils.factory import BaseFactory
2
+
3
+
4
+ class DefectBuilderFactory(BaseFactory):
5
+ __class_registry__ = {
6
+ "vacancy": "mat3ra.made.tools.build.defect.builders.VacancyPointDefectBuilder",
7
+ "substitution": "mat3ra.made.tools.build.defect.builders.SubstitutionPointDefectBuilder",
8
+ "interstitial": "mat3ra.made.tools.build.defect.builders.InterstitialPointDefectBuilder",
9
+ "adatom:coordinate": "mat3ra.made.tools.build.defect.builders.AdatomSlabDefectBuilder",
10
+ "adatom:crystal_site": "mat3ra.made.tools.build.defect.builders.CrystalSiteAdatomSlabDefectBuilder",
11
+ "adatom:equidistant": "mat3ra.made.tools.build.defect.builders.EquidistantAdatomSlabDefectBuilder",
12
+ }
@@ -4,7 +4,7 @@ from typing import Any, Dict
4
4
  from ase.build import bulk
5
5
  from mat3ra.made.material import Material
6
6
  from mat3ra.made.tools.build.interface.termination_pair import TerminationPair
7
- from mat3ra.made.tools.build.slab import SlabConfiguration, get_terminations
7
+ from mat3ra.made.tools.build.slab import SlabConfiguration, create_slab, get_terminations
8
8
  from mat3ra.made.tools.convert import from_ase
9
9
  from pymatgen.analysis.elasticity.strain import Strain
10
10
  from pymatgen.core.interface import Interface
@@ -236,3 +236,27 @@ SI_SLAB_VACUUM["basis"]["coordinates"] = [
236
236
  SI_SLAB_VACUUM["basis"]["cell"] = [[3.867, 0.0, 0.0], [-0.0, 3.867, 0.0], [0.0, 0.0, 15.937527692]]
237
237
  SI_SLAB_VACUUM["lattice"]["c"] = 15.937527692
238
238
  SI_SLAB_VACUUM["lattice"]["vectors"]["c"] = [0.0, 0.0, 15.937527692]
239
+
240
+
241
+ clean_material = Material.create(Material.default_config)
242
+ slab_111_config = SlabConfiguration(
243
+ bulk=clean_material,
244
+ miller_indices=(1, 1, 1),
245
+ thickness=4,
246
+ vacuum=6,
247
+ xy_supercell_matrix=[[1, 0], [0, 1]],
248
+ use_orthogonal_z=True,
249
+ )
250
+ t_111 = get_terminations(slab_111_config)[0]
251
+ SLAB_111 = create_slab(slab_111_config, t_111)
252
+
253
+ slab_001_config = SlabConfiguration(
254
+ bulk=clean_material,
255
+ miller_indices=(0, 0, 1),
256
+ thickness=3,
257
+ vacuum=3,
258
+ xy_supercell_matrix=[[2, 0], [0, 1]],
259
+ use_orthogonal_z=True,
260
+ )
261
+ t_001 = get_terminations(slab_001_config)[0]
262
+ SLAB_001 = create_slab(slab_001_config, t_001)
@@ -5,27 +5,26 @@ from mat3ra.made.tools.build.defect import (
5
5
  EquidistantAdatomSlabDefectBuilder,
6
6
  PointDefectBuilderParameters,
7
7
  PointDefectConfiguration,
8
+ PointDefectTypeEnum,
8
9
  create_defect,
9
10
  create_slab_defect,
10
11
  )
11
- from mat3ra.made.tools.build.defect.builders import IslandSlabDefectBuilder, TerraceSlabDefectBuilder
12
- from mat3ra.made.tools.build.defect.configuration import IslandSlabDefectConfiguration, TerraceSlabDefectConfiguration
13
- from mat3ra.made.tools.build.slab import SlabConfiguration, create_slab, get_terminations
12
+ from mat3ra.made.tools.build.defect.builders import (
13
+ IslandSlabDefectBuilder,
14
+ PointDefectPairBuilder,
15
+ TerraceSlabDefectBuilder,
16
+ )
17
+ from mat3ra.made.tools.build.defect.configuration import (
18
+ IslandSlabDefectConfiguration,
19
+ PointDefectPairConfiguration,
20
+ TerraceSlabDefectConfiguration,
21
+ )
14
22
  from mat3ra.made.tools.utils import CoordinateConditionBuilder
15
23
  from mat3ra.utils import assertion as assertion_utils
16
24
 
17
- clean_material = Material.create(Material.default_config)
25
+ from .fixtures import SLAB_001, SLAB_111
18
26
 
19
- slab_config = SlabConfiguration(
20
- bulk=clean_material,
21
- miller_indices=(1, 1, 1),
22
- thickness=4,
23
- vacuum=6,
24
- xy_supercell_matrix=[[1, 0], [0, 1]],
25
- use_orthogonal_z=True,
26
- )
27
- t = get_terminations(slab_config)[0]
28
- slab = create_slab(slab_config, t)
27
+ clean_material = Material.create(Material.default_config)
29
28
 
30
29
 
31
30
  def test_create_vacancy():
@@ -78,7 +77,7 @@ def test_create_defect_from_site_id():
78
77
  def test_create_adatom():
79
78
  # Adatom of Si at 0.5, 0.5 position
80
79
  configuration = AdatomSlabPointDefectConfiguration(
81
- crystal=slab, position_on_surface=[0.5, 0.5], distance_z=2, chemical_element="Si"
80
+ crystal=SLAB_111, position_on_surface=[0.5, 0.5], distance_z=2, chemical_element="Si"
82
81
  )
83
82
  defect = create_slab_defect(configuration=configuration, builder=None)
84
83
 
@@ -89,7 +88,7 @@ def test_create_adatom():
89
88
  def test_create_adatom_equidistant():
90
89
  # Adatom of Si at approximate 0.5, 0.5 position
91
90
  configuration = AdatomSlabPointDefectConfiguration(
92
- crystal=slab, position_on_surface=[0.5, 0.5], distance_z=2, chemical_element="Si"
91
+ crystal=SLAB_111, position_on_surface=[0.5, 0.5], distance_z=2, chemical_element="Si"
93
92
  )
94
93
  defect = create_slab_defect(configuration=configuration, builder=EquidistantAdatomSlabDefectBuilder())
95
94
 
@@ -103,7 +102,7 @@ def test_create_adatom_equidistant():
103
102
  def test_create_crystal_site_adatom():
104
103
  # Adatom of Si (autodetect) at approximate 0.5, 0.5 position
105
104
  configuration = AdatomSlabPointDefectConfiguration(
106
- crystal=slab, position_on_surface=[0.5, 0.5], distance_z=2, chemical_element=None
105
+ crystal=SLAB_111, position_on_surface=[0.5, 0.5], distance_z=2, chemical_element=None
107
106
  )
108
107
  builder = CrystalSiteAdatomSlabDefectBuilder()
109
108
  defect = create_slab_defect(configuration=configuration, builder=builder)
@@ -117,7 +116,7 @@ def test_create_crystal_site_adatom():
117
116
  def test_create_island():
118
117
  condition = CoordinateConditionBuilder.cylinder(center_position=[0.625, 0.5], radius=0.25, min_z=0, max_z=1)
119
118
  island_config = IslandSlabDefectConfiguration(
120
- crystal=slab,
119
+ crystal=SLAB_111,
121
120
  defect_type="island",
122
121
  condition=condition,
123
122
  thickness=1,
@@ -126,27 +125,35 @@ def test_create_island():
126
125
  defect = create_slab_defect(configuration=island_config, builder=IslandSlabDefectBuilder())
127
126
 
128
127
  # Only one atom is in the island for this configuration
129
- assert len(defect.basis.elements.values) == len(slab.basis.elements.values) + 1
128
+ assert len(defect.basis.elements.values) == len(SLAB_111.basis.elements.values) + 1
130
129
  assert defect.basis.elements.values[-1] == "Si"
131
130
 
132
131
 
133
132
  def test_create_terrace():
134
- slab_config = SlabConfiguration(
135
- bulk=clean_material,
136
- miller_indices=(0, 0, 1),
137
- thickness=3,
138
- vacuum=3,
139
- xy_supercell_matrix=[[2, 0], [0, 1]],
140
- use_orthogonal_z=True,
141
- )
142
- t = get_terminations(slab_config)[0]
143
- slab = create_slab(slab_config, t)
144
-
145
133
  config = TerraceSlabDefectConfiguration(
146
- crystal=slab,
134
+ crystal=SLAB_001,
147
135
  cut_direction=[1, 0, 0],
148
136
  pivot_coordinate=[0.5, 0.5, 0.5],
149
137
  number_of_added_layers=1,
150
138
  )
151
139
  new_slab = TerraceSlabDefectBuilder().get_material(configuration=config)
152
140
  assertion_utils.assert_deep_almost_equal([0.720082355, 0.5, 0.461401798], new_slab.basis.coordinates.values[42])
141
+
142
+
143
+ def test_create_defect_pair():
144
+ defect1_config = PointDefectConfiguration.from_approximate_position(
145
+ crystal=SLAB_001,
146
+ defect_type=PointDefectTypeEnum.VACANCY,
147
+ approximate_coordinate=[0.5, 0.5, 0.25],
148
+ )
149
+ defect2_config = PointDefectConfiguration(
150
+ defect_type=PointDefectTypeEnum.INTERSTITIAL, coordinate=[0.5, 0.55, 0.35], chemical_element="P"
151
+ )
152
+ defect_pair_config = PointDefectPairConfiguration(
153
+ primary_defect_configuration=defect1_config,
154
+ secondary_defect_configuration=defect2_config,
155
+ )
156
+ defect_material = PointDefectPairBuilder().get_material(defect_pair_config)
157
+
158
+ assertion_utils.assert_deep_almost_equal("P", defect_material.basis.elements.values[0])
159
+ assertion_utils.assert_deep_almost_equal([0.5, 0.55, 0.35], defect_material.basis.coordinates.values[0])