@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mat3ra/made",
3
- "version": "2026.4.2-0",
3
+ "version": "2026.5.7-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",
@@ -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=material_to_use,
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)],