@mat3ra/made 2026.7.13-0 → 2026.7.18-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.7.13-0",
3
+ "version": "2026.7.18-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",
@@ -16,6 +16,8 @@ class BuildMetadataAnalyzer(BaseMaterialAnalyzer, Generic[TypeConfiguration, Typ
16
16
 
17
17
  @property
18
18
  def build_metadata(self) -> BuildMetadata:
19
+ if not self.material.metadata.build:
20
+ raise ValueError("No metadata.build on the material.")
19
21
  metadata = self.material.metadata.get_build_metadata_of_type(self.configuration_cls.__name__)
20
22
  if not metadata:
21
23
  raise ValueError(f"Build metadata for {self.configuration_cls.__name__} not found.")
@@ -0,0 +1,31 @@
1
+ from typing import Dict, Type
2
+
3
+ from ..build_components.metadata import MaterialWithBuildMetadata
4
+ from .build_metadata_analyzer import BuildMetadataAnalyzer
5
+ from .interface_material import (
6
+ InterfaceMaterialAnalyzer,
7
+ )
8
+ from .slab import SlabMaterialAnalyzer
9
+
10
+ BUILD_METADATA_ANALYZER_REGISTRY: Dict[str, Type[BuildMetadataAnalyzer]] = {
11
+ "SlabConfiguration": SlabMaterialAnalyzer,
12
+ "InterfaceConfiguration": InterfaceMaterialAnalyzer,
13
+ }
14
+
15
+
16
+ def create_build_metadata_analyzer(
17
+ material: MaterialWithBuildMetadata,
18
+ **analyzer_kwargs,
19
+ ) -> BuildMetadataAnalyzer:
20
+ build_metadata = material.metadata.get_latest_build_metadata()
21
+ configuration_type = build_metadata.configuration.get("type")
22
+ if not configuration_type:
23
+ raise ValueError("Latest build step has no configuration type.")
24
+ analyzer_cls = BUILD_METADATA_ANALYZER_REGISTRY.get(configuration_type)
25
+ if analyzer_cls is None:
26
+ registered_types = ", ".join(sorted(BUILD_METADATA_ANALYZER_REGISTRY))
27
+ raise ValueError(
28
+ f"No build metadata analyzer registered for '{configuration_type}'. "
29
+ f"Registered types: {registered_types}."
30
+ )
31
+ return analyzer_cls(material=material, **analyzer_kwargs)
@@ -0,0 +1,45 @@
1
+ from typing import Type, Union
2
+
3
+ from ..build.compound_pristine_structures.two_dimensional.interface.base.build_parameters import (
4
+ InterfaceBuilderParameters,
5
+ )
6
+ from ..build.compound_pristine_structures.two_dimensional.interface.base.configuration import InterfaceConfiguration
7
+ from ..build.pristine_structures.two_dimensional.slab.configuration import SlabConfiguration
8
+ from ..build.pristine_structures.two_dimensional.slab_strained_supercell.configuration import (
9
+ SlabStrainedSupercellConfiguration,
10
+ )
11
+ from ..build_components.metadata import MaterialWithBuildMetadata
12
+ from ..convert.interface_parts_enum import INTERFACE_LABELS_MAP
13
+ from .build_metadata_analyzer import BuildMetadataAnalyzer
14
+
15
+
16
+ class InterfaceMaterialAnalyzer(
17
+ BuildMetadataAnalyzer[InterfaceConfiguration, InterfaceBuilderParameters],
18
+ ):
19
+ configuration_cls: Type[InterfaceConfiguration] = InterfaceConfiguration
20
+ build_parameters_cls: Type[InterfaceBuilderParameters] = InterfaceBuilderParameters
21
+
22
+ @property
23
+ def substrate_slab_configuration(self) -> Union[SlabStrainedSupercellConfiguration, SlabConfiguration]:
24
+ return self.build_configuration.substrate_configuration
25
+
26
+ @property
27
+ def film_slab_configuration(self) -> Union[SlabStrainedSupercellConfiguration, SlabConfiguration]:
28
+ return self.build_configuration.film_configuration
29
+
30
+ def get_bulk_crystal(self, part: str = "substrate") -> dict:
31
+ if part not in INTERFACE_LABELS_MAP:
32
+ raise ValueError(f"Unknown interface part '{part}'. Use 'substrate' or 'film'.")
33
+ slab_configuration = self.substrate_slab_configuration if part == "substrate" else self.film_slab_configuration
34
+ crystal = slab_configuration.atomic_layers.crystal
35
+ if crystal is None:
36
+ raise ValueError(f"No bulk crystal for {part} in interface build metadata.")
37
+ return crystal if isinstance(crystal, dict) else crystal.to_dict()
38
+
39
+
40
+ def get_interface_configuration(interface: MaterialWithBuildMetadata) -> InterfaceConfiguration:
41
+ return InterfaceMaterialAnalyzer(material=interface).build_configuration
42
+
43
+
44
+ def get_interface_bulk_crystal(interface: MaterialWithBuildMetadata, part: str = "substrate") -> dict:
45
+ return InterfaceMaterialAnalyzer(material=interface).get_bulk_crystal(part=part)
@@ -13,7 +13,11 @@ from .twisted import (
13
13
  TwistedNanoribbonsInterfaceConfiguration,
14
14
  create_interface_twisted,
15
15
  )
16
- from .utils import get_optimal_film_displacement, get_slab, remove_duplicate_interfaces
16
+ from .utils import (
17
+ get_optimal_film_displacement,
18
+ get_slab,
19
+ remove_duplicate_interfaces,
20
+ )
17
21
  from .zsl import (
18
22
  create_interface_zsl,
19
23
  create_interface_zsl_between_slabs,
@@ -10,6 +10,11 @@ class MaterialBuildMetadata(BaseMetadata):
10
10
  # Other metadata fields can be added as needed
11
11
  build: List[BuildMetadata] = Field([])
12
12
 
13
+ def get_latest_build_metadata(self) -> BuildMetadata:
14
+ if not self.build:
15
+ raise ValueError("No metadata.build on the material.")
16
+ return self.build[-1]
17
+
13
18
  def get_build_metadata_of_type(self, configuration_type: str) -> Optional[BuildMetadata]:
14
19
  """
15
20
  Returns the first build configuration of the specified type.
@@ -2,8 +2,9 @@ from typing import Optional
2
2
 
3
3
  from ...material import Material
4
4
  from ..analyze.other import get_surface_area
5
- from ..build.compound_pristine_structures.two_dimensional.interface import InterfaceConfiguration, get_slab
6
- from ..build_components import MaterialBuildMetadata, MaterialWithBuildMetadata
5
+ from ..analyze.interface_material import get_interface_bulk_crystal
6
+ from ..build.compound_pristine_structures.two_dimensional.interface import get_slab
7
+ from ..build_components.metadata import MaterialWithBuildMetadata
7
8
  from ..convert import decorator_convert_material_args_kwargs_to_atoms
8
9
  from ..third_party import ASEAtoms, ASECalculator, ASECalculatorEMT
9
10
  from .calculators import InterfaceMaterialCalculatorParameters
@@ -114,19 +115,14 @@ def calculate_interfacial_energy(
114
115
  substrate_slab = get_slab(interface, part="substrate") if substrate_slab is None else substrate_slab
115
116
  film_slab = get_slab(interface, part="film") if film_slab is None else film_slab
116
117
 
117
- metadata = (
118
- interface.metadata
119
- if isinstance(interface, MaterialWithBuildMetadata)
120
- else MaterialBuildMetadata(**interface.metadata)
121
- )
122
- build_configuration = metadata.build[-1].configuration if metadata.build else {}
123
- config = InterfaceConfiguration(**build_configuration)
124
-
125
118
  try:
126
- substrate_bulk = config.substrate_configuration.atomic_layers.crystal
127
- film_bulk = config.film_configuration.atomic_layers.crystal
128
- except KeyError:
129
- raise ValueError("The substrate and film bulk materials must be provided or defined in the interface metadata.")
119
+ interface_with_metadata = MaterialWithBuildMetadata.create(interface.to_dict())
120
+ substrate_bulk = Material.create(get_interface_bulk_crystal(interface_with_metadata, part="substrate"))
121
+ film_bulk = Material.create(get_interface_bulk_crystal(interface_with_metadata, part="film"))
122
+ except ValueError as exc:
123
+ raise ValueError(
124
+ "The substrate and film bulk materials must be provided or defined in the interface metadata."
125
+ ) from exc
130
126
  surface_energy_substrate = calculate_surface_energy(substrate_slab, substrate_bulk, calculator)
131
127
  surface_energy_film = calculate_surface_energy(film_slab, film_bulk, calculator)
132
128
  adhesion_energy = calculate_adhesion_energy(interface, substrate_slab, film_slab, calculator)
@@ -3,7 +3,7 @@ import pytest
3
3
 
4
4
  from mat3ra.made.cell.primitive_cell import get_primitive_lattice_vectors_from_config
5
5
  from mat3ra.made.lattice import Lattice, LatticeTypeEnum
6
- from tests.py.unit.utils import assert_two_entities_deep_almost_equal
6
+ from unit.utils import assert_two_entities_deep_almost_equal
7
7
 
8
8
  EXPECTED_PRIMITIVE_CELL_NA4CL4 = [
9
9
  [5.691694, 0.0, 0.0],
@@ -0,0 +1,63 @@
1
+ from typing import Final, Type
2
+
3
+ import pytest
4
+ from mat3ra.made.tools.analyze.build_metadata_analyzer import BuildMetadataAnalyzer
5
+ from mat3ra.made.tools.analyze.build_metadata_analyzer_factory import create_build_metadata_analyzer
6
+ from mat3ra.made.tools.analyze.interface_material import (
7
+ InterfaceMaterialAnalyzer,
8
+ get_interface_bulk_crystal,
9
+ get_interface_configuration,
10
+ )
11
+ from mat3ra.made.tools.analyze.slab import SlabMaterialAnalyzer
12
+ from mat3ra.made.tools.build import MaterialWithBuildMetadata
13
+ from mat3ra.made.tools.build.compound_pristine_structures.two_dimensional.interface import InterfaceConfiguration
14
+ from unit.fixtures.interface.zsl import GRAPHENE_NICKEL_INTERFACE
15
+ from unit.fixtures.slab import SI_CONVENTIONAL_SLAB_001
16
+
17
+ INTERFACE_MATERIAL: Final = MaterialWithBuildMetadata.create(GRAPHENE_NICKEL_INTERFACE)
18
+ SLAB_MATERIAL: Final = MaterialWithBuildMetadata.create(SI_CONVENTIONAL_SLAB_001)
19
+ SUBSTRATE_CRYSTAL_NAME: Final = "Ni, Nickel, FCC (Fm-3m) 3D (Bulk), mp-23"
20
+ FILM_CRYSTAL_NAME: Final = "Graphene"
21
+
22
+ GET_BULK_CRYSTAL_CASES = [
23
+ ("substrate", SUBSTRATE_CRYSTAL_NAME),
24
+ ("film", FILM_CRYSTAL_NAME),
25
+ ]
26
+
27
+ CREATE_BUILD_METADATA_ANALYZER_CASES = [
28
+ (INTERFACE_MATERIAL, InterfaceMaterialAnalyzer),
29
+ (SLAB_MATERIAL, SlabMaterialAnalyzer),
30
+ ]
31
+
32
+
33
+ def test_build_configuration():
34
+ configuration = InterfaceMaterialAnalyzer(material=INTERFACE_MATERIAL).build_configuration
35
+ assert isinstance(configuration, InterfaceConfiguration)
36
+ assert configuration.type == "InterfaceConfiguration"
37
+
38
+
39
+ @pytest.mark.parametrize("part,expected_name", GET_BULK_CRYSTAL_CASES)
40
+ def test_get_bulk_crystal(part, expected_name):
41
+ crystal = InterfaceMaterialAnalyzer(material=INTERFACE_MATERIAL).get_bulk_crystal(part=part)
42
+ assert crystal["name"] == expected_name
43
+
44
+
45
+ def test_get_interface_configuration():
46
+ configuration = get_interface_configuration(INTERFACE_MATERIAL)
47
+ assert isinstance(configuration, InterfaceConfiguration)
48
+ assert configuration.type == "InterfaceConfiguration"
49
+
50
+
51
+ @pytest.mark.parametrize("part,expected_name", GET_BULK_CRYSTAL_CASES)
52
+ def test_get_interface_bulk_crystal(part, expected_name):
53
+ crystal = get_interface_bulk_crystal(INTERFACE_MATERIAL, part=part)
54
+ assert crystal["name"] == expected_name
55
+
56
+
57
+ @pytest.mark.parametrize("material,expected_analyzer_cls", CREATE_BUILD_METADATA_ANALYZER_CASES)
58
+ def test_create_build_metadata_analyzer(
59
+ material: MaterialWithBuildMetadata,
60
+ expected_analyzer_cls: Type[BuildMetadataAnalyzer],
61
+ ):
62
+ analyzer = create_build_metadata_analyzer(material)
63
+ assert isinstance(analyzer, expected_analyzer_cls)
@@ -28,6 +28,16 @@ def test_metadata_empty_initialization():
28
28
  assert metadata.model_dump(exclude_none=True) == {"build": []}
29
29
 
30
30
 
31
+ def test_get_latest_build_metadata():
32
+ metadata = MaterialBuildMetadata(build=[BuildMetadata(configuration={"type": "SlabConfiguration"})])
33
+ assert metadata.get_latest_build_metadata().configuration["type"] == "SlabConfiguration"
34
+
35
+
36
+ def test_get_latest_build_metadata_raises_when_empty():
37
+ with pytest.raises(ValueError, match="No metadata.build"):
38
+ MaterialBuildMetadata().get_latest_build_metadata()
39
+
40
+
31
41
  @pytest.mark.parametrize(
32
42
  "config_object, expected_dict",
33
43
  [