@mat3ra/made 2024.7.31-1 → 2024.8.16-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 +16 -16
- package/src/py/mat3ra/made/lattice.py +28 -6
- package/src/py/mat3ra/made/material.py +5 -0
- package/src/py/mat3ra/made/tools/build/defect/builders.py +7 -6
- package/src/py/mat3ra/made/tools/build/defect/configuration.py +20 -7
- package/src/py/mat3ra/made/tools/build/interface/builders.py +7 -2
- package/src/py/mat3ra/made/tools/build/perturbation/__init__.py +37 -0
- package/src/py/mat3ra/made/tools/build/perturbation/builders.py +83 -0
- package/src/py/mat3ra/made/tools/build/perturbation/configuration.py +28 -0
- package/src/py/mat3ra/made/tools/build/supercell.py +10 -6
- package/src/py/mat3ra/made/tools/modify.py +10 -10
- package/src/py/mat3ra/made/tools/utils/__init__.py +108 -0
- package/src/py/mat3ra/made/tools/{utils.py → utils/coordinate.py} +55 -162
- package/src/py/mat3ra/made/tools/utils/factories.py +7 -0
- package/src/py/mat3ra/made/tools/utils/functions.py +29 -0
- package/src/py/mat3ra/made/tools/utils/perturbation.py +138 -0
- package/tests/py/unit/fixtures.py +30 -1
- package/tests/py/unit/test_tools_build_defect.py +4 -2
- package/tests/py/unit/test_tools_build_perturbation.py +50 -0
package/package.json
CHANGED
|
@@ -24,14 +24,14 @@ class Basis(RoundNumericValuesMixin, BaseModel):
|
|
|
24
24
|
coordinates: List[Dict],
|
|
25
25
|
units: str,
|
|
26
26
|
labels: Optional[List[Dict]] = None,
|
|
27
|
-
cell: Optional[
|
|
27
|
+
cell: Optional[List[List[float]]] = None,
|
|
28
28
|
constraints: Optional[List[Dict]] = None,
|
|
29
29
|
) -> "Basis":
|
|
30
30
|
return Basis(
|
|
31
31
|
elements=ArrayWithIds.from_list_of_dicts(elements),
|
|
32
32
|
coordinates=ArrayWithIds.from_list_of_dicts(coordinates),
|
|
33
33
|
units=units,
|
|
34
|
-
cell=Cell.
|
|
34
|
+
cell=Cell.from_vectors_array(cell),
|
|
35
35
|
labels=ArrayWithIds.from_list_of_dicts(labels) if labels else ArrayWithIds(values=[]),
|
|
36
36
|
constraints=ArrayWithIds.from_list_of_dicts(constraints) if constraints else ArrayWithIds(values=[]),
|
|
37
37
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import List, Optional
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
from mat3ra.utils.mixins import RoundNumericValuesMixin
|
|
@@ -13,13 +13,13 @@ class Cell(RoundNumericValuesMixin, BaseModel):
|
|
|
13
13
|
__round_precision__ = 6
|
|
14
14
|
|
|
15
15
|
@classmethod
|
|
16
|
-
def
|
|
17
|
-
if
|
|
18
|
-
|
|
19
|
-
return cls(vector1=
|
|
16
|
+
def from_vectors_array(cls, vectors_array: Optional[List[List[float]]] = None) -> "Cell":
|
|
17
|
+
if vectors_array is None:
|
|
18
|
+
vectors_array = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
|
|
19
|
+
return cls(vector1=vectors_array[0], vector2=vectors_array[1], vector3=vectors_array[2])
|
|
20
20
|
|
|
21
21
|
@property
|
|
22
|
-
def
|
|
22
|
+
def vectors_as_array(self, skip_rounding=False) -> List[List[float]]:
|
|
23
23
|
if skip_rounding:
|
|
24
24
|
return [self.vector1, self.vector2, self.vector3]
|
|
25
25
|
return self.round_array_or_number([self.vector1, self.vector2, self.vector3])
|
|
@@ -32,22 +32,22 @@ class Cell(RoundNumericValuesMixin, BaseModel):
|
|
|
32
32
|
self.vector3 if skip_rounding else _(self.vector3),
|
|
33
33
|
]
|
|
34
34
|
|
|
35
|
-
def clone(self):
|
|
36
|
-
return self.
|
|
35
|
+
def clone(self) -> "Cell":
|
|
36
|
+
return self.from_vectors_array(self.vectors_as_array)
|
|
37
37
|
|
|
38
|
-
def clone_and_scale_by_matrix(self, matrix):
|
|
38
|
+
def clone_and_scale_by_matrix(self, matrix: List[List[float]]) -> "Cell":
|
|
39
39
|
new_cell = self.clone()
|
|
40
40
|
new_cell.scale_by_matrix(matrix)
|
|
41
41
|
return new_cell
|
|
42
42
|
|
|
43
|
-
def convert_point_to_cartesian(self, point):
|
|
44
|
-
np_vector = np.array(self.
|
|
43
|
+
def convert_point_to_cartesian(self, point: List[float]) -> List[float]:
|
|
44
|
+
np_vector = np.array(self.vectors_as_array)
|
|
45
45
|
return np.dot(point, np_vector)
|
|
46
46
|
|
|
47
|
-
def convert_point_to_crystal(self, point):
|
|
48
|
-
np_vector = np.array(self.
|
|
47
|
+
def convert_point_to_crystal(self, point: List[float]) -> List[float]:
|
|
48
|
+
np_vector = np.array(self.vectors_as_array)
|
|
49
49
|
return np.dot(point, np.linalg.inv(np_vector))
|
|
50
50
|
|
|
51
|
-
def scale_by_matrix(self, matrix):
|
|
52
|
-
np_vector = np.array(self.
|
|
53
|
-
self.vector1, self.vector2, self.vector3 = np.dot(matrix, np_vector).tolist()
|
|
51
|
+
def scale_by_matrix(self, matrix: List[List[float]]):
|
|
52
|
+
np_vector = np.array(self.vectors_as_array)
|
|
53
|
+
self.vector1, self.vector2, self.vector3 = np.dot(np.array(matrix), np_vector).tolist()
|
|
@@ -8,6 +8,8 @@ from pydantic import BaseModel
|
|
|
8
8
|
from .cell import Cell
|
|
9
9
|
|
|
10
10
|
HASH_TOLERANCE = 3
|
|
11
|
+
DEFAULT_UNITS = {"length": "angstrom", "angle": "degree"}
|
|
12
|
+
DEFAULT_TYPE = "TRI"
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class Lattice(RoundNumericValuesMixin, BaseModel):
|
|
@@ -17,11 +19,8 @@ class Lattice(RoundNumericValuesMixin, BaseModel):
|
|
|
17
19
|
alpha: float = 90.0
|
|
18
20
|
beta: float = 90.0
|
|
19
21
|
gamma: float = 90.0
|
|
20
|
-
units: Dict[str, str] =
|
|
21
|
-
|
|
22
|
-
"angle": "degree",
|
|
23
|
-
}
|
|
24
|
-
type: str = "TRI"
|
|
22
|
+
units: Dict[str, str] = DEFAULT_UNITS
|
|
23
|
+
type: str = DEFAULT_TYPE
|
|
25
24
|
|
|
26
25
|
@property
|
|
27
26
|
def vectors(self) -> List[List[float]]:
|
|
@@ -52,6 +51,29 @@ class Lattice(RoundNumericValuesMixin, BaseModel):
|
|
|
52
51
|
[0.0, 0.0, c],
|
|
53
52
|
]
|
|
54
53
|
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_vectors_array(
|
|
56
|
+
cls, vectors: List[List[float]], units: Optional[Dict[str, str]] = None, type: Optional[str] = None
|
|
57
|
+
) -> "Lattice":
|
|
58
|
+
"""
|
|
59
|
+
Create a Lattice object from a nested array of vectors.
|
|
60
|
+
Args:
|
|
61
|
+
vectors (List[List[float]]): A nested array of vectors.
|
|
62
|
+
Returns:
|
|
63
|
+
Lattice: A Lattice object.
|
|
64
|
+
"""
|
|
65
|
+
a = np.linalg.norm(vectors[0])
|
|
66
|
+
b = np.linalg.norm(vectors[1])
|
|
67
|
+
c = np.linalg.norm(vectors[2])
|
|
68
|
+
alpha = np.degrees(np.arccos(np.dot(vectors[1], vectors[2]) / (b * c)))
|
|
69
|
+
beta = np.degrees(np.arccos(np.dot(vectors[0], vectors[2]) / (a * c)))
|
|
70
|
+
gamma = np.degrees(np.arccos(np.dot(vectors[0], vectors[1]) / (a * b)))
|
|
71
|
+
if units is None:
|
|
72
|
+
units = DEFAULT_UNITS
|
|
73
|
+
if type is None:
|
|
74
|
+
type = DEFAULT_TYPE
|
|
75
|
+
return cls(a=float(a), b=float(b), c=float(c), alpha=alpha, beta=beta, gamma=gamma, units=units, type=type)
|
|
76
|
+
|
|
55
77
|
def to_json(self, skip_rounding: bool = False) -> Dict[str, Any]:
|
|
56
78
|
__round__ = RoundNumericValuesMixin.round_array_or_number
|
|
57
79
|
round_func = __round__ if not skip_rounding else lambda x: x
|
|
@@ -78,7 +100,7 @@ class Lattice(RoundNumericValuesMixin, BaseModel):
|
|
|
78
100
|
|
|
79
101
|
@property
|
|
80
102
|
def cell(self) -> Cell:
|
|
81
|
-
return Cell.
|
|
103
|
+
return Cell.from_vectors_array(self.vector_arrays)
|
|
82
104
|
|
|
83
105
|
def volume(self) -> float:
|
|
84
106
|
np_vector = np.array(self.vector_arrays)
|
|
@@ -90,3 +90,8 @@ class Material(HasDescriptionHasMetadataNamedDefaultableInMemoryEntity):
|
|
|
90
90
|
new_basis = self.basis.copy()
|
|
91
91
|
new_basis.to_crystal()
|
|
92
92
|
self.basis = new_basis
|
|
93
|
+
|
|
94
|
+
def set_coordinates(self, coordinates: List[List[float]]) -> None:
|
|
95
|
+
new_basis = self.basis.copy()
|
|
96
|
+
new_basis.coordinates.values = coordinates
|
|
97
|
+
self.basis = new_basis
|
|
@@ -30,7 +30,8 @@ from ...analyze import (
|
|
|
30
30
|
get_closest_site_id_from_coordinate_and_element,
|
|
31
31
|
)
|
|
32
32
|
from ....utils import get_center_of_coordinates
|
|
33
|
-
from ...utils import transform_coordinate_to_supercell
|
|
33
|
+
from ...utils import transform_coordinate_to_supercell
|
|
34
|
+
from ...utils import coordinate as CoordinateCondition
|
|
34
35
|
from ..utils import merge_materials
|
|
35
36
|
from ..slab import SlabConfiguration, create_slab, Termination
|
|
36
37
|
from ..supercell import create_supercell
|
|
@@ -436,7 +437,7 @@ class IslandSlabDefectBuilder(SlabDefectBuilder):
|
|
|
436
437
|
return self.merge_slab_and_defect(island_material, new_material)
|
|
437
438
|
|
|
438
439
|
def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]:
|
|
439
|
-
condition_callable
|
|
440
|
+
condition_callable = configuration.condition.condition
|
|
440
441
|
return [
|
|
441
442
|
self.create_island(
|
|
442
443
|
material=configuration.crystal,
|
|
@@ -463,7 +464,7 @@ class TerraceSlabDefectBuilder(SlabDefectBuilder):
|
|
|
463
464
|
The normalized cut direction vector in Cartesian coordinates.
|
|
464
465
|
"""
|
|
465
466
|
np_cut_direction = np.array(cut_direction)
|
|
466
|
-
direction_vector = np.dot(np.array(material.basis.cell.
|
|
467
|
+
direction_vector = np.dot(np.array(material.basis.cell.vectors_as_array), np_cut_direction)
|
|
467
468
|
normalized_direction_vector = direction_vector / np.linalg.norm(direction_vector)
|
|
468
469
|
return normalized_direction_vector
|
|
469
470
|
|
|
@@ -499,7 +500,7 @@ class TerraceSlabDefectBuilder(SlabDefectBuilder):
|
|
|
499
500
|
"""
|
|
500
501
|
height_cartesian = self._calculate_height_cartesian(original_material, new_material)
|
|
501
502
|
cut_direction_xy_proj_cart = np.linalg.norm(
|
|
502
|
-
np.dot(np.array(new_material.basis.cell.
|
|
503
|
+
np.dot(np.array(new_material.basis.cell.vectors_as_array), normalized_direction_vector)
|
|
503
504
|
)
|
|
504
505
|
# Slope of the terrace along the cut direction
|
|
505
506
|
hypotenuse = np.linalg.norm([height_cartesian, cut_direction_xy_proj_cart])
|
|
@@ -592,10 +593,10 @@ class TerraceSlabDefectBuilder(SlabDefectBuilder):
|
|
|
592
593
|
)
|
|
593
594
|
|
|
594
595
|
normalized_direction_vector = self._calculate_cut_direction_vector(material, cut_direction)
|
|
595
|
-
condition
|
|
596
|
+
condition = CoordinateCondition.PlaneCoordinateCondition(
|
|
596
597
|
plane_normal=normalized_direction_vector,
|
|
597
598
|
plane_point_coordinate=pivot_coordinate,
|
|
598
|
-
)
|
|
599
|
+
).condition
|
|
599
600
|
atoms_within_terrace = filter_by_condition_on_coordinates(
|
|
600
601
|
material=material_with_additional_layers,
|
|
601
602
|
condition=condition,
|
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
from typing import Optional, List,
|
|
1
|
+
from typing import Optional, List, Union
|
|
2
2
|
from pydantic import BaseModel
|
|
3
3
|
|
|
4
4
|
from mat3ra.code.entity import InMemoryEntity
|
|
5
5
|
from mat3ra.made.material import Material
|
|
6
6
|
|
|
7
7
|
from ...analyze import get_closest_site_id_from_coordinate, get_atomic_coordinates_extremum
|
|
8
|
-
from ...utils import
|
|
8
|
+
from ...utils.coordinate import (
|
|
9
|
+
CylinderCoordinateCondition,
|
|
10
|
+
SphereCoordinateCondition,
|
|
11
|
+
BoxCoordinateCondition,
|
|
12
|
+
TriangularPrismCoordinateCondition,
|
|
13
|
+
PlaneCoordinateCondition,
|
|
14
|
+
)
|
|
9
15
|
from .enums import PointDefectTypeEnum, SlabDefectTypeEnum, AtomPlacementMethodEnum, ComplexDefectTypeEnum
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
class BaseDefectConfiguration(BaseModel):
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
crystal: Material = None
|
|
20
|
+
|
|
21
|
+
class Config:
|
|
22
|
+
arbitrary_types_allowed = True
|
|
15
23
|
|
|
16
24
|
@property
|
|
17
25
|
def _json(self):
|
|
@@ -169,16 +177,21 @@ class IslandSlabDefectConfiguration(SlabDefectConfiguration):
|
|
|
169
177
|
"""
|
|
170
178
|
|
|
171
179
|
defect_type: SlabDefectTypeEnum = SlabDefectTypeEnum.ISLAND
|
|
172
|
-
condition:
|
|
180
|
+
condition: Union[
|
|
181
|
+
CylinderCoordinateCondition,
|
|
182
|
+
SphereCoordinateCondition,
|
|
183
|
+
BoxCoordinateCondition,
|
|
184
|
+
TriangularPrismCoordinateCondition,
|
|
185
|
+
PlaneCoordinateCondition,
|
|
186
|
+
] = CylinderCoordinateCondition()
|
|
173
187
|
|
|
174
188
|
@property
|
|
175
189
|
def _json(self):
|
|
176
|
-
_, condition_json = self.condition
|
|
177
190
|
return {
|
|
178
191
|
**super()._json,
|
|
179
192
|
"type": self.get_cls_name(),
|
|
180
193
|
"defect_type": self.defect_type.name,
|
|
181
|
-
"condition":
|
|
194
|
+
"condition": self.condition.get_json(),
|
|
182
195
|
}
|
|
183
196
|
|
|
184
197
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Any, List, Optional
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
|
+
from mat3ra.made.tools.modify import translate_to_z_level
|
|
4
5
|
from pydantic import BaseModel
|
|
5
6
|
from ase.build.tools import niggli_reduce
|
|
6
7
|
from pymatgen.analysis.interfaces.coherent_interfaces import (
|
|
@@ -144,9 +145,13 @@ class ZSLStrainMatchingInterfaceBuilder(ConvertGeneratedItemsPymatgenStructureMi
|
|
|
144
145
|
|
|
145
146
|
def _generate(self, configuration: InterfaceConfiguration) -> List[PymatgenInterface]:
|
|
146
147
|
generator = ZSLGenerator(**self.build_parameters.strain_matching_parameters.dict())
|
|
148
|
+
substrate_with_atoms_translated_to_bottom = translate_to_z_level(
|
|
149
|
+
configuration.substrate_configuration.bulk, "bottom"
|
|
150
|
+
)
|
|
151
|
+
film_with_atoms_translated_to_bottom = translate_to_z_level(configuration.film_configuration.bulk, "bottom")
|
|
147
152
|
builder = CoherentInterfaceBuilder(
|
|
148
|
-
substrate_structure=to_pymatgen(
|
|
149
|
-
film_structure=to_pymatgen(
|
|
153
|
+
substrate_structure=to_pymatgen(substrate_with_atoms_translated_to_bottom),
|
|
154
|
+
film_structure=to_pymatgen(film_with_atoms_translated_to_bottom),
|
|
150
155
|
substrate_miller=configuration.substrate_configuration.miller_indices,
|
|
151
156
|
film_miller=configuration.film_configuration.miller_indices,
|
|
152
157
|
zslgen=generator,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Union, Optional
|
|
2
|
+
|
|
3
|
+
from mat3ra.made.material import Material
|
|
4
|
+
from .builders import (
|
|
5
|
+
SlabPerturbationBuilder,
|
|
6
|
+
DistancePreservingSlabPerturbationBuilder,
|
|
7
|
+
CellMatchingDistancePreservingSlabPerturbationBuilder,
|
|
8
|
+
)
|
|
9
|
+
from .configuration import PerturbationConfiguration
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_perturbation(
|
|
13
|
+
configuration: PerturbationConfiguration,
|
|
14
|
+
preserve_distance: Optional[bool] = False,
|
|
15
|
+
builder: Union[
|
|
16
|
+
SlabPerturbationBuilder,
|
|
17
|
+
DistancePreservingSlabPerturbationBuilder,
|
|
18
|
+
CellMatchingDistancePreservingSlabPerturbationBuilder,
|
|
19
|
+
None,
|
|
20
|
+
] = None,
|
|
21
|
+
) -> Material:
|
|
22
|
+
"""
|
|
23
|
+
Return a material with a perturbation applied.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
configuration: The configuration of the perturbation to be applied.
|
|
27
|
+
preserve_distance: If True, the builder that preserves the distance between atoms is used.
|
|
28
|
+
builder: The builder to be used to create the perturbation.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The material with the perturbation applied.
|
|
32
|
+
"""
|
|
33
|
+
if builder is None:
|
|
34
|
+
builder = SlabPerturbationBuilder()
|
|
35
|
+
if preserve_distance:
|
|
36
|
+
builder = CellMatchingDistancePreservingSlabPerturbationBuilder()
|
|
37
|
+
return builder.get_material(configuration)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from typing import List, Optional, Any
|
|
2
|
+
|
|
3
|
+
from mat3ra.made.material import Material
|
|
4
|
+
from mat3ra.made.tools.build import BaseBuilder
|
|
5
|
+
|
|
6
|
+
from .configuration import PerturbationConfiguration
|
|
7
|
+
from ...modify import wrap_to_unit_cell, translate_to_z_level
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PerturbationBuilder(BaseBuilder):
|
|
11
|
+
_ConfigurationType: type(PerturbationConfiguration) = PerturbationConfiguration # type: ignore
|
|
12
|
+
_GeneratedItemType: Material = Material
|
|
13
|
+
_PostProcessParametersType: Any = None
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def _prepare_material(configuration: _ConfigurationType) -> _GeneratedItemType:
|
|
17
|
+
new_material = configuration.material.clone()
|
|
18
|
+
new_material = translate_to_z_level(new_material, "center")
|
|
19
|
+
if configuration.use_cartesian_coordinates:
|
|
20
|
+
new_material.to_cartesian()
|
|
21
|
+
return new_material
|
|
22
|
+
|
|
23
|
+
def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]:
|
|
24
|
+
"""Generate materials with applied continuous perturbation based on the given configuration."""
|
|
25
|
+
new_material = self.create_perturbed_slab(configuration)
|
|
26
|
+
return [new_material]
|
|
27
|
+
|
|
28
|
+
def _post_process(
|
|
29
|
+
self,
|
|
30
|
+
items: List[_GeneratedItemType],
|
|
31
|
+
post_process_parameters: Optional[_PostProcessParametersType],
|
|
32
|
+
) -> List[Material]:
|
|
33
|
+
return [wrap_to_unit_cell(item) for item in items]
|
|
34
|
+
|
|
35
|
+
def _update_material_name(self, material: Material, configuration: _ConfigurationType) -> Material:
|
|
36
|
+
perturbation_details = f"Perturbation: {configuration.perturbation_function_holder.get_json().get('type')}"
|
|
37
|
+
material.name = f"{material.name} ({perturbation_details})"
|
|
38
|
+
return material
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SlabPerturbationBuilder(PerturbationBuilder):
|
|
42
|
+
def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Material:
|
|
43
|
+
new_material = self._prepare_material(configuration)
|
|
44
|
+
new_coordinates = [
|
|
45
|
+
configuration.perturbation_function_holder.apply_perturbation(coord)
|
|
46
|
+
for coord in new_material.basis.coordinates.values
|
|
47
|
+
]
|
|
48
|
+
new_material.set_coordinates(new_coordinates)
|
|
49
|
+
return new_material
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DistancePreservingSlabPerturbationBuilder(SlabPerturbationBuilder):
|
|
53
|
+
def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Material:
|
|
54
|
+
new_material = self._prepare_material(configuration)
|
|
55
|
+
new_coordinates = [
|
|
56
|
+
configuration.perturbation_function_holder.transform_coordinates(coord)
|
|
57
|
+
for coord in new_material.basis.coordinates.values
|
|
58
|
+
]
|
|
59
|
+
new_coordinates = [
|
|
60
|
+
configuration.perturbation_function_holder.apply_perturbation(coord) for coord in new_coordinates
|
|
61
|
+
]
|
|
62
|
+
new_material.set_coordinates(new_coordinates)
|
|
63
|
+
return new_material
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CellMatchingDistancePreservingSlabPerturbationBuilder(DistancePreservingSlabPerturbationBuilder):
|
|
67
|
+
def _transform_lattice_vectors(self, configuration: PerturbationConfiguration) -> List[List[float]]:
|
|
68
|
+
cell_vectors = configuration.material.basis.cell.vectors_as_array
|
|
69
|
+
return [configuration.perturbation_function_holder.transform_coordinates(coord) for coord in cell_vectors]
|
|
70
|
+
|
|
71
|
+
def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Material:
|
|
72
|
+
new_material = super().create_perturbed_slab(configuration)
|
|
73
|
+
new_lattice_vectors = self._transform_lattice_vectors(configuration)
|
|
74
|
+
new_lattice = new_material.lattice.copy()
|
|
75
|
+
new_lattice = new_lattice.from_vectors_array(new_lattice_vectors)
|
|
76
|
+
new_material.lattice = new_lattice
|
|
77
|
+
|
|
78
|
+
new_basis = new_material.basis.copy()
|
|
79
|
+
new_basis.to_cartesian()
|
|
80
|
+
new_basis.cell = new_basis.cell.from_vectors_array(new_lattice_vectors)
|
|
81
|
+
new_basis.to_crystal()
|
|
82
|
+
new_material.basis = new_basis
|
|
83
|
+
return new_material
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
from mat3ra.code.entity import InMemoryEntity
|
|
4
|
+
from mat3ra.made.material import Material
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from ...utils.perturbation import SineWavePerturbationFunctionHolder, PerturbationFunctionHolder
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PerturbationConfiguration(BaseModel, InMemoryEntity):
|
|
11
|
+
material: Material
|
|
12
|
+
perturbation_function_holder: Union[
|
|
13
|
+
SineWavePerturbationFunctionHolder, PerturbationFunctionHolder
|
|
14
|
+
] = SineWavePerturbationFunctionHolder()
|
|
15
|
+
use_cartesian_coordinates: bool = True
|
|
16
|
+
|
|
17
|
+
class Config:
|
|
18
|
+
arbitrary_types_allowed = True
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def _json(self):
|
|
22
|
+
perturbation_function_json = self.perturbation_function_holder.get_json()
|
|
23
|
+
return {
|
|
24
|
+
"type": self.get_cls_name(),
|
|
25
|
+
"material": self.material.to_json(),
|
|
26
|
+
"perturbation_function": perturbation_function_json,
|
|
27
|
+
"use_cartesian_coordinates": self.use_cartesian_coordinates,
|
|
28
|
+
}
|
|
@@ -2,28 +2,32 @@ from typing import List, Optional
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
from mat3ra.made.material import Material
|
|
5
|
-
from ..third_party import
|
|
5
|
+
from ..third_party import ase_make_supercell
|
|
6
6
|
from ..utils import decorator_convert_2x2_to_3x3
|
|
7
|
-
from ..convert import from_ase,
|
|
7
|
+
from ..convert import from_ase, to_ase
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@decorator_convert_2x2_to_3x3
|
|
11
|
-
@decorator_convert_material_args_kwargs_to_atoms
|
|
12
11
|
def create_supercell(
|
|
13
|
-
|
|
12
|
+
material: Material, supercell_matrix: Optional[List[List[int]]] = None, scaling_factor: Optional[List[int]] = None
|
|
14
13
|
) -> Material:
|
|
15
14
|
"""
|
|
16
15
|
Create a supercell of the atoms.
|
|
17
16
|
|
|
18
17
|
Args:
|
|
19
|
-
|
|
18
|
+
material (Material): The atoms to create a supercell of.
|
|
20
19
|
supercell_matrix (List[List[int]]): The supercell matrix (e.g. [[3,0,0],[0,3,0],[0,0,1]]).
|
|
21
20
|
scaling_factor (List[int], optional): The scaling factor instead of matrix (e.g. [3,3,1]).
|
|
22
21
|
|
|
23
22
|
Returns:
|
|
24
23
|
Material: The supercell of the atoms.
|
|
25
24
|
"""
|
|
25
|
+
atoms = to_ase(material)
|
|
26
26
|
if scaling_factor is not None:
|
|
27
27
|
supercell_matrix = np.multiply(scaling_factor, np.eye(3)).tolist()
|
|
28
28
|
supercell_atoms = ase_make_supercell(atoms, supercell_matrix)
|
|
29
|
-
|
|
29
|
+
new_material = Material(from_ase(supercell_atoms))
|
|
30
|
+
if material.metadata:
|
|
31
|
+
new_material.metadata = material.metadata
|
|
32
|
+
new_material.name = material.name
|
|
33
|
+
return new_material
|
|
@@ -7,9 +7,9 @@ from .analyze import (
|
|
|
7
7
|
get_atom_indices_within_radius_pbc,
|
|
8
8
|
get_atomic_coordinates_extremum,
|
|
9
9
|
)
|
|
10
|
-
from .convert import
|
|
11
|
-
from .third_party import
|
|
12
|
-
from .utils import (
|
|
10
|
+
from .convert import from_ase, to_ase
|
|
11
|
+
from .third_party import ase_add_vacuum
|
|
12
|
+
from .utils.coordinate import (
|
|
13
13
|
is_coordinate_in_box,
|
|
14
14
|
is_coordinate_in_cylinder,
|
|
15
15
|
is_coordinate_in_triangular_prism,
|
|
@@ -87,18 +87,18 @@ def translate_by_vector(
|
|
|
87
87
|
return Material(from_ase(atoms))
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
def wrap_to_unit_cell(structure: PymatgenStructure):
|
|
90
|
+
def wrap_to_unit_cell(material: Material) -> Material:
|
|
92
91
|
"""
|
|
93
|
-
Wrap
|
|
92
|
+
Wrap the material to the unit cell.
|
|
94
93
|
|
|
95
94
|
Args:
|
|
96
|
-
|
|
95
|
+
material (Material): The material to wrap.
|
|
97
96
|
Returns:
|
|
98
|
-
|
|
97
|
+
Material: The wrapped material.
|
|
99
98
|
"""
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
atoms = to_ase(material)
|
|
100
|
+
atoms.wrap()
|
|
101
|
+
return Material(from_ase(atoms))
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
def filter_material_by_ids(material: Material, ids: List[int], invert: bool = False) -> Material:
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from typing import Callable, List, Optional
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from mat3ra.utils.matrix import convert_2x2_to_3x3
|
|
6
|
+
|
|
7
|
+
from ..third_party import PymatgenStructure
|
|
8
|
+
from .coordinate import (
|
|
9
|
+
is_coordinate_behind_plane,
|
|
10
|
+
is_coordinate_in_box,
|
|
11
|
+
is_coordinate_in_cylinder,
|
|
12
|
+
is_coordinate_in_sphere,
|
|
13
|
+
is_coordinate_in_triangular_prism,
|
|
14
|
+
)
|
|
15
|
+
from .factories import PerturbationFunctionHolderFactory
|
|
16
|
+
|
|
17
|
+
DEFAULT_SCALING_FACTOR = np.array([3, 3, 3])
|
|
18
|
+
DEFAULT_TRANSLATION_VECTOR = 1 / DEFAULT_SCALING_FACTOR
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# TODO: convert to accept ASE Atoms object
|
|
22
|
+
def translate_to_bottom_pymatgen_structure(structure: PymatgenStructure):
|
|
23
|
+
"""
|
|
24
|
+
Translate the structure to the bottom of the cell.
|
|
25
|
+
Args:
|
|
26
|
+
structure (PymatgenStructure): The pymatgen Structure object to translate.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
PymatgenStructure: The translated pymatgen Structure object.
|
|
30
|
+
"""
|
|
31
|
+
min_c = min(site.c for site in structure)
|
|
32
|
+
translation_vector = [0, 0, -min_c]
|
|
33
|
+
translated_structure = structure.copy()
|
|
34
|
+
for site in translated_structure:
|
|
35
|
+
site.coords += translation_vector
|
|
36
|
+
return translated_structure
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def decorator_convert_2x2_to_3x3(func: Callable) -> Callable:
|
|
40
|
+
"""
|
|
41
|
+
Decorator to convert a 2x2 matrix to a 3x3 matrix.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
@wraps(func)
|
|
45
|
+
def wrapper(*args, **kwargs):
|
|
46
|
+
new_args = [convert_2x2_to_3x3(arg) if isinstance(arg, list) and len(arg) == 2 else arg for arg in args]
|
|
47
|
+
return func(*new_args, **kwargs)
|
|
48
|
+
|
|
49
|
+
return wrapper
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_distance_between_coordinates(coordinate1: List[float], coordinate2: List[float]) -> float:
|
|
53
|
+
"""
|
|
54
|
+
Get the distance between two coordinates.
|
|
55
|
+
Args:
|
|
56
|
+
coordinate1 (List[float]): The first coordinate.
|
|
57
|
+
coordinate2 (List[float]): The second coordinate.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
float: The distance between the two coordinates.
|
|
61
|
+
"""
|
|
62
|
+
return float(np.linalg.norm(np.array(coordinate1) - np.array(coordinate2)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_norm(vector: List[float]) -> float:
|
|
66
|
+
"""
|
|
67
|
+
Get the norm of a vector.
|
|
68
|
+
Args:
|
|
69
|
+
vector (List[float]): The vector.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
float: The norm of the vector.
|
|
73
|
+
"""
|
|
74
|
+
return float(np.linalg.norm(vector))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def transform_coordinate_to_supercell(
|
|
78
|
+
coordinate: List[float],
|
|
79
|
+
scaling_factor: Optional[List[int]] = None,
|
|
80
|
+
translation_vector: Optional[List[float]] = None,
|
|
81
|
+
reverse: bool = False,
|
|
82
|
+
) -> List[float]:
|
|
83
|
+
"""
|
|
84
|
+
Convert a crystal coordinate of unit cell to a coordinate in a supercell.
|
|
85
|
+
Args:
|
|
86
|
+
coordinate (List[float]): The coordinates to convert.
|
|
87
|
+
scaling_factor (List[int]): The scaling factor for the supercell.
|
|
88
|
+
translation_vector (List[float]): The translation vector for the supercell.
|
|
89
|
+
reverse (bool): Whether to convert in the reverse transformation.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
List[float]: The converted coordinates.
|
|
93
|
+
"""
|
|
94
|
+
if scaling_factor is None:
|
|
95
|
+
np_scaling_factor = np.array([3, 3, 3])
|
|
96
|
+
else:
|
|
97
|
+
np_scaling_factor = np.array(scaling_factor)
|
|
98
|
+
|
|
99
|
+
if translation_vector is None:
|
|
100
|
+
np_translation_vector = np.array([0, 0, 0])
|
|
101
|
+
else:
|
|
102
|
+
np_translation_vector = np.array(translation_vector)
|
|
103
|
+
|
|
104
|
+
np_coordinate = np.array(coordinate)
|
|
105
|
+
converted_array = np_coordinate * (1 / np_scaling_factor) + np_translation_vector
|
|
106
|
+
if reverse:
|
|
107
|
+
converted_array = (np_coordinate - np_translation_vector) * np_scaling_factor
|
|
108
|
+
return converted_array.tolist()
|
|
@@ -1,72 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
from typing import
|
|
1
|
+
# Place all functions acting on coordinates
|
|
2
|
+
from typing import Dict, List
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
from .third_party import PymatgenStructure
|
|
8
|
-
|
|
9
|
-
DEFAULT_SCALING_FACTOR = np.array([3, 3, 3])
|
|
10
|
-
DEFAULT_TRANSLATION_VECTOR = 1 / DEFAULT_SCALING_FACTOR
|
|
11
|
-
|
|
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
|
-
def decorator_convert_2x2_to_3x3(func: Callable) -> Callable:
|
|
32
|
-
"""
|
|
33
|
-
Decorator to convert a 2x2 matrix to a 3x3 matrix.
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
@wraps(func)
|
|
37
|
-
def wrapper(*args, **kwargs):
|
|
38
|
-
new_args = [convert_2x2_to_3x3(arg) if isinstance(arg, list) and len(arg) == 2 else arg for arg in args]
|
|
39
|
-
return func(*new_args, **kwargs)
|
|
40
|
-
|
|
41
|
-
return wrapper
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def get_distance_between_coordinates(coordinate1: List[float], coordinate2: List[float]) -> float:
|
|
45
|
-
"""
|
|
46
|
-
Get the distance between two coordinates.
|
|
47
|
-
Args:
|
|
48
|
-
coordinate1 (List[float]): The first coordinate.
|
|
49
|
-
coordinate2 (List[float]): The second coordinate.
|
|
50
|
-
|
|
51
|
-
Returns:
|
|
52
|
-
float: The distance between the two coordinates.
|
|
53
|
-
"""
|
|
54
|
-
return float(np.linalg.norm(np.array(coordinate1) - np.array(coordinate2)))
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def get_norm(vector: List[float]) -> float:
|
|
58
|
-
"""
|
|
59
|
-
Get the norm of a vector.
|
|
60
|
-
Args:
|
|
61
|
-
vector (List[float]): The vector.
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
float: The norm of the vector.
|
|
65
|
-
"""
|
|
66
|
-
return float(np.linalg.norm(vector))
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# Condition functions:
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
70
6
|
|
|
71
7
|
|
|
72
8
|
def is_coordinate_in_cylinder(
|
|
@@ -219,106 +155,63 @@ def is_coordinate_behind_plane(
|
|
|
219
155
|
return np.dot(np_plane_normal, np_coordinate - np_plane_point) < 0
|
|
220
156
|
|
|
221
157
|
|
|
222
|
-
|
|
223
|
-
coordinate: List[float]
|
|
224
|
-
|
|
225
|
-
translation_vector: Optional[List[float]] = None,
|
|
226
|
-
reverse: bool = False,
|
|
227
|
-
) -> List[float]:
|
|
228
|
-
"""
|
|
229
|
-
Convert a crystal coordinate of unit cell to a coordinate in a supercell.
|
|
230
|
-
Args:
|
|
231
|
-
coordinate (List[float]): The coordinates to convert.
|
|
232
|
-
scaling_factor (List[int]): The scaling factor for the supercell.
|
|
233
|
-
translation_vector (List[float]): The translation vector for the supercell.
|
|
234
|
-
reverse (bool): Whether to convert in the reverse transformation.
|
|
158
|
+
class CoordinateCondition(BaseModel):
|
|
159
|
+
def condition(self, coordinate: List[float]) -> bool:
|
|
160
|
+
raise NotImplementedError
|
|
235
161
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
np_scaling_factor = np.array([3, 3, 3])
|
|
241
|
-
else:
|
|
242
|
-
np_scaling_factor = np.array(scaling_factor)
|
|
162
|
+
def get_json(self) -> Dict:
|
|
163
|
+
json = {"type": self.__class__.__name__}
|
|
164
|
+
json.update(self.dict())
|
|
165
|
+
return json
|
|
243
166
|
|
|
244
|
-
if translation_vector is None:
|
|
245
|
-
np_translation_vector = np.array([0, 0, 0])
|
|
246
|
-
else:
|
|
247
|
-
np_translation_vector = np.array(translation_vector)
|
|
248
167
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
class CoordinateConditionBuilder:
|
|
257
|
-
@staticmethod
|
|
258
|
-
def create_condition(condition_type: str, evaluation_func: Callable, **kwargs) -> Tuple[Callable, Dict]:
|
|
259
|
-
condition_json = {"type": condition_type, **kwargs}
|
|
260
|
-
return lambda coordinate: evaluation_func(coordinate, **kwargs), condition_json
|
|
261
|
-
|
|
262
|
-
@staticmethod
|
|
263
|
-
def cylinder(center_position=None, radius: float = 0.25, min_z: float = 0, max_z: float = 1):
|
|
264
|
-
if center_position is None:
|
|
265
|
-
center_position = [0.5, 0.5]
|
|
266
|
-
return CoordinateConditionBuilder.create_condition(
|
|
267
|
-
condition_type="cylinder",
|
|
268
|
-
evaluation_func=is_coordinate_in_cylinder,
|
|
269
|
-
center_position=center_position,
|
|
270
|
-
radius=radius,
|
|
271
|
-
min_z=min_z,
|
|
272
|
-
max_z=max_z,
|
|
273
|
-
)
|
|
168
|
+
class CylinderCoordinateCondition(CoordinateCondition):
|
|
169
|
+
center_position: List[float] = Field(default_factory=lambda: [0.5, 0.5])
|
|
170
|
+
radius: float = 0.25
|
|
171
|
+
min_z: float = 0
|
|
172
|
+
max_z: float = 1
|
|
274
173
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if center_position is None:
|
|
278
|
-
center_position = [0.5, 0.5, 0.5]
|
|
279
|
-
return CoordinateConditionBuilder.create_condition(
|
|
280
|
-
condition_type="sphere",
|
|
281
|
-
evaluation_func=is_coordinate_in_sphere,
|
|
282
|
-
center_position=center_position,
|
|
283
|
-
radius=radius,
|
|
284
|
-
)
|
|
174
|
+
def condition(self, coordinate: List[float]) -> bool:
|
|
175
|
+
return is_coordinate_in_cylinder(coordinate, self.center_position, self.radius, self.min_z, self.max_z)
|
|
285
176
|
|
|
286
|
-
@staticmethod
|
|
287
|
-
def triangular_prism(
|
|
288
|
-
position_on_surface_1: List[float] = [0, 0],
|
|
289
|
-
position_on_surface_2: List[float] = [1, 0],
|
|
290
|
-
position_on_surface_3: List[float] = [0, 1],
|
|
291
|
-
min_z: float = 0,
|
|
292
|
-
max_z: float = 1,
|
|
293
|
-
):
|
|
294
|
-
return CoordinateConditionBuilder.create_condition(
|
|
295
|
-
condition_type="prism",
|
|
296
|
-
evaluation_func=is_coordinate_in_triangular_prism,
|
|
297
|
-
coordinate_1=position_on_surface_1,
|
|
298
|
-
coordinate_2=position_on_surface_2,
|
|
299
|
-
coordinate_3=position_on_surface_3,
|
|
300
|
-
min_z=min_z,
|
|
301
|
-
max_z=max_z,
|
|
302
|
-
)
|
|
303
177
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
)
|
|
178
|
+
class SphereCoordinateCondition(CoordinateCondition):
|
|
179
|
+
center_position: List[float] = Field(default_factory=lambda: [0.5, 0.5])
|
|
180
|
+
radius: float = 0.25
|
|
181
|
+
|
|
182
|
+
def condition(self, coordinate: List[float]) -> bool:
|
|
183
|
+
return is_coordinate_in_sphere(coordinate, self.center_position, self.radius)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class BoxCoordinateCondition(CoordinateCondition):
|
|
187
|
+
min_coordinate: List[float] = Field(default_factory=lambda: [0, 0, 0])
|
|
188
|
+
max_coordinate: List[float] = Field(default_factory=lambda: [1, 1, 1])
|
|
316
189
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
190
|
+
def condition(self, coordinate: List[float]) -> bool:
|
|
191
|
+
return is_coordinate_in_box(coordinate, self.min_coordinate, self.max_coordinate)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class TriangularPrismCoordinateCondition(CoordinateCondition):
|
|
195
|
+
position_on_surface_1: List[float] = [0, 0]
|
|
196
|
+
position_on_surface_2: List[float] = [1, 0]
|
|
197
|
+
position_on_surface_3: List[float] = [0, 1]
|
|
198
|
+
min_z: float = 0
|
|
199
|
+
max_z: float = 1
|
|
200
|
+
|
|
201
|
+
def condition(self, coordinate: List[float]) -> bool:
|
|
202
|
+
return is_coordinate_in_triangular_prism(
|
|
203
|
+
coordinate,
|
|
204
|
+
self.position_on_surface_1,
|
|
205
|
+
self.position_on_surface_2,
|
|
206
|
+
self.position_on_surface_3,
|
|
207
|
+
self.min_z,
|
|
208
|
+
self.max_z,
|
|
324
209
|
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class PlaneCoordinateCondition(CoordinateCondition):
|
|
213
|
+
plane_normal: List[float]
|
|
214
|
+
plane_point_coordinate: List[float]
|
|
215
|
+
|
|
216
|
+
def condition(self, coordinate: List[float]) -> bool:
|
|
217
|
+
return is_coordinate_behind_plane(coordinate, self.plane_normal, self.plane_point_coordinate)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FunctionHolder(BaseModel):
|
|
7
|
+
def apply_function(self, coordinate: List[float]) -> float:
|
|
8
|
+
"""
|
|
9
|
+
Get the value of the function at the given coordinate.
|
|
10
|
+
"""
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
|
|
13
|
+
def calculate_derivative(self, coordinate: List[float], axis: str) -> float:
|
|
14
|
+
"""
|
|
15
|
+
Get the derivative of the function at the given coordinate
|
|
16
|
+
"""
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
def calculate_arc_length(self, a: float, b: float) -> float:
|
|
20
|
+
"""
|
|
21
|
+
Get the arc length of the function between a and b.
|
|
22
|
+
"""
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
|
|
25
|
+
def get_json(self) -> dict:
|
|
26
|
+
"""
|
|
27
|
+
Get the json representation of the function holder.
|
|
28
|
+
"""
|
|
29
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from typing import Any, Callable, List, Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import sympy as sp
|
|
5
|
+
from scipy.integrate import quad
|
|
6
|
+
from scipy.optimize import root_scalar
|
|
7
|
+
|
|
8
|
+
from .functions import FunctionHolder
|
|
9
|
+
|
|
10
|
+
AXIS_TO_INDEX_MAP = {"x": 0, "y": 1, "z": 2}
|
|
11
|
+
EQUATION_RANGE_COEFFICIENT = 5
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def default_function() -> sp.Expr:
|
|
15
|
+
return sp.Symbol("f")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PerturbationFunctionHolder(FunctionHolder):
|
|
19
|
+
variables: List[str] = ["x"]
|
|
20
|
+
symbols: List[sp.Symbol] = [sp.Symbol(var) for var in variables]
|
|
21
|
+
function: sp.Expr = sp.Symbol("f")
|
|
22
|
+
function_numeric: Callable = default_function
|
|
23
|
+
derivatives_numeric: dict = {}
|
|
24
|
+
|
|
25
|
+
class Config:
|
|
26
|
+
arbitrary_types_allowed = True
|
|
27
|
+
|
|
28
|
+
def __init__(self, function: Optional[sp.Expr] = None, variables: Optional[List[str]] = None, **data: Any):
|
|
29
|
+
"""
|
|
30
|
+
Initializes with a function involving multiple variables.
|
|
31
|
+
"""
|
|
32
|
+
if function is None:
|
|
33
|
+
function = default_function()
|
|
34
|
+
if variables is None:
|
|
35
|
+
variables = ["x", "y", "z"]
|
|
36
|
+
super().__init__(**data)
|
|
37
|
+
self.variables = variables
|
|
38
|
+
self.symbols = sp.symbols(variables)
|
|
39
|
+
self.function = function
|
|
40
|
+
self.function_numeric = sp.lambdify(self.symbols, self.function, modules=["numpy"])
|
|
41
|
+
self.derivatives_numeric = {
|
|
42
|
+
var: sp.lambdify(self.symbols, sp.diff(self.function, var), modules=["numpy"]) for var in variables
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def apply_function(self, coordinate: List[float]) -> float:
|
|
46
|
+
values = [coordinate[AXIS_TO_INDEX_MAP[var]] for var in self.variables]
|
|
47
|
+
return self.function_numeric(*values)
|
|
48
|
+
|
|
49
|
+
def calculate_derivative(self, coordinate: List[float], axis: str) -> float:
|
|
50
|
+
if axis in self.variables:
|
|
51
|
+
values = [coordinate[AXIS_TO_INDEX_MAP[var]] for var in self.variables]
|
|
52
|
+
return self.derivatives_numeric[axis](*values)
|
|
53
|
+
else:
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
def _integrand(self, t: float, coordinate: List[float], axis: str) -> float:
|
|
57
|
+
temp_coordinate = coordinate[:]
|
|
58
|
+
temp_coordinate[AXIS_TO_INDEX_MAP[axis]] = t
|
|
59
|
+
return np.sqrt(1 + self.calculate_derivative(temp_coordinate, axis) ** 2)
|
|
60
|
+
|
|
61
|
+
def get_arc_length_equation(self, w_prime: float, coordinate: List[float], axis: str) -> float:
|
|
62
|
+
"""
|
|
63
|
+
Calculate arc length considering a change along one specific axis.
|
|
64
|
+
"""
|
|
65
|
+
a, b = 0, w_prime
|
|
66
|
+
arc_length = quad(self._integrand, a, b, args=(coordinate, axis))[0]
|
|
67
|
+
return arc_length - coordinate[AXIS_TO_INDEX_MAP[axis]]
|
|
68
|
+
|
|
69
|
+
def transform_coordinates(self, coordinate: List[float]) -> List[float]:
|
|
70
|
+
"""
|
|
71
|
+
Transform coordinates to preserve the distance between points on a sine wave when perturbation is applied.
|
|
72
|
+
Achieved by calculating the integral of the length between [0,0,0] and given coordinate.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Callable[[List[float]], List[float]]: The coordinates transformation function.
|
|
76
|
+
"""
|
|
77
|
+
for i, var in enumerate(self.variables):
|
|
78
|
+
index = AXIS_TO_INDEX_MAP[var]
|
|
79
|
+
w = coordinate[index]
|
|
80
|
+
result = root_scalar(
|
|
81
|
+
self.get_arc_length_equation,
|
|
82
|
+
args=(coordinate, var),
|
|
83
|
+
bracket=[0, EQUATION_RANGE_COEFFICIENT * w],
|
|
84
|
+
method="brentq",
|
|
85
|
+
)
|
|
86
|
+
coordinate[index] = result.root
|
|
87
|
+
return coordinate
|
|
88
|
+
|
|
89
|
+
def apply_perturbation(self, coordinate: List[float]) -> List[float]:
|
|
90
|
+
"""
|
|
91
|
+
Apply the perturbation to the given coordinate by adding the function's value to the third coordinate (z-axis).
|
|
92
|
+
"""
|
|
93
|
+
perturbation_value = self.apply_function(coordinate)
|
|
94
|
+
perturbed_coordinate = coordinate[:]
|
|
95
|
+
perturbed_coordinate[2] += perturbation_value
|
|
96
|
+
return perturbed_coordinate
|
|
97
|
+
|
|
98
|
+
def get_json(self) -> dict:
|
|
99
|
+
return {
|
|
100
|
+
"type": self.__class__.__name__,
|
|
101
|
+
"function": str(self.function),
|
|
102
|
+
"variables": self.variables,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class SineWavePerturbationFunctionHolder(PerturbationFunctionHolder):
|
|
107
|
+
amplitude: float = 0.05
|
|
108
|
+
wavelength: float = 1
|
|
109
|
+
phase: float = 0
|
|
110
|
+
axis: str = "x"
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
amplitude: float = 0.05,
|
|
115
|
+
wavelength: float = 1,
|
|
116
|
+
phase: float = 0,
|
|
117
|
+
axis: str = "x",
|
|
118
|
+
**data: Any,
|
|
119
|
+
):
|
|
120
|
+
w = sp.Symbol(axis)
|
|
121
|
+
function = amplitude * sp.sin(2 * sp.pi * w / wavelength + phase)
|
|
122
|
+
variables = [axis]
|
|
123
|
+
super().__init__(function=function, variables=variables, **data)
|
|
124
|
+
self.amplitude = amplitude
|
|
125
|
+
self.wavelength = wavelength
|
|
126
|
+
self.phase = phase
|
|
127
|
+
self.axis = axis
|
|
128
|
+
|
|
129
|
+
def get_json(self) -> dict:
|
|
130
|
+
return {
|
|
131
|
+
"type": self.__class__.__name__,
|
|
132
|
+
"function": str(self.function),
|
|
133
|
+
"variables": self.variables,
|
|
134
|
+
"amplitude": self.amplitude,
|
|
135
|
+
"wavelength": self.wavelength,
|
|
136
|
+
"phase": self.phase,
|
|
137
|
+
"axis": self.axis,
|
|
138
|
+
}
|
|
@@ -114,7 +114,7 @@ SI_CONVENTIONAL_CELL: Dict[str, Any] = {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
SI_SUPERCELL_2X2X1: Dict[str, Any] = {
|
|
117
|
-
"name": "
|
|
117
|
+
"name": "Silicon FCC",
|
|
118
118
|
"basis": {
|
|
119
119
|
"elements": [
|
|
120
120
|
{"id": 0, "value": "Si"},
|
|
@@ -260,3 +260,32 @@ slab_001_config = SlabConfiguration(
|
|
|
260
260
|
)
|
|
261
261
|
t_001 = get_terminations(slab_001_config)[0]
|
|
262
262
|
SLAB_001 = create_slab(slab_001_config, t_001)
|
|
263
|
+
|
|
264
|
+
GRAPHENE = {
|
|
265
|
+
"name": "Graphene",
|
|
266
|
+
"basis": {
|
|
267
|
+
"elements": [{"id": 0, "value": "C"}, {"id": 1, "value": "C"}],
|
|
268
|
+
"coordinates": [{"id": 0, "value": [0, 0, 0]}, {"id": 1, "value": [0.333333, 0.666667, 0]}],
|
|
269
|
+
"units": "crystal",
|
|
270
|
+
"cell": [[2.467291, 0, 0], [-1.2336454999, 2.1367366845, 0], [0, 0, 20]],
|
|
271
|
+
"constraints": [],
|
|
272
|
+
},
|
|
273
|
+
"lattice": {
|
|
274
|
+
"a": 2.467291,
|
|
275
|
+
"b": 2.467291,
|
|
276
|
+
"c": 20,
|
|
277
|
+
"alpha": 90,
|
|
278
|
+
"beta": 90,
|
|
279
|
+
"gamma": 120,
|
|
280
|
+
"units": {"length": "angstrom", "angle": "degree"},
|
|
281
|
+
"type": "HEX",
|
|
282
|
+
"vectors": {
|
|
283
|
+
"a": [2.467291, 0, 0],
|
|
284
|
+
"b": [-1.233645, 2.136737, 0],
|
|
285
|
+
"c": [0, 0, 20],
|
|
286
|
+
"alat": 1,
|
|
287
|
+
"units": "angstrom",
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
"isNonPeriodic": False,
|
|
291
|
+
}
|
|
@@ -19,7 +19,7 @@ from mat3ra.made.tools.build.defect.configuration import (
|
|
|
19
19
|
PointDefectPairConfiguration,
|
|
20
20
|
TerraceSlabDefectConfiguration,
|
|
21
21
|
)
|
|
22
|
-
from mat3ra.made.tools.utils import
|
|
22
|
+
from mat3ra.made.tools.utils import coordinate as CoordinateCondition
|
|
23
23
|
from mat3ra.utils import assertion as assertion_utils
|
|
24
24
|
|
|
25
25
|
from .fixtures import SLAB_001, SLAB_111
|
|
@@ -114,7 +114,9 @@ def test_create_crystal_site_adatom():
|
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
def test_create_island():
|
|
117
|
-
condition =
|
|
117
|
+
condition = CoordinateCondition.CylinderCoordinateCondition(
|
|
118
|
+
center_position=[0.625, 0.5], radius=0.25, min_z=0, max_z=1
|
|
119
|
+
)
|
|
118
120
|
island_config = IslandSlabDefectConfiguration(
|
|
119
121
|
crystal=SLAB_111,
|
|
120
122
|
defect_type="island",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from mat3ra.made.cell import Cell
|
|
2
|
+
from mat3ra.made.material import Material
|
|
3
|
+
from mat3ra.made.tools.build.perturbation import create_perturbation
|
|
4
|
+
from mat3ra.made.tools.build.perturbation.builders import SlabPerturbationBuilder
|
|
5
|
+
from mat3ra.made.tools.build.perturbation.configuration import PerturbationConfiguration
|
|
6
|
+
from mat3ra.made.tools.build.supercell import create_supercell
|
|
7
|
+
from mat3ra.made.tools.utils.perturbation import SineWavePerturbationFunctionHolder
|
|
8
|
+
from mat3ra.utils import assertion as assertion_utils
|
|
9
|
+
|
|
10
|
+
from .fixtures import GRAPHENE
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_sine_perturbation():
|
|
14
|
+
material = Material(GRAPHENE)
|
|
15
|
+
slab = create_supercell(material, [[10, 0, 0], [0, 10, 0], [0, 0, 1]])
|
|
16
|
+
|
|
17
|
+
perturbation_config = PerturbationConfiguration(
|
|
18
|
+
material=slab,
|
|
19
|
+
perturbation_function=SineWavePerturbationFunctionHolder(amplitude=0.05, wavelength=1),
|
|
20
|
+
use_cartesian_coordinates=False,
|
|
21
|
+
)
|
|
22
|
+
builder = SlabPerturbationBuilder()
|
|
23
|
+
perturbed_slab = builder.get_material(perturbation_config)
|
|
24
|
+
# Check selected atoms to avoid using 100+ atoms fixture
|
|
25
|
+
assertion_utils.assert_deep_almost_equal([0.0, 0.0, 0.5], perturbed_slab.basis.coordinates.values[0])
|
|
26
|
+
assertion_utils.assert_deep_almost_equal([0.2, 0.1, 0.547552826], perturbed_slab.basis.coordinates.values[42])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_distance_preserved_sine_perturbation():
|
|
30
|
+
material = Material(GRAPHENE)
|
|
31
|
+
slab = create_supercell(material, [[10, 0, 0], [0, 10, 0], [0, 0, 1]])
|
|
32
|
+
|
|
33
|
+
perturbation_config = PerturbationConfiguration(
|
|
34
|
+
material=slab,
|
|
35
|
+
perturbation_function=SineWavePerturbationFunctionHolder(amplitude=0.05, wavelength=1, phase=0.25, axis="y"),
|
|
36
|
+
use_cartesian_coordinates=False,
|
|
37
|
+
)
|
|
38
|
+
perturbed_slab = create_perturbation(configuration=perturbation_config, preserve_distance=True)
|
|
39
|
+
# Check selected atoms to avoid using 100+ atoms fixture
|
|
40
|
+
assertion_utils.assert_deep_almost_equal([0.0, 0.0, 0.5], perturbed_slab.basis.coordinates.values[0])
|
|
41
|
+
assertion_utils.assert_deep_almost_equal(
|
|
42
|
+
[0.197552693, 0.1, 0.546942315], perturbed_slab.basis.coordinates.values[42]
|
|
43
|
+
)
|
|
44
|
+
# Value taken from visually inspected notebook
|
|
45
|
+
expected_cell = Cell(
|
|
46
|
+
vector1=[24.087442, 0.0, 0.0],
|
|
47
|
+
vector2=[-12.043583, 21.367367, 0.0],
|
|
48
|
+
vector3=[0.0, 0.0, 20.0],
|
|
49
|
+
)
|
|
50
|
+
assertion_utils.assert_deep_almost_equal(expected_cell.vectors_as_array, perturbed_slab.basis.cell.vectors_as_array)
|