From bfc267806359abb60e03dfd4601a44dd4184cfcc Mon Sep 17 00:00:00 2001 From: njzjz-bot Date: Thu, 16 Jul 2026 12:24:08 +0800 Subject: [PATCH] fix(gromacs): preserve inferred atom type order Infer GROMACS atom types in first-seen order when no type_map is supplied. Add a regression fixture matching issue #740 so both atom names and type IDs are checked instead of membership alone. Coding-Agent: Codex Codex-Version: codex-cli 0.144.4 Model: gpt-5.6-sol Reasoning-Effort: xhigh --- dpdata/formats/gromacs/gro.py | 7 ++++++- tests/gromacs/type_order.gro | 7 +++++++ tests/test_gromacs_gro.py | 10 ++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tests/gromacs/type_order.gro diff --git a/dpdata/formats/gromacs/gro.py b/dpdata/formats/gromacs/gro.py index 0c61544fd..a5950c7be 100644 --- a/dpdata/formats/gromacs/gro.py +++ b/dpdata/formats/gromacs/gro.py @@ -77,7 +77,12 @@ def file_to_system_data(fname: FileType, format_atom_name=True, **kwargs): posis = np.array(posis) if frame == 1: system["orig"] = np.zeros(3) - system["atom_names"] = list(set(names)) + # A .gro file does not carry a separate atom-type table. When + # callers do not provide ``type_map``, infer that table from + # the first occurrence of each atom name. ``dict`` preserves + # insertion order, unlike ``set`` whose hash-dependent order + # could silently change the type IDs used by later exporters. + system["atom_names"] = list(dict.fromkeys(names)) system["atom_numbs"] = [ names.count(ii) for ii in system["atom_names"] ] diff --git a/tests/gromacs/type_order.gro b/tests/gromacs/type_order.gro new file mode 100644 index 000000000..ac52c7386 --- /dev/null +++ b/tests/gromacs/type_order.gro @@ -0,0 +1,7 @@ +Generated with MDTraj, t= 0.0 + 4 + 1MOL LI 1 0.254 0.254 0.024 + 2MOL CL 2 0.507 0.000 0.000 + 3MOL P 3 0.507 0.507 0.507 + 4MOL S1 4 0.896 0.389 0.118 + 1.01400 1.01400 1.01400 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 diff --git a/tests/test_gromacs_gro.py b/tests/test_gromacs_gro.py index 674c65100..a779a8f4b 100644 --- a/tests/test_gromacs_gro.py +++ b/tests/test_gromacs_gro.py @@ -7,6 +7,16 @@ class TestGromacsGro(unittest.TestCase): + def test_infer_atom_types_in_first_seen_order(self): + # Without an explicit type_map, atom names define the type IDs in the + # order in which they first occur in the .gro file. Checking membership + # alone would miss the order regression reported for this conversion. + system = dpdata.System("gromacs/type_order.gro") + + self.assertEqual(system["atom_names"], ["Li", "Cl", "P", "S"]) + self.assertEqual(system["atom_numbs"], [1, 1, 1, 1]) + self.assertEqual(system["atom_types"].tolist(), [0, 1, 2, 3]) + def test_read_file(self): system = dpdata.System("gromacs/1h.gro", type_map=["H", "O"]) self.assertTrue("H" in system["atom_names"])