Skip to content
Merged
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
18 changes: 16 additions & 2 deletions 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,11 @@ 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:
# 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:
return None
Expand Down Expand Up @@ -411,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])

Expand Down Expand Up @@ -567,7 +577,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
4 changes: 3 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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
**kwargs : dict
Other parameters

Expand All @@ -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
--------
Expand Down Expand Up @@ -100,6 +101,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
34 changes: 34 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,29 @@ 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,
timeout=60,
)
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)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
def test_read_input(self):
# check if dpdata can read the spins
tmp_system = dpdata.System(
Expand Down