@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 +1 -1
- package/src/py/mat3ra/made/basis.py +2 -2
- package/src/py/mat3ra/made/cell.py +1 -1
- package/src/py/mat3ra/made/tools/analyze.py +29 -2
- package/src/py/mat3ra/made/tools/convert/__init__.py +3 -0
- package/src/py/mat3ra/made/tools/modify.py +153 -28
- package/src/py/mat3ra/made/tools/third_party.py +2 -0
- package/src/py/mat3ra/made/tools/utils.py +57 -6
- package/tests/py/unit/fixtures.py +19 -6
- package/tests/py/unit/test_tools_modify.py +33 -1
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
50
|
+
Material: The translated material object.
|
|
48
51
|
"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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 =
|
|
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
|
-
|
|
245
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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())
|