@mat3ra/made 2024.9.11-1 → 2024.9.17-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 +15 -4
- package/src/py/mat3ra/made/tools/analyze.py +1 -1
- package/src/py/mat3ra/made/tools/build/interface/__init__.py +55 -1
- package/src/py/mat3ra/made/tools/{calculate.py → calculate/__init__.py} +6 -5
- package/src/py/mat3ra/made/tools/calculate/calculators.py +122 -0
- package/src/py/mat3ra/made/tools/calculate/interaction_functions.py +23 -0
- package/src/py/mat3ra/made/tools/convert/utils.py +7 -0
- package/src/py/mat3ra/made/tools/modify.py +50 -0
- package/src/py/mat3ra/made/tools/optimize.py +55 -0
- package/src/py/mat3ra/made/tools/utils/__init__.py +0 -9
- package/tests/py/unit/fixtures.py +141 -0
- package/tests/py/unit/test_tools_modify.py +40 -1
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from typing import Dict, List, Optional
|
|
2
|
+
from typing import Dict, List, Optional, Union
|
|
3
3
|
|
|
4
4
|
from mat3ra.code.constants import AtomicCoordinateUnits
|
|
5
5
|
from mat3ra.utils.mixins import RoundNumericValuesMixin
|
|
@@ -115,13 +115,24 @@ class Basis(RoundNumericValuesMixin, BaseModel):
|
|
|
115
115
|
self.elements.add_item(element)
|
|
116
116
|
self.coordinates.add_item(coordinate)
|
|
117
117
|
|
|
118
|
-
def remove_atom_by_id(self, id
|
|
118
|
+
def remove_atom_by_id(self, id: int):
|
|
119
119
|
self.elements.remove_item(id)
|
|
120
120
|
self.coordinates.remove_item(id)
|
|
121
|
-
self.labels
|
|
121
|
+
if self.labels is not None:
|
|
122
|
+
self.labels.remove_item(id)
|
|
122
123
|
|
|
123
|
-
def filter_atoms_by_ids(self, ids):
|
|
124
|
+
def filter_atoms_by_ids(self, ids: Union[List[int], int]) -> "Basis":
|
|
124
125
|
self.elements.filter_by_ids(ids)
|
|
125
126
|
self.coordinates.filter_by_ids(ids)
|
|
126
127
|
if self.labels is not None:
|
|
127
128
|
self.labels.filter_by_ids(ids)
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def filter_atoms_by_labels(self, labels: Union[List[str], str]) -> "Basis":
|
|
132
|
+
if self.labels is None:
|
|
133
|
+
return self
|
|
134
|
+
self.labels.filter_by_values(labels)
|
|
135
|
+
ids = self.labels.ids
|
|
136
|
+
self.elements.filter_by_ids(ids)
|
|
137
|
+
self.coordinates.filter_by_ids(ids)
|
|
138
|
+
return self
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from typing import Callable, List, Literal, Optional
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
|
+
from mat3ra.made.material import Material
|
|
4
5
|
from scipy.spatial import cKDTree
|
|
5
6
|
|
|
6
|
-
from ..material import Material
|
|
7
7
|
from .convert import decorator_convert_material_args_kwargs_to_atoms, to_pymatgen
|
|
8
8
|
from .enums import SurfaceTypes
|
|
9
9
|
from .third_party import ASEAtoms, PymatgenIStructure, PymatgenVoronoiNN
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
from typing import Union, List, Optional
|
|
1
|
+
from typing import Union, List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
2
4
|
|
|
3
5
|
from mat3ra.made.material import Material
|
|
6
|
+
from ...calculate.calculators import InterfaceMaterialCalculator
|
|
7
|
+
from ...modify import displace_interface_part
|
|
8
|
+
from ...optimize import evaluate_calculator_on_xy_grid
|
|
4
9
|
from .builders import (
|
|
5
10
|
SimpleInterfaceBuilder,
|
|
6
11
|
SimpleInterfaceBuilderParameters,
|
|
@@ -24,3 +29,52 @@ def create_interface(
|
|
|
24
29
|
if builder is None:
|
|
25
30
|
builder = SimpleInterfaceBuilder(build_parameters=SimpleInterfaceBuilderParameters())
|
|
26
31
|
return builder.get_material(configuration)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_optimal_film_displacement(
|
|
35
|
+
material: Material,
|
|
36
|
+
grid_size_xy: Tuple[int, int] = (10, 10),
|
|
37
|
+
grid_offset_position: List[float] = [0, 0],
|
|
38
|
+
grid_range_x=(-0.5, 0.5),
|
|
39
|
+
grid_range_y=(-0.5, 0.5),
|
|
40
|
+
use_cartesian_coordinates=False,
|
|
41
|
+
calculator: InterfaceMaterialCalculator = InterfaceMaterialCalculator(),
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Calculate the optimal displacement in of the film to minimize the interaction energy
|
|
45
|
+
between the film and the substrate. The displacement is calculated on a grid.
|
|
46
|
+
|
|
47
|
+
This function evaluates the interaction energy between the film and substrate
|
|
48
|
+
over a specified grid of (x,y) displacements. It returns the displacement vector that
|
|
49
|
+
results in the minimum interaction energy.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
material (Material): The interface Material object.
|
|
53
|
+
grid_size_xy (Tuple[int, int]): The size of the grid to search for the optimal displacement.
|
|
54
|
+
grid_offset_position (List[float]): The offset position of the grid.
|
|
55
|
+
grid_range_x (Tuple[float, float]): The range of the grid in x.
|
|
56
|
+
grid_range_y (Tuple[float, float]): The range of the grid in y.
|
|
57
|
+
use_cartesian_coordinates (bool): Whether to use Cartesian coordinates.
|
|
58
|
+
calculator (InterfaceMaterialCalculator): The calculator to use for the calculation of the interaction energy.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
List[float]: The optimal displacement vector.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
xy_matrix, results_matrix = evaluate_calculator_on_xy_grid(
|
|
65
|
+
material=material,
|
|
66
|
+
calculator_function=calculator.get_energy,
|
|
67
|
+
modifier=displace_interface_part,
|
|
68
|
+
modifier_parameters={},
|
|
69
|
+
grid_size_xy=grid_size_xy,
|
|
70
|
+
grid_offset_position=grid_offset_position,
|
|
71
|
+
grid_range_x=grid_range_x,
|
|
72
|
+
grid_range_y=grid_range_y,
|
|
73
|
+
use_cartesian_coordinates=use_cartesian_coordinates,
|
|
74
|
+
)
|
|
75
|
+
min_index = np.unravel_index(np.argmin(results_matrix), results_matrix.shape)
|
|
76
|
+
|
|
77
|
+
optimal_x = xy_matrix[0][min_index[0]]
|
|
78
|
+
optimal_y = xy_matrix[1][min_index[1]]
|
|
79
|
+
|
|
80
|
+
return [optimal_x, optimal_y, 0]
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from
|
|
3
|
+
from ...material import Material
|
|
4
|
+
from ..analyze import get_surface_area
|
|
5
|
+
from ..build.interface.utils import get_slab
|
|
6
|
+
from ..convert import decorator_convert_material_args_kwargs_to_atoms
|
|
7
|
+
from ..third_party import ASEAtoms, ASECalculator, ASECalculatorEMT
|
|
8
|
+
from .interaction_functions import sum_of_inverse_distances_squared
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@decorator_convert_material_args_kwargs_to_atoms
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from mat3ra.made.material import Material
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from ..analyze import get_surface_atom_indices
|
|
8
|
+
from ..convert.utils import InterfacePartsEnum
|
|
9
|
+
from ..enums import SurfaceTypes
|
|
10
|
+
from ..modify import get_interface_part
|
|
11
|
+
from .interaction_functions import sum_of_inverse_distances_squared
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MaterialCalculatorParameters(BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
Defines the parameters for a material calculator.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
interaction_function (Callable): A function used to calculate the interaction metric between
|
|
20
|
+
sets of coordinates. The default function is sum_of_inverse_distances_squared.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
interaction_function: Callable = sum_of_inverse_distances_squared
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InterfaceMaterialCalculatorParameters(MaterialCalculatorParameters):
|
|
27
|
+
"""
|
|
28
|
+
Parameters specific to the calculation of interaction energies between
|
|
29
|
+
an interface material's film and substrate.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
shadowing_radius (float): Radius used to determine the surface atoms of the film or substrate
|
|
33
|
+
for interaction calculations. Default is 2.5 Å.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
shadowing_radius: float = 2.5
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MaterialCalculator(BaseModel):
|
|
40
|
+
"""
|
|
41
|
+
A base class for performing calculations on materials.
|
|
42
|
+
|
|
43
|
+
This class uses the parameters defined in MaterialCalculatorParameters to calculate
|
|
44
|
+
interaction metrics between atoms or sets of coordinates within the material.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
calculator_parameters (MaterialCalculatorParameters): Parameters controlling the calculator,
|
|
48
|
+
including the interaction function.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
calculator_parameters: MaterialCalculatorParameters = MaterialCalculatorParameters()
|
|
52
|
+
|
|
53
|
+
def get_energy(self, material: Material):
|
|
54
|
+
"""
|
|
55
|
+
Calculate the energy (or other metric) for a material.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
material (Material): The material to calculate the interaction energy for.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
float: The interaction energy between the coordinates of the material,
|
|
62
|
+
calculated using the specified interaction function.
|
|
63
|
+
"""
|
|
64
|
+
return self.calculator_parameters.interaction_function(material.coordinates, material.coordinates)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class InterfaceMaterialCalculator(MaterialCalculator):
|
|
68
|
+
"""
|
|
69
|
+
A specialized calculator for computing the interaction energy between a film and a substrate
|
|
70
|
+
in an interface material.
|
|
71
|
+
|
|
72
|
+
This class extends MaterialCalculator and uses additional parameters specific to interface materials,
|
|
73
|
+
such as the shadowing radius to detect surface atoms.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
calculator_parameters (InterfaceMaterialCalculatorParameters): Parameters that include the
|
|
77
|
+
shadowing radius and interaction function.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
calculator_parameters: InterfaceMaterialCalculatorParameters = InterfaceMaterialCalculatorParameters()
|
|
81
|
+
|
|
82
|
+
def get_energy(
|
|
83
|
+
self,
|
|
84
|
+
material: Material,
|
|
85
|
+
shadowing_radius: float = 2.5,
|
|
86
|
+
interaction_function: Callable = sum_of_inverse_distances_squared,
|
|
87
|
+
) -> float:
|
|
88
|
+
"""
|
|
89
|
+
Calculate the interaction energy between the film and substrate in an interface material.
|
|
90
|
+
|
|
91
|
+
This method uses the shadowing radius to detect surface atoms and applies the given
|
|
92
|
+
interaction function to calculate the interaction between the film and substrate.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
material (Material): The interface Material object consisting of both the film and substrate.
|
|
96
|
+
shadowing_radius (float): The radius used to detect surface atoms for the interaction
|
|
97
|
+
calculation. Defaults to 2.5 Å.
|
|
98
|
+
interaction_function (Callable): A function to compute the interaction between the film and
|
|
99
|
+
substrate. Defaults to sum_of_inverse_distances_squared.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
float: The calculated interaction energy between the film and substrate.
|
|
103
|
+
"""
|
|
104
|
+
film_material = get_interface_part(material, part=InterfacePartsEnum.FILM)
|
|
105
|
+
substrate_material = get_interface_part(material, part=InterfacePartsEnum.SUBSTRATE)
|
|
106
|
+
|
|
107
|
+
film_surface_atom_indices = get_surface_atom_indices(
|
|
108
|
+
film_material, SurfaceTypes.BOTTOM, shadowing_radius=shadowing_radius
|
|
109
|
+
)
|
|
110
|
+
substrate_surface_atom_indices = get_surface_atom_indices(
|
|
111
|
+
substrate_material, SurfaceTypes.TOP, shadowing_radius=shadowing_radius
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
film_surface_atom_coordinates = film_material.basis.coordinates
|
|
115
|
+
film_surface_atom_coordinates.filter_by_ids(film_surface_atom_indices)
|
|
116
|
+
substrate_surface_atom_coordinates = substrate_material.basis.coordinates
|
|
117
|
+
substrate_surface_atom_coordinates.filter_by_ids(substrate_surface_atom_indices)
|
|
118
|
+
|
|
119
|
+
film_coordinates_values = np.array(film_surface_atom_coordinates.values)
|
|
120
|
+
substrate_coordinates_values = np.array(substrate_surface_atom_coordinates.values)
|
|
121
|
+
|
|
122
|
+
return interaction_function(film_coordinates_values, substrate_coordinates_values)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def sum_of_inverse_distances_squared(
|
|
5
|
+
coordinates_1: np.ndarray, coordinates_2: np.ndarray, epsilon: float = 1e-12
|
|
6
|
+
) -> float:
|
|
7
|
+
"""
|
|
8
|
+
Calculate the sum of inverse squares of distances between two sets of coordinates.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
coordinates_1 (np.ndarray): The first set of coordinates, shape (N1, 3).
|
|
12
|
+
coordinates_2 (np.ndarray): The second set of coordinates, shape (N2, 3).
|
|
13
|
+
epsilon (float): Small value to prevent division by zero.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
float: The calculated sum.
|
|
17
|
+
"""
|
|
18
|
+
differences = coordinates_1[:, np.newaxis, :] - coordinates_2[np.newaxis, :, :] # Shape: (N1, N2, 3)
|
|
19
|
+
distances_squared = np.sum(differences**2, axis=2) # Shape: (N1, N2)
|
|
20
|
+
distances_squared = np.where(distances_squared == 0, epsilon, distances_squared)
|
|
21
|
+
inv_distances_squared = -1 / distances_squared
|
|
22
|
+
total = np.sum(inv_distances_squared)
|
|
23
|
+
return float(total)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from enum import Enum
|
|
2
3
|
from typing import Any, Dict, List, Union
|
|
3
4
|
|
|
4
5
|
from mat3ra.made.utils import map_array_to_array_with_id_value
|
|
@@ -6,6 +7,12 @@ from mat3ra.utils.object import NumpyNDArrayRoundEncoder
|
|
|
6
7
|
|
|
7
8
|
from ..third_party import ASEAtoms, PymatgenInterface, PymatgenStructure
|
|
8
9
|
|
|
10
|
+
|
|
11
|
+
class InterfacePartsEnum(str, Enum):
|
|
12
|
+
SUBSTRATE = 0
|
|
13
|
+
FILM = 1
|
|
14
|
+
|
|
15
|
+
|
|
9
16
|
INTERFACE_LABELS_MAP = {"substrate": 0, "film": 1}
|
|
10
17
|
|
|
11
18
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Callable, List, Literal, Optional, Union
|
|
2
2
|
|
|
3
|
+
import numpy as np
|
|
3
4
|
from mat3ra.made.material import Material
|
|
4
5
|
|
|
5
6
|
from .analyze import (
|
|
@@ -8,6 +9,7 @@ from .analyze import (
|
|
|
8
9
|
get_atomic_coordinates_extremum,
|
|
9
10
|
)
|
|
10
11
|
from .convert import from_ase, to_ase
|
|
12
|
+
from .convert.utils import InterfacePartsEnum
|
|
11
13
|
from .third_party import ase_add_vacuum
|
|
12
14
|
from .utils.coordinate import (
|
|
13
15
|
is_coordinate_in_box,
|
|
@@ -462,3 +464,51 @@ def rotate_material(material: Material, axis: List[int], angle: float) -> Materi
|
|
|
462
464
|
atoms.wrap()
|
|
463
465
|
|
|
464
466
|
return Material(from_ase(atoms))
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def displace_interface_part(
|
|
470
|
+
interface: Material,
|
|
471
|
+
displacement: List[float],
|
|
472
|
+
label: InterfacePartsEnum = InterfacePartsEnum.FILM,
|
|
473
|
+
use_cartesian_coordinates=True,
|
|
474
|
+
) -> Material:
|
|
475
|
+
"""
|
|
476
|
+
Displace atoms in an interface along a certain direction.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
interface (Material): The interface Material object.
|
|
480
|
+
displacement (List[float]): The displacement vector in angstroms or crystal coordinates.
|
|
481
|
+
label (InterfacePartsEnum): The label of the atoms to displace ("substrate" or "film").
|
|
482
|
+
use_cartesian_coordinates (bool): Whether to use cartesian coordinates.
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Material: The displaced material object.
|
|
486
|
+
"""
|
|
487
|
+
new_material = interface.clone()
|
|
488
|
+
if use_cartesian_coordinates:
|
|
489
|
+
new_material.to_cartesian()
|
|
490
|
+
labels_array = new_material.basis.labels.to_array_of_values_with_ids()
|
|
491
|
+
displaced_label_ids = [_label.id for _label in labels_array if _label.value == int(label)]
|
|
492
|
+
|
|
493
|
+
new_coordinates_values = new_material.basis.coordinates.values
|
|
494
|
+
for atom_id in displaced_label_ids:
|
|
495
|
+
current_coordinate = new_material.basis.coordinates.get_element_value_by_index(atom_id)
|
|
496
|
+
new_atom_coordinate = np.array(current_coordinate) + np.array(displacement)
|
|
497
|
+
new_coordinates_values[atom_id] = new_atom_coordinate
|
|
498
|
+
|
|
499
|
+
new_material.set_coordinates(new_coordinates_values)
|
|
500
|
+
new_material.to_crystal()
|
|
501
|
+
new_material = wrap_to_unit_cell(new_material)
|
|
502
|
+
return new_material
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def get_interface_part(
|
|
506
|
+
interface: Material,
|
|
507
|
+
part: InterfacePartsEnum = InterfacePartsEnum.FILM,
|
|
508
|
+
) -> Material:
|
|
509
|
+
if interface.metadata["build"]["configuration"]["type"] != "InterfaceConfiguration":
|
|
510
|
+
raise ValueError("The material is not an interface.")
|
|
511
|
+
interface_part_material = interface.clone()
|
|
512
|
+
film_atoms_basis = interface_part_material.basis.filter_atoms_by_labels([int(part)])
|
|
513
|
+
interface_part_material.basis = film_atoms_basis
|
|
514
|
+
return interface_part_material
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from mat3ra.made.material import Material
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def evaluate_calculator_on_xy_grid(
|
|
8
|
+
material: Material,
|
|
9
|
+
calculator_function: Callable[[Material], Any],
|
|
10
|
+
modifier: Optional[Callable] = None,
|
|
11
|
+
modifier_parameters: Dict[str, Any] = {},
|
|
12
|
+
grid_size_xy: Tuple[int, int] = (10, 10),
|
|
13
|
+
grid_offset_position: List[float] = [0, 0],
|
|
14
|
+
grid_range_x: Tuple[float, float] = (-0.5, 0.5),
|
|
15
|
+
grid_range_y: Tuple[float, float] = (-0.5, 0.5),
|
|
16
|
+
use_cartesian_coordinates: bool = False,
|
|
17
|
+
) -> Tuple[List[np.ndarray], np.ndarray]:
|
|
18
|
+
"""
|
|
19
|
+
Calculate a property on a grid of x-y positions.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
material (Material): The material object.
|
|
23
|
+
modifier (Callable): The modifier function to apply to the material.
|
|
24
|
+
modifier_parameters (Dict[str, Any]): The parameters to pass to the modifier.
|
|
25
|
+
calculator_function (Callable): The calculator function to apply to the modified material.
|
|
26
|
+
grid_size_xy (Tuple[int, int]): The size of the grid in x and y directions.
|
|
27
|
+
grid_offset_position (List[float]): The offset position of the grid, in Angstroms or crystal coordinates.
|
|
28
|
+
grid_range_x (Tuple[float, float]): The range to search in x direction, in Angstroms or crystal coordinates.
|
|
29
|
+
grid_range_y (Tuple[float, float]): The range to search in y direction, in Angstroms or crystal coordinates.
|
|
30
|
+
use_cartesian_coordinates (bool): Whether to use Cartesian coordinates.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Tuple[List[np.ndarray[float]], np.ndarray[float]]: The x-y positions and the calculated property values.
|
|
34
|
+
"""
|
|
35
|
+
x_values = np.linspace(grid_range_x[0], grid_range_x[1], grid_size_xy[0]) + grid_offset_position[0]
|
|
36
|
+
y_values = np.linspace(grid_range_y[0], grid_range_y[1], grid_size_xy[1]) + grid_offset_position[1]
|
|
37
|
+
|
|
38
|
+
xy_matrix = [x_values, y_values]
|
|
39
|
+
results_matrix = np.zeros(grid_size_xy)
|
|
40
|
+
|
|
41
|
+
for i, x in enumerate(x_values):
|
|
42
|
+
for j, y in enumerate(y_values):
|
|
43
|
+
if modifier is None:
|
|
44
|
+
modified_material = material
|
|
45
|
+
else:
|
|
46
|
+
modified_material = modifier(
|
|
47
|
+
material,
|
|
48
|
+
displacement=[x, y, 0],
|
|
49
|
+
use_cartesian_coordinates=use_cartesian_coordinates,
|
|
50
|
+
**modifier_parameters,
|
|
51
|
+
)
|
|
52
|
+
result = calculator_function(modified_material)
|
|
53
|
+
results_matrix[i, j] = result
|
|
54
|
+
|
|
55
|
+
return xy_matrix, results_matrix
|
|
@@ -3,18 +3,9 @@ from typing import Callable, List, Optional
|
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
from mat3ra.made.material import Material
|
|
6
|
-
from mat3ra.made.utils import ArrayWithIds
|
|
7
6
|
from mat3ra.utils.matrix import convert_2x2_to_3x3
|
|
8
7
|
|
|
9
8
|
from ..third_party import PymatgenStructure
|
|
10
|
-
from .coordinate import (
|
|
11
|
-
is_coordinate_behind_plane,
|
|
12
|
-
is_coordinate_in_box,
|
|
13
|
-
is_coordinate_in_cylinder,
|
|
14
|
-
is_coordinate_in_sphere,
|
|
15
|
-
is_coordinate_in_triangular_prism,
|
|
16
|
-
)
|
|
17
|
-
from .factories import PerturbationFunctionHolderFactory
|
|
18
9
|
|
|
19
10
|
DEFAULT_SCALING_FACTOR = np.array([3, 3, 3])
|
|
20
11
|
DEFAULT_TRANSLATION_VECTOR = 1 / DEFAULT_SCALING_FACTOR
|
|
@@ -507,3 +507,144 @@ GRAPHENE_ARMCHAIR_NANORIBBON = {
|
|
|
507
507
|
},
|
|
508
508
|
"isUpdated": True,
|
|
509
509
|
}
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
GRAPHENE_NICKEL_INTERFACE = {
|
|
513
|
+
"name": "C2(001)-Ni4(111), Interface, Strain 0.105pct",
|
|
514
|
+
"basis": {
|
|
515
|
+
"elements": [
|
|
516
|
+
{"id": 0, "value": "Ni"},
|
|
517
|
+
{"id": 1, "value": "Ni"},
|
|
518
|
+
{"id": 2, "value": "Ni"},
|
|
519
|
+
{"id": 3, "value": "C"},
|
|
520
|
+
{"id": 4, "value": "C"},
|
|
521
|
+
],
|
|
522
|
+
"coordinates": [
|
|
523
|
+
{"id": 0, "value": [0.666666667, 0.666666667, 0.350869517]},
|
|
524
|
+
{"id": 1, "value": [1.0, 0.0, 0.425701769]},
|
|
525
|
+
{"id": 2, "value": [0.333333333, 0.333333333, 0.500534021]},
|
|
526
|
+
{"id": 3, "value": [0.333333333, 0.333333333, 0.611447347]},
|
|
527
|
+
{"id": 4, "value": [0.666666667, 0.666666667, 0.611447347]},
|
|
528
|
+
],
|
|
529
|
+
"units": "crystal",
|
|
530
|
+
"cell": [[2.478974, 0.0, 0.0], [1.239487, 2.14685446, 0.0], [0.0, 0.0, 27.048147591]],
|
|
531
|
+
"constraints": [],
|
|
532
|
+
"labels": [
|
|
533
|
+
{"id": 0, "value": 0},
|
|
534
|
+
{"id": 1, "value": 0},
|
|
535
|
+
{"id": 2, "value": 0},
|
|
536
|
+
{"id": 3, "value": 1},
|
|
537
|
+
{"id": 4, "value": 1},
|
|
538
|
+
],
|
|
539
|
+
},
|
|
540
|
+
"lattice": {
|
|
541
|
+
"a": 2.478974,
|
|
542
|
+
"b": 2.478974,
|
|
543
|
+
"c": 27.048147591,
|
|
544
|
+
"alpha": 90.0,
|
|
545
|
+
"beta": 90.0,
|
|
546
|
+
"gamma": 60.0,
|
|
547
|
+
"units": {"length": "angstrom", "angle": "degree"},
|
|
548
|
+
"type": "TRI",
|
|
549
|
+
"vectors": {
|
|
550
|
+
"a": [2.478974, 0.0, 0.0],
|
|
551
|
+
"b": [1.239487, 2.14685446, 0.0],
|
|
552
|
+
"c": [0.0, 0.0, 27.048147591],
|
|
553
|
+
"alat": 1,
|
|
554
|
+
"units": "angstrom",
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
"isNonPeriodic": False,
|
|
558
|
+
"_id": "",
|
|
559
|
+
"metadata": {
|
|
560
|
+
"interface_properties": {
|
|
561
|
+
"film_sl_vectors": [[2.467291, 0.0, 0.0], [1.2336455, -2.136736685, -0.0]],
|
|
562
|
+
"substrate_sl_vectors": [[-1.752899326, 1.752899326, 0.0], [-1.752899326, 0.0, 1.752899326]],
|
|
563
|
+
"film_vectors": [[2.467291, 0.0, 0.0], [-1.2336455, 2.136736685, 0.0]],
|
|
564
|
+
"substrate_vectors": [[-1.752899326, 1.752899326, 0.0], [-1.752899326, 0.0, 1.752899326]],
|
|
565
|
+
"film_transformation": [[1.0, 0.0], [0.0, 1.0]],
|
|
566
|
+
"substrate_transformation": [[1.0, 0.0], [0.0, 1.0]],
|
|
567
|
+
"strain": [[0.004746364, -0.0, -0.0], [-0.0, 0.004746364, -0.0], [-0.0, -0.0, 0.0]],
|
|
568
|
+
"von_mises_strain": 0.003164242537164297,
|
|
569
|
+
"termination": "('C_P6/mmm_2', 'Ni_R-3m_1')",
|
|
570
|
+
"film_thickness": 1,
|
|
571
|
+
"substrate_thickness": 3,
|
|
572
|
+
"mean_abs_strain": 0.0010500000000000002,
|
|
573
|
+
},
|
|
574
|
+
"boundaryConditions": {"type": "pbc", "offset": 0},
|
|
575
|
+
"mean_abs_strain": 0.0010500000000000002,
|
|
576
|
+
"build": {
|
|
577
|
+
"configuration": {
|
|
578
|
+
"type": "InterfaceConfiguration",
|
|
579
|
+
"film_configuration": {
|
|
580
|
+
"type": "SlabConfiguration",
|
|
581
|
+
"bulk": {**GRAPHENE, "name": "C2"},
|
|
582
|
+
"miller_indices": [0, 0, 1],
|
|
583
|
+
"thickness": 1,
|
|
584
|
+
"vacuum": 0,
|
|
585
|
+
"xy_supercell_matrix": [[1, 0], [0, 1]],
|
|
586
|
+
"use_conventional_cell": True,
|
|
587
|
+
"use_orthogonal_z": True,
|
|
588
|
+
"make_primitive": False,
|
|
589
|
+
},
|
|
590
|
+
"substrate_configuration": {
|
|
591
|
+
"type": "SlabConfiguration",
|
|
592
|
+
"bulk": {
|
|
593
|
+
"name": "Ni4",
|
|
594
|
+
"basis": {
|
|
595
|
+
"elements": [
|
|
596
|
+
{"id": 0, "value": "Ni"},
|
|
597
|
+
{"id": 1, "value": "Ni"},
|
|
598
|
+
{"id": 2, "value": "Ni"},
|
|
599
|
+
{"id": 3, "value": "Ni"},
|
|
600
|
+
],
|
|
601
|
+
"coordinates": [
|
|
602
|
+
{"id": 0, "value": [0.0, 0.0, 0.0]},
|
|
603
|
+
{"id": 1, "value": [0.0, 0.5, 0.5]},
|
|
604
|
+
{"id": 2, "value": [0.5, 0.0, 0.5]},
|
|
605
|
+
{"id": 3, "value": [0.5, 0.5, 0.0]},
|
|
606
|
+
],
|
|
607
|
+
"units": "crystal",
|
|
608
|
+
"cell": [[3.505798652, 0.0, 0.0], [-0.0, 3.505798652, 0.0], [0.0, 0.0, 3.505798652]],
|
|
609
|
+
"constraints": [],
|
|
610
|
+
"labels": [],
|
|
611
|
+
},
|
|
612
|
+
"lattice": {
|
|
613
|
+
"a": 3.505798652,
|
|
614
|
+
"b": 3.505798652,
|
|
615
|
+
"c": 3.505798652,
|
|
616
|
+
"alpha": 90.0,
|
|
617
|
+
"beta": 90.0,
|
|
618
|
+
"gamma": 90.0,
|
|
619
|
+
"units": {"length": "angstrom", "angle": "degree"},
|
|
620
|
+
"type": "TRI",
|
|
621
|
+
"vectors": {
|
|
622
|
+
"a": [3.505798652, 0.0, 0.0],
|
|
623
|
+
"b": [-0.0, 3.505798652, 0.0],
|
|
624
|
+
"c": [0.0, 0.0, 3.505798652],
|
|
625
|
+
"alat": 1,
|
|
626
|
+
"units": "angstrom",
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
"isNonPeriodic": False,
|
|
630
|
+
"_id": "",
|
|
631
|
+
"metadata": {"boundaryConditions": {"type": "pbc", "offset": 0}},
|
|
632
|
+
"isUpdated": True,
|
|
633
|
+
},
|
|
634
|
+
"miller_indices": [1, 1, 1],
|
|
635
|
+
"thickness": 3,
|
|
636
|
+
"vacuum": 3,
|
|
637
|
+
"xy_supercell_matrix": [[1, 0], [0, 1]],
|
|
638
|
+
"use_conventional_cell": True,
|
|
639
|
+
"use_orthogonal_z": True,
|
|
640
|
+
"make_primitive": False,
|
|
641
|
+
},
|
|
642
|
+
"film_termination": "C_P6/mmm_2",
|
|
643
|
+
"substrate_termination": "Ni_P6/mmm_4",
|
|
644
|
+
"distance_z": 3.0,
|
|
645
|
+
"vacuum": 20.0,
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
"isUpdated": True,
|
|
650
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from ase.build import bulk
|
|
2
2
|
from mat3ra.made.material import Material
|
|
3
|
+
from mat3ra.made.tools.build.interface import get_optimal_film_displacement
|
|
3
4
|
from mat3ra.made.tools.convert import from_ase
|
|
5
|
+
from mat3ra.made.tools.convert.utils import InterfacePartsEnum
|
|
4
6
|
from mat3ra.made.tools.modify import (
|
|
5
7
|
add_vacuum,
|
|
8
|
+
displace_interface_part,
|
|
6
9
|
filter_by_circle_projection,
|
|
7
10
|
filter_by_label,
|
|
8
11
|
filter_by_layers,
|
|
@@ -15,7 +18,7 @@ from mat3ra.made.tools.modify import (
|
|
|
15
18
|
)
|
|
16
19
|
from mat3ra.utils import assertion as assertion_utils
|
|
17
20
|
|
|
18
|
-
from .fixtures import SI_CONVENTIONAL_CELL, SI_SLAB, SI_SLAB_VACUUM
|
|
21
|
+
from .fixtures import GRAPHENE_NICKEL_INTERFACE, SI_CONVENTIONAL_CELL, SI_SLAB, SI_SLAB_VACUUM
|
|
19
22
|
|
|
20
23
|
COMMON_PART = {
|
|
21
24
|
"units": "crystal",
|
|
@@ -170,3 +173,39 @@ def test_rotate_material():
|
|
|
170
173
|
material.basis.coordinates.values.sort(), rotated_material.basis.coordinates.values.sort()
|
|
171
174
|
)
|
|
172
175
|
assertion_utils.assert_deep_almost_equal(material.lattice, rotated_material.lattice)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_displace_interface():
|
|
179
|
+
material = Material(GRAPHENE_NICKEL_INTERFACE)
|
|
180
|
+
expected_coordinates = [
|
|
181
|
+
{"id": 0, "value": [0.666666667, 0.666666667, 0.350869517]},
|
|
182
|
+
{"id": 1, "value": [-0.0, 0.0, 0.425701769]},
|
|
183
|
+
{"id": 2, "value": [0.333333333, 0.333333333, 0.500534021]},
|
|
184
|
+
{"id": 3, "value": [0.433333333, 0.533333333, 0.911447347]},
|
|
185
|
+
{"id": 4, "value": [0.766666667, 0.866666667, 0.911447347]},
|
|
186
|
+
]
|
|
187
|
+
expected_labels = GRAPHENE_NICKEL_INTERFACE["basis"]["labels"]
|
|
188
|
+
displaced_material = displace_interface_part(
|
|
189
|
+
material, [0.1, 0.2, 0.3], InterfacePartsEnum.FILM, use_cartesian_coordinates=False
|
|
190
|
+
)
|
|
191
|
+
assertion_utils.assert_deep_almost_equal(expected_coordinates, displaced_material.basis.coordinates.to_dict())
|
|
192
|
+
assertion_utils.assert_deep_almost_equal(expected_labels, displaced_material.basis.labels.to_dict())
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_displace_interface_optimized():
|
|
196
|
+
material = Material(GRAPHENE_NICKEL_INTERFACE)
|
|
197
|
+
expected_coordinates = [
|
|
198
|
+
{"id": 0, "value": [0.666666667, 0.666666667, 0.350869517]},
|
|
199
|
+
{"id": 1, "value": [-0.0, 0.0, 0.425701769]},
|
|
200
|
+
{"id": 2, "value": [0.333333333, 0.333333333, 0.500534021]},
|
|
201
|
+
{"id": 3, "value": [0.285973954, 0.203945038, 0.611447347]},
|
|
202
|
+
{"id": 4, "value": [0.619307288, 0.537278372, 0.611447347]},
|
|
203
|
+
]
|
|
204
|
+
expected_labels = GRAPHENE_NICKEL_INTERFACE["basis"]["labels"]
|
|
205
|
+
|
|
206
|
+
optimal_displacement = get_optimal_film_displacement(
|
|
207
|
+
material, grid_size_xy=(10, 10), grid_range_x=(-0.5, 0.5), grid_range_y=(-0.5, 0.5)
|
|
208
|
+
)
|
|
209
|
+
displaced_material = displace_interface_part(material, optimal_displacement, use_cartesian_coordinates=True)
|
|
210
|
+
assertion_utils.assert_deep_almost_equal(expected_coordinates, displaced_material.basis.coordinates.to_dict())
|
|
211
|
+
assertion_utils.assert_deep_almost_equal(expected_labels, displaced_material.basis.labels.to_dict())
|