@mat3ra/made 2026.7.21-0 → 2026.7.29-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.21-0",
3
+ "version": "2026.7.29-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",
@@ -31,7 +31,7 @@ class SolidSolutionBuilder(MergeBuilder):
31
31
  source_element = configuration.source_element
32
32
  target_element = configuration.target_element
33
33
  material.name = (
34
- f"{formula} - Solid Solution ({source_element}\u2192{target_element} "
34
+ f"{formula} - Solid Solution ({source_element}->{target_element} "
35
35
  f"{concentration_percentage:.4g}%)"
36
36
  )
37
37
  return material
@@ -1,4 +1,4 @@
1
- from typing import Optional
1
+ from typing import Optional, Tuple
2
2
 
3
3
  from ...material import Material
4
4
  from ..analyze.other import get_surface_area
@@ -88,6 +88,31 @@ def calculate_adhesion_energy(
88
88
  return (energy_substrate_slab + energy_film_slab - energy_interface) / area
89
89
 
90
90
 
91
+ def _get_interface_with_build_metadata(interface: Material) -> MaterialWithBuildMetadata:
92
+ if isinstance(interface, MaterialWithBuildMetadata):
93
+ return interface
94
+ return MaterialWithBuildMetadata.create(interface.to_dict())
95
+
96
+
97
+ def _resolve_interface_bulk_materials(
98
+ interface: Material,
99
+ substrate_bulk: Optional[Material],
100
+ film_bulk: Optional[Material],
101
+ ) -> Tuple[Optional[Material], Optional[Material]]:
102
+ """Resolve missing bulks from interface build metadata (handles BoundaryConditions metadata)."""
103
+ try:
104
+ interface_with_metadata = _get_interface_with_build_metadata(interface)
105
+ if substrate_bulk is None:
106
+ substrate_bulk = Material.create(get_interface_bulk_crystal(interface_with_metadata, part="substrate"))
107
+ if film_bulk is None:
108
+ film_bulk = Material.create(get_interface_bulk_crystal(interface_with_metadata, part="film"))
109
+ except (ValueError, TypeError) as exc:
110
+ raise ValueError(
111
+ "The substrate and film bulk materials must be provided or defined in the interface metadata."
112
+ ) from exc
113
+ return substrate_bulk, film_bulk
114
+
115
+
91
116
  def calculate_interfacial_energy(
92
117
  interface: Material,
93
118
  substrate_slab: Optional[Material] = None,
@@ -115,14 +140,9 @@ def calculate_interfacial_energy(
115
140
  substrate_slab = get_slab(interface, part="substrate") if substrate_slab is None else substrate_slab
116
141
  film_slab = get_slab(interface, part="film") if film_slab is None else film_slab
117
142
 
118
- try:
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
143
+ if substrate_bulk is None or film_bulk is None:
144
+ substrate_bulk, film_bulk = _resolve_interface_bulk_materials(interface, substrate_bulk, film_bulk)
145
+
126
146
  surface_energy_substrate = calculate_surface_energy(substrate_slab, substrate_bulk, calculator)
127
147
  surface_energy_film = calculate_surface_energy(film_slab, film_bulk, calculator)
128
148
  adhesion_energy = calculate_adhesion_energy(interface, substrate_slab, film_slab, calculator)
@@ -22,9 +22,12 @@ def supercell(material: MaterialWithBuildMetadata, supercell_matrix) -> Material
22
22
  atoms = to_ase(material)
23
23
 
24
24
  supercell_atoms = ase_make_supercell(atoms, supercell_matrix)
25
- new_material = MaterialWithBuildMetadata.create(from_ase(supercell_atoms))
26
- # if material.metadata:
27
- # new_material.metadata.update(**material.metadata.to_dict())
25
+ material_config = from_ase(supercell_atoms)
26
+ # ASE make_supercell drops atoms.info; merge prior build/boundaryConditions when present.
27
+ source_metadata = material.to_dict().get("metadata") or {}
28
+ if source_metadata:
29
+ material_config["metadata"] = {**material_config.get("metadata", {}), **source_metadata}
30
+ new_material = MaterialWithBuildMetadata.create(material_config)
28
31
  new_material.name = material.name
29
32
  return new_material
30
33
 
@@ -5,7 +5,7 @@ from mat3ra.standata.materials import Materials
5
5
  BULK_SrTiO3 = Materials.get_by_name_first_match("SrTiO3")
6
6
  BULK_GRAPHITE = Materials.get_by_name_first_match("Graphite")
7
7
  BULK_SiO2 = Materials.get_by_name_first_match("SiO2")
8
- BULK_Hf2O_MCL = Materials.get_by_name_first_match("Hafnium.*MCL")
8
+ BULK_Hf2O_MCL = Materials.get_by_name_first_match("Hafnium IV Oxide, MCL")
9
9
  BULK_TiN = Materials.get_by_name_first_match("TiN")
10
10
  BULK_Ni_PRIMITIVE = Materials.get_by_name_first_match("Nickel")
11
11
  BULK_GRAPHENE = Materials.get_by_name_first_match("Graphene")
@@ -2,18 +2,22 @@ import numpy as np
2
2
  import pytest
3
3
  from mat3ra.made.material import Material
4
4
  from mat3ra.made.tools.build_components.entities.reusable.three_dimensional.supercell.helpers import create_supercell
5
+ from mat3ra.made.tools.build_components.metadata import MaterialWithBuildMetadata
6
+ from unit.fixtures.interface.zsl import GRAPHENE_NICKEL_INTERFACE
5
7
  from unit.fixtures.supercell import SI_SUPERCELL_2X2X1
6
8
 
7
9
  from .fixtures.bulk import BULK_Si_PRIMITIVE
8
10
  from .utils import assert_two_entities_deep_almost_equal
9
11
 
12
+ SUPERCELL_MATRIX_2X2X1 = [[2, 0, 0], [0, 2, 0], [0, 0, 1]]
13
+
10
14
 
11
15
  @pytest.mark.parametrize(
12
16
  "material_config, supercell_matrix, expected_material_config",
13
17
  [
14
18
  (
15
19
  BULK_Si_PRIMITIVE,
16
- [[2, 0, 0], [0, 2, 0], [0, 0, 1]],
20
+ SUPERCELL_MATRIX_2X2X1,
17
21
  SI_SUPERCELL_2X2X1,
18
22
  ),
19
23
  ],
@@ -27,3 +31,17 @@ def test_create_supercell(material_config, supercell_matrix, expected_material_c
27
31
  expected_vectors = np.array(supercell_matrix) @ np.array(material.lattice.vector_arrays)
28
32
  actual_vectors = np.array(supercell_material.lattice.vector_arrays)
29
33
  assert np.allclose(actual_vectors, expected_vectors, atol=1e-6), "Supercell vectors do not match expected values."
34
+
35
+
36
+ def test_create_supercell_preserves_prior_build_metadata():
37
+ material = MaterialWithBuildMetadata.create(GRAPHENE_NICKEL_INTERFACE)
38
+ prior_build_types = [step.configuration.get("type") for step in material.metadata.build]
39
+
40
+ supercell_material = create_supercell(material, SUPERCELL_MATRIX_2X2X1)
41
+
42
+ result_build_types = [step.configuration.get("type") for step in supercell_material.metadata.build]
43
+ assert result_build_types[:-1] == prior_build_types
44
+ assert result_build_types[-1] == "SupercellConfiguration"
45
+ assert supercell_material.metadata.to_dict().get("boundaryConditions") == material.metadata.to_dict().get(
46
+ "boundaryConditions"
47
+ )
@@ -1,5 +1,4 @@
1
1
  import numpy as np
2
- import pytest
3
2
  from ase.build import add_adsorbate, bulk, fcc111, graphene, surface
4
3
  from ase.calculators import emt
5
4
  from mat3ra.made.material import Material
@@ -20,16 +19,15 @@ graphene_layer.cell = nickel_slab.cell
20
19
  interface = nickel_slab.copy()
21
20
  add_adsorbate(interface, graphene_layer, height=2, position="ontop")
22
21
 
23
- # Material objects setup
22
+ # Material.create yields MaterialMetadataBoundaryConditions; bulks are resolved from nested build.
24
23
  interface_material = Material.create(GRAPHENE_NICKEL_INTERFACE)
25
24
  nickel_slab_material = Material.create(from_ase(nickel_slab))
26
- nickel_bulk_material = Material.create(from_ase(bulk("Ni", "fcc", a=3.52)))
27
- graphene_layer_material = Material.create(from_ase(graphene_layer))
28
- graphene_bulk_material = graphene_layer
29
25
 
30
26
  # Calculator setup
31
27
  calculator = emt.EMT()
32
28
 
29
+ EXPECTED_INTERFACIAL_ENERGY = -0.78488
30
+
33
31
 
34
32
  def test_calculate_total_energy():
35
33
  atoms = bulk("C", cubic=True)
@@ -59,17 +57,11 @@ def test_calculate_adhesion_energy():
59
57
  assert np.isclose(adhesion_energy, 0.07345)
60
58
 
61
59
 
62
- @pytest.mark.skip(reason="This test is not working as expected")
63
60
  def test_calculate_interfacial_energy():
64
61
  interfacial_energy = calculate_interfacial_energy(
65
62
  interface_material,
66
- nickel_slab_material,
67
- nickel_bulk_material,
68
- graphene_layer,
69
- graphene_bulk_material,
70
- calculator,
71
- )
72
- assert np.isclose(
73
- interfacial_energy,
74
- -0.78488,
63
+ substrate_slab=nickel_slab_material,
64
+ film_slab=graphene_layer,
65
+ calculator=calculator,
75
66
  )
67
+ assert np.isclose(interfacial_energy, EXPECTED_INTERFACIAL_ENERGY)