Skip to content

Commit b7a96d9

Browse files
committed
Account for gaps in entity-reference alignment
When checking that the entity sequence matches that in the reference database, consider gaps in the alignment resulting from SeqDif records that correspond to insertions or deletions. Closes #181.
1 parent 1cd7de2 commit b7a96d9

2 files changed

Lines changed: 129 additions & 21 deletions

File tree

‎ihm/dumper.py‎

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -552,9 +552,10 @@ def _get_sequence(self, reference):
552552
(code if len(code) == 1 else '(%s)' % code
553553
for code in fullrefseq[db_begin - 1:db_end]), 70))
554554

555-
def _check_seq_dif(self, entity, refseq, align):
555+
def _check_seq_dif(self, entity, ref, refseq, align):
556556
"""Check all SeqDif objects for the Entity sequence. Return the mutated
557-
sequence (to match the reference)."""
557+
sequence (to match the reference) plus sets of all insertion
558+
and deletion positions."""
558559
entseq = [comp.code_canonical for comp in entity.sequence]
559560
mutations = []
560561
insertions = []
@@ -569,14 +570,15 @@ def _check_seq_dif(self, entity, refseq, align):
569570
for sd in insertions:
570571
self._check_seq_dif_entity(entity, entseq, sd)
571572
for sd in deletions:
572-
self._check_seq_dif_reference(entity, refseq, sd)
573+
self._check_seq_dif_reference(ref, refseq, sd)
573574
for sd in mutations:
574575
self._check_seq_dif_entity(entity, entseq, sd)
575-
# todo: db_seq_id is often missing so we can't check reference
576-
# info - unless we determine it automatically from seq_id
576+
# we don't check db_monomer against the reference sequence here,
577+
# but this will be done later when we check the alignment
577578
if sd.db_monomer:
578579
entseq[sd.seq_id - 1] = sd.db_monomer.code_canonical
579-
return entseq
580+
return (entseq, frozenset(sd.seq_id for sd in insertions),
581+
frozenset(sd.db_seq_id for sd in deletions))
580582

581583
def _check_seq_dif_entity(self, entity, entseq, sd):
582584
"""Make sure the entity information in a SeqDif record matches"""
@@ -591,18 +593,18 @@ def _check_seq_dif_entity(self, entity, entseq, sd):
591593
% (sd.monomer.code_canonical, entity,
592594
entseq[sd.seq_id - 1], sd.seq_id))
593595

594-
def _check_seq_dif_reference(self, entity, refseq, sd):
596+
def _check_seq_dif_reference(self, ref, refseq, sd):
595597
"""Make sure the reference information in a SeqDif record matches"""
596598
if sd.db_seq_id < 1 or sd.db_seq_id > len(refseq):
597599
raise IndexError("SeqDif.db_seq_id for %s is %d, out of "
598600
"range 1-%d"
599-
% (entity, sd.db_seq_id, len(refseq)))
601+
% (ref, sd.db_seq_id, len(refseq)))
600602
if (sd.db_monomer and len(refseq[sd.db_seq_id - 1]) == 1
601603
and sd.db_monomer.code_canonical != refseq[sd.db_seq_id - 1]):
602604
raise ValueError("SeqDif.db_monomer one-letter code (%s) does "
603605
"not match that in %s (%s at position %d)"
604-
% (sd.db_monomer.code_canonical, entity,
605-
refseq[sd.seq_id - 1], sd.db_seq_id))
606+
% (sd.db_monomer.code_canonical, ref,
607+
refseq[sd.db_seq_id - 1], sd.db_seq_id))
606608

607609
def _get_ranges(self, entity, fullrefseq, align):
608610
"""Get the sequence ranges for an Entity and Reference"""
@@ -612,6 +614,31 @@ def _get_ranges(self, entity, fullrefseq, align):
612614
(align.db_begin,
613615
len(fullrefseq) if align.db_end is None else align.db_end))
614616

617+
def _get_gapped_alignment(self, entity_rng, db_rng, entseq, refseq,
618+
insertions, deletions):
619+
"""Get the given ranges from the entity and reference sequences,
620+
with gaps added to account for any insertion or deletion SeqDif
621+
records"""
622+
gapentseq = []
623+
gaprefseq = []
624+
ent_i = entity_rng[0]
625+
ref_i = db_rng[0]
626+
while ent_i <= entity_rng[1] and ref_i <= db_rng[1]:
627+
if ent_i in insertions:
628+
gapentseq.append(entseq[ent_i - 1])
629+
ent_i += 1
630+
gaprefseq.append('-')
631+
elif ref_i in deletions:
632+
gapentseq.append('-')
633+
gaprefseq.append(refseq[ref_i - 1])
634+
ref_i += 1
635+
else:
636+
gapentseq.append(entseq[ent_i - 1])
637+
gaprefseq.append(refseq[ref_i - 1])
638+
ref_i += 1
639+
ent_i += 1
640+
return gapentseq, gaprefseq
641+
615642
def _check_reference_sequence(self, entity, ref):
616643
"""Make sure that the Entity and Reference sequences match"""
617644
for align in ref._get_alignments():
@@ -625,7 +652,11 @@ def _check_alignment(self, entity, ref, align):
625652
# Reference sequence may contain non-standard residues, so parse them
626653
# out; e.g. "FLGHGGN(WP9)LHFVQLAS"
627654
fullrefseq = list(util._get_codes(ref.sequence))
628-
entseq = self._check_seq_dif(entity, fullrefseq, align)
655+
656+
# Get mutated entity sequence, plus sets of insertions and deletions,
657+
# from SeqDif records
658+
entseq, insertions, deletions = self._check_seq_dif(
659+
entity, ref, fullrefseq, align)
629660

630661
def check_rng(rng, seq, rngstr, obj):
631662
if any(r < 1 or r > len(seq) for r in rng):
@@ -636,14 +667,13 @@ def check_rng(rng, seq, rngstr, obj):
636667
check_rng(entity_rng, entseq, "entity_begin,entity_end", entity)
637668
check_rng(db_rng, fullrefseq, "db_begin,db_end", ref)
638669

639-
matchlen = min(entity_rng[1] - entity_rng[0], db_rng[1] - db_rng[0])
640-
entseq = entseq[entity_rng[0] - 1:entity_rng[0] + matchlen]
641-
refseq = fullrefseq[db_rng[0] - 1:db_rng[0] + matchlen]
670+
entseq, refseq = self._get_gapped_alignment(
671+
entity_rng, db_rng, entseq, fullrefseq, insertions, deletions)
642672

643673
# Entity sequence is canonical so likely won't match any non-standard
644674
# residue (anything of length > 1), so just skip checks of these
645675
def matchseq(a, b):
646-
return a == b or len(a) > 1 or len(b) > 1
676+
return a == b or len(a) > 1 or len(b) > 1 or a == '-' or b == '-'
647677
if (len(refseq) != len(entseq)
648678
or not all(matchseq(a, b) for (a, b) in zip(refseq, entseq))):
649679
raise ValueError(

‎test/test_dumper.py‎

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -950,27 +950,105 @@ def test_struct_ref_seq_dif_mismatch(self):
950950
# Should work with checks disabled
951951
_ = _get_dumper_output(dumper, system, check=False)
952952

953+
def test_struct_ref_deletion_mismatch(self):
954+
"""Test StructRefDumper with deletion residue type mismatch"""
955+
system = ihm.System()
956+
lpep = ihm.LPeptideAlphabet()
957+
sd = ihm.reference.DeletionSeqDif(db_seq_id=6, db_monomer=lpep['W'])
958+
r = ihm.reference.UniProtSequence(
959+
db_code='NUP84_YEAST', accession='P52891', sequence='MELWPTYQT',
960+
details='test sequence')
961+
r.alignments.append(ihm.reference.Alignment(seq_dif=[sd]))
962+
system.entities.append(ihm.Entity('LSPT', references=[r]))
963+
dumper = ihm.dumper._EntityDumper()
964+
dumper.finalize(system) # Assign entity IDs
965+
966+
dumper = ihm.dumper._StructRefDumper()
967+
dumper.finalize(system) # Assign IDs
968+
with self.assertRaises(ValueError) as cm:
969+
_get_dumper_output(dumper, system)
970+
self.assertIn('one-letter code (W) does not match', str(cm.exception))
971+
self.assertIn('(T at position 6)', str(cm.exception))
972+
# Should work with checks disabled
973+
_ = _get_dumper_output(dumper, system, check=False)
974+
975+
def test_struct_ref_deletion_out_of_range(self):
976+
"""Test StructRefDumper with deletion out of range"""
977+
system = ihm.System()
978+
lpep = ihm.LPeptideAlphabet()
979+
sd = ihm.reference.DeletionSeqDif(db_seq_id=12, db_monomer=lpep['W'])
980+
r = ihm.reference.UniProtSequence(
981+
db_code='NUP84_YEAST', accession='P52891', sequence='MELWPTYQT',
982+
details='test sequence')
983+
r.alignments.append(ihm.reference.Alignment(seq_dif=[sd]))
984+
system.entities.append(ihm.Entity('LSPT', references=[r]))
985+
dumper = ihm.dumper._EntityDumper()
986+
dumper.finalize(system) # Assign entity IDs
987+
988+
dumper = ihm.dumper._StructRefDumper()
989+
dumper.finalize(system) # Assign IDs
990+
with self.assertRaises(IndexError) as cm:
991+
_get_dumper_output(dumper, system)
992+
self.assertIn('SeqDif.db_seq_id for ', str(cm.exception))
993+
self.assertIn('is 12, out of range 1-9', str(cm.exception))
994+
# Should work with checks disabled
995+
_ = _get_dumper_output(dumper, system, check=False)
996+
953997
def test_struct_ref_seq_dif_ins_del(self):
954998
"""Test StructRefDumper with SeqDif insertions and deletions"""
955999
system = ihm.System()
9561000
lpep = ihm.LPeptideAlphabet()
957-
sd1 = ihm.reference.SeqDif(seq_id=2, db_monomer=lpep['G'],
958-
monomer=None, details='deletion')
959-
sd2 = ihm.reference.SeqDif(seq_id=3, db_monomer=lpep['C'],
960-
monomer=None, details='insertion')
1001+
sd1 = ihm.reference.DeletionSeqDif(db_seq_id=7, db_monomer=lpep['Q'])
1002+
sd2 = ihm.reference.InsertionSeqDif(
1003+
seq_id=1, monomer=lpep['H'], details='expression tag')
9611004
r = ihm.reference.UniProtSequence(
9621005
db_code='NUP84_YEAST', accession='P52891', sequence='MEWPTYQT',
9631006
details='test sequence')
9641007
r.alignments.append(ihm.reference.Alignment(seq_dif=[sd1, sd2]))
965-
system.entities.append(ihm.Entity('MEWPTYQT', references=[r]))
1008+
system.entities.append(ihm.Entity('HMEWPTYT', references=[r]))
9661009
dumper = ihm.dumper._EntityDumper()
9671010
dumper.finalize(system) # Assign entity IDs
9681011

9691012
dumper = ihm.dumper._StructRefDumper()
9701013
dumper.finalize(system) # Assign IDs
9711014
# Insertions and deletions are not currently checked, so
9721015
# this should pass
973-
_ = _get_dumper_output(dumper, system)
1016+
out = _get_dumper_output(dumper, system)
1017+
self.assertEqual(out, """#
1018+
loop_
1019+
_struct_ref.id
1020+
_struct_ref.entity_id
1021+
_struct_ref.db_name
1022+
_struct_ref.db_code
1023+
_struct_ref.pdbx_db_accession
1024+
_struct_ref.pdbx_align_begin
1025+
_struct_ref.pdbx_seq_one_letter_code
1026+
_struct_ref.details
1027+
1 1 UNP NUP84_YEAST P52891 1 MEWPTYQT 'test sequence'
1028+
#
1029+
#
1030+
loop_
1031+
_struct_ref_seq.align_id
1032+
_struct_ref_seq.ref_id
1033+
_struct_ref_seq.seq_align_beg
1034+
_struct_ref_seq.seq_align_end
1035+
_struct_ref_seq.db_align_beg
1036+
_struct_ref_seq.db_align_end
1037+
1 1 1 8 1 8
1038+
#
1039+
#
1040+
loop_
1041+
_struct_ref_seq_dif.pdbx_ordinal
1042+
_struct_ref_seq_dif.align_id
1043+
_struct_ref_seq_dif.db_mon_id
1044+
_struct_ref_seq_dif.pdbx_seq_db_seq_num
1045+
_struct_ref_seq_dif.mon_id
1046+
_struct_ref_seq_dif.seq_num
1047+
_struct_ref_seq_dif.details
1048+
1 1 GLN 7 ? ? deletion
1049+
2 1 ? ? HIS 1 'expression tag'
1050+
#
1051+
""")
9741052

9751053
def test_chem_comp_dumper(self):
9761054
"""Test ChemCompDumper"""

0 commit comments

Comments
 (0)