IMP logo
IMP Reference Guide  develop.0d0d3c41d6,2026/09/13
The Integrative Modeling Platform
mmcif.py
1 """@namespace IMP.pmi.mmcif
2  @brief Support for the mmCIF file format.
3 
4  IMP has basic support for writing out files in mmCIF format, for
5  deposition in [PDB-IHM](https://pdb-ihm.org/).
6  mmCIF files are currently generated by creating an
7  IMP.pmi.mmcif.ProtocolOutput class, and attaching it to an
8  IMP.pmi.representation.Representation object, after which any
9  generated models and metadata are collected and output as mmCIF.
10 """
11 
12 import copy
13 import RMF
14 import IMP.core
15 import IMP.algebra
16 import IMP.atom
17 import IMP.em
18 import IMP.isd
19 import IMP.pmi.tools
20 import IMP.pmi.alphabets
21 from IMP.pmi.tools import OrderedDict
22 import IMP.pmi.output
23 import IMP.mmcif.data
24 import re
25 import ast
26 import sys
27 import os
28 import time
29 import weakref
30 import ihm.location
31 import ihm.dataset
32 import ihm.dumper
33 import ihm.reader
34 import ihm.metadata
35 import ihm.startmodel
36 import ihm.model
37 import ihm.protocol
38 import ihm.analysis
39 import ihm.representation
40 import ihm.geometry
41 import ihm.cross_linkers
42 import ihm.reference
43 
44 
45 def _assign_id(obj, seen_objs, obj_by_id):
46  """Assign a unique ID to obj, and track all ids in obj_by_id."""
47  if obj not in seen_objs:
48  if not hasattr(obj, 'id'):
49  obj_by_id.append(obj)
50  obj.id = len(obj_by_id)
51  seen_objs[obj] = obj.id
52  else:
53  obj.id = seen_objs[obj]
54 
55 
56 def _get_by_residue(p):
57  """Determine whether the given particle represents a specific residue
58  or a more coarse-grained object."""
60 
61 
62 class _ComponentMapper:
63  """Map a Particle to a component name"""
64  def __init__(self, prot):
65  self.o = IMP.pmi.output.Output()
66  self.prot = prot
67  self.name = 'cif-output'
68  self.o.dictionary_pdbs[self.name] = self.prot
69  self.o._init_dictchain(self.name, self.prot,
70  multichar_chain=True, mmcif=True)
71 
72  def __getitem__(self, p):
73  protname, is_a_bead = self.o.get_prot_name_from_particle(self.name, p)
74  return protname
75 
76 
77 class _AsymMapper:
78  """Map a Particle to an asym_unit"""
79  def __init__(self, simo, prot):
80  self.simo = simo
81  self._cm = _ComponentMapper(prot)
82  self._seen_ranges = {}
83 
84  def __getitem__(self, p):
85  protname = self._cm[p]
86  return self.simo.asym_units[protname]
87 
88  def get_feature(self, ps):
89  """Get an ihm.restraint.Feature that covers the given particles"""
90  # todo: handle things other than residues
91  rngs = []
92  for p in ps:
93  asym = self[p]
94  # todo: handle overlapping ranges
96  rind = IMP.atom.Residue(p).get_index()
97  rng = asym(rind, rind)
99  # PMI Fragments always contain contiguous residues
100  rinds = IMP.atom.Fragment(p).get_residue_indexes()
101  rng = asym(rinds[0], rinds[-1])
102  else:
103  raise ValueError("Unsupported particle type %s" % str(p))
104  # Join contiguous ranges
105  if len(rngs) > 0 and rngs[-1].asym == asym \
106  and rngs[-1].seq_id_range[1] == rng.seq_id_range[0] - 1:
107  rngs[-1].seq_id_range = (rngs[-1].seq_id_range[0],
108  rng.seq_id_range[1])
109  else:
110  rngs.append(rng)
111  # If an identical feature already exists, return that
112  # todo: python-ihm should handle this automatically for us
113  hrngs = tuple(rngs)
114  if hrngs in self._seen_ranges:
115  return self._seen_ranges[hrngs]
116  else:
117  feat = ihm.restraint.ResidueFeature(rngs)
118  self._seen_ranges[hrngs] = feat
119  return feat
120 
121 
122 class _AllSoftware:
123  def __init__(self, system):
124  self.system = system
125  self.modeller_used = self.phyre2_used = False
126  self.pmi = ihm.Software(
127  name="IMP PMI module",
128  version=IMP.pmi.__version__,
129  classification="integrative model building",
130  description="integrative model building",
131  location='https://integrativemodeling.org')
132  self.imp = ihm.Software(
133  name="Integrative Modeling Platform (IMP)",
134  version=IMP.__version__,
135  classification="integrative model building",
136  description="integrative model building",
137  location='https://integrativemodeling.org')
138  # Only recent versions of python-ihm support adding citations for
139  # software
140  if hasattr(self.imp, 'citation'):
141  javi = 'Vel\u00e1zquez-Muriel J'
142  self.imp.citation = ihm.Citation(
143  pmid='22272186',
144  title='Putting the pieces together: integrative modeling '
145  'platform software for structure determination of '
146  'macromolecular assemblies',
147  journal='PLoS Biol', volume=10, page_range='e1001244',
148  year=2012,
149  authors=['Russel D', 'Lasker K', 'Webb B', javi, 'Tjioe E',
150  'Schneidman-Duhovny D', 'Peterson B', 'Sali A'],
151  doi='10.1371/journal.pbio.1001244')
152  self.pmi.citation = ihm.Citation(
153  pmid='31396911',
154  title='Modeling Biological Complexes Using Integrative '
155  'Modeling Platform.',
156  journal='Methods Mol Biol', volume=2022, page_range=(353, 377),
157  year=2019,
158  authors=['Saltzberg D', 'Greenberg CH', 'Viswanath S',
159  'Chemmama I', 'Webb B', 'Pellarin R', 'Echeverria I',
160  'Sali A'],
161  doi='10.1007/978-1-4939-9608-7_15')
162  self.system.software.extend([self.pmi, self.imp])
163 
164  def set_modeller_used(self, version, date):
165  if self.modeller_used:
166  return
167  self.modeller_used = True
168  s = ihm.Software(
169  name='MODELLER', classification='comparative modeling',
170  description='Comparative modeling by satisfaction '
171  'of spatial restraints, build ' + date,
172  location='https://salilab.org/modeller/', version=version)
173  self.system.software.append(s)
174  if hasattr(s, 'citation'):
175  s.citation = ihm.Citation(
176  pmid='8254673',
177  title='Comparative protein modelling by satisfaction of '
178  'spatial restraints.',
179  journal='J Mol Biol', volume=234, page_range=(779, 815),
180  year=1993, authors=['Sali A', 'Blundell TL'],
181  doi='10.1006/jmbi.1993.1626')
182 
183  def set_phyre2_used(self):
184  if self.phyre2_used:
185  return
186  self.phyre2_used = True
187  s = ihm.Software(
188  name='Phyre2', classification='protein homology modeling',
189  description='Protein Homology/analogY Recognition Engine V 2.0',
190  version='2.0', location='http://www.sbg.bio.ic.ac.uk/~phyre2/')
191  if hasattr(s, 'citation'):
192  s.citation = ihm.Citation(
193  pmid='25950237',
194  title='The Phyre2 web portal for protein modeling, '
195  'prediction and analysis.',
196  journal='Nat Protoc', volume=10, page_range=(845, 858),
197  authors=['Kelley LA', 'Mezulis S', 'Yates CM', 'Wass MN',
198  'Sternberg MJ'],
199  year=2015, doi='10.1038/nprot.2015.053')
200  self.system.software.append(s)
201 
202 
203 def _get_fragment_is_rigid(fragment):
204  """Determine whether a fragment is modeled rigidly"""
205  leaves = IMP.atom.get_leaves(fragment.hier)
206  # Assume all leaves are set up as rigid/flexible in the same way
207  return IMP.core.RigidMember.get_is_setup(leaves[0])
208 
209 
210 class _PDBFragment(ihm.representation.ResidueSegment):
211  """Record details about part of a PDB file used as input
212  for a component."""
213  def __init__(self, state, component, start, end, pdb_offset,
214  pdbname, chain, hier, asym_unit):
215  # start, end are PMI residue indexes (not IHM)
216  super().__init__(
217  asym_unit=asym_unit.pmi_range(start, end),
218  rigid=None, primitive='sphere')
219  self.component, self.start, self.end, self.offset, self.pdbname \
220  = component, start, end, pdb_offset, pdbname
221  self.state, self.chain, self.hier = state, chain, hier
223  & IMP.atom.ChainPDBSelector([chain])
224  # Read file in mmCIF or BinaryCIF format if requested
225  if pdbname.endswith('.cif'):
226  read_file = IMP.atom.read_mmcif
227  elif pdbname.endswith('.bcif'):
228  read_file = IMP.atom.read_bcif
229  else:
230  read_file = IMP.atom.read_pdb
231  self.starting_hier = read_file(pdbname, state.model, sel)
232 
233  rigid = property(lambda self: _get_fragment_is_rigid(self),
234  lambda self, val: None)
235 
236  def combine(self, other):
237  pass
238 
239 
240 class _BeadsFragment(ihm.representation.FeatureSegment):
241  """Record details about beads used to represent part of a component."""
242  chain = None
243 
244  def __init__(self, state, component, start, end, count, hier, asym_unit):
245  super().__init__(
246  asym_unit=asym_unit(start, end), rigid=None, primitive='sphere',
247  count=count)
248  self.state, self.component, self.hier = state, component, hier
249 
250  rigid = property(lambda self: _get_fragment_is_rigid(self),
251  lambda self, val: None)
252 
253  def combine(self, other):
254  # todo: don't combine if one fragment is rigid and the other flexible
255  if (type(other) == type(self) and # noqa: E721
256  other.asym_unit.seq_id_range[0]
257  == self.asym_unit.seq_id_range[1] + 1):
258  self.asym_unit.seq_id_range = (self.asym_unit.seq_id_range[0],
259  other.asym_unit.seq_id_range[1])
260  self.count += other.count
261  return True
262 
263 
264 class _AllModelRepresentations:
265  def __init__(self, simo):
266  self.simo = simo
267  # dict of fragments, ordered by representation, then component name,
268  # then state
269  self.fragments = OrderedDict()
270  self._all_representations = {}
271 
272  def copy_component(self, state, name, original, asym_unit):
273  """Copy all representation for `original` in `state` to `name`"""
274  def copy_frag(f):
275  newf = copy.copy(f)
276  newf.asym_unit = asym_unit(*f.asym_unit.seq_id_range)
277  return newf
278  for rep in self.fragments:
279  if original in self.fragments[rep]:
280  if name not in self.fragments[rep]:
281  self.fragments[rep][name] = OrderedDict()
282  self.fragments[rep][name][state] = [
283  copy_frag(f) for f in self.fragments[rep][original][state]]
284  # Assume representation for a component is the same in all
285  # states, so only write out the first one
286  first_state = list(self.fragments[rep][name].keys())[0]
287  if state is first_state:
288  representation = self._all_representations[rep]
289  representation.extend(self.fragments[rep][name][state])
290 
291  def add_fragment(self, state, representation, fragment):
292  """Add a model fragment."""
293  comp = fragment.component
294  id_rep = id(representation)
295  self._all_representations[id_rep] = representation
296  if id_rep not in self.fragments:
297  self.fragments[id_rep] = OrderedDict()
298  if comp not in self.fragments[id_rep]:
299  self.fragments[id_rep][comp] = OrderedDict()
300  if state not in self.fragments[id_rep][comp]:
301  self.fragments[id_rep][comp][state] = []
302  fragments = self.fragments[id_rep][comp][state]
303  if len(fragments) == 0 or not fragments[-1].combine(fragment):
304  fragments.append(fragment)
305  # Assume representation for a component is the same in all states,
306  # so only write out the first one
307  first_state = list(self.fragments[id_rep][comp].keys())[0]
308  if state is first_state:
309  representation.append(fragment)
310 
311 
312 class _AllDatasets:
313  """Track all datasets generated by PMI and add them to the ihm.System"""
314  def __init__(self, system):
315  self.system = system
316  self._datasets = []
317  self._datasets_by_state = {}
318  self._restraints_by_state = {}
319 
320  def get_all_group(self, state):
321  """Get a DatasetGroup encompassing all datasets so far in this state"""
322  # Note that if a restraint dataset is replaced after this is called,
323  # the group will still contain the old dataset - mark dataset as read
324  # only?
325  g = ihm.dataset.DatasetGroup(
326  self._datasets_by_state.get(state, [])
327  + [r.dataset for r in self._restraints_by_state.get(state, [])
328  if r.dataset])
329  return g
330 
331  def add(self, state, dataset):
332  """Add a new dataset."""
333  self._datasets.append(dataset)
334  if state not in self._datasets_by_state:
335  self._datasets_by_state[state] = []
336  self._datasets_by_state[state].append(dataset)
337  # Make sure that the dataset ends up in the mmCIF file even if nothing
338  # refers to it
339  self.system.orphan_datasets.append(dataset)
340  return dataset
341 
342  def add_restraint(self, state, restraint):
343  """Add the dataset for a restraint"""
344  if state not in self._restraints_by_state:
345  self._restraints_by_state[state] = []
346  self._restraints_by_state[state].append(restraint)
347 
348 
349 class _CrossLinkRestraint(ihm.restraint.CrossLinkRestraint):
350  """Restrain to a set of cross-links"""
351 
352  assembly = None
353  _label_map = {'wtDSS': 'DSS', 'scDSS': 'DSS', 'scEDC': 'EDC'}
354  _descriptor_map = {'DSS': ihm.cross_linkers.dss,
355  'EDC': ihm.cross_linkers.edc}
356 
357  def __init__(self, pmi_restraint):
358  self.pmi_restraint = pmi_restraint
359  # Use ChemDescriptor from PMI restraint if available, otherwise guess
360  # it using the label
361  linker = getattr(self.pmi_restraint, 'linker', None)
362  label = self.pmi_restraint.label
363  self.label = label
364  super().__init__(
365  dataset=self.pmi_restraint.dataset,
366  linker=linker or self._get_chem_descriptor(label))
367 
368  @classmethod
369  def _get_chem_descriptor(cls, label):
370  # Map commonly-used subtypes to more standard labels
371  label = cls._label_map.get(label, label)
372  if label not in cls._descriptor_map:
373  # If label is not a standard linker type, make a new chemical
374  # descriptor containing just the name. We don't know the chemistry
375  # so cannot specify a SMILES or INCHI string for it at this point
376  d = ihm.ChemDescriptor(label)
377  cls._descriptor_map[label] = d
378  return cls._descriptor_map[label]
379 
380  def _set_psi_sigma(self, model):
381  old_values = []
382  # Do nothing if given a different state than the restraint applies to
383  if model.m != self.pmi_restraint.model:
384  return
385  for resolution in self.pmi_restraint.sigma_dictionary:
386  statname = 'ISDCrossLinkMS_Sigma_%s_%s' % (resolution, self.label)
387  if model.stats and statname in model.stats:
388  sigma = float(model.stats[statname])
389  p = self.pmi_restraint.sigma_dictionary[resolution][0]
390  old_values.append((p, p.get_scale()))
391  p.set_scale(sigma)
392  for psiindex in self.pmi_restraint.psi_dictionary:
393  statname = 'ISDCrossLinkMS_Psi_%s_%s' % (psiindex, self.label)
394  if model.stats and statname in model.stats:
395  psi = float(model.stats[statname])
396  p = self.pmi_restraint.psi_dictionary[psiindex][0]
397  old_values.append((p, p.get_scale()))
398  p.set_scale(psi)
399  # If the same particle is set multiple times, make sure we get the
400  # original value last
401  return list(reversed(old_values))
402 
403  def add_fits_from_model_statfile(self, model):
404  # Set psi/sigma for all particles
405  old_values = self._set_psi_sigma(model)
406  # If no stats were collected, we can't show the fit
407  if not old_values:
408  return
409  for xl in self.cross_links:
410  # Get current psi/sigma particle value for each XL
411  xl.fits[model] = ihm.restraint.CrossLinkFit(
412  psi=xl.psi, sigma1=xl.sigma1, sigma2=xl.sigma2)
413  # Restore original psi/sigma
414  for p, scale in old_values:
415  p.set_scale(scale)
416 
417  # Have our dataset point to that in the original PMI restraint
418  def __set_dataset(self, val):
419  self.pmi_restraint.dataset = val
420  dataset = property(lambda self: self.pmi_restraint.dataset,
421  __set_dataset)
422 
423 
424 def get_asym_mapper_for_state(simo, state, asym_map):
425  asym = asym_map.get(state, None)
426  if asym is None:
427  asym = _AsymMapper(simo, state.prot)
428  asym_map[state] = asym
429  return asym
430 
431 
432 class _PMICrossLink:
433  # Query PMI particles to get psi and sigma values at time of use, not
434  # time of construction (since they may be set after creating the restraint)
435  psi = property(lambda self: self.psi_p.get_scale(),
436  lambda self, val: None)
437  sigma1 = property(lambda self: self.sigma1_p.get_scale(),
438  lambda self, val: None)
439  sigma2 = property(lambda self: self.sigma2_p.get_scale(),
440  lambda self, val: None)
441 
442 
443 class _ResidueCrossLink(ihm.restraint.ResidueCrossLink, _PMICrossLink):
444  pass
445 
446 
447 class _FeatureCrossLink(ihm.restraint.FeatureCrossLink, _PMICrossLink):
448  pass
449 
450 
451 class _EM2DRestraint(ihm.restraint.EM2DRestraint):
452  def __init__(self, state, pmi_restraint, image_number, resolution,
453  pixel_size, image_resolution, projection_number,
454  micrographs_number):
455  self.pmi_restraint, self.image_number = pmi_restraint, image_number
456  super().__init__(
457  dataset=pmi_restraint.datasets[image_number],
458  assembly=state.modeled_assembly,
459  segment=False, number_raw_micrographs=micrographs_number,
460  pixel_size_width=pixel_size, pixel_size_height=pixel_size,
461  image_resolution=image_resolution,
462  number_of_projections=projection_number)
463 
464  # Have our dataset point to that in the original PMI restraint
465  def __get_dataset(self):
466  return self.pmi_restraint.datasets[self.image_number]
467 
468  def __set_dataset(self, val):
469  self.pmi_restraint.datasets[self.image_number] = val
470 
471  dataset = property(__get_dataset, __set_dataset)
472 
473  def add_fits_from_model_statfile(self, model):
474  ccc = self._get_cross_correlation(model)
475  transform = self._get_transformation(model)
476  rot = transform.get_rotation()
477  rm = [[e for e in rot.get_rotation_matrix_row(i)] for i in range(3)]
478  self.fits[model] = ihm.restraint.EM2DRestraintFit(
479  cross_correlation_coefficient=ccc,
480  rot_matrix=rm,
481  tr_vector=transform.get_translation())
482 
483  def _get_transformation(self, model):
484  """Get the transformation that places the model on the image"""
485  stats = model.em2d_stats or model.stats
486  prefix = 'ElectronMicroscopy2D_%s_Image%d' % (self.pmi_restraint.label,
487  self.image_number + 1)
488  r = [float(stats[prefix + '_Rotation%d' % i]) for i in range(4)]
489  t = [float(stats[prefix + '_Translation%d' % i])
490  for i in range(3)]
491  # If the model coordinates are transformed, need to back transform
492  # them first
493  inv = model.transform.get_inverse()
495  IMP.algebra.Vector3D(*t)) * inv
496 
497  def _get_cross_correlation(self, model):
498  """Get the cross correlation coefficient between the model projection
499  and the image"""
500  stats = model.em2d_stats or model.stats
501  return float(stats['ElectronMicroscopy2D_%s_Image%d_CCC'
502  % (self.pmi_restraint.label,
503  self.image_number + 1)])
504 
505 
506 class _EM3DRestraint(ihm.restraint.EM3DRestraint):
507 
508  def __init__(self, simo, state, pmi_restraint, target_ps, densities):
509  self.pmi_restraint = pmi_restraint
510  super().__init__(
511  dataset=pmi_restraint.dataset,
512  assembly=self._get_assembly(densities, simo, state),
513  fitting_method='Gaussian mixture models',
514  number_of_gaussians=len(target_ps))
515 
516  # Have our dataset point to that in the original PMI restraint
517  def __set_dataset(self, val):
518  self.pmi_restraint.dataset = val
519  dataset = property(lambda self: self.pmi_restraint.dataset,
520  __set_dataset)
521 
522  def _get_assembly(self, densities, simo, state):
523  """Get the Assembly that this restraint acts on"""
524  cm = _ComponentMapper(state.prot)
525  components = {}
526  for d in densities:
527  components[cm[d]] = None # None == all residues in this component
528  a = simo._get_subassembly(
529  components, name="EM subassembly",
530  description="All components that fit the EM map")
531  return a
532 
533  def add_fits_from_model_statfile(self, model):
534  ccc = self._get_cross_correlation(model)
535  self.fits[model] = ihm.restraint.EM3DRestraintFit(
536  cross_correlation_coefficient=ccc)
537 
538  def _get_cross_correlation(self, model):
539  """Get the cross correlation coefficient between the model
540  and the map"""
541  if model.stats is not None:
542  return float(model.stats['GaussianEMRestraint_%s_CCC'
543  % self.pmi_restraint.label])
544 
545 
546 class _GeometricRestraint(ihm.restraint.GeometricRestraint):
547 
548  def __init__(self, simo, state, pmi_restraint, geometric_object,
549  feature, distance, sigma):
550  self.pmi_restraint = pmi_restraint
551  super().__init__(
552  dataset=pmi_restraint.dataset,
553  geometric_object=geometric_object, feature=feature,
554  distance=distance, harmonic_force_constant=1. / sigma,
555  restrain_all=True)
556 
557  # Have our dataset point to that in the original PMI restraint
558  def __set_dataset(self, val):
559  self.pmi_restraint.dataset = val
560  dataset = property(lambda self: self.pmi_restraint.dataset,
561  __set_dataset)
562 
563 
564 class _ReplicaExchangeProtocolStep(ihm.protocol.Step):
565  def __init__(self, state, rex):
566  if rex.monte_carlo_sample_objects is not None:
567  method = 'Replica exchange monte carlo'
568  else:
569  method = 'Replica exchange molecular dynamics'
570  self.monte_carlo_temperature = rex.vars['monte_carlo_temperature']
571  self.replica_exchange_minimum_temperature = \
572  rex.vars['replica_exchange_minimum_temperature']
573  self.replica_exchange_maximum_temperature = \
574  rex.vars['replica_exchange_maximum_temperature']
575  super().__init__(
576  assembly=state.modeled_assembly,
577  dataset_group=None, # filled in by add_step()
578  method=method, name='Sampling',
579  num_models_begin=None, # filled in by add_step()
580  num_models_end=rex.vars["number_of_frames"],
581  multi_scale=True, multi_state=False, ordered=False, ensemble=True)
582 
583 
584 class _ReplicaExchangeProtocolDumper(ihm.dumper.Dumper):
585  """Write IMP-specific information about replica exchange to mmCIF.
586  Note that IDs will have already been assigned by python-ihm's
587  standard modeling protocol dumper."""
588  def dump(self, system, writer):
589  with writer.loop("_imp_replica_exchange_protocol",
590  ["protocol_id", "step_id", "monte_carlo_temperature",
591  "replica_exchange_minimum_temperature",
592  "replica_exchange_maximum_temperature"]) as lp:
593  for p in system._all_protocols():
594  for s in p.steps:
595  if isinstance(s, _ReplicaExchangeProtocolStep):
596  self._dump_step(p, s, lp)
597 
598  def _dump_step(self, p, s, lp):
599  mintemp = s.replica_exchange_minimum_temperature
600  maxtemp = s.replica_exchange_maximum_temperature
601  lp.write(protocol_id=p._id, step_id=s._id,
602  monte_carlo_temperature=s.monte_carlo_temperature,
603  replica_exchange_minimum_temperature=mintemp,
604  replica_exchange_maximum_temperature=maxtemp)
605 
606 
607 class _ReplicaExchangeProtocolHandler(ihm.reader.Handler):
608  category = '_imp_replica_exchange_protocol'
609 
610  """Read IMP-specific information about replica exchange from mmCIF."""
611  def __call__(self, protocol_id, step_id, monte_carlo_temperature,
612  replica_exchange_minimum_temperature,
613  replica_exchange_maximum_temperature):
614  p = self.sysr.protocols.get_by_id(protocol_id)
615  # todo: match step_id properly (don't assume contiguous)
616  s = p.steps[int(step_id)-1]
617  # Upgrade from plain ihm Step to IMP subclass
618  s.__class__ = _ReplicaExchangeProtocolStep
619  s.monte_carlo_temperature = \
620  self.get_float(monte_carlo_temperature)
621  s.replica_exchange_minimum_temperature = \
622  self.get_float(replica_exchange_minimum_temperature)
623  s.replica_exchange_maximum_temperature = \
624  self.get_float(replica_exchange_maximum_temperature)
625 
626 
627 class _SimpleProtocolStep(ihm.protocol.Step):
628  def __init__(self, state, num_models_end, method):
629  super().__init__(
630  assembly=state.modeled_assembly,
631  dataset_group=None, # filled in by add_step()
632  method=method, name='Sampling',
633  num_models_begin=None, # filled in by add_step()
634  num_models_end=num_models_end,
635  multi_scale=True, multi_state=False, ordered=False,
636  ensemble=True)
637 
638 
639 class _Chain:
640  """Represent a single chain in a Model"""
641  def __init__(self, pmi_chain_id, asym_unit):
642  self.pmi_chain_id, self.asym_unit = pmi_chain_id, asym_unit
643  self.spheres = []
644  self.atoms = []
645 
646  def add(self, xyz, atom_type, residue_type, residue_index,
647  all_indexes, radius):
648  if atom_type is None:
649  self.spheres.append((xyz, residue_type, residue_index,
650  all_indexes, radius))
651  else:
652  self.atoms.append((xyz, atom_type, residue_type, residue_index,
653  all_indexes, radius))
654  orig_comp = property(lambda self: self.comp)
655 
656 
657 class _TransformedChain:
658  """Represent a chain that is a transformed version of another"""
659  def __init__(self, orig_chain, asym_unit, transform):
660  self.orig_chain, self.asym_unit = orig_chain, asym_unit
661  self.transform = transform
662 
663  def __get_spheres(self):
664  for (xyz, residue_type, residue_index, all_indexes,
665  radius) in self.orig_chain.spheres:
666  yield (self.transform * xyz, residue_type, residue_index,
667  all_indexes, radius)
668  spheres = property(__get_spheres)
669 
670  def __get_atoms(self):
671  for (xyz, atom_type, residue_type, residue_index, all_indexes,
672  radius) in self.orig_chain.atoms:
673  yield (self.transform * xyz, atom_type, residue_type,
674  residue_index, all_indexes, radius)
675  atoms = property(__get_atoms)
676 
677  entity = property(lambda self: self.orig_chain.entity)
678  orig_comp = property(lambda self: self.orig_chain.comp)
679 
680 
681 class _Excluder:
682  def __init__(self, component, simo):
683  self._seqranges = simo._exclude_coords.get(component, [])
684 
685  def is_excluded(self, indexes):
686  """Return True iff the given sequence range is excluded."""
687  for seqrange in self._seqranges:
688  if indexes[0] >= seqrange[0] and indexes[-1] <= seqrange[1]:
689  return True
690 
691 
692 class _Model(ihm.model.Model):
693  def __init__(self, prot, simo, protocol, assembly, representation):
694  super().__init__(assembly=assembly, protocol=protocol,
695  representation=representation)
696  self.simo = weakref.proxy(simo)
697  # Transformation from IMP coordinates into mmCIF coordinate system.
698  # Normally we pass through coordinates unchanged, but in some cases
699  # we may want to translate them (e.g. Nup84, where the deposited PDB
700  # files are all centered; we want the mmCIF files to match)
702  self.em2d_stats = None
703  self.stats = None
704  # True iff restraints act on this model
705  self._is_restrained = True
706  o = self.output = IMP.pmi.output.Output(atomistic=True)
707  name = 'cif-output'
708  self.m = prot.get_model()
709  o.dictionary_pdbs[name] = prot
710  o._init_dictchain(name, prot, multichar_chain=True)
711  (particle_infos_for_pdb,
712  self.geometric_center) = o.get_particle_infos_for_pdb_writing(name)
713  self.geometric_center = IMP.algebra.Vector3D(*self.geometric_center)
714  self._make_spheres_atoms(particle_infos_for_pdb, o, name, simo)
715  self.rmsf = {}
716 
717  def all_chains(self, simo):
718  """Yield all chains, including transformed ones"""
719  chain_for_comp = {}
720  for c in self.chains:
721  yield c
722  chain_for_comp[c.comp] = c
723  for tc in simo._transformed_components:
724  orig_chain = chain_for_comp.get(tc.original, None)
725  if orig_chain:
726  asym = simo.asym_units[tc.name]
727  c = _TransformedChain(orig_chain, asym, tc.transform)
728  c.comp = tc.name
729  yield c
730 
731  def _make_spheres_atoms(self, particle_infos_for_pdb, o, name, simo):
732  entity_for_chain = {}
733  comp_for_chain = {}
734  correct_asym = {}
735  for protname, chain_id in o.dictchain[name].items():
736  if protname in simo.entities:
737  entity_for_chain[chain_id] = simo.entities[protname]
738  else:
739  # Remove copy number
740  pn = protname.split('.')[0]
741  entity_for_chain[chain_id] = simo.entities[pn]
742  comp_for_chain[chain_id] = protname
743  # When doing multi-state modeling, the chain ID returned here
744  # (assigned sequentially) might not be correct (states may have
745  # gaps in the chain IDs). Map it to the correct asym unit.
746  correct_asym[chain_id] = simo.asym_units[protname]
747 
748  # Gather by chain ID (should be sorted by chain ID already)
749  self.chains = []
750  chain = None
751  excluder = None
752 
753  for (xyz, atom_type, residue_type, chain_id, residue_index,
754  all_indexes, radius) in particle_infos_for_pdb:
755  if chain is None or chain.pmi_chain_id != chain_id:
756  chain = _Chain(chain_id, correct_asym[chain_id])
757  chain.entity = entity_for_chain[chain_id]
758  chain.comp = comp_for_chain[chain_id]
759  self.chains.append(chain)
760  excluder = _Excluder(chain.comp, simo)
761  if not excluder.is_excluded(all_indexes if all_indexes
762  else [residue_index]):
763  chain.add(xyz, atom_type, residue_type, residue_index,
764  all_indexes, radius)
765 
766  def parse_rmsf_file(self, fname, component):
767  self.rmsf[component] = rmsf = {}
768  with open(str(fname)) as fh:
769  for line in fh:
770  resnum, blocknum, val = line.split()
771  rmsf[int(resnum)] = (int(blocknum), float(val))
772 
773  def get_rmsf(self, component, indexes):
774  """Get the RMSF value for the given residue indexes."""
775  if not self.rmsf:
776  return None
777  rmsf = self.rmsf[component]
778  blocknums = dict.fromkeys(rmsf[ind][0] for ind in indexes)
779  if len(blocknums) != 1:
780  raise ValueError("Residue indexes %s aren't all in the same block"
781  % str(indexes))
782  return rmsf[indexes[0]][1]
783 
784  def get_atoms(self):
785  for chain in self.all_chains(self.simo):
786  pmi_offset = chain.asym_unit.entity.pmi_offset
787  for atom in chain.atoms:
788  (xyz, atom_type, residue_type, residue_index,
789  all_indexes, radius) = atom
790  pt = self.transform * xyz
791  yield ihm.model.Atom(
792  asym_unit=chain.asym_unit,
793  seq_id=residue_index - pmi_offset,
794  atom_id=atom_type.get_string(),
795  type_symbol=None, # todo: get element
796  x=pt[0], y=pt[1], z=pt[2])
797 
798  def get_spheres(self):
799  for chain in self.all_chains(self.simo):
800  pmi_offset = chain.asym_unit.entity.pmi_offset
801  for sphere in chain.spheres:
802  (xyz, residue_type, residue_index,
803  all_indexes, radius) = sphere
804  if all_indexes is None:
805  all_indexes = (residue_index,)
806  pt = self.transform * xyz
807  yield ihm.model.Sphere(
808  asym_unit=chain.asym_unit,
809  seq_id_range=(all_indexes[0] - pmi_offset,
810  all_indexes[-1] - pmi_offset),
811  x=pt[0], y=pt[1], z=pt[2], radius=radius,
812  rmsf=self.get_rmsf(chain.orig_comp, all_indexes))
813 
814 
815 class _AllProtocols:
816  def __init__(self, simo):
817  self.simo = weakref.proxy(simo)
818  # Lists of Protocols by state
819  self.protocols = OrderedDict()
820 
821  def add_protocol(self, state):
822  """Add a new Protocol"""
823  if state not in self.protocols:
824  self.protocols[state] = []
825  p = ihm.protocol.Protocol()
826  self.simo.system.orphan_protocols.append(p)
827  self.protocols[state].append(p)
828 
829  def add_step(self, step, state):
830  """Add a ProtocolStep to the last Protocol of the given State"""
831  if state not in self.protocols:
832  self.add_protocol(state)
833  protocol = self.get_last_protocol(state)
834  if len(protocol.steps) == 0:
835  step.num_models_begin = 0
836  else:
837  step.num_models_begin = protocol.steps[-1].num_models_end
838  protocol.steps.append(step)
839  step.id = len(protocol.steps)
840  # Assume that protocol uses all currently-defined datasets
841  step.dataset_group = self.simo.all_datasets.get_all_group(state)
842 
843  def add_postproc(self, step, state):
844  """Add a postprocessing step to the last protocol"""
845  protocol = self.get_last_protocol(state)
846  if len(protocol.analyses) == 0:
847  protocol.analyses.append(ihm.analysis.Analysis())
848  protocol.analyses[-1].steps.append(step)
849 
850  def get_last_protocol(self, state):
851  """Return the most recently-added _Protocol"""
852  return self.protocols[state][-1]
853 
854 
855 class _AllStartingModels:
856  def __init__(self, simo):
857  self.simo = simo
858  # dict of starting models (entire PDB files), collected from fragments,
859  # ordered by component name and state
860  self.models = OrderedDict()
861  self.output = IMP.pmi.output.Output()
862 
863  def add_pdb_fragment(self, fragment):
864  """Add a starting model PDB fragment."""
865  comp = fragment.component
866  state = fragment.state
867  if comp not in self.models:
868  self.models[comp] = OrderedDict()
869  if state not in self.models[comp]:
870  self.models[comp][state] = []
871  models = self.models[comp][state]
872  if len(models) == 0 \
873  or models[-1].fragments[0].pdbname != fragment.pdbname:
874  model = self._add_model(fragment)
875  models.append(model)
876  else:
877  # Break circular ref between fragment and model
878  models[-1].fragments.append(weakref.proxy(fragment))
879  # Update residue range to cover all fragments
880  # We don't use fragment.offset here because fragment numbering
881  # is already in seq_id, not PDB numbering
882  pmi_offset = models[-1].asym_unit.entity.pmi_offset
883  sid_begin = min(fragment.start - pmi_offset,
884  models[-1].asym_unit.seq_id_range[0])
885  sid_end = max(fragment.end - pmi_offset,
886  models[-1].asym_unit.seq_id_range[1])
887  models[-1].asym_unit = fragment.asym_unit.asym(sid_begin, sid_end)
888  fragment.starting_model = models[-1]
889 
890  def _add_model(self, f):
891  if (hasattr(ihm.metadata, 'CIFParser')
892  and f.pdbname.endswith('.cif')):
893  parser = ihm.metadata.CIFParser()
894  elif (hasattr(ihm.metadata, 'BinaryCIFParser')
895  and f.pdbname.endswith('.bcif')):
896  parser = ihm.metadata.BinaryCIFParser()
897  else:
898  parser = ihm.metadata.PDBParser()
899  r = parser.parse_file(f.pdbname)
900 
901  self.simo._add_dataset(r['dataset'])
902  # We only want the templates that model the starting model chain
903  templates = r['templates'].get(f.chain, [])
904  for t in templates:
905  if t.alignment_file:
906  self.simo.system.locations.append(t.alignment_file)
907  if t.dataset:
908  self.simo._add_dataset(t.dataset)
909  source = r.get('entity_source', {}).get(f.chain)
910  if source:
911  f.asym_unit.entity.source = source
912  pmi_offset = f.asym_unit.entity.pmi_offset
913  m = _StartingModel(
914  asym_unit=f.asym_unit.asym.pmi_range(f.start, f.end),
915  dataset=r['dataset'], asym_id=f.chain,
916  templates=templates, offset=f.offset + pmi_offset,
917  metadata=r.get('metadata'),
918  software=r['software'][0] if r['software'] else None,
919  script_file=r['script'])
920  m.fragments = [weakref.proxy(f)]
921  return m
922 
923 
924 class _StartingModel(ihm.startmodel.StartingModel):
925  def get_seq_dif(self):
926  return self._seq_dif
927 
928  def get_atoms(self):
929  pmi_offset = self.asym_unit.entity.pmi_offset
930  mh = IMP.mmcif.data._StartingModelAtomHandler(self.templates,
931  self.asym_unit)
932  for f in self.fragments:
933  sel = IMP.atom.Selection(
934  f.starting_hier,
935  residue_indexes=list(range(f.start - f.offset,
936  f.end - f.offset + 1)))
937  for a in mh.get_ihm_atoms(sel.get_selected_particles(),
938  f.offset - pmi_offset):
939  yield a
940  self._seq_dif = mh._seq_dif
941 
942 
943 class _ReplicaExchangeAnalysisPostProcess(ihm.analysis.ClusterStep):
944  """Post processing using AnalysisReplicaExchange0 macro"""
945 
946  def __init__(self, rex, num_models_begin):
947  self.rex = rex
948  num_models_end = 0
949  for fname in self.get_all_stat_files():
950  with open(str(fname)) as fh:
951  num_models_end += len(fh.readlines())
952  super().__init__(
953  feature='RMSD', num_models_begin=num_models_begin,
954  num_models_end=num_models_end)
955 
956  def get_stat_file(self, cluster_num):
957  return self.rex._outputdir / ("cluster.%d" % cluster_num) / 'stat.out'
958 
959  def get_all_stat_files(self):
960  for i in range(self.rex._number_of_clusters):
961  yield self.get_stat_file(i)
962 
963 
964 class _ReplicaExchangeAnalysisEnsemble(ihm.model.Ensemble):
965  """Ensemble generated using AnalysisReplicaExchange0 macro"""
966 
967  num_models_deposited = None # Override base class property
968 
969  def __init__(self, pp, cluster_num, model_group, num_deposit):
970  with open(str(pp.get_stat_file(cluster_num))) as fh:
971  num_models = len(fh.readlines())
972  super().__init__(
973  num_models=num_models,
974  model_group=model_group, post_process=pp,
975  clustering_feature=pp.feature,
976  name=model_group.name)
977  self.cluster_num = cluster_num
978  self.num_models_deposited = num_deposit
979 
980  def get_rmsf_file(self, component):
981  return (self.post_process.rex._outputdir
982  / ('cluster.%d' % self.cluster_num)
983  / ('rmsf.%s.dat' % component))
984 
985  def load_rmsf(self, model, component):
986  fname = self.get_rmsf_file(component)
987  if fname.exists():
988  model.parse_rmsf_file(fname, component)
989 
990  def get_localization_density_file(self, fname):
991  return (self.post_process.rex._outputdir
992  / ('cluster.%d' % self.cluster_num)
993  / ('%s.mrc' % fname))
994 
995  def load_localization_density(self, state, fname, select_tuple,
996  asym_units):
997  fullpath = self.get_localization_density_file(fname)
998  if fullpath.exists():
999  details = "Localization density for %s %s" \
1000  % (fname, self.model_group.name)
1001  local_file = ihm.location.OutputFileLocation(str(fullpath),
1002  details=details)
1003  for s in select_tuple:
1004  if isinstance(s, tuple) and len(s) == 3:
1005  asym = asym_units[s[2]].pmi_range(s[0], s[1])
1006  else:
1007  asym = asym_units[s]
1008  den = ihm.model.LocalizationDensity(file=local_file,
1009  asym_unit=asym)
1010  self.densities.append(den)
1011 
1012  def load_all_models(self, simo, state):
1013  stat_fname = self.post_process.get_stat_file(self.cluster_num)
1014  model_num = 0
1015  with open(str(stat_fname)) as fh:
1016  stats = ast.literal_eval(fh.readline())
1017  # Correct path
1018  rmf_file = stat_fname.parent / ("%d.rmf3" % model_num)
1019  # todo: test with real PMI2 systems
1020  if rmf_file.exists():
1021  rh = RMF.open_rmf_file_read_only(str(rmf_file))
1022  system = state._pmi_object.system
1023  IMP.rmf.link_hierarchies(rh, [system.hier])
1024  IMP.rmf.load_frame(fh, RMF.FrameID(0))
1025  # todo: fill in other data from stat file, e.g. cross-link phi/psi
1026  yield stats
1027  model_num += 1
1028  if model_num >= self.num_models_deposited:
1029  return
1030 
1031  # todo: also support dRMS precision
1032  def _get_precision(self):
1033  precfile = (self.post_process.rex._outputdir /
1034  ("precision.%d.%d.out" % (self.cluster_num,
1035  self.cluster_num)))
1036  if not precfile.exists():
1037  return ihm.unknown
1038  # Fail if the precision.x.x.out file doesn't match the cluster
1039  r = re.compile(
1040  r'All .*/cluster.%d/ average centroid distance ([\d\.]+)'
1041  % self.cluster_num)
1042  with open(str(precfile)) as fh:
1043  for line in fh:
1044  m = r.match(line)
1045  if m:
1046  return float(m.group(1))
1047 
1048  precision = property(lambda self: self._get_precision(),
1049  lambda self, val: None)
1050 
1051 
1052 class _SimpleEnsemble(ihm.model.Ensemble):
1053  """Simple manually-created ensemble"""
1054 
1055  num_models_deposited = None # Override base class property
1056 
1057  def __init__(self, pp, model_group, num_models, drmsd,
1058  num_models_deposited, ensemble_file):
1059  super().__init__(
1060  model_group=model_group, post_process=pp, num_models=num_models,
1061  file=ensemble_file, precision=drmsd, name=model_group.name,
1062  clustering_feature='dRMSD')
1063  self.num_models_deposited = num_models_deposited
1064 
1065  def load_localization_density(self, state, component, asym, local_file):
1066  den = ihm.model.LocalizationDensity(file=local_file, asym_unit=asym)
1067  self.densities.append(den)
1068 
1069 
1070 class _CustomDNAAlphabet:
1071  """Custom DNA alphabet that maps A,C,G,T (rather than DA,DC,DG,DT
1072  as in python-ihm)"""
1073  _comps = dict([cc.code_canonical, cc]
1074  for cc in ihm.DNAAlphabet._comps.values())
1075 
1076 
1077 class _EntityMapper(dict):
1078  """Handle mapping from IMP components (without copy number) to CIF
1079  entities. Multiple components may map to the same entity if they
1080  share sequence."""
1081  def __init__(self, system):
1082  super().__init__()
1083  self._sequence_dict = {}
1084  self._entities = []
1085  self.system = system
1086 
1087  def _get_alphabet(self, alphabet):
1088  """Map a PMI alphabet to an IHM alphabet"""
1089  # todo: we should probably ignore alphabets entirely and use
1090  # residue types directly (e.g. we shouldn't compare one-letter
1091  # code sequence to determine if an Entity is unique)
1092  alphabet_map = {None: ihm.LPeptideAlphabet,
1093  IMP.pmi.alphabets.amino_acid: ihm.LPeptideAlphabet,
1094  IMP.pmi.alphabets.rna: ihm.RNAAlphabet,
1095  IMP.pmi.alphabets.dna: _CustomDNAAlphabet}
1096  if alphabet in alphabet_map:
1097  return alphabet_map[alphabet]
1098  else:
1099  raise TypeError("Don't know how to handle %s" % alphabet)
1100 
1101  def add(self, component_name, sequence, offset, alphabet, uniprot):
1102  def entity_seq(sequence):
1103  # Map X to UNK
1104  if 'X' in sequence:
1105  return ['UNK' if s == 'X' else s for s in sequence]
1106  else:
1107  return sequence
1108  if sequence not in self._sequence_dict:
1109  # Use the name of the first component, stripped of any copy number,
1110  # as the description of the entity
1111  d = component_name.split("@")[0].split(".")[0]
1112  entity = Entity(entity_seq(sequence), description=d,
1113  pmi_offset=offset,
1114  alphabet=self._get_alphabet(alphabet),
1115  uniprot=uniprot)
1116  self.system.entities.append(entity)
1117  self._sequence_dict[sequence] = entity
1118  self[component_name] = self._sequence_dict[sequence]
1119 
1120 
1121 class _TransformedComponent:
1122  def __init__(self, name, original, transform):
1123  self.name, self.original, self.transform = name, original, transform
1124 
1125 
1126 class _SimpleRef:
1127  """Class with similar interface to weakref.ref, but keeps a strong ref"""
1128  def __init__(self, ref):
1129  self.ref = ref
1130 
1131  def __call__(self):
1132  return self.ref
1133 
1134 
1135 class _State(ihm.model.State):
1136  """Representation of a single state in the system."""
1137 
1138  def __init__(self, pmi_object, po):
1139  # Point to the PMI object for this state. Use a weak reference
1140  # since the state object typically points to us too, so we need
1141  # to break the reference cycle. In PMI1 this will be a
1142  # Representation object; in PMI2 it is the PMI2 State object itself.
1143  self._pmi_object = weakref.proxy(pmi_object)
1144  if hasattr(pmi_object, 'state'):
1145  # Need a strong ref to pmi_object.state to prevent it from being
1146  # cleaned up when doing PMI1 multistate modeling
1147  self._pmi_state = _SimpleRef(pmi_object.state)
1148  else:
1149  self._pmi_state = weakref.ref(pmi_object)
1150  # Preserve PMI state name
1151  old_name = self.name
1152  super().__init__(experiment_type='Fraction of bulk')
1153  self.name = old_name
1154 
1155  # The assembly of all components modeled by IMP in this state.
1156  # This may be smaller than the complete assembly.
1157  self.modeled_assembly = ihm.Assembly(
1158  name="Modeled assembly",
1159  description=self.get_postfixed_name(
1160  "All components modeled by IMP"))
1161  po.system.orphan_assemblies.append(self.modeled_assembly)
1162 
1163  self.all_modeled_components = []
1164 
1165  def __hash__(self):
1166  return hash(self._pmi_state())
1167 
1168  def __eq__(self, other):
1169  return self._pmi_state() == other._pmi_state()
1170 
1171  def add_model_group(self, group):
1172  self.append(group)
1173  return group
1174 
1175  def get_prefixed_name(self, name):
1176  """Prefix the given name with the state name, if available."""
1177  if self.short_name:
1178  return self.short_name + ' ' + name
1179  else:
1180  # Can't use capitalize() since that un-capitalizes everything
1181  # but the first letter
1182  return name[0].upper() + name[1:] if name else ''
1183 
1184  def get_postfixed_name(self, name):
1185  """Postfix the given name with the state name, if available."""
1186  if self.short_name:
1187  return "%s in state %s" % (name, self.short_name)
1188  else:
1189  return name
1190 
1191  short_name = property(lambda self: self._pmi_state().short_name)
1192  long_name = property(lambda self: self._pmi_state().long_name)
1193 
1194  def __get_name(self):
1195  return self._pmi_state().long_name
1196 
1197  def __set_name(self, val):
1198  self._pmi_state().long_name = val
1199 
1200  name = property(__get_name, __set_name)
1201 
1202 
1203 class Entity(ihm.Entity):
1204  """A single entity in the system. This contains information (such as
1205  database identifiers) specific to a particular sequence rather than
1206  a copy (for example, when modeling a homodimer, two AsymUnits will
1207  point to the same Entity).
1208 
1209  This functions identically to the base ihm.Entity class, but it
1210  allows identifying residues by either the PMI numbering scheme
1211  (which is always contiguous starting at 1, covering the entire
1212  sequence in the FASTA files, or the IHM scheme (seq_id, which also
1213  starts at 1, but which only covers the modeled subset of the full
1214  sequence, with non-modeled N-terminal or C-terminal residues
1215  removed). The actual offset (which is the integer to be added to the
1216  IHM numbering to get PMI numbering, or equivalently the number of
1217  not-represented N-terminal residues in the PMI sequence) is
1218  available in the `pmi_offset` member.
1219 
1220  If a UniProt accession was provided for the sequence (either when
1221  State.create_molecule() was called, or in the FASTA alignment file
1222  header) then that is available in the `uniprot` member, and can be
1223  added to the IHM system with the add_uniprot_reference method.
1224  """
1225  def __init__(self, sequence, pmi_offset, uniprot, *args, **keys):
1226  # Offset between PMI numbering and IHM; <pmi_#> = <ihm_#> + pmi_offset
1227  # (pmi_offset is also the number of N-terminal gaps in the FASTA file)
1228  self.pmi_offset = pmi_offset
1229  self.uniprot = uniprot
1230  super().__init__(sequence, *args, **keys)
1231 
1232  def pmi_residue(self, res_id):
1233  """Return a single IHM residue indexed using PMI numbering"""
1234  return self.residue(res_id - self.pmi_offset)
1235 
1236  def pmi_range(self, res_id_begin, res_id_end):
1237  """Return a range of IHM residues indexed using PMI numbering"""
1238  off = self.pmi_offset
1239  return self(res_id_begin - off, res_id_end - off)
1240 
1241  def add_uniprot_reference(self):
1242  """Add UniProt accession (if available) to the IHM system.
1243  If a UniProt accession was provided for the sequence (either when
1244  State.create_molecule() was called, or in the FASTA alignment file
1245  header), then look this up at the UniProt web site (requires
1246  network access) to get full information, and add it to the IHM
1247  system. The resulting reference object is returned. If the IMP
1248  and UniProt sequences are not identical, then this object may
1249  need to be modified by specifying an alignment and/or
1250  single-point mutations.
1251  """
1252  if self.uniprot:
1253  print('Adding UniProt accession %s reference for entity %s'
1254  % (self.uniprot, self.description))
1255  ref = ihm.reference.UniProtSequence.from_accession(self.uniprot)
1256  self.references.append(ref)
1257  return ref
1258 
1259 
1260 class AsymUnit(ihm.AsymUnit):
1261  """A single asymmetric unit in the system. This roughly corresponds to
1262  a single PMI subunit (sequence, copy, or clone).
1263 
1264  This functions identically to the base ihm.AsymUnit class, but it
1265  allows identifying residues by either the PMI numbering scheme
1266  (which is always contiguous starting at 1, covering the entire
1267  sequence in the FASTA files, or the IHM scheme (seq_id, which also
1268  starts at 1, but which only covers the modeled subset of the full
1269  sequence, with non-modeled N-terminal or C-terminal residues
1270  removed).
1271 
1272  The `entity` member of this class points to an Entity object, which
1273  contains information (such as database identifiers) specific to
1274  a particular sequence rather than a copy (for example, when modeling
1275  a homodimer, two AsymUnits will point to the same Entity).
1276  """
1277 
1278  def __init__(self, entity, *args, **keys):
1279  super().__init__(
1280  entity, auth_seq_id_map=entity.pmi_offset, *args, **keys)
1281 
1282  def pmi_residue(self, res_id):
1283  """Return a single IHM residue indexed using PMI numbering"""
1284  return self.residue(res_id - self.entity.pmi_offset)
1285 
1286  def pmi_range(self, res_id_begin, res_id_end):
1287  """Return a range of IHM residues indexed using PMI numbering"""
1288  off = self.entity.pmi_offset
1289  return self(res_id_begin - off, res_id_end - off)
1290 
1291 
1293  """Class to encode a modeling protocol as mmCIF.
1294 
1295  IMP has basic support for writing out files in mmCIF format, for
1296  deposition in [PDB-IHM](https://pdb-ihm.org/).
1297  After creating an instance of this class, attach it to an
1298  IMP.pmi.topology.System object. After this, any
1299  generated models and metadata are automatically collected in the
1300  `system` attribute, which is an
1301  [ihm.System](https://python-ihm.readthedocs.io/en/latest/main.html#ihm.System) object.
1302  Once the protocol is complete, call finalize() to make sure `system`
1303  contains everything, then use the
1304  [python-ihm API](https://python-ihm.readthedocs.io/en/latest/dumper.html#ihm.dumper.write)
1305  to write out files in mmCIF or BinaryCIF format.
1306 
1307  Each PMI subunit will be mapped to an IHM AsymUnit class which contains the
1308  subset of the sequence that was represented. Use the `asym_units` dict to
1309  get this object given a PMI subunit name. Each unique sequence will be
1310  mapped to an IHM Entity class (for example when modeling a homodimer
1311  there will be two AsymUnits which both point to the same Entity). Use
1312  the `entities` dict to get this object from a PMI subunit name.
1313 
1314  See also Entity, AsymUnit, get_handlers(), get_dumpers().
1315  """ # noqa: E501
1316  def __init__(self):
1317  # Ultimately, collect data in an ihm.System object
1318  self.system = ihm.System(model_details=self._get_model_details())
1319  self._state_group = ihm.model.StateGroup()
1320  self.system.state_groups.append(self._state_group)
1321 
1322  self._state_ensemble_offset = 0
1323  self._main_script = os.path.abspath(sys.argv[0])
1324 
1325  # Point to the main modeling script
1326  loc = ihm.location.WorkflowFileLocation(
1327  path=self._main_script,
1328  details="The main integrative modeling script")
1329  self.system.locations.append(loc)
1330 
1331  self._states = {}
1332  self.__asym_states = {}
1333  self._working_directory = os.getcwd()
1334  self.default_representation = self.create_representation(
1335  "Default representation")
1336  self.entities = _EntityMapper(self.system)
1337  # Mapping from component names to ihm.AsymUnit
1338  self.asym_units = {}
1339  self._all_components = {}
1340  self.all_modeled_components = []
1341  self._transformed_components = []
1342  self.sequence_dict = {}
1343 
1344  # Common geometric objects used in PMI systems
1345  self._xy_plane = ihm.geometry.XYPlane()
1346  self._xz_plane = ihm.geometry.XZPlane()
1347  self._z_axis = ihm.geometry.ZAxis()
1348  self._center_origin = ihm.geometry.Center(0, 0, 0)
1349  self._identity_transform = ihm.geometry.Transformation.identity()
1350 
1351  # Coordinates to exclude
1352  self._exclude_coords = {}
1353 
1354  self.all_representations = _AllModelRepresentations(self)
1355  self.all_protocols = _AllProtocols(self)
1356  self.all_datasets = _AllDatasets(self.system)
1357  self.all_starting_models = _AllStartingModels(self)
1358 
1359  self.all_software = _AllSoftware(self.system)
1360 
1361  def _get_model_details(self):
1362  """Return a more detailed description of the modeling."""
1363  tm = time.strftime("%c")
1364  return "Generated by the Integrative Modeling Platform (IMP) on " + tm
1365 
1366  def create_representation(self, name):
1367  """Create a new Representation and return it. This can be
1368  passed to add_model(), add_bead_element() or add_pdb_element()."""
1369  r = ihm.representation.Representation(name=name)
1370  self.system.orphan_representations.append(r)
1371  return r
1372 
1373  def exclude_coordinates(self, component, seqrange):
1374  """Don't record coordinates for the given domain.
1375  Coordinates for the given domain (specified by a component name
1376  and a 2-element tuple giving the start and end residue numbers)
1377  will be excluded from the mmCIF file. This can be used to exclude
1378  parts of the structure that weren't well resolved in modeling.
1379  Any bead or residue that lies wholly within this range will be
1380  excluded. Multiple ranges for a given component can be excluded
1381  by calling this method multiple times."""
1382  if component not in self._exclude_coords:
1383  self._exclude_coords[component] = []
1384  self._exclude_coords[component].append(seqrange)
1385 
1386  def _is_excluded(self, component, start, end):
1387  """Return True iff this chunk of sequence should be excluded"""
1388  for seqrange in self._exclude_coords.get(component, ()):
1389  if start >= seqrange[0] and end <= seqrange[1]:
1390  return True
1391 
1392  def _add_state(self, state):
1393  """Create a new state and return a pointer to it."""
1394  self._state_ensemble_offset = len(self.system.ensembles)
1395  s = _State(state, self)
1396  self._state_group.append(s)
1397  self._last_state = s
1398  return s
1399 
1400  def _get_chain_for_component(self, name, output):
1401  """Get the chain ID for a component, if any."""
1402  # todo: handle multiple copies
1403  if name in self.asym_units:
1404  return self.asym_units[name]._id
1405  else:
1406  # A non-modeled component doesn't have a chain ID
1407  return None
1408 
1409  def _get_assembly_comps(self, assembly):
1410  """Get the names of the components in the given assembly"""
1411  # component name = asym_unit.details
1412  comps = {}
1413  for ca in assembly:
1414  comps[ca.details] = None
1415  return comps
1416 
1417  def create_transformed_component(self, state, name, original, transform):
1418  """Make a new component that's a transformed copy of another.
1419  All representation for the existing component is copied to the
1420  new one."""
1421  assembly_comps = self._get_assembly_comps(state.modeled_assembly)
1422  if name in assembly_comps:
1423  raise ValueError("Component %s already exists" % name)
1424  elif original not in assembly_comps:
1425  raise ValueError("Original component %s does not exist" % original)
1426  self.create_component(state, name, True)
1427  self.add_component_sequence(state, name, self.sequence_dict[original])
1428  self._transformed_components.append(_TransformedComponent(
1429  name, original, transform))
1430  self.all_representations.copy_component(state, name, original,
1431  self.asym_units[name])
1432 
1433  def create_component(self, state, name, modeled, asym_name=None):
1434  if asym_name is None:
1435  asym_name = name
1436  new_comp = name not in self._all_components
1437  self._all_components[name] = None
1438  if modeled:
1439  state.all_modeled_components.append(name)
1440  if asym_name not in self.asym_units:
1441  # assign asym once we get sequence
1442  self.asym_units[asym_name] = None
1443  if new_comp:
1444  self.all_modeled_components.append(name)
1445 
1446  def add_component_sequence(self, state, name, seq, asym_name=None,
1447  alphabet=None, uniprot=None):
1448  if asym_name is None:
1449  asym_name = name
1450 
1451  if name in self.sequence_dict:
1452  if self.sequence_dict[name] != seq:
1453  raise ValueError("Sequence mismatch for component %s" % name)
1454  else:
1455  self.sequence_dict[name] = seq
1456  # Offset is always zero to start with; this may be modified
1457  # later in finalize_build() if any non-modeled N-terminal
1458  # residues are removed
1459  self.entities.add(name, seq, 0, alphabet, uniprot)
1460  if asym_name in self.asym_units:
1461  if self.asym_units[asym_name] is None:
1462  # Set up a new asymmetric unit for this component
1463  entity = self.entities[name]
1464  asym = AsymUnit(entity, details=asym_name)
1465  self.system.asym_units.append(asym)
1466  self.asym_units[asym_name] = asym
1467  state.modeled_assembly.append(self.asym_units[asym_name])
1468 
1469  def finalize_build(self):
1470  """Called immediately after the PMI system is built"""
1471  for entity in self.system.entities:
1472  _trim_unrep_termini(entity, self.system.asym_units,
1473  self.system.orphan_representations)
1474 
1475  def _add_restraint_model_fits(self):
1476  """Add fits to restraints for all known models"""
1477  for group, m in self.system._all_models():
1478  if m._is_restrained:
1479  for r in self.system.restraints:
1480  if hasattr(r, 'add_fits_from_model_statfile'):
1481  r.add_fits_from_model_statfile(m)
1482 
1483  def finalize(self):
1484  """Do any final processing on the class hierarchy.
1485  After calling this method, the `system` member (an instance
1486  of `ihm.System`) completely reproduces the PMI modeling, and
1487  can be written out to an mmCIF file with `ihm.dumper.write`,
1488  and/or modified using the ihm API."""
1489  self._add_restraint_model_fits()
1490 
1491  def add_pdb_element(self, state, name, start, end, offset, pdbname,
1492  chain, hier, representation=None):
1493  if self._is_excluded(name, start, end):
1494  return
1495  if representation is None:
1496  representation = self.default_representation
1497  asym = self.asym_units[name]
1498  p = _PDBFragment(state, name, start, end, offset, pdbname, chain,
1499  hier, asym)
1500  self.all_representations.add_fragment(state, representation, p)
1501  self.all_starting_models.add_pdb_fragment(p)
1502 
1503  def add_bead_element(self, state, name, start, end, num, hier,
1504  representation=None):
1505  if self._is_excluded(name, start, end):
1506  return
1507  if representation is None:
1508  representation = self.default_representation
1509  asym = self.asym_units[name]
1510  pmi_offset = asym.entity.pmi_offset
1511  b = _BeadsFragment(state, name, start - pmi_offset, end - pmi_offset,
1512  num, hier, asym)
1513  self.all_representations.add_fragment(state, representation, b)
1514 
1515  def get_cross_link_group(self, pmi_restraint):
1516  r = _CrossLinkRestraint(pmi_restraint)
1517  self.system.restraints.append(r)
1518  self._add_restraint_dataset(r) # so that all-dataset group works
1519  return r
1520 
1521  def add_experimental_cross_link(self, r1, c1, r2, c2, rsr):
1522  if c1 not in self._all_components or c2 not in self._all_components:
1523  # Crosslink refers to a component we didn't model
1524  # As a quick hack, just ignore it.
1525  # todo: need to add an entity for this anyway (so will need the
1526  # sequence, and add to struct_assembly)
1527  return None
1528  e1 = self.entities[c1]
1529  e2 = self.entities[c2]
1530  xl = ihm.restraint.ExperimentalCrossLink(residue1=e1.pmi_residue(r1),
1531  residue2=e2.pmi_residue(r2))
1532  rsr.experimental_cross_links.append([xl])
1533  return xl
1534 
1535  def add_cross_link(self, state, ex_xl, p1, p2, length, sigma1_p, sigma2_p,
1536  psi_p, rsr):
1537  # Map p1/p2 to asym units
1538  asym = get_asym_mapper_for_state(self, state, self.__asym_states)
1539  d = ihm.restraint.UpperBoundDistanceRestraint(length)
1540 
1541  if _get_by_residue(p1) and _get_by_residue(p2):
1542  cls = _ResidueCrossLink
1543  else:
1544  cls = _FeatureCrossLink
1545  xl = cls(ex_xl, asym1=asym[p1], asym2=asym[p2], distance=d,
1546  restrain_all=False)
1547  # Needed to look up fits later
1548  xl.psi_p, xl.sigma1_p, xl.sigma2_p = psi_p, sigma1_p, sigma2_p
1549  rsr.cross_links.append(xl)
1550 
1551  def add_replica_exchange(self, state, rex):
1552  # todo: allow for metadata to say how many replicas were used in the
1553  # actual experiment, and how many independent runs were carried out
1554  # (use these as multipliers to get the correct total number of
1555  # output models)
1556  step = _ReplicaExchangeProtocolStep(state, rex)
1557  step.software = self.all_software.pmi
1558  self.all_protocols.add_step(step, state)
1559 
1560  def _add_simple_dynamics(self, num_models_end, method):
1561  # Always assumed that we're dealing with the last state
1562  state = self._last_state
1563  self.all_protocols.add_step(_SimpleProtocolStep(state, num_models_end,
1564  method), state)
1565 
1566  def _add_protocol(self):
1567  # Always assumed that we're dealing with the last state
1568  state = self._last_state
1569  self.all_protocols.add_protocol(state)
1570 
1571  def _add_dataset(self, dataset):
1572  return self.all_datasets.add(self._last_state, dataset)
1573 
1574  def _add_restraint_dataset(self, restraint):
1575  return self.all_datasets.add_restraint(self._last_state, restraint)
1576 
1577  def _add_simple_postprocessing(self, num_models_begin, num_models_end):
1578  # Always assumed that we're dealing with the last state
1579  state = self._last_state
1580  pp = ihm.analysis.ClusterStep('RMSD', num_models_begin, num_models_end)
1581  self.all_protocols.add_postproc(pp, state)
1582  return pp
1583 
1584  def _add_no_postprocessing(self, num_models):
1585  # Always assumed that we're dealing with the last state
1586  state = self._last_state
1587  pp = ihm.analysis.EmptyStep()
1588  pp.num_models_begin = pp.num_models_end = num_models
1589  self.all_protocols.add_postproc(pp, state)
1590  return pp
1591 
1592  def _add_simple_ensemble(self, pp, name, num_models, drmsd,
1593  num_models_deposited, localization_densities,
1594  ensemble_file):
1595  """Add an ensemble generated by ad hoc methods (not using PMI).
1596  This is currently only used by the Nup84 system."""
1597  # Always assumed that we're dealing with the last state
1598  state = self._last_state
1599  group = ihm.model.ModelGroup(name=state.get_postfixed_name(name))
1600  state.add_model_group(group)
1601  if ensemble_file:
1602  self.system.locations.append(ensemble_file)
1603  e = _SimpleEnsemble(pp, group, num_models, drmsd, num_models_deposited,
1604  ensemble_file)
1605  self.system.ensembles.append(e)
1606  for c in state.all_modeled_components:
1607  den = localization_densities.get(c, None)
1608  if den:
1609  e.load_localization_density(state, c, self.asym_units[c], den)
1610  return e
1611 
1612  def set_ensemble_file(self, i, location):
1613  """Point a previously-created ensemble to an 'all-models' file.
1614  This could be a trajectory such as DCD, an RMF, or a multimodel
1615  PDB file."""
1616  self.system.locations.append(location)
1617  # Ensure that we point to an ensemble related to the current state
1618  ind = i + self._state_ensemble_offset
1619  self.system.ensembles[ind].file = location
1620 
1621  def add_replica_exchange_analysis(self, state, rex, density_custom_ranges):
1622  # todo: add prefilter as an additional postprocess step (complication:
1623  # we don't know how many models it filtered)
1624  # todo: postpone rmsf/density localization extraction until after
1625  # relevant methods are called (currently it is done from the
1626  # constructor)
1627  protocol = self.all_protocols.get_last_protocol(state)
1628  num_models = protocol.steps[-1].num_models_end
1629  pp = _ReplicaExchangeAnalysisPostProcess(rex, num_models)
1630  pp.software = self.all_software.pmi
1631  self.all_protocols.add_postproc(pp, state)
1632  for i in range(rex._number_of_clusters):
1633  group = ihm.model.ModelGroup(name=state.get_prefixed_name(
1634  'cluster %d' % (i + 1)))
1635  state.add_model_group(group)
1636  # todo: make # of models to deposit configurable somewhere
1637  e = _ReplicaExchangeAnalysisEnsemble(pp, i, group, 1)
1638  self.system.ensembles.append(e)
1639  # Add localization density info if available
1640  for fname, stuple in sorted(density_custom_ranges.items()):
1641  e.load_localization_density(state, fname, stuple,
1642  self.asym_units)
1643  for stats in e.load_all_models(self, state):
1644  m = self.add_model(group)
1645  # Since we currently only deposit 1 model, it is the
1646  # best scoring one
1647  m.name = 'Best scoring model'
1648  m.stats = stats
1649  # Add RMSF info if available
1650  for c in state.all_modeled_components:
1651  e.load_rmsf(m, c)
1652 
1653  def _get_subassembly(self, comps, name, description):
1654  """Get an Assembly consisting of the given components.
1655  `compdict` is a dictionary of the components to add, where keys
1656  are the component names and values are the sequence ranges (or
1657  None to use all residues in the component)."""
1658  asyms = []
1659  for comp, seqrng in comps.items():
1660  a = self.asym_units[comp]
1661  asyms.append(a if seqrng is None else a(*seqrng))
1662 
1663  a = ihm.Assembly(asyms, name=name, description=description)
1664  return a
1665 
1666  def _add_foxs_restraint(self, model, comp, seqrange, dataset, rg, chi,
1667  details):
1668  """Add a basic FoXS fit. This is largely intended for use from the
1669  NPC application."""
1670  assembly = self._get_subassembly(
1671  {comp: seqrange},
1672  name="SAXS subassembly",
1673  description="All components that fit SAXS data")
1674  r = ihm.restraint.SASRestraint(
1675  dataset, assembly, segment=False,
1676  fitting_method='FoXS', fitting_atom_type='Heavy atoms',
1677  multi_state=False, radius_of_gyration=rg, details=details)
1678  r.fits[model] = ihm.restraint.SASRestraintFit(chi_value=chi)
1679  self.system.restraints.append(r)
1680  self._add_restraint_dataset(r) # so that all-dataset group works
1681 
1682  def add_em2d_restraint(self, state, r, i, resolution, pixel_size,
1683  image_resolution, projection_number,
1684  micrographs_number):
1685  r = _EM2DRestraint(state, r, i, resolution, pixel_size,
1686  image_resolution, projection_number,
1687  micrographs_number)
1688  self.system.restraints.append(r)
1689  self._add_restraint_dataset(r) # so that all-dataset group works
1690 
1691  def add_em3d_restraint(self, state, target_ps, densities, pmi_restraint):
1692  # todo: need to set allow_duplicates on this dataset?
1693  r = _EM3DRestraint(self, state, pmi_restraint, target_ps, densities)
1694  self.system.restraints.append(r)
1695  self._add_restraint_dataset(r) # so that all-dataset group works
1696 
1697  def add_zaxial_restraint(self, state, ps, lower_bound, upper_bound,
1698  sigma, pmi_restraint):
1699  self._add_geometric_restraint(state, ps, lower_bound, upper_bound,
1700  sigma, pmi_restraint, self._xy_plane)
1701 
1702  def add_yaxial_restraint(self, state, ps, lower_bound, upper_bound,
1703  sigma, pmi_restraint):
1704  self._add_geometric_restraint(state, ps, lower_bound, upper_bound,
1705  sigma, pmi_restraint, self._xz_plane)
1706 
1707  def add_xyradial_restraint(self, state, ps, lower_bound, upper_bound,
1708  sigma, pmi_restraint):
1709  self._add_geometric_restraint(state, ps, lower_bound, upper_bound,
1710  sigma, pmi_restraint, self._z_axis)
1711 
1712  def _add_geometric_restraint(self, state, ps, lower_bound, upper_bound,
1713  sigma, pmi_restraint, geom):
1714  asym = get_asym_mapper_for_state(self, state, self.__asym_states)
1715  r = _GeometricRestraint(
1716  self, state, pmi_restraint, geom, asym.get_feature(ps),
1717  ihm.restraint.LowerUpperBoundDistanceRestraint(lower_bound,
1718  upper_bound),
1719  sigma)
1720  self.system.restraints.append(r)
1721  self._add_restraint_dataset(r) # so that all-dataset group works
1722 
1723  def _get_membrane(self, tor_R, tor_r, tor_th):
1724  """Get an object representing a half-torus membrane"""
1725  if not hasattr(self, '_seen_membranes'):
1726  self._seen_membranes = {}
1727  # If a membrane already exists with all dimensions within 0.01 of
1728  # this one, reuse it
1729  membrane_id = tuple(int(x * 100.) for x in (tor_R, tor_r, tor_th))
1730  if membrane_id not in self._seen_membranes:
1731  m = ihm.geometry.HalfTorus(
1732  center=self._center_origin,
1733  transformation=self._identity_transform,
1734  major_radius=tor_R, minor_radius=tor_r, thickness=tor_th,
1735  inner=True, name='Membrane')
1736  self._seen_membranes[membrane_id] = m
1737  return self._seen_membranes[membrane_id]
1738 
1739  def add_membrane_surface_location_restraint(
1740  self, state, ps, tor_R, tor_r, tor_th, sigma, pmi_restraint):
1741  self._add_membrane_restraint(
1742  state, ps, tor_R, tor_r, tor_th, sigma, pmi_restraint,
1743  ihm.restraint.UpperBoundDistanceRestraint(0.))
1744 
1745  def add_membrane_exclusion_restraint(
1746  self, state, ps, tor_R, tor_r, tor_th, sigma, pmi_restraint):
1747  self._add_membrane_restraint(
1748  state, ps, tor_R, tor_r, tor_th, sigma, pmi_restraint,
1749  ihm.restraint.LowerBoundDistanceRestraint(0.))
1750 
1751  def _add_membrane_restraint(self, state, ps, tor_R, tor_r, tor_th,
1752  sigma, pmi_restraint, rsr):
1753  asym = get_asym_mapper_for_state(self, state, self.__asym_states)
1754  r = _GeometricRestraint(
1755  self, state, pmi_restraint,
1756  self._get_membrane(tor_R, tor_r, tor_th), asym.get_feature(ps),
1757  rsr, sigma)
1758  self.system.restraints.append(r)
1759  self._add_restraint_dataset(r) # so that all-dataset group works
1760 
1761  def add_model(self, group, assembly=None, representation=None):
1762  state = self._last_state
1763  if representation is None:
1764  representation = self.default_representation
1765  protocol = self.all_protocols.get_last_protocol(state)
1766  m = _Model(state.prot, self, protocol,
1767  assembly if assembly else state.modeled_assembly,
1768  representation)
1769  group.append(m)
1770  return m
1771 
1772 
1774  """Get custom python-ihm dumpers for writing PMI to from mmCIF.
1775  This returns a list of custom dumpers that can be passed as all or
1776  part of the `dumpers` argument to ihm.dumper.write(). They add
1777  PMI-specific information to mmCIF or BinaryCIF files written out
1778  by python-ihm."""
1779  return [_ReplicaExchangeProtocolDumper]
1780 
1781 
1783  """Get custom python-ihm handlers for reading PMI data from mmCIF.
1784  This returns a list of custom handlers that can be passed as all or
1785  part of the `handlers` argument to ihm.reader.read(). They read
1786  PMI-specific information from mmCIF or BinaryCIF files read in
1787  by python-ihm."""
1788  return [_ReplicaExchangeProtocolHandler]
1789 
1790 
1791 class GMMParser(ihm.metadata.Parser):
1792  """Extract metadata from an EM density GMM file."""
1793 
1794  def parse_file(self, filename):
1795  """Extract metadata from `filename`.
1796  @return a dict with key `dataset` pointing to the GMM file and
1797  `number_of_gaussians` to the number of GMMs (or None)"""
1798  loc = ihm.location.InputFileLocation(
1799  filename,
1800  details="Electron microscopy density map, "
1801  "represented as a Gaussian Mixture Model (GMM)")
1802  # A 3DEM restraint's dataset ID uniquely defines the mmCIF
1803  # restraint, so we need to allow duplicates
1804  loc._allow_duplicates = True
1805  d = ihm.dataset.EMDensityDataset(loc)
1806  ret = {'dataset': d, 'number_of_gaussians': None}
1807 
1808  with open(filename) as fh:
1809  for line in fh:
1810  if line.startswith('# data_fn: '):
1811  p = ihm.metadata.MRCParser()
1812  fn = line[11:].rstrip('\r\n')
1813  dataset = p.parse_file(os.path.join(
1814  os.path.dirname(filename), fn))['dataset']
1815  ret['dataset'].parents.append(dataset)
1816  elif line.startswith('# ncenters: '):
1817  ret['number_of_gaussians'] = int(line[12:])
1818  return ret
1819 
1820 
1821 def _trim_unrep_termini(entity, asyms, representations):
1822  """Trim Entity sequence to only cover represented residues.
1823 
1824  PDB policy is for amino acid Entity sequences to be polymers (so
1825  they should include any loops or other gaps in the midst of the
1826  sequence) but for the termini to be trimmed of any not-modeled
1827  residues. Here, we modify the Entity sequence to remove any parts
1828  that are not included in any representation. This may change the
1829  numbering if any N-terminal residues are removed, and thus the offset
1830  between PMI and IHM numbering, as both count from 1."""
1831  rep_range = None
1832  for rep in representations:
1833  for seg in rep:
1834  if seg.asym_unit.entity is entity:
1835  seg_range = seg.asym_unit.seq_id_range
1836  if rep_range is None:
1837  rep_range = list(seg_range)
1838  else:
1839  rep_range[0] = min(rep_range[0], seg_range[0])
1840  rep_range[1] = max(rep_range[1], seg_range[1])
1841  # Do nothing if no representations or if everything is represented
1842  if rep_range is None or rep_range == [1, len(entity.sequence)]:
1843  return
1844  # Update offset (= number of unrepresented N terminal entity residues)
1845  # in entity and all asyms
1846  # Force offset to be a real Python int (not numpy.int64) as python-ihm
1847  # up to version 1.8 requires auth_seq_id_map to be int.
1848  pmi_offset = int(rep_range[0]) - 1
1849  entity.pmi_offset = pmi_offset
1850  for asym in asyms:
1851  if asym.entity is entity:
1852  asym.auth_seq_id_map = entity.pmi_offset
1853  # Modify entity sequence to only include represented residues
1854  entity.sequence = entity.sequence[rep_range[0] - 1:rep_range[1]]
1855 
1856  # If N terminal deletions, we must fix the numbering of all segments
1857  # and starting models
1858  if pmi_offset != 0:
1859  seen_models = set()
1860  for rep in representations:
1861  for seg in rep:
1862  if seg.asym_unit.entity is entity:
1863  seg_range = seg.asym_unit.seq_id_range
1864  seg.asym_unit.seq_id_range = (seg_range[0] - pmi_offset,
1865  seg_range[1] - pmi_offset)
1866  if seg.starting_model:
1867  model = seg.starting_model
1868  # Make sure we only modify a given StartingModel
1869  # once (it may be referenced by multiple segments)
1870  if id(model) in seen_models:
1871  continue
1872  seen_models.add(id(model))
1873  seg_range = model.asym_unit.seq_id_range
1874  model.asym_unit.seq_id_range = \
1875  (seg_range[0] - pmi_offset,
1876  seg_range[1] - pmi_offset)
1877  model.offset = model.offset - pmi_offset
Select non water and non hydrogen atoms.
Definition: pdb.h:314
def get_handlers
Get custom python-ihm handlers for reading PMI data from mmCIF.
Definition: mmcif.py:1782
def create_representation
Create a new Representation and return it.
Definition: mmcif.py:1366
def create_transformed_component
Make a new component that's a transformed copy of another.
Definition: mmcif.py:1417
Simple 3D transformation class.
static bool get_is_setup(const IMP::ParticleAdaptor &p)
Definition: Residue.h:158
def exclude_coordinates
Don't record coordinates for the given domain.
Definition: mmcif.py:1376
A decorator to associate a particle with a part of a protein/DNA/RNA.
Definition: Fragment.h:20
def add_uniprot_reference
Add UniProt accession (if available) to the IHM system.
Definition: mmcif.py:1247
def finalize_build
Called immediately after the PMI system is built.
Definition: mmcif.py:1469
Class to encode a modeling protocol as mmCIF.
Definition: mmcif.py:1312
static bool get_is_setup(const IMP::ParticleAdaptor &p)
Definition: atom/Atom.h:245
def parse_file
Extract metadata from filename.
Definition: mmcif.py:1795
def pmi_range
Return a range of IHM residues indexed using PMI numbering.
Definition: mmcif.py:1236
Miscellaneous utilities.
Definition: pmi/tools.py:1
Extract metadata from an EM density GMM file.
Definition: mmcif.py:1791
static bool get_is_setup(const IMP::ParticleAdaptor &p)
Definition: rigid_bodies.h:640
def pmi_residue
Return a single IHM residue indexed using PMI numbering.
Definition: mmcif.py:1282
A single asymmetric unit in the system.
Definition: mmcif.py:1266
A single entity in the system.
Definition: mmcif.py:1203
def set_ensemble_file
Point a previously-created ensemble to an 'all-models' file.
Definition: mmcif.py:1612
Classes to represent data structures used in mmCIF.
Definition: data.py:1
def pmi_range
Return a range of IHM residues indexed using PMI numbering.
Definition: mmcif.py:1286
void add_restraint(RMF::FileHandle fh, Restraint *hs)
static bool get_is_setup(Model *m, ParticleIndex pi)
Definition: Fragment.h:46
Ints get_index(const ParticlesTemp &particles, const Subset &subset, const Subsets &excluded)
def finalize
Do any final processing on the class hierarchy.
Definition: mmcif.py:1483
Base class for capturing a modeling protocol.
Definition: output.py:41
Basic utilities for handling cryo-electron microscopy 3D density maps.
void load_frame(RMF::FileConstHandle file, RMF::FrameID frame)
Load the given RMF frame into the state of the linked objects.
def get_dumpers
Get custom python-ihm dumpers for writing PMI to from mmCIF.
Definition: mmcif.py:1773
Class for easy writing of PDBs, RMFs, and stat files.
Definition: output.py:199
3D rotation class.
Definition: Rotation3D.h:52
Transformation3D get_identity_transformation_3d()
Return a transformation that does not do anything.
Classes for writing output files and processing them.
Definition: output.py:1
A decorator for a residue.
Definition: Residue.h:137
Basic functionality that is expected to be used by a wide variety of IMP users.
def pmi_residue
Return a single IHM residue indexed using PMI numbering.
Definition: mmcif.py:1232
General purpose algebraic and geometric methods that are expected to be used by a wide variety of IMP...
Mapping between FASTA one-letter codes and residue types.
Definition: alphabets.py:1
void link_hierarchies(RMF::FileConstHandle fh, const atom::Hierarchies &hs)
Functionality for loading, creating, manipulating and scoring atomic structures.
Hierarchies get_leaves(const Selection &h)
Select hierarchy particles identified by the biological name.
Definition: Selection.h:70
Select all ATOM and HETATM records with the given chain ids.
Definition: pdb.h:256
Inferential scoring building on methods developed as part of the Inferential Structure Determination ...