@mat3ra/made 2026.5.7-0 → 2026.5.21-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/py/mat3ra/made/basis/__init__.py +4 -0
- package/src/py/mat3ra/made/tools/analyze/interface/__init__.py +2 -0
- package/src/py/mat3ra/made/tools/analyze/interface/utils/__init__.py +72 -0
- package/src/py/mat3ra/made/tools/build/defective_structures/zero_dimensional/point_defect/substitutional/configuration.py +12 -0
- package/src/py/mat3ra/made/tools/build_components/entities/auxiliary/zero_dimensional/point_defect_site/builder.py +1 -0
- package/src/py/mat3ra/made/tools/build_components/entities/auxiliary/zero_dimensional/point_defect_site/configuration.py +2 -1
- package/tests/py/unit/test_tools_analyze_interface.py +26 -0
- package/tests/py/unit/test_tools_build_defect/test_point_defect.py +33 -0
package/package.json
CHANGED
|
@@ -132,6 +132,7 @@ class Basis(BasisSchema, InMemoryEntityPydantic):
|
|
|
132
132
|
coordinate: Optional[List[float]] = None,
|
|
133
133
|
use_cartesian_coordinates: bool = False,
|
|
134
134
|
force: bool = False,
|
|
135
|
+
label: Optional[Union[int, str]] = None,
|
|
135
136
|
):
|
|
136
137
|
"""
|
|
137
138
|
Add an atom to the basis at a specified coordinate. Check that no other atom is overlapping with it.
|
|
@@ -141,6 +142,7 @@ class Basis(BasisSchema, InMemoryEntityPydantic):
|
|
|
141
142
|
coordinate (List[float]): Coordinate of the atom to be added.
|
|
142
143
|
use_cartesian_coordinates (bool): Whether the coordinate is in Cartesian units (or crystal by default).
|
|
143
144
|
force (bool): Whether to force adding the atom even if it overlaps with another atom.
|
|
145
|
+
label (int|str|None): Per-atom label when the basis already uses labels; omit if the basis has none.
|
|
144
146
|
"""
|
|
145
147
|
if coordinate is None:
|
|
146
148
|
coordinate = [0, 0, 0]
|
|
@@ -162,6 +164,8 @@ class Basis(BasisSchema, InMemoryEntityPydantic):
|
|
|
162
164
|
return
|
|
163
165
|
self.elements.add_item(element)
|
|
164
166
|
self.coordinates.add_item(coordinate)
|
|
167
|
+
if label is not None:
|
|
168
|
+
self.labels.add_item(label)
|
|
165
169
|
|
|
166
170
|
def add_atoms_from_another_basis(self, other_basis: "Basis"):
|
|
167
171
|
"""
|
|
@@ -4,6 +4,7 @@ from .simple import InterfaceAnalyzer
|
|
|
4
4
|
from .twisted_nanoribbons import TwistedNanoribbonsInterfaceAnalyzer
|
|
5
5
|
from .utils.holders import MatchedSubstrateFilmConfigurationHolder
|
|
6
6
|
from .zsl import ZSLInterfaceAnalyzer, ZSLMatchHolder
|
|
7
|
+
from .utils import calculate_interfacial_distance_from_rdf
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
9
10
|
"InterfaceAnalyzer",
|
|
@@ -15,4 +16,5 @@ __all__ = [
|
|
|
15
16
|
"GrainBoundaryPlanarMatchHolder",
|
|
16
17
|
"TwistedNanoribbonsInterfaceAnalyzer",
|
|
17
18
|
"MatchedSubstrateFilmConfigurationHolder",
|
|
19
|
+
"calculate_interfacial_distance_from_rdf",
|
|
18
20
|
]
|
|
@@ -1,5 +1,77 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
from mat3ra.made.material import Material
|
|
4
|
+
|
|
5
|
+
from ....build.pristine_structures.two_dimensional.slab import SlabConfiguration
|
|
6
|
+
from ....build_components.entities.reusable.three_dimensional.supercell.helpers import create_supercell
|
|
7
|
+
from ...rdf import RadialDistributionFunction
|
|
1
8
|
from .holders import MatchedSubstrateFilmConfigurationHolder
|
|
2
9
|
|
|
10
|
+
|
|
11
|
+
def calculate_interfacial_distance_from_rdf(
|
|
12
|
+
substrate_material: Union[Material, dict, "SlabConfiguration"],
|
|
13
|
+
film_material: Union[Material, dict, "SlabConfiguration"],
|
|
14
|
+
rdf_cutoff: float = 10.0,
|
|
15
|
+
rdf_bin_size: float = 0.1,
|
|
16
|
+
supercell_size: tuple = (3, 3, 3),
|
|
17
|
+
) -> float:
|
|
18
|
+
"""
|
|
19
|
+
Calculate interfacial distance based on RDF analysis of bulk materials.
|
|
20
|
+
|
|
21
|
+
Creates temporary supercells of substrate and film bulk materials,
|
|
22
|
+
calculates their RDFs to find the first peak (nearest neighbor distance),
|
|
23
|
+
and returns the average of these distances as the initial guess for interfacial distance.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
substrate_material: Material, dict, or SlabConfiguration for the substrate
|
|
27
|
+
film_material: Material, dict, or SlabConfiguration for the film
|
|
28
|
+
rdf_cutoff: Maximum distance for RDF calculation in Angstroms
|
|
29
|
+
rdf_bin_size: Bin size for RDF histogram in Angstroms
|
|
30
|
+
supercell_size: Size of supercell for RDF analysis (default: 3x3x3)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
float: Calculated interfacial distance in Angstroms
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
if isinstance(substrate_material, SlabConfiguration):
|
|
37
|
+
substrate_bulk = substrate_material.atomic_layers.crystal
|
|
38
|
+
elif isinstance(substrate_material, dict):
|
|
39
|
+
substrate_bulk = Material.create(substrate_material)
|
|
40
|
+
else:
|
|
41
|
+
substrate_bulk = substrate_material
|
|
42
|
+
|
|
43
|
+
if isinstance(film_material, SlabConfiguration):
|
|
44
|
+
film_bulk = film_material.atomic_layers.crystal
|
|
45
|
+
elif isinstance(film_material, dict):
|
|
46
|
+
film_bulk = Material.create(film_material)
|
|
47
|
+
else:
|
|
48
|
+
film_bulk = film_material
|
|
49
|
+
|
|
50
|
+
substrate_supercell = create_supercell(material=substrate_bulk, scaling_factor=list(supercell_size))
|
|
51
|
+
|
|
52
|
+
film_supercell = create_supercell(material=film_bulk, scaling_factor=list(supercell_size))
|
|
53
|
+
|
|
54
|
+
substrate_rdf = RadialDistributionFunction.from_material(
|
|
55
|
+
substrate_supercell,
|
|
56
|
+
cutoff=rdf_cutoff,
|
|
57
|
+
bin_size=rdf_bin_size,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
film_rdf = RadialDistributionFunction.from_material(
|
|
61
|
+
film_supercell,
|
|
62
|
+
cutoff=rdf_cutoff,
|
|
63
|
+
bin_size=rdf_bin_size,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
substrate_first_peak = substrate_rdf.first_peak_distance
|
|
67
|
+
film_first_peak = film_rdf.first_peak_distance
|
|
68
|
+
|
|
69
|
+
interfacial_distance = (substrate_first_peak + film_first_peak) / 2.0
|
|
70
|
+
|
|
71
|
+
return interfacial_distance
|
|
72
|
+
|
|
73
|
+
|
|
3
74
|
__all__ = [
|
|
4
75
|
"MatchedSubstrateFilmConfigurationHolder",
|
|
76
|
+
"calculate_interfacial_distance_from_rdf",
|
|
5
77
|
]
|
|
@@ -7,6 +7,7 @@ from mat3ra.esse.models.materials_category_components.entities.core.zero_dimensi
|
|
|
7
7
|
from mat3ra.esse.models.materials_category_components.operations.core.combinations.merge import MergeMethodsEnum
|
|
8
8
|
|
|
9
9
|
from mat3ra.made.material import Material
|
|
10
|
+
from mat3ra.made.tools.analyze.other import get_closest_site_id_from_coordinate
|
|
10
11
|
from ..base.configuration import PointDefectConfiguration
|
|
11
12
|
from ......build_components import MaterialWithBuildMetadata
|
|
12
13
|
from ......build_components.entities.auxiliary.zero_dimensional.point_defect_site.configuration import (
|
|
@@ -14,6 +15,16 @@ from ......build_components.entities.auxiliary.zero_dimensional.point_defect_sit
|
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
def _host_atom_label_at_coordinate(
|
|
19
|
+
crystal: Union[Material, MaterialWithBuildMetadata], coordinate: List[float]
|
|
20
|
+
) -> Union[int, str, None]:
|
|
21
|
+
host_labels = crystal.basis.labels.values
|
|
22
|
+
if not host_labels:
|
|
23
|
+
return None
|
|
24
|
+
site_index = get_closest_site_id_from_coordinate(crystal, coordinate)
|
|
25
|
+
return host_labels[site_index]
|
|
26
|
+
|
|
27
|
+
|
|
17
28
|
class SubstitutionalDefectConfiguration(PointDefectConfiguration, SubstitutionalPointDefectSchema):
|
|
18
29
|
type: str = "SubstitutionalDefectConfiguration"
|
|
19
30
|
|
|
@@ -25,5 +36,6 @@ class SubstitutionalDefectConfiguration(PointDefectConfiguration, Substitutional
|
|
|
25
36
|
crystal=crystal,
|
|
26
37
|
element=AtomSchema(chemical_element=element),
|
|
27
38
|
coordinate=coordinate,
|
|
39
|
+
host_atom_label=_host_atom_label_at_coordinate(crystal, coordinate),
|
|
28
40
|
)
|
|
29
41
|
return cls(merge_components=[crystal, substitution_site], merge_method=MergeMethodsEnum.REPLACE, **kwargs)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Union
|
|
1
|
+
from typing import Optional, Union
|
|
2
2
|
|
|
3
3
|
from mat3ra.esse.models.materials_category.defective_structures.two_dimensional.adatom.configuration import (
|
|
4
4
|
PointDefectSiteSchema,
|
|
@@ -11,3 +11,4 @@ from ..crystal_site import CrystalSite
|
|
|
11
11
|
|
|
12
12
|
class PointDefectSiteConfiguration(CrystalSite, PointDefectSiteSchema):
|
|
13
13
|
element: Union[VacancySchema, AtomSchema]
|
|
14
|
+
host_atom_label: Optional[Union[int, str]] = None
|
|
@@ -3,8 +3,10 @@ from typing import Final
|
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
import pytest
|
|
6
|
+
from mat3ra.made.material import Material
|
|
6
7
|
from mat3ra.made.tools.analyze.interface import InterfaceAnalyzer
|
|
7
8
|
from mat3ra.made.tools.analyze.interface.commensurate import CommensurateLatticeInterfaceAnalyzer
|
|
9
|
+
from mat3ra.made.tools.analyze.interface.utils import calculate_interfacial_distance_from_rdf
|
|
8
10
|
from mat3ra.made.tools.build.pristine_structures.two_dimensional.slab import SlabConfiguration
|
|
9
11
|
from unit.fixtures.bulk import BULK_GRAPHENE, BULK_Ge_CONVENTIONAL, BULK_Si_CONVENTIONAL
|
|
10
12
|
|
|
@@ -195,3 +197,27 @@ def test_optimal_supercell_functions(substrate, film, expected_n, expected_m):
|
|
|
195
197
|
|
|
196
198
|
assert optimal_n == expected_n
|
|
197
199
|
assert optimal_m == expected_m
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@pytest.mark.parametrize(
|
|
203
|
+
"substrate_config, film_config, expected_distance_range",
|
|
204
|
+
[
|
|
205
|
+
(BULK_Si_CONVENTIONAL, BULK_Si_CONVENTIONAL, (3.8, 3.9)),
|
|
206
|
+
(BULK_Si_CONVENTIONAL, BULK_Ge_CONVENTIONAL, (3.1, 3.2)),
|
|
207
|
+
],
|
|
208
|
+
)
|
|
209
|
+
def test_calculate_interfacial_distance_from_rdf(substrate_config, film_config, expected_distance_range):
|
|
210
|
+
"""Test RDF-based interfacial distance calculation with different material types."""
|
|
211
|
+
substrate_material = Material.create(substrate_config)
|
|
212
|
+
film_material = Material.create(film_config)
|
|
213
|
+
|
|
214
|
+
distance = calculate_interfacial_distance_from_rdf(
|
|
215
|
+
substrate_material=substrate_material,
|
|
216
|
+
film_material=film_material,
|
|
217
|
+
rdf_cutoff=10.0,
|
|
218
|
+
rdf_bin_size=0.1,
|
|
219
|
+
supercell_size=(3, 3, 3),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
assert isinstance(distance, float)
|
|
223
|
+
assert expected_distance_range[0] <= distance <= expected_distance_range[1]
|
|
@@ -16,6 +16,7 @@ from mat3ra.made.tools.build.defective_structures.zero_dimensional.point_defect.
|
|
|
16
16
|
)
|
|
17
17
|
from mat3ra.made.tools.build.defective_structures.zero_dimensional.point_defect.types import PointDefectDict
|
|
18
18
|
from unit.fixtures.bulk import BULK_Si_CONVENTIONAL, BULK_Si_PRIMITIVE
|
|
19
|
+
from unit.fixtures.interface.zsl import GRAPHENE_NICKEL_INTERFACE
|
|
19
20
|
from unit.fixtures.point_defects import (
|
|
20
21
|
INTERSTITIAL_DEFECT_BULK_PRIMITIVE_Si,
|
|
21
22
|
INTERSTITIAL_VORONOI_DEFECT_BULK_PRIMITIVE_Si,
|
|
@@ -25,6 +26,19 @@ from unit.fixtures.point_defects import (
|
|
|
25
26
|
)
|
|
26
27
|
from unit.utils import assert_two_entities_deep_almost_equal, get_platform_specific_value
|
|
27
28
|
|
|
29
|
+
FILM_LABEL = 1
|
|
30
|
+
SUBSTRATE_LABEL = 0
|
|
31
|
+
SUBSTITUTION_ELEMENT = "Ge"
|
|
32
|
+
INTERFACE_SUBSTITUTION_PLACEMENT_METHOD = SubstitutionPlacementMethodEnum.CLOSEST_SITE.value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _film_atom_coordinate(material: Material) -> list:
|
|
36
|
+
labels_by_id = {entry["id"]: entry["value"] for entry in material.basis.labels.to_dict()}
|
|
37
|
+
for coordinate_entry in material.basis.coordinates.to_dict():
|
|
38
|
+
if labels_by_id.get(coordinate_entry["id"]) == FILM_LABEL:
|
|
39
|
+
return coordinate_entry["value"]
|
|
40
|
+
raise ValueError("No film atom found in fixture material")
|
|
41
|
+
|
|
28
42
|
|
|
29
43
|
@pytest.mark.parametrize(
|
|
30
44
|
"material_config, defect_params, expected_material_config",
|
|
@@ -91,6 +105,25 @@ def test_point_defect_helpers(material_config, defect_params, expected_material_
|
|
|
91
105
|
assert_two_entities_deep_almost_equal(defect, expected_material_config)
|
|
92
106
|
|
|
93
107
|
|
|
108
|
+
@pytest.mark.parametrize(
|
|
109
|
+
"expected_label_count, expected_unique_labels",
|
|
110
|
+
[(5, {SUBSTRATE_LABEL, FILM_LABEL})],
|
|
111
|
+
)
|
|
112
|
+
def test_create_defect_point_substitution_preserves_interface_labels(
|
|
113
|
+
expected_label_count, expected_unique_labels
|
|
114
|
+
):
|
|
115
|
+
material = Material.create(GRAPHENE_NICKEL_INTERFACE)
|
|
116
|
+
coordinate = _film_atom_coordinate(material)
|
|
117
|
+
|
|
118
|
+
defect_material = create_defect_point_substitution(
|
|
119
|
+
material, coordinate, SUBSTITUTION_ELEMENT, INTERFACE_SUBSTITUTION_PLACEMENT_METHOD
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
assert len(defect_material.basis.elements.values) == expected_label_count
|
|
123
|
+
assert len(defect_material.basis.labels.values) == expected_label_count
|
|
124
|
+
assert set(defect_material.basis.labels.values) == expected_unique_labels
|
|
125
|
+
|
|
126
|
+
|
|
94
127
|
@pytest.mark.parametrize(
|
|
95
128
|
"material_config, defect_params_list, expected_material_config",
|
|
96
129
|
[
|