From 048633794b98db738c52cc36e6549f16ac992234 Mon Sep 17 00:00:00 2001 From: njzjz-bot Date: Wed, 15 Jul 2026 03:36:55 +0800 Subject: [PATCH] fix(ase): symmetrize reshaped virial stress Reshape the nine-component virial before transposing and project it onto ASE symmetric Cauchy stress while preserving the original virial matrix. Add a backend-independent asymmetric mock regression for the Voigt shear components. Coding-Agent: Codex Codex-Version: codex-cli 0.144.1 Model: gpt-5.6-sol Reasoning-Effort: xhigh --- deepmd/calculator.py | 8 +++-- source/tests/test_calculator.py | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 source/tests/test_calculator.py diff --git a/deepmd/calculator.py b/deepmd/calculator.py index 8c679acf3c..0eca4776ed 100644 --- a/deepmd/calculator.py +++ b/deepmd/calculator.py @@ -152,13 +152,17 @@ def calculate( # see https://gitlab.com/ase/ase/-/merge_requests/2485 self.results["free_energy"] = e[0][0] self.results["forces"] = f[0] - self.results["virial"] = v[0].reshape(3, 3) + virial = v[0].reshape(3, 3) + self.results["virial"] = virial # convert virial into stress for lattice relaxation if cell is not None: # the usual convention (tensile stress is positive) # stress = -virial / volume - stress = -0.5 * (v[0].copy() + v[0].copy().T) / atoms.get_volume() + # ASE represents Cauchy stress as a symmetric tensor. Reshape the + # flat model output before transposing; ``.T`` on the original + # one-dimensional virial array would otherwise be a no-op. + stress = -0.5 * (virial + virial.T) / atoms.get_volume() # Voigt notation self.results["stress"] = stress.flat[[0, 4, 8, 5, 2, 1]] elif "stress" in properties: diff --git a/source/tests/test_calculator.py b/source/tests/test_calculator.py new file mode 100644 index 0000000000..121bfa4fcd --- /dev/null +++ b/source/tests/test_calculator.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Backend-independent regression tests for the ASE calculator adapter.""" + +from unittest.mock import ( + Mock, + patch, +) + +import numpy as np +from ase import ( + Atoms, +) + +from deepmd.calculator import ( + DP, +) + + +def test_stress_symmetrizes_flat_virial() -> None: + """Convert the model virial to ASE's symmetric Voigt stress convention. + + Real DeePMD models normally produce an already-symmetric total virial, so + the existing integration tests could not detect that transposing its flat + nine-component representation was a no-op. An intentionally asymmetric + mock output makes each off-diagonal average independently observable. + """ + model = Mock() + model.get_type_map.return_value = ["H"] + model.get_ntypes.return_value = 1 + virial = np.arange(1.0, 10.0).reshape(1, 9) + model.eval.return_value = ( + np.array([[0.0]]), + np.zeros((1, 1, 3)), + virial, + ) + + with patch("deepmd.calculator.DeepPot", return_value=model): + calculator = DP("unused-model") + + atoms = Atoms( + "H", + positions=[[0.0, 0.0, 0.0]], + cell=np.eye(3) * 2.0, + pbc=True, + calculator=calculator, + ) + + np.testing.assert_allclose( + atoms.get_stress(voigt=True), + -np.array([1.0, 5.0, 9.0, 7.0, 5.0, 3.0]) / atoms.get_volume(), + ) + # Symmetrizing stress must not discard diagnostic information from the + # model: callers requesting the virial still receive the original tensor. + np.testing.assert_array_equal(calculator.results["virial"], virial.reshape(3, 3))