Skip to content

Commit a415791

Browse files
author
Han Wang
committed
test(spin-mpi): cover empty-subdomain and NULL-type for spin DPA3
Resolves the two spin-specific gaps left open by the previous commit: - test_pair_deepmd_mpi_dpa3_spin_empty_subdomain: elongated 30 A box + processors '2 1 1' leaves rank 1 with nloc=0. Exercises the copy_from_nlist empty-rank guard for the spin path (the with-comm artifact still runs on rank 1 with nloc_real=0). - test_pair_deepmd_mpi_dpa3_spin_null_type: 2 NULL (LAMMPS type-3, deepmd atype=-1) atoms straddling the x=6.5 rank boundary, within rcut of real atoms on both sides. Goes through DeepSpinPTExpt with nall_real < nall, triggering the has_null_atoms branch that calls build_comm_tensors_positional_with_virtual_atoms (fwd_map-based sendlist remap) for spin. Asserts NULL atoms get zero forces from the deepmd model and real-atom values match the mpi-1 reference. Both compare mpi-2 vs same-archive mpi-1 (atol 1e-8) so any divergence is necessarily in the multi-rank dispatch, not in tracing precision. Runner generalised with --pair-coeff and --mass3 flags (mirrors the non-spin DPA3 runner).
1 parent 86aec7a commit a415791

2 files changed

Lines changed: 151 additions & 4 deletions

File tree

source/lmp/tests/run_mpi_pair_deepmd_spin_dpa3_pt2.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@
6060
"domain decomposition (nswap>0). Pass '1 1 1' for a single-rank "
6161
"reference run on the same archive.",
6262
)
63+
parser.add_argument(
64+
"--pair-coeff",
65+
type=str,
66+
default="* *",
67+
help="pair_coeff arguments (after 'pair_coeff'). Default '* *' "
68+
"uses identity LAMMPS-type-to-deepmd-atype mapping. For NULL-type "
69+
"tests pass e.g. '* * Ni O NULL' so the third LAMMPS type becomes "
70+
"deepmd atype=-1 (filtered before model evaluation).",
71+
)
72+
parser.add_argument(
73+
"--mass3",
74+
type=float,
75+
default=None,
76+
help="Optional mass for LAMMPS atom type 3 (and any higher types). "
77+
"Used by the NULL-type fixture; ignored when only 2 types exist.",
78+
)
6379
args = parser.parse_args()
6480

6581
lammps = PyLammps()
@@ -73,11 +89,16 @@
7389
lammps.read_data(args.DATAFILE)
7490
lammps.mass("1 58")
7591
lammps.mass("2 16")
92+
if args.mass3 is not None:
93+
# NULL-type fixture: third LAMMPS type maps to deepmd atype=-1
94+
# via pair_coeff and is filtered before model evaluation. Mass
95+
# is physically irrelevant.
96+
lammps.mass(f"3 {args.mass3}")
7697
lammps.timestep(0.0005)
7798
lammps.fix("1 all nve")
7899

79100
lammps.pair_style(f"deepspin {args.PB_FILE}")
80-
lammps.pair_coeff("* *")
101+
lammps.pair_coeff(args.pair_coeff)
81102
lammps.compute("virial all centroid/stress/atom NULL pair")
82103
# Per-atom magnetic force components. LAMMPS does not expose ``fm``
83104
# through the legacy ``extract``/``gather_atoms`` registry, so we go

source/lmp/tests/test_lammps_spin_dpa3_pt2.py

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@
5757
/ "deeppot_dpa3_spin_mpi.pt2"
5858
)
5959
data_file = Path(__file__).parent / "data_dpa3_spin_pt2.lmp"
60+
# Elongated-box fixture for the spin empty-subdomain MPI test: x is
61+
# extended to 30 A while atoms remain in x in [3, 13]. Combined with
62+
# ``processors 2 1 1`` this leaves rank 1 (x >= 15) with zero local
63+
# atoms, exercising the ``copy_from_nlist`` empty-rank guard for spin.
64+
data_file_empty_subdomain = (
65+
Path(__file__).parent / "data_dpa3_spin_pt2_empty_subdomain.lmp"
66+
)
67+
# NULL-type fixture: 4 real Ni-O atoms + 2 LAMMPS type-3 atoms
68+
# straddling the x=6.5 rank boundary. With ``pair_coeff * * Ni O NULL``
69+
# LAMMPS type 3 maps to deepmd atype=-1, so those atoms are filtered
70+
# by ``select_real_atoms_coord`` and the comm tensors must be remapped
71+
# via ``fwd_map`` before being handed to the with-comm artifact.
72+
# Forces / force_mag on the 4 real atoms must match the no-NULL
73+
# baseline (mpi-1 reference run); NULL atoms get zero forces from the
74+
# deepmd model.
75+
data_file_null_type = Path(__file__).parent / "data_dpa3_spin_pt2_null_type.lmp"
6076

