Skip to content

Commit d6f62cc

Browse files
authored
Merge pull request #401 from OpenBioSim/fix_400
Fix issue #400
2 parents 721a781 + 3da5fd3 commit d6f62cc

File tree

4 files changed

+68
-6
lines changed

4 files changed

+68
-6
lines changed

corelib/src/libs/SireIO/amberprm.cpp

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -337,11 +337,14 @@ QVector<QVector<int>> indexCMAPs(const QVector<qint64> &cmaps, const QVector<int
337337
for (int i = 0; i < cmaps.count(); i += 6)
338338
{
339339
// format is atom0-atom1-atom2-atom3-atom4-parameter
340-
int mol0 = atom_to_mol_data[cmaps_data[i]]; // DO NOT DIVIDE BY THREE
341-
int mol1 = atom_to_mol_data[cmaps_data[i + 1]]; // THIS IS THE RAW ATOM INDEX
342-
int mol2 = atom_to_mol_data[cmaps_data[i + 2]];
343-
int mol3 = atom_to_mol_data[cmaps_data[i + 3]];
344-
int mol4 = atom_to_mol_data[cmaps_data[i + 4]];
340+
// CMAP indices are plain 1-based atom numbers (unlike bonds/angles/dihedrals
341+
// which are 3×(atom_0based)). Subtract 1 to convert to the 0-based index
342+
// used by atom_to_mol.
343+
int mol0 = atom_to_mol_data[cmaps_data[i] - 1];
344+
int mol1 = atom_to_mol_data[cmaps_data[i + 1] - 1];
345+
int mol2 = atom_to_mol_data[cmaps_data[i + 2] - 1];
346+
int mol3 = atom_to_mol_data[cmaps_data[i + 3] - 1];
347+
int mol4 = atom_to_mol_data[cmaps_data[i + 4] - 1];
345348

346349
if (mol0 != mol1 or mol0 != mol2 or mol0 != mol3 or mol0 != mol4)
347350
throw SireIO::parse_error(
@@ -2249,7 +2252,10 @@ std::tuple<QVector<qint64>, QHash<CMAPParameter, qint64>> getCMAPData(const Ambe
22492252
QHash<CMAPParameter, qint64> param_to_idx;
22502253
QVector<qint64> cmap_idxs;
22512254

2252-
start_idx /= 3;
2255+
// NOTE: start_idx is in atom units (0-based count of atoms before this
2256+
// molecule). CMAP indices in the prmtop file are plain 1-based atom
2257+
// numbers (NOT multiplied by 3 as bonds/angles/dihedrals are).
2258+
// Do NOT divide by 3 here.
22532259

22542260
const auto info = params.info();
22552261
const auto cmaps = params.cmapFunctions().parameters();

doc/source/changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ organisation on `GitHub <https://github.com/openbiosim/sire>`__.
1717

1818
* Please add an item to this CHANGELOG for any new features or bug fixes when creating a PR.
1919

20+
* Fixed a bug in the AMBER prmtop writer where CMAP atom indices were calculated
21+
incorrectly for systems containing more than one molecule with CMAP terms (e.g.
22+
multi-chain glycoproteins).
23+
2024
`2025.4.0 <https://github.com/openbiosim/sire/compare/2025.3.0...2025.4.0>`__ - February 2026
2125
---------------------------------------------------------------------------------------------
2226

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ def amber_cmap():
260260
return sr.load_test_files("amber_cmap.prm7")
261261

262262

263+
@pytest.fixture(scope="session")
264+
def multichain_cmap():
265+
return sr.load_test_files("multichain_cmap.prm7", "multichain_cmap.rst7")
266+
267+
263268
@pytest.fixture(scope="session")
264269
def gromacs_cmap():
265270
return sr.load_test_files("1aki.gro", "1aki.top")

tests/io/test_ambercmap.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,53 @@ def test_amber_cmap(tmpdir, amber_cmap):
6060
assert line1 == line2
6161

6262

63+
def test_amber_multichain_cmap(tmpdir, multichain_cmap):
64+
"""Regression test for multi-chain CMAP write bug.
65+
66+
When a topology has more than one molecule with CMAP terms, the write path
67+
previously applied a spurious '/= 3' to the per-molecule atom offset,
68+
producing wrong global atom indices for molecules 2, 3, … . The re-parse
69+
of the generated text then raised:
70+
"there is a cmap between more than one different molecule"
71+
"""
72+
mols = multichain_cmap
73+
74+
# Collect per-molecule CMAP term counts indexed by position in the
75+
# molecule list. There must be at least two molecules with CMAP
76+
# to trigger the multi-chain bug.
77+
cmap_counts = {}
78+
for i, mol in enumerate(mols.molecules()):
79+
if mol.has_property("cmap"):
80+
cmap_counts[i] = len(mol.property("cmap").parameters())
81+
82+
assert (
83+
len(cmap_counts) >= 2
84+
), "Expected at least two molecules with CMAP terms in this topology"
85+
86+
dir = tmpdir.mkdir("test_amber_multichain_cmap")
87+
88+
# Write the topology back to file. (This is where the bug used to trigger.)
89+
f = sr.save(mols, dir.join("output"), format="prm7")
90+
91+
# Reead the file back in.
92+
mols2 = sr.load(f)
93+
94+
# Each molecule must still have the same number of CMAP terms after the
95+
# roundtrip.
96+
for i, count in cmap_counts.items():
97+
mol2 = mols2[i]
98+
assert mol2.has_property(
99+
"cmap"
100+
), f"Molecule at index {i} lost its cmap property after roundtrip"
101+
count2 = len(mol2.property("cmap").parameters())
102+
assert (
103+
count2 == count
104+
), f"Molecule at index {i}: CMAP count changed from {count} to {count2}"
105+
106+
# Verify a second write also succeeds without error.
107+
sr.save(mols2, dir.join("output2"), format="prm7")
108+
109+
63110
def test_amber_cmap_grotop(tmpdir, amber_cmap):
64111
"""Testing reading and writing from amber to gromacs and back to amber."""
65112
mols = amber_cmap.clone()

0 commit comments

Comments
 (0)