Skip to content

Commit

Permalink
Squashed 'modules/core/dependency/python-ihm/' changes from 56116f58b…
Browse files Browse the repository at this point in the history
…1..db438ff7e7

db438ff7e7 Fix test of dump-time check
385893312b Note that reference sequences may be numbered differently
f667231ccb Check residue ranges at creation time
d84a0c1885 Explicitly check residue ranges at dump time

git-subtree-dir: modules/core/dependency/python-ihm
git-subtree-split: db438ff7e7bf185cb011c83dc85262f8b77f4235
  • Loading branch information
benmwebb committed Sep 5, 2024
1 parent 77d0126 commit f7af679
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 11 deletions.
6 changes: 6 additions & 0 deletions modules/core/dependency/python-ihm/docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ of the data used in modeling:
line up with the internal numbering. In this case an offset from starting
model numbering to internal numbering can be provided - see the ``offset``
parameter to :class:`~ihm.startmodel.StartingModel`.
- *Reference sequence numbering*. The modeled sequence may differ from that
in a database such as UniProt, which is itself numbered sequentially from 1
(for example, the modeled sequence may be a subset of the UniProt sequence,
such that the first modeled residue is not the first residue in UniProt).
The correspondence between the internal and reference sequences is given
with :class:`ihm.reference.Alignment` objects.

Output
======
Expand Down
18 changes: 10 additions & 8 deletions modules/core/dependency/python-ihm/ihm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1235,12 +1235,13 @@ class EntityRange(object):
entity = ihm.Entity(sequence=...)
rng = entity(4,7)
"""
def __init__(self, entity, seq_id_begin, seq_id_end):
def __init__(self, entity, seq_id_begin, seq_id_end, _check=True):
if not entity.is_polymeric():
raise TypeError("Can only create ranges for polymeric entities")
self.entity = entity
# todo: check range for validity (at property read time)
self.seq_id_range = (seq_id_begin, seq_id_end)
if _check:
util._check_residue_range(self)

def __eq__(self, other):
try:
Expand Down Expand Up @@ -1467,8 +1468,8 @@ def __hash__(self):
else:
return hash(self.sequence)

def __call__(self, seq_id_begin, seq_id_end):
return EntityRange(self, seq_id_begin, seq_id_end)
def __call__(self, seq_id_begin, seq_id_end, _check=True):
return EntityRange(self, seq_id_begin, seq_id_end, _check=_check)

def __get_seq_id_range(self):
if self.is_polymeric() or self.is_branched():
Expand All @@ -1487,12 +1488,13 @@ class AsymUnitRange(object):
asym = ihm.AsymUnit(entity)
rng = asym(4,7)
"""
def __init__(self, asym, seq_id_begin, seq_id_end):
def __init__(self, asym, seq_id_begin, seq_id_end, _check=True):
if asym.entity is not None and not asym.entity.is_polymeric():
raise TypeError("Can only create ranges for polymeric entities")
self.asym = asym
# todo: check range for validity (at property read time)
self.seq_id_range = (seq_id_begin, seq_id_end)
if _check:
util._check_residue_range(self)

def __eq__(self, other):
try:
Expand Down Expand Up @@ -1613,8 +1615,8 @@ def _get_pdb_auth_seq_id_ins_code(self, seq_id):
auth_seq_num = self.orig_auth_seq_id_map.get(seq_id, pdb_seq_num)
return pdb_seq_num, auth_seq_num, ins_code

def __call__(self, seq_id_begin, seq_id_end):
return AsymUnitRange(self, seq_id_begin, seq_id_end)
def __call__(self, seq_id_begin, seq_id_end, _check=True):
return AsymUnitRange(self, seq_id_begin, seq_id_end, _check=_check)

