@mat3ra/made 2024.6.4-0 → 2024.6.12-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/.husky/pre-commit CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/bin/sh
2
2
  . "$(dirname "$0")/_/husky.sh"
3
3
 
4
- npx lint-staged
4
+ npx lint-staged --allow-empty
5
5
  npm run transpile
6
6
  git add dist
@@ -15,7 +15,7 @@ repos:
15
15
  - id: end-of-file-fixer
16
16
  exclude: ^tests/fixtures*
17
17
  - id: trailing-whitespace
18
- exclude: ^tests/fixtures*
18
+ exclude: ^tests/fixtures*|^dist*
19
19
  - repo: local
20
20
  hooks:
21
21
  - id: lint-staged
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mat3ra/made",
3
- "version": "2024.6.4-0",
3
+ "version": "2024.6.12-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",
package/pyproject.toml CHANGED
@@ -27,6 +27,7 @@ dependencies = [
27
27
  tools = [
28
28
  "pymatgen",
29
29
  "ase",
30
+ "pymatgen-analysis-defects"
30
31
  ]
31
32
  dev = [
32
33
  "pre-commit",
@@ -3,6 +3,7 @@ from typing import Any, Dict, List, Union
3
3
  from mat3ra.code.constants import AtomicCoordinateUnits, Units
4
4
  from mat3ra.code.entity import HasDescriptionHasMetadataNamedDefaultableInMemoryEntity
5
5
  from mat3ra.esse.models.material import MaterialSchema
6
+ from mat3ra.made.utils import map_array_with_id_value_to_array
6
7
 
7
8
  defaultMaterialConfig = {
8
9
  "name": "Silicon FCC",
@@ -57,3 +58,7 @@ class Material(HasDescriptionHasMetadataNamedDefaultableInMemoryEntity):
57
58
 
58
59
  def to_json(self, exclude: List[str] = []) -> MaterialSchemaJSON:
59
60
  return {**super().to_json()}
61
+
62
+ @property
63
+ def coordinates_array(self) -> List[List[float]]:
64
+ return map_array_with_id_value_to_array(self.basis["coordinates"])
@@ -1,6 +1,9 @@
1
+ from typing import List
2
+
1
3
  import numpy as np
2
4
  from ase import Atoms
3
5
 
6
+ from ..material import Material
4
7
  from .convert import decorator_convert_material_args_kwargs_to_atoms
5
8
 
6
9
 
@@ -69,3 +72,20 @@ def get_chemical_formula(atoms: Atoms):
69
72
  str: The formula of the atoms.
70
73
  """
71
74
  return atoms.get_chemical_formula()
75
+
76
+
77
+ def get_closest_site_id_from_position(material: Material, position: List[float]) -> int:
78
+ """
79
+ Get the site ID of the closest site to a given position in the crystal.
80
+
81
+ Args:
82
+ material (Material): The material object to find the closest site in.
83
+ position (List[float]): The position to find the closest site to.
84
+
85
+ Returns:
86
+ int: The site ID of the closest site.
87
+ """
88
+ coordinates = np.array(material.coordinates_array)
89
+ position = np.array(position) # type: ignore
90
+ distances = np.linalg.norm(coordinates - position, axis=1)
91
+ return int(np.argmin(distances))
@@ -73,7 +73,8 @@ class BaseBuilder(BaseModel):
73
73
  return material_config
74
74
 
75
75
  def _finalize(self, materials: List[Material], configuration: _ConfigurationType) -> List[Material]:
76
- return [self._update_material_name(material, configuration) for material in materials]
76
+ materials_with_metadata = [self._update_material_metadata(material, configuration) for material in materials]
77
+ return [self._update_material_name(material, configuration) for material in materials_with_metadata]
77
78
 
78
79
  def get_materials(
79
80
  self,
@@ -99,3 +100,7 @@ class BaseBuilder(BaseModel):
99
100
  def _update_material_name(self, material, configuration):
100
101
  # Do nothing by default
101
102
  return material
103
+
104
+ def _update_material_metadata(self, material, configuration):
105
+ material.metadata["build"] = {"configuration": configuration.to_json()}
106
+ return material
@@ -0,0 +1,36 @@
1
+ from typing import Optional
2
+
3
+ from mat3ra.utils.factory import BaseFactory
4
+ from mat3ra.made.material import Material
5
+
6
+ from .builders import PointDefectBuilderParameters
7
+ from .configuration import PointDefectConfiguration
8
+ from .enums import PointDefectTypeEnum
9
+
10
+
11
+ class DefectBuilderFactory(BaseFactory):
12
+ __class_registry__ = {
13
+ PointDefectTypeEnum.VACANCY: "mat3ra.made.tools.build.defect.builders.VacancyPointDefectBuilder",
14
+ PointDefectTypeEnum.SUBSTITUTION: "mat3ra.made.tools.build.defect.builders.SubstitutionPointDefectBuilder",
15
+ PointDefectTypeEnum.INTERSTITIAL: "mat3ra.made.tools.build.defect.builders.InterstitialPointDefectBuilder",
16
+ }
17
+
18
+
19
+ def create_defect(
20
+ configuration: PointDefectConfiguration,
21
+ builder_parameters: Optional[PointDefectBuilderParameters] = None,
22
+ ) -> Material:
23
+ """
24
+ Return a material with a selected defect added.
25
+
26
+ Args:
27
+ configuration: The configuration of the defect to be added.
28
+ builder_parameters: The parameters to be used by the defect builder.
29
+
30
+ Returns:
31
+ The material with the defect added.
32
+ """
33
+ BuilderClass = DefectBuilderFactory.get_class_by_name(configuration.defect_type)
34
+ builder = BuilderClass(builder_parameters)
35
+
36
+ return builder.get_material(configuration) if builder else configuration.crystal
@@ -0,0 +1,69 @@
1
+ from typing import List, Callable
2
+
3
+ from mat3ra.made.material import Material
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
+
12
+ from ...build import BaseBuilder
13
+ from ...convert import PymatgenStructure, to_pymatgen
14
+ from ..mixins import ConvertGeneratedItemsPymatgenStructureMixin
15
+ from .configuration import PointDefectConfiguration
16
+ from mat3ra.made.utils import get_array_with_id_value_element_value_by_index
17
+
18
+
19
+ class PointDefectBuilderParameters(BaseModel):
20
+ center_defect: bool = False
21
+
22
+
23
+ class PointDefectBuilder(ConvertGeneratedItemsPymatgenStructureMixin, BaseBuilder):
24
+ """
25
+ Builder class for generating point defects.
26
+ """
27
+
28
+ _BuildParametersType = PointDefectBuilderParameters
29
+ _DefaultBuildParameters = PointDefectBuilderParameters()
30
+ _GeneratedItemType: PymatgenStructure = PymatgenStructure
31
+ _ConfigurationType = PointDefectConfiguration
32
+ _generator: Callable
33
+
34
+ def _get_species(self, configuration: BaseBuilder._ConfigurationType):
35
+ crystal_elements = configuration.crystal.basis["elements"]
36
+ placeholder_specie = get_array_with_id_value_element_value_by_index(crystal_elements, 0)
37
+ return configuration.chemical_element or placeholder_specie
38
+
39
+ def _generate(self, configuration: BaseBuilder._ConfigurationType) -> List[_GeneratedItemType]:
40
+ pymatgen_structure = to_pymatgen(configuration.crystal)
41
+ pymatgen_periodic_site = PymatgenPeriodicSite(
42
+ species=self._get_species(configuration),
43
+ coords=configuration.position,
44
+ lattice=pymatgen_structure.lattice,
45
+ )
46
+ defect = self._generator(pymatgen_structure, pymatgen_periodic_site)
47
+ defect_structure = defect.defect_structure.copy()
48
+ defect_structure.remove_oxidation_states()
49
+ return [defect_structure]
50
+
51
+ def _update_material_name(self, material: Material, configuration: BaseBuilder._ConfigurationType) -> Material:
52
+ updated_material = super()._update_material_name(material, configuration)
53
+ capitalized_defect_type = configuration.defect_type.name.capitalize()
54
+ chemical_element = configuration.chemical_element if configuration.chemical_element else ""
55
+ new_name = f"{updated_material.name}, {capitalized_defect_type} {chemical_element} Defect"
56
+ updated_material.name = new_name
57
+ return updated_material
58
+
59
+
60
+ class VacancyPointDefectBuilder(PointDefectBuilder):
61
+ _generator: PymatgenVacancy = PymatgenVacancy
62
+
63
+
64
+ class SubstitutionPointDefectBuilder(PointDefectBuilder):
65
+ _generator: PymatgenSubstitution = PymatgenSubstitution
66
+
67
+
68
+ class InterstitialPointDefectBuilder(PointDefectBuilder):
69
+ _generator: PymatgenInterstitial = PymatgenInterstitial
@@ -0,0 +1,44 @@
1
+ from typing import Optional, List, Any
2
+ from pydantic import BaseModel
3
+
4
+ from mat3ra.code.entity import InMemoryEntity
5
+ from mat3ra.made.material import Material
6
+
7
+ from ...analyze import get_closest_site_id_from_position
8
+ from .enums import PointDefectTypeEnum
9
+
10
+
11
+ class BaseDefectConfiguration(BaseModel):
12
+ # TODO: fix arbitrary_types_allowed error and set Material class type
13
+ crystal: Any = None
14
+
15
+
16
+ class PointDefectConfiguration(BaseDefectConfiguration, InMemoryEntity):
17
+ defect_type: PointDefectTypeEnum
18
+ position: Optional[List[float]] = [0, 0, 0] # fractional coordinates
19
+ site_id: Optional[int] = None
20
+ chemical_element: Optional[str] = None
21
+
22
+ def __init__(self, position=position, site_id=None, **data):
23
+ super().__init__(**data)
24
+ if site_id is not None:
25
+ self.position = self.crystal.coordinates_array[site_id]
26
+ else:
27
+ self.site_id = get_closest_site_id_from_position(self.crystal, position)
28
+
29
+ @classmethod
30
+ def from_site_id(cls, site_id: int, crystal: Material, **data):
31
+ if crystal:
32
+ position = crystal.coordinates_array[site_id]
33
+ else:
34
+ RuntimeError("Crystal is not defined")
35
+ return cls(crystal=crystal, position=position, site_id=site_id, **data)
36
+
37
+ @property
38
+ def _json(self):
39
+ return {
40
+ "type": "PointDefectConfiguration",
41
+ "defect_type": self.defect_type.name,
42
+ "position": self.position,
43
+ "chemical_element": self.chemical_element,
44
+ }
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+
4
+ class PointDefectTypeEnum(str, Enum):
5
+ VACANCY = "vacancy"
6
+ SUBSTITUTION = "substitution"
7
+ INTERSTITIAL = "interstitial"
@@ -54,7 +54,7 @@ class SimpleInterfaceBuilder(ConvertGeneratedItemsASEAtomsMixin, InterfaceBuilde
54
54
  Creates matching interface between substrate and film by straining the film to match the substrate.
55
55
  """
56
56
 
57
- _BuildParametersType = Optional[SimpleInterfaceBuilderParameters]
57
+ _BuildParametersType = SimpleInterfaceBuilderParameters
58
58
  _DefaultBuildParameters = SimpleInterfaceBuilderParameters(scale_film=True)
59
59
  _GeneratedItemType: type(ASEAtoms) = ASEAtoms # type: ignore
60
60
 
@@ -116,9 +116,9 @@ class StrainMatchingInterfaceBuilder(InterfaceBuilder):
116
116
  updated_material = super()._update_material_name(material, configuration)
117
117
  if StrainModes.mean_abs_strain in material.metadata:
118
118
  strain = material.metadata[StrainModes.mean_abs_strain]
119
- new_name = f"{updated_material.name}, Strain {strain*100:.3f}%"
119
+ new_name = f"{updated_material.name}, Strain {strain*100:.3f}pct"
120
120
  updated_material.name = new_name
121
- return material
121
+ return updated_material
122
122
 
123
123
 
124
124
  class ZSLStrainMatchingParameters(BaseModel):
@@ -1,3 +1,4 @@
1
+ from mat3ra.code.entity import InMemoryEntity
1
2
  from pydantic import BaseModel
2
3
 
3
4
  from .termination_pair import TerminationPair
@@ -5,7 +6,7 @@ from ..slab import Termination
5
6
  from ..slab.configuration import SlabConfiguration
6
7
 
7
8
 
8
- class InterfaceConfiguration(BaseModel):
9
+ class InterfaceConfiguration(BaseModel, InMemoryEntity):
9
10
  film_configuration: SlabConfiguration
10
11
  substrate_configuration: SlabConfiguration
11
12
  film_termination: Termination
@@ -16,3 +17,15 @@ class InterfaceConfiguration(BaseModel):
16
17
  @property
17
18
  def termination_pair(self):
18
19
  return TerminationPair(self.film_termination, self.substrate_termination)
20
+
21
+ @property
22
+ def _json(self):
23
+ return {
24
+ "type": "InterfaceConfiguration",
25
+ "film_configuration": self.film_configuration.to_json(),
26
+ "substrate_configuration": self.substrate_configuration.to_json(),
27
+ "film_termination": str(self.film_termination),
28
+ "substrate_termination": str(self.substrate_termination),
29
+ "distance_z": self.distance_z,
30
+ "vacuum": self.vacuum,
31
+ }
@@ -1,8 +1,11 @@
1
1
  import types
2
2
  from typing import List
3
3
  import numpy as np
4
+ from mat3ra.made.material import Material
5
+
6
+ from ...modify import filter_by_label
7
+ from ...convert import PymatgenInterface, INTERFACE_LABELS_MAP
4
8
  from .enums import StrainModes
5
- from ...convert import PymatgenInterface
6
9
 
7
10
 
8
11
  def interface_patch_with_mean_abs_strain(target: PymatgenInterface, tolerance: float = 10e-6):
@@ -34,3 +37,10 @@ def remove_duplicate_interfaces(
34
37
  if not any(are_interfaces_duplicate(interface, unique_interface) for unique_interface in filtered_interfaces):
35
38
  filtered_interfaces.append(interface)
36
39
  return filtered_interfaces
40
+
41
+
42
+ def get_slab(interface: Material, part: str = "film"):
43
+ try:
44
+ return filter_by_label(interface, INTERFACE_LABELS_MAP[part])
45
+ except ValueError:
46
+ raise ValueError(f"Material does not contain label for {part}.")
@@ -1,4 +1,6 @@
1
1
  from typing import List, Tuple, Any
2
+
3
+ from mat3ra.code.entity import InMemoryEntity
2
4
  from pymatgen.symmetry.analyzer import SpacegroupAnalyzer as PymatgenSpacegroupAnalyzer
3
5
  from pydantic import BaseModel
4
6
 
@@ -6,7 +8,7 @@ from mat3ra.made.material import Material
6
8
  from ...convert import to_pymatgen, from_pymatgen
7
9
 
8
10
 
9
- class SlabConfiguration(BaseModel):
11
+ class SlabConfiguration(BaseModel, InMemoryEntity):
10
12
  """
11
13
  Configuration for building a slab.
12
14
 
@@ -54,3 +56,16 @@ class SlabConfiguration(BaseModel):
54
56
  xy_supercell_matrix=xy_supercell_matrix,
55
57
  use_orthogonal_z=use_orthogonal_z,
56
58
  )
59
+
60
+ @property
61
+ def _json(self):
62
+ return {
63
+ "type": "SlabConfiguration",
64
+ "bulk": self.bulk.to_json(),
65
+ "miller_indices": self.miller_indices,
66
+ "thickness": self.thickness,
67
+ "vacuum": self.vacuum,
68
+ "xy_supercell_matrix": self.xy_supercell_matrix,
69
+ "use_conventional_cell": self.use_conventional_cell,
70
+ "use_orthogonal_z": self.use_orthogonal_z,
71
+ }
@@ -1,7 +1,12 @@
1
+ from typing import Optional
2
+
1
3
  from ase import Atoms
2
4
  from ase.calculators.calculator import Calculator
5
+ from ase.calculators.emt import EMT
3
6
 
7
+ from ..material import Material
4
8
  from .analyze import get_surface_area
9
+ from .build.interface.utils import get_slab
5
10
  from .convert import decorator_convert_material_args_kwargs_to_atoms
6
11
 
7
12
 
@@ -57,56 +62,67 @@ def calculate_surface_energy(slab: Atoms, bulk: Atoms, calculator: Calculator):
57
62
 
58
63
 
59
64
  @decorator_convert_material_args_kwargs_to_atoms
60
- def calculate_adhesion_energy(interface: Atoms, substrate_slab: Atoms, layer_slab: Atoms, calculator: Calculator):
65
+ def calculate_adhesion_energy(interface: Atoms, substrate_slab: Atoms, film_slab: Atoms, calculator: Calculator):
61
66
  """
62
67
  Calculate the adhesion energy.
63
68
  The adhesion energy is the difference between the energy of the interface and
64
- the sum of the energies of the substrate and layer.
69
+ the sum of the energies of the substrate and film.
65
70
  According to: 10.1088/0953-8984/27/30/305004
66
71
 
67
72
  Args:
68
73
  interface (ase.Atoms): The interface Atoms object to calculate the adhesion energy of.
69
74
  substrate_slab (ase.Atoms): The substrate slab Atoms object to calculate the adhesion energy of.
70
- layer_slab (ase.Atoms): The layer 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.
71
76
  calculator (ase.calculators.calculator.Calculator): The calculator to use for the energy calculation.
72
77
 
73
78
  Returns:
74
79
  float: The adhesion energy of the interface.
75
80
  """
76
81
  energy_substrate_slab = calculate_total_energy(substrate_slab, calculator)
77
- energy_layer_slab = calculate_total_energy(layer_slab, calculator)
82
+ energy_film_slab = calculate_total_energy(film_slab, calculator)
78
83
  energy_interface = calculate_total_energy(interface, calculator)
79
84
  area = get_surface_area(interface)
80
- return (energy_substrate_slab + energy_layer_slab - energy_interface) / area
85
+ return (energy_substrate_slab + energy_film_slab - energy_interface) / area
81
86
 
82
87
 
83
- @decorator_convert_material_args_kwargs_to_atoms
84
88
  def calculate_interfacial_energy(
85
- interface: Atoms,
86
- substrate_slab: Atoms,
87
- substrate_bulk: Atoms,
88
- layer_slab: Atoms,
89
- layer_bulk: Atoms,
90
- calculator: Calculator,
89
+ interface: Material,
90
+ substrate_slab: Optional[Material] = None,
91
+ substrate_bulk: Optional[Material] = None,
92
+ film_slab: Optional[Material] = None,
93
+ film_bulk: Optional[Material] = None,
94
+ calculator: Calculator = EMT(),
91
95
  ):
92
96
  """
93
97
  Calculate the interfacial energy.
94
- The interfacial energy is the sum of the surface energies of the substrate and layer minus the adhesion energy.
98
+ The interfacial energy is the sum of the surface energies of the substrate and film minus the adhesion energy.
95
99
  According to Dupré's formula
96
100
 
97
101
  Args:
98
- interface (ase.Atoms): The interface Atoms object to calculate the interfacial energy of.
99
- substrate_slab (ase.Atoms): The substrate slab Atoms object to calculate the interfacial energy of.
100
- substrate_bulk (ase.Atoms): The substrate bulk Atoms object to calculate the interfacial energy of.
101
- layer_slab (ase.Atoms): The layer slab Atoms object to calculate the interfacial energy of.
102
- layer_bulk (ase.Atoms): The layer bulk Atoms object to calculate the interfacial energy of.
103
- calculator (ase.calculators.calculator.Calculator): The calculator to use for the energy calculation.
102
+ interface (Material): The interface Material object to calculate the interfacial energy of.
103
+ substrate_slab (Material): The substrate slab Material object to calculate the interfacial energy of.
104
+ substrate_bulk (Material): The substrate bulk Material object to calculate the interfacial energy of.
105
+ film_slab (Material): The film slab Material object to calculate the interfacial energy of.
106
+ film_bulk (Material): The film bulk Material object to calculate the interfacial energy of.
107
+ calculator (ase.calculators.calculator.Calculator): The calculator to use for the energy calculation
104
108
 
105
109
  Returns:
106
110
  float: The interfacial energy of the interface.
107
111
  """
108
-
112
+ substrate_slab = get_slab(interface, part="substrate") if substrate_slab is None else substrate_slab
113
+ film_slab = get_slab(interface, part="film") if film_slab is None else film_slab
114
+
115
+ build_configuration = interface.metadata["build"]["configuration"] if "build" in interface.metadata else {}
116
+ try:
117
+ substrate_bulk = (
118
+ Material(build_configuration["substrate_configuration"]["bulk"])
119
+ if substrate_bulk is None
120
+ else substrate_bulk
121
+ )
122
+ film_bulk = Material(build_configuration["film_configuration"]["bulk"]) if film_bulk is None else film_bulk
123
+ except KeyError:
124
+ raise ValueError("The substrate and film bulk materials must be provided or defined in the interface metadata.")
109
125
  surface_energy_substrate = calculate_surface_energy(substrate_slab, substrate_bulk, calculator)
110
- surface_energy_layer = calculate_surface_energy(layer_slab, layer_bulk, calculator)
111
- adhesion_energy = calculate_adhesion_energy(interface, substrate_slab, layer_slab, calculator)
112
- return surface_energy_layer + surface_energy_substrate - adhesion_energy
126
+ surface_energy_film = calculate_surface_energy(film_slab, film_bulk, calculator)
127
+ adhesion_energy = calculate_adhesion_energy(interface, substrate_slab, film_slab, calculator)
128
+ return surface_energy_film + surface_energy_substrate - adhesion_energy
@@ -1,27 +1,29 @@
1
1
  import inspect
2
- import json
3
2
  from functools import wraps
4
3
  from typing import Any, Callable, Dict, Union
5
4
 
6
- from ase import Atoms
5
+ from mat3ra.made.material import Material
6
+ from mat3ra.made.utils import map_array_with_id_value_to_array
7
7
  from mat3ra.utils.mixins import RoundNumericValuesMixin
8
- from mat3ra.utils.object import NumpyNDArrayRoundEncoder
9
- from pymatgen.core.interface import Interface, label_termination
10
- from pymatgen.core.structure import Lattice, Structure
11
- from pymatgen.core.surface import Slab
12
8
  from pymatgen.io.ase import AseAtomsAdaptor
13
9
  from pymatgen.io.vasp.inputs import Poscar
14
10
 
15
- from ..material import Material
11
+ from .utils import (
12
+ INTERFACE_LABELS_MAP,
13
+ ASEAtoms,
14
+ PymatgenInterface,
15
+ PymatgenLattice,
16
+ PymatgenSlab,
17
+ PymatgenStructure,
18
+ extract_labels_from_pymatgen_structure,
19
+ extract_metadata_from_pymatgen_structure,
20
+ extract_tags_from_ase_atoms,
21
+ label_pymatgen_slab_termination,
22
+ map_array_to_array_with_id_value,
23
+ )
16
24
 
17
- PymatgenStructure = Structure
18
- PymatgenSlab = Slab
19
- PymatgenInterface = Interface
20
- ASEAtoms = Atoms
21
- label_pymatgen_slab_termination = label_termination
22
25
 
23
-
24
- def to_pymatgen(material_or_material_data: Union[Material, Dict[str, Any]]) -> Structure:
26
+ def to_pymatgen(material_or_material_data: Union[Material, Dict[str, Any]]) -> PymatgenStructure:
25
27
  """
26
28
  Converts material object in ESSE format to a pymatgen Structure object.
27
29
 
@@ -43,27 +45,28 @@ def to_pymatgen(material_or_material_data: Union[Material, Dict[str, Any]]) -> S
43
45
  alpha = lattice_params["alpha"]
44
46
  beta = lattice_params["beta"]
45
47
  gamma = lattice_params["gamma"]
46
- lattice = Lattice.from_parameters(a, b, c, alpha, beta, gamma)
48
+ lattice = PymatgenLattice.from_parameters(a, b, c, alpha, beta, gamma)
47
49
 
48
50
  basis = material_data["basis"]
49
51
  elements = [element["value"] for element in basis["elements"]]
50
52
  coordinates = [coord["value"] for coord in basis["coordinates"]]
51
53
  labels = [label["value"] for label in basis.get("labels", [])]
52
-
53
54
  # Assuming that the basis units are fractional since it's a crystal basis
54
55
  coords_are_cartesian = "units" in basis and basis["units"].lower() == "angstrom"
55
56
 
56
- if "labels" in inspect.signature(Structure.__init__).parameters:
57
- structure = Structure(lattice, elements, coordinates, coords_are_cartesian=coords_are_cartesian, labels=labels)
57
+ if "labels" in inspect.signature(PymatgenStructure.__init__).parameters:
58
+ structure = PymatgenStructure(
59
+ lattice, elements, coordinates, coords_are_cartesian=coords_are_cartesian, labels=labels
60
+ )
58
61
  else:
59
62
  # Passing labels does not work for pymatgen `2023.6.23` supporting py3.8
60
63
  print(f"labels: {labels}. Not passing labels to pymatgen.")
61
- structure = Structure(lattice, elements, coordinates, coords_are_cartesian=coords_are_cartesian)
64
+ structure = PymatgenStructure(lattice, elements, coordinates, coords_are_cartesian=coords_are_cartesian)
62
65
 
63
66
  return structure
64
67
 
65
68
 
66
- def from_pymatgen(structure: Union[Structure, Interface]) -> Dict[str, Any]:
69
+ def from_pymatgen(structure: Union[PymatgenStructure, PymatgenInterface]) -> Dict[str, Any]:
67
70
  """
68
71
  Converts a pymatgen Structure object to a material object in ESSE format.
69
72
 
@@ -102,18 +105,14 @@ def from_pymatgen(structure: Union[Structure, Interface]) -> Dict[str, Any]:
102
105
  },
103
106
  }
104
107
 
105
- metadata = {"boundaryConditions": {"type": "pbc", "offset": 0}}
108
+ metadata = {
109
+ **extract_metadata_from_pymatgen_structure(structure),
110
+ "boundaryConditions": {"type": "pbc", "offset": 0},
111
+ }
106
112
 
107
- # TODO: consider using Interface JSONSchema from ESSE when such created and adapt interface_properties accordingly.
108
- # Add interface properties to metadata according to pymatgen Interface as a JSON object
109
- if hasattr(structure, "interface_properties"):
110
- interface_props = structure.interface_properties
111
- # TODO: figure out how to round the values and stringify terminations tuple
112
- # in the interface properties with Encoder
113
- for key, value in interface_props.items():
114
- if isinstance(value, tuple):
115
- interface_props[key] = str(value)
116
- metadata["interface_properties"] = json.loads(json.dumps(interface_props, cls=NumpyNDArrayRoundEncoder))
113
+ basis["labels"] = map_array_to_array_with_id_value(
114
+ extract_labels_from_pymatgen_structure(structure), remove_none=True
115
+ )
117
116
 
118
117
  material_data = {
119
118
  "name": structure.formula,
@@ -157,11 +156,11 @@ def from_poscar(poscar: str) -> Dict[str, Any]:
157
156
  Returns:
158
157
  dict: A dictionary containing the material information in ESSE format.
159
158
  """
160
- structure = Structure.from_str(poscar, "poscar")
159
+ structure = PymatgenStructure.from_str(poscar, "poscar")
161
160
  return from_pymatgen(structure)
162
161
 
163
162
 
164
- def to_ase(material_or_material_data: Union[Material, Dict[str, Any]]) -> Atoms:
163
+ def to_ase(material_or_material_data: Union[Material, Dict[str, Any]]) -> ASEAtoms:
165
164
  """
166
165
  Converts material object in ESSE format to an ASE Atoms object.
167
166
 
@@ -171,12 +170,22 @@ def to_ase(material_or_material_data: Union[Material, Dict[str, Any]]) -> Atoms:
171
170
  Returns:
172
171
  Any: An ASE Atoms object.
173
172
  """
174
- # TODO: check that atomic labels are properly handled
175
- structure = to_pymatgen(material_or_material_data)
176
- return AseAtomsAdaptor.get_atoms(structure)
173
+ if isinstance(material_or_material_data, Material):
174
+ material_config = material_or_material_data.to_json()
175
+ else:
176
+ material_config = material_or_material_data
177
+ structure = to_pymatgen(material_config)
178
+ atoms = AseAtomsAdaptor.get_atoms(structure)
177
179
 
180
+ atomic_labels = material_config["basis"].get("labels", [])
181
+ if atomic_labels:
182
+ atoms.set_tags(map_array_with_id_value_to_array(atomic_labels))
183
+ if "metadata" in material_config:
184
+ atoms.info.update({"metadata": material_config["metadata"]})
185
+ return atoms
178
186
 
179
- def from_ase(ase_atoms: Atoms) -> Dict[str, Any]:
187
+
188
+ def from_ase(ase_atoms: ASEAtoms) -> Dict[str, Any]:
180
189
  """
181
190
  Converts an ASE Atoms object to a material object in ESSE format.
182
191
 
@@ -188,7 +197,13 @@ def from_ase(ase_atoms: Atoms) -> Dict[str, Any]:
188
197
  """
189
198
  # TODO: check that atomic labels/tags are properly handled
190
199
  structure = AseAtomsAdaptor.get_structure(ase_atoms)
191
- return from_pymatgen(structure)
200
+ material = from_pymatgen(structure)
201
+ ase_tags = extract_tags_from_ase_atoms(ase_atoms)
202
+ material["basis"]["labels"] = ase_tags
203
+ ase_metadata = ase_atoms.info.get("metadata", {})
204
+ if ase_metadata:
205
+ material["metadata"].update(ase_metadata)
206
+ return material
192
207
 
193
208
 
194
209
  def decorator_convert_material_args_kwargs_to_atoms(func: Callable) -> Callable:
@@ -230,8 +245,8 @@ def decorator_convert_material_args_kwargs_to_structure(func: Callable) -> Calla
230
245
 
231
246
 
232
247
  def convert_atoms_or_structure_to_material(item):
233
- if isinstance(item, Structure):
248
+ if isinstance(item, PymatgenStructure):
234
249
  return from_pymatgen(item)
235
- elif isinstance(item, Atoms):
250
+ elif isinstance(item, ASEAtoms):
236
251
  return from_ase(item)
237
252
  return item
@@ -0,0 +1,52 @@
1
+ import json
2
+ from typing import Any, Dict, List, Union
3
+
4
+ from ase import Atoms as ASEAtoms
5
+ from mat3ra.made.utils import map_array_to_array_with_id_value
6
+ 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
20
+
21
+ INTERFACE_LABELS_MAP = {"substrate": 0, "film": 1}
22
+
23
+
24
+ def extract_labels_from_pymatgen_structure(structure: PymatgenStructure) -> List[int]:
25
+ labels = []
26
+ if isinstance(structure, PymatgenInterface):
27
+ labels = list(map(lambda s: INTERFACE_LABELS_MAP[s.properties["interface_label"]], structure.sites))
28
+ return labels
29
+
30
+
31
+ def extract_metadata_from_pymatgen_structure(structure: PymatgenStructure) -> Dict[str, Any]:
32
+ metadata = {}
33
+ # TODO: consider using Interface JSONSchema from ESSE when such created and adapt interface_properties accordingly.
34
+ # Add interface properties to metadata according to pymatgen Interface as a JSON object
35
+ if hasattr(structure, "interface_properties"):
36
+ interface_props = structure.interface_properties
37
+ # TODO: figure out how to round the values and stringify terminations tuple
38
+ # in the interface properties with Encoder
39
+ for key, value in interface_props.items():
40
+ if isinstance(value, tuple):
41
+ interface_props[key] = str(value)
42
+ metadata["interface_properties"] = json.loads(json.dumps(interface_props, cls=NumpyNDArrayRoundEncoder))
43
+
44
+ return metadata
45
+
46
+
47
+ def extract_tags_from_ase_atoms(atoms: ASEAtoms) -> List[Union[str, int]]:
48
+ result = []
49
+ if "tags" in atoms.arrays:
50
+ int_tags = [int(tag) for tag in atoms.arrays["tags"] if tag is not None]
51
+ result = map_array_to_array_with_id_value(int_tags, remove_none=True)
52
+ return result
@@ -1,29 +1,32 @@
1
1
  from typing import Union
2
2
 
3
- from ase import Atoms
3
+ from mat3ra.made.material import Material
4
+ from mat3ra.made.utils import filter_array_with_id_value_by_ids, filter_array_with_id_value_by_values
4
5
  from pymatgen.analysis.structure_analyzer import SpacegroupAnalyzer
5
6
  from pymatgen.core.structure import Structure
6
7
 
7
- from .convert import (
8
- decorator_convert_material_args_kwargs_to_atoms,
9
- decorator_convert_material_args_kwargs_to_structure,
10
- )
8
+ from .convert import decorator_convert_material_args_kwargs_to_structure
11
9
  from .utils import translate_to_bottom_pymatgen_structure
12
10
 
13
11
 
14
- @decorator_convert_material_args_kwargs_to_atoms
15
- def filter_by_label(atoms: Atoms, label: Union[int, str]):
12
+ def filter_by_label(material: Material, label: Union[int, str]) -> Material:
16
13
  """
17
- Filter out only atoms corresponding to the label/tag.
14
+ Filter out only atoms corresponding to the label.
18
15
 
19
16
  Args:
20
- atoms (ase.Atoms): The Atoms object to filter.
17
+ material (Material): The material object to filter.
21
18
  label (int|str): The tag/label to filter by.
22
19
 
23
20
  Returns:
24
- ase.Atoms: The filtered Atoms object.
21
+ Material: The filtered material object.
25
22
  """
26
- return atoms[atoms.get_tags() == label]
23
+ new_material = material.clone()
24
+ labels = material.basis["labels"]
25
+ filtered_labels = filter_array_with_id_value_by_values(labels, label)
26
+ filtered_label_ids = [item["id"] for item in filtered_labels]
27
+ for key in ["coordinates", "elements", "labels"]:
28
+ new_material.basis[key] = filter_array_with_id_value_by_ids(new_material.basis[key], filtered_label_ids)
29
+ return new_material
27
30
 
28
31
 
29
32
  @decorator_convert_material_args_kwargs_to_structure
@@ -0,0 +1,41 @@
1
+ from typing import Any, Dict, List, Union
2
+
3
+ from mat3ra.utils.array import convert_to_array_if_not
4
+
5
+
6
+ # TODO: move to a more general location
7
+ def map_array_to_array_with_id_value(array: List[Any], remove_none: bool = False) -> List[Any]:
8
+ full_array = [{"id": i, "value": item} for i, item in enumerate(array)]
9
+ if remove_none:
10
+ return list(filter(lambda x: x["value"] is not None, full_array))
11
+ return full_array
12
+
13
+
14
+ def map_array_with_id_value_to_array(array: List[Dict[str, Any]]) -> List[Any]:
15
+ return [item["value"] for item in array]
16
+
17
+
18
+ def get_array_with_id_value_element_value_by_index(array: List[Dict[str, Any]], index: int = 0) -> List[Any]:
19
+ return map_array_with_id_value_to_array(array)[index]
20
+
21
+
22
+ def filter_array_with_id_value_by_values(
23
+ array: List[Dict[str, Any]], values: Union[List[Any], Any]
24
+ ) -> List[Dict[str, Any]]:
25
+ values = convert_to_array_if_not(values)
26
+ return [item for item in array if item["value"] in values]
27
+ # Alternative implementation:
28
+ # return list(filter(lambda x: x["value"] in values, array))
29
+
30
+
31
+ def filter_array_with_id_value_by_ids(
32
+ array: List[Dict[str, Any]], ids: Union[List[int], List[str], int, str]
33
+ ) -> List[Dict[str, Any]]:
34
+ int_ids = list(map(lambda i: int(i), convert_to_array_if_not(ids)))
35
+ return [item for item in array if item["id"] in int_ids]
36
+ # Alternative implementation:
37
+ # return list(filter(lambda x: x["id"] in ids, array))
38
+
39
+
40
+ def are_arrays_equal_by_id_value(array1: List[Dict[str, Any]], array2: List[Dict[str, Any]]) -> bool:
41
+ return map_array_with_id_value_to_array(array1) == map_array_with_id_value_to_array(array2)
@@ -56,9 +56,60 @@ INTERFACE_PROPERTIES_JSON = {
56
56
 
57
57
  # Add properties to interface structure
58
58
  INTERFACE_STRUCTURE.interface_properties = INTERFACE_PROPERTIES_MOCK
59
- INTERFACE_NAME = "Cu4(001)-Si8(001), Interface, Strain 0.062%"
59
+ INTERFACE_NAME = "Cu4(001)-Si8(001), Interface, Strain 0.062pct"
60
60
 
61
61
  # TODO: Use fixtures package when available
62
+ SI_CONVENTIONAL_CELL = {
63
+ "name": "Si8",
64
+ "basis": {
65
+ "elements": [
66
+ {"id": 0, "value": "Si"},
67
+ {"id": 1, "value": "Si"},
68
+ {"id": 2, "value": "Si"},
69
+ {"id": 3, "value": "Si"},
70
+ {"id": 4, "value": "Si"},
71
+ {"id": 5, "value": "Si"},
72
+ {"id": 6, "value": "Si"},
73
+ {"id": 7, "value": "Si"},
74
+ ],
75
+ "coordinates": [
76
+ {"id": 0, "value": [0.5, 0.0, 0.0]},
77
+ {"id": 1, "value": [0.25, 0.25, 0.75]},
78
+ {"id": 2, "value": [0.5, 0.5, 0.5]},
79
+ {"id": 3, "value": [0.25, 0.75, 0.25]},
80
+ {"id": 4, "value": [0.0, 0.0, 0.5]},
81
+ {"id": 5, "value": [0.75, 0.25, 0.25]},
82
+ {"id": 6, "value": [0.0, 0.5, 0.0]},
83
+ {"id": 7, "value": [0.75, 0.75, 0.75]},
84
+ ],
85
+ "units": "crystal",
86
+ "cell": [[5.468763846, 0.0, 0.0], [-0.0, 5.468763846, 0.0], [0.0, 0.0, 5.468763846]],
87
+ "constraints": [],
88
+ "labels": [],
89
+ },
90
+ "lattice": {
91
+ "a": 5.468763846,
92
+ "b": 5.468763846,
93
+ "c": 5.468763846,
94
+ "alpha": 90.0,
95
+ "beta": 90.0,
96
+ "gamma": 90.0,
97
+ "units": {"length": "angstrom", "angle": "degree"},
98
+ "type": "TRI",
99
+ "vectors": {
100
+ "a": [5.468763846, 0.0, 0.0],
101
+ "b": [-0.0, 5.468763846, 0.0],
102
+ "c": [0.0, 0.0, 5.468763846],
103
+ "alat": 1,
104
+ "units": "angstrom",
105
+ },
106
+ },
107
+ "isNonPeriodic": False,
108
+ "_id": "",
109
+ "metadata": {"boundaryConditions": {"type": "pbc", "offset": 0}},
110
+ "isUpdated": True,
111
+ }
112
+
62
113
  SI_SUPERCELL_2X2X1 = {
63
114
  "name": "Si8",
64
115
  "basis": {
@@ -85,6 +136,7 @@ SI_SUPERCELL_2X2X1 = {
85
136
  "units": "crystal",
86
137
  "cell": [[6.697840473, 0.0, 3.867], [2.232613491, 6.314784557, 3.867], [0.0, 0.0, 3.867]],
87
138
  "constraints": [],
139
+ "labels": [],
88
140
  },
89
141
  "lattice": {
90
142
  "a": 7.734,
@@ -109,6 +161,19 @@ SI_SUPERCELL_2X2X1 = {
109
161
  "isUpdated": True,
110
162
  }
111
163
 
164
+
165
+ SI_SLAB_CONFIGURATION = {
166
+ "type": "SlabConfiguration",
167
+ "bulk": SI_CONVENTIONAL_CELL,
168
+ "miller_indices": (0, 0, 1),
169
+ "thickness": 1,
170
+ "vacuum": 1,
171
+ "xy_supercell_matrix": [[1, 0], [0, 1]],
172
+ "use_conventional_cell": True,
173
+ "use_orthogonal_z": True,
174
+ }
175
+
176
+
112
177
  SI_SLAB = {
113
178
  "name": "Si8(001), termination Si_P4/mmm_1, Slab",
114
179
  "basis": {
@@ -127,6 +192,7 @@ SI_SLAB = {
127
192
  "units": "crystal",
128
193
  "cell": [[3.867, 0.0, 0.0], [0.0, 3.867, 0.0], [0.0, 0.0, 10.937527692]],
129
194
  "constraints": [],
195
+ "labels": [],
130
196
  },
131
197
  "lattice": {
132
198
  "a": 3.867,
@@ -150,6 +216,7 @@ SI_SLAB = {
150
216
  "metadata": {
151
217
  "boundaryConditions": {"type": "pbc", "offset": 0},
152
218
  "termination": "Si_P4/mmm_1",
219
+ "build": {"configuration": SI_SLAB_CONFIGURATION},
153
220
  },
154
221
  "isUpdated": True,
155
222
  }
@@ -0,0 +1,51 @@
1
+ from mat3ra.made.material import Material
2
+ from mat3ra.made.tools.build.defect import PointDefectBuilderParameters, PointDefectConfiguration, create_defect
3
+
4
+ clean_material = Material.create(Material.default_config)
5
+
6
+
7
+ def test_create_vacancy():
8
+ # vacancy in place of 0 element
9
+ configuration = PointDefectConfiguration(crystal=clean_material, defect_type="vacancy", site_id=0)
10
+ defect = create_defect(configuration)
11
+
12
+ assert len(defect.basis["elements"]) == 1
13
+
14
+
15
+ def test_create_substitution():
16
+ # Substitution of Ge in place of Si at default site_id=0
17
+ configuration = PointDefectConfiguration(crystal=clean_material, defect_type="substitution", chemical_element="Ge")
18
+ defect = create_defect(configuration)
19
+
20
+ assert defect.basis["elements"] == [{"id": 0, "value": "Ge"}, {"id": 1, "value": "Si"}]
21
+ assert defect.basis["coordinates"][0] == {"id": 0, "value": [0.0, 0.0, 0.0]}
22
+
23
+
24
+ def test_create_interstitial():
25
+ # Interstitial Ge at 0.5, 0.5, 0.5 position
26
+ configuration = PointDefectConfiguration(
27
+ crystal=clean_material, defect_type="interstitial", chemical_element="Ge", position=[0.5, 0.5, 0.5]
28
+ )
29
+ defect = create_defect(configuration)
30
+
31
+ assert defect.basis["elements"] == [
32
+ {"id": 0, "value": "Ge"},
33
+ {"id": 1, "value": "Si"},
34
+ {"id": 2, "value": "Si"},
35
+ ]
36
+
37
+
38
+ def test_create_defect_from_site_id():
39
+ # Substitution of Ge in place of Si at site_id=1
40
+ defect_configuration = PointDefectConfiguration.from_site_id(
41
+ crystal=clean_material, defect_type="substitution", chemical_element="Ge", site_id=1
42
+ )
43
+ defect_builder_parameters = PointDefectBuilderParameters(center_defect=False)
44
+ material_with_defect = create_defect(
45
+ builder_parameters=defect_builder_parameters, configuration=defect_configuration
46
+ )
47
+
48
+ assert material_with_defect.basis["elements"] == [
49
+ {"id": 0, "value": "Si"},
50
+ {"id": 1, "value": "Ge"},
51
+ ]
@@ -18,4 +18,4 @@ def test_build_slab():
18
18
  )
19
19
  termination = get_terminations(slab_config)[0]
20
20
  slab = create_slab(slab_config, termination)
21
- assertion_utils.assert_deep_almost_equal(slab.to_json(), SI_SLAB)
21
+ assertion_utils.assert_deep_almost_equal(SI_SLAB, slab.to_json())
@@ -1,6 +1,7 @@
1
1
  import numpy as np
2
2
  from ase.build import add_adsorbate, bulk, fcc111, graphene, surface
3
3
  from ase.calculators import emt
4
+ from mat3ra.made.material import Material
4
5
  from mat3ra.made.tools.calculate import (
5
6
  calculate_adhesion_energy,
6
7
  calculate_interfacial_energy,
@@ -8,6 +9,7 @@ from mat3ra.made.tools.calculate import (
8
9
  calculate_total_energy,
9
10
  calculate_total_energy_per_atom,
10
11
  )
12
+ from mat3ra.made.tools.convert import from_ase
11
13
 
12
14
  # Interface and its constituents structures setup
13
15
  nickel_slab = fcc111("Ni", size=(2, 2, 3), vacuum=10, a=3.52)
@@ -16,14 +18,15 @@ graphene_layer.cell = nickel_slab.cell
16
18
  interface = nickel_slab.copy()
17
19
  add_adsorbate(interface, graphene_layer, height=2, position="ontop")
18
20
 
19
- # Assign calculators
20
- calculator = emt.EMT()
21
- nickel_slab.set_calculator(calculator)
22
- graphene_layer.set_calculator(calculator)
23
- interface.set_calculator(calculator)
21
+ # Material objects setup
22
+ interface_material = Material(from_ase(interface))
23
+ nickel_slab_material = Material(from_ase(nickel_slab))
24
+ nickel_bulk_material = Material(from_ase(bulk("Ni", "fcc", a=3.52)))
25
+ graphene_layer_material = Material(from_ase(graphene_layer))
26
+ graphene_bulk_material = graphene_layer
24
27
 
25
- nickel_bulk = bulk("Ni", "fcc", a=3.52)
26
- graphene_bulk = graphene_layer
28
+ # Calculator setup
29
+ calculator = emt.EMT()
27
30
 
28
31
 
29
32
  def test_calculate_total_energy():
@@ -56,7 +59,12 @@ def test_calculate_adhesion_energy():
56
59
 
57
60
  def test_calculate_interfacial_energy():
58
61
  interfacial_energy = calculate_interfacial_energy(
59
- interface, nickel_slab, nickel_bulk, graphene_layer, graphene_bulk, calculator
62
+ interface_material,
63
+ nickel_slab_material,
64
+ nickel_bulk_material,
65
+ graphene_layer,
66
+ graphene_bulk_material,
67
+ calculator,
60
68
  )
61
69
  assert np.isclose(
62
70
  interfacial_energy,
@@ -56,6 +56,7 @@ def test_from_poscar():
56
56
 
57
57
  def test_to_ase():
58
58
  material = Material.create(Material.default_config)
59
+ material.basis["labels"] = [{"id": 0, "value": 0}, {"id": 1, "value": 1}]
59
60
  ase_atoms = to_ase(material)
60
61
  assert isinstance(ase_atoms, Atoms)
61
62
  assert np.allclose(
@@ -68,10 +69,13 @@ def test_to_ase():
68
69
  )
69
70
  assert ase_atoms.get_chemical_symbols() == ["Si", "Si"]
70
71
  assert np.allclose(ase_atoms.get_scaled_positions().tolist(), [[0.0, 0.0, 0.0], [0.25, 0.25, 0.25]])
72
+ assert ase_atoms.get_tags().tolist() == [0, 1]
71
73
 
72
74
 
73
75
  def test_from_ase():
74
76
  ase_atoms = bulk("Si")
77
+ ase_atoms.set_tags([0, 1])
75
78
  material_data = from_ase(ase_atoms)
76
79
  assert material_data["lattice"]["a"] == 3.839589822
77
80
  assert material_data["lattice"]["alpha"] == 60
81
+ assert material_data["basis"]["labels"] == [{"id": 0, "value": 0}, {"id": 1, "value": 1}]
@@ -1,4 +1,6 @@
1
1
  from ase.build import bulk
2
+ from mat3ra.made.material import Material
3
+ from mat3ra.made.tools.convert import from_ase
2
4
  from mat3ra.made.tools.modify import filter_by_label
3
5
 
4
6
 
@@ -7,5 +9,11 @@ def test_filter_by_label():
7
9
  film = bulk("Cu", cubic=True)
8
10
  interface_atoms = substrate + film
9
11
  interface_atoms.set_tags([1] * len(substrate) + [2] * len(film))
10
- film_extracted = filter_by_label(interface_atoms, 2)
11
- assert (film.symbols == film_extracted.symbols).all()
12
+ material_interface = Material(from_ase(interface_atoms))
13
+ film_extracted = filter_by_label(material_interface, 2)
14
+ film_material = Material(from_ase(film))
15
+
16
+ # Ids of filtered elements will be missing, comparing the resulting values
17
+ assert [el["value"] for el in film_material.basis["elements"]] == [
18
+ el["value"] for el in film_extracted.basis["elements"]
19
+ ]