6177
# 4-atom Ni-O system; same layout as ``test_lammps_spin_pt2.py``. With
6278
# ``processors 2 1 1`` the split sits at x=6.5 -> 2 atoms per rank.
@@ -86,22 +102,56 @@ def setup_module() -> None:
86102
"Skip test because PyTorch support is not enabled.",
87103
)
88104
write_lmp_data_spin(box, coord, spin, type_NiO, data_file)
105+
# Elongated x-axis; atoms unchanged. ``processors 2 1 1`` splits
106+
# at x=15 A and rank 1 owns x >= 15, which is empty.
107+
box_empty = np.array([0, 30, 0, 13, 0, 13, 0, 0, 0])
108+
write_lmp_data_spin(box_empty, coord, spin, type_NiO, data_file_empty_subdomain)
109+
# NULL-type fixture: append 2 LAMMPS type-3 atoms within rcut
110+
# (~4 A) of real atoms on BOTH sides of the x=6.5 rank boundary,
111+
# so they appear in cross-rank sendlists and the fwd_map-based
112+
# comm-tensor remap is genuinely exercised. NULL atoms still need
113+
# spin coordinates (write_lmp_data_spin format); we give them
114+
# zero spin like the type-2 (O) atoms.
115+
coord_null = np.concatenate(
116+
[
117+
coord,
118+
np.array(
119+
[
120+
[5.5, 6.0, 6.0], # rank 0 side, near boundary
121+
[7.5, 7.0, 7.0], # rank 1 side, near boundary
122+
]
123+
),
124+
]
125+
)
126+
spin_null = np.concatenate([spin, np.zeros((2, 3))])
127+
type_null = np.concatenate([type_NiO, np.array([3, 3])])
128+
write_lmp_data_spin(box, coord_null, spin_null, type_null, data_file_null_type)
89129

90130

91131
def teardown_module() -> None:
92-
if data_file.exists():
93-
os.remove(data_file)
132+
for f in [data_file, data_file_empty_subdomain, data_file_null_type]:
133+
if f.exists():
134+
os.remove(f)
94135

95136

