Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion dpdata/formats/lammps/lmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}


Expand Down Expand Up @@ -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"}:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads-up: the still-open PR #1020 (fix/issue-995) adds this exact guard (if atom_style not in {"atomic", "spin"}: return None) -- only the comment placement differs. They branch from a shared merge-base, so git merge-tree shows one add/add conflict right here; whichever of #1020 / #1032 merges second will hit it (cosmetic -- identical logic).

This PR is the natural completion of #1020: #1020's whitelist references "spin" but never registered it in ATOM_STYLE_COLUMNS, so that arm was dead code -- the "spin" entry added here (line 25) makes it live, and emitting Atoms # spin fixes the # spin-labeled-file regression #1020 introduced. Suggest sequencing them: merge one, then rebase the other and drop the duplicate guard hunk (keep #1020's detect_atom_style neutral-charge fix + tests, keep #1032's dict entry + writer).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on the sequencing. No code change is appropriate until either #1020 or #1032 merges. After the first PR lands, the other branch should be rebased and the duplicate get_spins guard hunk dropped while preserving #1020's detection fixes and #1032's real spin-style definition and writer support. I am leaving this thread unresolved until that coordination step can be completed.

Coding agent: Codex
Codex version: codex-cli 0.144.6
Model: gpt-5.6-sol
Reasoning effort: xhigh

# 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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion dpdata/plugins/lammps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]
Expand Down
33 changes: 33 additions & 0 deletions tests/test_lammps_spin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
import shutil
import subprocess
import unittest

import numpy as np
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand Down
Loading