@mat3ra/made 2024.8.16-0 → 2024.8.20-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.8.16-0",
3
+ "version": "2024.8.20-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",
@@ -95,3 +95,16 @@ class Material(HasDescriptionHasMetadataNamedDefaultableInMemoryEntity):
95
95
  new_basis = self.basis.copy()
96
96
  new_basis.coordinates.values = coordinates
97
97
  self.basis = new_basis
98
+
99
+ def set_new_lattice_vectors(
100
+ self, lattice_vector1: List[float], lattice_vector2: List[float], lattice_vector3: List[float]
101
+ ) -> None:
102
+ new_basis = self.basis.copy()
103
+ new_basis.to_cartesian()
104
+ new_basis.cell.vector1 = lattice_vector1
105
+ new_basis.cell.vector2 = lattice_vector2
106
+ new_basis.cell.vector3 = lattice_vector3
107
+ new_basis.to_crystal()
108
+ self.basis = new_basis
109
+ lattice = Lattice.from_vectors_array([lattice_vector1, lattice_vector2, lattice_vector3])
110
+ self.lattice = lattice
@@ -1,10 +1,28 @@
1
1
  from typing import List, Optional, Any
2
2
 
3
+ from mat3ra.code.entity import InMemoryEntity
3
4
  from pydantic import BaseModel
4
5
 
5
6
  from ...material import Material
6
7
 
7
8
 
9
+ class BaseConfiguration(BaseModel, InMemoryEntity):
10
+ """
11
+ Base class for material build configurations.
12
+ This class provides an interface for defining the configuration parameters.
13
+
14
+ The class is designed to be subclassed and the subclass should define the following attributes:
15
+ - `_json`: The JSON representation of the configuration.
16
+ """
17
+
18
+ class Config:
19
+ arbitrary_types_allowed = True
20
+
21
+ @property
22
+ def _json(self):
23
+ raise NotImplementedError
24
+
25
+
8
26
  class BaseBuilder(BaseModel):
9
27
  """
10
28
  Base class for material builders.
@@ -40,7 +40,8 @@ def safely_select_termination_pair(
40
40
  ) -> TerminationPair:
41
41
  """
42
42
  Attempt finding provided in generated terminations to find a complete match,