96137
def _run_mpi_subprocess(
97138
nprocs: int,
98139
extra_args: list[str] | None = None,
99140
processors: str | None = None,
141+
data_path: Path | None = None,
142+
runner_args: list[str] | None = None,
100143
) -> dict:
101144
"""Run ``run_mpi_pair_deepmd_spin_dpa3_pt2.py`` under
102145
``mpirun -n <nprocs>`` and return
103146
``{"pe", "forces", "force_mag", "virials"}``.
147+
148+
``data_path`` (default ``data_file``) selects the LAMMPS data file
149+
-- the empty-subdomain and NULL-type tests point at non-default
150+
fixtures. ``runner_args`` flows additional flags (e.g.
151+
``--pair-coeff``, ``--mass3``) to the subprocess runner.
104152
"""
153+
if data_path is None:
154+
data_path = data_file
105155
with tempfile.NamedTemporaryFile(mode="r", suffix=".out", delete=False) as f:
106156
out_path = f.name
107157
try:
@@ -111,7 +161,7 @@ def _run_mpi_subprocess(
111161
str(nprocs),
112162
sys.executable,
113163
str(Path(__file__).parent / "run_mpi_pair_deepmd_spin_dpa3_pt2.py"),
114-
str(data_file.resolve()),
164+
str(data_path.resolve()),
115165
str(pb_file_mpi.resolve()),
116166
out_path,
117167
]
@@ -121,6 +171,8 @@ def _run_mpi_subprocess(
121171
argv.extend(["--processors", "1 1 1"])
122172
if extra_args:
123173
argv.extend(extra_args)
174+
if runner_args:
175+
argv.extend(runner_args)
124176
sp.check_call(argv)
125177
with open(out_path) as fh:
126178
lines = fh.read().strip().splitlines()
@@ -176,3 +228,77 @@ def test_pair_deepmd_mpi_dpa3_spin() -> None:
176228
np.testing.assert_allclose(
177229
out_mpi["virials"], out_ref["virials"], atol=1e-8, rtol=0
178230
)
231+
232+
233+
@pytest.mark.skipif(
234+
shutil.which("mpirun") is None, reason="MPI is not installed on this system"
235+
)
236+
@pytest.mark.skipif(
237+
importlib.util.find_spec("mpi4py") is None, reason="mpi4py is not installed"
238+
)
239+
def test_pair_deepmd_mpi_dpa3_spin_empty_subdomain() -> None:
240+
"""Spin DPA3 multi-rank with one empty rank.
241+
242+
Elongated x box (30 A) + ``processors 2 1 1`` puts all 4 atoms on
243+
rank 0; rank 1 has nloc=0. Exercises the C++ ``copy_from_nlist``
244+
empty-rank guard for the spin path (the with-comm artifact still
245+
runs on rank 1 with nloc_real=0). Compares against same-archive
246+
mpi-1 reference.
247+
"""
248+
out_mpi = _run_mpi_subprocess(nprocs=2, data_path=data_file_empty_subdomain)
249+
out_ref = _run_mpi_subprocess(nprocs=1, data_path=data_file_empty_subdomain)
250+
251+
assert out_mpi["pe"] == pytest.approx(out_ref["pe"], rel=1e-10, abs=1e-12)
252+
np.testing.assert_allclose(out_mpi["forces"], out_ref["forces"], atol=1e-8, rtol=0)
253+
np.testing.assert_allclose(
254+
out_mpi["force_mag"], out_ref["force_mag"], atol=1e-8, rtol=0
255+
)
256+
np.testing.assert_allclose(
257+
out_mpi["virials"], out_ref["virials"], atol=1e-8, rtol=0
258+
)
259+
260+
261+
@pytest.mark.skipif(
262+
shutil.which("mpirun") is None, reason="MPI is not installed on this system"
263+
)
264+
@pytest.mark.skipif(
265+
importlib.util.find_spec("mpi4py") is None, reason="mpi4py is not installed"
266+
)
267+
def test_pair_deepmd_mpi_dpa3_spin_null_type() -> None:
268+
"""Spin DPA3 multi-rank with NULL-type atoms straddling the
269+
rank boundary.
270+
271+
Two LAMMPS type-3 atoms (mapped to deepmd atype=-1 via
272+
``pair_coeff * * Ni O NULL``) sit at x=5.5 and x=7.5, just inside
273+
the rcut window of either side of the x=6.5 boundary. They appear
274+
in the cross-rank sendlists and are filtered by
275+
``select_real_atoms_coord`` -- so the spin path goes through
276+
``DeepSpinPTExpt::compute`` with ``nall_real < nall``, triggering
277+
the ``has_null_atoms`` branch that calls
278+
``build_comm_tensors_positional_with_virtual_atoms`` (fwd_map-based
279+
sendlist remap). Compares mpi-2 vs same-archive mpi-1 reference
280+
(nullifying NULL forces and using the same fwd_map remap on rank 0
281+
too).
282+
"""
283+
runner_args = ["--pair-coeff", "* * Ni O NULL", "--mass3", "1.0"]
284+
out_mpi = _run_mpi_subprocess(
285+
nprocs=2, data_path=data_file_null_type, runner_args=runner_args
286+
)
287+
out_ref = _run_mpi_subprocess(
288+
nprocs=1, data_path=data_file_null_type, runner_args=runner_args
289+
)
290+
291+
assert out_mpi["pe"] == pytest.approx(out_ref["pe"], rel=1e-10, abs=1e-12)
292+
np.testing.assert_allclose(out_mpi["forces"], out_ref["forces"], atol=1e-8, rtol=0)
293+
np.testing.assert_allclose(
294+
out_mpi["force_mag"], out_ref["force_mag"], atol=1e-8, rtol=0
295+
)
296+
np.testing.assert_allclose(
297+
out_mpi["virials"], out_ref["virials"], atol=1e-8, rtol=0
298+
)
299+
# Sanity: NULL atoms (ids 5, 6) get exactly zero forces from the
300+
# deepmd model. ``write_lmp_data_spin`` writes atoms in the order
301+
# given (id 1..N), so type-3 NULL atoms are ids 5, 6 (after the 4
302+
# real Ni-O atoms).
303+
np.testing.assert_array_equal(out_mpi["forces"][4:], np.zeros((2, 3)))
304+
np.testing.assert_array_equal(out_mpi["force_mag"][4:], np.zeros((2, 3)))

0 commit comments

Comments
 (0)