@mat3ra/made 2026.4.2-1 → 2026.5.21-1
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/material.py +6 -1
- 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/pristine_structures/two_dimensional/slab/helpers.py +1 -9
- package/tests/py/unit/test_material.py +10 -0
- package/tests/py/unit/test_tools_analyze_interface.py +26 -0
- package/tests/py/unit/test_tools_build_interface.py +27 -0
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ from typing import Any, List, Optional, Union
|
|
|
4
4
|
from mat3ra.code.constants import AtomicCoordinateUnits, Units
|
|
5
5
|
from mat3ra.code.entity import HasDescriptionHasMetadataNamedDefaultableInMemoryEntityPydantic
|
|
6
6
|
from mat3ra.esse.models.material import MaterialSchema
|
|
7
|
-
from pydantic import ConfigDict, SkipValidation
|
|
7
|
+
from pydantic import ConfigDict, SkipValidation, computed_field, field_serializer
|
|
8
8
|
|
|
9
9
|
from .basis import Basis
|
|
10
10
|
from .lattice import Lattice
|
|
@@ -120,10 +120,15 @@ class Material(MaterialSchema, HasDescriptionHasMetadataNamedDefaultableInMemory
|
|
|
120
120
|
message = f"{self.basis.hash_string}#{self.lattice.get_hash_string(is_scaled)}#{salt}"
|
|
121
121
|
return hashlib.md5(message.encode()).hexdigest()
|
|
122
122
|
|
|
123
|
+
@computed_field
|
|
123
124
|
@property
|
|
124
125
|
def hash(self) -> str:
|
|
125
126
|
return self.calculate_hash()
|
|
126
127
|
|
|
128
|
+
@field_serializer("scaledHash")
|
|
129
|
+
def serialize_scaled_hash(self, _scaled_hash: Optional[str]) -> str:
|
|
130
|
+
return self.scaled_hash
|
|
131
|
+
|
|
127
132
|
@property
|
|
128
133
|
def scaled_hash(self) -> str:
|
|
129
134
|
return self.calculate_hash(is_scaled=True)
|
|
@@ -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
|
]
|
|
@@ -40,14 +40,6 @@ def create_slab(
|
|
|
40
40
|
Returns:
|
|
41
41
|
Material: The generated slab material.
|
|
42
42
|
"""
|
|
43
|
-
material_to_use = crystal
|
|
44
|
-
|
|
45
|
-
if use_conventional_cell:
|
|
46
|
-
crystal_lattice_planes_analyzer = CrystalLatticePlanesMaterialAnalyzer(
|
|
47
|
-
material=crystal, miller_indices=miller_indices
|
|
48
|
-
)
|
|
49
|
-
material_to_use = crystal_lattice_planes_analyzer.material_with_conventional_lattice
|
|
50
|
-
|
|
51
43
|
if termination_top is not None:
|
|
52
44
|
termination_top_formula = termination_top.formula
|
|
53
45
|
if termination_bottom is not None:
|
|
@@ -58,7 +50,7 @@ def create_slab(
|
|
|
58
50
|
use_orthogonal_c=use_orthogonal_c,
|
|
59
51
|
)
|
|
60
52
|
slab_configuration = SlabConfiguration.from_parameters(
|
|
61
|
-
material_or_dict=
|
|
53
|
+
material_or_dict=crystal,
|
|
62
54
|
miller_indices=miller_indices,
|
|
63
55
|
number_of_layers=number_of_layers,
|
|
64
56
|
termination_top_formula=termination_top_formula,
|
|
@@ -12,6 +12,8 @@ from unit.fixtures.slab import BULK_Si_CONVENTIONAL
|
|
|
12
12
|
from unit.utils import assert_two_entities_deep_almost_equal
|
|
13
13
|
|
|
14
14
|
FIXTURES_DIR = Path(__file__).parents[2] / "fixtures"
|
|
15
|
+
HASH_KEY = "hash"
|
|
16
|
+
SCALED_HASH_KEY = "scaledHash"
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
def load_fixture(name: str) -> dict:
|
|
@@ -136,3 +138,11 @@ def test_calculate_hash(fixture_file):
|
|
|
136
138
|
material = Material.create(fixture)
|
|
137
139
|
assert material.hash == fixture["hash"]
|
|
138
140
|
assert material.scaled_hash == fixture["scaledHash"]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_model_dump_includes_hashes():
|
|
144
|
+
material = Material.create_default()
|
|
145
|
+
serialized_material = material.model_dump()
|
|
146
|
+
|
|
147
|
+
assert serialized_material[HASH_KEY] == material.hash
|
|
148
|
+
assert serialized_material[SCALED_HASH_KEY] == material.scaled_hash
|
|
@@ -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]
|
|
@@ -5,6 +5,7 @@ import pytest
|
|
|
5
5
|
from mat3ra.esse.models.core.reusable.axis_enum import AxisEnum
|
|
6
6
|
from mat3ra.made.material import Material
|
|
7
7
|
from mat3ra.made.tools.analyze.interface.simple import InterfaceAnalyzer
|
|
8
|
+
from mat3ra.made.tools.analyze.lattice_planes import CrystalLatticePlanesMaterialAnalyzer
|
|
8
9
|
from mat3ra.made.tools.build import MaterialWithBuildMetadata
|
|
9
10
|
from mat3ra.made.tools.build.compound_pristine_structures.two_dimensional.interface.base.build_parameters import (
|
|
10
11
|
InterfaceBuilderParameters,
|
|
@@ -39,6 +40,9 @@ from .fixtures.interface.twisted_nanoribbons import TWISTED_INTERFACE_GRAPHENE_G
|
|
|
39
40
|
from .fixtures.monolayer import GRAPHENE
|
|
40
41
|
from .utils import OSPlatform, assert_two_entities_deep_almost_equal
|
|
41
42
|
|
|
43
|
+
HASH_KEY = "hash"
|
|
44
|
+
SCALED_HASH_KEY = "scaledHash"
|
|
45
|
+
|
|
42
46
|
Si_Ge_SIMPLE_INTERFACE_TEST_CASE = (
|
|
43
47
|
SimpleNamespace(
|
|
44
48
|
bulk_config=BULK_Si_CONVENTIONAL,
|
|
@@ -253,6 +257,29 @@ def test_commensurate_interface_creation(material_config, analyzer_params, direc
|
|
|
253
257
|
assert_two_entities_deep_almost_equal(interface, expected_interface, atol=PRECISION)
|
|
254
258
|
|
|
255
259
|
|
|
260
|
+
def test_create_slab_with_conventional_cell_stores_crystal_hashes_in_metadata():
|
|
261
|
+
miller_indices = (0, 0, 1)
|
|
262
|
+
material = Material.create(BULK_Ni_PRIMITIVE)
|
|
263
|
+
expected_crystal = CrystalLatticePlanesMaterialAnalyzer(
|
|
264
|
+
material=material, miller_indices=miller_indices
|
|
265
|
+
).material_with_conventional_lattice
|
|
266
|
+
|
|
267
|
+
slab = create_slab(
|
|
268
|
+
crystal=material,
|
|
269
|
+
miller_indices=miller_indices,
|
|
270
|
+
use_conventional_cell=True,
|
|
271
|
+
use_orthogonal_c=True,
|
|
272
|
+
number_of_layers=1,
|
|
273
|
+
vacuum=0.0,
|
|
274
|
+
)
|
|
275
|
+
serialized_slab = slab.model_dump()
|
|
276
|
+
crystal = serialized_slab["metadata"]["build"][-1]["configuration"]["stack_components"][0]["crystal"]
|
|
277
|
+
|
|
278
|
+
assert crystal[HASH_KEY] == expected_crystal.hash
|
|
279
|
+
assert crystal[SCALED_HASH_KEY] == expected_crystal.scaled_hash
|
|
280
|
+
assert "bulkId" not in serialized_slab["metadata"]
|
|
281
|
+
|
|
282
|
+
|
|
256
283
|
@pytest.mark.parametrize(
|
|
257
284
|
"interface_config, expected_coordinate_level",
|
|
258
285
|
[(GRAPHENE_NICKEL_INTERFACE_TOP_HCP, 12.048)],
|