@mat3ra/made 2026.4.2-0 → 2026.5.7-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/py/mat3ra/made/lattice.py +14 -0
- package/src/py/mat3ra/made/material.py +6 -1
- package/src/py/mat3ra/made/tools/build/pristine_structures/two_dimensional/slab/helpers.py +1 -9
- package/tests/py/unit/test_lattice.py +25 -0
- package/tests/py/unit/test_material.py +10 -0
- package/tests/py/unit/test_tools_build_interface.py +27 -0
package/package.json
CHANGED
|
@@ -128,6 +128,20 @@ class Lattice(RoundNumericValuesMixin, LatticeSchemaVectorless, InMemoryEntityPy
|
|
|
128
128
|
def cell_volume_rounded(self) -> float:
|
|
129
129
|
return self.vectors.volume_rounded
|
|
130
130
|
|
|
131
|
+
@property
|
|
132
|
+
def reciprocal_vectors(self):
|
|
133
|
+
return np.linalg.inv(np.array(self.vector_arrays, dtype=float)).T.tolist()
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def reciprocal_vector_norms(self) -> List[float]:
|
|
137
|
+
return [float(np.linalg.norm(vector)) for vector in self.reciprocal_vectors]
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def reciprocal_vector_ratios(self) -> List[float]:
|
|
141
|
+
norms = self.reciprocal_vector_norms
|
|
142
|
+
max_norm = max(norms)
|
|
143
|
+
return [round(float(value / max_norm), 3) for value in norms]
|
|
144
|
+
|
|
131
145
|
def get_hash_string(self, is_scaled: bool = False) -> str:
|
|
132
146
|
"""Mirrors JS Lattice.getHashString(isScaled). Rounds to HASH_TOLERANCE decimal places."""
|
|
133
147
|
scale = self.a if is_scaled else 1
|
|
@@ -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)
|
|
@@ -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,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
1
3
|
from mat3ra.code.vector import RoundedVector3D
|
|
2
4
|
from mat3ra.made.lattice import Lattice
|
|
3
5
|
from mat3ra.utils import assertion as assertion_utils
|
|
@@ -75,5 +77,28 @@ def test_lattice_get_scaled_by_matrix():
|
|
|
75
77
|
assertion_utils.assert_deep_almost_equal(lattice.vector_arrays, expected_vector_values)
|
|
76
78
|
|
|
77
79
|
|
|
80
|
+
def test_reciprocal_vectors():
|
|
81
|
+
lattice = Lattice(a=2.0, b=3.0, c=4.0)
|
|
82
|
+
expected_vectors = [[0.5, 0.0, 0.0], [0.0, 1 / 3, 0.0], [0.0, 0.0, 0.25]]
|
|
83
|
+
assertion_utils.assert_deep_almost_equal(lattice.reciprocal_vectors, expected_vectors)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_reciprocal_vector_norms():
|
|
87
|
+
lattice = Lattice(a=2.0, b=3.0, c=4.0)
|
|
88
|
+
expected_norms = [0.5, 1 / 3, 0.25]
|
|
89
|
+
assertion_utils.assert_deep_almost_equal(lattice.reciprocal_vector_norms, expected_norms)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.parametrize(
|
|
93
|
+
"lattice, expected",
|
|
94
|
+
[
|
|
95
|
+
(Lattice(a=2.0, b=3.0, c=4.0), [1.0, 0.667, 0.5]),
|
|
96
|
+
(Lattice(a=5.43, b=5.43, c=5.43), [1.0, 1.0, 1.0]),
|
|
97
|
+
],
|
|
98
|
+
)
|
|
99
|
+
def test_reciprocal_vector_ratios(lattice, expected):
|
|
100
|
+
assert lattice.reciprocal_vector_ratios == expected
|
|
101
|
+
|
|
102
|
+
|
|
78
103
|
# to test: create, calculate_vectors, from_vectors, get_lattice_type, clone
|
|
79
104
|
# from_vectors, to_dict, cell, cell_volume, scale_by_matrix, update_from_lattice,
|
|
@@ -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
|
|
@@ -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)],
|