diff --git a/modules/core/dependency/python-ihm/ChangeLog.rst b/modules/core/dependency/python-ihm/ChangeLog.rst index 54b71834d6..76534ecf64 100644 --- a/modules/core/dependency/python-ihm/ChangeLog.rst +++ b/modules/core/dependency/python-ihm/ChangeLog.rst @@ -1,3 +1,10 @@ +0.41 - 2023-10-02 +================= + - More complete support for oligosaccharides, in particular correct + numbering for atoms in `atom_site`, and the addition of some + data items to the output which are required for full + dictionary compliance. + 0.40 - 2023-09-25 ================= - Basic support for oligosaccharides is now provided. New classes are diff --git a/modules/core/dependency/python-ihm/MANIFEST.in b/modules/core/dependency/python-ihm/MANIFEST.in index 8e5bdeb63f..7f3c14dc2b 100644 --- a/modules/core/dependency/python-ihm/MANIFEST.in +++ b/modules/core/dependency/python-ihm/MANIFEST.in @@ -4,4 +4,4 @@ include examples/* include util/make-mmcif.py include src/ihm_format.h include src/ihm_format.i -include src/ihm_format_wrap_0.40.c +include src/ihm_format_wrap_0.41.c diff --git a/modules/core/dependency/python-ihm/ihm/__init__.py b/modules/core/dependency/python-ihm/ihm/__init__.py index fe22eaf439..7964659de7 100644 --- a/modules/core/dependency/python-ihm/ihm/__init__.py +++ b/modules/core/dependency/python-ihm/ihm/__init__.py @@ -20,7 +20,7 @@ import json from . import util -__version__ = '0.40' +__version__ = '0.41' class __UnknownValue(object): @@ -1341,7 +1341,7 @@ def __call__(self, seq_id_begin, seq_id_end): return EntityRange(self, seq_id_begin, seq_id_end) def __get_seq_id_range(self): - if self.is_polymeric(): + if self.is_polymeric() or self.is_branched(): return (1, len(self.sequence)) else: # Nonpolymers don't have the concept of seq_id diff --git a/modules/core/dependency/python-ihm/ihm/dictionary.py b/modules/core/dependency/python-ihm/ihm/dictionary.py index 071b764d66..296407cf43 100644 --- a/modules/core/dependency/python-ihm/ihm/dictionary.py +++ b/modules/core/dependency/python-ihm/ihm/dictionary.py @@ -143,7 +143,8 @@ def _check_linked_items(self): # the chem_comp_* categories don't need to be fully populated if cat in self.dictionary.categories \ and not cat.startswith('chem_comp_'): - missing = self._seen_ids[child] - self._seen_ids[parent] + missing = sorted(self._seen_ids[child] + - self._seen_ids[parent]) self.errors.append( "The following IDs referenced by %s " "were not defined in the parent category (%s): %s" diff --git a/modules/core/dependency/python-ihm/ihm/dumper.py b/modules/core/dependency/python-ihm/ihm/dumper.py index 1bc7803fac..20a833731f 100644 --- a/modules/core/dependency/python-ihm/ihm/dumper.py +++ b/modules/core/dependency/python-ihm/ihm/dumper.py @@ -597,7 +597,7 @@ def dump(self, system, writer): with writer.loop("_pdbx_entity_nonpoly", ["entity_id", "name", "comp_id"]) as lp: for entity in system.entities: - if entity.is_polymeric(): + if entity.is_polymeric() or entity.is_branched(): continue lp.write(entity_id=entity._id, name=entity.description, comp_id=entity.sequence[0].id) @@ -734,7 +734,7 @@ def dump(self, system, writer): with writer.loop("_pdbx_branch_scheme", ["asym_id", "entity_id", "mon_id", "num", "pdb_seq_num", "auth_seq_num", - "auth_mon_id", "pdb_asym_id"]) as lp: + "auth_mon_id", "pdb_mon_id", "pdb_asym_id"]) as lp: for asym in system.asym_units: entity = asym.entity if not entity.is_branched(): @@ -747,8 +747,8 @@ def dump(self, system, writer): num=num + 1, pdb_seq_num=auth_seq_num, auth_seq_num=auth_seq_num, - mon_id=comp.id, - auth_mon_id=comp.id) + mon_id=comp.id, auth_mon_id=comp.id, + pdb_mon_id=comp.id) class _BranchDescriptorDumper(Dumper): @@ -1460,6 +1460,9 @@ def __call__(self, obj): else: type_check = self._type_check_atom seq_id_range = (obj.seq_id, obj.seq_id) + # Allow seq_id to be either 1 or None for ligands + if obj.seq_id == 1 and asym.entity.type == 'non-polymer': + seq_id_range = (None, None) self._check_duplicate_atom(obj) self._check_assembly(obj, asym, seq_id_range) self._check_representation(obj, asym, type_check, seq_id_range) @@ -1591,7 +1594,7 @@ def dump_atoms(self, system, writer, add_ihm=True): "label_alt_id", "label_comp_id", "label_seq_id", "auth_seq_id", "pdbx_PDB_ins_code", "label_asym_id", "Cartn_x", "Cartn_y", "Cartn_z", "occupancy", "label_entity_id", "auth_asym_id", - "B_iso_or_equiv", "pdbx_PDB_model_num"] + "auth_comp_id", "B_iso_or_equiv", "pdbx_PDB_model_num"] if add_ihm: it.append("ihm_model_id") with writer.loop("_atom_site", it) as lp: @@ -1600,8 +1603,10 @@ def dump_atoms(self, system, writer, add_ihm=True): for atom in model.get_atoms(): rngcheck(atom) seq_id = 1 if atom.seq_id is None else atom.seq_id + label_seq_id = atom.seq_id + if not atom.asym_unit.entity.is_polymeric(): + label_seq_id = None comp = atom.asym_unit.sequence[seq_id - 1] - water = isinstance(atom.asym_unit, ihm.WaterAsymUnit) seen_types[atom.type_symbol] = None auth_seq_id, ins = \ atom.asym_unit._get_auth_seq_id_ins_code(seq_id) @@ -1612,8 +1617,8 @@ def dump_atoms(self, system, writer, add_ihm=True): label_comp_id=comp.id, label_asym_id=atom.asym_unit._id, label_entity_id=atom.asym_unit.entity._id, - label_seq_id=None if water else atom.seq_id, - auth_seq_id=auth_seq_id, + label_seq_id=label_seq_id, + auth_seq_id=auth_seq_id, auth_comp_id=comp.id, pdbx_PDB_ins_code=ins or ihm.unknown, auth_asym_id=atom.asym_unit.strand_id, Cartn_x=atom.x, Cartn_y=atom.y, Cartn_z=atom.z, diff --git a/modules/core/dependency/python-ihm/ihm/model.py b/modules/core/dependency/python-ihm/ihm/model.py index 91a60bdf89..f9f1fe0f34 100644 --- a/modules/core/dependency/python-ihm/ihm/model.py +++ b/modules/core/dependency/python-ihm/ihm/model.py @@ -44,7 +44,9 @@ class Atom(object): :type asym_unit: :class:`ihm.AsymUnit` :param int seq_id: The sequence ID of the residue represented by this atom. This should generally be a number starting at 1 for any - polymer chain or water, or None for a ligand. + polymer chain, water, or oligosaccharide. For ligands, a seq_id + is not needed (as a given asym can only contain a single ligand), + so either 1 or None can be used. :param str atom_id: The name of the atom in the residue :param str type_symbol: Element name :param float x: x coordinate of the atom diff --git a/modules/core/dependency/python-ihm/ihm/reader.py b/modules/core/dependency/python-ihm/ihm/reader.py index 7471aec521..942ec29b55 100644 --- a/modules/core/dependency/python-ihm/ihm/reader.py +++ b/modules/core/dependency/python-ihm/ihm/reader.py @@ -1994,8 +1994,9 @@ def __init__(self, *args): self._missing_sequence = collections.defaultdict(dict) self._seq_id_map = {} - def _get_water_seq_id(self, auth_seq_id, pdbx_pdb_ins_code, asym): - """Get an internal seq_id for a water, given author-provided info""" + def _get_seq_id_from_auth(self, auth_seq_id, pdbx_pdb_ins_code, asym): + """Get an internal seq_id for something not a polymer (nonpolymer, + water, branched), given author-provided info""" if asym._id not in self._seq_id_map: m = {} # Make reverse mapping from author-provided info to seq_id @@ -2028,23 +2029,24 @@ def __call__(self, pdbx_pdb_model_num, label_asym_id, b_iso_or_equiv, else: asym = self.sysr.asym_units.get_by_id(label_asym_id) auth_seq_id = self.get_int_or_string(auth_seq_id) - water = isinstance(asym, ihm.WaterAsymUnit) - if water: + if seq_id is None: # Fill in our internal seq_id if possible - seq_id = self._get_water_seq_id(auth_seq_id, pdbx_pdb_ins_code, - asym) + our_seq_id = self._get_seq_id_from_auth( + auth_seq_id, pdbx_pdb_ins_code, asym) + else: + our_seq_id = seq_id biso = self.get_float(b_iso_or_equiv) occupancy = self.get_float(occupancy) group = 'ATOM' if group_pdb is None else group_pdb a = ihm.model.Atom( - asym_unit=asym, seq_id=seq_id, atom_id=label_atom_id, + asym_unit=asym, seq_id=our_seq_id, atom_id=label_atom_id, type_symbol=type_symbol, x=float(cartn_x), y=float(cartn_y), z=float(cartn_z), het=group != 'ATOM', biso=biso, occupancy=occupancy) model.add_atom(a) # Note any residues that have different seq_id and auth_seq_id - if (auth_seq_id is not None and not water and + if (auth_seq_id is not None and seq_id is not None and (seq_id != auth_seq_id or pdbx_pdb_ins_code not in (None, ihm.unknown))): if asym.auth_seq_id_map == 0: diff --git a/modules/core/dependency/python-ihm/make-release.sh b/modules/core/dependency/python-ihm/make-release.sh index ff87031826..28f634a71f 100755 --- a/modules/core/dependency/python-ihm/make-release.sh +++ b/modules/core/dependency/python-ihm/make-release.sh @@ -3,13 +3,13 @@ # First, do # - Update AuditConformDumper to match latest IHM dictionary if necessary # - Run util/validate-outputs.py to make sure all example outputs validate +# (cd util; PYTHONPATH=.. python3 ./validate-outputs.py) # - Update ChangeLog.rst and util/python-ihm.spec with the release number # - Update release number in ihm/__init__.py, MANIFEST.in, and setup.py # - Commit, tag, and push # - Make release on GitHub # - Upload the release tarball from # https://github.com/ihmwg/python-ihm/releases to Zenodo as a new release -# - Build SRPM and upload to COPR # - Make sure there are no extraneous .py files (setup.py will include them # in the pypi package) @@ -23,4 +23,4 @@ python3 setup.py sdist rm -f "src/ihm_format_wrap_${VERSION}.c" echo "Now use 'twine upload dist/ihm-${VERSION}.tar.gz' to publish the release on PyPi." -echo "Then, update the conda-forge and Homebrew packages to match." +echo "Then, update the conda-forge, COPR, and Homebrew packages to match." diff --git a/modules/core/dependency/python-ihm/setup.py b/modules/core/dependency/python-ihm/setup.py index e154967c58..c3aa471153 100755 --- a/modules/core/dependency/python-ihm/setup.py +++ b/modules/core/dependency/python-ihm/setup.py @@ -7,7 +7,7 @@ import sys import os -VERSION = "0.40" +VERSION = "0.41" copy_args = sys.argv[1:] diff --git a/modules/core/dependency/python-ihm/test/test_dumper.py b/modules/core/dependency/python-ihm/test/test_dumper.py index b157dc06fa..5dcdd00452 100644 --- a/modules/core/dependency/python-ihm/test/test_dumper.py +++ b/modules/core/dependency/python-ihm/test/test_dumper.py @@ -805,7 +805,9 @@ def test_entity_nonpoly_dumper(self): # Non-polymeric entity e2 = ihm.Entity([ihm.NonPolymerChemComp('HEM')], description='heme') e3 = ihm.Entity([ihm.WaterChemComp()]) - system.entities.extend((e1, e2, e3)) + # Branched entity + e4 = ihm.Entity([ihm.SaccharideChemComp('NAG')]) + system.entities.extend((e1, e2, e3, e4)) ed = ihm.dumper._EntityDumper() ed.finalize(system) # Assign entity IDs @@ -2101,10 +2103,13 @@ def test_range_checker_repr_seq_id(self): x=1.0, y=2.0, z=3.0, radius=4.0) self.assertRaises(ValueError, rngcheck, sphere) - # Atom in a nonpolymer must have no seq_id + # Atom in a nonpolymer must have no seq_id (or seq_id==1) atom = ihm.model.Atom(asym_unit=asym_nonpol, seq_id=None, atom_id='C', type_symbol='C', x=1.0, y=2.0, z=3.0) rngcheck(atom) + atom = ihm.model.Atom(asym_unit=asym_nonpol, seq_id=1, atom_id='C', + type_symbol='C', x=1.0, y=2.0, z=3.0) + rngcheck(atom) atom = ihm.model.Atom(asym_unit=asym2, seq_id=None, atom_id='C', type_symbol='C', x=1.0, y=2.0, z=3.0) self.assertRaises(ValueError, rngcheck, atom) @@ -2328,12 +2333,13 @@ def test_model_dumper_atoms(self): _atom_site.occupancy _atom_site.label_entity_id _atom_site.auth_asym_id +_atom_site.auth_comp_id _atom_site.B_iso_or_equiv _atom_site.pdbx_PDB_model_num _atom_site.ihm_model_id -ATOM 1 C C . ALA 1 1 ? X 1.000 2.000 3.000 . 9 X . 1 1 -HETATM 2 C CA . ALA 1 1 ? X 10.000 20.000 30.000 . 9 X . 1 1 -ATOM 3 N N . CYS 2 2 ? X 4.000 5.000 6.000 0.200 9 X 42.000 1 1 +ATOM 1 C C . ALA 1 1 ? X 1.000 2.000 3.000 . 9 X ALA . 1 1 +HETATM 2 C CA . ALA 1 1 ? X 10.000 20.000 30.000 . 9 X ALA . 1 1 +ATOM 3 N N . CYS 2 2 ? X 4.000 5.000 6.000 0.200 9 X CYS 42.000 1 1 # # loop_ @@ -2352,19 +2358,19 @@ def test_model_dumper_atoms(self): asym.auth_seq_id_map = -1 out = _get_dumper_output(dumper, system) self.assertEqual( - out.split('\n')[43:46:2], - ["ATOM 1 C C . ALA 1 0 ? X 1.000 2.000 3.000 . 9 X . 1 1", + out.split('\n')[44:47:2], + ["ATOM 1 C C . ALA 1 0 ? X 1.000 2.000 3.000 . 9 X ALA . 1 1", "ATOM 3 N N . CYS 2 1 ? X 4.000 5.000 6.000 " - "0.200 9 X 42.000 1 1"]) + "0.200 9 X CYS 42.000 1 1"]) # With auth_seq_id map asym.auth_seq_id_map = {1: 42, 2: 99} out = _get_dumper_output(dumper, system) self.assertEqual( - out.split('\n')[43:46:2], - ["ATOM 1 C C . ALA 1 42 ? X 1.000 2.000 3.000 . 9 X . 1 1", + out.split('\n')[44:47:2], + ["ATOM 1 C C . ALA 1 42 ? X 1.000 2.000 3.000 . 9 X ALA . 1 1", "ATOM 3 N N . CYS 2 99 ? X 4.000 5.000 6.000 " - "0.200 9 X 42.000 1 1"]) + "0.200 9 X CYS 42.000 1 1"]) def test_model_dumper_water_atoms(self): """Test ModelDumper with water atoms""" @@ -2394,10 +2400,10 @@ def test_model_dumper_water_atoms(self): out = _get_dumper_output(dumper, system) self.assertEqual( - out.split('\n')[43:46], - ['HETATM 1 O O . HOH . 42 ? X 1.000 2.000 3.000 . 9 X . 1 1', - 'HETATM 2 O O . HOH . 99 ? X 4.000 5.000 6.000 . 9 X . 1 1', - 'HETATM 3 O O . HOH . 3 ? X 7.000 8.000 9.000 . 9 X . 1 1']) + out.split('\n')[44:47], + ['HETATM 1 O O . HOH . 42 ? X 1.000 2.000 3.000 . 9 X HOH . 1 1', + 'HETATM 2 O O . HOH . 99 ? X 4.000 5.000 6.000 . 9 X HOH . 1 1', + 'HETATM 3 O O . HOH . 3 ? X 7.000 8.000 9.000 . 9 X HOH . 1 1']) def test_ensemble_dumper(self): """Test EnsembleDumper""" @@ -4515,9 +4521,10 @@ def test_branch_scheme_dumper(self): _pdbx_branch_scheme.pdb_seq_num _pdbx_branch_scheme.auth_seq_num _pdbx_branch_scheme.auth_mon_id +_pdbx_branch_scheme.pdb_mon_id _pdbx_branch_scheme.pdb_asym_id -A 1 NAG 1 1 1 NAG A -B 2 FUC 1 6 6 FUC B +A 1 NAG 1 1 1 NAG NAG A +B 2 FUC 1 6 6 FUC FUC B # """) diff --git a/modules/core/dependency/python-ihm/test/test_examples.py b/modules/core/dependency/python-ihm/test/test_examples.py index 7af1084275..c0a78e94d2 100644 --- a/modules/core/dependency/python-ihm/test/test_examples.py +++ b/modules/core/dependency/python-ihm/test/test_examples.py @@ -41,7 +41,7 @@ def test_simple_docking_example(self): # can read it with open(os.path.join(tmpdir, 'output.cif')) as fh: contents = fh.readlines() - self.assertEqual(len(contents), 317) + self.assertEqual(len(contents), 318) with open(os.path.join(tmpdir, 'output.cif')) as fh: s, = ihm.reader.read(fh) @@ -70,7 +70,7 @@ def test_ligands_water_example(self): # can read it with open(out) as fh: contents = fh.readlines() - self.assertEqual(len(contents), 253) + self.assertEqual(len(contents), 254) with open(out) as fh: s, = ihm.reader.read(fh) # Make sure that resulting Python objects are picklable diff --git a/modules/core/dependency/python-ihm/test/test_main.py b/modules/core/dependency/python-ihm/test/test_main.py index e0430f5907..1d861da003 100644 --- a/modules/core/dependency/python-ihm/test/test_main.py +++ b/modules/core/dependency/python-ihm/test/test_main.py @@ -223,9 +223,10 @@ def test_entity(self): self.assertNotEqual(e1, e3) self.assertEqual(e1.seq_id_range, (1, 4)) self.assertEqual(e3.seq_id_range, (1, 5)) - # seq_id does not exist for nonpolymers or branched entities + # seq_id does not exist for nonpolymers self.assertEqual(heme.seq_id_range, (None, None)) - self.assertEqual(sugar.seq_id_range, (None, None)) + # We do have an internal seq_id_range for branched entities + self.assertEqual(sugar.seq_id_range, (1, 1)) def test_entity_weight(self): """Test Entity.formula_weight""" @@ -466,9 +467,10 @@ def test_asym_range(self): asugar = ihm.AsymUnit(sugar) a._id = 42 self.assertEqual(a.seq_id_range, (1, 6)) - # seq_id is not defined for nonpolymers or branched entities + # seq_id is not defined for nonpolymers self.assertEqual(aheme.seq_id_range, (None, None)) - self.assertEqual(asugar.seq_id_range, (None, None)) + # We use seq_id internally for branched entities + self.assertEqual(asugar.seq_id_range, (1, 1)) r = a(3, 4) self.assertEqual(r.seq_id_range, (3, 4)) self.assertEqual(r._id, 42) diff --git a/modules/core/dependency/python-ihm/util/python-ihm.spec b/modules/core/dependency/python-ihm/util/python-ihm.spec index d9f6c8bbf5..1da3df9512 100644 --- a/modules/core/dependency/python-ihm/util/python-ihm.spec +++ b/modules/core/dependency/python-ihm/util/python-ihm.spec @@ -1,7 +1,7 @@ Name: python3-ihm License: MIT Group: Applications/Engineering -Version: 0.40 +Version: 0.41 Release: 1%{?dist} Summary: Package for handling IHM mmCIF and BinaryCIF files Packager: Ben Webb @@ -36,6 +36,9 @@ sed -i -e "s/install_requires=\['msgpack'\]/#/" setup.py %defattr(-,root,root) %changelog +* Mon Oct 02 2023 Ben Webb 0.41-1 +- Update to latest upstream. + * Mon Sep 25 2023 Ben Webb 0.40-1 - Update to latest upstream.