Skip to content
Open
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
12 changes: 12 additions & 0 deletions src/biotite/structure/bonds.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,13 @@ class BondList(Copyable):
"Input array containing bonds must be either of shape "
"(n,2) or (n,3)"
)
# After per-row sorting a self-bond appears as a row whose two
# atom indices are equal. These cannot represent valid chemistry,
# so reject them at the construction boundary.
if (self._bonds[:, 0] == self._bonds[:, 1]).any():
raise ValueError(
"Input contains a bond from an atom to itself"
)
self._remove_redundant_bonds()
self._max_bonds_per_atom = self._get_max_bonds_per_atom()

Expand Down Expand Up @@ -939,6 +946,11 @@ class BondList(Copyable):

cdef uint32 index1 = _to_positive_index(atom_index1, self._atom_count)
cdef uint32 index2 = _to_positive_index(atom_index2, self._atom_count)
if index1 == index2:
raise ValueError(
f"Cannot create a bond from an atom to itself "
f"(atom index {index1})"
)
_sort(&index1, &index2)

cdef int i
Expand Down
13 changes: 13 additions & 0 deletions src/biotite/structure/io/pdbx/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,19 @@ def _set_intra_residue_bonds(array, atom_site):
# Take the residue name from the first atom index, as the residue
# name is the same for both atoms, since we have only intra bonds
comp_id = array.res_name[bond_array[:, 0]]
# Two distinct atom indices can share the same (res_name, atom_name)
# annotations — that bond would surface in chem_comp_bond as a
# self-bond on the chemical component, which is meaningless.
not_self_bond = atom_id_1 != atom_id_2
atom_id_1 = atom_id_1[not_self_bond]
atom_id_2 = atom_id_2[not_self_bond]
comp_id = comp_id[not_self_bond]
bond_array = bond_array[not_self_bond]
value_order = value_order[not_self_bond]
aromatic_flag = aromatic_flag[not_self_bond]
any_mask = any_mask[not_self_bond]
if len(bond_array) == 0:
return None
_, unique_indices = np.unique(
np.stack([comp_id, atom_id_1, atom_id_2], axis=-1), axis=0, return_index=True
)
Expand Down
23 changes: 23 additions & 0 deletions tests/structure/test_bonds.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,29 @@ def test_invalid_creation():
),
)

# Reject self-bonds at construction time
with pytest.raises(ValueError, match="atom to itself"):
struc.BondList(5, np.array([[2, 2]]))
# Self-bonds expressed via mixed positive and negative indices
# (-1 resolves to 4 with atom_count=5) should also be rejected.
with pytest.raises(ValueError, match="atom to itself"):
struc.BondList(5, np.array([[4, -1]]))


def test_add_self_bond_rejected():
"""
``BondList.add_bond`` must reject bonds whose two atom indices refer to
the same atom, regardless of how the indices are written (positive,
negative, or a mix that resolves to the same positive index).
"""
bond_list = struc.BondList(5)
with pytest.raises(ValueError, match="atom to itself"):
bond_list.add_bond(2, 2)
with pytest.raises(ValueError, match="atom to itself"):
bond_list.add_bond(4, -1)
# The list should remain empty after the rejected adds.
assert len(bond_list.as_array()) == 0


def test_modification(bond_list):
"""
Expand Down