@mat3ra/made 2026.7.21-0 → 2026.8.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/tools/build/defective_structures/three_dimensional/solid_solution/builder.py +1 -1
- package/src/py/mat3ra/made/tools/calculate/__init__.py +29 -9
- package/src/py/mat3ra/made/tools/operations/core/unary.py +47 -3
- package/tests/py/unit/fixtures/bulk.py +1 -1
- package/tests/py/unit/test_operations.py +26 -2
- package/tests/py/unit/test_tools_build_supercell.py +19 -1
- package/tests/py/unit/test_tools_calculate.py +7 -15
package/package.json
CHANGED
|
@@ -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}
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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)
|
|
@@ -17,14 +17,58 @@ def translate(material: Material, vector: Vector3D) -> Material:
|
|
|
17
17
|
return translate_by_vector(material, vector, use_cartesian_coordinates=True)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def translate_atoms(
|
|
21
|
+
material: Material,
|
|
22
|
+
atom_ids: List[int],
|
|
23
|
+
vector: Vector3D,
|
|
24
|
+
use_cartesian_coordinates: bool = True,
|
|
25
|
+
) -> Material:
|
|
26
|
+
"""
|
|
27
|
+
Translate the given atoms by a vector.
|
|
28
|
+
|
|
29
|
+
Basis order is preserved.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
material (Material): The material to translate atoms in.
|
|
33
|
+
atom_ids (List[int]): Indices of the atoms to translate.
|
|
34
|
+
vector (Vector3D): The vector to translate the atoms by.
|
|
35
|
+
use_cartesian_coordinates (bool): Whether `vector` is in cartesian coordinates.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Material: The material with the given atoms translated.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
IndexError: If any atom id is out of range for the basis.
|
|
42
|
+
"""
|
|
43
|
+
new_material = material.clone()
|
|
44
|
+
if use_cartesian_coordinates:
|
|
45
|
+
new_material.to_cartesian()
|
|
46
|
+
coordinates: List[List[float]] = [list(coordinate) for coordinate in new_material.basis.coordinates.values]
|
|
47
|
+
|
|
48
|
+
out_of_range = [atom_id for atom_id in atom_ids if not -len(coordinates) <= atom_id < len(coordinates)]
|
|
49
|
+
if out_of_range:
|
|
50
|
+
raise IndexError(f"Atom ids {out_of_range} are out of range for a basis of {len(coordinates)} atoms.")
|
|
51
|
+
|
|
52
|
+
for atom_id in atom_ids:
|
|
53
|
+
coordinates[atom_id] = (np.array(coordinates[atom_id]) + np.array(vector)).tolist()
|
|
54
|
+
|
|
55
|
+
new_material.set_coordinates(coordinates)
|
|
56
|
+
if use_cartesian_coordinates:
|
|
57
|
+
new_material.to_crystal()
|
|
58
|
+
return new_material
|
|
59
|
+
|
|
60
|
+
|
|
20
61
|
@decorator_convert_supercell_matrix_2x2_to_3x3
|
|
21
62
|
def supercell(material: MaterialWithBuildMetadata, supercell_matrix) -> MaterialWithBuildMetadata:
|
|
22
63
|
atoms = to_ase(material)
|
|
23
64
|
|
|
24
65
|
supercell_atoms = ase_make_supercell(atoms, supercell_matrix)
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
|
|
66
|
+
material_config = from_ase(supercell_atoms)
|
|
67
|
+
# ASE make_supercell drops atoms.info; merge prior build/boundaryConditions when present.
|
|
68
|
+
source_metadata = material.to_dict().get("metadata") or {}
|
|
69
|
+
if source_metadata:
|
|
70
|
+
material_config["metadata"] = {**material_config.get("metadata", {}), **source_metadata}
|
|
71
|
+
new_material = MaterialWithBuildMetadata.create(material_config)
|
|
28
72
|
new_material.name = material.name
|
|
29
73
|
return new_material
|
|
30
74
|
|
|
@@ -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
|
|
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")
|
|
@@ -5,9 +5,11 @@ import pytest
|
|
|
5
5
|
from mat3ra.esse.models.core.abstract.matrix_3x3 import Matrix3x3Schema
|
|
6
6
|
from mat3ra.esse.models.core.reusable.axis_enum import AxisEnum
|
|
7
7
|
from mat3ra.made.material import Material
|
|
8
|
-
from mat3ra.made.tools.build_components.operations.core.modifications.perturb import
|
|
8
|
+
from mat3ra.made.tools.build_components.operations.core.modifications.perturb import (
|
|
9
|
+
SineWavePerturbationFunctionHolder,
|
|
10
|
+
)
|
|
9
11
|
from mat3ra.made.tools.operations.core.binary import stack_two_materials
|
|
10
|
-
from mat3ra.made.tools.operations.core.unary import perturb, strain
|
|
12
|
+
from mat3ra.made.tools.operations.core.unary import perturb, strain, translate_atoms
|
|
11
13
|
from mat3ra.made.tools.operations.reusable.unary import transform_material_by_matrix
|
|
12
14
|
from unit.fixtures.bulk import BULK_Si_CONVENTIONAL
|
|
13
15
|
from unit.fixtures.strain import BULK_Si_CONVENTIONAL_STRAINED
|
|
@@ -100,3 +102,25 @@ def test_transform_material_by_matrix(matrix, expected_a, expected_b, expected_c
|
|
|
100
102
|
assert transformed_material.lattice.a == pytest.approx(expected_a, abs=1e-6)
|
|
101
103
|
assert transformed_material.lattice.b == pytest.approx(expected_b, abs=1e-6)
|
|
102
104
|
assert transformed_material.lattice.c == pytest.approx(expected_c, abs=1e-6)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.parametrize(
|
|
108
|
+
"material_config, atom_ids, vector, expected_coord_change",
|
|
109
|
+
[
|
|
110
|
+
(SI_CONVENTIONAL_SLAB_001, [3], [0.0, 0.0, -1.5], [0.0, 0.0, -1.5]),
|
|
111
|
+
],
|
|
112
|
+
)
|
|
113
|
+
def test_translate_atoms(material_config, atom_ids, vector, expected_coord_change):
|
|
114
|
+
material = Material.create(material_config)
|
|
115
|
+
original_coords = [coord[:] for coord in material.basis.coordinates.values]
|
|
116
|
+
|
|
117
|
+
translated_material = translate_atoms(material, atom_ids, vector)
|
|
118
|
+
lattice_vectors = np.array(material.lattice.vector_arrays)
|
|
119
|
+
|
|
120
|
+
actual_delta = (
|
|
121
|
+
np.array(translated_material.basis.coordinates.values[atom_ids[0]]) - np.array(original_coords[atom_ids[0]])
|
|
122
|
+
) @ lattice_vectors
|
|
123
|
+
assert np.isclose(actual_delta, expected_coord_change, atol=1e-4).all()
|
|
124
|
+
|
|
125
|
+
untouched_id = atom_ids[0] - 1
|
|
126
|
+
assert np.allclose(translated_material.basis.coordinates.values[untouched_id], original_coords[untouched_id])
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
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)
|