@mat3ra/made 2024.9.28-0 → 2024.10.1-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.9.28-0",
3
+ "version": "2024.10.1-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,14 +1,17 @@
1
- from typing import Optional
1
+ from typing import Optional, Union
2
2
 
3
3
  from mat3ra.made.material import Material
4
4
 
5
- from .builders import SlabGrainBoundaryBuilder
6
- from .configuration import SlabGrainBoundaryConfiguration
5
+ from .builders import SlabGrainBoundaryBuilder, SurfaceGrainBoundaryBuilder, SurfaceGrainBoundaryBuilderParameters
6
+ from .configuration import (
7
+ SlabGrainBoundaryConfiguration,
8
+ SurfaceGrainBoundaryConfiguration,
9
+ )
7
10
 
8
11
 
9
12
  def create_grain_boundary(
10
- configuration: SlabGrainBoundaryConfiguration,
11
- builder: Optional[SlabGrainBoundaryBuilder] = None,
13
+ configuration: Union[SlabGrainBoundaryConfiguration, SurfaceGrainBoundaryConfiguration],
14
+ builder: Union[SlabGrainBoundaryBuilder, SurfaceGrainBoundaryBuilder, None] = None,
12
15
  ) -> Material:
13
16
  """
14
17
  Create a grain boundary according to provided configuration with selected builder.
@@ -3,13 +3,19 @@ from typing import List
3
3
  import numpy as np
4
4
  from mat3ra.made.material import Material
5
5
 
6
- from ..slab import SlabConfiguration, get_terminations, create_slab
6
+ from ...third_party import PymatgenInterface
7
7
  from ...analyze import get_chemical_formula
8
+ from ..slab import SlabConfiguration, get_terminations, create_slab
8
9
  from ..interface import ZSLStrainMatchingInterfaceBuilderParameters, InterfaceConfiguration
9
- from ..interface.builders import ZSLStrainMatchingInterfaceBuilder
10
+
11
+ from ..interface.builders import (
12
+ ZSLStrainMatchingInterfaceBuilder,
13
+ CommensurateLatticeTwistedInterfaceBuilder,
14
+ CommensurateLatticeTwistedInterfaceBuilderParameters,
15
+ )
10
16
  from ..supercell import create_supercell
11
- from .configuration import SlabGrainBoundaryConfiguration
12
- from ...third_party import PymatgenInterface
17
+ from ..utils import stack_two_materials_xy
18
+ from .configuration import SurfaceGrainBoundaryConfiguration, SlabGrainBoundaryConfiguration
13
19
 
14
20
 
15
21
  class SlabGrainBoundaryBuilderParameters(ZSLStrainMatchingInterfaceBuilderParameters):
@@ -27,6 +33,7 @@ class SlabGrainBoundaryBuilder(ZSLStrainMatchingInterfaceBuilder):
27
33
  """
28
34
 
29
35
  _BuildParametersType: type(SlabGrainBoundaryBuilderParameters) = SlabGrainBoundaryBuilderParameters # type: ignore
36
+ _DefaultBuildParameters = SlabGrainBoundaryBuilderParameters()
30
37
  _ConfigurationType: type(SlabGrainBoundaryConfiguration) = SlabGrainBoundaryConfiguration # type: ignore
31
38
  _GeneratedItemType: PymatgenInterface = PymatgenInterface # type: ignore
32
39
  selector_parameters: type( # type: ignore
@@ -78,3 +85,46 @@ class SlabGrainBoundaryBuilder(ZSLStrainMatchingInterfaceBuilder):
78
85
  )
79
86
  material.name = new_name
80
87
  return material