43
- if match isn't found, get terminations with equivalent chemical elements.
43
+ if match isn't found, get terminations with equivalent chemical elements,
44
+ if that fails, return the first generated termination pair.
44
45
  """
45
46
  provided_film_termination = provided_termination_pair.film_termination
46
47
  provided_substrate_termination = provided_termination_pair.substrate_termination
@@ -55,5 +56,8 @@ def safely_select_termination_pair(
55
56
  == provided_substrate_termination.chemical_elements
56
57
  ):
57
58
  hotfix_termination_pair = termination_pair
58
- print("Interface will be built with terminations: ", hotfix_termination_pair)
59
+ else:
60
+ hotfix_termination_pair = generated_termination_pairs[0]
61
+ print("Interface will be built with terminations: ", hotfix_termination_pair)
62
+
59
63
  return hotfix_termination_pair
@@ -0,0 +1,9 @@
1
+ from mat3ra.made.material import Material
2
+
3
+ from .builders import NanoribbonBuilder
4
+ from .configuration import NanoribbonConfiguration
5
+
6
+
7
+ def create_nanoribbon(configuration: NanoribbonConfiguration) -> Material:
8
+ builder = NanoribbonBuilder()
9
+ return builder.get_material(configuration)
@@ -0,0 +1,149 @@
1
+ from typing import List, Optional, Any, Tuple
2
+
3
+ import numpy as np
4
+
5
+ from mat3ra.made.material import Material
6
+ from mat3ra.made.tools.build import BaseBuilder
7
+ from mat3ra.made.tools.build.supercell import create_supercell
8
+ from mat3ra.made.tools.modify import filter_by_rectangle_projection, wrap_to_unit_cell
9
+
10
+ from ...modify import translate_to_center
11
+ from .configuration import NanoribbonConfiguration
12
+ from .enums import EdgeTypes
13
+
14
+
15
+ class NanoribbonBuilder(BaseBuilder):
16
+ """
17
+ Builder class for creating a nanoribbon from a material.
18
+
19
+ The process creates a supercell with large enough dimensions to contain the nanoribbon and then
20
+ filters the supercell to only include the nanoribbon. The supercell is then centered and returned as the nanoribbon.
21
+ The nanoribbon can have either Armchair or Zigzag edge. The edge is defined along the vector 1 of the material cell,
22
+ which corresponds to [1,0,0] direction.
23
+ """
24
+
25
+ _ConfigurationType: type(NanoribbonConfiguration) = NanoribbonConfiguration # type: ignore
26
+ _GeneratedItemType: Material = Material
27
+ _PostProcessParametersType: Any = None
28
+
29
+ def create_nanoribbon(self, config: NanoribbonConfiguration) -> Material:
30
+ material = config.material
31
+ (
32
+ length_cartesian,
33
+ width_cartesian,
34
+ height_cartesian,
35
+ vacuum_length_cartesian,
36
+ vacuum_width_cartesian,
37
+ ) = self._calculate_cartesian_dimensions(config, material)
38
+ length_lattice_vector, width_lattice_vector, height_lattice_vector = self._get_new_lattice_vectors(
39
+ length_cartesian,
40
+ width_cartesian,
41
+ height_cartesian,
42
+ vacuum_length_cartesian,
43
+ vacuum_width_cartesian,
44
+ config.edge_type,
45
+ )
46
+ n = max(config.length, config.width)
47
+ large_supercell_to_cut = create_supercell(material, np.diag([2 * n, 2 * n, 1]))
48
+
49
+ min_coordinate, max_coordinate = self._calculate_coordinates_of_cut(
50
+ length_cartesian, width_cartesian, height_cartesian, config.edge_type
51
+ )
52
+ nanoribbon = filter_by_rectangle_projection(
53
+ large_supercell_to_cut,
54
+ min_coordinate=min_coordinate,
55
+ max_coordinate=max_coordinate,
56
+ use_cartesian_coordinates=True,
57
+ )
58
+ nanoribbon.set_new_lattice_vectors(length_lattice_vector, width_lattice_vector, height_lattice_vector)
59
+ return translate_to_center(nanoribbon)
60
+
61
+ @staticmethod
62
+ def _calculate_cartesian_dimensions(config: NanoribbonConfiguration, material: Material):
63
+ """
64
+ Calculate the dimensions of the nanoribbon in the cartesian coordinate system.
65
+ """
66
+ nanoribbon_width = config.width
67
+ nanoribbon_length = config.length
68
+ vacuum_width = config.vacuum_width
69
+ vacuum_length = config.vacuum_length
70
+ edge_type = config.edge_type
71
+
72
+ if edge_type == EdgeTypes.armchair:
73
+ nanoribbon_length, nanoribbon_width = nanoribbon_width, nanoribbon_length
74
+ vacuum_width, vacuum_length = vacuum_length, vacuum_width
75
+
76
+ length_cartesian = nanoribbon_length * np.dot(np.array(material.basis.cell.vector1), np.array([1, 0, 0]))
77
+ width_cartesian = nanoribbon_width * np.dot(np.array(material.basis.cell.vector2), np.array([0, 1, 0]))
78
+ height_cartesian = np.dot(np.array(material.basis.cell.vector3), np.array([0, 0, 1]))
79
+ vacuum_length_cartesian = vacuum_length * np.dot(np.array(material.basis.cell.vector1), np.array([1, 0, 0]))
80
+ vacuum_width_cartesian = vacuum_width * np.dot(np.array(material.basis.cell.vector2), np.array([0, 1, 0]))
81
+
82
+ return length_cartesian, width_cartesian, height_cartesian, vacuum_length_cartesian, vacuum_width_cartesian
83
+
84
+ @staticmethod
85
+ def _get_new_lattice_vectors(
86
+ length: float, width: float, height: float, vacuum_length: float, vacuum_width: float, edge_type: EdgeTypes
87
+ ) -> Tuple[List[float], List[float], List[float]]:
88
+ """
89
+ Calculate the new lattice vectors for the nanoribbon.
90
+
91
+ Args:
92
+ length: Length of the nanoribbon.
93
+ width: Width of the nanoribbon.
94
+ height: Height of the nanoribbon.
95
+ vacuum_length: Length of the vacuum region.
96
+ vacuum_width: Width of the vacuum region.
97
+ edge_type: Type of the edge of the nanoribbon.
98
+
99
+ Returns:
100
+ Tuple of the new lattice vectors.
101
+ """
102
+ length_lattice_vector = [length + vacuum_length, 0, 0]
103
+ width_lattice_vector = [0, width + vacuum_width, 0]
104
+ height_lattice_vector = [0, 0, height]
105
+
106
+ if edge_type == EdgeTypes.armchair:
107
+ length_lattice_vector, width_lattice_vector = width_lattice_vector, length_lattice_vector
108
+
109
+ return length_lattice_vector, width_lattice_vector, height_lattice_vector
110
+
111
+ @staticmethod
112
+ def _calculate_coordinates_of_cut(
113
+ length: float, width: float, height: float, edge_type: EdgeTypes
114
+ ) -> Tuple[List[float], List[float]]:
115
+ """
116
+ Calculate the coordinates of the rectangular nanoribbon cut from the supercell.
117
+
118
+ Args:
119
+ length: Length of the nanoribbon.
120
+ width: Width of the nanoribbon.
121
+ height: Height of the nanoribbon.
122
+ edge_type: Type of the edge of the nanoribbon.
123
+
124
+ Returns:
125
+ Tuple of the minimum and maximum coordinates of the cut.
126
+ """
127
+ edge_nudge_value = 0.01
128
+ conditional_nudge_value = edge_nudge_value * (
129
+ -1 * (edge_type == EdgeTypes.armchair) + 1 * (edge_type == EdgeTypes.zigzag)
130
+ )
131
+ min_coordinate = [-edge_nudge_value, conditional_nudge_value, 0]
132
+ max_coordinate = [length - edge_nudge_value, width + conditional_nudge_value, height]
133
+ return min_coordinate, max_coordinate
134
+
135
+ def _generate(self, configuration: NanoribbonConfiguration) -> List[_GeneratedItemType]:
136
+ nanoribbon = self.create_nanoribbon(configuration)
137
+ return [nanoribbon]
138
+
139
+ def _post_process(
140
+ self,
141
+ items: List[_GeneratedItemType],
142
+ post_process_parameters: Optional[_PostProcessParametersType],
143
+ ) -> List[Material]:
144
+ return [wrap_to_unit_cell(item) for item in items]
145
+
146
+ def _update_material_name(self, material: Material, configuration: NanoribbonConfiguration) -> Material:
147
+ edge_type = configuration.edge_type.capitalize()
148
+ material.name = f"{material.name} ({edge_type} nanoribbon)"
149
+ return material
@@ -0,0 +1,37 @@
1
+ from mat3ra.made.material import Material
2
+ from mat3ra.made.tools.build.nanoribbon.enums import EdgeTypes
3
+
4
+ from ...build import BaseConfiguration
5
+
6
+
7
+ class NanoribbonConfiguration(BaseConfiguration):
8
+ """
9
+ Configuration for building a nanoribbon from a material.
10
+
11
+
12
+ Attributes:
13
+ material (Material): The material to build the nanoribbon from.
14
+ width (int): The width of the nanoribbon in number of unit cells.
15
+ length (int): The length of the nanoribbon in number of unit cells.
16
+ vacuum_width (int): The width of the vacuum region in number of unit cells.
17
+ vacuum_length (int): The length of the vacuum region in number of unit cells.
18
+ edge_type (EdgeTypes): The type of edge to use for the nanoribbon, either zigzag or armchair.
19
+ """
20
+
21
+ material: Material
22
+ width: int # in number of unit cells
23
+ length: int # in number of unit cells
24
+ vacuum_width: int = 3 # in number of unit cells
25
+ vacuum_length: int = 0 # in number of unit cells
26
+ edge_type: EdgeTypes = EdgeTypes.zigzag
27
+
28
+ @property
29
+ def _json(self):
30
+ return {
31
+ "material": self.material.to_json(),
32
+ "width": self.width,
33
+ "length": self.length,
34
+ "vacuum_width": self.vacuum_width,
35
+ "vacuum_length": self.vacuum_length,
36
+ "edge_type": self.edge_type,
37
+ }
@@ -0,0 +1,10 @@
1
+ from enum import Enum
2
+
3
+
4
+ class EdgeTypes(str, Enum):
5
+ """
6
+ Enum for nanoribbon edge types.
7
+ """
8
+
9
+ zigzag = "zigzag"
10
+ armchair = "armchair"
@@ -87,6 +87,31 @@ def translate_by_vector(
87
87
  return Material(from_ase(atoms))
88
88
 
89
89
 
90
+ def translate_to_center(material: Material, axes: Optional[List[str]] = None) -> Material:
91
+ """
92
+ Center the material in the unit cell.
93
+
94
+ Args:
95
+ material (Material): The material object to center.
96
+ axes (List[str]): The axes to center the material along.
97
+ Returns:
98
+ Material: The centered material object.
99
+ """
100
+ new_material = material.clone()
101
+ new_material.to_crystal()
102
+ if axes is None:
103
+ axes = ["x", "y", "z"]
104
+ min_x = get_atomic_coordinates_extremum(material, axis="x", extremum="min") if "x" in axes else 0
105
+ max_x = get_atomic_coordinates_extremum(material, axis="x", extremum="max") if "x" in axes else 1
106
+ min_y = get_atomic_coordinates_extremum(material, axis="y", extremum="min") if "y" in axes else 0
107
+ max_y = get_atomic_coordinates_extremum(material, axis="y", extremum="max") if "y" in axes else 1
108
+ if "z" in axes:
109
+ material = translate_to_z_level(material, z_level="center")
110
+
111
+ material = translate_by_vector(material, vector=[(1 - min_x - max_x) / 2, (1 - min_y - max_y) / 2, 0])
112
+ return material
113
+
114
+
90
115
  def wrap_to_unit_cell(material: Material) -> Material:
91
116
  """
