From 632e56431502cbf91c7b8cd66ade35fe8bd28b3f Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 12 Dec 2025 16:51:31 -0800 Subject: [PATCH 01/22] update: add maxwell disorder (cursor) --- pyproject.toml | 1 + src/py/mat3ra/made/periodic_table.py | 85 +++++++++++++++++++ .../core/modifications/perturb/__init__.py | 10 ++- .../perturb/functions/__init__.py | 2 + .../functions/elemental_function_holder.py | 78 +++++++++++++++++ .../perturb/maxwell_boltzmann.py | 80 +++++++++++++++++ .../made/tools/operations/core/unary.py | 13 ++- 7 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 src/py/mat3ra/made/periodic_table.py create mode 100644 src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py create mode 100644 src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/maxwell_boltzmann.py diff --git a/pyproject.toml b/pyproject.toml index 6822785c0..e83f4f786 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ tools = [ "pymatgen==2024.4.13", "ase", "pymatgen-analysis-defects==2024.4.23", + "mat3ra-periodic-table>=2025.1.18", ] dev = [ "pre-commit", diff --git a/src/py/mat3ra/made/periodic_table.py b/src/py/mat3ra/made/periodic_table.py new file mode 100644 index 000000000..c6eb7ca07 --- /dev/null +++ b/src/py/mat3ra/made/periodic_table.py @@ -0,0 +1,85 @@ +import json +from pathlib import Path +from typing import Dict, Optional + +from mat3ra.periodic_table import PERIODIC_TABLE + + +_PERIODIC_TABLE_DATA: Optional[Dict[str, float]] = None + + +def _load_periodic_table_data() -> Dict[str, float]: + global _PERIODIC_TABLE_DATA + if _PERIODIC_TABLE_DATA is not None: + return _PERIODIC_TABLE_DATA + + if PERIODIC_TABLE is None: + raise ImportError( + "mat3ra.periodic_table is required for periodic table functionality. " + "Install it with: pip install mat3ra-made[tools] or pip install mat3ra-periodic-table" + ) + + _PERIODIC_TABLE_DATA = {} + possible_mass_fields = [ + "atomic_mass", + "atomicMass", + "mass", + "atomic_weight", + "atomicWeight", + "standard_atomic_weight", + ] + + for symbol, element_data in PERIODIC_TABLE.items(): + atomic_mass = None + for field in possible_mass_fields: + if field in element_data: + atomic_mass = element_data[field] + break + + if atomic_mass is not None: + _PERIODIC_TABLE_DATA[symbol] = float(atomic_mass) + else: + available_keys = list(element_data.keys()) + raise ValueError( + f"Atomic mass not found for element {symbol} in PERIODIC_TABLE. " f"Available keys: {available_keys}" + ) + + return _PERIODIC_TABLE_DATA + + +def get_atomic_mass_from_element(element: str) -> float: + """ + Get atomic mass for an element symbol. + + Args: + element: Element symbol (e.g., "Si", "H", "O") + + Returns: + Atomic mass in atomic mass units (amu) + + Raises: + ValueError: If element symbol is not found + ImportError: If mat3ra.periodic_table is not installed + """ + data = _load_periodic_table_data() + element_upper = element.strip().capitalize() + if element_upper not in data: + raise ValueError(f"Element symbol '{element}' not found in periodic table") + return data[element_upper] + + +def export_periodic_table_json(output_path: Optional[Path] = None) -> Dict[str, float]: + """ + Export periodic table data as JSON. + + Args: + output_path: Optional path to save JSON file. If None, returns dict only. + + Returns: + Dictionary mapping element symbols to atomic masses + """ + data = _load_periodic_table_data() + if output_path is not None: + with open(output_path, "w") as f: + json.dump(data, f, indent=2) + return data diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py index 327a92ee9..5a61a2e10 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py @@ -1,7 +1,15 @@ -from .functions import FunctionHolder, PerturbationFunctionHolder, SineWavePerturbationFunctionHolder +from .functions import ( + ElementalFunctionHolder, + FunctionHolder, + PerturbationFunctionHolder, + SineWavePerturbationFunctionHolder, +) +from .maxwell_boltzmann import create_maxwell_displacement_function __all__ = [ + "ElementalFunctionHolder", "FunctionHolder", "PerturbationFunctionHolder", "SineWavePerturbationFunctionHolder", + "create_maxwell_displacement_function", ] diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py index 4449011cb..aded5d8be 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py @@ -1,8 +1,10 @@ +from .elemental_function_holder import ElementalFunctionHolder from .function_holder import FunctionHolder from .perturbation_function_holder import PerturbationFunctionHolder from .sine_wave_perturbation_function_holder import SineWavePerturbationFunctionHolder __all__ = [ + "ElementalFunctionHolder", "FunctionHolder", "PerturbationFunctionHolder", "SineWavePerturbationFunctionHolder", diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py new file mode 100644 index 000000000..15c99f447 --- /dev/null +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py @@ -0,0 +1,78 @@ +from typing import Any, Callable, List, Optional, Union + +import sympy as sp +from mat3ra.code.entity import InMemoryEntityPydantic +from pydantic import field_serializer +from sympy import parse_expr + +from mat3ra.made.utils import AXIS_TO_INDEX_MAP + + +class ElementalFunctionHolder(InMemoryEntityPydantic): + variables: List[str] = ["x", "y", "z", "m"] + symbols: List[sp.Symbol] = sp.symbols(["x", "y", "z", "m"]) + function: sp.Expr = sp.Symbol("f") + function_numeric: Callable = None + atom_masses: List[float] = [] + + def __init__( + self, + function: Union[sp.Expr, str], + atom_masses: List[float], + variables: Optional[List[str]] = None, + **data: Any, + ): + expr = self._to_expr(function) + + if variables is None: + vs = sorted(expr.free_symbols, key=lambda s: s.name) + variables = [str(v) for v in vs] or ["x", "y", "z", "m"] + + super().__init__(**data) + + self.variables = variables + self.symbols = sp.symbols(variables) + self.function = expr + self.atom_masses = atom_masses + + self.function_numeric = sp.lambdify(self.symbols, self.function, modules=["numpy"]) + + @staticmethod + def _to_expr(expr_or_str: Union[sp.Expr, str]) -> sp.Expr: + if isinstance(expr_or_str, sp.Expr): + return expr_or_str + if isinstance(expr_or_str, str): + return parse_expr(expr_or_str, evaluate=True) + raise TypeError(f"Expected sympy.Expr or str, got {type(expr_or_str)}") + + @property + def function_str(self) -> str: + return str(self.function) + + def apply_function(self, coordinate: List[float], atom_index: int) -> Union[float, List[float]]: + if atom_index < 0 or atom_index >= len(self.atom_masses): + raise ValueError(f"Atom index {atom_index} out of range [0, {len(self.atom_masses)})") + + mass = self.atom_masses[atom_index] + values = [] + for var in self.variables: + if var == "m": + values.append(mass) + elif var in AXIS_TO_INDEX_MAP: + values.append(coordinate[AXIS_TO_INDEX_MAP[var]]) + else: + raise ValueError(f"Unknown variable: {var}") + + return self.function_numeric(*values) + + @field_serializer("function") + def serialize_function(self, value: sp.Expr) -> str: + return str(value) + + @field_serializer("symbols") + def serialize_symbols(self, value: List[sp.Symbol]) -> List[str]: + return [str(symbol) for symbol in value] + + @field_serializer("function_numeric") + def serialize_function_numeric(self, value: Callable) -> str: + return "lambdified_function" diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/maxwell_boltzmann.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/maxwell_boltzmann.py new file mode 100644 index 000000000..08d02d996 --- /dev/null +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/maxwell_boltzmann.py @@ -0,0 +1,80 @@ +from typing import Any, List, Optional + +import numpy as np +import sympy as sp +from mat3ra.made.material import Material +from mat3ra.made.periodic_table import get_atomic_mass_from_element +from pydantic import Field + +from .functions import ElementalFunctionHolder + +BOLTZMANN_CONSTANT_EV_PER_K = 8.617333262145e-5 + + +class MaxwellBoltzmannDisplacementHolder(ElementalFunctionHolder): + temperature: float = Field(exclude=True) + kT: float = Field(exclude=True) + random_state: Any = Field(default=None, exclude=True) + + def __init__( + self, + atom_masses: List[float], + temperature_in_kelvin: float, + random_seed: Optional[int] = None, + ): + if random_seed is not None: + np.random.seed(random_seed) + + temperature = temperature_in_kelvin + kT = BOLTZMANN_CONSTANT_EV_PER_K * temperature_in_kelvin + random_state = np.random.RandomState(random_seed) if random_seed is not None else np.random + + function_expr = sp.Symbol("f") + super().__init__( + function=function_expr, + atom_masses=atom_masses, + temperature=temperature, + kT=kT, + random_state=random_state, + ) + + def apply_function(self, coordinate: List[float], atom_index: int) -> List[float]: + if atom_index < 0 or atom_index >= len(self.atom_masses): + raise ValueError(f"Atom index {atom_index} out of range [0, {len(self.atom_masses)})") + + mass = self.atom_masses[atom_index] + variance = self.kT / mass + std_dev = np.sqrt(variance) + displacement = self.random_state.normal(0.0, std_dev, size=3) + return displacement.tolist() + + +def create_maxwell_displacement_function( + material: Material, temperature_in_kelvin: float, random_seed: Optional[int] = None +) -> MaxwellBoltzmannDisplacementHolder: + """ + Create a Maxwell-Boltzmann displacement function for thermal perturbations. + + The function generates random 3D displacement vectors where each component + follows a normal distribution with variance proportional to kT/m, where + k is Boltzmann's constant, T is temperature, and m is atomic mass. + + Args: + material: The material containing atoms to be perturbed. + temperature_in_kelvin: Temperature in Kelvin. + random_seed: Optional random seed for deterministic behavior. + + Returns: + MaxwellBoltzmannDisplacementHolder that generates Maxwell-Boltzmann displacements. + """ + atom_masses = [] + for element_value in material.basis.elements.values: + atomic_mass = get_atomic_mass_from_element(element_value) + atom_masses.append(atomic_mass) + + return MaxwellBoltzmannDisplacementHolder( + atom_masses=atom_masses, + temperature_in_kelvin=temperature_in_kelvin, + random_seed=random_seed, + ) + diff --git a/src/py/mat3ra/made/tools/operations/core/unary.py b/src/py/mat3ra/made/tools/operations/core/unary.py index 2c6eb0f43..63525a6bb 100644 --- a/src/py/mat3ra/made/tools/operations/core/unary.py +++ b/src/py/mat3ra/made/tools/operations/core/unary.py @@ -7,6 +7,7 @@ from ...build_components.metadata import MaterialWithBuildMetadata from ...build_components.operations.core.modifications.perturb import FunctionHolder +from ...build_components.operations.core.modifications.perturb.functions import ElementalFunctionHolder from ...convert import from_ase, to_ase from ...modify import translate_by_vector, wrap_to_unit_cell from ...third_party import ase_make_supercell @@ -83,9 +84,15 @@ def perturb( original_coordinates = new_material.basis.coordinates.values perturbed_coordinates: List[List[float]] = [] - for coordinate in original_coordinates: - # If func_holder returns a scalar, assume z-axis; otherwise vector - displacement = perturbation_function.apply_function(coordinate) + is_elemental = isinstance(perturbation_function, ElementalFunctionHolder) + + for atom_index, coordinate in enumerate(original_coordinates): + if is_elemental: + displacement = perturbation_function.apply_function(coordinate, atom_index) + else: + # If func_holder returns a scalar, assume z-axis; otherwise vector + displacement = perturbation_function.apply_function(coordinate) + if isinstance(displacement, (list, tuple, np.ndarray)): delta = np.array(displacement) else: From f841bfe23fcc41531b725cd754befa0631679f94 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 12 Dec 2025 16:51:53 -0800 Subject: [PATCH 02/22] update: add maxwell disorder test --- tests/py/unit/test_maxwell_boltzmann.py | 150 ++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 tests/py/unit/test_maxwell_boltzmann.py diff --git a/tests/py/unit/test_maxwell_boltzmann.py b/tests/py/unit/test_maxwell_boltzmann.py new file mode 100644 index 000000000..1533fa6ae --- /dev/null +++ b/tests/py/unit/test_maxwell_boltzmann.py @@ -0,0 +1,150 @@ +import numpy as np +import pytest + + +try: + from mat3ra.periodic_table import PERIODIC_TABLE + + PERIODIC_TABLE_AVAILABLE = True +except ImportError: + PERIODIC_TABLE_AVAILABLE = False + +from mat3ra.made.material import Material +from mat3ra.made.periodic_table import get_atomic_mass_from_element +from mat3ra.made.tools.build_components.operations.core.modifications.perturb.maxwell_boltzmann import ( + BOLTZMANN_CONSTANT_EV_PER_K, + create_maxwell_displacement_function, +) +from mat3ra.made.tools.helpers import create_supercell +from mat3ra.made.tools.operations.core.unary import perturb + +from .fixtures.bulk import BULK_Si_PRIMITIVE +from .fixtures.slab import SI_CONVENTIONAL_SLAB_001 + +ELEMENT_SYMBOL_TO_MASS_TEST_CASES = [ + ("H", 1.008), + ("He", 4.003), + ("Li", 6.941), + ("C", 12.011), + ("N", 14.007), + ("O", 15.999), + ("Si", 28.085), + ("Fe", 55.845), + ("Cu", 63.546), + ("Au", 196.967), +] + +TEMPERATURE_K = 300.0 +RANDOM_SEED = 42 +NUM_SAMPLES_FOR_MSD = 1000 + + +@pytest.mark.parametrize("random_seed", [None, 42, 123, 999]) +def test_maxwell_displacement_deterministic(random_seed): + material = Material.create(BULK_Si_PRIMITIVE) + displacement_func1 = create_maxwell_displacement_function( + material, temperature_in_kelvin=TEMPERATURE_K, random_seed=random_seed + ) + displacement_func2 = create_maxwell_displacement_function( + material, temperature_in_kelvin=TEMPERATURE_K, random_seed=random_seed + ) + + if random_seed is not None: + coord = [0.0, 0.0, 0.0] + disp1 = displacement_func1.apply_function(coord, atom_index=0) + disp2 = displacement_func2.apply_function(coord, atom_index=0) + assert np.allclose(disp1, disp2) + else: + coord = [0.0, 0.0, 0.0] + disp1 = displacement_func1.apply_function(coord, atom_index=0) + disp2 = displacement_func2.apply_function(coord, atom_index=0) + assert not np.allclose(disp1, disp2) or np.allclose(disp1, [0, 0, 0], atol=1e-10) + + +def test_maxwell_displacement_perturb_integration(): + material = Material.create(BULK_Si_PRIMITIVE) + original_coords = [coord[:] for coord in material.basis.coordinates.values] + + displacement_func = create_maxwell_displacement_function( + material, temperature_in_kelvin=TEMPERATURE_K, random_seed=RANDOM_SEED + ) + + perturbed_material = perturb(material, displacement_func, use_cartesian_coordinates=True) + + assert len(perturbed_material.basis.coordinates.values) == len(original_coords) + for i, (orig, pert) in enumerate(zip(original_coords, perturbed_material.basis.coordinates.values)): + delta = np.array(pert) - np.array(orig) + assert np.linalg.norm(delta) > 0 or np.allclose(delta, 0, atol=1e-10) + + +def test_maxwell_displacement_msd_expectation(): + material = Material.create(BULK_Si_PRIMITIVE) + si_mass = get_atomic_mass_from_element("Si") + temperature = TEMPERATURE_K + kT = BOLTZMANN_CONSTANT_EV_PER_K * temperature + expected_variance = kT / si_mass + expected_msd = 3 * expected_variance + + displacements = [] + for _ in range(NUM_SAMPLES_FOR_MSD): + displacement_func = create_maxwell_displacement_function( + material, temperature_in_kelvin=temperature, random_seed=None + ) + coord = [0.0, 0.0, 0.0] + disp = displacement_func.apply_function(coord, atom_index=0) + displacements.append(disp) + + displacements_array = np.array(displacements) + msd = np.mean(np.sum(displacements_array**2, axis=1)) + + assert abs(msd - expected_msd) / expected_msd < 0.3 + + +@pytest.mark.parametrize( + "slab_config, temperature_k, random_seed", + [ + (SI_CONVENTIONAL_SLAB_001, 1300.0, 42), + (SI_CONVENTIONAL_SLAB_001, 1300.0, 42), + ], +) +@pytest.mark.skipif( + not PERIODIC_TABLE_AVAILABLE, + reason="mat3ra-periodic-table not installed", +) +def test_maxwell_boltzmann_on_slab(slab_config, temperature_k, random_seed): + material = Material.create(slab_config) + material = create_supercell(material, scaling_factor=[4, 4, 1]) + original_coords = [coord[:] for coord in material.basis.coordinates.values] + original_lattice = material.lattice.vector_arrays.copy() + + displacement_func = create_maxwell_displacement_function( + material, temperature_in_kelvin=temperature_k, random_seed=random_seed + ) + + perturbed_material = perturb(material, displacement_func, use_cartesian_coordinates=True) + + assert len(perturbed_material.basis.coordinates.values) == len(original_coords) + assert len(perturbed_material.basis.elements.values) == len(material.basis.elements.values) + + coordinate_changes = [] + for i, (orig, pert) in enumerate(zip(original_coords, perturbed_material.basis.coordinates.values)): + delta = np.array(pert) - np.array(orig) + displacement_magnitude = np.linalg.norm(delta) + coordinate_changes.append(displacement_magnitude) + + max_displacement = max(coordinate_changes) + mean_displacement = np.mean(coordinate_changes) + + assert max_displacement > 0 + assert mean_displacement > 0 + + si_mass = get_atomic_mass_from_element("Si") + kT = BOLTZMANN_CONSTANT_EV_PER_K * temperature_k + expected_std = np.sqrt(kT / si_mass) + + assert mean_displacement < 5 * expected_std + + assert np.allclose(perturbed_material.lattice.vector_arrays, original_lattice, atol=1e-10) + + for i, element in enumerate(material.basis.elements.values): + assert perturbed_material.basis.elements.values[i] == element From f11da254fb7a883c059a698fba69fa7074b268d4 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 15 Dec 2025 14:30:25 -0800 Subject: [PATCH 03/22] chore: move --- .../core/modifications/perturb/__init__.py | 4 +- .../perturb/functions/__init__.py | 6 +++ .../{ => functions}/maxwell_boltzmann.py | 49 +++++++++++++------ tests/py/unit/test_maxwell_boltzmann.py | 2 +- 4 files changed, 43 insertions(+), 18 deletions(-) rename src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/{ => functions}/maxwell_boltzmann.py (57%) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py index 5a61a2e10..916a1884d 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py @@ -1,14 +1,16 @@ from .functions import ( ElementalFunctionHolder, FunctionHolder, + MaxwellBoltzmannDisplacementHolder, PerturbationFunctionHolder, SineWavePerturbationFunctionHolder, + create_maxwell_displacement_function, ) -from .maxwell_boltzmann import create_maxwell_displacement_function __all__ = [ "ElementalFunctionHolder", "FunctionHolder", + "MaxwellBoltzmannDisplacementHolder", "PerturbationFunctionHolder", "SineWavePerturbationFunctionHolder", "create_maxwell_displacement_function", diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py index aded5d8be..5603e6253 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py @@ -1,11 +1,17 @@ from .elemental_function_holder import ElementalFunctionHolder from .function_holder import FunctionHolder +from .maxwell_boltzmann import ( + MaxwellBoltzmannDisplacementHolder, + create_maxwell_displacement_function, +) from .perturbation_function_holder import PerturbationFunctionHolder from .sine_wave_perturbation_function_holder import SineWavePerturbationFunctionHolder __all__ = [ "ElementalFunctionHolder", "FunctionHolder", + "MaxwellBoltzmannDisplacementHolder", "PerturbationFunctionHolder", "SineWavePerturbationFunctionHolder", + "create_maxwell_displacement_function", ] diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/maxwell_boltzmann.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py similarity index 57% rename from src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/maxwell_boltzmann.py rename to src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py index 08d02d996..c0adc6e28 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/maxwell_boltzmann.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py @@ -6,75 +6,92 @@ from mat3ra.made.periodic_table import get_atomic_mass_from_element from pydantic import Field -from .functions import ElementalFunctionHolder +from .elemental_function_holder import ElementalFunctionHolder BOLTZMANN_CONSTANT_EV_PER_K = 8.617333262145e-5 class MaxwellBoltzmannDisplacementHolder(ElementalFunctionHolder): - temperature: float = Field(exclude=True) + disorder_parameter: float = Field(exclude=True) kT: float = Field(exclude=True) random_state: Any = Field(default=None, exclude=True) + is_mass_used: bool = Field(default=True, exclude=True) def __init__( self, atom_masses: List[float], - temperature_in_kelvin: float, + disorder_parameter: float, random_seed: Optional[int] = None, + is_mass_used: bool = True, ): if random_seed is not None: np.random.seed(random_seed) - temperature = temperature_in_kelvin - kT = BOLTZMANN_CONSTANT_EV_PER_K * temperature_in_kelvin + kT = BOLTZMANN_CONSTANT_EV_PER_K * disorder_parameter random_state = np.random.RandomState(random_seed) if random_seed is not None else np.random function_expr = sp.Symbol("f") super().__init__( function=function_expr, atom_masses=atom_masses, - temperature=temperature, + disorder_parameter=disorder_parameter, kT=kT, random_state=random_state, + is_mass_used=is_mass_used, ) def apply_function(self, coordinate: List[float], atom_index: int) -> List[float]: if atom_index < 0 or atom_index >= len(self.atom_masses): raise ValueError(f"Atom index {atom_index} out of range [0, {len(self.atom_masses)})") - mass = self.atom_masses[atom_index] - variance = self.kT / mass + if self.is_mass_used: + mass = self.atom_masses[atom_index] + variance = self.kT / mass + else: + variance = self.kT + std_dev = np.sqrt(variance) displacement = self.random_state.normal(0.0, std_dev, size=3) return displacement.tolist() def create_maxwell_displacement_function( - material: Material, temperature_in_kelvin: float, random_seed: Optional[int] = None + material: Material, + disorder_parameter: float, + random_seed: Optional[int] = None, + is_mass_used: bool = True, ) -> MaxwellBoltzmannDisplacementHolder: """ Create a Maxwell-Boltzmann displacement function for thermal perturbations. The function generates random 3D displacement vectors where each component - follows a normal distribution with variance proportional to kT/m, where - k is Boltzmann's constant, T is temperature, and m is atomic mass. + follows a normal distribution with variance proportional to kT/m (if is_mass_used=True) + or kT (if is_mass_used=False), where k is Boltzmann's constant, T is the disorder + parameter, and m is atomic mass. Args: material: The material containing atoms to be perturbed. - temperature_in_kelvin: Temperature in Kelvin. + disorder_parameter: Disorder parameter (typically temperature in Kelvin for + Maxwell-Boltzmann distribution). random_seed: Optional random seed for deterministic behavior. + is_mass_used: If True, displacement variance is kT/m (mass-dependent). + If False, displacement variance is kT (mass-independent). Returns: MaxwellBoltzmannDisplacementHolder that generates Maxwell-Boltzmann displacements. """ atom_masses = [] - for element_value in material.basis.elements.values: - atomic_mass = get_atomic_mass_from_element(element_value) - atom_masses.append(atomic_mass) + if is_mass_used: + for element_value in material.basis.elements.values: + atomic_mass = get_atomic_mass_from_element(element_value) + atom_masses.append(atomic_mass) + else: + atom_masses = [1.0] * len(material.basis.elements.values) return MaxwellBoltzmannDisplacementHolder( atom_masses=atom_masses, - temperature_in_kelvin=temperature_in_kelvin, + disorder_parameter=disorder_parameter, random_seed=random_seed, + is_mass_used=is_mass_used, ) diff --git a/tests/py/unit/test_maxwell_boltzmann.py b/tests/py/unit/test_maxwell_boltzmann.py index 1533fa6ae..831f3ff8f 100644 --- a/tests/py/unit/test_maxwell_boltzmann.py +++ b/tests/py/unit/test_maxwell_boltzmann.py @@ -11,7 +11,7 @@ from mat3ra.made.material import Material from mat3ra.made.periodic_table import get_atomic_mass_from_element -from mat3ra.made.tools.build_components.operations.core.modifications.perturb.maxwell_boltzmann import ( +from mat3ra.made.tools.build_components.operations.core.modifications.perturb.functions.maxwell_boltzmann import ( BOLTZMANN_CONSTANT_EV_PER_K, create_maxwell_displacement_function, ) From 962ff0fe9fb7d35712f95f67655b9f557a3224ab Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 15 Dec 2025 14:33:18 -0800 Subject: [PATCH 04/22] chore: update test --- tests/py/unit/test_maxwell_boltzmann.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/tests/py/unit/test_maxwell_boltzmann.py b/tests/py/unit/test_maxwell_boltzmann.py index 831f3ff8f..294a25ce5 100644 --- a/tests/py/unit/test_maxwell_boltzmann.py +++ b/tests/py/unit/test_maxwell_boltzmann.py @@ -1,14 +1,6 @@ import numpy as np import pytest - -try: - from mat3ra.periodic_table import PERIODIC_TABLE - - PERIODIC_TABLE_AVAILABLE = True -except ImportError: - PERIODIC_TABLE_AVAILABLE = False - from mat3ra.made.material import Material from mat3ra.made.periodic_table import get_atomic_mass_from_element from mat3ra.made.tools.build_components.operations.core.modifications.perturb.functions.maxwell_boltzmann import ( @@ -43,10 +35,10 @@ def test_maxwell_displacement_deterministic(random_seed): material = Material.create(BULK_Si_PRIMITIVE) displacement_func1 = create_maxwell_displacement_function( - material, temperature_in_kelvin=TEMPERATURE_K, random_seed=random_seed + material, disorder_parameter=TEMPERATURE_K, random_seed=random_seed ) displacement_func2 = create_maxwell_displacement_function( - material, temperature_in_kelvin=TEMPERATURE_K, random_seed=random_seed + material, disorder_parameter=TEMPERATURE_K, random_seed=random_seed ) if random_seed is not None: @@ -66,7 +58,7 @@ def test_maxwell_displacement_perturb_integration(): original_coords = [coord[:] for coord in material.basis.coordinates.values] displacement_func = create_maxwell_displacement_function( - material, temperature_in_kelvin=TEMPERATURE_K, random_seed=RANDOM_SEED + material, disorder_parameter=TEMPERATURE_K, random_seed=RANDOM_SEED ) perturbed_material = perturb(material, displacement_func, use_cartesian_coordinates=True) @@ -88,7 +80,7 @@ def test_maxwell_displacement_msd_expectation(): displacements = [] for _ in range(NUM_SAMPLES_FOR_MSD): displacement_func = create_maxwell_displacement_function( - material, temperature_in_kelvin=temperature, random_seed=None + material, disorder_parameter=temperature, random_seed=None ) coord = [0.0, 0.0, 0.0] disp = displacement_func.apply_function(coord, atom_index=0) @@ -107,10 +99,6 @@ def test_maxwell_displacement_msd_expectation(): (SI_CONVENTIONAL_SLAB_001, 1300.0, 42), ], ) -@pytest.mark.skipif( - not PERIODIC_TABLE_AVAILABLE, - reason="mat3ra-periodic-table not installed", -) def test_maxwell_boltzmann_on_slab(slab_config, temperature_k, random_seed): material = Material.create(slab_config) material = create_supercell(material, scaling_factor=[4, 4, 1]) @@ -118,7 +106,7 @@ def test_maxwell_boltzmann_on_slab(slab_config, temperature_k, random_seed): original_lattice = material.lattice.vector_arrays.copy() displacement_func = create_maxwell_displacement_function( - material, temperature_in_kelvin=temperature_k, random_seed=random_seed + material, disorder_parameter=temperature_k, random_seed=random_seed ) perturbed_material = perturb(material, displacement_func, use_cartesian_coordinates=True) From 227ae3f9c66c1489604afa91cd034af100c5e6b2 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 26 Dec 2025 12:24:06 -0800 Subject: [PATCH 05/22] update: use OOP --- .../core/modifications/perturb/__init__.py | 4 +- .../perturb/functions/__init__.py | 4 +- .../functions/elemental_function_holder.py | 48 ++----------------- .../perturb/functions/maxwell_boltzmann.py | 4 +- .../made/tools/operations/core/unary.py | 4 +- 5 files changed, 13 insertions(+), 51 deletions(-) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py index 916a1884d..ca6c6e771 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py @@ -1,5 +1,5 @@ from .functions import ( - ElementalFunctionHolder, + AtomicMassDependentFunctionHolder, FunctionHolder, MaxwellBoltzmannDisplacementHolder, PerturbationFunctionHolder, @@ -8,7 +8,7 @@ ) __all__ = [ - "ElementalFunctionHolder", + "AtomicMassDependentFunctionHolder", "FunctionHolder", "MaxwellBoltzmannDisplacementHolder", "PerturbationFunctionHolder", diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py index 5603e6253..e1ba63a05 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py @@ -1,4 +1,4 @@ -from .elemental_function_holder import ElementalFunctionHolder +from .elemental_function_holder import AtomicMassDependentFunctionHolder from .function_holder import FunctionHolder from .maxwell_boltzmann import ( MaxwellBoltzmannDisplacementHolder, @@ -8,7 +8,7 @@ from .sine_wave_perturbation_function_holder import SineWavePerturbationFunctionHolder __all__ = [ - "ElementalFunctionHolder", + "AtomicMassDependentFunctionHolder", "FunctionHolder", "MaxwellBoltzmannDisplacementHolder", "PerturbationFunctionHolder", diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py index 15c99f447..72e43b216 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py @@ -1,18 +1,12 @@ -from typing import Any, Callable, List, Optional, Union +from typing import Any, List, Optional, Union import sympy as sp -from mat3ra.code.entity import InMemoryEntityPydantic -from pydantic import field_serializer -from sympy import parse_expr -from mat3ra.made.utils import AXIS_TO_INDEX_MAP +from .function_holder import AXIS_TO_INDEX_MAP, FunctionHolder -class ElementalFunctionHolder(InMemoryEntityPydantic): +class AtomicMassDependentFunctionHolder(FunctionHolder): variables: List[str] = ["x", "y", "z", "m"] - symbols: List[sp.Symbol] = sp.symbols(["x", "y", "z", "m"]) - function: sp.Expr = sp.Symbol("f") - function_numeric: Callable = None atom_masses: List[float] = [] def __init__( @@ -22,32 +16,12 @@ def __init__( variables: Optional[List[str]] = None, **data: Any, ): - expr = self._to_expr(function) - if variables is None: + expr = self._to_expr(function) vs = sorted(expr.free_symbols, key=lambda s: s.name) variables = [str(v) for v in vs] or ["x", "y", "z", "m"] - super().__init__(**data) - - self.variables = variables - self.symbols = sp.symbols(variables) - self.function = expr - self.atom_masses = atom_masses - - self.function_numeric = sp.lambdify(self.symbols, self.function, modules=["numpy"]) - - @staticmethod - def _to_expr(expr_or_str: Union[sp.Expr, str]) -> sp.Expr: - if isinstance(expr_or_str, sp.Expr): - return expr_or_str - if isinstance(expr_or_str, str): - return parse_expr(expr_or_str, evaluate=True) - raise TypeError(f"Expected sympy.Expr or str, got {type(expr_or_str)}") - - @property - def function_str(self) -> str: - return str(self.function) + super().__init__(function=function, variables=variables, atom_masses=atom_masses, **data) def apply_function(self, coordinate: List[float], atom_index: int) -> Union[float, List[float]]: if atom_index < 0 or atom_index >= len(self.atom_masses): @@ -64,15 +38,3 @@ def apply_function(self, coordinate: List[float], atom_index: int) -> Union[floa raise ValueError(f"Unknown variable: {var}") return self.function_numeric(*values) - - @field_serializer("function") - def serialize_function(self, value: sp.Expr) -> str: - return str(value) - - @field_serializer("symbols") - def serialize_symbols(self, value: List[sp.Symbol]) -> List[str]: - return [str(symbol) for symbol in value] - - @field_serializer("function_numeric") - def serialize_function_numeric(self, value: Callable) -> str: - return "lambdified_function" diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py index c0adc6e28..2334163ce 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py @@ -6,12 +6,12 @@ from mat3ra.made.periodic_table import get_atomic_mass_from_element from pydantic import Field -from .elemental_function_holder import ElementalFunctionHolder +from .elemental_function_holder import AtomicMassDependentFunctionHolder BOLTZMANN_CONSTANT_EV_PER_K = 8.617333262145e-5 -class MaxwellBoltzmannDisplacementHolder(ElementalFunctionHolder): +class MaxwellBoltzmannDisplacementHolder(AtomicMassDependentFunctionHolder): disorder_parameter: float = Field(exclude=True) kT: float = Field(exclude=True) random_state: Any = Field(default=None, exclude=True) diff --git a/src/py/mat3ra/made/tools/operations/core/unary.py b/src/py/mat3ra/made/tools/operations/core/unary.py index 63525a6bb..25ed278c1 100644 --- a/src/py/mat3ra/made/tools/operations/core/unary.py +++ b/src/py/mat3ra/made/tools/operations/core/unary.py @@ -7,7 +7,7 @@ from ...build_components.metadata import MaterialWithBuildMetadata from ...build_components.operations.core.modifications.perturb import FunctionHolder -from ...build_components.operations.core.modifications.perturb.functions import ElementalFunctionHolder +from ...build_components.operations.core.modifications.perturb.functions import AtomicMassDependentFunctionHolder from ...convert import from_ase, to_ase from ...modify import translate_by_vector, wrap_to_unit_cell from ...third_party import ase_make_supercell @@ -84,7 +84,7 @@ def perturb( original_coordinates = new_material.basis.coordinates.values perturbed_coordinates: List[List[float]] = [] - is_elemental = isinstance(perturbation_function, ElementalFunctionHolder) + is_elemental = isinstance(perturbation_function, AtomicMassDependentFunctionHolder) for atom_index, coordinate in enumerate(original_coordinates): if is_elemental: From ccc8247fb6241bb2ea428cc1c73051fd00feb686 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 26 Dec 2025 13:27:58 -0800 Subject: [PATCH 06/22] update: remove temperatuirew --- .../perturb/functions/maxwell_boltzmann.py | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py index 2334163ce..f838c892b 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py @@ -8,12 +8,9 @@ from .elemental_function_holder import AtomicMassDependentFunctionHolder -BOLTZMANN_CONSTANT_EV_PER_K = 8.617333262145e-5 - class MaxwellBoltzmannDisplacementHolder(AtomicMassDependentFunctionHolder): disorder_parameter: float = Field(exclude=True) - kT: float = Field(exclude=True) random_state: Any = Field(default=None, exclude=True) is_mass_used: bool = Field(default=True, exclude=True) @@ -27,7 +24,6 @@ def __init__( if random_seed is not None: np.random.seed(random_seed) - kT = BOLTZMANN_CONSTANT_EV_PER_K * disorder_parameter random_state = np.random.RandomState(random_seed) if random_seed is not None else np.random function_expr = sp.Symbol("f") @@ -35,7 +31,6 @@ def __init__( function=function_expr, atom_masses=atom_masses, disorder_parameter=disorder_parameter, - kT=kT, random_state=random_state, is_mass_used=is_mass_used, ) @@ -53,45 +48,3 @@ def apply_function(self, coordinate: List[float], atom_index: int) -> List[float std_dev = np.sqrt(variance) displacement = self.random_state.normal(0.0, std_dev, size=3) return displacement.tolist() - - -def create_maxwell_displacement_function( - material: Material, - disorder_parameter: float, - random_seed: Optional[int] = None, - is_mass_used: bool = True, -) -> MaxwellBoltzmannDisplacementHolder: - """ - Create a Maxwell-Boltzmann displacement function for thermal perturbations. - - The function generates random 3D displacement vectors where each component - follows a normal distribution with variance proportional to kT/m (if is_mass_used=True) - or kT (if is_mass_used=False), where k is Boltzmann's constant, T is the disorder - parameter, and m is atomic mass. - - Args: - material: The material containing atoms to be perturbed. - disorder_parameter: Disorder parameter (typically temperature in Kelvin for - Maxwell-Boltzmann distribution). - random_seed: Optional random seed for deterministic behavior. - is_mass_used: If True, displacement variance is kT/m (mass-dependent). - If False, displacement variance is kT (mass-independent). - - Returns: - MaxwellBoltzmannDisplacementHolder that generates Maxwell-Boltzmann displacements. - """ - atom_masses = [] - if is_mass_used: - for element_value in material.basis.elements.values: - atomic_mass = get_atomic_mass_from_element(element_value) - atom_masses.append(atomic_mass) - else: - atom_masses = [1.0] * len(material.basis.elements.values) - - return MaxwellBoltzmannDisplacementHolder( - atom_masses=atom_masses, - disorder_parameter=disorder_parameter, - random_seed=random_seed, - is_mass_used=is_mass_used, - ) - From 6a3ecb647460f9f8bc6b8db44f33b8077e67decc Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 26 Dec 2025 14:21:03 -0800 Subject: [PATCH 07/22] update: generalize and reuse --- .../core/modifications/perturb/__init__.py | 3 +- .../perturb/functions/__init__.py | 6 +-- .../functions/elemental_function_holder.py | 17 +++--- .../perturb/functions/function_holder.py | 2 +- .../perturb/functions/maxwell_boltzmann.py | 18 +++---- .../core/modifications/perturb/helpers.py | 43 ++++++++++++++- .../made/tools/operations/core/unary.py | 12 ++--- tests/py/unit/test_maxwell_boltzmann.py | 52 +++++++++---------- 8 files changed, 91 insertions(+), 62 deletions(-) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py index ca6c6e771..07fe57480 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py @@ -4,8 +4,8 @@ MaxwellBoltzmannDisplacementHolder, PerturbationFunctionHolder, SineWavePerturbationFunctionHolder, - create_maxwell_displacement_function, ) +from .helpers import create_maxwell_displacement __all__ = [ "AtomicMassDependentFunctionHolder", @@ -13,5 +13,4 @@ "MaxwellBoltzmannDisplacementHolder", "PerturbationFunctionHolder", "SineWavePerturbationFunctionHolder", - "create_maxwell_displacement_function", ] diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py index e1ba63a05..d10896c38 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py @@ -1,9 +1,6 @@ from .elemental_function_holder import AtomicMassDependentFunctionHolder from .function_holder import FunctionHolder -from .maxwell_boltzmann import ( - MaxwellBoltzmannDisplacementHolder, - create_maxwell_displacement_function, -) +from .maxwell_boltzmann import MaxwellBoltzmannDisplacementHolder from .perturbation_function_holder import PerturbationFunctionHolder from .sine_wave_perturbation_function_holder import SineWavePerturbationFunctionHolder @@ -13,5 +10,4 @@ "MaxwellBoltzmannDisplacementHolder", "PerturbationFunctionHolder", "SineWavePerturbationFunctionHolder", - "create_maxwell_displacement_function", ] diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py index 72e43b216..231870d89 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py @@ -2,17 +2,16 @@ import sympy as sp +from mat3ra.made.periodic_table import get_atomic_mass_from_element from .function_holder import AXIS_TO_INDEX_MAP, FunctionHolder class AtomicMassDependentFunctionHolder(FunctionHolder): variables: List[str] = ["x", "y", "z", "m"] - atom_masses: List[float] = [] def __init__( self, function: Union[sp.Expr, str], - atom_masses: List[float], variables: Optional[List[str]] = None, **data: Any, ): @@ -21,13 +20,17 @@ def __init__( vs = sorted(expr.free_symbols, key=lambda s: s.name) variables = [str(v) for v in vs] or ["x", "y", "z", "m"] - super().__init__(function=function, variables=variables, atom_masses=atom_masses, **data) + super().__init__(function=function, variables=variables, **data) - def apply_function(self, coordinate: List[float], atom_index: int) -> Union[float, List[float]]: - if atom_index < 0 or atom_index >= len(self.atom_masses): - raise ValueError(f"Atom index {atom_index} out of range [0, {len(self.atom_masses)})") + def apply_function( + self, coordinate: List[float], material=None, atom_index: Optional[int] = None, **kwargs: Any + ) -> Union[float, List[float]]: + if material is None or atom_index is None: + raise ValueError("AtomicMassDependentFunctionHolder requires 'material' and 'atom_index' kwargs") + + element = material.basis.elements.values[atom_index] + mass = get_atomic_mass_from_element(element) - mass = self.atom_masses[atom_index] values = [] for var in self.variables: if var == "m": diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/function_holder.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/function_holder.py index 7b6961e5e..47e755e22 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/function_holder.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/function_holder.py @@ -52,7 +52,7 @@ def _to_expr(expr_or_str: Union[sp.Expr, str]) -> sp.Expr: def function_str(self) -> str: return str(self.function) - def apply_function(self, coordinate: List[float]) -> float: + def apply_function(self, coordinate: List[float], **kwargs: Any) -> float: values = [coordinate[AXIS_TO_INDEX_MAP[var]] for var in self.variables] return self.function_numeric(*values) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py index f838c892b..d803cbab0 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py @@ -1,8 +1,7 @@ -from typing import Any, List, Optional +from typing import Any, Optional import numpy as np import sympy as sp -from mat3ra.made.material import Material from mat3ra.made.periodic_table import get_atomic_mass_from_element from pydantic import Field @@ -16,7 +15,6 @@ class MaxwellBoltzmannDisplacementHolder(AtomicMassDependentFunctionHolder): def __init__( self, - atom_masses: List[float], disorder_parameter: float, random_seed: Optional[int] = None, is_mass_used: bool = True, @@ -29,21 +27,21 @@ def __init__( function_expr = sp.Symbol("f") super().__init__( function=function_expr, - atom_masses=atom_masses, disorder_parameter=disorder_parameter, random_state=random_state, is_mass_used=is_mass_used, ) - def apply_function(self, coordinate: List[float], atom_index: int) -> List[float]: - if atom_index < 0 or atom_index >= len(self.atom_masses): - raise ValueError(f"Atom index {atom_index} out of range [0, {len(self.atom_masses)})") + def apply_function(self, coordinate, material=None, atom_index: Optional[int] = None, **kwargs) -> list: + if material is None or atom_index is None: + raise ValueError("MaxwellBoltzmannDisplacementHolder requires 'material' and 'atom_index' kwargs") if self.is_mass_used: - mass = self.atom_masses[atom_index] - variance = self.kT / mass + element = material.basis.elements.values[atom_index] + mass = get_atomic_mass_from_element(element) + variance = self.disorder_parameter / mass else: - variance = self.kT + variance = self.disorder_parameter std_dev = np.sqrt(variance) displacement = self.random_state.normal(0.0, std_dev, size=3) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py index 26404b7cd..957ff2fb8 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Optional, Union import sympy as sp from mat3ra.made.material import Material @@ -8,6 +8,7 @@ from .builders.isometric import IsometricPerturbationBuilder from .configuration import PerturbationConfiguration from .functions import PerturbationFunctionHolder +from .functions.maxwell_boltzmann import MaxwellBoltzmannDisplacementHolder def create_perturbation( @@ -39,3 +40,43 @@ def create_perturbation( else: builder = PerturbationBuilder() return builder.get_material(configuration) + + +def create_maxwell_displacement( + material: Union[Material, MaterialWithBuildMetadata], + disorder_parameter: float, + random_seed: Optional[int] = None, + is_mass_used: bool = True, +) -> Material: + """ + Apply Maxwell-Boltzmann random displacements to a material. + + Generates random 3D displacement vectors where each component follows a normal + distribution with variance proportional to disorder_parameter/m (if is_mass_used=True) + or disorder_parameter (if is_mass_used=False), where m is atomic mass. + + Args: + material: The material to be perturbed. + disorder_parameter: Disorder parameter controlling displacement magnitude. + random_seed: Optional random seed for deterministic behavior. + is_mass_used: If True, displacement variance is disorder_parameter/m (mass-dependent). + If False, displacement variance is disorder_parameter (mass-independent). + + Returns: + Material with applied Maxwell-Boltzmann displacements. + """ + displacement_holder = MaxwellBoltzmannDisplacementHolder( + disorder_parameter=disorder_parameter, + random_seed=random_seed, + is_mass_used=is_mass_used, + ) + + configuration = PerturbationConfiguration( + material=material, + perturbation_function_holder=displacement_holder, + use_cartesian_coordinates=True, + ) + + builder = PerturbationBuilder() + + return builder.get_material(configuration) diff --git a/src/py/mat3ra/made/tools/operations/core/unary.py b/src/py/mat3ra/made/tools/operations/core/unary.py index 25ed278c1..3fd7512df 100644 --- a/src/py/mat3ra/made/tools/operations/core/unary.py +++ b/src/py/mat3ra/made/tools/operations/core/unary.py @@ -7,7 +7,6 @@ from ...build_components.metadata import MaterialWithBuildMetadata from ...build_components.operations.core.modifications.perturb import FunctionHolder -from ...build_components.operations.core.modifications.perturb.functions import AtomicMassDependentFunctionHolder from ...convert import from_ase, to_ase from ...modify import translate_by_vector, wrap_to_unit_cell from ...third_party import ase_make_supercell @@ -84,19 +83,14 @@ def perturb( original_coordinates = new_material.basis.coordinates.values perturbed_coordinates: List[List[float]] = [] - is_elemental = isinstance(perturbation_function, AtomicMassDependentFunctionHolder) - for atom_index, coordinate in enumerate(original_coordinates): - if is_elemental: - displacement = perturbation_function.apply_function(coordinate, atom_index) - else: - # If func_holder returns a scalar, assume z-axis; otherwise vector - displacement = perturbation_function.apply_function(coordinate) + displacement = perturbation_function.apply_function( + coordinate, material=new_material, atom_index=atom_index + ) if isinstance(displacement, (list, tuple, np.ndarray)): delta = np.array(displacement) else: - # scalar: apply to z-axis delta = np.array([0.0, 0.0, displacement]) new_coordinate = np.array(coordinate) + delta diff --git a/tests/py/unit/test_maxwell_boltzmann.py b/tests/py/unit/test_maxwell_boltzmann.py index 294a25ce5..a0e072049 100644 --- a/tests/py/unit/test_maxwell_boltzmann.py +++ b/tests/py/unit/test_maxwell_boltzmann.py @@ -3,9 +3,11 @@ from mat3ra.made.material import Material from mat3ra.made.periodic_table import get_atomic_mass_from_element +from mat3ra.made.tools.build_components.operations.core.modifications.perturb import ( + create_maxwell_displacement, +) from mat3ra.made.tools.build_components.operations.core.modifications.perturb.functions.maxwell_boltzmann import ( - BOLTZMANN_CONSTANT_EV_PER_K, - create_maxwell_displacement_function, + MaxwellBoltzmannDisplacementHolder, ) from mat3ra.made.tools.helpers import create_supercell from mat3ra.made.tools.operations.core.unary import perturb @@ -34,22 +36,23 @@ @pytest.mark.parametrize("random_seed", [None, 42, 123, 999]) def test_maxwell_displacement_deterministic(random_seed): material = Material.create(BULK_Si_PRIMITIVE) - displacement_func1 = create_maxwell_displacement_function( - material, disorder_parameter=TEMPERATURE_K, random_seed=random_seed + displacement_func1 = MaxwellBoltzmannDisplacementHolder( + disorder_parameter=TEMPERATURE_K, random_seed=random_seed ) - displacement_func2 = create_maxwell_displacement_function( - material, disorder_parameter=TEMPERATURE_K, random_seed=random_seed + displacement_func2 = MaxwellBoltzmannDisplacementHolder( + disorder_parameter=TEMPERATURE_K, random_seed=random_seed ) + coord = [0.0, 0.0, 0.0] + atom_index = 0 + if random_seed is not None: - coord = [0.0, 0.0, 0.0] - disp1 = displacement_func1.apply_function(coord, atom_index=0) - disp2 = displacement_func2.apply_function(coord, atom_index=0) + disp1 = displacement_func1.apply_function(coord, material=material, atom_index=atom_index) + disp2 = displacement_func2.apply_function(coord, material=material, atom_index=atom_index) assert np.allclose(disp1, disp2) else: - coord = [0.0, 0.0, 0.0] - disp1 = displacement_func1.apply_function(coord, atom_index=0) - disp2 = displacement_func2.apply_function(coord, atom_index=0) + disp1 = displacement_func1.apply_function(coord, material=material, atom_index=atom_index) + disp2 = displacement_func2.apply_function(coord, material=material, atom_index=atom_index) assert not np.allclose(disp1, disp2) or np.allclose(disp1, [0, 0, 0], atol=1e-10) @@ -57,12 +60,10 @@ def test_maxwell_displacement_perturb_integration(): material = Material.create(BULK_Si_PRIMITIVE) original_coords = [coord[:] for coord in material.basis.coordinates.values] - displacement_func = create_maxwell_displacement_function( + perturbed_material = create_maxwell_displacement( material, disorder_parameter=TEMPERATURE_K, random_seed=RANDOM_SEED ) - perturbed_material = perturb(material, displacement_func, use_cartesian_coordinates=True) - assert len(perturbed_material.basis.coordinates.values) == len(original_coords) for i, (orig, pert) in enumerate(zip(original_coords, perturbed_material.basis.coordinates.values)): delta = np.array(pert) - np.array(orig) @@ -72,18 +73,18 @@ def test_maxwell_displacement_perturb_integration(): def test_maxwell_displacement_msd_expectation(): material = Material.create(BULK_Si_PRIMITIVE) si_mass = get_atomic_mass_from_element("Si") - temperature = TEMPERATURE_K - kT = BOLTZMANN_CONSTANT_EV_PER_K * temperature - expected_variance = kT / si_mass + disorder_parameter = TEMPERATURE_K + expected_variance = disorder_parameter / si_mass expected_msd = 3 * expected_variance displacements = [] + atom_index = 0 + coord = [0.0, 0.0, 0.0] for _ in range(NUM_SAMPLES_FOR_MSD): - displacement_func = create_maxwell_displacement_function( - material, disorder_parameter=temperature, random_seed=None + displacement_func = MaxwellBoltzmannDisplacementHolder( + disorder_parameter=disorder_parameter, random_seed=None ) - coord = [0.0, 0.0, 0.0] - disp = displacement_func.apply_function(coord, atom_index=0) + disp = displacement_func.apply_function(coord, material=material, atom_index=atom_index) displacements.append(disp) displacements_array = np.array(displacements) @@ -105,12 +106,10 @@ def test_maxwell_boltzmann_on_slab(slab_config, temperature_k, random_seed): original_coords = [coord[:] for coord in material.basis.coordinates.values] original_lattice = material.lattice.vector_arrays.copy() - displacement_func = create_maxwell_displacement_function( + perturbed_material = create_maxwell_displacement( material, disorder_parameter=temperature_k, random_seed=random_seed ) - perturbed_material = perturb(material, displacement_func, use_cartesian_coordinates=True) - assert len(perturbed_material.basis.coordinates.values) == len(original_coords) assert len(perturbed_material.basis.elements.values) == len(material.basis.elements.values) @@ -127,8 +126,7 @@ def test_maxwell_boltzmann_on_slab(slab_config, temperature_k, random_seed): assert mean_displacement > 0 si_mass = get_atomic_mass_from_element("Si") - kT = BOLTZMANN_CONSTANT_EV_PER_K * temperature_k - expected_std = np.sqrt(kT / si_mass) + expected_std = np.sqrt(temperature_k / si_mass) assert mean_displacement < 5 * expected_std From baa5121b45fffd045fba7fc2f42770a1c784afa7 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 26 Dec 2025 14:30:32 -0800 Subject: [PATCH 08/22] update: cleanup test --- .../core/modifications/perturb/__init__.py | 2 +- tests/py/unit/test_maxwell_boltzmann.py | 47 ++++++++----------- 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py index 07fe57480..1d25b2804 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py @@ -5,7 +5,7 @@ PerturbationFunctionHolder, SineWavePerturbationFunctionHolder, ) -from .helpers import create_maxwell_displacement + __all__ = [ "AtomicMassDependentFunctionHolder", diff --git a/tests/py/unit/test_maxwell_boltzmann.py b/tests/py/unit/test_maxwell_boltzmann.py index a0e072049..5eb6e6e67 100644 --- a/tests/py/unit/test_maxwell_boltzmann.py +++ b/tests/py/unit/test_maxwell_boltzmann.py @@ -3,54 +3,47 @@ from mat3ra.made.material import Material from mat3ra.made.periodic_table import get_atomic_mass_from_element -from mat3ra.made.tools.build_components.operations.core.modifications.perturb import ( - create_maxwell_displacement, -) from mat3ra.made.tools.build_components.operations.core.modifications.perturb.functions.maxwell_boltzmann import ( MaxwellBoltzmannDisplacementHolder, ) +from mat3ra.made.tools.build_components.operations.core.modifications.perturb.helpers import ( + create_maxwell_displacement, +) from mat3ra.made.tools.helpers import create_supercell -from mat3ra.made.tools.operations.core.unary import perturb - from .fixtures.bulk import BULK_Si_PRIMITIVE from .fixtures.slab import SI_CONVENTIONAL_SLAB_001 -ELEMENT_SYMBOL_TO_MASS_TEST_CASES = [ - ("H", 1.008), - ("He", 4.003), - ("Li", 6.941), - ("C", 12.011), - ("N", 14.007), - ("O", 15.999), - ("Si", 28.085), - ("Fe", 55.845), - ("Cu", 63.546), - ("Au", 196.967), -] - -TEMPERATURE_K = 300.0 +DISORDER_PARAMETER = 3000.0 # Temperature-like RANDOM_SEED = 42 NUM_SAMPLES_FOR_MSD = 1000 -@pytest.mark.parametrize("random_seed", [None, 42, 123, 999]) +@pytest.mark.parametrize("random_seed", [None, 42, 123]) def test_maxwell_displacement_deterministic(random_seed): material = Material.create(BULK_Si_PRIMITIVE) displacement_func1 = MaxwellBoltzmannDisplacementHolder( - disorder_parameter=TEMPERATURE_K, random_seed=random_seed + disorder_parameter=DISORDER_PARAMETER, random_seed=random_seed ) displacement_func2 = MaxwellBoltzmannDisplacementHolder( - disorder_parameter=TEMPERATURE_K, random_seed=random_seed + disorder_parameter=DISORDER_PARAMETER, random_seed=random_seed ) coord = [0.0, 0.0, 0.0] atom_index = 0 - + if random_seed is not None: disp1 = displacement_func1.apply_function(coord, material=material, atom_index=atom_index) disp2 = displacement_func2.apply_function(coord, material=material, atom_index=atom_index) assert np.allclose(disp1, disp2) + + # Different seed should give different results + displacement_func3 = MaxwellBoltzmannDisplacementHolder( + disorder_parameter=DISORDER_PARAMETER, random_seed=random_seed + 1 + ) + disp3 = displacement_func3.apply_function(coord, material=material, atom_index=atom_index) + assert not np.allclose(disp1, disp3) or np.allclose(disp1, [0, 0, 0], atol=1e-10) else: + # No seed: different instances should give different results (non-deterministic) disp1 = displacement_func1.apply_function(coord, material=material, atom_index=atom_index) disp2 = displacement_func2.apply_function(coord, material=material, atom_index=atom_index) assert not np.allclose(disp1, disp2) or np.allclose(disp1, [0, 0, 0], atol=1e-10) @@ -61,7 +54,7 @@ def test_maxwell_displacement_perturb_integration(): original_coords = [coord[:] for coord in material.basis.coordinates.values] perturbed_material = create_maxwell_displacement( - material, disorder_parameter=TEMPERATURE_K, random_seed=RANDOM_SEED + material, disorder_parameter=DISORDER_PARAMETER, random_seed=RANDOM_SEED ) assert len(perturbed_material.basis.coordinates.values) == len(original_coords) @@ -73,7 +66,7 @@ def test_maxwell_displacement_perturb_integration(): def test_maxwell_displacement_msd_expectation(): material = Material.create(BULK_Si_PRIMITIVE) si_mass = get_atomic_mass_from_element("Si") - disorder_parameter = TEMPERATURE_K + disorder_parameter = DISORDER_PARAMETER expected_variance = disorder_parameter / si_mass expected_msd = 3 * expected_variance @@ -81,9 +74,7 @@ def test_maxwell_displacement_msd_expectation(): atom_index = 0 coord = [0.0, 0.0, 0.0] for _ in range(NUM_SAMPLES_FOR_MSD): - displacement_func = MaxwellBoltzmannDisplacementHolder( - disorder_parameter=disorder_parameter, random_seed=None - ) + displacement_func = MaxwellBoltzmannDisplacementHolder(disorder_parameter=disorder_parameter, random_seed=None) disp = displacement_func.apply_function(coord, material=material, atom_index=atom_index) displacements.append(disp) From 1b326abc1a8600097f7c1de5ee6c3dc7df6f4394 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 26 Dec 2025 16:41:53 -0800 Subject: [PATCH 09/22] update: calibrate disorder param --- pyproject.toml | 1 - .../modifications/perturb/functions/maxwell_boltzmann.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e83f4f786..d8fcc945c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ dependencies = [ "mat3ra-utils", "mat3ra-esse", "mat3ra-code" - ] [project.optional-dependencies] diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py index d803cbab0..8e4a20003 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py @@ -12,6 +12,7 @@ class MaxwellBoltzmannDisplacementHolder(AtomicMassDependentFunctionHolder): disorder_parameter: float = Field(exclude=True) random_state: Any = Field(default=None, exclude=True) is_mass_used: bool = Field(default=True, exclude=True) + conversion_constant: float = Field(default=2e-3, exclude=True) def __init__( self, @@ -23,11 +24,11 @@ def __init__( np.random.seed(random_seed) random_state = np.random.RandomState(random_seed) if random_seed is not None else np.random - + calibrated_disorder_parameter = disorder_parameter * self.conversion_constant function_expr = sp.Symbol("f") super().__init__( function=function_expr, - disorder_parameter=disorder_parameter, + disorder_parameter=calibrated_disorder_parameter, random_state=random_state, is_mass_used=is_mass_used, ) From 079b4fbf97df9c18cfbd85cc8180f2cf33041b96 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 26 Dec 2025 16:53:01 -0800 Subject: [PATCH 10/22] update: add calibration param --- .../perturb/functions/maxwell_boltzmann.py | 8 ++++++-- .../operations/core/modifications/perturb/helpers.py | 3 +++ tests/py/unit/test_maxwell_boltzmann.py | 10 +++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py index 8e4a20003..3782e4817 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py @@ -7,30 +7,34 @@ from .elemental_function_holder import AtomicMassDependentFunctionHolder +DEFAULT_CONVERSION_CONSTANT = 2e-3 + class MaxwellBoltzmannDisplacementHolder(AtomicMassDependentFunctionHolder): disorder_parameter: float = Field(exclude=True) random_state: Any = Field(default=None, exclude=True) is_mass_used: bool = Field(default=True, exclude=True) - conversion_constant: float = Field(default=2e-3, exclude=True) + conversion_constant: float = Field(exclude=True) def __init__( self, disorder_parameter: float, random_seed: Optional[int] = None, is_mass_used: bool = True, + conversion_constant: float = DEFAULT_CONVERSION_CONSTANT, ): if random_seed is not None: np.random.seed(random_seed) random_state = np.random.RandomState(random_seed) if random_seed is not None else np.random - calibrated_disorder_parameter = disorder_parameter * self.conversion_constant + calibrated_disorder_parameter = disorder_parameter * conversion_constant function_expr = sp.Symbol("f") super().__init__( function=function_expr, disorder_parameter=calibrated_disorder_parameter, random_state=random_state, is_mass_used=is_mass_used, + conversion_constant=conversion_constant, ) def apply_function(self, coordinate, material=None, atom_index: Optional[int] = None, **kwargs) -> list: diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py index 957ff2fb8..78be719d9 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py @@ -47,6 +47,7 @@ def create_maxwell_displacement( disorder_parameter: float, random_seed: Optional[int] = None, is_mass_used: bool = True, + conversion_constant: float = 2e-3, ) -> Material: """ Apply Maxwell-Boltzmann random displacements to a material. @@ -61,6 +62,7 @@ def create_maxwell_displacement( random_seed: Optional random seed for deterministic behavior. is_mass_used: If True, displacement variance is disorder_parameter/m (mass-dependent). If False, displacement variance is disorder_parameter (mass-independent). + conversion_constant: Calibration constant to scale disorder_parameter (default: 2e-3). Returns: Material with applied Maxwell-Boltzmann displacements. @@ -69,6 +71,7 @@ def create_maxwell_displacement( disorder_parameter=disorder_parameter, random_seed=random_seed, is_mass_used=is_mass_used, + conversion_constant=conversion_constant, ) configuration = PerturbationConfiguration( diff --git a/tests/py/unit/test_maxwell_boltzmann.py b/tests/py/unit/test_maxwell_boltzmann.py index 5eb6e6e67..679b664b7 100644 --- a/tests/py/unit/test_maxwell_boltzmann.py +++ b/tests/py/unit/test_maxwell_boltzmann.py @@ -1,15 +1,13 @@ import numpy as np import pytest - from mat3ra.made.material import Material from mat3ra.made.periodic_table import get_atomic_mass_from_element from mat3ra.made.tools.build_components.operations.core.modifications.perturb.functions.maxwell_boltzmann import ( MaxwellBoltzmannDisplacementHolder, ) -from mat3ra.made.tools.build_components.operations.core.modifications.perturb.helpers import ( - create_maxwell_displacement, -) +from mat3ra.made.tools.build_components.operations.core.modifications.perturb.helpers import create_maxwell_displacement from mat3ra.made.tools.helpers import create_supercell + from .fixtures.bulk import BULK_Si_PRIMITIVE from .fixtures.slab import SI_CONVENTIONAL_SLAB_001 @@ -67,7 +65,9 @@ def test_maxwell_displacement_msd_expectation(): material = Material.create(BULK_Si_PRIMITIVE) si_mass = get_atomic_mass_from_element("Si") disorder_parameter = DISORDER_PARAMETER - expected_variance = disorder_parameter / si_mass + conversion_constant = 2e-3 + calibrated_disorder_parameter = disorder_parameter * conversion_constant + expected_variance = calibrated_disorder_parameter / si_mass expected_msd = 3 * expected_variance displacements = [] From b7d9d8a660353c70defe966aa4cf459dbf4893fb Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 26 Dec 2025 22:06:20 -0800 Subject: [PATCH 11/22] update: use periodic table --- pyproject.toml | 2 +- src/py/mat3ra/made/periodic_table.py | 85 ------------------- .../functions/elemental_function_holder.py | 10 +-- .../perturb/functions/maxwell_boltzmann.py | 2 +- tests/py/unit/test_maxwell_boltzmann.py | 2 +- 5 files changed, 7 insertions(+), 94 deletions(-) delete mode 100644 src/py/mat3ra/made/periodic_table.py diff --git a/pyproject.toml b/pyproject.toml index d8fcc945c..ae35d94e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ tools = [ "pymatgen==2024.4.13", "ase", "pymatgen-analysis-defects==2024.4.23", - "mat3ra-periodic-table>=2025.1.18", + "mat3ra-periodic-table>=2025.12.26", ] dev = [ "pre-commit", diff --git a/src/py/mat3ra/made/periodic_table.py b/src/py/mat3ra/made/periodic_table.py deleted file mode 100644 index c6eb7ca07..000000000 --- a/src/py/mat3ra/made/periodic_table.py +++ /dev/null @@ -1,85 +0,0 @@ -import json -from pathlib import Path -from typing import Dict, Optional - -from mat3ra.periodic_table import PERIODIC_TABLE - - -_PERIODIC_TABLE_DATA: Optional[Dict[str, float]] = None - - -def _load_periodic_table_data() -> Dict[str, float]: - global _PERIODIC_TABLE_DATA - if _PERIODIC_TABLE_DATA is not None: - return _PERIODIC_TABLE_DATA - - if PERIODIC_TABLE is None: - raise ImportError( - "mat3ra.periodic_table is required for periodic table functionality. " - "Install it with: pip install mat3ra-made[tools] or pip install mat3ra-periodic-table" - ) - - _PERIODIC_TABLE_DATA = {} - possible_mass_fields = [ - "atomic_mass", - "atomicMass", - "mass", - "atomic_weight", - "atomicWeight", - "standard_atomic_weight", - ] - - for symbol, element_data in PERIODIC_TABLE.items(): - atomic_mass = None - for field in possible_mass_fields: - if field in element_data: - atomic_mass = element_data[field] - break - - if atomic_mass is not None: - _PERIODIC_TABLE_DATA[symbol] = float(atomic_mass) - else: - available_keys = list(element_data.keys()) - raise ValueError( - f"Atomic mass not found for element {symbol} in PERIODIC_TABLE. " f"Available keys: {available_keys}" - ) - - return _PERIODIC_TABLE_DATA - - -def get_atomic_mass_from_element(element: str) -> float: - """ - Get atomic mass for an element symbol. - - Args: - element: Element symbol (e.g., "Si", "H", "O") - - Returns: - Atomic mass in atomic mass units (amu) - - Raises: - ValueError: If element symbol is not found - ImportError: If mat3ra.periodic_table is not installed - """ - data = _load_periodic_table_data() - element_upper = element.strip().capitalize() - if element_upper not in data: - raise ValueError(f"Element symbol '{element}' not found in periodic table") - return data[element_upper] - - -def export_periodic_table_json(output_path: Optional[Path] = None) -> Dict[str, float]: - """ - Export periodic table data as JSON. - - Args: - output_path: Optional path to save JSON file. If None, returns dict only. - - Returns: - Dictionary mapping element symbols to atomic masses - """ - data = _load_periodic_table_data() - if output_path is not None: - with open(output_path, "w") as f: - json.dump(data, f, indent=2) - return data diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py index 231870d89..e224a6432 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py @@ -2,7 +2,7 @@ import sympy as sp -from mat3ra.made.periodic_table import get_atomic_mass_from_element +from mat3ra.periodic_table.helpers import get_atomic_mass_from_element from .function_holder import AXIS_TO_INDEX_MAP, FunctionHolder @@ -22,13 +22,11 @@ def __init__( super().__init__(function=function, variables=variables, **data) - def apply_function( - self, coordinate: List[float], material=None, atom_index: Optional[int] = None, **kwargs: Any - ) -> Union[float, List[float]]: - if material is None or atom_index is None: + def apply_function(self, coordinate: List[float], material=None, **kwargs: Any) -> Union[float, List[float]]: + if material is None: raise ValueError("AtomicMassDependentFunctionHolder requires 'material' and 'atom_index' kwargs") - element = material.basis.elements.values[atom_index] + element = material.basis.elements.values mass = get_atomic_mass_from_element(element) values = [] diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py index 3782e4817..b664a43a1 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py @@ -2,7 +2,7 @@ import numpy as np import sympy as sp -from mat3ra.made.periodic_table import get_atomic_mass_from_element +from mat3ra.periodic_table.helpers import get_atomic_mass_from_element from pydantic import Field from .elemental_function_holder import AtomicMassDependentFunctionHolder diff --git a/tests/py/unit/test_maxwell_boltzmann.py b/tests/py/unit/test_maxwell_boltzmann.py index 679b664b7..41116e03d 100644 --- a/tests/py/unit/test_maxwell_boltzmann.py +++ b/tests/py/unit/test_maxwell_boltzmann.py @@ -1,7 +1,7 @@ import numpy as np import pytest from mat3ra.made.material import Material -from mat3ra.made.periodic_table import get_atomic_mass_from_element +from mat3ra.periodic_table.helpers import get_atomic_mass_from_element from mat3ra.made.tools.build_components.operations.core.modifications.perturb.functions.maxwell_boltzmann import ( MaxwellBoltzmannDisplacementHolder, ) From fd396f1365731882f9062b336de966b4c50343d3 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 26 Dec 2025 22:31:47 -0800 Subject: [PATCH 12/22] update: remove index --- .../modifications/perturb/functions/maxwell_boltzmann.py | 7 ++++--- src/py/mat3ra/made/tools/operations/core/unary.py | 6 ++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py index b664a43a1..add602750 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py @@ -37,12 +37,13 @@ def __init__( conversion_constant=conversion_constant, ) - def apply_function(self, coordinate, material=None, atom_index: Optional[int] = None, **kwargs) -> list: - if material is None or atom_index is None: + def apply_function(self, coordinate, material=None) -> list: + if material is None: raise ValueError("MaxwellBoltzmannDisplacementHolder requires 'material' and 'atom_index' kwargs") if self.is_mass_used: - element = material.basis.elements.values[atom_index] + atom_id = material.basis.coordinates.get_element_id_by_value(coordinate) + element = material.basis.elements.get_element_value_by_index(atom_id) mass = get_atomic_mass_from_element(element) variance = self.disorder_parameter / mass else: diff --git a/src/py/mat3ra/made/tools/operations/core/unary.py b/src/py/mat3ra/made/tools/operations/core/unary.py index 3fd7512df..6aca184c4 100644 --- a/src/py/mat3ra/made/tools/operations/core/unary.py +++ b/src/py/mat3ra/made/tools/operations/core/unary.py @@ -83,10 +83,8 @@ def perturb( original_coordinates = new_material.basis.coordinates.values perturbed_coordinates: List[List[float]] = [] - for atom_index, coordinate in enumerate(original_coordinates): - displacement = perturbation_function.apply_function( - coordinate, material=new_material, atom_index=atom_index - ) + for coordinate in original_coordinates: + displacement = perturbation_function.apply_function(coordinate, material=new_material) if isinstance(displacement, (list, tuple, np.ndarray)): delta = np.array(displacement) From a25acdb51c47e4a2a786a46557411a25b105b43a Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 26 Dec 2025 22:40:38 -0800 Subject: [PATCH 13/22] update: get mass from material --- .../functions/elemental_function_holder.py | 25 ++++++------------- .../perturb/functions/maxwell_boltzmann.py | 7 ++---- tests/py/unit/test_maxwell_boltzmann.py | 12 ++++----- 3 files changed, 16 insertions(+), 28 deletions(-) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py index e224a6432..775993c84 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py @@ -1,9 +1,9 @@ from typing import Any, List, Optional, Union import sympy as sp - from mat3ra.periodic_table.helpers import get_atomic_mass_from_element -from .function_holder import AXIS_TO_INDEX_MAP, FunctionHolder + +from .function_holder import FunctionHolder class AtomicMassDependentFunctionHolder(FunctionHolder): @@ -22,20 +22,11 @@ def __init__( super().__init__(function=function, variables=variables, **data) - def apply_function(self, coordinate: List[float], material=None, **kwargs: Any) -> Union[float, List[float]]: + @staticmethod + def get_atomic_mass(coordinate: List[float], material) -> float: if material is None: - raise ValueError("AtomicMassDependentFunctionHolder requires 'material' and 'atom_index' kwargs") - - element = material.basis.elements.values - mass = get_atomic_mass_from_element(element) - - values = [] - for var in self.variables: - if var == "m": - values.append(mass) - elif var in AXIS_TO_INDEX_MAP: - values.append(coordinate[AXIS_TO_INDEX_MAP[var]]) - else: - raise ValueError(f"Unknown variable: {var}") + raise ValueError("Material is required to extract atomic mass") - return self.function_numeric(*values) + atom_id = material.basis.coordinates.get_element_id_by_value(coordinate) + element = material.basis.elements.get_element_value_by_index(atom_id) + return get_atomic_mass_from_element(element) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py index add602750..939f9b17b 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py @@ -2,7 +2,6 @@ import numpy as np import sympy as sp -from mat3ra.periodic_table.helpers import get_atomic_mass_from_element from pydantic import Field from .elemental_function_holder import AtomicMassDependentFunctionHolder @@ -39,12 +38,10 @@ def __init__( def apply_function(self, coordinate, material=None) -> list: if material is None: - raise ValueError("MaxwellBoltzmannDisplacementHolder requires 'material' and 'atom_index' kwargs") + raise ValueError("MaxwellBoltzmannDisplacementHolder requires 'material' kwargs") if self.is_mass_used: - atom_id = material.basis.coordinates.get_element_id_by_value(coordinate) - element = material.basis.elements.get_element_value_by_index(atom_id) - mass = get_atomic_mass_from_element(element) + mass = self.get_atomic_mass(coordinate, material) variance = self.disorder_parameter / mass else: variance = self.disorder_parameter diff --git a/tests/py/unit/test_maxwell_boltzmann.py b/tests/py/unit/test_maxwell_boltzmann.py index 41116e03d..cda7d2913 100644 --- a/tests/py/unit/test_maxwell_boltzmann.py +++ b/tests/py/unit/test_maxwell_boltzmann.py @@ -30,20 +30,20 @@ def test_maxwell_displacement_deterministic(random_seed): atom_index = 0 if random_seed is not None: - disp1 = displacement_func1.apply_function(coord, material=material, atom_index=atom_index) - disp2 = displacement_func2.apply_function(coord, material=material, atom_index=atom_index) + disp1 = displacement_func1.apply_function(coord, material=material) + disp2 = displacement_func2.apply_function(coord, material=material) assert np.allclose(disp1, disp2) # Different seed should give different results displacement_func3 = MaxwellBoltzmannDisplacementHolder( disorder_parameter=DISORDER_PARAMETER, random_seed=random_seed + 1 ) - disp3 = displacement_func3.apply_function(coord, material=material, atom_index=atom_index) + disp3 = displacement_func3.apply_function(coord, material=material) assert not np.allclose(disp1, disp3) or np.allclose(disp1, [0, 0, 0], atol=1e-10) else: # No seed: different instances should give different results (non-deterministic) - disp1 = displacement_func1.apply_function(coord, material=material, atom_index=atom_index) - disp2 = displacement_func2.apply_function(coord, material=material, atom_index=atom_index) + disp1 = displacement_func1.apply_function(coord, material=material) + disp2 = displacement_func2.apply_function(coord, material=material) assert not np.allclose(disp1, disp2) or np.allclose(disp1, [0, 0, 0], atol=1e-10) @@ -75,7 +75,7 @@ def test_maxwell_displacement_msd_expectation(): coord = [0.0, 0.0, 0.0] for _ in range(NUM_SAMPLES_FOR_MSD): displacement_func = MaxwellBoltzmannDisplacementHolder(disorder_parameter=disorder_parameter, random_seed=None) - disp = displacement_func.apply_function(coord, material=material, atom_index=atom_index) + disp = displacement_func.apply_function(coord, material=material) displacements.append(disp) displacements_array = np.array(displacements) From 238ca41465fbc346be94a3567184638e4e28076e Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 26 Dec 2025 22:41:27 -0800 Subject: [PATCH 14/22] chore: lint fix --- tests/py/unit/test_maxwell_boltzmann.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/py/unit/test_maxwell_boltzmann.py b/tests/py/unit/test_maxwell_boltzmann.py index cda7d2913..d33e45c57 100644 --- a/tests/py/unit/test_maxwell_boltzmann.py +++ b/tests/py/unit/test_maxwell_boltzmann.py @@ -27,7 +27,6 @@ def test_maxwell_displacement_deterministic(random_seed): ) coord = [0.0, 0.0, 0.0] - atom_index = 0 if random_seed is not None: disp1 = displacement_func1.apply_function(coord, material=material) From 6b2fcb617134b8caefaac5ee2ea443df08cacf51 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 26 Dec 2025 22:44:29 -0800 Subject: [PATCH 15/22] chore: lint fix --- tests/py/unit/test_maxwell_boltzmann.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/py/unit/test_maxwell_boltzmann.py b/tests/py/unit/test_maxwell_boltzmann.py index d33e45c57..5aa14fc00 100644 --- a/tests/py/unit/test_maxwell_boltzmann.py +++ b/tests/py/unit/test_maxwell_boltzmann.py @@ -1,12 +1,12 @@ import numpy as np import pytest from mat3ra.made.material import Material -from mat3ra.periodic_table.helpers import get_atomic_mass_from_element from mat3ra.made.tools.build_components.operations.core.modifications.perturb.functions.maxwell_boltzmann import ( MaxwellBoltzmannDisplacementHolder, ) from mat3ra.made.tools.build_components.operations.core.modifications.perturb.helpers import create_maxwell_displacement from mat3ra.made.tools.helpers import create_supercell +from mat3ra.periodic_table.helpers import get_atomic_mass_from_element from .fixtures.bulk import BULK_Si_PRIMITIVE from .fixtures.slab import SI_CONVENTIONAL_SLAB_001 @@ -70,7 +70,6 @@ def test_maxwell_displacement_msd_expectation(): expected_msd = 3 * expected_variance displacements = [] - atom_index = 0 coord = [0.0, 0.0, 0.0] for _ in range(NUM_SAMPLES_FOR_MSD): displacement_func = MaxwellBoltzmannDisplacementHolder(disorder_parameter=disorder_parameter, random_seed=None) From 7efdef775d2936077f7fc5501a0a69f1c173d265 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 26 Dec 2025 23:09:58 -0800 Subject: [PATCH 16/22] chore: rename --- ...ction_holder.py => atomic_mass_dependent_function_holder.py} | 0 .../core/modifications/perturb/functions/maxwell_boltzmann.py | 2 +- ...axwell_boltzmann.py => test_tools_build_maxwell_disorder.py} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/{elemental_function_holder.py => atomic_mass_dependent_function_holder.py} (100%) rename tests/py/unit/{test_maxwell_boltzmann.py => test_tools_build_maxwell_disorder.py} (100%) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/atomic_mass_dependent_function_holder.py similarity index 100% rename from src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/elemental_function_holder.py rename to src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/atomic_mass_dependent_function_holder.py diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py index 939f9b17b..1caae1546 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py @@ -4,7 +4,7 @@ import sympy as sp from pydantic import Field -from .elemental_function_holder import AtomicMassDependentFunctionHolder +from .atomic_mass_dependent_function_holder import AtomicMassDependentFunctionHolder DEFAULT_CONVERSION_CONSTANT = 2e-3 diff --git a/tests/py/unit/test_maxwell_boltzmann.py b/tests/py/unit/test_tools_build_maxwell_disorder.py similarity index 100% rename from tests/py/unit/test_maxwell_boltzmann.py rename to tests/py/unit/test_tools_build_maxwell_disorder.py From b8d006e416aacd560af6c9503254bfb456519793 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 26 Dec 2025 23:10:47 -0800 Subject: [PATCH 17/22] chore: rename --- .../operations/core/modifications/perturb/__init__.py | 1 - .../operations/core/modifications/perturb/functions/__init__.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py index 1d25b2804..ca481437e 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/__init__.py @@ -6,7 +6,6 @@ SineWavePerturbationFunctionHolder, ) - __all__ = [ "AtomicMassDependentFunctionHolder", "FunctionHolder", diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py index d10896c38..7a314ddd0 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/__init__.py @@ -1,4 +1,4 @@ -from .elemental_function_holder import AtomicMassDependentFunctionHolder +from .atomic_mass_dependent_function_holder import AtomicMassDependentFunctionHolder from .function_holder import FunctionHolder from .maxwell_boltzmann import MaxwellBoltzmannDisplacementHolder from .perturbation_function_holder import PerturbationFunctionHolder From 9bfe7eb29ac619e9f2213dacb9aa8a09334c7da7 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sat, 27 Dec 2025 10:58:50 -0800 Subject: [PATCH 18/22] chore: cleanup --- .../perturb/functions/maxwell_boltzmann.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py index 1caae1546..7ee2f268f 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any, List, Optional import numpy as np import sympy as sp @@ -11,9 +11,10 @@ class MaxwellBoltzmannDisplacementHolder(AtomicMassDependentFunctionHolder): disorder_parameter: float = Field(exclude=True) + random_seed: Optional[int] = Field(default=None, exclude=True) random_state: Any = Field(default=None, exclude=True) is_mass_used: bool = Field(default=True, exclude=True) - conversion_constant: float = Field(exclude=True) + conversion_constant: float = Field(default=DEFAULT_CONVERSION_CONSTANT, exclude=True) def __init__( self, @@ -22,12 +23,10 @@ def __init__( is_mass_used: bool = True, conversion_constant: float = DEFAULT_CONVERSION_CONSTANT, ): - if random_seed is not None: - np.random.seed(random_seed) - - random_state = np.random.RandomState(random_seed) if random_seed is not None else np.random calibrated_disorder_parameter = disorder_parameter * conversion_constant + random_state = np.random.RandomState(random_seed) if random_seed is not None else np.random function_expr = sp.Symbol("f") + super().__init__( function=function_expr, disorder_parameter=calibrated_disorder_parameter, @@ -36,7 +35,7 @@ def __init__( conversion_constant=conversion_constant, ) - def apply_function(self, coordinate, material=None) -> list: + def apply_function(self, coordinate, material=None) -> List[float]: if material is None: raise ValueError("MaxwellBoltzmannDisplacementHolder requires 'material' kwargs") From b70cd237f9abeba2a6933ec0c563a156f54a54fa Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sun, 28 Dec 2025 18:18:20 -0800 Subject: [PATCH 19/22] update: disorder in eV --- .../atomic_mass_dependent_function_holder.py | 2 +- .../perturb/functions/maxwell_boltzmann.py | 37 +++++++------------ 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/atomic_mass_dependent_function_holder.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/atomic_mass_dependent_function_holder.py index 775993c84..fb92f9aac 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/atomic_mass_dependent_function_holder.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/atomic_mass_dependent_function_holder.py @@ -11,7 +11,7 @@ class AtomicMassDependentFunctionHolder(FunctionHolder): def __init__( self, - function: Union[sp.Expr, str], + function: Union[sp.Expr, str] = sp.Symbol("f"), variables: Optional[List[str]] = None, **data: Any, ): diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py index 7ee2f268f..5bfbc6ae2 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/functions/maxwell_boltzmann.py @@ -1,39 +1,28 @@ from typing import Any, List, Optional import numpy as np -import sympy as sp -from pydantic import Field +from pydantic import Field, model_validator from .atomic_mass_dependent_function_holder import AtomicMassDependentFunctionHolder -DEFAULT_CONVERSION_CONSTANT = 2e-3 +DEFAULT_DISORDER_PARAMETER = 1.0 class MaxwellBoltzmannDisplacementHolder(AtomicMassDependentFunctionHolder): - disorder_parameter: float = Field(exclude=True) + disorder_parameter: float = Field( + default=DEFAULT_DISORDER_PARAMETER, + exclude=True, + description="Disorder parameter. Can be viewed as effective temperature in eV.", + ) random_seed: Optional[int] = Field(default=None, exclude=True) random_state: Any = Field(default=None, exclude=True) is_mass_used: bool = Field(default=True, exclude=True) - conversion_constant: float = Field(default=DEFAULT_CONVERSION_CONSTANT, exclude=True) - - def __init__( - self, - disorder_parameter: float, - random_seed: Optional[int] = None, - is_mass_used: bool = True, - conversion_constant: float = DEFAULT_CONVERSION_CONSTANT, - ): - calibrated_disorder_parameter = disorder_parameter * conversion_constant - random_state = np.random.RandomState(random_seed) if random_seed is not None else np.random - function_expr = sp.Symbol("f") - - super().__init__( - function=function_expr, - disorder_parameter=calibrated_disorder_parameter, - random_state=random_state, - is_mass_used=is_mass_used, - conversion_constant=conversion_constant, - ) + + @model_validator(mode="after") + def setup_random_state(self): + if self.random_state is None: + self.random_state = np.random.RandomState(self.random_seed) if self.random_seed is not None else np.random + return self def apply_function(self, coordinate, material=None) -> List[float]: if material is None: From 92102c0951c67da129fa8e89d1ec98ef1f90de78 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sun, 28 Dec 2025 18:20:33 -0800 Subject: [PATCH 20/22] chore: fix test --- tests/py/unit/test_tools_build_maxwell_disorder.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/py/unit/test_tools_build_maxwell_disorder.py b/tests/py/unit/test_tools_build_maxwell_disorder.py index 5aa14fc00..27904a832 100644 --- a/tests/py/unit/test_tools_build_maxwell_disorder.py +++ b/tests/py/unit/test_tools_build_maxwell_disorder.py @@ -11,7 +11,7 @@ from .fixtures.bulk import BULK_Si_PRIMITIVE from .fixtures.slab import SI_CONVENTIONAL_SLAB_001 -DISORDER_PARAMETER = 3000.0 # Temperature-like +DISORDER_PARAMETER = 1.0 # Temperature-like RANDOM_SEED = 42 NUM_SAMPLES_FOR_MSD = 1000 @@ -64,9 +64,7 @@ def test_maxwell_displacement_msd_expectation(): material = Material.create(BULK_Si_PRIMITIVE) si_mass = get_atomic_mass_from_element("Si") disorder_parameter = DISORDER_PARAMETER - conversion_constant = 2e-3 - calibrated_disorder_parameter = disorder_parameter * conversion_constant - expected_variance = calibrated_disorder_parameter / si_mass + expected_variance = disorder_parameter / si_mass expected_msd = 3 * expected_variance displacements = [] From 44d62a2b95ea8a432d965f874c71b393f4122387 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sun, 28 Dec 2025 18:21:18 -0800 Subject: [PATCH 21/22] chore: add description --- .../operations/core/modifications/perturb/helpers.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py index 78be719d9..048c4341f 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py @@ -47,7 +47,6 @@ def create_maxwell_displacement( disorder_parameter: float, random_seed: Optional[int] = None, is_mass_used: bool = True, - conversion_constant: float = 2e-3, ) -> Material: """ Apply Maxwell-Boltzmann random displacements to a material. @@ -58,11 +57,10 @@ def create_maxwell_displacement( Args: material: The material to be perturbed. - disorder_parameter: Disorder parameter controlling displacement magnitude. + disorder_parameter: Disorder parameter controlling displacement magnitude, can be viewed as effective temperature in eV. random_seed: Optional random seed for deterministic behavior. is_mass_used: If True, displacement variance is disorder_parameter/m (mass-dependent). If False, displacement variance is disorder_parameter (mass-independent). - conversion_constant: Calibration constant to scale disorder_parameter (default: 2e-3). Returns: Material with applied Maxwell-Boltzmann displacements. @@ -71,7 +69,6 @@ def create_maxwell_displacement( disorder_parameter=disorder_parameter, random_seed=random_seed, is_mass_used=is_mass_used, - conversion_constant=conversion_constant, ) configuration = PerturbationConfiguration( From dadf6bd96be34b6a983e81ac647a4fd8ae75d48b Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sun, 28 Dec 2025 18:24:40 -0800 Subject: [PATCH 22/22] chore: lint fix --- .../operations/core/modifications/perturb/helpers.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py index 048c4341f..fd3999007 100644 --- a/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py +++ b/src/py/mat3ra/made/tools/build_components/operations/core/modifications/perturb/helpers.py @@ -1,14 +1,14 @@ from typing import Optional, Union import sympy as sp -from mat3ra.made.material import Material -from ..... import MaterialWithBuildMetadata +from mat3ra.made.material import Material from .builders.base import PerturbationBuilder from .builders.isometric import IsometricPerturbationBuilder from .configuration import PerturbationConfiguration from .functions import PerturbationFunctionHolder from .functions.maxwell_boltzmann import MaxwellBoltzmannDisplacementHolder +from ..... import MaterialWithBuildMetadata def create_perturbation( @@ -57,8 +57,9 @@ def create_maxwell_displacement( Args: material: The material to be perturbed. - disorder_parameter: Disorder parameter controlling displacement magnitude, can be viewed as effective temperature in eV. - random_seed: Optional random seed for deterministic behavior. + disorder_parameter: Disorder parameter controlling displacement magnitude, + can be viewed as effective temperature in eV. + random_seed: Optional random seed for reproducibility for the same material and parameters. is_mass_used: If True, displacement variance is disorder_parameter/m (mass-dependent). If False, displacement variance is disorder_parameter (mass-independent).