@mat3ra/made 2024.6.25-0 → 2024.7.1-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.6.25-0",
3
+ "version": "2024.7.1-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,24 +1,21 @@
1
- from typing import List, Optional
1
+ from typing import Callable, List, Optional
2
2
 
3
3
  import numpy as np
4
- from ase import Atoms
5
- from pymatgen.core import IStructure as PymatgenIStructure
6
4
 
7
5
  from ..material import Material
8
6
  from .convert import decorator_convert_material_args_kwargs_to_atoms, to_pymatgen
9
-
10
- PymatgenIStructure = PymatgenIStructure
7
+ from .third_party import ASEAtoms, PymatgenIStructure
11
8
 
12
9
 
13
10
  @decorator_convert_material_args_kwargs_to_atoms
14
11
  def get_average_interlayer_distance(
15
- interface_atoms: Atoms, tag_substrate: str, tag_film: str, threshold: float = 0.5
12
+ interface_atoms: ASEAtoms, tag_substrate: str, tag_film: str, threshold: float = 0.5
16
13
  ) -> float:
17
14
  """
18
15
  Calculate the average distance between the top layer of substrate atoms and the bottom layer of film atoms.
19
16
 
20
17
  Args:
21
- interface_atoms (ase.Atoms): The ASE Atoms object containing both sets of atoms.
18
+ interface_atoms (ase.ASEAtoms): The ASE ASEAtoms object containing both sets of atoms.
22
19
  tag_substrate (int): The tag representing the substrate atoms.
23
20
  tag_film (int): The tag representing the film atoms.
24
21
  threshold (float): The threshold for identifying the top and bottom layers of atoms.
@@ -48,12 +45,12 @@ def get_average_interlayer_distance(
48
45
 
49
46
 
50
47
  @decorator_convert_material_args_kwargs_to_atoms
51
- def get_surface_area(atoms: Atoms):
48
+ def get_surface_area(atoms: ASEAtoms):
52
49
  """
53
50
  Calculate the area of the surface perpendicular to the z-axis of the atoms structure.
54
51
 
55
52
  Args:
56
- atoms (ase.Atoms): The Atoms object to calculate the surface area of.
53
+ atoms (ase.ASEAtoms): The ASEAtoms object to calculate the surface area of.
57
54
 
58
55
  Returns:
59
56
  float: The surface area of the atoms.
@@ -64,12 +61,12 @@ def get_surface_area(atoms: Atoms):
64
61
 
65
62
 
66
63
  @decorator_convert_material_args_kwargs_to_atoms