92
117
  Wrap the material to the unit cell.
@@ -289,3 +289,161 @@ GRAPHENE = {
289
289
  },
290
290
  "isNonPeriodic": False,
291
291
  }
292
+
293
+ GRAPHENE_ZIGZAG_NANORIBBON = {
294
+ "name": "Graphene (Zigzag nanoribbon)",
295
+ "basis": {
296
+ "elements": [
297
+ {"id": 0, "value": "C"},
298
+ {"id": 1, "value": "C"},
299
+ {"id": 2, "value": "C"},
300
+ {"id": 3, "value": "C"},
301
+ {"id": 4, "value": "C"},
302
+ {"id": 5, "value": "C"},
303
+ {"id": 6, "value": "C"},
304
+ {"id": 7, "value": "C"},
305
+ {"id": 8, "value": "C"},
306
+ {"id": 9, "value": "C"},
307
+ {"id": 10, "value": "C"},
308
+ {"id": 11, "value": "C"},
309
+ {"id": 12, "value": "C"},
310
+ {"id": 13, "value": "C"},
311
+ {"id": 14, "value": "C"},
312
+ {"id": 15, "value": "C"},
313
+ ],
314
+ "coordinates": [
315
+ {"id": 0, "value": [0.062499937, 0.366666681, 0.5]},
316
+ {"id": 1, "value": [0.312499937, 0.366666681, 0.5]},
317
+ {"id": 2, "value": [0.187500062, 0.433333286, 0.5]},
318
+ {"id": 3, "value": [0.187499937, 0.566666695, 0.5]},
319
+ {"id": 4, "value": [0.062500062, 0.6333333, 0.5]},
320
+ {"id": 5, "value": [0.562499937, 0.366666681, 0.5]},
321
+ {"id": 6, "value": [0.437500062, 0.433333286, 0.5]},
322
+ {"id": 7, "value": [0.437499937, 0.566666695, 0.5]},
323
+ {"id": 8, "value": [0.312500062, 0.6333333, 0.5]},
324
+ {"id": 9, "value": [0.812499938, 0.366666681, 0.5]},
325
+ {"id": 10, "value": [0.687500062, 0.433333286, 0.5]},
326
+ {"id": 11, "value": [0.687499937, 0.566666695, 0.5]},
327
+ {"id": 12, "value": [0.562500062, 0.6333333, 0.5]},
328
+ {"id": 13, "value": [0.937500063, 0.433333286, 0.5]},
329
+ {"id": 14, "value": [0.937499937, 0.566666695, 0.5]},
330
+ {"id": 15, "value": [0.812500063, 0.6333333, 0.5]},
331
+ ],
332
+ "units": "crystal",
333
+ "cell": [[9.869164, 0.0, 0.0], [-0.0, 10.683683422, 0.0], [0.0, 0.0, 20.0]],
334
+ "constraints": [],
335
+ "labels": [],
336
+ },
337
+ "lattice": {
338
+ "a": 9.869164,
339
+ "b": 10.683683422,
340
+ "c": 20.0,
341
+ "alpha": 90.0,
342
+ "beta": 90.0,
343
+ "gamma": 90.0,
344
+ "units": {"length": "angstrom", "angle": "degree"},
345
+ "type": "TRI",
346
+ "vectors": {
347
+ "a": [9.869164, 0.0, 0.0],
348
+ "b": [-0.0, 10.683683422, 0.0],
349
+ "c": [0.0, 0.0, 20.0],
350
+ "alat": 1,
351
+ "units": "angstrom",
352
+ },
353
+ },
354
+ "isNonPeriodic": False,
355
+ "_id": "",
356
+ "metadata": {
357
+ "boundaryConditions": {"type": "pbc", "offset": 0},
358
+ "build": {
359
+ "configuration": {
360
+ "material": GRAPHENE,
361
+ "width": 2,
362
+ "length": 4,
363
+ "vacuum_width": 3,
364
+ "vacuum_length": 0,
365
+ "edge_type": "zigzag",
366
+ }
367
+ },
368
+ },
369
+ "isUpdated": True,
370
+ }
371
+
372
+ GRAPHENE_ARMCHAIR_NANORIBBON = {
373
+ "name": "Graphene (Armchair nanoribbon)",
374
+ "basis": {
375
+ "elements": [
376
+ {"id": 0, "value": "C"},
377
+ {"id": 1, "value": "C"},
378
+ {"id": 2, "value": "C"},
379
+ {"id": 3, "value": "C"},
380
+ {"id": 4, "value": "C"},
381
+ {"id": 5, "value": "C"},
382
+ {"id": 6, "value": "C"},
383
+ {"id": 7, "value": "C"},
384
+ {"id": 8, "value": "C"},
385
+ {"id": 9, "value": "C"},
386
+ {"id": 10, "value": "C"},
387
+ {"id": 11, "value": "C"},
388
+ {"id": 12, "value": "C"},
389
+ {"id": 13, "value": "C"},
390
+ {"id": 14, "value": "C"},
391
+ {"id": 15, "value": "C"},
392
+ ],
393
+ "coordinates": [
394
+ {"id": 0, "value": [0.041666626, 0.35000005, 0.5]},
395
+ {"id": 1, "value": [0.208333376, 0.34999995, 0.5]},
396
+ {"id": 2, "value": [0.041666626, 0.55000005, 0.5]},
397
+ {"id": 3, "value": [0.208333376, 0.54999995, 0.5]},
398
+ {"id": 4, "value": [0.291666626, 0.45000005, 0.5]},
399
+ {"id": 5, "value": [0.458333376, 0.44999995, 0.5]},
400
+ {"id": 6, "value": [0.541666626, 0.35000005, 0.5]},
401
+ {"id": 7, "value": [0.708333376, 0.34999995, 0.5]},
402
+ {"id": 8, "value": [0.291666626, 0.65000005, 0.5]},
403
+ {"id": 9, "value": [0.458333376, 0.64999995, 0.5]},
404
+ {"id": 10, "value": [0.541666626, 0.55000005, 0.5]},
405
+ {"id": 11, "value": [0.708333376, 0.54999995, 0.5]},
406
+ {"id": 12, "value": [0.791666626, 0.45000005, 0.5]},
407
+ {"id": 13, "value": [0.958333376, 0.44999995, 0.5]},
408
+ {"id": 14, "value": [0.791666626, 0.65000005, 0.5]},
409
+ {"id": 15, "value": [0.958333376, 0.64999995, 0.5]},
410
+ ],
411
+ "units": "crystal",
412
+ "cell": [[8.546946738, 0.0, 0.0], [-0.0, 12.336455, 0.0], [0.0, 0.0, 20.0]],
413
+ "constraints": [],
414
+ "labels": [],
415
+ },
416
+ "lattice": {
417
+ "a": 8.546946738,
418
+ "b": 12.336455,
419
+ "c": 20.0,
420
+ "alpha": 90.0,
421
+ "beta": 90.0,
422
+ "gamma": 90.0,
423
+ "units": {"length": "angstrom", "angle": "degree"},
424
+ "type": "TRI",
425
+ "vectors": {
426
+ "a": [8.546946738, 0.0, 0.0],
427
+ "b": [-0.0, 12.336455, 0.0],
428
+ "c": [0.0, 0.0, 20.0],
429
+ "alat": 1,
430
+ "units": "angstrom",
431
+ },
432
+ },
433
+ "isNonPeriodic": False,
434
+ "_id": "",
435
+ "metadata": {
436
+ "boundaryConditions": {"type": "pbc", "offset": 0},
437
+ "build": {
438
+ "configuration": {
439
+ "material": GRAPHENE,
440
+ "width": 2,
441
+ "length": 4,
442
+ "vacuum_width": 3,
443
+ "vacuum_length": 0,
444
+ "edge_type": "armchair",
445
+ }
446
+ },
447
+ },
448
+ "isUpdated": True,
449
+ }
@@ -0,0 +1,32 @@
1
+ from mat3ra.made.material import Material
2
+ from mat3ra.made.tools.build.nanoribbon import NanoribbonConfiguration, create_nanoribbon
3
+ from mat3ra.made.tools.build.nanoribbon.enums import EdgeTypes
4
+ from mat3ra.utils import assertion as assertion_utils
5
+
6
+ from .fixtures import GRAPHENE, GRAPHENE_ARMCHAIR_NANORIBBON, GRAPHENE_ZIGZAG_NANORIBBON
7
+
8
+
9
+ def test_build_zigzag_nanoribbon():
10
+ config = NanoribbonConfiguration(
11
+ material=Material(GRAPHENE),
12
+ width=2,
13
+ length=4,
14
+ vacuum_width=3,
15
+ edge_type=EdgeTypes.zigzag,
16
+ )
17
+
18
+ nanoribbon = create_nanoribbon(config)
19
+ assertion_utils.assert_deep_almost_equal(GRAPHENE_ZIGZAG_NANORIBBON, nanoribbon.to_json())
20
+
21
+
22
+ def test_build_armchair_nanoribbon():
23
+ config = NanoribbonConfiguration(
24
+ material=Material(GRAPHENE),
25
+ width=2,
26
+ length=4,
27
+ vacuum_width=3,
28
+ edge_type=EdgeTypes.armchair,
29
+ )
30
+
31
+ nanoribbon = create_nanoribbon(config)
32
+ assertion_utils.assert_deep_almost_equal(GRAPHENE_ARMCHAIR_NANORIBBON, nanoribbon.to_json())