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
9 changes: 9 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,15 @@ 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 sharing the same (res_name, atom_name)
# annotations would surface in chem_comp_bond as a self-bond on the
# chemical component, which is structurally invalid.
if np.any(atom_id_1 == atom_id_2):
raise BadStructureError(
"Structure contains bonded atoms sharing the same "
"(res_name, atom_name) annotations, which cannot be "
"written to chem_comp_bond without producing a self-bond"
)
_, 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/io/test_pdbx.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,29 @@ def test_metal_coordination_bonds():
assert np.all(conn_type_id == "metalc")


def test_set_structure_self_bond_raises():
"""
Two distinct atoms sharing the same ``(res_name, atom_name)`` annotation
with a bond between them would produce a ``chem_comp_bond`` self-bond.
``set_structure()`` must raise :class:`BadStructureError` instead of
silently filtering the offending row.
"""
atoms = struc.AtomArray(2)
atoms.coord[:] = 0.0
atoms.chain_id[:] = "A"
atoms.res_id[:] = 1
atoms.res_name[:] = "ALA"
# Same atom_name on both atoms — the ambiguous annotation.
atoms.atom_name[:] = "CA"
atoms.element[:] = "C"
atoms.hetero[:] = False
atoms.bonds = struc.BondList(2, np.array([[0, 1]]))

pdbx_file = pdbx.BinaryCIFFile()
with pytest.raises(struc.BadStructureError, match="sharing the same"):
pdbx.set_structure(pdbx_file, atoms)


def test_bond_sparsity():
"""
Ensure that only as much intra-residue bonds are written as necessary,
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