88
+
89
+
90
+ class SurfaceGrainBoundaryBuilderParameters(CommensurateLatticeTwistedInterfaceBuilderParameters):
91
+ """
92
+ Parameters for creating a grain boundary between two surface phases.
93
+
94
+ Args:
95
+ edge_inclusion_tolerance (float): The tolerance to include atoms on the edge of each phase, in angstroms.
96
+ distance_tolerance (float): The distance tolerance to remove atoms that are too close, in angstroms.
97
+ """
98
+
99
+ edge_inclusion_tolerance: float = 1.0
100
+ distance_tolerance: float = 1.0
101
+
102
+
103
+ class SurfaceGrainBoundaryBuilder(CommensurateLatticeTwistedInterfaceBuilder):
104
+ _ConfigurationType: type(SurfaceGrainBoundaryConfiguration) = SurfaceGrainBoundaryConfiguration # type: ignore
105
+ _BuildParametersType = SurfaceGrainBoundaryBuilderParameters
106
+ _DefaultBuildParameters = SurfaceGrainBoundaryBuilderParameters()
107
+
108
+ def _post_process(self, items: List[Material], post_process_parameters=None) -> List[Material]:
109
+ grain_boundaries = []
110
+ for item in items:
111
+ matrix1 = np.dot(np.array(item.configuration.xy_supercell_matrix), item.matrix1)
112
+ matrix2 = np.dot(np.array(item.configuration.xy_supercell_matrix), item.matrix2)
113
+ phase_1_material_initial = create_supercell(item.configuration.film, matrix1.tolist())
114
+ phase_2_material_initial = create_supercell(item.configuration.film, matrix2.tolist())
115
+
116
+ interface = stack_two_materials_xy(
117
+ phase_1_material_initial,
118
+ phase_2_material_initial,
119
+ gap=item.configuration.gap,
120
+ edge_inclusion_tolerance=self.build_parameters.edge_inclusion_tolerance,
121
+ distance_tolerance=self.build_parameters.distance_tolerance,
122
+ )
123
+ grain_boundaries.append(interface)
124
+
125
+ return grain_boundaries
126
+
127
+ def _update_material_name(self, material: Material, configuration: SurfaceGrainBoundaryConfiguration) -> Material:
128
+ new_name = f"{configuration.film.name}, Grain Boundary ({configuration.twist_angle:.2f}°)"
129
+ material.name = new_name
130
+ return material
@@ -1,8 +1,9 @@
1
- from typing import Optional
1
+ from typing import Optional, List
2
2
 
3
3
  from .. import BaseConfiguration
4
4
  from ..slab.configuration import SlabConfiguration
5
5
  from ..slab.termination import Termination
6
+ from ..interface.configuration import TwistedInterfaceConfiguration
6
7
 
7
8
 
8
9
  class SlabGrainBoundaryConfiguration(BaseConfiguration):
@@ -38,3 +39,24 @@ class SlabGrainBoundaryConfiguration(BaseConfiguration):
38
39
  "gap": self.gap,
39
40
  "slab_configuration": self.slab_configuration.to_json(),
40
41
  }
42
+
43
+
44
+ class SurfaceGrainBoundaryConfiguration(TwistedInterfaceConfiguration):
45
+ """
46
+ Configuration for creating a surface grain boundary.
47
+
48
+ Args:
49
+ gap (float): The gap between the two phases.
50
+ xy_supercell_matrix (List[List[int]]): The supercell matrix to apply for both phases.
51
+ """
52
+
53
+ gap: float = 0.0
54
+ xy_supercell_matrix: List[List[int]] = [[1, 0], [0, 1]]
55
+
56
+ @property
57
+ def _json(self):
58
+ return {
59
+ "type": self.get_cls_name(),
60
+ "gap": self.gap,
61
+ "xy_supercell_matrix": self.xy_supercell_matrix,
62
+ }
@@ -247,7 +247,7 @@ class NanoRibbonTwistedInterfaceBuilder(BaseBuilder):
247
247
  return material
248
248
 
249
249
 
