@mat3ra/made 2024.7.2-0 → 2024.7.8-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 +65 -3
- package/src/py/mat3ra/made/tools/build/__init__.py +2 -0
- package/src/py/mat3ra/made/tools/build/defect/__init__.py +30 -6
- package/src/py/mat3ra/made/tools/build/defect/builders.py +129 -3
- package/src/py/mat3ra/made/tools/build/defect/configuration.py +24 -3
- package/src/py/mat3ra/made/tools/build/defect/enums.py +4 -0
- package/src/py/mat3ra/made/tools/convert/__init__.py +3 -0
- package/src/py/mat3ra/made/tools/modify.py +110 -18
- package/src/py/mat3ra/made/tools/third_party.py +4 -0
- package/src/py/mat3ra/made/utils.py +14 -0
- package/tests/py/unit/fixtures.py +19 -6
- package/tests/py/unit/test_tools_build_defect.py +44 -1
- package/tests/py/unit/test_tools_modify.py +23 -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,10 +1,10 @@
|
|
|
1
|
-
from typing import Callable, List, Optional
|
|
1
|
+
from typing import Callable, List, Literal, Optional
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
5
|
from ..material import Material
|
|
6
6
|
from .convert import decorator_convert_material_args_kwargs_to_atoms, to_pymatgen
|
|
7
|
-
from .third_party import ASEAtoms, PymatgenIStructure
|
|
7
|
+
from .third_party import ASEAtoms, PymatgenIStructure, PymatgenVoronoiNN
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@decorator_convert_material_args_kwargs_to_atoms
|
|
@@ -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,65 @@ 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_nearest_neighbors_atom_indices(
|
|
235
|
+
material: Material,
|
|
236
|
+
position: Optional[List[float]] = None,
|
|
237
|
+
) -> Optional[List[int]]:
|
|
238
|
+
"""
|
|
239
|
+
Returns the indices of direct neighboring atoms to a specified position in the material using Voronoi tessellation.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
material (Material): The material object to find neighbors in.
|
|
243
|
+
position (List[float]): The position to find neighbors for.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
List[int]: A list of indices of neighboring atoms, or an empty list if no neighbors are found.
|
|
247
|
+
"""
|
|
248
|
+
if position is None:
|
|
249
|
+
position = [0, 0, 0]
|
|
250
|
+
structure = to_pymatgen(material)
|
|
251
|
+
voronoi_nn = PymatgenVoronoiNN(
|
|
252
|
+
tol=0.5,
|
|
253
|
+
cutoff=13.0,
|
|
254
|
+
allow_pathological=False,
|
|
255
|
+
weight="solid_angle",
|
|
256
|
+
extra_nn_info=True,
|
|
257
|
+
compute_adj_neighbors=True,
|
|
258
|
+
)
|
|
259
|
+
structure.append("X", position, validate_proximity=False)
|
|
260
|
+
neighbors = voronoi_nn.get_nn_info(structure, len(structure.sites) - 1)
|
|
261
|
+
neighboring_atoms_pymatgen_ids = [n["site_index"] for n in neighbors]
|
|
262
|
+
structure.remove_sites([-1])
|
|
263
|
+
|
|
264
|
+
all_coordinates = material.basis.coordinates
|
|
265
|
+
all_coordinates.filter_by_indices(neighboring_atoms_pymatgen_ids)
|
|
266
|
+
return all_coordinates.ids
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def get_atomic_coordinates_extremum(
|
|
270
|
+
material: Material,
|
|
271
|
+
extremum: Literal["max", "min"] = "max",
|
|
272
|
+
axis: Literal["x", "y", "z"] = "z",
|
|
273
|
+
use_cartesian_coordinates: bool = False,
|
|
274
|
+
) -> float:
|
|
275
|
+
"""
|
|
276
|
+
Return minimum or maximum of coordinates along the specified axis.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
material (Material): Material object.
|
|
280
|
+
extremum (str): "min" or "max".
|
|
281
|
+
axis (str): "x", "y", or "z".
|
|
282
|
+
use_cartesian_coordinates (bool): Whether to use Cartesian coordinates.
|
|
283
|
+
Returns:
|
|
284
|
+
float: Minimum or maximum of coordinates along the specified axis.
|
|
285
|
+
"""
|
|
286
|
+
new_material = material.clone()
|
|
287
|
+
if use_cartesian_coordinates:
|
|
288
|
+
new_basis = new_material.basis
|
|
289
|
+
new_basis.to_cartesian()
|
|
290
|
+
new_material.basis = new_basis
|
|
291
|
+
coordinates = new_material.basis.coordinates.to_array_of_values_with_ids()
|
|
292
|
+
values = [coord.value[{"x": 0, "y": 1, "z": 2}[axis]] for coord in coordinates]
|
|
293
|
+
return getattr(np, extremum)(values)
|
|
@@ -65,6 +65,8 @@ class BaseBuilder(BaseModel):
|
|
|
65
65
|
def _post_process(
|
|
66
66
|
self, items: List[_GeneratedItemType], post_process_parameters: Optional[_PostProcessParametersType]
|
|
67
67
|
) -> List[Material]:
|
|
68
|
+
if self._GeneratedItemType == Material:
|
|
69
|
+
return items
|
|
68
70
|
return [Material(self._convert_generated_item(item)) for item in items]
|
|
69
71
|
|
|
70
72
|
@staticmethod
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
from typing import Optional
|
|
1
|
+
from typing import Optional, Union
|
|
2
2
|
|
|
3
3
|
from mat3ra.utils.factory import BaseFactory
|
|
4
4
|
from mat3ra.made.material import Material
|
|
5
5
|
|
|
6
|
-
from .builders import
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
from .builders import (
|
|
7
|
+
PointDefectBuilderParameters,
|
|
8
|
+
SlabDefectBuilderParameters,
|
|
9
|
+
AdatomSlabDefectBuilder,
|
|
10
|
+
EquidistantAdatomSlabDefectBuilder,
|
|
11
|
+
)
|
|
12
|
+
from .configuration import PointDefectConfiguration, AdatomSlabDefectConfiguration
|
|
13
|
+
from .enums import PointDefectTypeEnum, SlabDefectTypeEnum
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
class DefectBuilderFactory(BaseFactory):
|
|
@@ -17,8 +22,8 @@ class DefectBuilderFactory(BaseFactory):
|
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
def create_defect(
|
|
20
|
-
configuration: PointDefectConfiguration,
|
|
21
|
-
builder_parameters:
|
|
25
|
+
configuration: Union[PointDefectConfiguration, AdatomSlabDefectConfiguration],
|
|
26
|
+
builder_parameters: Union[PointDefectBuilderParameters, SlabDefectBuilderParameters, None] = None,
|
|
22
27
|
) -> Material:
|
|
23
28
|
"""
|
|
24
29
|
Return a material with a selected defect added.
|
|
@@ -34,3 +39,22 @@ def create_defect(
|
|
|
34
39
|
builder = BuilderClass(builder_parameters)
|
|
35
40
|
|
|
36
41
|
return builder.get_material(configuration) if builder else configuration.crystal
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_slab_defect(
|
|
45
|
+
configuration: Union[AdatomSlabDefectConfiguration],
|
|
46
|
+
builder: Optional[Union[AdatomSlabDefectBuilder, EquidistantAdatomSlabDefectBuilder]] = None,
|
|
47
|
+
) -> Material:
|
|
48
|
+
"""
|
|
49
|
+
Return a material with a selected slab defect added.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
configuration: The configuration of the defect to be added.
|
|
53
|
+
builder: The builder to be used to create the defect.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The material with the defect added.
|
|
57
|
+
"""
|
|
58
|
+
if builder is None:
|
|
59
|
+
builder = AdatomSlabDefectBuilder(build_parameters=SlabDefectBuilderParameters())
|
|
60
|
+
return builder.get_material(configuration)
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
from typing import List, Callable
|
|
1
|
+
from typing import List, Callable, Optional
|
|
2
2
|
|
|
3
|
-
from mat3ra.made.
|
|
3
|
+
from mat3ra.made.tools.build.supercell import create_supercell
|
|
4
|
+
from mat3ra.made.tools.modify import add_vacuum
|
|
4
5
|
from pydantic import BaseModel
|
|
6
|
+
from mat3ra.made.material import Material
|
|
7
|
+
|
|
5
8
|
|
|
6
9
|
from ...third_party import (
|
|
7
10
|
PymatgenStructure,
|
|
@@ -12,8 +15,10 @@ from ...third_party import (
|
|
|
12
15
|
)
|
|
13
16
|
from ...build import BaseBuilder
|
|
14
17
|
from ...convert import to_pymatgen
|
|
18
|
+
from ...analyze import get_nearest_neighbors_atom_indices, get_atomic_coordinates_extremum
|
|
19
|
+
from ....utils import get_center_of_coordinates
|
|
15
20
|
from ..mixins import ConvertGeneratedItemsPymatgenStructureMixin
|
|
16
|
-
from .configuration import PointDefectConfiguration
|
|
21
|
+
from .configuration import PointDefectConfiguration, AdatomSlabDefectConfiguration
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
class PointDefectBuilderParameters(BaseModel):
|
|
@@ -67,3 +72,124 @@ class SubstitutionPointDefectBuilder(PointDefectBuilder):
|
|
|
67
72
|
|
|
68
73
|
class InterstitialPointDefectBuilder(PointDefectBuilder):
|
|
69
74
|
_generator: PymatgenInterstitial = PymatgenInterstitial
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SlabDefectBuilderParameters(BaseModel):
|
|
78
|
+
auto_add_vacuum: bool = True
|
|
79
|
+
vacuum_thickness: float = 5.0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SlabDefectBuilder(BaseBuilder):
|
|
83
|
+
_BuildParametersType = SlabDefectBuilderParameters
|
|
84
|
+
_DefaultBuildParameters = SlabDefectBuilderParameters()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AdatomSlabDefectBuilder(SlabDefectBuilder):
|
|
88
|
+
_ConfigurationType: type(AdatomSlabDefectConfiguration) = AdatomSlabDefectConfiguration # type: ignore
|
|
89
|
+
_GeneratedItemType: Material = Material
|
|
90
|
+
|
|
91
|
+
def create_adatom(
|
|
92
|
+
self,
|
|
93
|
+
material: Material,
|
|
94
|
+
chemical_element: str = "Si",
|
|
95
|
+
position_on_surface: Optional[List[float]] = None,
|
|
96
|
+
distance_z: float = 2.0,
|
|
97
|
+
) -> List[Material]:
|
|
98
|
+
"""
|
|
99
|
+
Create an adatom at the specified position on the surface of the material.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
material: The material to add the adatom to.
|
|
103
|
+
chemical_element: The chemical element of the adatom.
|
|
104
|
+
position_on_surface: The position on the surface of the material.
|
|
105
|
+
distance_z: The distance of the adatom from the surface.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The material with the adatom added.
|
|
109
|
+
"""
|
|
110
|
+
if position_on_surface is None:
|
|
111
|
+
position_on_surface = [0.5, 0.5]
|
|
112
|
+
new_material = material.clone()
|
|
113
|
+
new_basis = new_material.basis
|
|
114
|
+
adatom_position = self._calculate_position_from_2d(material, position_on_surface, distance_z)
|
|
115
|
+
new_basis.add_atom(chemical_element, adatom_position)
|
|
116
|
+
new_material.basis = new_basis
|
|
117
|
+
return [new_material]
|
|
118
|
+
|
|
119
|
+
def _calculate_position_from_2d(
|
|
120
|
+
self, material: Material, position_on_surface: List[float], distance_z: float
|
|
121
|
+
) -> List[float]:
|
|
122
|
+
max_z = get_atomic_coordinates_extremum(material)
|
|
123
|
+
distance_z = distance_z
|
|
124
|
+
distance_in_crystal_units = distance_z / material.lattice.c
|
|
125
|
+
position = position_on_surface.copy()
|
|
126
|
+
position = position[:2]
|
|
127
|
+
position.append(max_z + distance_in_crystal_units)
|
|
128
|
+
return position
|
|
129
|
+
|
|
130
|
+
def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]:
|
|
131
|
+
return self.create_adatom(
|
|
132
|
+
material=configuration.crystal,
|
|
133
|
+
chemical_element=configuration.chemical_element,
|
|
134
|
+
position_on_surface=configuration.position_on_surface,
|
|
135
|
+
distance_z=configuration.distance_z,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class EquidistantAdatomSlabDefectBuilder(AdatomSlabDefectBuilder):
|
|
140
|
+
def create_adatom(
|
|
141
|
+
self,
|
|
142
|
+
material: Material,
|
|
143
|
+
chemical_element: str = "Si",
|
|
144
|
+
position_on_surface: Optional[List[float]] = None,
|
|
145
|
+
distance_z: float = 2.0,
|
|
146
|
+
) -> List[Material]:
|
|
147
|
+
"""
|
|
148
|
+
Create an adatom with an equidistant XY position among the nearest neighbors
|
|
149
|
+
at the given distance from the surface.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
material: The material to add the adatom to.
|
|
153
|
+
chemical_element: The chemical element of the adatom.
|
|
154
|
+
position_on_surface: The position on the surface of the material.
|
|
155
|
+
distance_z: The distance of the adatom from the surface.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
The material with the adatom added.
|
|
159
|
+
"""
|
|
160
|
+
if position_on_surface is None:
|
|
161
|
+
position_on_surface = [0.5, 0.5]
|
|
162
|
+
equidistant_position = self.get_equidistant_position(material, position_on_surface, distance_z)
|
|
163
|
+
new_material = material.clone()
|
|
164
|
+
if equidistant_position[2] > 1:
|
|
165
|
+
if self.build_parameters.auto_add_vacuum:
|
|
166
|
+
new_material = add_vacuum(material, self.build_parameters.vacuum_thickness)
|
|
167
|
+
equidistant_position = self.get_equidistant_position(new_material, position_on_surface, distance_z)
|
|
168
|
+
else:
|
|
169
|
+
raise ValueError("Not enough vacuum space to place the adatom.")
|
|
170
|
+
|
|
171
|
+
return super().create_adatom(new_material, chemical_element, equidistant_position, distance_z)
|
|
172
|
+
|
|
173
|
+
def get_equidistant_position(
|
|
174
|
+
self, material: Material, position_on_surface: List[float], distance_z: float = 2.0
|
|
175
|
+
) -> List[float]:
|
|
176
|
+
new_basis = material.basis
|
|
177
|
+
adatom_position = self._calculate_position_from_2d(material, position_on_surface, distance_z)
|
|
178
|
+
neighboring_atoms_ids = get_nearest_neighbors_atom_indices(material, adatom_position)
|
|
179
|
+
# We need to check if neighboring atoms number is the same in pbc
|
|
180
|
+
supercell_material = create_supercell(material, [[3, 0, 0], [0, 3, 0], [0, 0, 1]])
|
|
181
|
+
# Move the coordinate to the central unit cell of the supercell (crystal coordinates)
|
|
182
|
+
supercell_adatom_position = [1 / 3 + adatom_position[0] / 3, 1 / 3 + adatom_position[1] / 3, adatom_position[2]]
|
|
183
|
+
supercell_neighboring_atoms_ids = get_nearest_neighbors_atom_indices(
|
|
184
|
+
supercell_material, supercell_adatom_position
|
|
185
|
+
)
|
|
186
|
+
if neighboring_atoms_ids is None or supercell_neighboring_atoms_ids is None:
|
|
187
|
+
raise ValueError("No neighboring atoms found. Try reducing the distance_z.")
|
|
188
|
+
if len(supercell_neighboring_atoms_ids) != len(neighboring_atoms_ids):
|
|
189
|
+
raise ValueError("Number of neighboring atoms is not the same in PBC. Try increasing the supercell size.")
|
|
190
|
+
neighboring_atoms_coordinates = [new_basis.coordinates.values[atom_id] for atom_id in neighboring_atoms_ids]
|
|
191
|
+
|
|
192
|
+
equidistant_position = get_center_of_coordinates(neighboring_atoms_coordinates)
|
|
193
|
+
equidistant_position[2] = adatom_position[2]
|
|
194
|
+
|
|
195
|
+
return equidistant_position
|
|
@@ -5,7 +5,7 @@ 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_position
|
|
8
|
-
from .enums import PointDefectTypeEnum
|
|
8
|
+
from .enums import PointDefectTypeEnum, SlabDefectTypeEnum
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class BaseDefectConfiguration(BaseModel):
|
|
@@ -23,7 +23,7 @@ class PointDefectConfiguration(BaseDefectConfiguration, InMemoryEntity):
|
|
|
23
23
|
cls, crystal: Material, defect_type: PointDefectTypeEnum, site_id: int, chemical_element: Optional[str] = None
|
|
24
24
|
):
|
|
25
25
|
if not crystal:
|
|
26
|
-
RuntimeError("Crystal is not defined")
|
|
26
|
+
raise RuntimeError("Crystal is not defined")
|
|
27
27
|
position = crystal.coordinates_array[site_id]
|
|
28
28
|
return cls(crystal=crystal, defect_type=defect_type, position=position, chemical_element=chemical_element)
|
|
29
29
|
|
|
@@ -36,7 +36,7 @@ class PointDefectConfiguration(BaseDefectConfiguration, InMemoryEntity):
|
|
|
36
36
|
chemical_element: Optional[str] = None,
|
|
37
37
|
):
|
|
38
38
|
if not crystal:
|
|
39
|
-
RuntimeError("Crystal is not defined")
|
|
39
|
+
raise RuntimeError("Crystal is not defined")
|
|
40
40
|
closest_site_id = get_closest_site_id_from_position(crystal, approximate_position)
|
|
41
41
|
return cls.from_site_id(
|
|
42
42
|
crystal=crystal, defect_type=defect_type, site_id=closest_site_id, chemical_element=chemical_element
|
|
@@ -50,3 +50,24 @@ class PointDefectConfiguration(BaseDefectConfiguration, InMemoryEntity):
|
|
|
50
50
|
"position": self.position,
|
|
51
51
|
"chemical_element": self.chemical_element,
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SlabDefectConfiguration(BaseDefectConfiguration, InMemoryEntity):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AdatomSlabDefectConfiguration(SlabDefectConfiguration):
|
|
60
|
+
defect_type: SlabDefectTypeEnum = SlabDefectTypeEnum.ADATOM
|
|
61
|
+
position_on_surface: List[float] = [0.5, 0.5]
|
|
62
|
+
distance_z: float = 2.0
|
|
63
|
+
chemical_element: str = "Si"
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def _json(self):
|
|
67
|
+
return {
|
|
68
|
+
"type": "AdatomSlabDefectConfiguration",
|
|
69
|
+
"defect_type": self.defect_type.name,
|
|
70
|
+
"position_on_surface": self.position_on_surface,
|
|
71
|
+
"distance_z": self.distance_z,
|
|
72
|
+
"chemical_element": self.chemical_element,
|
|
73
|
+
}
|
|
@@ -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,17 +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,
|
|
12
15
|
is_coordinate_in_triangular_prism,
|
|
13
16
|
is_coordinate_within_layer,
|
|
14
|
-
translate_to_bottom_pymatgen_structure,
|
|
15
17
|
)
|
|
16
18
|
|
|
17
19
|
|
|
@@ -35,22 +37,54 @@ def filter_by_label(material: Material, label: Union[int, str]) -> Material:
|
|
|
35
37
|
return new_material
|
|
36
38
|
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
def translate_to_z_level(
|
|
41
|
+
material: Material, z_level: Optional[Literal["top", "bottom", "center"]] = "bottom"
|
|
42
|
+
) -> Material:
|
|
40
43
|
"""
|
|
41
|
-
Translate atoms to the
|
|
42
|
-
If use_conventional_cell is passed, conventional cell is used.
|
|
44
|
+
Translate atoms to the specified z-level.
|
|
43
45
|
|
|
44
46
|
Args:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
material (Material): The material object to normalize.
|
|
48
|
+
z_level (str): The z-level to translate the atoms to (top, bottom, center)
|
|
47
49
|
Returns:
|
|
48
|
-
|
|
50
|
+
Material: The translated material object.
|
|
49
51
|
"""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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))
|
|
54
88
|
|
|
55
89
|
|
|
56
90
|
@decorator_convert_material_args_kwargs_to_structure
|
|
@@ -140,7 +174,7 @@ def filter_by_layers(
|
|
|
140
174
|
if central_atom_id is not None:
|
|
141
175
|
center_coordinate = material.basis.coordinates.get_element_value_by_index(central_atom_id)
|
|
142
176
|
vectors = material.lattice.vectors
|
|
143
|
-
direction_vector =
|
|
177
|
+
direction_vector = vectors[2]
|
|
144
178
|
|
|
145
179
|
def condition(coordinate):
|
|
146
180
|
return is_coordinate_within_layer(coordinate, center_coordinate, direction_vector, layer_thickness)
|
|
@@ -323,3 +357,61 @@ def filter_by_triangle_projection(
|
|
|
323
357
|
return filter_by_condition_on_coordinates(
|
|
324
358
|
material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection
|
|
325
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,10 +1,12 @@
|
|
|
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
|
|
5
6
|
from pymatgen.analysis.defects.core import Interstitial as PymatgenInterstitial
|
|
6
7
|
from pymatgen.analysis.defects.core import Substitution as PymatgenSubstitution
|
|
7
8
|
from pymatgen.analysis.defects.core import Vacancy as PymatgenVacancy
|
|
9
|
+
from pymatgen.analysis.local_env import VoronoiNN as PymatgenVoronoiNN
|
|
8
10
|
from pymatgen.core import IStructure as PymatgenIStructure
|
|
9
11
|
from pymatgen.core import PeriodicSite as PymatgenPeriodicSite
|
|
10
12
|
from pymatgen.core.interface import Interface as PymatgenInterface
|
|
@@ -36,6 +38,8 @@ __all__ = [
|
|
|
36
38
|
"PymatgenInterstitial",
|
|
37
39
|
"label_pymatgen_slab_termination",
|
|
38
40
|
"ase_make_supercell",
|
|
41
|
+
"ase_add_vacuum",
|
|
39
42
|
"PymatgenAseAtomsAdaptor",
|
|
40
43
|
"PymatgenPoscar",
|
|
44
|
+
"PymatgenVoronoiNN",
|
|
41
45
|
]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
3
3
|
|
|
4
|
+
import numpy as np
|
|
4
5
|
from mat3ra.utils.array import convert_to_array_if_not
|
|
5
6
|
from mat3ra.utils.mixins import RoundNumericValuesMixin
|
|
6
7
|
from pydantic import BaseModel
|
|
@@ -44,6 +45,19 @@ def are_arrays_equal_by_id_value(array1: List[Dict[str, Any]], array2: List[Dict
|
|
|
44
45
|
return map_array_with_id_value_to_array(array1) == map_array_with_id_value_to_array(array2)
|
|
45
46
|
|
|
46
47
|
|
|
48
|
+
def get_center_of_coordinates(coordinates: List[List[float]]) -> List[float]:
|
|
49
|
+
"""
|
|
50
|
+
Calculate the center of the coordinates.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
coordinates (List[List[float]]): The list of coordinates.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List[float]: The center of the coordinates.
|
|
57
|
+
"""
|
|
58
|
+
return list(np.mean(np.array(coordinates), axis=0))
|
|
59
|
+
|
|
60
|
+
|
|
47
61
|
class ValueWithId(RoundNumericValuesMixin, BaseModel):
|
|
48
62
|
id: int = 0
|
|
49
63
|
value: Any = None
|
|
@@ -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]
|
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
from mat3ra.made.material import Material
|
|
2
|
-
from mat3ra.made.tools.build.defect import
|
|
2
|
+
from mat3ra.made.tools.build.defect import (
|
|
3
|
+
AdatomSlabDefectConfiguration,
|
|
4
|
+
EquidistantAdatomSlabDefectBuilder,
|
|
5
|
+
PointDefectBuilderParameters,
|
|
6
|
+
PointDefectConfiguration,
|
|
7
|
+
create_defect,
|
|
8
|
+
create_slab_defect,
|
|
9
|
+
)
|
|
10
|
+
from mat3ra.made.tools.build.slab import SlabConfiguration, create_slab, get_terminations
|
|
11
|
+
from mat3ra.utils import assertion as assertion_utils
|
|
3
12
|
|
|
4
13
|
clean_material = Material.create(Material.default_config)
|
|
5
14
|
|
|
15
|
+
slab_config = SlabConfiguration(
|
|
16
|
+
clean_material,
|
|
17
|
+
(1, 1, 1),
|
|
18
|
+
thickness=3,
|
|
19
|
+
vacuum=6,
|
|
20
|
+
use_orthogonal_z=True,
|
|
21
|
+
xy_supercell_matrix=[[2, 0, 0], [0, 2, 0], [0, 0, 1]],
|
|
22
|
+
)
|
|
23
|
+
t = get_terminations(slab_config)[0]
|
|
24
|
+
slab = create_slab(slab_config, t)
|
|
25
|
+
|
|
6
26
|
|
|
7
27
|
def test_create_vacancy():
|
|
8
28
|
# vacancy in place of 0 element
|
|
@@ -49,3 +69,26 @@ def test_create_defect_from_site_id():
|
|
|
49
69
|
{"id": 0, "value": "Si"},
|
|
50
70
|
{"id": 1, "value": "Ge"},
|
|
51
71
|
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_create_adatom():
|
|
75
|
+
# Adatom of Si at 0.5, 0.5 position
|
|
76
|
+
configuration = AdatomSlabDefectConfiguration(
|
|
77
|
+
crystal=slab, position_on_surface=[0.5, 0.5], distance_z=2, chemical_element="Si"
|
|
78
|
+
)
|
|
79
|
+
defect = create_slab_defect(configuration=configuration, builder=None)
|
|
80
|
+
|
|
81
|
+
assert defect.basis.elements.values[-1] == "Si"
|
|
82
|
+
assertion_utils.assert_deep_almost_equal([0.5, 0.5, 0.389826], defect.basis.coordinates.values[-1])
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_create_adatom_equidistant():
|
|
86
|
+
# Adatom of Si at approximate 0.5, 0.5 position
|
|
87
|
+
configuration = AdatomSlabDefectConfiguration(
|
|
88
|
+
crystal=slab, position_on_surface=[0.5, 0.5], distance_z=2, chemical_element="Si"
|
|
89
|
+
)
|
|
90
|
+
defect = create_slab_defect(configuration=configuration, builder=EquidistantAdatomSlabDefectBuilder())
|
|
91
|
+
|
|
92
|
+
assert defect.basis.elements.values[-1] == "Si"
|
|
93
|
+
# We expect adatom to shift from provided position
|
|
94
|
+
assertion_utils.assert_deep_almost_equal([0.4583333333, 0.541666667, 0.389826], defect.basis.coordinates.values[-1])
|
|
@@ -2,16 +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,
|
|
10
11
|
filter_by_triangle_projection,
|
|
12
|
+
remove_vacuum,
|
|
13
|
+
translate_to_z_level,
|
|
11
14
|
)
|
|
12
15
|
from mat3ra.utils import assertion as assertion_utils
|
|
13
16
|
|
|
14
|
-
from .fixtures import SI_CONVENTIONAL_CELL
|
|
17
|
+
from .fixtures import SI_CONVENTIONAL_CELL, SI_SLAB, SI_SLAB_VACUUM
|
|
15
18
|
|
|
16
19
|
COMMON_PART = {
|
|
17
20
|
"units": "crystal",
|
|
@@ -136,3 +139,22 @@ def test_filter_by_triangle_projection():
|
|
|
136
139
|
cavity = filter_by_triangle_projection(material, [0.4, 0.4], [0.4, 0.5], [0.5, 0.5], invert_selection=True)
|
|
137
140
|
assertion_utils.assert_deep_almost_equal(expected_basis_sphere_cluster, section.basis.to_json())
|
|
138
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())
|