diff --git a/src/atomate2/common/schemas/elastic.py b/src/atomate2/common/schemas/elastic.py index 37cf9f8d84..04b9c34d76 100644 --- a/src/atomate2/common/schemas/elastic.py +++ b/src/atomate2/common/schemas/elastic.py @@ -302,15 +302,29 @@ def expand_strains( strains will not contain the ones with other strain states. Also see: `generate_elastic_deformations()`. """ + if tol <= 0: + raise ValueError(f"tol must be positive, got {tol}") + for strain in strains: + applied_components = np.abs(strain.voigt) + if len(applied_components[applied_components > tol]) == 0: + raise ValueError("tol must be smaller than the applied strain magnitude") + + def zero_numerical_components(strain: Strain) -> Strain: + voigt = np.asarray(strain.voigt).copy() + voigt[np.abs(voigt) <= tol] = 0 + return Strain.from_voigt(voigt) + sga = SpacegroupAnalyzer(structure, symprec=symprec) symm_ops = sga.get_symmetry_operations(cartesian=True) - full_strains = deepcopy(strains) + full_strains = [zero_numerical_components(strain) for strain in strains] full_stresses = deepcopy(stresses) full_uuids = deepcopy(uuids) full_job_dirs = deepcopy(job_dirs) - mapping = TensorMapping(full_strains, [True for _ in full_strains]) + # Preserve the original strains for identity comparisons so normalization does + # not change which symmetry-equivalent strains are accepted. + mapping = TensorMapping(deepcopy(strains), [True for _ in strains]) for idx, strain in enumerate(strains): for symm_op in symm_ops: rotated_strain = strain.transform(symm_op) @@ -327,7 +341,7 @@ def expand_strains( mapping[rotated_strain] = True # expand the other properties - full_strains.append(rotated_strain) + full_strains.append(zero_numerical_components(rotated_strain)) full_stresses.append(stresses[idx].transform(symm_op)) full_uuids.append(uuids[idx]) full_job_dirs.append(job_dirs[idx]) diff --git a/tests/common/jobs/test_elastic.py b/tests/common/jobs/test_elastic.py index 6f2362083e..7f915ec33b 100644 --- a/tests/common/jobs/test_elastic.py +++ b/tests/common/jobs/test_elastic.py @@ -1,7 +1,8 @@ import numpy as np import pytest from jobflow import run_locally -from pymatgen.analysis.elasticity import Stress +from pymatgen.analysis.elasticity import Strain, Stress +from pymatgen.core import Lattice, Structure from pymatgen.symmetry.analyzer import SpacegroupAnalyzer from atomate2 import SETTINGS @@ -43,6 +44,56 @@ def test_reduce_expand_strains(clean_dir, symmetry_structure, conventional): assert any(np.allclose(fs, rs) for rs in recovered_strains) +def test_expand_strains_zeroes_numerical_components(): + """Ensure symmetry residues use the same zero tolerance as expansion.""" + base = Structure.from_spacegroup("Im-3m", Lattice.cubic(2.87), ["Fe"], [[0, 0, 0]]) + lattice = np.asarray(base.lattice.matrix).copy() + lattice[0, 1] += 1e-3 + lattice[1, 2] -= 7e-4 + lattice[2, 0] += 3e-4 + structure = Structure(Lattice(lattice), base.species, base.frac_coords) + + strain = Strain.from_voigt([0.01, 0, 0, 0, 0, 0]) + stresses = [Stress(np.zeros((3, 3)))] + expanded, _, _, _ = expand_strains( + structure, + [strain], + stresses=stresses, + uuids=["dummy"], + job_dirs=["dummy"], + symprec=0.1, + ) + + assert len(expanded) == 3 + for expanded_strain in expanded: + components = np.abs(expanded_strain.voigt) + assert np.all((components == 0) | (components > 1e-3)) + + +@pytest.mark.parametrize( + ("tol", "strain_voigt"), + [ + (-1e-3, [0.01, 0, 0, 0, 0, 0]), + (0, [0.01, 0, 0, 0, 0, 0]), + (1e-3, [5e-4, 0, 0, 0, 0, 0]), + ], +) +def test_expand_strains_rejects_invalid_zero_tolerance(tol, strain_voigt): + structure = Structure(Lattice.cubic(2.87), ["Fe"], [[0, 0, 0]]) + strain = Strain.from_voigt(strain_voigt) + + with pytest.raises(ValueError, match="tol"): + expand_strains( + structure, + [strain], + stresses=[Stress(np.zeros((3, 3)))], + uuids=["dummy"], + job_dirs=["dummy"], + symprec=0.1, + tol=tol, + ) + + def _get_strains(structure, sym_reduce): """Get applied strains to deform the deformed structures."""