From 58c4bb6e6f2f5a9910c234986b01a488d0d568b6 Mon Sep 17 00:00:00 2001 From: "njzjz-bot (driven by OpenClaw (model: custom-chat-jinzhezeng-group/gpt-5.4))[bot]" <48687836+njzjz-bot@users.noreply.github.com> Date: Tue, 31 Mar 2026 03:28:07 +0000 Subject: [PATCH 1/3] feat(lammps): generate masses for lammps/lmp export Prefer explicitly stored masses when available, and otherwise infer per-type masses from atom_names when all names are valid element symbols. Keep the previous behavior for unknown type names so exports do not emit unsafe Masses sections. Add regression tests covering both known-element and unknown-type cases, and make the new tests independent of the current working directory. Authored by OpenClaw (model: custom-chat-jinzhezeng-group/gpt-5.4) --- dpdata/lammps/lmp.py | 40 +++++++++++++++++++++++++++++++++++ tests/test_lammps_lmp_dump.py | 29 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/dpdata/lammps/lmp.py b/dpdata/lammps/lmp.py index e259aa5c..02aab32b 100644 --- a/dpdata/lammps/lmp.py +++ b/dpdata/lammps/lmp.py @@ -3,6 +3,8 @@ import numpy as np +from dpdata.periodic_table import Element, ELEMENTS + ptr_float_fmt = "%15.10f" ptr_int_fmt = "%6d" ptr_key_fmt = "%15s" @@ -484,6 +486,34 @@ def rotate_to_lower_triangle( return cell, coord +def _get_lammps_masses(system) -> np.ndarray | None: + """Get masses for the LAMMPS ``Masses`` section. + + Prefer explicitly stored masses when available. Otherwise, infer masses from + ``atom_names`` when all names are valid chemical element symbols. + + Parameters + ---------- + system : dict + System data dictionary + + Returns + ------- + np.ndarray or None + Per-type masses aligned with ``atom_names``. Returns ``None`` when the + masses cannot be determined safely. + """ + atom_names = system["atom_names"] + masses = system.get("masses") + if masses is not None and len(masses) >= len(atom_names): + return np.asarray(masses[: len(atom_names)], dtype=float) + + if not all(name in ELEMENTS for name in atom_names): + return None + + return np.array([Element(name).mass for name in atom_names], dtype=float) + + def from_system_data(system, f_idx=0): ret = "" ret += "\n" @@ -514,6 +544,16 @@ def from_system_data(system, f_idx=0): cell[2][1], ) # noqa: UP031 ret += "\n" + + masses = _get_lammps_masses(system) + if masses is not None: + ret += "Masses\n" + ret += "\n" + mass_fmt = ptr_int_fmt + " " + ptr_float_fmt + " # %s\n" # noqa: UP031 + for ii, (mass, atom_name) in enumerate(zip(masses, system["atom_names"])): + ret += mass_fmt % (ii + 1, mass, atom_name) + ret += "\n" + ret += "Atoms # atomic\n" ret += "\n" coord_fmt = ( diff --git a/tests/test_lammps_lmp_dump.py b/tests/test_lammps_lmp_dump.py index c2c2f811..9a6a3846 100644 --- a/tests/test_lammps_lmp_dump.py +++ b/tests/test_lammps_lmp_dump.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import tempfile import unittest import numpy as np @@ -9,6 +10,9 @@ from dpdata.lammps.lmp import rotate_to_lower_triangle +TEST_DIR = os.path.dirname(__file__) +POSCAR_CONF_LMP = os.path.join(TEST_DIR, "poscars", "conf.lmp") + class TestLmpDump(unittest.TestCase, TestPOSCARoh): def setUp(self): @@ -100,5 +104,30 @@ def test_negative_diagonal(self): ) +class TestLmpDumpMasses(unittest.TestCase): + def test_dump_known_elements_writes_masses(self): + system = dpdata.System(POSCAR_CONF_LMP, type_map=["O", "H"]) + with tempfile.TemporaryDirectory() as tmpdir: + output = os.path.join(tmpdir, "tmp_masses.lmp") + system.to_lammps_lmp(output) + with open(output) as f: + content = f.read() + + self.assertIn("Masses\n", content) + self.assertIn(" 1 15.9994000000 # O", content) + self.assertIn(" 2 1.0079400000 # H", content) + self.assertLess(content.index("Masses\n"), content.index("Atoms # atomic\n")) + + def test_dump_unknown_types_skips_masses(self): + system = dpdata.System(POSCAR_CONF_LMP) + with tempfile.TemporaryDirectory() as tmpdir: + output = os.path.join(tmpdir, "tmp_unknown_types.lmp") + system.to_lammps_lmp(output) + with open(output) as f: + content = f.read() + + self.assertNotIn("Masses\n", content) + + if __name__ == "__main__": unittest.main() From 6d6dcda242a35b621341b49eeba71670ebae6f31 Mon Sep 17 00:00:00 2001 From: "njzjz-bot (driven by OpenClaw (model: custom-chat-jinzhezeng-group/gpt-5.4))[bot]" <48687836+njzjz-bot@users.noreply.github.com> Date: Tue, 31 Mar 2026 03:31:09 +0000 Subject: [PATCH 2/3] style(lammps): apply pre-commit import ordering Run repository pre-commit hooks and keep the ruff import ordering fix. Authored by OpenClaw (model: custom-chat-jinzhezeng-group/gpt-5.4) --- dpdata/lammps/lmp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dpdata/lammps/lmp.py b/dpdata/lammps/lmp.py index 02aab32b..758381e2 100644 --- a/dpdata/lammps/lmp.py +++ b/dpdata/lammps/lmp.py @@ -3,7 +3,7 @@ import numpy as np -from dpdata.periodic_table import Element, ELEMENTS +from dpdata.periodic_table import ELEMENTS, Element ptr_float_fmt = "%15.10f" ptr_int_fmt = "%6d" From 9e336572664a46fbf264fccafa258f82d6492c9a Mon Sep 17 00:00:00 2001 From: "njzjz-bot (driven by OpenClaw (model: custom-chat-jinzhezeng-group/gpt-5.4))[bot]" <48687836+njzjz-bot@users.noreply.github.com> Date: Tue, 31 Mar 2026 04:44:34 +0000 Subject: [PATCH 3/3] fix(lammps): reject mismatched explicit masses Reject explicit system["masses"] arrays unless they match atom_names exactly to avoid silently truncating and mis-mapping LAMMPS Masses entries. Add a regression test covering the mismatched-length case. Authored by OpenClaw (model: custom-chat-jinzhezeng-group/gpt-5.4) --- dpdata/lammps/lmp.py | 17 +++++++++++++++-- tests/test_lammps_lmp_dump.py | 9 +++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/dpdata/lammps/lmp.py b/dpdata/lammps/lmp.py index 758381e2..c9d60ec5 100644 --- a/dpdata/lammps/lmp.py +++ b/dpdata/lammps/lmp.py @@ -502,11 +502,24 @@ def _get_lammps_masses(system) -> np.ndarray | None: np.ndarray or None Per-type masses aligned with ``atom_names``. Returns ``None`` when the masses cannot be determined safely. + + Raises + ------ + ValueError + If explicit ``system["masses"]`` is present but does not match the + length of ``atom_names``. """ atom_names = system["atom_names"] masses = system.get("masses") - if masses is not None and len(masses) >= len(atom_names): - return np.asarray(masses[: len(atom_names)], dtype=float) + if masses is not None: + masses = np.asarray(masses, dtype=float) + if masses.ndim != 1 or len(masses) != len(atom_names): + raise ValueError( + 'Explicit system["masses"] must be a 1D array with the same ' + 'length as system["atom_names"] to write the LAMMPS Masses ' + "section." + ) + return masses if not all(name in ELEMENTS for name in atom_names): return None diff --git a/tests/test_lammps_lmp_dump.py b/tests/test_lammps_lmp_dump.py index 9a6a3846..a717c6cf 100644 --- a/tests/test_lammps_lmp_dump.py +++ b/tests/test_lammps_lmp_dump.py @@ -128,6 +128,15 @@ def test_dump_unknown_types_skips_masses(self): self.assertNotIn("Masses\n", content) + def test_dump_rejects_mismatched_explicit_masses(self): + system = dpdata.System(POSCAR_CONF_LMP, type_map=["O", "H"]) + system.data["masses"] = np.array([15.9994, 1.00794, 99.0]) + + with tempfile.TemporaryDirectory() as tmpdir: + output = os.path.join(tmpdir, "tmp_bad_masses.lmp") + with self.assertRaisesRegex(ValueError, r'system\["masses"\]'): + system.to_lammps_lmp(output) + if __name__ == "__main__": unittest.main()