def residue(self, seq_id):
"""Get a :class:`Residue` at the given sequence position"""
Expand Down
6 changes: 5 additions & 1 deletion modules/core/dependency/python-ihm/ihm/dumper.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,11 @@ def dump(self, system, writer):
["id", "entity_id", "seq_id_begin", "seq_id_end",
"comp_id_begin", "comp_id_end"]) as lp:
for rng in self._ranges_by_id:
entity = rng.entity if hasattr(rng, 'entity') else rng
if hasattr(rng, 'entity'):
entity = rng.entity
util._check_residue_range(rng)
else:
entity = rng
lp.write(
id=rng._range_id, entity_id=entity._id,
seq_id_begin=rng.seq_id_range[0],
Expand Down
6 changes: 4 additions & 2 deletions modules/core/dependency/python-ihm/ihm/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ def get(self, asym_or_entity, range_id):
if range_id is None:
return asym_or_entity
else:
return asym_or_entity(*self._id_map[range_id])
# Allow reading out-of-range ranges
return asym_or_entity(*self._id_map[range_id], _check=False)


class _AnalysisIDMapper(IDMapper):
Expand Down Expand Up @@ -2239,7 +2240,8 @@ def __call__(self, feature_id, entity_id, asym_id, seq_id_begin,
asym_or_entity = self._get_asym_or_entity(asym_id, entity_id)
r1 = int(seq_id_begin)
r2 = int(seq_id_end)
f.ranges.append(asym_or_entity(r1, r2))
# allow out-of-range ranges
f.ranges.append(asym_or_entity(r1, r2, _check=False))


class _FeatureListHandler(Handler):
Expand Down
12 changes: 12 additions & 0 deletions modules/core/dependency/python-ihm/ihm/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,15 @@ def setfunc(obj, val):
setattr(obj, "_" + attr, val)

return property(getfunc, setfunc, doc=doc)


def _check_residue_range(rng):
"""Make sure that a residue range is not out of range of its Entity"""
if rng.seq_id_range[1] < rng.seq_id_range[0]:
raise ValueError("Range %d-%d is invalid; end is before start"
% rng.seq_id_range)
if (rng.seq_id_range[1] > len(rng.entity.sequence)
or rng.seq_id_range[0] < 1):
raise IndexError("Range %d-%d out of range for %s (1-%d)"
% (rng.seq_id_range[0], rng.seq_id_range[1],
rng.entity, len(rng.entity.sequence)))
20 changes: 20 additions & 0 deletions modules/core/dependency/python-ihm/test/test_dumper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2663,6 +2663,26 @@ def test_entity_poly_segment_dumper(self):
#
""")

def test_entity_poly_segment_dumper_bad_range(self):
"""Test EntityPolySegmentDumper with bad residue ranges"""
for badrng, exc in [((10, 14), IndexError),
((-4, 1), IndexError),
((3, 1), ValueError)]:
system = ihm.System()
e1 = ihm.Entity('AHCD')
system.entities.append(e1)
# Disable construction-time check so that we
# can see dump time check
system.orphan_features.append(
ihm.restraint.ResidueFeature([e1(*badrng, _check=False)]))

dumper = ihm.dumper._EntityDumper()
dumper.finalize(system) # assign IDs
dumper = ihm.dumper._EntityPolySegmentDumper()
dumper.finalize(system) # assign IDs

self.assertRaises(exc, _get_dumper_output, dumper, system)

def test_single_state(self):
"""Test MultiStateDumper with a single state"""
system = ihm.System()
Expand Down
10 changes: 10 additions & 0 deletions modules/core/dependency/python-ihm/test/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,11 @@ def test_entity_range(self):
self.assertEqual(hash(r), hash(samer))
self.assertNotEqual(r, otherr)
self.assertNotEqual(r, e) # entity_range != entity
# Cannot create reversed range
self.assertRaises(ValueError, e.__call__, 3, 1)
# Cannot create out-of-range range
self.assertRaises(IndexError, e.__call__, -3, 1)
self.assertRaises(IndexError, e.__call__, 1, 10)

def test_asym_range(self):
"""Test AsymUnitRange class"""
Expand Down Expand Up @@ -518,6 +523,11 @@ def test_asym_range(self):
self.assertNotEqual(r, a) # asym_range != asym
self.assertNotEqual(r, e(3, 4)) # asym_range != entity_range
self.assertNotEqual(r, e) # asym_range != entity
# Cannot create reversed range
self.assertRaises(ValueError, a.__call__, 3, 1)
# Cannot create out-of-range range
self.assertRaises(IndexError, a.__call__, -3, 1)
self.assertRaises(IndexError, a.__call__, 1, 10)

def test_asym_segment(self):
"""Test AsymUnitSegment class"""
Expand Down

0 comments on commit f7af679

Please sign in to comment.