@mat3ra/made 2024.7.1-0 → 2024.7.7-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.1-0",
3
+ "version": "2024.7.7-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",
@@ -67,13 +67,13 @@ class Basis(RoundNumericValuesMixin, BaseModel):
67
67
  def to_cartesian(self):
68
68
  if self.is_in_cartesian_units:
69
69
  return
70
- self.coordinates = self.coordinates.map_array_in_place(self.cell.convert_point_to_cartesian)
70
+ self.coordinates.map_array_in_place(self.cell.convert_point_to_cartesian)
71
71
  self.units = AtomicCoordinateUnits.cartesian
72
72
 
73
73
  def to_crystal(self):
74
74
  if self.is_in_crystal_units:
75
75
  return
76
- self.coordinates = self.coordinates.map_array_in_place(self.cell.convert_point_to_crystal)
76
+ self.coordinates.map_array_in_place(self.cell.convert_point_to_crystal)
77
77
  self.units = AtomicCoordinateUnits.crystal
78
78
 
79
79
  def add_atom(self, element="Si", coordinate=[0.5, 0.5, 0.5]):
@@ -44,7 +44,7 @@ class Cell(RoundNumericValuesMixin, BaseModel):
44
44
  np_vector = np.array(self.vectors_as_nested_array)
45
45
  return np.dot(point, np_vector)
46
46
 
47
- def convert_point_to_fractional(self, point):
47
+ def convert_point_to_crystal(self, point):
48
48
  np_vector = np.array(self.vectors_as_nested_array)
49
49
  return np.dot(point, np.linalg.inv(np_vector))
50
50
 
@@ -1,4 +1,4 @@
1
- from typing import Callable, List, Optional
1
+ from typing import Callable, List, Optional, Literal
2
2
 
3
3
  import numpy as np
4
4
 