67
- def get_chemical_formula(atoms: Atoms):
64
+ def get_chemical_formula(atoms: ASEAtoms):
68
65
  """
69
66
  Calculate the formula of the atoms structure.
70
67
 
71
68
  Args:
72
- atoms (ase.Atoms): The Atoms object to calculate the formula of.
69
+ atoms (ase.ASEAtoms): The ASEAtoms object to calculate the formula of.
73
70
 
74
71
  Returns:
75
72
  str: The formula of the atoms.
@@ -201,3 +198,34 @@ def get_atom_indices_within_radius_pbc(
201
198
 
202
199
  selected_indices = [site.index for site in sites_within_radius]
203
200
  return selected_indices
201
+
202
+
203
+ def get_atom_indices_with_condition_on_coordinates(
204
+ material: Material,
205
+ condition: Callable[[List[float]], bool],
206
+ use_cartesian_coordinates: bool = False,
207
+ ) -> List[int]:
208
+ """
209
+ Select atoms whose coordinates satisfy the given condition.
210
+
211
+ Args:
212
+ material (Material): Material object
213
+ condition (Callable[List[float], bool]): Function that checks if coordinates satisfy the condition.
214
+ use_cartesian (bool): Whether to use Cartesian coordinates for the condition evaluation.
215
+
216
+ Returns:
217
+ List[int]: List of indices of atoms whose coordinates satisfy the condition.
218
+ """
219
+ new_material = material.clone()
220
+ if use_cartesian_coordinates:
221
+ new_basis = new_material.basis
222
+ new_basis.to_cartesian()
223
+ new_material.basis = new_basis
224
+ coordinates = new_material.basis.coordinates.to_array_of_values_with_ids()
225
+
226
+ selected_indices = []
227
+ for coord in coordinates:
228
+ if condition(coord.value):
229
+ selected_indices.append(coord.id)
230
+
231
+ return selected_indices
@@ -2,15 +2,16 @@ from typing import List, Callable
2
2
 
3
3
  from mat3ra.made.material import Material
4
4
  from pydantic import BaseModel
5
- from pymatgen.analysis.defects.core import (
6
- Substitution as PymatgenSubstitution,
7
- Vacancy as PymatgenVacancy,
8
- Interstitial as PymatgenInterstitial,
9
- )
10
- from pymatgen.core import PeriodicSite as PymatgenPeriodicSite
11
5
 
6
+ from ...third_party import (
7
+ PymatgenStructure,
8
+ PymatgenPeriodicSite,
9
+ PymatgenVacancy,
10
+ PymatgenSubstitution,
11
+ PymatgenInterstitial,
12
+ )
12
13
  from ...build import BaseBuilder
13
- from ...convert import PymatgenStructure, to_pymatgen
14
+ from ...convert import to_pymatgen
14
15
  from ..mixins import ConvertGeneratedItemsPymatgenStructureMixin
15
16
  from .configuration import PointDefectConfiguration
16
17
 
@@ -1,4 +1,5 @@
1
- from ..convert import from_ase, from_pymatgen, ASEAtoms, PymatgenStructure
1
+ from ..convert import from_ase, from_pymatgen
2
+ from ..third_party import ASEAtoms, PymatgenStructure
2
3
 
3
4
 
4
5
  class ConvertGeneratedItemsASEAtomsMixin:
@@ -1,18 +1,16 @@
1
- from pymatgen.core.surface import SlabGenerator as PymatgenSlabGenerator
2
- from ...convert import label_pymatgen_slab_termination
3
1
  from typing import List
4
2
  from pydantic import BaseModel
5
3
 
6
-
7
4
  from mat3ra.made.material import Material
8
5
 
9
- from .termination import Termination
6
+ from ...third_party import PymatgenSlab, PymatgenSlabGenerator, label_pymatgen_slab_termination
10
7
  from ...analyze import get_chemical_formula
11
- from ...convert import to_pymatgen, PymatgenSlab
8
+ from ...convert import to_pymatgen
12
9
  from ...build import BaseBuilder
13
10
  from ...build.mixins import ConvertGeneratedItemsPymatgenStructureMixin
14
11
  from ..supercell import create_supercell
15
12
  from .configuration import SlabConfiguration
13
+ from .termination import Termination
16
14
 
17
15
 
18
16
  class SlabSelectorParameters(BaseModel):
@@ -1,10 +1,10 @@
1
1
  from typing import List, Tuple, Any
2
+ from pydantic import BaseModel
2
3
 
3
4
  from mat3ra.code.entity import InMemoryEntity
4
- from pymatgen.symmetry.analyzer import SpacegroupAnalyzer as PymatgenSpacegroupAnalyzer
5
- from pydantic import BaseModel
6
5
 
7
6
  from mat3ra.made.material import Material
7
+ from ...third_party import PymatgenSpacegroupAnalyzer
8
8
  from ...convert import to_pymatgen, from_pymatgen
9
9
 
10
10
 
@@ -1,15 +1,14 @@
1
1
  from typing import List
2
- from ase import Atoms
3
- from ase.build.supercells import make_supercell
4
2
 
5
3
  from mat3ra.made.material import Material
4
+ from ..third_party import ASEAtoms, ase_make_supercell
6
5
  from ..utils import decorator_convert_2x2_to_3x3
7
6
  from ..convert import from_ase, decorator_convert_material_args_kwargs_to_atoms
8
7
 
9
8
 
10
9
  @decorator_convert_2x2_to_3x3
11
10
  @decorator_convert_material_args_kwargs_to_atoms
12
- def create_supercell(atoms: Atoms, supercell_matrix: List[List[int]]) -> Material:
11
+ def create_supercell(atoms: ASEAtoms, supercell_matrix: List[List[int]]) -> Material:
13
12
  """
14
13
  Create a supercell of the atoms.
15
14
 
@@ -21,5 +20,5 @@ def create_supercell(atoms: Atoms, supercell_matrix: List[List[int]]) -> Materia
21
20
  Material: The supercell of the atoms.
22
21
  """
23
22
 
24
- supercell_atoms = make_supercell(atoms, supercell_matrix)
23
+ supercell_atoms = ase_make_supercell(atoms, supercell_matrix)
25
24
  return Material(from_ase(supercell_atoms))
@@ -1,22 +1,19 @@
1
1
  from typing import Optional
2
2
 
3
- from ase import Atoms
4
- from ase.calculators.calculator import Calculator
5
- from ase.calculators.emt import EMT
6
-
7
3
  from ..material import Material
8
4
  from .analyze import get_surface_area
9
5
  from .build.interface.utils import get_slab
10
6
  from .convert import decorator_convert_material_args_kwargs_to_atoms
7
+ from .third_party import ASEAtoms, ASECalculator, ASECalculatorEMT
11
8
 
12
9
 
13
10
  @decorator_convert_material_args_kwargs_to_atoms
14
- def calculate_total_energy(atoms: Atoms, calculator: Calculator):
11
+ def calculate_total_energy(atoms: ASEAtoms, calculator: ASECalculator):
15
12
  """
16
13
  Set calculator for ASE Atoms and calculate the total energy.
17
14
 
18
15
  Args:
19
- atoms (ase.Atoms): The Atoms object to calculate the energy of.
16
+ atoms (ASEAtoms): The Atoms object to calculate the energy of.
20
17
  calculator (ase.calculators.calculator.Calculator): The calculator to use for the energy calculation.
21
18
 
22
19
  Returns:
@@ -27,12 +24,12 @@ def calculate_total_energy(atoms: Atoms, calculator: Calculator):
27
24
 
28
25
 
29
26
  @decorator_convert_material_args_kwargs_to_atoms
