@@ -316,7 +316,7 @@ def _try_strip_unmatched_terminals(
316316_BONDTYPE_ORDER = {"1" : 1 , "2" : 2 , "3" : 3 , "4" : 4 , "5" : 5 , "6" : 6 , "ar" : 1 , "un" : 1 , "mc" : 1 }
317317
318318
319- def _detect_cross_residue_bonds (mol , selidx ):
319+ def _detect_interresidue_bonds (mol , selidx ):
320320 """Return ``(cross_bonds, sel_start, sel_end)`` for covalent bonds linking
321321 the selected residue to the rest of ``mol``.
322322
@@ -342,55 +342,39 @@ def _detect_cross_residue_bonds(mol, selidx):
342342 else :
343343 cross_bonds .append ((b - sel_start , a , bt ))
344344
345- # PDB inputs often arrive without explicit peptide / phosphodiester bonds
346- # (CONECT records cover only HET groups). Without them, the boundary
347- # atoms look free-standing and AddHs over-protonates them. Mirror the
348- # proximity check used by ``_has_peptide_neighbour`` in
349- # nonstandard_residues so a caller doesn't have to run ``mol.guessBonds``
350- # first.
351- sel_names = mol .name [selidx ]
352- sel_elems = mol .element [selidx ]
353-
354- def _add_cross_by_proximity (local_idx , other_mask_full , threshold ):
355- if local_idx in {li for li , _ , _ in cross_bonds }:
356- return
357- own_pos = mol .coords [selidx [local_idx ], :, mol .frame ]
358- other_mask = other_mask_full .copy ()
359- other_mask [selidx ] = False
360- candidates = np .where (other_mask )[0 ]
361- if not len (candidates ):
362- return
363- d = np .linalg .norm (
364- mol .coords [candidates , :, mol .frame ] - own_pos , axis = 1
365- )
366- within = d < threshold
367- if not within .any ():
368- return
369- partner = int (candidates [np .argmin (np .where (within , d , np .inf ))])
370- cross_bonds .append ((local_idx , partner , "1" ))
371-
372- # Peptide N-C bonds (~1.33 A, threshold 1.6 A)
373- if {"N" , "CA" , "C" }.issubset (sel_names ):
374- for own_side , other_name in (("N" , "C" ), ("C" , "N" )):
375- hits = np .where (sel_names == own_side )[0 ]
376- if len (hits ):
377- _add_cross_by_proximity (
378- int (hits [0 ]), mol .name == other_name , threshold = 1.6
379- )
380-
381- # Nucleic acid phosphodiester P-O3' bonds (~1.6 A, threshold 1.8 A).
382- # Two directions: (1) own P to external O3' of previous residue,
383- # (2) own O3' to external P of next residue. The O3' check also runs for
384- # 5'-terminal residues that lack their own P but still bond to next.
385- if "P" in sel_elems :
386- for own_p_idx in np .where (sel_elems == "P" )[0 ]:
387- _add_cross_by_proximity (
388- int (own_p_idx ), mol .element == "O" , threshold = 1.8
389- )
390- for own_o3_idx in np .where (sel_names == "O3'" )[0 ]:
391- _add_cross_by_proximity (
392- int (own_o3_idx ), mol .element == "P" , threshold = 1.8
345+ # PDB inputs often arrive without explicit peptide / phosphodiester /
346+ # isopeptide bonds (CONECT records cover only HET groups). Without them the
347+ # boundary atoms look free-standing and AddHs over-protonates them (or leaves
348+ # an isopeptide carbon over-valent). Recover them from geometry against the
349+ # file-adjacent residues using the shared ``geometric_interresidue_links`` primitive, so
350+ # the definition and thresholds match autoSegment / detect / systemPrepare.
351+ # This covers the standard peptide / phosphodiester backbone AND the
352+ # non-standard side-chain isopeptide (e.g. microcystin's ACB.CG -> ARG.N) in
353+ # one pass.
354+ from moleculekit .tools .nonstandard_residues import geometric_interresidue_links
355+
356+ def _residue_atoms_of (rep ):
357+ mask = (
358+ (mol .resid == mol .resid [rep ])
359+ & (mol .chain == mol .chain [rep ])
360+ & (mol .insertion == mol .insertion [rep ])
361+ & (mol .segid == mol .segid [rep ])
393362 )
363+ return np .where (mask )[0 ]
364+
365+ neighbors = []
366+ if sel_start > 0 :
367+ neighbors .append (_residue_atoms_of (sel_start - 1 ))
368+ if sel_end + 1 < mol .numAtoms :
369+ neighbors .append (_residue_atoms_of (sel_end + 1 ))
370+
371+ have_cross = {li for li , _ , _ in cross_bonds }
372+ for neighbor in neighbors :
373+ for ia , ib , _kind in geometric_interresidue_links (mol , selidx , neighbor ):
374+ local_idx = int (ia ) - sel_start
375+ if local_idx not in have_cross :
376+ cross_bonds .append ((local_idx , int (ib ), "1" ))
377+ have_cross .add (local_idx )
394378
395379 return cross_bonds , sel_start , sel_end
396380
@@ -645,7 +629,7 @@ def template_residue_from_smiles(
645629 "The selection contains gaps in the atom indexes. Please select a single molecule residue only."
646630 )
647631
648- cross_bonds , sel_start , sel_end = _detect_cross_residue_bonds (mol , selidx )
632+ cross_bonds , sel_start , sel_end = _detect_interresidue_bonds (mol , selidx )
649633
650634 residue = mol .copy (sel = selidx )
651635 if guessBonds :
@@ -912,7 +896,7 @@ def template_residue_from_molecule(
912896 )
913897 return
914898
915- cross_bonds , sel_start , sel_end = _detect_cross_residue_bonds (mol , selidx )
899+ cross_bonds , sel_start , sel_end = _detect_interresidue_bonds (mol , selidx )
916900
917901 residue = mol .copy (sel = selidx )
918902 if guessBonds :
0 commit comments