@mat3ra/made 2025.12.16-0 → 2026.3.3-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/basis/__init__.py +19 -0
- package/src/py/mat3ra/made/lattice.py +7 -0
- package/src/py/mat3ra/made/material.py +15 -0
- 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/operations/core/unary.py +2 -3
- package/tests/fixtures/Graphene.json +2 -2
- package/tests/fixtures/si-standata.json +2 -2
- package/tests/js/material.test.ts +14 -1
- package/tests/py/unit/test_material.py +18 -0
- package/tests/py/unit/test_tools_build_maxwell_disorder.py +123 -0
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",
|
|
@@ -2,6 +2,7 @@ from typing import Any, Dict, List, Optional, Union
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
from mat3ra.code.array_with_ids import ArrayWithIds
|
|
5
|
+
from mat3ra.code.constants import HASH_TOLERANCE
|
|
5
6
|
from mat3ra.code.entity import InMemoryEntityPydantic
|
|
6
7
|
from mat3ra.esse.models.core.abstract.matrix_3x3 import Matrix3x3Schema
|
|
7
8
|
from mat3ra.esse.models.material import BasisSchema, BasisUnitsEnum
|
|
@@ -87,6 +88,24 @@ class Basis(BasisSchema, InMemoryEntityPydantic):
|
|
|
87
88
|
constraints=ArrayWithIds.from_list_of_dicts(constraints),
|
|
88
89
|
)
|
|
89
90
|
|
|
91
|
+
@property
|
|
92
|
+
def hash_string(self) -> str:
|
|
93
|
+
"""
|
|
94
|
+
Mirrors JS Basis.hashString (getAsSortedString in crystal units).
|
|
95
|
+
Converts to crystal, applies mod 1 to bring coords into [0,1), builds sorted atom strings.
|
|
96
|
+
"""
|
|
97
|
+
original_is_in_cartesian = self.is_in_cartesian_units
|
|
98
|
+
self.to_crystal()
|
|
99
|
+
labels_map = {lbl["id"]: str(lbl["value"]) for lbl in self.labels.to_dict()}
|
|
100
|
+
parts = []
|
|
101
|
+
for elem, coord in zip(self.elements.to_dict(), self.coordinates.to_dict()):
|
|
102
|
+
label = labels_map.get(elem["id"], "")
|
|
103
|
+
rounded = [f"{round(v % 1, HASH_TOLERANCE):g}" for v in coord["value"]]
|
|
104
|
+
parts.append(f"{elem['value']}{label} {','.join(rounded)}")
|
|
105
|
+
if original_is_in_cartesian:
|
|
106
|
+
self.to_cartesian()
|
|
107
|
+
return ";".join(sorted(parts)) + ";"
|
|
108
|
+
|
|
90
109
|
@property
|
|
91
110
|
def is_in_crystal_units(self):
|
|
92
111
|
return self.units == BasisUnitsEnum.crystal
|
|
@@ -2,6 +2,7 @@ import math
|
|
|
2
2
|
from typing import List, Optional
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
|
+
from mat3ra.code.constants import HASH_TOLERANCE
|
|
5
6
|
from mat3ra.code.entity import InMemoryEntityPydantic
|
|
6
7
|
from mat3ra.esse.models.properties_directory.structural.lattice import (
|
|
7
8
|
LatticeSchema,
|
|
@@ -127,6 +128,12 @@ class Lattice(RoundNumericValuesMixin, LatticeSchemaVectorless, InMemoryEntityPy
|
|
|
127
128
|
def cell_volume_rounded(self) -> float:
|
|
128
129
|
return self.vectors.volume_rounded
|
|
129
130
|
|
|
131
|
+
def get_hash_string(self, is_scaled: bool = False) -> str:
|
|
132
|
+
"""Mirrors JS Lattice.getHashString(isScaled). Rounds to HASH_TOLERANCE decimal places."""
|
|
133
|
+
scale = self.a if is_scaled else 1
|
|
134
|
+
values = [self.a / scale, self.b / scale, self.c / scale, self.alpha, self.beta, self.gamma]
|
|
135
|
+
return ";".join(f"{round(v, HASH_TOLERANCE):g}" for v in values) + ";"
|
|
136
|
+
|
|
130
137
|
def get_scaled_by_matrix(self, matrix: List[List[float]]):
|
|
131
138
|
"""
|
|
132
139
|
Scale the lattice by a matrix.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import hashlib
|
|
1
2
|
from typing import Any, List, Optional, Union
|
|
2
3
|
|
|
3
4
|
from mat3ra.code.constants import AtomicCoordinateUnits, Units
|
|
@@ -111,3 +112,17 @@ class Material(MaterialSchema, HasDescriptionHasMetadataNamedDefaultableInMemory
|
|
|
111
112
|
|
|
112
113
|
def set_labels_from_value(self, value: Union[int, str]) -> None:
|
|
113
114
|
self.basis.set_labels_from_list([value] * self.basis.number_of_atoms)
|
|
115
|
+
|
|
116
|
+
def calculate_hash(self, salt: str = "", is_scaled: bool = False) -> str:
|
|
117
|
+
"""Mirrors JS materialMixin.calculateHash(). MD5 of basis + lattice hash strings."""
|
|
118
|
+
message = f"{self.basis.hash_string}#{self.lattice.get_hash_string(is_scaled)}#{salt}"
|
|
119
|
+
return hashlib.md5(message.encode()).hexdigest()
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def hash(self) -> str:
|
|
123
|
+
return self.calculate_hash()
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def scaled_hash(self) -> str:
|
|
127
|
+
return self.calculate_hash(is_scaled=True)
|
|
128
|
+
|
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)
|
|
@@ -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
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
version https://git-lfs.github.com/spec/v1
|
|
2
|
-
oid sha256:
|
|
3
|
-
size
|
|
2
|
+
oid sha256:43712ea8ada9350751be35d18f0ef92880a095be6536615f86b0c9ffd3b10dae
|
|
3
|
+
size 1835
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
version https://git-lfs.github.com/spec/v1
|
|
2
|
-
oid sha256:
|
|
3
|
-
size
|
|
2
|
+
oid sha256:62175309558d4a75da4fd723ccb47dc558cdbb89fa224e61921c4ee2d5b48094
|
|
3
|
+
size 1133
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { expect } from "chai";
|
|
2
2
|
|
|
3
3
|
import { Material } from "../../src/js/material";
|
|
4
|
-
import { Na4Cl4, Silicon } from "./fixtures";
|
|
4
|
+
import { Graphene, Na4Cl4, Silicon } from "./fixtures";
|
|
5
5
|
|
|
6
6
|
const newBasisXYZ = `Si 0.000000 0.000000 0.000000
|
|
7
7
|
Ge 0.250000 0.250000 0.250000
|
|
@@ -19,4 +19,17 @@ describe("Material", () => {
|
|
|
19
19
|
clonedMaterial.setBasis(newBasisXYZ, "xyz", clonedMaterial.Basis.units);
|
|
20
20
|
expect(clonedMaterial.Basis.elements).to.have.lengthOf(2);
|
|
21
21
|
});
|
|
22
|
+
|
|
23
|
+
describe("calculateHash", () => {
|
|
24
|
+
[
|
|
25
|
+
{ name: "Silicon", fixture: Silicon },
|
|
26
|
+
{ name: "Graphene", fixture: Graphene },
|
|
27
|
+
].forEach(({ name, fixture }) => {
|
|
28
|
+
it(`should match expected hash for ${name}`, () => {
|
|
29
|
+
const material = new Material(fixture);
|
|
30
|
+
expect(material.calculateHash()).to.equal((fixture as any).hash);
|
|
31
|
+
expect(material.scaledHash).to.equal((fixture as any).scaledHash);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
22
35
|
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
1
4
|
import numpy as np
|
|
2
5
|
import pytest
|
|
3
6
|
from mat3ra.made.basis import Basis, Coordinates
|
|
@@ -8,6 +11,13 @@ from unit.fixtures.bulk import BULK_Si_PRIMITIVE
|
|
|
8
11
|
from unit.fixtures.slab import BULK_Si_CONVENTIONAL
|
|
9
12
|
from unit.utils import assert_two_entities_deep_almost_equal
|
|
10
13
|
|
|
14
|
+
FIXTURES_DIR = Path(__file__).parents[2] / "fixtures"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_fixture(name: str) -> dict:
|
|
18
|
+
with open(FIXTURES_DIR / name) as f:
|
|
19
|
+
return json.load(f)
|
|
20
|
+
|
|
11
21
|
|
|
12
22
|
def test_create_default():
|
|
13
23
|
material = Material.create_default()
|
|
@@ -118,3 +128,11 @@ def test_set_labels_from_list(initial_labels, reset_labels, expected_final):
|
|
|
118
128
|
|
|
119
129
|
assert len(material.basis.labels.values) == len(expected_final)
|
|
120
130
|
assert material.basis.labels.values == expected_final
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.mark.parametrize("fixture_file", ["si-standata.json", "Graphene.json"])
|
|
134
|
+
def test_calculate_hash(fixture_file):
|
|
135
|
+
fixture = load_fixture(fixture_file)
|
|
136
|
+
material = Material.create(fixture)
|
|
137
|
+
assert material.hash == fixture["hash"]
|
|
138
|
+
assert material.scaled_hash == fixture["scaledHash"]
|
|
@@ -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
|