@mat3ra/made 2025.11.24-0 → 2025.12.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 +1 -1
- package/pyproject.toml +1 -1
- package/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py +9 -1
- package/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py +4 -0
- package/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/atomic_mass_dependent_function_holder.py +32 -0
- package/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/function_holder.py +1 -1
- package/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py +39 -0
- package/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py +45 -3
- package/src/py/mat3ra/made/tools/convert/__init__.py +24 -8
- package/src/py/mat3ra/made/tools/convert/utils.py +28 -2
- package/src/py/mat3ra/made/tools/operations/core/unary.py +2 -3
- package/tests/py/unit/fixtures/thrid_party/ase_atoms.py +14 -0
- package/tests/py/unit/test_tools_build_maxwell_disorder.py +123 -0
- package/tests/py/unit/test_tools_build_metadata.py +2 -12
- package/tests/py/unit/test_tools_convert.py +47 -9
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -21,7 +21,6 @@ dependencies = [
|
|
|
21
21
|
"mat3ra-utils",
|
|
22
22
|
"mat3ra-esse",
|
|
23
23
|
"mat3ra-code"
|
|
24
|
-
|
|
25
24
|
]
|
|
26
25
|
|
|
27
26
|
[project.optional-dependencies]
|
|
@@ -31,6 +30,7 @@ tools = [
|
|
|
31
30
|
"pymatgen==2024.4.13",
|
|
32
31
|
"ase",
|
|
33
32
|
"pymatgen-analysis-defects==2024.4.23",
|
|
33
|
+
"mat3ra-periodic-table>=2025.12.26",
|
|
34
34
|
]
|
|
35
35
|
dev = [
|
|
36
36
|
"pre-commit",
|
package/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
from .functions import
|
|
1
|
+
from .functions import (
|
|
2
|
+
AtomicMassDependentFunctionHolder,
|
|
3
|
+
FunctionHolder,
|
|
4
|
+
MaxwellBoltzmannDisplacementHolder,
|
|
5
|
+
PerturbationFunctionHolder,
|
|
6
|
+
SineWavePerturbationFunctionHolder,
|
|
7
|
+
)
|
|
2
8
|
|
|
3
9
|
__all__ = [
|
|
10
|
+
"AtomicMassDependentFunctionHolder",
|
|
4
11
|
"FunctionHolder",
|
|
12
|
+
"MaxwellBoltzmannDisplacementHolder",
|
|
5
13
|
"PerturbationFunctionHolder",
|
|
6
14
|
"SineWavePerturbationFunctionHolder",
|
|
7
15
|
]
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
from .atomic_mass_dependent_function_holder import AtomicMassDependentFunctionHolder
|
|
1
2
|
from .function_holder import FunctionHolder
|
|
3
|
+
from .maxwell_boltzmann import MaxwellBoltzmannDisplacementHolder
|
|
2
4
|
from .perturbation_function_holder import PerturbationFunctionHolder
|
|
3
5
|
from .sine_wave_perturbation_function_holder import SineWavePerturbationFunctionHolder
|
|
4
6
|
|
|
5
7
|
__all__ = [
|
|
8
|
+
"AtomicMassDependentFunctionHolder",
|
|
6
9
|
"FunctionHolder",
|
|
10
|
+
"MaxwellBoltzmannDisplacementHolder",
|
|
7
11
|
"PerturbationFunctionHolder",
|
|
8
12
|
"SineWavePerturbationFunctionHolder",
|
|
9
13
|
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import Any, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
import sympy as sp
|
|
4
|
+
from mat3ra.periodic_table.helpers import get_atomic_mass_from_element
|
|
5
|
+
|
|
6
|
+
from .function_holder import FunctionHolder
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AtomicMassDependentFunctionHolder(FunctionHolder):
|
|
10
|
+
variables: List[str] = ["x", "y", "z", "m"]
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
function: Union[sp.Expr, str] = sp.Symbol("f"),
|
|
15
|
+
variables: Optional[List[str]] = None,
|
|
16
|
+
**data: Any,
|
|
17
|
+
):
|
|
18
|
+
if variables is None:
|
|
19
|
+
expr = self._to_expr(function)
|
|
20
|
+
vs = sorted(expr.free_symbols, key=lambda s: s.name)
|
|
21
|
+
variables = [str(v) for v in vs] or ["x", "y", "z", "m"]
|
|
22
|
+
|
|
23
|
+
super().__init__(function=function, variables=variables, **data)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def get_atomic_mass(coordinate: List[float], material) -> float:
|
|
27
|
+
if material is None:
|
|
28
|
+
raise ValueError("Material is required to extract atomic mass")
|
|
29
|
+
|
|
30
|
+
atom_id = material.basis.coordinates.get_element_id_by_value(coordinate)
|
|
31
|
+
element = material.basis.elements.get_element_value_by_index(atom_id)
|
|
32
|
+
return get_atomic_mass_from_element(element)
|
|
@@ -52,7 +52,7 @@ class FunctionHolder(InMemoryEntityPydantic):
|
|
|
52
52
|
def function_str(self) -> str:
|
|
53
53
|
return str(self.function)
|
|
54
54
|
|
|
55
|
-
def apply_function(self, coordinate: List[float]) -> float:
|
|
55
|
+
def apply_function(self, coordinate: List[float], **kwargs: Any) -> float:
|
|
56
56
|
values = [coordinate[AXIS_TO_INDEX_MAP[var]] for var in self.variables]
|
|
57
57
|
return self.function_numeric(*values)
|
|
58
58
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Any, List, Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from pydantic import Field, model_validator
|
|
5
|
+
|
|
6
|
+
from .atomic_mass_dependent_function_holder import AtomicMassDependentFunctionHolder
|
|
7
|
+
|
|
8
|
+
DEFAULT_DISORDER_PARAMETER = 1.0
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MaxwellBoltzmannDisplacementHolder(AtomicMassDependentFunctionHolder):
|
|
12
|
+
disorder_parameter: float = Field(
|
|
13
|
+
default=DEFAULT_DISORDER_PARAMETER,
|
|
14
|
+
exclude=True,
|
|
15
|
+
description="Disorder parameter. Can be viewed as effective temperature in eV.",
|
|
16
|
+
)
|
|
17
|
+
random_seed: Optional[int] = Field(default=None, exclude=True)
|
|
18
|
+
random_state: Any = Field(default=None, exclude=True)
|
|
19
|
+
is_mass_used: bool = Field(default=True, exclude=True)
|
|
20
|
+
|
|
21
|
+
@model_validator(mode="after")
|
|
22
|
+
def setup_random_state(self):
|
|
23
|
+
if self.random_state is None:
|
|
24
|
+
self.random_state = np.random.RandomState(self.random_seed) if self.random_seed is not None else np.random
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
def apply_function(self, coordinate, material=None) -> List[float]:
|
|
28
|
+
if material is None:
|
|
29
|
+
raise ValueError("MaxwellBoltzmannDisplacementHolder requires 'material' kwargs")
|
|
30
|
+
|
|
31
|
+
if self.is_mass_used:
|
|
32
|
+
mass = self.get_atomic_mass(coordinate, material)
|
|
33
|
+
variance = self.disorder_parameter / mass
|
|
34
|
+
else:
|
|
35
|
+
variance = self.disorder_parameter
|
|
36
|
+
|
|
37
|
+
std_dev = np.sqrt(variance)
|
|
38
|
+
displacement = self.random_state.normal(0.0, std_dev, size=3)
|
|
39
|
+
return displacement.tolist()
|
package/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
from typing import Union
|
|
1
|
+
from typing import Optional, Union
|
|
2
2
|
|
|
3
3
|
import sympy as sp
|
|
4
|
-
from mat3ra.made.material import Material
|
|
5
4
|
|
|
6
|
-
from
|
|
5
|
+
from mat3ra.made.material import Material
|
|
7
6
|
from .builders.base import PerturbationBuilder
|
|
8
7
|
from .builders.isometric import IsometricPerturbationBuilder
|
|
9
8
|
from .configuration import PerturbationConfiguration
|
|
10
9
|
from .functions import PerturbationFunctionHolder
|
|
10
|
+
from .functions.maxwell_boltzmann import MaxwellBoltzmannDisplacementHolder
|
|
11
|
+
from ..... import MaterialWithBuildMetadata
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def create_perturbation(
|
|
@@ -39,3 +40,44 @@ def create_perturbation(
|
|
|
39
40
|
else:
|
|
40
41
|
builder = PerturbationBuilder()
|
|
41
42
|
return builder.get_material(configuration)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_maxwell_displacement(
|
|
46
|
+
material: Union[Material, MaterialWithBuildMetadata],
|
|
47
|
+
disorder_parameter: float,
|
|
48
|
+
random_seed: Optional[int] = None,
|
|
49
|
+
is_mass_used: bool = True,
|
|
50
|
+
) -> Material:
|
|
51
|
+
"""
|
|
52
|
+
Apply Maxwell-Boltzmann random displacements to a material.
|
|
53
|
+
|
|
54
|
+
Generates random 3D displacement vectors where each component follows a normal
|
|
55
|
+
distribution with variance proportional to disorder_parameter/m (if is_mass_used=True)
|
|
56
|
+
or disorder_parameter (if is_mass_used=False), where m is atomic mass.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
material: The material to be perturbed.
|
|
60
|
+
disorder_parameter: Disorder parameter controlling displacement magnitude,
|
|
61
|
+
can be viewed as effective temperature in eV.
|
|
62
|
+
random_seed: Optional random seed for reproducibility for the same material and parameters.
|
|
63
|
+
is_mass_used: If True, displacement variance is disorder_parameter/m (mass-dependent).
|
|
64
|
+
If False, displacement variance is disorder_parameter (mass-independent).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Material with applied Maxwell-Boltzmann displacements.
|
|
68
|
+
"""
|
|
69
|
+
displacement_holder = MaxwellBoltzmannDisplacementHolder(
|
|
70
|
+
disorder_parameter=disorder_parameter,
|
|
71
|
+
random_seed=random_seed,
|
|
72
|
+
is_mass_used=is_mass_used,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
configuration = PerturbationConfiguration(
|
|
76
|
+
material=material,
|
|
77
|
+
perturbation_function_holder=displacement_holder,
|
|
78
|
+
use_cartesian_coordinates=True,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
builder = PerturbationBuilder()
|
|
82
|
+
|
|
83
|
+
return builder.get_material(configuration)
|
|
@@ -2,10 +2,19 @@ import inspect
|
|
|
2
2
|
from functools import wraps
|
|
3
3
|
from typing import Any, Callable, Dict, Union
|
|
4
4
|
|
|
5
|
-
from mat3ra.made.material import Material
|
|
6
|
-
from mat3ra.made.utils import map_array_to_array_with_id_value, map_array_with_id_value_to_array
|
|
7
5
|
from mat3ra.utils.mixins import RoundNumericValuesMixin
|
|
8
6
|
|
|
7
|
+
from mat3ra.made.material import Material
|
|
8
|
+
from mat3ra.made.utils import (
|
|
9
|
+
map_array_to_array_with_id_value,
|
|
10
|
+
map_array_with_id_value_to_array,
|
|
11
|
+
)
|
|
12
|
+
from .utils import (
|
|
13
|
+
extract_labels_from_pymatgen_structure,
|
|
14
|
+
extract_metadata_from_pymatgen_structure,
|
|
15
|
+
extract_tags_from_ase_atoms,
|
|
16
|
+
calculate_padded_cell_simple_cubic,
|
|
17
|
+
)
|
|
9
18
|
from ..third_party import (
|
|
10
19
|
ASEAtoms,
|
|
11
20
|
PymatgenAseAtomsAdaptor,
|
|
@@ -14,11 +23,6 @@ from ..third_party import (
|
|
|
14
23
|
PymatgenPoscar,
|
|
15
24
|
PymatgenStructure,
|
|
16
25
|
)
|
|
17
|
-
from .utils import (
|
|
18
|
-
extract_labels_from_pymatgen_structure,
|
|
19
|
-
extract_metadata_from_pymatgen_structure,
|
|
20
|
-
extract_tags_from_ase_atoms,
|
|
21
|
-
)
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
def to_pymatgen(material_or_material_data: Union[Material, Dict[str, Any]]) -> PymatgenStructure:
|
|
@@ -198,15 +202,27 @@ def from_ase(ase_atoms: ASEAtoms) -> Dict[str, Any]:
|
|
|
198
202
|
Returns:
|
|
199
203
|
dict: A dictionary containing the material information in ESSE format.
|
|
200
204
|
"""
|
|
205
|
+
is_molecule = not any(ase_atoms.pbc)
|
|
206
|
+
|
|
207
|
+
if is_molecule:
|
|
208
|
+
lattice_vectors = calculate_padded_cell_simple_cubic(ase_atoms.get_positions())
|
|
209
|
+
ase_atoms.set_cell(lattice_vectors)
|
|
210
|
+
ase_atoms.center()
|
|
211
|
+
|
|
201
212
|
# TODO: check that atomic labels/tags are properly handled
|
|
202
213
|
structure = PymatgenAseAtomsAdaptor.get_structure(ase_atoms)
|
|
214
|
+
|
|
203
215
|
material = from_pymatgen(structure)
|
|
216
|
+
material["isNonPeriodic"] = is_molecule
|
|
217
|
+
if is_molecule:
|
|
218
|
+
material["lattice"]["type"] = "CUB"
|
|
219
|
+
|
|
204
220
|
ase_tags = extract_tags_from_ase_atoms(ase_atoms)
|
|
205
221
|
material["basis"]["labels"] = ase_tags
|
|
206
222
|
ase_metadata = ase_atoms.info.get("metadata", {})
|
|
207
223
|
if ase_metadata:
|
|
208
224
|
material["metadata"].update(ase_metadata)
|
|
209
|
-
material["name"] = ase_atoms.info.get("name",
|
|
225
|
+
material["name"] = ase_atoms.info.get("name", ase_atoms.get_chemical_formula())
|
|
210
226
|
return material
|
|
211
227
|
|
|
212
228
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from typing import Any, Dict, List, Union
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import numpy as np
|
|
5
5
|
from mat3ra.utils.object import NumpyNDArrayRoundEncoder
|
|
6
|
+
from scipy.spatial.distance import pdist
|
|
6
7
|
|
|
7
|
-
from
|
|
8
|
+
from mat3ra.made.utils import map_array_to_array_with_id_value, get_center_of_coordinates
|
|
8
9
|
from .interface_parts_enum import INTERFACE_LABELS_MAP
|
|
10
|
+
from ..third_party import ASEAtoms, PymatgenInterface, PymatgenStructure
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
def extract_labels_from_pymatgen_structure(structure: PymatgenStructure) -> List[int]:
|
|
@@ -37,3 +39,27 @@ def extract_tags_from_ase_atoms(atoms: ASEAtoms) -> List[Union[str, int]]:
|
|
|
37
39
|
int_tags = [int(tag) for tag in atoms.arrays["tags"] if tag is not None]
|
|
38
40
|
result = map_array_to_array_with_id_value(int_tags, remove_none=True)
|
|
39
41
|
return result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def calculate_padded_cell_simple_cubic(
|
|
45
|
+
coordinates: List[List[float]], padding_factor: float = 2.0
|
|
46
|
+
) -> List[List[float]]:
|
|
47
|
+
"""
|
|
48
|
+
Calculate values for a padded cell for a molecule based on its coordinates.
|
|
49
|
+
Args:
|
|
50
|
+
coordinates (Array[Array[float]]): A list of atomic coordinates.
|
|
51
|
+
padding_factor (float): The factor by which to multiply the maximum distance for padding.
|
|
52
|
+
Returns:
|
|
53
|
+
Array[float]: A list containing the final cell latice vectors with padding applied.
|
|
54
|
+
"""
|
|
55
|
+
positions = np.array(coordinates)
|
|
56
|
+
center = get_center_of_coordinates(positions)
|
|
57
|
+
shifted_positions = positions - center
|
|
58
|
+
max_distance = np.max(pdist(shifted_positions)) if len(positions) >= 2 else 10.0
|
|
59
|
+
padding_value = padding_factor * max_distance
|
|
60
|
+
|
|
61
|
+
return [
|
|
62
|
+
[padding_value, 0.0, 0.0],
|
|
63
|
+
[0.0, padding_value, 0.0],
|
|
64
|
+
[0.0, 0.0, padding_value],
|
|
65
|
+
]
|
|
@@ -84,12 +84,11 @@ def perturb(
|
|
|
84
84
|
perturbed_coordinates: List[List[float]] = []
|
|
85
85
|
|
|
86
86
|
for coordinate in original_coordinates:
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
displacement = perturbation_function.apply_function(coordinate, material=new_material)
|
|
88
|
+
|
|
89
89
|
if isinstance(displacement, (list, tuple, np.ndarray)):
|
|
90
90
|
delta = np.array(displacement)
|
|
91
91
|
else:
|
|
92
|
-
# scalar: apply to z-axis
|
|
93
92
|
delta = np.array([0.0, 0.0, displacement])
|
|
94
93
|
|
|
95
94
|
new_coordinate = np.array(coordinate) + delta
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from ase.build import bulk, molecule
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
BULK_SI_LATTICE_A = 3.8395
|
|
5
|
+
BULK_SI_LATTICE_ALPHA = 60
|
|
6
|
+
BULK_SI_LABELS = [{"id": 0, "value": 0}, {"id": 1, "value": 1}]
|
|
7
|
+
|
|
8
|
+
atoms = bulk("Si")
|
|
9
|
+
atoms.set_tags([0, 1])
|
|
10
|
+
ASE_BULK_Si = atoms
|
|
11
|
+
|
|
12
|
+
atoms = molecule("H2O")
|
|
13
|
+
atoms.set_pbc(False)
|
|
14
|
+
ASE_MOLECULE_H2O = atoms
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
from mat3ra.made.material import Material
|
|
4
|
+
from mat3ra.made.tools.build_components.operations.core.modifications.perturb.functions.maxwell_boltzmann import (
|
|
5
|
+
MaxwellBoltzmannDisplacementHolder,
|
|
6
|
+
)
|
|
7
|
+
from mat3ra.made.tools.build_components.operations.core.modifications.perturb.helpers import create_maxwell_displacement
|
|
8
|
+
from mat3ra.made.tools.helpers import create_supercell
|
|
9
|
+
from mat3ra.periodic_table.helpers import get_atomic_mass_from_element
|
|
10
|
+
|
|
11
|
+
from .fixtures.bulk import BULK_Si_PRIMITIVE
|
|
12
|
+
from .fixtures.slab import SI_CONVENTIONAL_SLAB_001
|
|
13
|
+
|
|
14
|
+
DISORDER_PARAMETER = 1.0 # Temperature-like
|
|
15
|
+
RANDOM_SEED = 42
|
|
16
|
+
NUM_SAMPLES_FOR_MSD = 1000
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.parametrize("random_seed", [None, 42, 123])
|
|
20
|
+
def test_maxwell_displacement_deterministic(random_seed):
|
|
21
|
+
material = Material.create(BULK_Si_PRIMITIVE)
|
|
22
|
+
displacement_func1 = MaxwellBoltzmannDisplacementHolder(
|
|
23
|
+
disorder_parameter=DISORDER_PARAMETER, random_seed=random_seed
|
|
24
|
+
)
|
|
25
|
+
displacement_func2 = MaxwellBoltzmannDisplacementHolder(
|
|
26
|
+
disorder_parameter=DISORDER_PARAMETER, random_seed=random_seed
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
coord = [0.0, 0.0, 0.0]
|
|
30
|
+
|
|
31
|
+
if random_seed is not None:
|
|
32
|
+
disp1 = displacement_func1.apply_function(coord, material=material)
|
|
33
|
+
disp2 = displacement_func2.apply_function(coord, material=material)
|
|
34
|
+
assert np.allclose(disp1, disp2)
|
|
35
|
+
|
|
36
|
+
# Different seed should give different results
|
|
37
|
+
displacement_func3 = MaxwellBoltzmannDisplacementHolder(
|
|
38
|
+
disorder_parameter=DISORDER_PARAMETER, random_seed=random_seed + 1
|
|
39
|
+
)
|
|
40
|
+
disp3 = displacement_func3.apply_function(coord, material=material)
|
|
41
|
+
assert not np.allclose(disp1, disp3) or np.allclose(disp1, [0, 0, 0], atol=1e-10)
|
|
42
|
+
else:
|
|
43
|
+
# No seed: different instances should give different results (non-deterministic)
|
|
44
|
+
disp1 = displacement_func1.apply_function(coord, material=material)
|
|
45
|
+
disp2 = displacement_func2.apply_function(coord, material=material)
|
|
46
|
+
assert not np.allclose(disp1, disp2) or np.allclose(disp1, [0, 0, 0], atol=1e-10)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_maxwell_displacement_perturb_integration():
|
|
50
|
+
material = Material.create(BULK_Si_PRIMITIVE)
|
|
51
|
+
original_coords = [coord[:] for coord in material.basis.coordinates.values]
|
|
52
|
+
|
|
53
|
+
perturbed_material = create_maxwell_displacement(
|
|
54
|
+
material, disorder_parameter=DISORDER_PARAMETER, random_seed=RANDOM_SEED
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
assert len(perturbed_material.basis.coordinates.values) == len(original_coords)
|
|
58
|
+
for i, (orig, pert) in enumerate(zip(original_coords, perturbed_material.basis.coordinates.values)):
|
|
59
|
+
delta = np.array(pert) - np.array(orig)
|
|
60
|
+
assert np.linalg.norm(delta) > 0 or np.allclose(delta, 0, atol=1e-10)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_maxwell_displacement_msd_expectation():
|
|
64
|
+
material = Material.create(BULK_Si_PRIMITIVE)
|
|
65
|
+
si_mass = get_atomic_mass_from_element("Si")
|
|
66
|
+
disorder_parameter = DISORDER_PARAMETER
|
|
67
|
+
expected_variance = disorder_parameter / si_mass
|
|
68
|
+
expected_msd = 3 * expected_variance
|
|
69
|
+
|
|
70
|
+
displacements = []
|
|
71
|
+
coord = [0.0, 0.0, 0.0]
|
|
72
|
+
for _ in range(NUM_SAMPLES_FOR_MSD):
|
|
73
|
+
displacement_func = MaxwellBoltzmannDisplacementHolder(disorder_parameter=disorder_parameter, random_seed=None)
|
|
74
|
+
disp = displacement_func.apply_function(coord, material=material)
|
|
75
|
+
displacements.append(disp)
|
|
76
|
+
|
|
77
|
+
displacements_array = np.array(displacements)
|
|
78
|
+
msd = np.mean(np.sum(displacements_array**2, axis=1))
|
|
79
|
+
|
|
80
|
+
assert abs(msd - expected_msd) / expected_msd < 0.3
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.mark.parametrize(
|
|
84
|
+
"slab_config, temperature_k, random_seed",
|
|
85
|
+
[
|
|
86
|
+
(SI_CONVENTIONAL_SLAB_001, 1300.0, 42),
|
|
87
|
+
(SI_CONVENTIONAL_SLAB_001, 1300.0, 42),
|
|
88
|
+
],
|
|
89
|
+
)
|
|
90
|
+
def test_maxwell_boltzmann_on_slab(slab_config, temperature_k, random_seed):
|
|
91
|
+
material = Material.create(slab_config)
|
|
92
|
+
material = create_supercell(material, scaling_factor=[4, 4, 1])
|
|
93
|
+
original_coords = [coord[:] for coord in material.basis.coordinates.values]
|
|
94
|
+
original_lattice = material.lattice.vector_arrays.copy()
|
|
95
|
+
|
|
96
|
+
perturbed_material = create_maxwell_displacement(
|
|
97
|
+
material, disorder_parameter=temperature_k, random_seed=random_seed
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert len(perturbed_material.basis.coordinates.values) == len(original_coords)
|
|
101
|
+
assert len(perturbed_material.basis.elements.values) == len(material.basis.elements.values)
|
|
102
|
+
|
|
103
|
+
coordinate_changes = []
|
|
104
|
+
for i, (orig, pert) in enumerate(zip(original_coords, perturbed_material.basis.coordinates.values)):
|
|
105
|
+
delta = np.array(pert) - np.array(orig)
|
|
106
|
+
displacement_magnitude = np.linalg.norm(delta)
|
|
107
|
+
coordinate_changes.append(displacement_magnitude)
|
|
108
|
+
|
|
109
|
+
max_displacement = max(coordinate_changes)
|
|
110
|
+
mean_displacement = np.mean(coordinate_changes)
|
|
111
|
+
|
|
112
|
+
assert max_displacement > 0
|
|
113
|
+
assert mean_displacement > 0
|
|
114
|
+
|
|
115
|
+
si_mass = get_atomic_mass_from_element("Si")
|
|
116
|
+
expected_std = np.sqrt(temperature_k / si_mass)
|
|
117
|
+
|
|
118
|
+
assert mean_displacement < 5 * expected_std
|
|
119
|
+
|
|
120
|
+
assert np.allclose(perturbed_material.lattice.vector_arrays, original_lattice, atol=1e-10)
|
|
121
|
+
|
|
122
|
+
for i, element in enumerate(material.basis.elements.values):
|
|
123
|
+
assert perturbed_material.basis.elements.values[i] == element
|
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
import pytest
|
|
2
|
-
from mat3ra.code.entity import
|
|
2
|
+
from mat3ra.code.entity import InMemoryEntityPydantic
|
|
3
|
+
|
|
3
4
|
from mat3ra.made.tools.build_components.metadata import BuildMetadata, MaterialBuildMetadata
|
|
4
|
-
from pydantic import BaseModel
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class ConfigWithToDict(InMemoryEntityPydantic):
|
|
8
8
|
value: str = "test_dict"
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
# TODO: Remove this class when all configurations moved to Pydantic
|
|
12
|
-
class ConfigWithToJsonReturnsDict(BaseModel, InMemoryEntity):
|
|
13
|
-
value: str = "test_json_dict"
|
|
14
|
-
|
|
15
|
-
@property
|
|
16
|
-
def _json(self):
|
|
17
|
-
return {"value": self.value}
|
|
18
|
-
|
|
19
|
-
|
|
20
11
|
class ConfigWithToJsonReturnsStr(InMemoryEntityPydantic):
|
|
21
12
|
value: str = "test_json_str"
|
|
22
13
|
|
|
@@ -42,7 +33,6 @@ def test_metadata_empty_initialization():
|
|
|
42
33
|
"config_object, expected_dict",
|
|
43
34
|
[
|
|
44
35
|
(ConfigWithToDict(), {"value": "test_dict"}),
|
|
45
|
-
(ConfigWithToJsonReturnsDict(), {"value": "test_json_dict"}),
|
|
46
36
|
(ConfigWithToJsonReturnsStr(), {"value": "test_json_str"}),
|
|
47
37
|
],
|
|
48
38
|
)
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import pytest
|
|
3
3
|
from ase import Atoms
|
|
4
|
-
from ase.build import bulk
|
|
5
4
|
from mat3ra.code.array_with_ids import ArrayWithIds
|
|
6
|
-
from mat3ra.made.material import Material
|
|
7
|
-
from mat3ra.made.tools.convert import from_ase, from_poscar, from_pymatgen, to_ase, to_poscar, to_pymatgen
|
|
8
5
|
from mat3ra.utils import assertion as assertion_utils
|
|
9
6
|
from pymatgen.core.structure import Element, Lattice, Structure
|
|
10
7
|
|
|
8
|
+
from mat3ra.made.material import Material
|
|
9
|
+
from mat3ra.made.tools.convert import from_ase, from_poscar, from_pymatgen, to_ase, to_poscar, to_pymatgen
|
|
11
10
|
from .fixtures.monolayer import GRAPHENE
|
|
11
|
+
from .fixtures.thrid_party.ase_atoms import (
|
|
12
|
+
ASE_BULK_Si,
|
|
13
|
+
ASE_MOLECULE_H2O,
|
|
14
|
+
BULK_SI_LATTICE_A,
|
|
15
|
+
BULK_SI_LATTICE_ALPHA,
|
|
16
|
+
BULK_SI_LABELS,
|
|
17
|
+
)
|
|
12
18
|
|
|
13
19
|
PYMATGEN_LATTICE = Lattice.from_parameters(a=3.84, b=3.84, c=3.84, alpha=120, beta=90, gamma=60)
|
|
14
20
|
PYMATGEN_STRUCTURE = Structure(PYMATGEN_LATTICE, ["Si", "Si"], [[0, 0, 0], [0.75, 0.5, 0.75]])
|
|
@@ -98,10 +104,42 @@ def test_to_ase():
|
|
|
98
104
|
assert ase_atoms.get_tags().tolist() == [0, 1]
|
|
99
105
|
|
|
100
106
|
|
|
101
|
-
|
|
102
|
-
ase_atoms
|
|
103
|
-
|
|
107
|
+
@pytest.mark.parametrize(
|
|
108
|
+
"ase_atoms, expected_lattice_a, expected_lattice_alpha, expected_labels,"
|
|
109
|
+
+ " expected_is_non_periodic, expected_lattice_type",
|
|
110
|
+
[
|
|
111
|
+
(
|
|
112
|
+
ASE_BULK_Si,
|
|
113
|
+
BULK_SI_LATTICE_A,
|
|
114
|
+
BULK_SI_LATTICE_ALPHA,
|
|
115
|
+
BULK_SI_LABELS,
|
|
116
|
+
False,
|
|
117
|
+
None,
|
|
118
|
+
),
|
|
119
|
+
(
|
|
120
|
+
ASE_MOLECULE_H2O,
|
|
121
|
+
None,
|
|
122
|
+
None,
|
|
123
|
+
[],
|
|
124
|
+
True,
|
|
125
|
+
"CUB",
|
|
126
|
+
),
|
|
127
|
+
],
|
|
128
|
+
)
|
|
129
|
+
def test_from_ase(
|
|
130
|
+
ase_atoms,
|
|
131
|
+
expected_lattice_a,
|
|
132
|
+
expected_lattice_alpha,
|
|
133
|
+
expected_labels,
|
|
134
|
+
expected_is_non_periodic,
|
|
135
|
+
expected_lattice_type,
|
|
136
|
+
):
|
|
104
137
|
material_data = from_ase(ase_atoms)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
138
|
+
if expected_lattice_a is not None:
|
|
139
|
+
assertion_utils.assert_almost_equal_numbers(material_data["lattice"]["a"], expected_lattice_a, atol=1e-4)
|
|
140
|
+
if expected_lattice_alpha is not None:
|
|
141
|
+
assert material_data["lattice"]["alpha"] == expected_lattice_alpha
|
|
142
|
+
assert material_data["basis"]["labels"] == expected_labels
|
|
143
|
+
assert material_data["isNonPeriodic"] == expected_is_non_periodic
|
|
144
|
+
if expected_lattice_type is not None:
|
|
145
|
+
assert material_data["lattice"]["type"] == expected_lattice_type
|