@mat3ra/made 2025.1.1-0 → 2025.1.18-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/lattice.py +20 -8
- package/src/py/mat3ra/made/tools/build/defect/__init__.py +19 -8
- package/src/py/mat3ra/made/tools/build/defect/builders.py +1 -0
- package/src/py/mat3ra/made/tools/build/nanoribbon/builders.py +5 -5
- package/src/py/mat3ra/made/tools/build/passivation/__init__.py +23 -1
- package/src/py/mat3ra/made/tools/build/perturbation/builders.py +1 -1
- package/src/py/mat3ra/made/tools/modify.py +40 -11
- package/src/py/mat3ra/made/tools/utils/__init__.py +0 -18
- package/tests/py/unit/test_lattice.py +17 -0
- package/tests/py/unit/test_material.py +12 -1
- package/tests/py/unit/test_tools_build_interface.py +1 -1
- package/tests/py/unit/test_tools_build_perturbation.py +1 -1
- package/tests/py/unit/test_tools_modify.py +1 -1
package/package.json
CHANGED
|
@@ -12,6 +12,16 @@ DEFAULT_UNITS = {"length": "angstrom", "angle": "degree"}
|
|
|
12
12
|
DEFAULT_TYPE = "TRI"
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
class LatticeVectors(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
A class to represent the lattice vectors.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
a: List[float] = [1.0, 0.0, 0.0]
|
|
21
|
+
b: List[float] = [0.0, 1.0, 0.0]
|
|
22
|
+
c: List[float] = [0.0, 0.0, 1.0]
|
|
23
|
+
|
|
24
|
+
|
|
15
25
|
class Lattice(RoundNumericValuesMixin, BaseModel):
|
|
16
26
|
a: float = 1.0
|
|
17
27
|
b: float = a
|
|
@@ -23,7 +33,7 @@ class Lattice(RoundNumericValuesMixin, BaseModel):
|
|
|
23
33
|
type: str = DEFAULT_TYPE
|
|
24
34
|
|
|
25
35
|
@property
|
|
26
|
-
def vectors(self) ->
|
|
36
|
+
def vectors(self) -> LatticeVectors:
|
|
27
37
|
a = self.a
|
|
28
38
|
b = self.b
|
|
29
39
|
c = self.c
|
|
@@ -44,12 +54,12 @@ class Lattice(RoundNumericValuesMixin, BaseModel):
|
|
|
44
54
|
cos_gamma_star = math.cos(gamma_star)
|
|
45
55
|
sin_gamma_star = math.sin(gamma_star)
|
|
46
56
|
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
# Calculate the vectors
|
|
58
|
+
vector_a = [a * sin_beta, 0.0, a * cos_beta]
|
|
59
|
+
vector_b = [-b * sin_alpha * cos_gamma_star, b * sin_alpha * sin_gamma_star, b * cos_alpha]
|
|
60
|
+
vector_c = [0.0, 0.0, c]
|
|
61
|
+
|
|
62
|
+
return LatticeVectors(a=vector_a, b=vector_b, c=vector_c)
|
|
53
63
|
|
|
54
64
|
@classmethod
|
|
55
65
|
def from_vectors_array(
|
|
@@ -96,7 +106,9 @@ class Lattice(RoundNumericValuesMixin, BaseModel):
|
|
|
96
106
|
|
|
97
107
|
@property
|
|
98
108
|
def vector_arrays(self) -> List[List[float]]:
|
|
99
|
-
|
|
109
|
+
"""Returns lattice vectors as a nested array"""
|
|
110
|
+
v = self.vectors
|
|
111
|
+
return [v.a, v.b, v.c]
|
|
100
112
|
|
|
101
113
|
@property
|
|
102
114
|
def cell(self) -> Cell:
|
|
@@ -16,10 +16,7 @@ from .enums import PointDefectTypeEnum
|
|
|
16
16
|
from .factories import DefectBuilderFactory
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def
|
|
20
|
-
configuration: Union[PointDefectConfiguration, AdatomSlabPointDefectConfiguration],
|
|
21
|
-
builder_parameters: Union[PointDefectBuilderParameters, SlabDefectBuilderParameters, None] = None,
|
|
22
|
-
) -> Material:
|
|
19
|
+
def get_material_with_defect(configuration, builder_parameters):
|
|
23
20
|
"""
|
|
24
21
|
Return a material with a selected defect added.
|
|
25
22
|
|
|
@@ -38,6 +35,23 @@ def create_defect(
|
|
|
38
35
|
return builder.get_material(configuration) if builder else configuration.crystal
|
|
39
36
|
|
|
40
37
|
|
|
38
|
+
def create_defect(
|
|
39
|
+
configuration: Union[PointDefectConfiguration, AdatomSlabPointDefectConfiguration],
|
|
40
|
+
builder_parameters: Union[PointDefectBuilderParameters, SlabDefectBuilderParameters, None] = None,
|
|
41
|
+
) -> Material:
|
|
42
|
+
"""
|
|
43
|
+
Return a material with a selected defect added.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
configuration: The configuration of the defect to be added.
|
|
47
|
+
builder_parameters: The parameters to be used by the defect builder.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The material with the defect added.
|
|
51
|
+
"""
|
|
52
|
+
return get_material_with_defect(configuration, builder_parameters)
|
|
53
|
+
|
|
54
|
+
|
|
41
55
|
def create_defects(
|
|
42
56
|
configurations: Union[List[PointDefectConfiguration], List[AdatomSlabPointDefectConfiguration]],
|
|
43
57
|
builder_parameters: Union[PointDefectBuilderParameters, SlabDefectBuilderParameters, None] = None,
|
|
@@ -57,10 +71,7 @@ def create_defects(
|
|
|
57
71
|
for configuration in configurations:
|
|
58
72
|
if material_with_defect:
|
|
59
73
|
configuration.crystal = material_with_defect
|
|
60
|
-
|
|
61
|
-
BuilderClass = DefectBuilderFactory.get_class_by_name(configuration.defect_type)
|
|
62
|
-
builder = BuilderClass(builder_parameters)
|
|
63
|
-
material_with_defect = builder.get_material(configuration) if builder else configuration.crystal
|
|
74
|
+
material_with_defect = get_material_with_defect(configuration, builder_parameters)
|
|
64
75
|
|
|
65
76
|
return material_with_defect
|
|
66
77
|
|
|
@@ -146,6 +146,7 @@ class VoronoiInterstitialPointDefectBuilder(PointDefectBuilder):
|
|
|
146
146
|
),
|
|
147
147
|
)
|
|
148
148
|
pymatgen_structure.append(closest_interstitial.site.species, closest_interstitial.site.frac_coords)
|
|
149
|
+
pymatgen_structure.remove_oxidation_states()
|
|
149
150
|
return [pymatgen_structure]
|
|
150
151
|
|
|
151
152
|
|
|
@@ -73,11 +73,11 @@ class NanoribbonBuilder(BaseBuilder):
|
|
|
73
73
|
nanoribbon_length, nanoribbon_width = nanoribbon_width, nanoribbon_length
|
|
74
74
|
vacuum_width, vacuum_length = vacuum_length, vacuum_width
|
|
75
75
|
|
|
76
|
-
length_cartesian = nanoribbon_length * np.dot(np.array(material.lattice.vectors
|
|
77
|
-
width_cartesian = nanoribbon_width * np.dot(np.array(material.lattice.vectors
|
|
78
|
-
height_cartesian = np.dot(np.array(material.lattice.vectors
|
|
79
|
-
vacuum_length_cartesian = vacuum_length * np.dot(np.array(material.lattice.vectors
|
|
80
|
-
vacuum_width_cartesian = vacuum_width * np.dot(np.array(material.lattice.vectors
|
|
76
|
+
length_cartesian = nanoribbon_length * np.dot(np.array(material.lattice.vectors.a), np.array([1, 0, 0]))
|
|
77
|
+
width_cartesian = nanoribbon_width * np.dot(np.array(material.lattice.vectors.b), np.array([0, 1, 0]))
|
|
78
|
+
height_cartesian = np.dot(np.array(material.lattice.vectors.c), np.array([0, 0, 1]))
|
|
79
|
+
vacuum_length_cartesian = vacuum_length * np.dot(np.array(material.lattice.vectors.a), np.array([1, 0, 0]))
|
|
80
|
+
vacuum_width_cartesian = vacuum_width * np.dot(np.array(material.lattice.vectors.b), np.array([0, 1, 0]))
|
|
81
81
|
|
|
82
82
|
return length_cartesian, width_cartesian, height_cartesian, vacuum_length_cartesian, vacuum_width_cartesian
|
|
83
83
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections import Counter
|
|
2
|
+
from typing import Union, List, Dict
|
|
2
3
|
|
|
3
4
|
from mat3ra.made.material import Material
|
|
4
5
|
from .configuration import PassivationConfiguration
|
|
@@ -36,3 +37,24 @@ def get_unique_coordination_numbers(
|
|
|
36
37
|
material_with_crystal_sites = MaterialWithCrystalSites.from_material(configuration.slab)
|
|
37
38
|
material_with_crystal_sites.analyze()
|
|
38
39
|
return material_with_crystal_sites.get_unique_coordination_numbers(cutoff=cutoff)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_coordination_numbers_distribution(
|
|
43
|
+
configuration: PassivationConfiguration,
|
|
44
|
+
cutoff: float = 3.0,
|
|
45
|
+
) -> Dict[int, int]:
|
|
46
|
+
"""
|
|
47
|
+
Get the unique coordination numbers for the provided passivation configuration and cutoff radius.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
configuration (PassivationConfiguration): The configuration object.
|
|
51
|
+
cutoff (float): The cutoff radius for defining neighbors.
|
|
52
|
+
Returns:
|
|
53
|
+
set: The unique coordination numbers.
|
|
54
|
+
"""
|
|
55
|
+
material_with_crystal_sites = MaterialWithCrystalSites.from_material(configuration.slab)
|
|
56
|
+
material_with_crystal_sites.analyze()
|
|
57
|
+
coordinatation_numbers = material_with_crystal_sites.get_coordination_numbers(cutoff=cutoff)
|
|
58
|
+
sorted_coordinatation_numbers = sorted(coordinatation_numbers.values)
|
|
59
|
+
distribution = dict(Counter(sorted_coordinatation_numbers))
|
|
60
|
+
return distribution
|
|
@@ -65,7 +65,7 @@ class DistancePreservingSlabPerturbationBuilder(SlabPerturbationBuilder):
|
|
|
65
65
|
|
|
66
66
|
class CellMatchingDistancePreservingSlabPerturbationBuilder(DistancePreservingSlabPerturbationBuilder):
|
|
67
67
|
def _transform_lattice_vectors(self, configuration: PerturbationConfiguration) -> List[List[float]]:
|
|
68
|
-
cell_vectors = configuration.material.lattice.
|
|
68
|
+
cell_vectors = configuration.material.lattice.vector_arrays
|
|
69
69
|
return [configuration.perturbation_function_holder.transform_coordinates(coord) for coord in cell_vectors]
|
|
70
70
|
|
|
71
71
|
def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Material:
|
|
@@ -40,7 +40,7 @@ def filter_by_label(material: Material, label: Union[int, str]) -> Material:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def translate_to_z_level(
|
|
43
|
-
material: Material, z_level: Optional[Literal["top", "bottom", "center"]] = "bottom"
|
|
43
|
+
material: Material, z_level: Optional[Literal["top", "bottom", "center"]] = "bottom", tolerance: float = 1e-6
|
|
44
44
|
) -> Material:
|
|
45
45
|
"""
|
|
46
46
|
Translate atoms to the specified z-level.
|
|
@@ -48,15 +48,16 @@ def translate_to_z_level(
|
|
|
48
48
|
Args:
|
|
49
49
|
material (Material): The material object to normalize.
|
|
50
50
|
z_level (str): The z-level to translate the atoms to (top, bottom, center)
|
|
51
|
+
tolerance (float): The tolerance value to avoid moving past unit cell.
|
|
51
52
|
Returns:
|
|
52
53
|
Material: The translated material object.
|
|
53
54
|
"""
|
|
54
55
|
min_z = get_atomic_coordinates_extremum(material, "min")
|
|
55
56
|
max_z = get_atomic_coordinates_extremum(material)
|
|
56
57
|
if z_level == "top":
|
|
57
|
-
material = translate_by_vector(material, vector=[0, 0, 1 - max_z])
|
|
58
|
+
material = translate_by_vector(material, vector=[0, 0, 1 - max_z - tolerance])
|
|
58
59
|
elif z_level == "bottom":
|
|
59
|
-
material = translate_by_vector(material, vector=[0, 0, -min_z])
|
|
60
|
+
material = translate_by_vector(material, vector=[0, 0, -min_z + tolerance])
|
|
60
61
|
elif z_level == "center":
|
|
61
62
|
material = translate_by_vector(material, vector=[0, 0, (1 - min_z - max_z) / 2])
|
|
62
63
|
return material
|
|
@@ -201,7 +202,7 @@ def filter_by_layers(
|
|
|
201
202
|
if central_atom_id is not None:
|
|
202
203
|
center_coordinate = material.basis.coordinates.get_element_value_by_index(central_atom_id)
|
|
203
204
|
vectors = material.lattice.vectors
|
|
204
|
-
direction_vector = vectors
|
|
205
|
+
direction_vector = vectors.c
|
|
205
206
|
|
|
206
207
|
def condition(coordinate):
|
|
207
208
|
return is_coordinate_within_layer(coordinate, center_coordinate, direction_vector, layer_thickness)
|
|
@@ -240,6 +241,7 @@ def filter_by_sphere(
|
|
|
240
241
|
center_coordinate: List[float] = [0, 0, 0],
|
|
241
242
|
central_atom_id: Optional[int] = None,
|
|
242
243
|
radius: float = 1,
|
|
244
|
+
tolerance: float = 0.0,
|
|
243
245
|
invert: bool = False,
|
|
244
246
|
) -> Material:
|
|
245
247
|
"""
|
|
@@ -249,6 +251,7 @@ def filter_by_sphere(
|
|
|
249
251
|
material (Material): The material object to filter.
|
|
250
252
|
central_atom_id (int): Index of the central atom.
|
|
251
253
|
radius (float): Radius of the sphere in angstroms.
|
|
254
|
+
tolerance (float): The tolerance value to include atoms on the edge of the sphere.
|
|
252
255
|
invert (bool): Whether to invert the selection.
|
|
253
256
|
|
|
254
257
|
Returns:
|
|
@@ -258,7 +261,7 @@ def filter_by_sphere(
|
|
|
258
261
|
material=material,
|
|
259
262
|
atom_index=central_atom_id,
|
|
260
263
|
coordinate=center_coordinate,
|
|
261
|
-
radius=radius,
|
|
264
|
+
radius=radius + tolerance,
|
|
262
265
|
)
|
|
263
266
|
return filter_by_ids(material, ids, invert=invert)
|
|
264
267
|
|
|
@@ -268,6 +271,7 @@ def filter_by_circle_projection(
|
|
|
268
271
|
x: float = 0.5,
|
|
269
272
|
y: float = 0.5,
|
|
270
273
|
r: float = 0.25,
|
|
274
|
+
tolerance: float = 0.0,
|
|
271
275
|
use_cartesian_coordinates: bool = False,
|
|
272
276
|
invert_selection: bool = False,
|
|
273
277
|
) -> Material:
|
|
@@ -279,6 +283,7 @@ def filter_by_circle_projection(
|
|
|
279
283
|
x (float): The x-coordinate of the circle center.
|
|
280
284
|
y (float): The y-coordinate of the circle center.
|
|
281
285
|
r (float): The radius of the circle.
|
|
286
|
+
tolerance (float): The tolerance value to include atoms on the edge of the circle.
|
|
282
287
|
use_cartesian_coordinates (bool): Whether to use cartesian coordinates
|
|
283
288
|
invert_selection (bool): Whether to invert the selection.
|
|
284
289
|
|
|
@@ -287,7 +292,7 @@ def filter_by_circle_projection(
|
|
|
287
292
|
"""
|
|
288
293
|
|
|
289
294
|
def condition(coordinate):
|
|
290
|
-
return is_coordinate_in_cylinder(coordinate, [x, y, 0], r, min_z=0, max_z=1)
|
|
295
|
+
return is_coordinate_in_cylinder(coordinate, [x, y, 0], r + tolerance, min_z=0, max_z=1)
|
|
291
296
|
|
|
292
297
|
return filter_by_condition_on_coordinates(
|
|
293
298
|
material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection
|
|
@@ -300,6 +305,7 @@ def filter_by_cylinder(
|
|
|
300
305
|
min_z: Optional[float] = None,
|
|
301
306
|
max_z: Optional[float] = None,
|
|
302
307
|
radius: float = 0.25,
|
|
308
|
+
tolerance: float = 0.0,
|
|
303
309
|
use_cartesian_coordinates: bool = False,
|
|
304
310
|
invert_selection: bool = False,
|
|
305
311
|
) -> Material:
|
|
@@ -331,7 +337,9 @@ def filter_by_cylinder(
|
|
|
331
337
|
)
|
|
332
338
|
|
|
333
339
|
def condition(coordinate):
|
|
334
|
-
return is_coordinate_in_cylinder(
|
|
340
|
+
return is_coordinate_in_cylinder(
|
|
341
|
+
coordinate, center_position, radius + tolerance, min_z - tolerance, max_z + tolerance
|
|
342
|
+
)
|
|
335
343
|
|
|
336
344
|
return filter_by_condition_on_coordinates(
|
|
337
345
|
material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection
|
|
@@ -342,6 +350,7 @@ def filter_by_rectangle_projection(
|
|
|
342
350
|
material: Material,
|
|
343
351
|
min_coordinate: List[float] = [0, 0],
|
|
344
352
|
max_coordinate: List[float] = [1, 1],
|
|
353
|
+
tolerance: float = 0.0,
|
|
345
354
|
use_cartesian_coordinates: bool = False,
|
|
346
355
|
invert_selection: bool = False,
|
|
347
356
|
) -> Material:
|
|
@@ -352,6 +361,7 @@ def filter_by_rectangle_projection(
|
|
|
352
361
|
material (Material): The material object to filter.
|
|
353
362
|
min_coordinate (List[float]): The minimum coordinate of the rectangle.
|
|
354
363
|
max_coordinate (List[float]): The maximum coordinate of the rectangle.
|
|
364
|
+
tolerance (float): The tolerance value to include atoms on the edges of the rectangle.
|
|
355
365
|
use_cartesian_coordinates (bool): Whether to use cartesian coordinates
|
|
356
366
|
invert_selection (bool): Whether to invert the selection.
|
|
357
367
|
|
|
@@ -360,8 +370,8 @@ def filter_by_rectangle_projection(
|
|
|
360
370
|
"""
|
|
361
371
|
min_z = get_atomic_coordinates_extremum(material, "min", "z", use_cartesian_coordinates=use_cartesian_coordinates)
|
|
362
372
|
max_z = get_atomic_coordinates_extremum(material, "max", "z", use_cartesian_coordinates=use_cartesian_coordinates)
|
|
363
|
-
min_coordinate = min_coordinate[:2] + [min_z]
|
|
364
|
-
max_coordinate = max_coordinate[:2] + [max_z]
|
|
373
|
+
min_coordinate = [c - tolerance for c in min_coordinate[:2]] + [min_z]
|
|
374
|
+
max_coordinate = [c + tolerance for c in max_coordinate[:2]] + [max_z]
|
|
365
375
|
|
|
366
376
|
def condition(coordinate):
|
|
367
377
|
return is_coordinate_in_box(coordinate, min_coordinate, max_coordinate)
|
|
@@ -375,6 +385,7 @@ def filter_by_box(
|
|
|
375
385
|
material: Material,
|
|
376
386
|
min_coordinate: Optional[List[float]] = None,
|
|
377
387
|
max_coordinate: Optional[List[float]] = None,
|
|
388
|
+
tolerance: float = 0.0,
|
|
378
389
|
use_cartesian_coordinates: bool = False,
|
|
379
390
|
invert_selection: bool = False,
|
|
380
391
|
) -> Material:
|
|
@@ -385,6 +396,7 @@ def filter_by_box(
|
|
|
385
396
|
material (Material): The material to filter.
|
|
386
397
|
min_coordinate (List[float], optional): The minimum coordinate of the box. Defaults to material's min.
|
|
387
398
|
max_coordinate (List[float], optional): The maximum coordinate of the box. Defaults to material's max.
|
|
399
|
+
tolerance (float): The tolerance value to include atoms on the edges of the box.
|
|
388
400
|
use_cartesian_coordinates (bool): Whether to use cartesian coordinates.
|
|
389
401
|
invert_selection (bool): Whether to invert the selection.
|
|
390
402
|
|
|
@@ -396,6 +408,9 @@ def filter_by_box(
|
|
|
396
408
|
min_coordinate = min_coordinate if min_coordinate is not None else default_min
|
|
397
409
|
max_coordinate = max_coordinate if max_coordinate is not None else default_max
|
|
398
410
|
|
|
411
|
+
min_coordinate = [c - tolerance for c in min_coordinate]
|
|
412
|
+
max_coordinate = [c + tolerance for c in max_coordinate]
|
|
413
|
+
|
|
399
414
|
def condition(coordinate):
|
|
400
415
|
return is_coordinate_in_box(coordinate, min_coordinate, max_coordinate)
|
|
401
416
|
|
|
@@ -411,6 +426,7 @@ def filter_by_triangle_projection(
|
|
|
411
426
|
coordinate_3: Optional[List[float]] = None,
|
|
412
427
|
min_z: Optional[float] = None,
|
|
413
428
|
max_z: Optional[float] = None,
|
|
429
|
+
tolerance: float = 0.0,
|
|
414
430
|
use_cartesian_coordinates: bool = False,
|
|
415
431
|
invert_selection: bool = False,
|
|
416
432
|
) -> Material:
|
|
@@ -424,6 +440,7 @@ def filter_by_triangle_projection(
|
|
|
424
440
|
coordinate_3 (List[float], optional): Third vertex of the triangle. Defaults to material's corner.
|
|
425
441
|
min_z (float, optional): Lower z-limit. Defaults to material's min z.
|
|
426
442
|
max_z (float, optional): Upper z-limit. Defaults to material's max z.
|
|
443
|
+
tolerance (float): The tolerance value to include atoms on the top and bottom faces of the prism.
|
|
427
444
|
use_cartesian_coordinates (bool): Whether to use cartesian coordinates.
|
|
428
445
|
invert_selection (bool): Whether to invert the selection.
|
|
429
446
|
|
|
@@ -440,13 +457,25 @@ def filter_by_triangle_projection(
|
|
|
440
457
|
min_z = get_atomic_coordinates_extremum(
|
|
441
458
|
material, "min", "z", use_cartesian_coordinates=use_cartesian_coordinates
|
|
442
459
|
)
|
|
460
|
+
else:
|
|
461
|
+
min_z -= tolerance
|
|
462
|
+
|
|
443
463
|
if max_z is None:
|
|
444
464
|
max_z = get_atomic_coordinates_extremum(
|
|
445
465
|
material, "max", "z", use_cartesian_coordinates=use_cartesian_coordinates
|
|
446
466
|
)
|
|
467
|
+
else:
|
|
468
|
+
max_z += tolerance
|
|
447
469
|
|
|
448
470
|
def condition(coordinate):
|
|
449
|
-
return is_coordinate_in_triangular_prism(
|
|
471
|
+
return is_coordinate_in_triangular_prism(
|
|
472
|
+
coordinate,
|
|
473
|
+
coordinate_1,
|
|
474
|
+
coordinate_2,
|
|
475
|
+
coordinate_3,
|
|
476
|
+
min_z,
|
|
477
|
+
max_z,
|
|
478
|
+
)
|
|
450
479
|
|
|
451
480
|
return filter_by_condition_on_coordinates(
|
|
452
481
|
material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection
|
|
@@ -535,7 +564,7 @@ def remove_vacuum(material: Material, from_top=True, from_bottom=True, fixed_pad
|
|
|
535
564
|
new_basis.to_cartesian()
|
|
536
565
|
new_lattice = translated_material.lattice
|
|
537
566
|
new_lattice.c = get_atomic_coordinates_extremum(translated_material, use_cartesian_coordinates=True) + fixed_padding
|
|
538
|
-
new_basis.cell.vector3 = new_lattice.vectors
|
|
567
|
+
new_basis.cell.vector3 = new_lattice.vectors.c
|
|
539
568
|
new_basis.to_crystal()
|
|
540
569
|
new_material = material.clone()
|
|
541
570
|
|
|
@@ -10,24 +10,6 @@ DEFAULT_SCALING_FACTOR = np.array([3, 3, 3])
|
|
|
10
10
|
DEFAULT_TRANSLATION_VECTOR = 1 / DEFAULT_SCALING_FACTOR
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
# TODO: convert to accept ASE Atoms object
|
|
14
|
-
def translate_to_bottom_pymatgen_structure(structure: PymatgenStructure):
|
|
15
|
-
"""
|
|
16
|
-
Translate the structure to the bottom of the cell.
|
|
17
|
-
Args:
|
|
18
|
-
structure (PymatgenStructure): The pymatgen Structure object to translate.
|
|
19
|
-
|
|
20
|
-
Returns:
|
|
21
|
-
PymatgenStructure: The translated pymatgen Structure object.
|
|
22
|
-
"""
|
|
23
|
-
min_c = min(site.c for site in structure)
|
|
24
|
-
translation_vector = [0, 0, -min_c]
|
|
25
|
-
translated_structure = structure.copy()
|
|
26
|
-
for site in translated_structure:
|
|
27
|
-
site.coords += translation_vector
|
|
28
|
-
return translated_structure
|
|
29
|
-
|
|
30
|
-
|
|
31
13
|
def decorator_convert_2x2_to_3x3(func: Callable) -> Callable:
|
|
32
14
|
"""
|
|
33
15
|
Decorator to convert a 2x2 matrix to a 3x3 matrix.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from mat3ra.made.lattice import Lattice
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_lattice_vectors_access():
|
|
5
|
+
lattice = Lattice(a=2.0, b=3.0, c=4.0)
|
|
6
|
+
|
|
7
|
+
# Test individual vector access
|
|
8
|
+
assert isinstance(lattice.vectors.a, list)
|
|
9
|
+
assert isinstance(lattice.vectors.b, list)
|
|
10
|
+
assert isinstance(lattice.vectors.c, list)
|
|
11
|
+
|
|
12
|
+
# Test vector arrays access
|
|
13
|
+
arrays = lattice.vector_arrays
|
|
14
|
+
assert len(arrays) == 3
|
|
15
|
+
assert arrays[0] == lattice.vectors.a
|
|
16
|
+
assert arrays[1] == lattice.vectors.b
|
|
17
|
+
assert arrays[2] == lattice.vectors.c
|
|
@@ -25,4 +25,15 @@ def test_basis_to_json():
|
|
|
25
25
|
assertion_utils.assert_deep_almost_equal(expected_basis_config, basis.to_json())
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
def test_basis_cell_lattice_sync():
|
|
29
|
+
"""Test synchronization between basis.cell and material.lattice"""
|
|
30
|
+
material = Material.create(Material.default_config)
|
|
31
|
+
|
|
32
|
+
# Change lattice vectors
|
|
33
|
+
new_vectors = [[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0]]
|
|
34
|
+
material.set_new_lattice_vectors(*new_vectors)
|
|
35
|
+
|
|
36
|
+
# Verify basis.cell matches new lattice vectors
|
|
37
|
+
assertion_utils.assert_deep_almost_equal(new_vectors, material.basis.cell.vectors_as_array)
|
|
38
|
+
|
|
39
|
+
assertion_utils.assert_deep_almost_equal(new_vectors, material.lattice.vector_arrays)
|
|
@@ -64,7 +64,7 @@ def test_create_twisted_nanoribbon_interface():
|
|
|
64
64
|
|
|
65
65
|
expected_cell_vectors = [[15.102811, 0.0, 0.0], [0.0, 16.108175208, 0.0], [0.0, 0.0, 20.0]]
|
|
66
66
|
expected_coordinate = [0.704207885, 0.522108183, 0.65]
|
|
67
|
-
assertion_utils.assert_deep_almost_equal(expected_cell_vectors, interface.lattice.
|
|
67
|
+
assertion_utils.assert_deep_almost_equal(expected_cell_vectors, interface.lattice.vector_arrays)
|
|
68
68
|
assertion_utils.assert_deep_almost_equal(expected_coordinate, interface.basis.coordinates.values[42])
|
|
69
69
|
|
|
70
70
|
|
|
@@ -46,4 +46,4 @@ def test_distance_preserved_sine_perturbation():
|
|
|
46
46
|
[-12.043583, 21.367367, 0.0],
|
|
47
47
|
[0.0, 0.0, 20.0],
|
|
48
48
|
]
|
|
49
|
-
assertion_utils.assert_deep_almost_equal(expected_cell, perturbed_slab.lattice.
|
|
49
|
+
assertion_utils.assert_deep_almost_equal(expected_cell, perturbed_slab.lattice.vector_arrays)
|
|
@@ -160,7 +160,7 @@ def test_remove_vacuum():
|
|
|
160
160
|
material = Material(SI_SLAB)
|
|
161
161
|
material_down = translate_to_z_level(material, z_level="bottom")
|
|
162
162
|
|
|
163
|
-
assertion_utils.assert_deep_almost_equal(material_down.to_json(), material_with_set_vacuum.to_json())
|
|
163
|
+
assertion_utils.assert_deep_almost_equal(material_down.to_json(), material_with_set_vacuum.to_json(), atol=1e-3)
|
|
164
164
|
|
|
165
165
|
|
|
166
166
|
def test_rotate():
|