@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 +1 -1
- package/.pre-commit-config.yaml +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -0
- package/src/py/mat3ra/made/material.py +5 -0
- package/src/py/mat3ra/made/tools/analyze.py +20 -0
- package/src/py/mat3ra/made/tools/build/__init__.py +6 -1
- package/src/py/mat3ra/made/tools/build/defect/__init__.py +36 -0
- package/src/py/mat3ra/made/tools/build/defect/builders.py +69 -0
- package/src/py/mat3ra/made/tools/build/defect/configuration.py +44 -0
- package/src/py/mat3ra/made/tools/build/defect/enums.py +7 -0
- package/src/py/mat3ra/made/tools/build/interface/builders.py +3 -3
- package/src/py/mat3ra/made/tools/build/interface/configuration.py +14 -1
- package/src/py/mat3ra/made/tools/build/interface/utils.py +11 -1
- package/src/py/mat3ra/made/tools/build/slab/configuration.py +16 -1
- package/src/py/mat3ra/made/tools/calculate.py +39 -23
- package/src/py/mat3ra/made/tools/{convert.py → convert/__init__.py} +55 -40
- package/src/py/mat3ra/made/tools/convert/utils.py +52 -0
- package/src/py/mat3ra/made/tools/modify.py +14 -11
- package/src/py/mat3ra/made/utils.py +41 -0
- package/tests/py/unit/fixtures.py +68 -1
- package/tests/py/unit/test_tools_build_defect.py +51 -0
- package/tests/py/unit/test_tools_build_slab.py +1 -1
- package/tests/py/unit/test_tools_calculate.py +16 -8
- package/tests/py/unit/test_tools_convert.py +4 -0
- package/tests/py/unit/test_tools_modify.py +10 -2
package/.husky/pre-commit
CHANGED
package/.pre-commit-config.yaml
CHANGED
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
|
@@ -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 =
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 +
|
|
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:
|
|
86
|
-
substrate_slab:
|
|
87
|
-
substrate_bulk:
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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 (
|
|
99
|
-
substrate_slab (
|
|
100
|
-
substrate_bulk (
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
111
|
-
adhesion_energy = calculate_adhesion_energy(interface, substrate_slab,
|
|
112
|
-
return
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
57
|
-
structure =
|
|
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 =
|
|
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[
|
|
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 = {
|
|
108
|
+
metadata = {
|
|
109
|
+
**extract_metadata_from_pymatgen_structure(structure),
|
|
110
|
+
"boundaryConditions": {"type": "pbc", "offset": 0},
|
|
111
|
+
}
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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 =
|
|
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]]) ->
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
248
|
+
if isinstance(item, PymatgenStructure):
|
|
234
249
|
return from_pymatgen(item)
|
|
235
|
-
elif isinstance(item,
|
|
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
|
|
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
|
-
|
|
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
|
|
14
|
+
Filter out only atoms corresponding to the label.
|
|
18
15
|
|
|
19
16
|
Args:
|
|
20
|
-
|
|
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
|
-
|
|
21
|
+
Material: The filtered material object.
|
|
25
22
|
"""
|
|
26
|
-
|
|
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.
|
|
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
|
+
]
|
|
@@ -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
|
-
#
|
|
20
|
-
|
|
21
|
-
nickel_slab
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
+
]
|