Skip to content

Commit 2250195

Browse files
Robust sort_backbone: detect disconnected selections and avoid infinite loops (#5113)
* fix #5112 * analysis.polymer: sort_backbone can hang indefinitely when the input AtomGroup is disconnected (e.g., some backbone atoms were excluded by selection) * changed logic for sort_backbone * added test for internal missing * update CHANGELOG
1 parent 9ccde9b commit 2250195

File tree

3 files changed

+36
-32
lines changed

3 files changed

+36
-32
lines changed

package/CHANGELOG

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ Fixes
4343
* Fixes the benchmark `SimpleRmsBench` in `benchmarks/analysis/rms.py`
4444
by changing the way weights for RMSD are calculated, instead of
4545
directly passing them. (Issue #3520, PR #5006)
46+
* `analysis.polymer.sort_backbone` is now working for discontinuous polymers
47+
and does not result in an infinite loop (Issue #5112, PR #5113)
4648

4749
Enhancements
4850
* Added support for reading and processing streamed data in `coordinates.base`
@@ -90,6 +92,7 @@ Changes
9092
`analysis.lineardensity.LinearDensity.totalmass` (PR #5007)
9193
* Remove `default` channel from RTD conda env. (Issue # 5036, PR # 5037)
9294

95+
9396
Deprecations
9497
* The RDKit converter parameter `NoImplicit` has been deprecated in favour of
9598
`implicit_hydrogens` and `inferrer` parameters. `max_iter` has been moved

package/MDAnalysis/analysis/polymer.py

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -67,37 +67,32 @@ def sort_backbone(backbone):
6767
6868
.. versionadded:: 0.20.0
6969
"""
70-
if not backbone.n_fragments == 1:
71-
raise ValueError(
72-
"{} fragments found in backbone. "
73-
"backbone must be a single contiguous AtomGroup"
74-
"".format(backbone.n_fragments)
75-
)
70+
degrees = [len(atom.bonded_atoms & backbone) for atom in backbone]
71+
deg1_atoms = [atom for atom, d in zip(backbone, degrees) if d == 1]
72+
wrong_atoms = [
73+
atom for atom, d in zip(backbone, degrees) if d not in (1, 2)
74+
]
7675

77-
branches = [at for at in backbone if len(at.bonded_atoms & backbone) > 2]
78-
if branches:
79-
# find which atom has too many bonds for easier debug
76+
if len(wrong_atoms) > 0:
8077
raise ValueError(
81-
"Backbone is not linear. "
82-
"The following atoms have more than two bonds in backbone: {}."
83-
"".format(",".join(str(a) for a in branches))
78+
"Backbone contains atoms with connectivity degree not equal to 1 or 2. "
79+
"This suggests branches or isolated atoms. Problematic atoms: {}."
80+
"".format(", ".join(str(a) for a in wrong_atoms))
8481
)
8582

86-
caps = [
87-
atom for atom in backbone if len(atom.bonded_atoms & backbone) == 1
88-
]
89-
if not caps:
90-
# cyclical structure
83+
if len(deg1_atoms) != 2:
9184
raise ValueError(
92-
"Could not find starting point of backbone, "
93-
"is the backbone cyclical?"
85+
"Backbone connectivity invalid: "
86+
"expected exactly 2 atoms with connectivity degree 1 (caps). "
87+
"Cyclical structures are not supported. "
88+
"Atoms with connectivity degree 1 found: {}."
89+
"".format(", ".join(str(a) for a in deg1_atoms))
9490
)
9591

9692
# arbitrarily choose one of the capping atoms to be the startpoint
97-
sorted_backbone = AtomGroup([caps[0]])
93+
sorted_backbone = AtomGroup([deg1_atoms[0]])
9894

99-
# iterate until the sorted chain length matches the backbone size
100-
while len(sorted_backbone) < len(backbone):
95+
for _ in range(len(backbone) - 1):
10196
# current end of the chain
10297
end_atom = sorted_backbone[-1]
10398

testsuite/MDAnalysisTests/analysis/test_persistencelength.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -149,25 +149,31 @@ def test_sortbb(self, u):
149149

150150
assert_equal(s_ag.ids, [0, 1, 4, 6, 8])
151151

152-
def test_not_fragment(self, u):
153-
# two fragments don't work
154-
bad_ag = u.residues[0].atoms[:2] + u.residues[1].atoms[:2]
155-
with pytest.raises(ValueError):
156-
polymer.sort_backbone(bad_ag)
157-
158152
def test_branches(self, u):
159153
# includes side branches, can't sort
160154
bad_ag = u.atoms[:10] # include -H etc
161155

162-
with pytest.raises(ValueError):
156+
with pytest.raises(ValueError, match="branches or isolated atoms"):
163157
polymer.sort_backbone(bad_ag)
164158

159+
def test_isolated(self, u):
160+
u = mda.Universe.empty(4, trajectory=True)
161+
bondlist = [(0, 1), (1, 2)]
162+
u.add_TopologyAttr(Bonds(bondlist))
163+
with pytest.raises(ValueError, match="branches or isolated atoms"):
164+
polymer.sort_backbone(u.atoms)
165+
166+
def test_missing_internal(self, u):
167+
u = mda.Universe.empty(4, trajectory=True)
168+
bondlist = [(0, 1), (2, 3)]
169+
u.add_TopologyAttr(Bonds(bondlist))
170+
with pytest.raises(ValueError, match="Backbone connectivity invalid"):
171+
polymer.sort_backbone(u.atoms)
172+
165173
def test_circular(self):
166174
u = mda.Universe.empty(6, trajectory=True)
167175
# circular structure
168176
bondlist = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)]
169177
u.add_TopologyAttr(Bonds(bondlist))
170-
171-
with pytest.raises(ValueError) as ex:
178+
with pytest.raises(ValueError, match="Cyclical"):
172179
polymer.sort_backbone(u.atoms)
173-
assert "cyclical" in str(ex.value)

0 commit comments

Comments
 (0)