30
- def calculate_total_energy_per_atom(atoms: Atoms, calculator: Calculator):
27
+ def calculate_total_energy_per_atom(atoms: ASEAtoms, calculator: ASECalculator):
31
28
  """
32
29
  Set calculator for ASE Atoms and calculate the total energy per atom.
33
30
 
34
31
  Args:
35
- atoms (ase.Atoms): The Atoms object to calculate the energy of.
32
+ atoms (ASEAtoms): The Atoms object to calculate the energy of.
36
33
  calculator (ase.calculators.calculator.Calculator): The calculator to use for the energy calculation.
37
34
 
38
35
  Returns:
@@ -42,13 +39,13 @@ def calculate_total_energy_per_atom(atoms: Atoms, calculator: Calculator):
42
39
 
43
40
 
44
41
  @decorator_convert_material_args_kwargs_to_atoms
45
- def calculate_surface_energy(slab: Atoms, bulk: Atoms, calculator: Calculator):
42
+ def calculate_surface_energy(slab: ASEAtoms, bulk: ASEAtoms, calculator: ASECalculator):
46
43
  """
47
44
  Calculate the surface energy by subtracting the weighted bulk energy from the slab energy.
48
45
 
49
46
  Args:
50
- slab (ase.Atoms): The slab Atoms object to calculate the surface energy of.
51
- bulk (ase.Atoms): The bulk Atoms object to calculate the surface energy of.
47
+ slab (ASEAtoms): The slab Atoms object to calculate the surface energy of.
48
+ bulk (ASEAtoms): The bulk Atoms object to calculate the surface energy of.
52
49
  calculator (ase.calculators.calculator.Calculator): The calculator to use for the energy calculation.
53
50
 
54
51
  Returns:
@@ -62,7 +59,9 @@ def calculate_surface_energy(slab: Atoms, bulk: Atoms, calculator: Calculator):
62
59
 
63
60
 
64
61
  @decorator_convert_material_args_kwargs_to_atoms
65
- def calculate_adhesion_energy(interface: Atoms, substrate_slab: Atoms, film_slab: Atoms, calculator: Calculator):
62
+ def calculate_adhesion_energy(
63
+ interface: ASEAtoms, substrate_slab: ASEAtoms, film_slab: ASEAtoms, calculator: ASECalculator
64
+ ):
66
65
  """
67
66
  Calculate the adhesion energy.
68
67
  The adhesion energy is the difference between the energy of the interface and
@@ -70,9 +69,9 @@ def calculate_adhesion_energy(interface: Atoms, substrate_slab: Atoms, film_slab
70
69
  According to: 10.1088/0953-8984/27/30/305004
71
70
 
72
71
  Args:
73
- interface (ase.Atoms): The interface Atoms object to calculate the adhesion energy of.
74
- substrate_slab (ase.Atoms): The substrate slab Atoms object to calculate the adhesion energy of.
75
- film_slab (ase.Atoms): The film slab Atoms object to calculate the adhesion energy of.
72
+ interface (ASEAtoms): The interface ASEAtoms object to calculate the adhesion energy of.
73
+ substrate_slab (ASEAtoms): The substrate slab ASEAtoms object to calculate the adhesion energy of.
74
+ film_slab (ASEAtoms): The film slab ASEAtoms object to calculate the adhesion energy of.
76
75
  calculator (ase.calculators.calculator.Calculator): The calculator to use for the energy calculation.
77
76
 
78
77
  Returns:
@@ -91,7 +90,7 @@ def calculate_interfacial_energy(
91
90
  substrate_bulk: Optional[Material] = None,
92
91
  film_slab: Optional[Material] = None,
93
92
  film_bulk: Optional[Material] = None,
94
- calculator: Calculator = EMT(),
93
+ calculator: ASECalculator = ASECalculatorEMT(),
95
94
  ):
96
95
  """
97
96
  Calculate the interfacial energy.
@@ -5,20 +5,22 @@ from typing import Any, Callable, Dict, Union
5
5
  from mat3ra.made.material import Material
6
6
  from mat3ra.made.utils import map_array_with_id_value_to_array
7
7
  from mat3ra.utils.mixins import RoundNumericValuesMixin
8
- from pymatgen.io.ase import AseAtomsAdaptor
9
- from pymatgen.io.vasp.inputs import Poscar
10
8
 
