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