@@ -211,7 +211,7 @@ def get_atom_indices_with_condition_on_coordinates(
211
211
  Args:
212
212
  material (Material): Material object
213
213
  condition (Callable[List[float], bool]): Function that checks if coordinates satisfy the condition.
214
- use_cartesian (bool): Whether to use Cartesian coordinates for the condition evaluation.
214
+ use_cartesian_coordinates (bool): Whether to use Cartesian coordinates for the condition evaluation.
215
215
 
216
216
  Returns:
217
217
  List[int]: List of indices of atoms whose coordinates satisfy the condition.
@@ -229,3 +229,30 @@ def get_atom_indices_with_condition_on_coordinates(
229
229
  selected_indices.append(coord.id)
230
230
 
231
231
  return selected_indices
232
+
233
+
234
+ def get_atomic_coordinates_extremum(
235
+ material: Material,
236
+ extremum: Literal["max", "min"] = "max",
237
+ axis: Literal["x", "y", "z"] = "z",
238
+ use_cartesian_coordinates: bool = False,
239
+ ) -> float:
240
+ """
241
+ Return minimum or maximum of coordinates along the specified axis.
242
+
243
+ Args:
244
+ material (Material): Material object.
245
+ extremum (str): "min" or "max".
246
+ axis (str): "x", "y", or "z".
247
+ use_cartesian_coordinates (bool): Whether to use Cartesian coordinates.
248
+ Returns:
249
+ float: Minimum or maximum of coordinates along the specified axis.
250
+ """
251
+ new_material = material.clone()
252
+ if use_cartesian_coordinates:
253
+ new_basis = new_material.basis
254
+ new_basis.to_cartesian()
255
+ new_material.basis = new_basis
256
+ coordinates = new_material.basis.coordinates.to_array_of_values_with_ids()
257
+ values = [coord.value[{"x": 0, "y": 1, "z": 2}[axis]] for coord in coordinates]
258
+ return getattr(np, extremum)(values)
@@ -184,6 +184,8 @@ def to_ase(material_or_material_data: Union[Material, Dict[str, Any]]) -> ASEAto
184
184
  atoms.set_tags(map_array_with_id_value_to_array(atomic_labels))
185
185
  if "metadata" in material_config:
186
186
  atoms.info.update({"metadata": material_config["metadata"]})
187
+
188
+ atoms.info.update({"name": material_config["name"]})
187
189
  return atoms
188
190
 
189
191
 
@@ -205,6 +207,7 @@ def from_ase(ase_atoms: ASEAtoms) -> Dict[str, Any]:
205
207
  ase_metadata = ase_atoms.info.get("metadata", {})
206
208
  if ase_metadata:
207
209
  material["metadata"].update(ase_metadata)
210
+ material["name"] = ase_atoms.info.get("name", "")
208
211
  return material
209
212
 
210
213
 
@@ -1,16 +1,19 @@
1
- from typing import Callable, List, Optional, Union
1
+ from typing import Callable, List, Literal, Optional, Union
2
2
 
3
- import numpy as np
4
3
  from mat3ra.made.material import Material
5
4
 
6
- from .analyze import get_atom_indices_with_condition_on_coordinates, get_atom_indices_within_radius_pbc
7
- from .convert import decorator_convert_material_args_kwargs_to_structure
8
- from .third_party import PymatgenSpacegroupAnalyzer, PymatgenStructure
5
+ from .analyze import (
6
+ get_atom_indices_with_condition_on_coordinates,
7
+ get_atom_indices_within_radius_pbc,
8
+ get_atomic_coordinates_extremum,
9
+ )
10
+ from .convert import decorator_convert_material_args_kwargs_to_structure, from_ase, to_ase
11
+ from .third_party import PymatgenStructure, ase_add_vacuum
9
12
  from .utils import (
10
13
  is_coordinate_in_box,
11
14
  is_coordinate_in_cylinder,
15
+ is_coordinate_in_triangular_prism,
12
16
  is_coordinate_within_layer,
13
- translate_to_bottom_pymatgen_structure,
14
17
  )
15
18
 
16
19
 
@@ -34,22 +37,54 @@ def filter_by_label(material: Material, label: Union[int, str]) -> Material:
34
37
  return new_material
35
38
 
36
39
 
37
- @decorator_convert_material_args_kwargs_to_structure
38
- def translate_to_bottom(structure: PymatgenStructure, use_conventional_cell: bool = True):
40
+ def translate_to_z_level(
41
+ material: Material, z_level: Optional[Literal["top", "bottom", "center"]] = "bottom"
42
+ ) -> Material:
39
43
  """
40
- Translate atoms to the bottom of the cell (vacuum on top) to allow for the correct consecutive interface generation.
41
- If use_conventional_cell is passed, conventional cell is used.
44
+ Translate atoms to the specified z-level.
42
45
 
43
46
  Args:
44
- structure (Structure): The pymatgen Structure object to normalize.
45
- use_conventional_cell: Whether to convert to the conventional cell.
47
+ material (Material): The material object to normalize.
48
+ z_level (str): The z-level to translate the atoms to (top, bottom, center)
46
49
  Returns:
47
- Structure: The normalized pymatgen Structure object.
50
+ Material: The translated material object.
48
51
  """
49
- if use_conventional_cell:
50
- structure = PymatgenSpacegroupAnalyzer(structure).get_conventional_standard_structure()
51
- structure = translate_to_bottom_pymatgen_structure(structure)
52
- return structure
52
+ min_z = get_atomic_coordinates_extremum(material, "min")
53
+ max_z = get_atomic_coordinates_extremum(material)
54
+ if z_level == "top":
55
+ material = translate_by_vector(material, vector=[0, 0, 1 - max_z])
56
+ elif z_level == "bottom":
57
+ material = translate_by_vector(material, vector=[0, 0, -min_z])
58
+ elif z_level == "center":
59
+ material = translate_by_vector(material, vector=[0, 0, (1 - min_z - max_z) / 2])
60
+ return material
61
+
62
+
63
+ def translate_by_vector(
64
+ material: Material,
65
+ vector: Optional[List[float]] = None,
66
+ use_cartesian_coordinates: bool = False,
67
+ ) -> Material:
68
+ """
69
+ Translate atoms by a vector.
70
+
71
+ Args:
72
+ material (Material): The material object to normalize.
73
+ vector (List[float]): The vector to translate the atoms by (in crystal coordinates by default).
74
+ use_cartesian_coordinates (bool): Whether to use cartesian coordinates.
75
+ Returns:
76
+ Material: The translated material object.
77
+ """
78
+ if not use_cartesian_coordinates:
79
+ vector = material.basis.cell.convert_point_to_cartesian(vector)
80
+
81
+ if vector is None:
82
+ vector = [0, 0, 0]
83
+
84
+ atoms = to_ase(material)
85
+ # ASE accepts cartesian coordinates for translation
86
+ atoms.translate(tuple(vector))
87
+ return Material(from_ase(atoms))
53
88
 
54
89
 
55
90
  @decorator_convert_material_args_kwargs_to_structure
@@ -139,7 +174,7 @@ def filter_by_layers(
139
174
  if central_atom_id is not None:
140
175
  center_coordinate = material.basis.coordinates.get_element_value_by_index(central_atom_id)
141
176
  vectors = material.lattice.vectors
142
- direction_vector = np.array(vectors[2])
177
+ direction_vector = vectors[2]
143
178
 
144
179
  def condition(coordinate):
145
180
  return is_coordinate_within_layer(coordinate, center_coordinate, direction_vector, layer_thickness)
@@ -241,10 +276,8 @@ def filter_by_cylinder(
241
276
 
242
277
  def filter_by_rectangle_projection(
243
278
  material: Material,
244
- x_min: float = 0.0,
245
- y_min: float = 0.0,
246
- x_max: float = 1.0,
247
- y_max: float = 1.0,
279
+ min_coordinate: List[float] = [0, 0],
280
+ max_coordinate: List[float] = [1, 1],
248
281
  use_cartesian_coordinates: bool = False,
249
282
  invert_selection: bool = False,
250
283
  ) -> Material:
@@ -252,21 +285,20 @@ def filter_by_rectangle_projection(
252
285
  Get material with atoms that are within or outside an XY rectangle projection.
253
286
 
254
287
  Args:
255
-
256
288
  material (Material): The material object to filter.
257
- x_min (float): The minimum x-coordinate of the rectangle.
258
- y_min (float): The minimum y-coordinate of the rectangle.
259
- x_max (float): The maximum x-coordinate of the rectangle.
260
- y_max (float): The maximum y-coordinate of the rectangle.
289
+ min_coordinate (List[float]): The minimum coordinate of the rectangle.
290
+ max_coordinate (List[float]): The maximum coordinate of the rectangle.
261
291
  use_cartesian_coordinates (bool): Whether to use cartesian coordinates
262
292
  invert_selection (bool): Whether to invert the selection.
263
293
 
264
294
  Returns:
265
295
  Material: The filtered material object.
266
296
  """
297
+ min_coordinate = min_coordinate[:2] + [0]
298
+ max_coordinate = max_coordinate[:2] + [1]
267
299
 
268
300
  def condition(coordinate):
269
- return is_coordinate_in_box(coordinate, [x_min, y_min, 0], [x_max, y_max, 1])
301
+ return is_coordinate_in_box(coordinate, min_coordinate, max_coordinate)
270
302
 
271
303
  return filter_by_condition_on_coordinates(
272
304
  material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection
@@ -290,3 +322,96 @@ def filter_by_box(
290
322
  return filter_by_condition_on_coordinates(
291
323
  material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection
292
324
  )
325
+
326
+
327
+ def filter_by_triangle_projection(
328
+ material: Material,
329
+ coordinate_1: List[float] = [0, 0],
330
+ coordinate_2: List[float] = [0, 1],
331
+ coordinate_3: List[float] = [1, 0],
332
+ min_z: float = 0,
333
+ max_z: float = 1,
334
+ use_cartesian_coordinates: bool = False,
335
+ invert_selection: bool = False,
336
+ ) -> Material:
337
+ """
338
+ Get material with atoms that are within or outside a prism formed by triangle projection.
339
+
340
+ Args:
341
+ material (Material): The material object to filter.
342
+ coordinate_1 (List[float]): The coordinate of the first vertex.
343
+ coordinate_2 (List[float]): The coordinate of the second vertex.
344
+ coordinate_3 (List[float]): The coordinate of the third vertex.
345
+ min_z (float): Lower limit of z-coordinate.
346
+ max_z (float): Upper limit of z-coordinate.
347
+ use_cartesian_coordinates (bool): Whether to use cartesian coordinates
348
+ invert_selection (bool): Whether to invert the selection.
349
+
350
+ Returns:
351
+ Material: The filtered material object.
352
+ """
353
+
354
+ def condition(coordinate):
355
+ return is_coordinate_in_triangular_prism(coordinate, coordinate_1, coordinate_2, coordinate_3, min_z, max_z)
356
+
357
+ return filter_by_condition_on_coordinates(
358
+ material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection
359
+ )
360
+
361
+
362
+ def add_vacuum(material: Material, vacuum: float = 5.0, on_top=True, to_bottom=False) -> Material:
363
+ """
364
+ Add vacuum to the material along the c-axis.
365
+ On top, on bottom, or both.
366
+
367
+ Args:
368
+ material (Material): The material object to add vacuum to.
369
+ vacuum (float): The thickness of the vacuum to add in angstroms.
370
+ on_top (bool): Whether to add vacuum on top.
371
+ to_bottom (bool): Whether to add vacuum on bottom.
372
+
373
+ Returns:
374
+ Material: The material object with vacuum added.
375
+ """
376
+ new_material_atoms = to_ase(material)
377
+ vacuum_amount = vacuum * 2 if on_top and to_bottom else vacuum
378
+ ase_add_vacuum(new_material_atoms, vacuum_amount)
379
+ new_material = Material(from_ase(new_material_atoms))
380
+ if to_bottom and not on_top:
381
+ new_material = translate_to_z_level(new_material, z_level="top")
382
+ elif on_top and to_bottom:
383
+ new_material = translate_to_z_level(new_material, z_level="center")
384
+ return new_material
385
+
386
+
387
+ def remove_vacuum(material: Material, from_top=True, from_bottom=True, fixed_padding=1.0) -> Material:
388
+ """
389
+ Remove vacuum from the material along the c-axis.
390
+ From top, from bottom, or from both.
391
+
392
+ Args:
393
+ material (Material): The material object to set the vacuum thickness.
394
+ from_top (bool): Whether to remove vacuum from the top.
395
+ from_bottom (bool): Whether to remove vacuum from the bottom.
396
+ fixed_padding (float): The fixed padding of vacuum to add to avoid collisions in pbc (in angstroms).
397
+
398
+ Returns:
399
+ Material: The material object with the vacuum thickness set.
400
+ """
401
+ translated_material = translate_to_z_level(material, z_level="bottom")
402
+ new_basis = translated_material.basis
403
+ new_basis.to_cartesian()
404
+ new_lattice = translated_material.lattice
405
+ new_lattice.c = get_atomic_coordinates_extremum(translated_material, use_cartesian_coordinates=True) + fixed_padding
406
+ new_basis.cell.vector3 = new_lattice.vectors[2]
407
+ new_basis.to_crystal()
408
+ new_material = material.clone()
409
+
410
+ new_material.basis = new_basis
411
+ new_material.lattice = new_lattice
412
+
413
+ if from_top and not from_bottom:
414
+ new_material = translate_to_z_level(new_material, z_level="top")
415
+ if from_bottom and not from_top:
416
+ new_material = translate_to_z_level(new_material, z_level="bottom")
417
+ return new_material
@@ -1,4 +1,5 @@
1
1
  from ase import Atoms as ASEAtoms
2
+ from ase.build import add_vacuum as ase_add_vacuum
2
3
  from ase.build.supercells import make_supercell as ase_make_supercell
3
4
  from ase.calculators.calculator import Calculator as ASECalculator
4
5
  from ase.calculators.emt import EMT as ASECalculatorEMT
@@ -36,6 +37,7 @@ __all__ = [
36
37
  "PymatgenInterstitial",
37
38
  "label_pymatgen_slab_termination",
38
39
  "ase_make_supercell",
40
+ "ase_add_vacuum",
39
41
  "PymatgenAseAtomsAdaptor",
40
42
  "PymatgenPoscar",
41
43
  ]
@@ -105,7 +105,7 @@ def is_coordinate_in_cylinder(
105
105
  coordinate: List[float], center_position: List[float], radius: float = 0.25, min_z: float = 0, max_z: float = 1
106
106
  ) -> bool:
107
107
  """
108
- Check if a point is inside a cylinder.
108
+ Check if a coordinate is inside a cylinder.
109
109
  Args:
110
110
  coordinate (List[float]): The coordinate to check.
111
111
  center_position (List[float]): The coordinates of the center position.
@@ -114,7 +114,7 @@ def is_coordinate_in_cylinder(
114
114
  radius (float): The radius of the cylinder.
115
115
 
116
116
  Returns:
117
- bool: True if the point is inside the cylinder, False otherwise.
117
+ bool: True if the coordinate is inside the cylinder, False otherwise.
118
118
  """
119
119
  return (coordinate[0] - center_position[0]) ** 2 + (coordinate[1] - center_position[1]) ** 2 <= radius**2 and (
120
120
  min_z <= coordinate[2] <= max_z
@@ -125,13 +125,13 @@ def is_coordinate_in_box(
125
125
  coordinate: List[float], min_coordinate: List[float] = [0, 0, 0], max_coordinate: List[float] = [1, 1, 1]
126
126
  ) -> bool:
127
127
  """
128
- Check if a point is inside a box.
128
+ Check if a coordinate is inside a box.
129
129
  Args:
130
130
  coordinate (List[float]): The coordinate to check.
131
131
  min_coordinate (List[float]): The minimum coordinate of the box.
132
132
  max_coordinate (List[float]): The maximum coordinate of the box.
133
133
  Returns:
134
- bool: True if the point is inside the box, False otherwise.
134
+ bool: True if the coordinate is inside the box, False otherwise.
135
135
  """
136
136
  x_min, y_min, z_min = min_coordinate
137
137
  x_max, y_max, z_max = max_coordinate
@@ -142,7 +142,7 @@ def is_coordinate_within_layer(
142
142
  coordinate: List[float], center_position: List[float], direction_vector: List[float], layer_thickness: float
143
143
  ) -> bool:
144
144
  """
145
- Checks if a point's projection along a specified direction vector
145
+ Checks if a coordinate's projection along a specified direction vector
146
146
  is within a certain layer thickness centered around a given position.
147
147
 
148
148
  Args:
@@ -152,7 +152,7 @@ def is_coordinate_within_layer(
152
152
  layer_thickness (float): The thickness of the layer along the direction vector.
153
153
 
154
154
  Returns:
155
- bool: True if the point is within the layer thickness, False otherwise.
155
+ bool: True if the coordinate is within the layer thickness, False otherwise.
156
156
  """
157
157
  direction_norm = np.array(direction_vector) / np.linalg.norm(direction_vector)
158
158
  central_projection = np.dot(center_position, direction_norm)
@@ -162,3 +162,54 @@ def is_coordinate_within_layer(
162
162
  upper_bound = central_projection + layer_thickness_frac / 2
163
163
 
164
164
  return lower_bound <= np.dot(coordinate, direction_norm) <= upper_bound
165
+
166
+
167
+ def is_coordinate_in_triangular_prism(
168
+ coordinate: List[float],
169
+ coordinate_1: List[float],
170
+ coordinate_2: List[float],
171
+ coordinate_3: List[float],
172
+ min_z: float = 0,
173
+ max_z: float = 1,
174
+ ) -> bool:
175
+ """
176
+ Check if a coordinate is inside a triangular prism.
177
+ Args:
178
+ coordinate (List[float]): The coordinate to check.
179
+ coordinate_1 (List[float]): The first coordinate of the triangle.
180
+ coordinate_2 (List[float]): The second coordinate of the triangle.
181
+ coordinate_3 (List[float]): The third coordinate of the triangle.
182
+ min_z (float): Lower limit of z-coordinate.
183
+ max_z (float): Upper limit of z-coordinate.
184
+
185
+ Returns:
186
+ bool: True if the coordinate is inside the triangular prism, False otherwise.
187
+ """
188
+ # convert to 3D coordinates at the origin XY plane
189
+ coordinate_1.extend([0] * (3 - len(coordinate_1)))
190
+ coordinate_2.extend([0] * (3 - len(coordinate_2)))
191
+ coordinate_3.extend([0] * (3 - len(coordinate_3)))
192
+
193
+ coordinate = np.array(coordinate)
194
+ v1 = np.array(coordinate_1)
195
+ v2 = np.array(coordinate_2)
196
+ v3 = np.array(coordinate_3)
197
+
198
+ v2_v1 = v2 - v1
199
+ v3_v1 = v3 - v1
200
+ coordinate_v1 = coordinate - v1
201
+
202
+ # Compute dot products for the barycentric coordinates
203
+ d00 = np.dot(v2_v1, v2_v1)
204
+ d01 = np.dot(v2_v1, v3_v1)
205
+ d11 = np.dot(v3_v1, v3_v1)
206
+ d20 = np.dot(coordinate_v1, v2_v1)
207
+ d21 = np.dot(coordinate_v1, v3_v1)
208
+
209
+ # Calculate barycentric coordinates
210
+ denom = d00 * d11 - d01 * d01
211
+ v = (d11 * d20 - d01 * d21) / denom
212
+ w = (d00 * d21 - d01 * d20) / denom
213
+ u = 1.0 - v - w
214
+
215
+ return (u >= 0) and (v >= 0) and (w >= 0) and (u + v + w <= 1) and (min_z <= coordinate[2] <= max_z)
@@ -1,3 +1,6 @@
1
+ import copy
2
+ from typing import Any, Dict
3
+
1
4
  from ase.build import bulk
2
5
  from mat3ra.made.material import Material
3
6
  from mat3ra.made.tools.build.interface.termination_pair import TerminationPair
@@ -59,7 +62,7 @@ INTERFACE_STRUCTURE.interface_properties = INTERFACE_PROPERTIES_MOCK
59
62
  INTERFACE_NAME = "Cu4(001)-Si8(001), Interface, Strain 0.062pct"
60
63
 
61
64
  # TODO: Use fixtures package when available
62
- SI_CONVENTIONAL_CELL = {
65
+ SI_CONVENTIONAL_CELL: Dict[str, Any] = {
63
66
  "name": "Si8",
64
67
  "basis": {
65
68
  "elements": [
@@ -110,7 +113,7 @@ SI_CONVENTIONAL_CELL = {
110
113
  "isUpdated": True,
111
114
  }
112
115
 
113
- SI_SUPERCELL_2X2X1 = {
116
+ SI_SUPERCELL_2X2X1: Dict[str, Any] = {
114
117
  "name": "Si8",
115
118
  "basis": {
116
119
  "elements": [
@@ -162,7 +165,7 @@ SI_SUPERCELL_2X2X1 = {
162
165
  }
163
166
 
164
167
 
165
- SI_SLAB_CONFIGURATION = {
168
+ SI_SLAB_CONFIGURATION: Dict[str, Any] = {
166
169
  "type": "SlabConfiguration",
167
170
  "bulk": SI_CONVENTIONAL_CELL,
168
171
  "miller_indices": (0, 0, 1),
@@ -173,9 +176,7 @@ SI_SLAB_CONFIGURATION = {
173
176
  "use_orthogonal_z": True,
174
177
  }
175
178
 
176
-
177
- SI_SLAB = {
178
- "name": "Si8(001), termination Si_P4/mmm_1, Slab",
179
+ SI_SLAB: Dict[str, Any] = {
179
180
  "basis": {
180
181
  "elements": [
181
182
  {"id": 0, "value": "Si"},
@@ -211,6 +212,7 @@ SI_SLAB = {
211
212
  "units": "angstrom",
212
213
  },
213
214
  },
215
+ "name": "Si8(001), termination Si_P4/mmm_1, Slab",
214
216
  "isNonPeriodic": False,
215
217
  "_id": "",
216
218
  "metadata": {
@@ -220,3 +222,14 @@ SI_SLAB = {
220
222
  },
221
223
  "isUpdated": True,
222
224
  }
225
+
226
+ SI_SLAB_VACUUM = copy.deepcopy(SI_SLAB)
227
+ SI_SLAB_VACUUM["basis"]["coordinates"] = [
228
+ {"id": 0, "value": [0.5, 0.5, 0.386029718]},
229
+ {"id": 1, "value": [0.5, 0.0, 0.4718141]},
230
+ {"id": 2, "value": [0.0, 0.0, 0.557598482]},
231
+ {"id": 3, "value": [-0.0, 0.5, 0.643382864]},
232
+ ]
233
+ SI_SLAB_VACUUM["basis"]["cell"] = [[3.867, 0.0, 0.0], [-0.0, 3.867, 0.0], [0.0, 0.0, 15.937527692]]
234
+ SI_SLAB_VACUUM["lattice"]["c"] = 15.937527692
235
+ SI_SLAB_VACUUM["lattice"]["vectors"]["c"] = [0.0, 0.0, 15.937527692]
@@ -2,15 +2,19 @@ from ase.build import bulk
2
2
  from mat3ra.made.material import Material
3
3
  from mat3ra.made.tools.convert import from_ase
4
4
  from mat3ra.made.tools.modify import (
5
+ add_vacuum,
5
6
  filter_by_circle_projection,
6
7
  filter_by_label,
7
8
  filter_by_layers,
8
9
  filter_by_rectangle_projection,
9
10
  filter_by_sphere,
11
+ filter_by_triangle_projection,
12
+ remove_vacuum,
13
+ translate_to_z_level,
10
14
  )
11
15
  from mat3ra.utils import assertion as assertion_utils
12
16
 
13
- from .fixtures import SI_CONVENTIONAL_CELL
17
+ from .fixtures import SI_CONVENTIONAL_CELL, SI_SLAB, SI_SLAB_VACUUM
14
18
 
15
19
  COMMON_PART = {
16
20
  "units": "crystal",
@@ -126,3 +130,31 @@ def test_filter_by_rectangle_projection():
126
130
  # Default will contain all the atoms
127
131
  section = filter_by_rectangle_projection(material)
128
132
  assertion_utils.assert_deep_almost_equal(material.basis.to_json(), section.basis.to_json())
133
+
134
+
135
+ def test_filter_by_triangle_projection():
136
+ # Small prism in the middle of the cell containing the central atom will be removed -- the same as with sphere
137
+ material = Material(SI_CONVENTIONAL_CELL)
138
+ section = filter_by_triangle_projection(material, [0.4, 0.4], [0.4, 0.5], [0.5, 0.5])
139
+ cavity = filter_by_triangle_projection(material, [0.4, 0.4], [0.4, 0.5], [0.5, 0.5], invert_selection=True)
140
+ assertion_utils.assert_deep_almost_equal(expected_basis_sphere_cluster, section.basis.to_json())
141
+ assertion_utils.assert_deep_almost_equal(expected_basis_sphere_cavity, cavity.basis.to_json())
142
+
143
+
144
+ def test_add_vacuum():
145
+ material = Material(SI_SLAB)
146
+ material_with_vacuum = add_vacuum(material, 5.0)
147
+ assertion_utils.assert_deep_almost_equal(SI_SLAB_VACUUM, material_with_vacuum.to_json())
148
+
149
+
150
+ def test_remove_vacuum():
151
+ material_with_vacuum = Material(SI_SLAB_VACUUM)
152
+ vacuum = 6.836
153
+ material_with_no_vacuum = remove_vacuum(material_with_vacuum, from_top=True, from_bottom=True, fixed_padding=0)
154
+ material_with_set_vacuum = add_vacuum(material_with_no_vacuum, vacuum)
155
+ # to compare correctly, we need to translate the expected material to the bottom
156
+ # as it down when setting vacuum to 0
157
+ material = Material(SI_SLAB)
158
+ material_down = translate_to_z_level(material, z_level="bottom")
159
+
160
+ assertion_utils.assert_deep_almost_equal(material_down.to_json(), material_with_set_vacuum.to_json())