@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mat3ra/made",
3
- "version": "2024.9.11-1",
3
+ "version": "2024.9.17-0",
4
4
  "description": "MAterials DEsign library",
5
5
  "scripts": {
6
6
  "lint": "eslint --cache src/js tests/js && prettier --write src/js tests/js",
@@ -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=None):
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.remove_item(id)
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 ..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
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())