250
- class CommensurateLatticeInterfaceBuilderParameters(BaseModel):
250
+ class CommensurateLatticeTwistedInterfaceBuilderParameters(BaseModel):
251
251
  """
252
252
  Parameters for the commensurate lattice interface builder.
253
253
 
@@ -263,7 +263,7 @@ class CommensurateLatticeInterfaceBuilderParameters(BaseModel):
263
263
  return_first_match: bool = False
264
264
 
265
265
 
266
- class CommensurateLatticeInterfaceBuilder(BaseBuilder):
266
+ class CommensurateLatticeTwistedInterfaceBuilder(BaseBuilder):
267
267
  _GeneratedItemType: type(CommensurateLatticePair) = CommensurateLatticePair # type: ignore
268
268
  _ConfigurationType: type(TwistedInterfaceConfiguration) = TwistedInterfaceConfiguration # type: ignore
269
269
 
@@ -1,3 +1,5 @@
1
+ from typing import Optional
2
+
1
3
  from mat3ra.code.entity import InMemoryEntity
2
4
  from pydantic import BaseModel
3
5
 
@@ -35,7 +37,7 @@ class InterfaceConfiguration(BaseModel, InMemoryEntity):
35
37
 
36
38
  class TwistedInterfaceConfiguration(BaseConfiguration):
37
39
  film: Material
38
- substrate: Material
40
+ substrate: Optional[Material] = None
39
41
  twist_angle: float = 0.0
40
42
  distance_z: float = 3.0
41
43
 
@@ -44,7 +46,7 @@ class TwistedInterfaceConfiguration(BaseConfiguration):
44
46
  return {
45
47
  "type": self.get_cls_name(),
46
48
  "film": self.film.to_json(),
47
- "substrate": self.substrate.to_json(),
49
+ "substrate": self.substrate.to_json() if self.substrate else None,
48
50
  "twist_angle": self.twist_angle,
49
51
  "distance_z": self.distance_z,
50
52
  }
@@ -3,6 +3,9 @@ from scipy.spatial import cKDTree
3
3
  from typing import List, Optional
4
4
  from mat3ra.made.basis import Basis
5
5
  from mat3ra.made.material import Material
6
+
7
+ from .supercell import create_supercell
8
+ from ..modify import filter_by_box, translate_by_vector
6
9
  from ...utils import ArrayWithIds
7
10
 
8
11
 
@@ -78,6 +81,20 @@ def merge_materials(
78
81
  distance_tolerance: float = 0.01,
79
82
  merge_dangerously=False,
80
83
  ) -> Material:
84
+ """
85
+ Merge multiple materials into a single material.
86
+
87
+ If some of the atoms are considered too close within a tolerance, only the last atom is kept.
88
+
89
+ Args:
90
+ materials (List[Material]): List of materials to merge.
91
+ material_name (Optional[str]): Name of the merged material.
92
+ distance_tolerance (float): The tolerance to replace atoms that are considered too close with respect
93
+ to the coordinates in the last material in the list, in angstroms.
94
+ merge_dangerously (bool): If True, the lattices are merged "as is" with no sanity checks.
95
+ Returns:
96
+ Material: The merged material.
97
+ """
81
98
  merged_material = materials[0]
82
99
  for material in materials[1:]:
83
100
  merged_material = merge_two_materials(
@@ -85,3 +102,77 @@ def merge_materials(
85
102
  )
86
103
 
87
104
  return merged_material
105
+
106
+
107
+ def double_and_filter_material(material: Material, start: List[float], end: List[float]) -> Material:
108
+ """
109
+ Double the material and filter it by a box defined by the start and end coordinates.
110
+ Args:
111
+ material (Material): The material to double and filter.
112
+ start (List[float]): The start coordinates of the box.
113
+ end (List[float]): The end coordinates of the box.
114
+ Returns:
115
+ Material: The filtered material.
116
+ """
117
+ material_doubled = create_supercell(material, scaling_factor=[2, 1, 1])
118
+ return filter_by_box(material_doubled, start, end)
119
+
120
+
121
+ def expand_lattice_vectors(material: Material, gap: float, direction: int = 0) -> Material:
122
+ """
123
+ Expand the lattice vectors of the material in the specified direction by the given gap.
124
+
125
+ Args:
126
+ material (Material): The material whose lattice vectors are to be expanded.
127
+ gap (float): The gap by which to expand the lattice vector.
128
+ direction (int): The index of the lattice vector to expand (0, 1, or 2).
129
+ """
130
+ new_lattice_vectors = material.lattice.vector_arrays
131
+ new_lattice_vectors[direction][direction] += gap
132
+ material.set_new_lattice_vectors(
133
+ lattice_vector1=new_lattice_vectors[0],
134
+ lattice_vector2=new_lattice_vectors[1],
135
+ lattice_vector3=new_lattice_vectors[2],
136
+ )
137
+ return material
138
+
139
+
140
+ def stack_two_materials_xy(
141
+ phase_1_material: Material,
142
+ phase_2_material: Material,
143
+ gap: float,
144
+ edge_inclusion_tolerance: Optional[float] = 1.0,
145
+ distance_tolerance: float = 1.0,
146
+ ) -> Material:
147
+ """
148
+ Stack two materials laterally with translation along x-axis with a gap between them.
149
+
150
+ Works correctly only for materials with the same lattice vectors (commensurate lattices).
151
+ Args:
152
+ phase_1_material (Material): The first material.
153
+ phase_2_material (Material): The second material.
154
+ gap (float): The gap between the two materials, in angstroms.
155
+ edge_inclusion_tolerance (float): The tolerance to include atoms on the edge of the phase, in angstroms.
156
+ distance_tolerance (float): The distance tolerance to remove atoms that are too close, in angstroms.
157
+
158
+ Returns:
159
+ Material: The merged material.
160
+ """
161
+ edge_inclusion_tolerance_crystal = abs(
162
+ phase_1_material.basis.cell.convert_point_to_crystal([edge_inclusion_tolerance, 0, 0])[0]
163
+ )
164
+
165
+ phase_1_material = double_and_filter_material(
166
+ phase_1_material, [0 - edge_inclusion_tolerance_crystal, 0, 0], [0.5 + edge_inclusion_tolerance_crystal, 1, 1]
167
+ )
168
+
169
+ phase_2_material = double_and_filter_material(
170
+ phase_2_material, [0.5 - edge_inclusion_tolerance_crystal, 0, 0], [1 + edge_inclusion_tolerance_crystal, 1, 1]
171
+ )
172
+
173
+ phase_1_material = expand_lattice_vectors(phase_1_material, gap)
174
+ phase_2_material = expand_lattice_vectors(phase_2_material, gap)
175
+
176
+ phase_2_material = translate_by_vector(phase_2_material, [gap / 2, 0, 0], use_cartesian_coordinates=True)
177
+ interface = merge_materials([phase_1_material, phase_2_material], distance_tolerance=distance_tolerance)
178
+ return interface
@@ -2,12 +2,17 @@ from mat3ra.made.material import Material
2
2
  from mat3ra.made.tools.build.grain_boundary import (
3
3
  SlabGrainBoundaryBuilder,
4
4
  SlabGrainBoundaryConfiguration,
5
+ SurfaceGrainBoundaryBuilder,
6
+ SurfaceGrainBoundaryBuilderParameters,
7
+ SurfaceGrainBoundaryConfiguration,
5
8
  create_grain_boundary,
6
9
  )
7
- from mat3ra.made.tools.build.interface import ZSLStrainMatchingInterfaceBuilderParameters
10
+ from mat3ra.made.tools.build.grain_boundary.builders import SlabGrainBoundaryBuilderParameters
8
11
  from mat3ra.made.tools.build.slab import SlabConfiguration, get_terminations
9
12
  from mat3ra.utils import assertion as assertion_utils
10
13
 
14
+ from .fixtures import GRAPHENE
15
+
11
16
 
12
17
  def test_slab_grain_boundary_builder():
13
18
  material = Material(Material.default_config)
@@ -44,7 +49,7 @@ def test_slab_grain_boundary_builder():
44
49
  slab_configuration=slab_config,
45
50
  )
46
51
 
47
- builder_params = ZSLStrainMatchingInterfaceBuilderParameters(max_area=50)
52
+ builder_params = SlabGrainBoundaryBuilderParameters()
48
53
  builder = SlabGrainBoundaryBuilder(build_parameters=builder_params)
49
54
  gb = create_grain_boundary(config, builder)
50
55
  expected_lattice_vectors = [
@@ -57,3 +62,30 @@ def test_slab_grain_boundary_builder():
57
62
  assert len(gb.basis.elements.values) == 32
58
63
  assertion_utils.assert_deep_almost_equal(expected_coordinate_15, gb.basis.coordinates.values[15])
59
64
  assertion_utils.assert_deep_almost_equal(expected_lattice_vectors, gb.lattice.vector_arrays)
65
+
66
+
67
+ def test_create_surface_grain_boundary():
68
+ config = SurfaceGrainBoundaryConfiguration(
69
+ film=Material(GRAPHENE),
70
+ twist_angle=13.0,
71
+ gap=2.0,
72
+ )
73
+
74
+ builder_params = SurfaceGrainBoundaryBuilderParameters(
75
+ max_repetition_int=5,
76
+ angle_tolerance=0.5,
77
+ return_first_match=True,
78
+ distance_tolerance=1.0,
79
+ )
80
+ builder = SurfaceGrainBoundaryBuilder(build_parameters=builder_params)
81
+
82
+ gb = builder.get_materials(config)
83
+
84
+ expected_cell_vectors = [
85
+ [23.509344266, 0.0, 0.0],
86
+ [5.377336066500001, 9.313819276550575, 0.0],
87
+ [0.0, 0.0, 20.0],
88
+ ]
89
+
90
+ assert len(gb) == 1
91
+ assertion_utils.assert_deep_almost_equal(expected_cell_vectors, gb[0].basis.cell.vectors_as_array)
@@ -9,8 +9,8 @@ from mat3ra.made.tools.build.interface import (
9
9
  create_interfaces,
10
10
  )
11
11
  from mat3ra.made.tools.build.interface.builders import (
12
- CommensurateLatticeInterfaceBuilder,
13
- CommensurateLatticeInterfaceBuilderParameters,
12
+ CommensurateLatticeTwistedInterfaceBuilder,
13
+ CommensurateLatticeTwistedInterfaceBuilderParameters,
14
14
  NanoRibbonTwistedInterfaceBuilder,
15
15
  NanoRibbonTwistedInterfaceConfiguration,
16
16
  TwistedInterfaceConfiguration,
@@ -72,10 +72,10 @@ def test_create_commensurate_supercell_twisted_interface():
72
72
  film = Material(GRAPHENE)
73
73
  substrate = Material(GRAPHENE)
74
74
  config = TwistedInterfaceConfiguration(film=film, substrate=substrate, twist_angle=13, distance_z=3.0)
75
- params = CommensurateLatticeInterfaceBuilderParameters(
75
+ params = CommensurateLatticeTwistedInterfaceBuilderParameters(
76
76
  max_repetition_int=5, angle_tolerance=0.5, return_first_match=True
77
77
  )
78
- builder = CommensurateLatticeInterfaceBuilder(build_parameters=params)
78
+ builder = CommensurateLatticeTwistedInterfaceBuilder(build_parameters=params)
79
79
  interfaces = builder.get_materials(config, post_process_parameters=config)
80
80
  assert len(interfaces) == 1
81
81
  interface = interfaces[0]