From c7cdaace41d949048d3d8951ca2b12184dd9bd35 Mon Sep 17 00:00:00 2001 From: njzjz-bot Date: Thu, 16 Jul 2026 12:27:06 +0800 Subject: [PATCH 1/3] fix(lammps): emit valid spin atom style sections Mark data files with spin vectors as Atoms # spin and teach automatic parsing about the official spin atom style. Add a LAMMPS integration regression; the previous test only inspected text while the atomic header made LAMMPS reject the extra columns. Coding-Agent: Codex Codex-Version: codex-cli 0.144.4 Model: gpt-5.6-sol Reasoning-Effort: xhigh --- dpdata/formats/lammps/lmp.py | 13 ++++++++++++- dpdata/plugins/lammps.py | 3 ++- tests/test_lammps_spin.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/dpdata/formats/lammps/lmp.py b/dpdata/formats/lammps/lmp.py index c9d60ec5..26278064 100644 --- a/dpdata/formats/lammps/lmp.py +++ b/dpdata/formats/lammps/lmp.py @@ -20,6 +20,9 @@ "molecular": (0, 2, 3, 4, 5, True, False, None), "dipole": (0, 1, 3, 4, 5, False, True, 2), "sphere": (0, 1, 4, 5, 6, False, False, None), + # LAMMPS ``atom_style spin`` stores x/y/z in the same columns as atomic + # style, followed by a unit spin direction and its magnetic moment. + "spin": (0, 1, 2, 3, 4, False, False, None), } @@ -327,6 +330,10 @@ def get_charges(lines: list[str], atom_style: str = "atomic") -> np.ndarray | No def get_spins(lines: list[str], atom_style: str = "atomic") -> np.ndarray | None: + if atom_style not in {"atomic", "spin"}: + # Extra columns in dipole/sphere/etc. styles describe those styles' + # own properties and must never be reinterpreted as magnetic spins. + return None atom_lines = get_atoms(lines) if len(atom_lines[0].split()) < 8: return None @@ -567,7 +574,11 @@ def from_system_data(system, f_idx=0): ret += mass_fmt % (ii + 1, mass, atom_name) ret += "\n" - ret += "Atoms # atomic\n" + # The extra direction/magnitude columns have an official LAMMPS layout: + # they belong to ``atom_style spin``, not ``atom_style atomic``. Using the + # matching section annotation lets ``read_data`` validate the rows. + atom_style = "spin" if "spins" in system else "atomic" + ret += f"Atoms # {atom_style}\n" ret += "\n" coord_fmt = ( ptr_int_fmt diff --git a/dpdata/plugins/lammps.py b/dpdata/plugins/lammps.py index 9a462265..13e8a143 100644 --- a/dpdata/plugins/lammps.py +++ b/dpdata/plugins/lammps.py @@ -60,7 +60,7 @@ def from_system( atom_style : str, optional The LAMMPS atom style. Default is "auto" which attempts to detect the style automatically from the file. Can also be explicitly set to: - atomic, full, charge, bond, angle, molecular, dipole, sphere + atomic, full, charge, bond, angle, molecular, dipole, sphere, spin **kwargs : dict Other parameters @@ -100,6 +100,7 @@ def from_system( - molecular: atom-ID molecule-ID atom-type x y z - dipole: atom-ID atom-type charge x y z mux muy muz - sphere: atom-ID atom-type diameter density x y z + - spin: atom-ID atom-type x y z spx spy spz sp """ with open_file(file_name) as fp: lines = [line.rstrip("\n") for line in fp] diff --git a/tests/test_lammps_spin.py b/tests/test_lammps_spin.py index bcb3442b..d8b28454 100644 --- a/tests/test_lammps_spin.py +++ b/tests/test_lammps_spin.py @@ -2,6 +2,7 @@ import os import shutil +import subprocess import unittest import numpy as np @@ -54,10 +55,20 @@ def test_dump_input(self): with open(self.lmp_coord_name) as f: c = f.read() + self.assertIn("Atoms # spin", c) coord_ref = """ 1 1 0.0000000000 0.0000000000 0.0000000000 0.6000000000 0.8000000000 0.0000000000 5.0000000000 2 2 1.2621856000 0.7018028000 0.5513885000 0.0000000000 0.8000000000 0.6000000000 5.0000000000""" self.assertTrue(coord_ref in c) + # Auto-detection must understand the official spin annotation and + # reconstruct the original vectors, including their magnitudes. + roundtrip = dpdata.System( + self.lmp_coord_name, fmt="lammps/lmp", type_map=["O", "H"] + ) + np.testing.assert_allclose( + roundtrip.data["spins"], self.tmp_system.data["spins"] + ) + def test_dump_input_zero_spin(self): self.tmp_system.data["spins"] = [[[0, 0, 0], [0, 0, 0]]] self.tmp_system.to("lammps/lmp", self.lmp_coord_name) @@ -68,6 +79,28 @@ def test_dump_input_zero_spin(self): 2 2 1.2621856000 0.7018028000 0.5513885000 0.0000000000 0.0000000000 1.0000000000 0.0000000000""" self.assertTrue(coord_ref in c) + def test_dump_input_is_accepted_by_lammps(self): + """The generated spin section must be valid for LAMMPS itself.""" + lmp = shutil.which("lmp") + if lmp is None: + self.skipTest("LAMMPS executable is not installed") + self.tmp_system.to("lammps/lmp", self.lmp_coord_name) + result = subprocess.run( + [lmp, "-log", "none", "-screen", "none"], + input=( + "units metal\n" + "atom_style spin\n" + f"read_data {self.lmp_coord_name}\n" + "run 0\n" + ), + text=True, + capture_output=True, + check=False, + ) + if result.returncode and "Unrecognized atom style" in result.stderr: + self.skipTest("LAMMPS was built without the SPIN package") + self.assertEqual(result.returncode, 0, result.stderr) + def test_read_input(self): # check if dpdata can read the spins tmp_system = dpdata.System( From 4c7d007174968dc9393ab963c9ccd0abee45d916 Mon Sep 17 00:00:00 2001 From: "A bot of @njzjz" <48687836+njzjz-bot@users.noreply.github.com> Date: Wed, 19 Aug 2026 23:34:12 +0800 Subject: [PATCH 2/3] fix(lammps): resolve spin-style conflict Preserve the atom-style guard from upstream #1020 and route explicit spin-style data through the legacy spin-column parser. Coding-Agent: ChatGPT Model: GPT-5.6 Sol --- dpdata/formats/lammps/lmp.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dpdata/formats/lammps/lmp.py b/dpdata/formats/lammps/lmp.py index 26278064..c258a951 100644 --- a/dpdata/formats/lammps/lmp.py +++ b/dpdata/formats/lammps/lmp.py @@ -330,9 +330,10 @@ def get_charges(lines: list[str], atom_style: str = "atomic") -> np.ndarray | No def get_spins(lines: list[str], atom_style: str = "atomic") -> np.ndarray | None: - if atom_style not in {"atomic", "spin"}: - # Extra columns in dipole/sphere/etc. styles describe those styles' - # own properties and must never be reinterpreted as magnetic spins. + # This branch predates explicit LAMMPS spin-style support and stores spin + # columns only in dpdata's legacy atomic layout. Other registered styles + # use their extra columns for unrelated physical quantities. + if atom_style != "atomic": return None atom_lines = get_atoms(lines) if len(atom_lines[0].split()) < 8: @@ -418,7 +419,9 @@ def system_data( if charges is not None: system["charges"] = np.array([charges]) - spins = get_spins(lines, atom_style=atom_style) + spins = get_spins( + lines, atom_style="atomic" if atom_style == "spin" else atom_style + ) if spins is not None: system["spins"] = np.array([spins]) From 84a9323079a0d7f25508d1a1f362daa643781c89 Mon Sep 17 00:00:00 2001 From: "A bot of @njzjz" <48687836+njzjz-bot@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:14:20 +0800 Subject: [PATCH 3/3] chore(lammps): address review feedback Document the spin return field and bound the LAMMPS validation subprocess runtime. Coding-Agent: ChatGPT Model: GPT-5.6 Sol --- dpdata/plugins/lammps.py | 1 + tests/test_lammps_spin.py | 1 + 2 files changed, 2 insertions(+) diff --git a/dpdata/plugins/lammps.py b/dpdata/plugins/lammps.py index 13e8a143..2ff5a966 100644 --- a/dpdata/plugins/lammps.py +++ b/dpdata/plugins/lammps.py @@ -70,6 +70,7 @@ def from_system( System data dictionary with additional data based on atom style: - charges: For styles with charge information (full, charge, dipole) - molecule_ids: For styles with molecule information (full, bond, angle, molecular) + - spins: For spin style with spin vectors Examples -------- diff --git a/tests/test_lammps_spin.py b/tests/test_lammps_spin.py index d8b28454..976fb9c3 100644 --- a/tests/test_lammps_spin.py +++ b/tests/test_lammps_spin.py @@ -96,6 +96,7 @@ def test_dump_input_is_accepted_by_lammps(self): text=True, capture_output=True, check=False, + timeout=60, ) if result.returncode and "Unrecognized atom style" in result.stderr: self.skipTest("LAMMPS was built without the SPIN package")