11
- from .utils import (
12
- INTERFACE_LABELS_MAP,
9
+ from ..third_party import (
13
10
  ASEAtoms,
11
+ PymatgenAseAtomsAdaptor,
14
12
  PymatgenInterface,
15
13
  PymatgenLattice,
14
+ PymatgenPoscar,
16
15
  PymatgenSlab,
17
16
  PymatgenStructure,
17
+ label_pymatgen_slab_termination,
18
+ )
19
+ from .utils import (
20
+ INTERFACE_LABELS_MAP,
18
21
  extract_labels_from_pymatgen_structure,
19
22
  extract_metadata_from_pymatgen_structure,
20
23
  extract_tags_from_ase_atoms,
21
- label_pymatgen_slab_termination,
22
24
  map_array_to_array_with_id_value,
23
25
  )
24
26
 
@@ -138,7 +140,7 @@ def to_poscar(material_or_material_data: Union[Material, Dict[str, Any]]) -> str
138
140
  str: A POSCAR string.
139
141
  """
140
142
  structure = to_pymatgen(material_or_material_data)
141
- poscar = Poscar(structure)
143
+ poscar = PymatgenPoscar(structure)
142
144
  # For pymatgen `2023.6.23` supporting py3.8 the method name is "get_string"
143
145
  # TODO: cleanup the if statement when dropping support for py3.8
144
146
  if hasattr(poscar, "get_string"):
@@ -175,7 +177,7 @@ def to_ase(material_or_material_data: Union[Material, Dict[str, Any]]) -> ASEAto
175
177
  else:
176
178
  material_config = material_or_material_data
177
179
  structure = to_pymatgen(material_config)
178
- atoms = AseAtomsAdaptor.get_atoms(structure)
180
+ atoms = PymatgenAseAtomsAdaptor.get_atoms(structure)
179
181
 
180
182
  atomic_labels = material_config["basis"].get("labels", [])
181
183
  if atomic_labels:
@@ -196,7 +198,7 @@ def from_ase(ase_atoms: ASEAtoms) -> Dict[str, Any]:
196
198
  dict: A dictionary containing the material information in ESSE format.
197
199
  """
198
200
  # TODO: check that atomic labels/tags are properly handled
199
- structure = AseAtomsAdaptor.get_structure(ase_atoms)
201
+ structure = PymatgenAseAtomsAdaptor.get_structure(ase_atoms)
200
202
  material = from_pymatgen(structure)
201
203
  ase_tags = extract_tags_from_ase_atoms(ase_atoms)
202
204
  material["basis"]["labels"] = ase_tags
@@ -1,22 +1,10 @@
1
1
  import json
2
2
  from typing import Any, Dict, List, Union
3
3
 
4
- from ase import Atoms as ASEAtoms
5
4
  from mat3ra.made.utils import map_array_to_array_with_id_value
6
5
  from mat3ra.utils.object import NumpyNDArrayRoundEncoder
7
- from pymatgen.core.interface import Interface as PymatgenInterface
8
- from pymatgen.core.interface import label_termination
9
- from pymatgen.core.structure import Lattice as PymatgenLattice
10
- from pymatgen.core.structure import Structure as PymatgenStructure
11
- from pymatgen.core.surface import Slab as PymatgenSlab
12
-
13
- # Re-exported imports to allow for both use in type hints and instantiation
14
- PymatgenLattice = PymatgenLattice
15
- PymatgenStructure = PymatgenStructure
16
- PymatgenSlab = PymatgenSlab
17
- PymatgenInterface = PymatgenInterface
18
- ASEAtoms = ASEAtoms
19
- label_pymatgen_slab_termination = label_termination
6
+
7
+ from ..third_party import ASEAtoms, PymatgenInterface, PymatgenStructure
20
8
 
21
9
  INTERFACE_LABELS_MAP = {"substrate": 0, "film": 1}
22
10
 
@@ -1,12 +1,17 @@
1
- from typing import List, Union
1
+ from typing import Callable, List, Optional, Union
2
2
 
3
+ import numpy as np
3
4
  from mat3ra.made.material import Material
4
- from pymatgen.analysis.structure_analyzer import SpacegroupAnalyzer
5
- from pymatgen.core.structure import Structure
6
5
 
7
- from .analyze import get_atom_indices_within_layer_by_atom_index, get_atom_indices_within_radius_pbc
6
+ from .analyze import get_atom_indices_with_condition_on_coordinates, get_atom_indices_within_radius_pbc
8
7
  from .convert import decorator_convert_material_args_kwargs_to_structure
9
- from .utils import translate_to_bottom_pymatgen_structure
8
+ from .third_party import PymatgenSpacegroupAnalyzer, PymatgenStructure
9
+ from .utils import (
10
+ is_coordinate_in_box,
11
+ is_coordinate_in_cylinder,
12
+ is_coordinate_within_layer,
13
+ translate_to_bottom_pymatgen_structure,
14
+ )
10
15
 
11
16
 
12
17
  def filter_by_label(material: Material, label: Union[int, str]) -> Material:
@@ -30,7 +35,7 @@ def filter_by_label(material: Material, label: Union[int, str]) -> Material:
30
35
 
31
36
 
32
37
  @decorator_convert_material_args_kwargs_to_structure
33
- def translate_to_bottom(structure: Structure, use_conventional_cell: bool = True):
38
+ def translate_to_bottom(structure: PymatgenStructure, use_conventional_cell: bool = True):
34
39
  """
35
40
  Translate atoms to the bottom of the cell (vacuum on top) to allow for the correct consecutive interface generation.
36
41
  If use_conventional_cell is passed, conventional cell is used.
@@ -42,20 +47,20 @@ def translate_to_bottom(structure: Structure, use_conventional_cell: bool = True
42
47
  Structure: The normalized pymatgen Structure object.
43
48
  """
44
49
  if use_conventional_cell:
45
- structure = SpacegroupAnalyzer(structure).get_conventional_standard_structure()
50
+ structure = PymatgenSpacegroupAnalyzer(structure).get_conventional_standard_structure()
46
51
  structure = translate_to_bottom_pymatgen_structure(structure)
47
52
  return structure
48
53
 
49
54
 
50
55
  @decorator_convert_material_args_kwargs_to_structure
51
- def wrap_to_unit_cell(structure: Structure):
56
+ def wrap_to_unit_cell(structure: PymatgenStructure):
52
57
  """
53
58
  Wrap atoms to the cell
54
59
 
55
60
  Args:
56
- structure (Structure): The pymatgen Structure object to normalize.
61
+ structure (PymatgenStructure): The pymatgen PymatgenStructure object to normalize.
57
62
  Returns:
58
- Structure: The wrapped pymatgen Structure object.
63
+ PymatgenStructure: The wrapped pymatgen PymatgenStructure object.
59
64
  """
60
65
  structure.make_supercell((1, 1, 1), to_unit_cell=True)
61
66
  return structure
@@ -82,30 +87,73 @@ def filter_material_by_ids(material: Material, ids: List[int], invert: bool = Fa
82
87
  return new_material
83
88
 
84
89
 
90
+ def filter_by_condition_on_coordinates(
91
+ material: Material,
92
+ condition: Callable[[List[float]], bool],
93
+ use_cartesian_coordinates: bool = False,
94
+ invert_selection: bool = False,
95
+ ) -> Material:
96
+ """
97
+ Filter atoms based on a condition on their coordinates.
98
+
99
+ Args:
100
+ material (Material): The material object to filter.
101
+ condition (Callable): The condition on coordinate function.
102
+ use_cartesian_coordinates (bool): Whether to use cartesian coordinates.
103
+ invert_selection (bool): Whether to invert the selection.
104
+
105
+ Returns:
106
+ Material: The filtered material object.
107
+ """
108
+ new_material = material.clone()
109
+ ids = get_atom_indices_with_condition_on_coordinates(
110
+ material,
111
+ condition,
112
+ use_cartesian_coordinates=use_cartesian_coordinates,
113
+ )
114
+
115
+ new_material = filter_material_by_ids(new_material, ids, invert=invert_selection)
116
+ return new_material
117
+
118
+
85
119
  def filter_by_layers(
86
- material: Material, central_atom_id: int, layer_thickness: float, invert: bool = False
120
+ material: Material,
121
+ center_coordinate: List[float] = [0, 0, 0],
122
+ central_atom_id: Optional[int] = None,
123
+ layer_thickness: float = 1.0,
124
+ invert_selection: bool = False,
87
125
  ) -> Material:
88
126
  """
89
127
  Filter out atoms within a specified layer thickness of a central atom along c-vector direction.
90
128
 
91
129
  Args:
92
130
  material (Material): The material object to filter.
131
+ center_coordinate (List[float]): Index of the central atom.
93
132
  central_atom_id (int): Index of the central atom.
94
133
  layer_thickness (float): Thickness of the layer in angstroms.
95
- invert (bool): Whether to invert the selection.
134
+ invert_selection (bool): Whether to invert the selection.
96
135
 
97
136
  Returns:
98
137
  Material: The filtered material object.
99
138
  """
100
- ids = get_atom_indices_within_layer_by_atom_index(
101
- material,
102
- central_atom_id,
103
- layer_thickness,
104
- )
105
- return filter_material_by_ids(material, ids, invert=invert)
139
+ if central_atom_id is not None:
140
+ center_coordinate = material.basis.coordinates.get_element_value_by_index(central_atom_id)
141
+ vectors = material.lattice.vectors
142
+ direction_vector = np.array(vectors[2])
143
+
144
+ def condition(coordinate):
145
+ return is_coordinate_within_layer(coordinate, center_coordinate, direction_vector, layer_thickness)
106
146
 
147
+ return filter_by_condition_on_coordinates(material, condition, invert_selection=invert_selection)
107
148
 
108
- def filter_by_sphere(material: Material, central_atom_id: int, radius: float, invert: bool = False) -> Material:
149
+
150
+ def filter_by_sphere(
151
+ material: Material,
152
+ center_coordinate: List[float] = [0, 0, 0],
153
+ central_atom_id: Optional[int] = None,
154
+ radius: float = 1,
155
+ invert: bool = False,
156
+ ) -> Material:
109
157
  """
110
158
  Filter out atoms within a specified radius of a central atom considering periodic boundary conditions.
111
159
 
@@ -121,6 +169,124 @@ def filter_by_sphere(material: Material, central_atom_id: int, radius: float, in
121
169
  ids = get_atom_indices_within_radius_pbc(
122
170
  material=material,
123
171
  atom_index=central_atom_id,
172
+ position=center_coordinate,
124
173
  radius=radius,
125
174
  )
126
175
  return filter_material_by_ids(material, ids, invert=invert)
176
+
177
+
178
+ def filter_by_circle_projection(
179
+ material: Material,
180
+ x: float = 0.5,
181
+ y: float = 0.5,
182
+ r: float = 0.25,
183
+ use_cartesian_coordinates: bool = False,
184
+ invert_selection: bool = False,
185
+ ) -> Material:
186
+ """
187
+ Get material with atoms that are within or outside an XY circle projection.
188
+
189
+ Args:
190
+ material (Material): The material object to filter.
191
+ x (float): The x-coordinate of the circle center.
192
+ y (float): The y-coordinate of the circle center.
193
+ r (float): The radius of the circle.
194
+ use_cartesian_coordinates (bool): Whether to use cartesian coordinates
195
+ invert_selection (bool): Whether to invert the selection.
196
+
197
+ Returns:
198
+ Material: The filtered material object.
199
+ """
200
+
201
+ def condition(coordinate):
202
+ return is_coordinate_in_cylinder(coordinate, [x, y, 0], r, min_z=0, max_z=1)
203
+
204
+ return filter_by_condition_on_coordinates(
205
+ material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection
206
+ )
207
+
208
+
209
+ def filter_by_cylinder(
210
+ material: Material,
211
+ center_position: List[float] = [0.5, 0.5],
212
+ min_z: float = 0,
213
+ max_z: float = 1,
214
+ radius: float = 0.25,
215
+ use_cartesian_coordinates: bool = False,
216
+ invert_selection: bool = False,
217
+ ) -> Material:
218
+ """
219
+ Get material with atoms that are within or outside a cylinder.
220
+
221
+ Args:
222
+ material (Material): The material object to filter.
223
+ center_position (List[float]): The coordinates of the center position.
224
+ radius (float): The radius of the cylinder.
225
+ min_z (float): Lower limit of z-coordinate.
226
+ max_z (float): Upper limit of z-coordinate.
227
+ use_cartesian_coordinates (bool): Whether to use cartesian coordinates
228
+ invert_selection (bool): Whether to invert the selection.
229
+
230
+ Returns:
231
+ Material: The filtered material object.
232
+ """
233
+
234
+ def condition(coordinate):
235
+ return is_coordinate_in_cylinder(coordinate, center_position, radius, min_z, max_z)
236
+
237
+ return filter_by_condition_on_coordinates(
238
+ material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection
239
+ )
240
+
241
+
242
+ def filter_by_rectangle_projection(
243
+ material: Material,
244
+ x_min: float = 0.0,
245
+ y_min: float = 0.0,
246
+ x_max: float = 1.0,
247
+ y_max: float = 1.0,
248
+ use_cartesian_coordinates: bool = False,
249
+ invert_selection: bool = False,
250
+ ) -> Material:
251
+ """
252
+ Get material with atoms that are within or outside an XY rectangle projection.
253
+
254
+ Args:
255
+
256
+ material (Material): The material object to filter.
257
+ x_min (float): The minimum x-coordinate of the rectangle.
258
+ y_min (float): The minimum y-coordinate of the rectangle.
259
+ x_max (float): The maximum x-coordinate of the rectangle.
260
+ y_max (float): The maximum y-coordinate of the rectangle.
261
+ use_cartesian_coordinates (bool): Whether to use cartesian coordinates
262
+ invert_selection (bool): Whether to invert the selection.
263
+
264
+ Returns:
265
+ Material: The filtered material object.
266
+ """
267
+
268
+ def condition(coordinate):
269
+ return is_coordinate_in_box(coordinate, [x_min, y_min, 0], [x_max, y_max, 1])
270
+
271
+ return filter_by_condition_on_coordinates(
272
+ material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection
273
+ )
274
+
275
+
276
+ def filter_by_box(
277
+ material: Material,
278
+ min_coordinate: List[float] = [0.0, 0.0, 0.0],
279
+ max_coordinate: List[float] = [1.0, 1.0, 1.0],
280
+ use_cartesian_coordinates: bool = False,
281
+ invert_selection: bool = False,
282
+ ) -> Material:
283
+ """
284
+ Get material with atoms that are within or outside an XYZ box.
285
+ """
286
+
287
+ def condition(coordinate):
288
+ return is_coordinate_in_box(coordinate, min_coordinate, max_coordinate)
289
+
290
+ return filter_by_condition_on_coordinates(
291
+ material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection
292
+ )
@@ -0,0 +1,41 @@
1
+ from ase import Atoms as ASEAtoms
2
+ from ase.build.supercells import make_supercell as ase_make_supercell
3
+ from ase.calculators.calculator import Calculator as ASECalculator
4
+ from ase.calculators.emt import EMT as ASECalculatorEMT
5
+ from pymatgen.analysis.defects.core import Interstitial as PymatgenInterstitial
6
+ from pymatgen.analysis.defects.core import Substitution as PymatgenSubstitution
7
+ from pymatgen.analysis.defects.core import Vacancy as PymatgenVacancy
8
+ from pymatgen.core import IStructure as PymatgenIStructure
9
+ from pymatgen.core import PeriodicSite as PymatgenPeriodicSite
10
+ from pymatgen.core.interface import Interface as PymatgenInterface
11
+ from pymatgen.core.interface import label_termination as label_pymatgen_slab_termination
12
+ from pymatgen.core.structure import Lattice as PymatgenLattice
13
+ from pymatgen.core.structure import Structure as PymatgenStructure
14
+ from pymatgen.core.surface import Slab as PymatgenSlab
15
+ from pymatgen.core.surface import SlabGenerator as PymatgenSlabGenerator
16
+ from pymatgen.io.ase import AseAtomsAdaptor as PymatgenAseAtomsAdaptor
17
+ from pymatgen.io.vasp.inputs import Poscar as PymatgenPoscar
18
+ from pymatgen.symmetry.analyzer import SpacegroupAnalyzer as PymatgenSpacegroupAnalyzer
19
+
20
+ # Re-exported imports to allow for both use in type hints and instantiation
21
+
22
+ __all__ = [
23
+ "ASEAtoms",
24
+ "ASECalculator",
25
+ "ASECalculatorEMT",
26
+ "PymatgenLattice",
27
+ "PymatgenStructure",
28
+ "PymatgenIStructure",
29
+ "PymatgenSlab",
30
+ "PymatgenSlabGenerator",
31
+ "PymatgenInterface",
32
+ "PymatgenPeriodicSite",
33
+ "PymatgenSpacegroupAnalyzer",
34
+ "PymatgenVacancy",
35
+ "PymatgenSubstitution",
36
+ "PymatgenInterstitial",
37
+ "label_pymatgen_slab_termination",
38
+ "ase_make_supercell",
39
+ "PymatgenAseAtomsAdaptor",
40
+ "PymatgenPoscar",
41
+ ]
@@ -4,18 +4,19 @@ from typing import Callable, List
4
4
  import numpy as np
5
5
  from mat3ra.made.basis import Basis
6
6
  from mat3ra.utils.matrix import convert_2x2_to_3x3
7
- from pymatgen.core.structure import Structure
7
+
8
+ from .third_party import PymatgenStructure
8
9
 
9
10
 
10
11
  # TODO: convert to accept ASE Atoms object
11
- def translate_to_bottom_pymatgen_structure(structure: Structure):
12
+ def translate_to_bottom_pymatgen_structure(structure: PymatgenStructure):
12
13
  """
13
14
  Translate the structure to the bottom of the cell.
14
15
  Args:
15
- structure (Structure): The pymatgen Structure object to translate.
16
+ structure (PymatgenStructure): The pymatgen Structure object to translate.
16
17
 
17
18
  Returns:
18
- Structure: The translated pymatgen Structure object.
19
+ PymatgenStructure: The translated pymatgen Structure object.
19
20
  """
20
21
  min_c = min(site.c for site in structure)
21
22
  translation_vector = [0, 0, -min_c]
@@ -95,3 +96,69 @@ def get_norm(vector: List[float]) -> float:
95
96
  float: The norm of the vector.
96
97
  """
97
98
  return float(np.linalg.norm(vector))
99
+
100
+
101
+ # Condition functions:
102
+
103
+
104
+ def is_coordinate_in_cylinder(
105
+ coordinate: List[float], center_position: List[float], radius: float = 0.25, min_z: float = 0, max_z: float = 1
106
+ ) -> bool:
107
+ """
108
+ Check if a point is inside a cylinder.
109
+ Args:
110
+ coordinate (List[float]): The coordinate to check.
111
+ center_position (List[float]): The coordinates of the center position.
112
+ min_z (float): Lower limit of z-coordinate.
113
+ max_z (float): Upper limit of z-coordinate.
114
+ radius (float): The radius of the cylinder.
115
+
116
+ Returns:
117
+ bool: True if the point is inside the cylinder, False otherwise.
118
+ """
119
+ return (coordinate[0] - center_position[0]) ** 2 + (coordinate[1] - center_position[1]) ** 2 <= radius**2 and (
120
+ min_z <= coordinate[2] <= max_z
121
+ )
122
+
123
+
124
+ def is_coordinate_in_box(
125
+ coordinate: List[float], min_coordinate: List[float] = [0, 0, 0], max_coordinate: List[float] = [1, 1, 1]
126
+ ) -> bool:
127
+ """
128
+ Check if a point is inside a box.
129
+ Args:
130
+ coordinate (List[float]): The coordinate to check.
131
+ min_coordinate (List[float]): The minimum coordinate of the box.
132
+ max_coordinate (List[float]): The maximum coordinate of the box.
133
+ Returns:
134
+ bool: True if the point is inside the box, False otherwise.
135
+ """
136
+ x_min, y_min, z_min = min_coordinate
137
+ x_max, y_max, z_max = max_coordinate
138
+ return x_min <= coordinate[0] <= x_max and y_min <= coordinate[1] <= y_max and z_min <= coordinate[2] <= z_max
139
+
140
+
141
+ def is_coordinate_within_layer(
142
+ coordinate: List[float], center_position: List[float], direction_vector: List[float], layer_thickness: float
143
+ ) -> bool:
144
+ """
145
+ Checks if a point's projection along a specified direction vector
146
+ is within a certain layer thickness centered around a given position.
147
+
148
+ Args:
149
+ coordinate (List[float]): The coordinate to check.
150
+ center_position (List[float]): The coordinates of the center position.
151
+ direction_vector (List[float]): The direction vector along which the layer thickness is defined.
152
+ layer_thickness (float): The thickness of the layer along the direction vector.
153
+
154
+ Returns:
155
+ bool: True if the point is within the layer thickness, False otherwise.
156
+ """
157
+ direction_norm = np.array(direction_vector) / np.linalg.norm(direction_vector)
158
+ central_projection = np.dot(center_position, direction_norm)
159
+ layer_thickness_frac = layer_thickness / np.linalg.norm(direction_vector)
160
+
161
+ lower_bound = central_projection - layer_thickness_frac / 2
162
+ upper_bound = central_projection + layer_thickness_frac / 2
163
+
164
+ return lower_bound <= np.dot(coordinate, direction_norm) <= upper_bound
@@ -7,8 +7,8 @@ from mat3ra.utils import assertion as assertion_utils
7
7
 
8
8
  ase_ni = bulk("Ni", "fcc", a=3.52, cubic=True)
9
9
  material = Material(from_ase(ase_ni))
10
- section = filter_by_layers(material, 0, 1.0)
11
- cavity = filter_by_layers(material, 0, 1.0, invert=True)
10
+ section = filter_by_layers(material, central_atom_id=0, layer_thickness=1.0)
11
+ cavity = filter_by_layers(material, central_atom_id=0, layer_thickness=1.0, invert_selection=True)
12
12
 
13
13
  # Change 0th element
14
14
  section.basis.elements.values[0] = "Ge"
@@ -1,7 +1,13 @@
1
1
  from ase.build import bulk
2
2
  from mat3ra.made.material import Material
3
3
  from mat3ra.made.tools.convert import from_ase
4
- from mat3ra.made.tools.modify import filter_by_label, filter_by_layers, filter_by_sphere
4
+ from mat3ra.made.tools.modify import (
5
+ filter_by_circle_projection,
6
+ filter_by_label,
7
+ filter_by_layers,
8
+ filter_by_rectangle_projection,
9
+ filter_by_sphere,
10
+ )
5
11
  from mat3ra.utils import assertion as assertion_utils
6
12
 
7
13
  from .fixtures import SI_CONVENTIONAL_CELL
@@ -46,15 +52,15 @@ expected_basis_layers_cavity = {
46
52
 
47
53
 
48
54
  expected_basis_sphere_cluster = {
49
- "elements": [{"id": 0, "value": "Si"}],
50
- "coordinates": [{"id": 0, "value": [0.5, 0.0, 0.0]}],
55
+ "elements": [{"id": 2, "value": "Si"}],
56
+ "coordinates": [{"id": 2, "value": [0.5, 0.5, 0.5]}],
51
57
  **COMMON_PART,
52
58
  }
53
59
 
54
60
  expected_basis_sphere_cavity = {
55
61
  "elements": [
62
+ {"id": 0, "value": "Si"},
56
63
  {"id": 1, "value": "Si"},
57
- {"id": 2, "value": "Si"},
58
64
  {"id": 3, "value": "Si"},
59
65
  {"id": 4, "value": "Si"},
60
66
  {"id": 5, "value": "Si"},
@@ -62,8 +68,8 @@ expected_basis_sphere_cavity = {
62
68
  {"id": 7, "value": "Si"},
63
69
  ],
64
70
  "coordinates": [
71
+ {"id": 0, "value": [0.5, 0.0, 0.0]},
65
72
  {"id": 1, "value": [0.25, 0.25, 0.75]},
66
- {"id": 2, "value": [0.5, 0.5, 0.5]},
67
73
  {"id": 3, "value": [0.25, 0.75, 0.25]},
68
74
  {"id": 4, "value": [0.0, 0.0, 0.5]},
69
75
  {"id": 5, "value": [0.75, 0.25, 0.25]},
@@ -73,6 +79,9 @@ expected_basis_sphere_cavity = {
73
79
  **COMMON_PART,
74
80
  }
75
81
 
82
+ CRYSTAL_RADIUS = 0.25 # in crystal coordinates
83
+ CRYSTAL_CENTER_3D = [0.5, 0.5, 0.5] # in crystal coordinates
84
+
76
85
 
77
86
  def test_filter_by_label():
78
87
  substrate = bulk("Si", cubic=True)
@@ -89,15 +98,31 @@ def test_filter_by_label():
89
98
 
90
99
  def test_filter_by_layers():
91
100
  material = Material(SI_CONVENTIONAL_CELL)
92
- section = filter_by_layers(material, 0, 3.0)
93
- cavity = filter_by_layers(material, 0, 3.0, invert=True)
101
+ section = filter_by_layers(material=material, central_atom_id=0, layer_thickness=3.0)
102
+ cavity = filter_by_layers(material=material, central_atom_id=0, layer_thickness=3.0, invert_selection=True)
94
103
  assertion_utils.assert_deep_almost_equal(expected_basis_layers_section, section.basis.to_json())
95
104
  assertion_utils.assert_deep_almost_equal(expected_basis_layers_cavity, cavity.basis.to_json())
96
105
 
97
106
 
98
107
  def test_filter_by_sphere():
99
108
  material = Material(SI_CONVENTIONAL_CELL)
100
- cluster = filter_by_sphere(material, 0, 2.0)
101
- cavity = filter_by_sphere(material, 0, 2.0, invert=True)
109
+ cluster = filter_by_sphere(material, center_coordinate=CRYSTAL_CENTER_3D, radius=CRYSTAL_RADIUS)
110
+ cavity = filter_by_sphere(material, center_coordinate=CRYSTAL_CENTER_3D, radius=CRYSTAL_RADIUS, invert=True)
102
111
  assertion_utils.assert_deep_almost_equal(expected_basis_sphere_cluster, cluster.basis.to_json())
103
112
  assertion_utils.assert_deep_almost_equal(expected_basis_sphere_cavity, cavity.basis.to_json())
113
+
114
+
115
+ def test_filter_by_circle_projection():
116
+ material = Material(SI_CONVENTIONAL_CELL)
117
+ # Small cylinder in the middle of the cell containing the central atom will be removed -- the same as with sphere
118
+ section = filter_by_circle_projection(material, 0.5, 0.5, CRYSTAL_RADIUS)
119
+ cavity = filter_by_circle_projection(material, 0.5, 0.5, CRYSTAL_RADIUS, invert_selection=True)
120
+ assertion_utils.assert_deep_almost_equal(expected_basis_sphere_cluster, section.basis.to_json())
121
+ assertion_utils.assert_deep_almost_equal(expected_basis_sphere_cavity, cavity.basis.to_json())
122
+
123
+
124
+ def test_filter_by_rectangle_projection():
125
+ material = Material(SI_CONVENTIONAL_CELL)
126
+ # Default will contain all the atoms
127
+ section = filter_by_rectangle_projection(material)
128
+ assertion_utils.assert_deep_almost_equal(material.basis.to_json(), section.basis.to_json())