summaryrefslogtreecommitdiff
path: root/modelcif/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'modelcif/__init__.py')
-rw-r--r--modelcif/__init__.py785
1 files changed, 785 insertions, 0 deletions
diff --git a/modelcif/__init__.py b/modelcif/__init__.py
new file mode 100644
index 0000000..08e912a
--- /dev/null
+++ b/modelcif/__init__.py
@@ -0,0 +1,785 @@
+import itertools
+import warnings
+import ihm
+from ihm import Entity, AsymUnit, Software, Assembly, Residue # noqa: F401
+from ihm import WaterAsymUnit, AsymUnitRange, _remove_identical # noqa: F401
+import modelcif.data
+
+__version__ = '1.5'
+
+
+class System:
+ """Top-level class representing a complete modeled system.
+
+ :param str title: Longer text description of the system.
+ :param str id: Unique identifier for this system in the mmCIF file.
+ :param database: If this system is part of an official database
+ (e.g. SwissModel, ModBase), details of the database identifiers.
+ :type database: :class:`Database`
+ :param str model_details: Detailed description of the system, like an
+ abstract.
+
+ The system contains a number of simple flat lists of various objects,
+ for example :attr:`alignments`. After constructing objects they should
+ usually be added to these lists so that a hierarchy of classes is
+ formed and is ultimately written out to mmCIF/BinaryCIF. After reading
+ a file the resulting ``System`` object will also populate these lists.
+
+ Most objects do not need to be explicitly added to the system since
+ they are referenced by other objects. For example :class:`Template`
+ objects are not usually added to the system because they are added
+ to alignments which in turn are added to the system. If however an
+ "orphan" Template is desired (not part of an alignment) the system does
+ maintain an appropriate list (``System.templates`` in this case) to
+ which it can be added.
+ """
+
+ structure_determination_methodology = "computational"
+
+ def __init__(self, title=None, id='model', database=None,
+ model_details=None):
+ self.id, self.title = id, title
+ self.database = database
+ self.model_details = model_details
+
+ #: List of plain text comments. These will be added to the top of
+ #: the mmCIF file.
+ self.comments = []
+
+ #: List of all authors of this system, as a list of strings (last name
+ #: followed by initials, e.g. "Smith AJ"). When writing out a file,
+ #: if this list is empty, all authors from the first citation
+ #: (see :attr:`citations` and :class:`ihm.Citation`) are used instead.
+ self.authors = []
+
+ #: List of all grants that supported this work. See :class:`ihm.Grant`.
+ self.grants = []
+
+ #: List of all citations. By convention the first citation describes
+ #: the system itself. See :class:`ihm.Citation`.
+ self.citations = []
+
+ #: Revision/update history. See :class:`ihm.Revision`.
+ self.revisions = []
+
+ #: Information on usage of the data. See :class:`ihm.DataUsage`.
+ self.data_usage = []
+
+ #: All groups of models. See :class:`~modelcif.model.ModelGroup`.
+ self.model_groups = []
+
+ #: All modeling protocols.
+ #: See :class:`~modelcif.protocol.Protocol`.
+ self.protocols = []
+
+ #: All modeling alignments.
+ #: See :mod:`modelcif.alignment`.
+ self.alignments = []
+
+ #: Any additional files with extra data about this system.
+ #: See :class:`modelcif.associated.Repository`.
+ self.repositories = []
+
+ self.entities = []
+ self.asym_units = []
+ self.templates = []
+ self.template_segments = []
+ self.template_transformations = []
+ self.data = []
+ self.data_groups = []
+ self.software = []
+ self.software_groups = []
+ self.assemblies = []
+ self._orphan_chem_comps = []
+
+ # Mapping from ID to QA metric classes
+ self._qa_by_id = {}
+
+ def _all_models(self):
+ """Iterate over all Models in the system"""
+ # todo: raise an error if a model is present in multiple groups?
+ seen_models = set()
+ for group in self._all_model_groups():
+ for model in group:
+ if model in seen_models:
+ continue
+ seen_models.add(model)
+ yield group, model
+
+ def _before_write(self):
+ # Populate flat lists to contain all referenced objects only once
+ # We must populate these in the correct order to get all objects
+ self.assemblies = list(_remove_identical(self._all_assemblies()))
+ self.asym_units = list(_remove_identical(self._all_asym_units()))
+ self.alignments = list(_remove_identical(self.alignments))
+ self.template_segments = list(
+ _remove_identical(self._all_template_segments()))
+ self.templates = list(_remove_identical(self._all_templates()))
+ self.entities = list(_remove_identical(self._all_entities()))
+ self.template_transformations = list(_remove_identical(
+ self._all_template_transformations()))
+ self.data_groups = list(_remove_identical(
+ self._all_data_groups()))
+ self.data = list(_remove_identical(
+ self._all_data()))
+ self.model_groups = list(_remove_identical(self.model_groups))
+ self.software_groups = list(_remove_identical(
+ self._all_software_groups()))
+ self.software = list(_remove_identical(
+ self._all_ref_software()))
+ self._add_missing_reference_sequence()
+
+ def _add_missing_reference_sequence(self):
+ """If any TargetReference has no sequence, use that of the Entity"""
+ for e in self.entities:
+ for r in e.references:
+ if r.sequence is None:
+ r.sequence = "".join(comp.code_canonical
+ for comp in e.sequence)
+
+ def _check_after_write(self):
+ pass
+
+ def _all_template_segments(self):
+ return itertools.chain(
+ self.template_segments,
+ (p.template for aln in self.alignments for p in aln.pairs))
+
+ def _all_templates(self):
+ return itertools.chain(
+ self.templates,
+ (x.template for x in self.template_segments),
+ (x.template for x in self.asym_units
+ if isinstance(x, NonPolymerFromTemplate)))
+
+ def _all_template_transformations(self):
+ return itertools.chain(
+ self.template_transformations,
+ (x.transformation for x in self.templates))
+
+ def _all_citations(self):
+ """Iterate over all Citations in the system.
+ This includes all Citations referenced from other objects, plus
+ any referenced from the top-level system.
+ Duplicates are filtered out."""
+ return _remove_identical(itertools.chain(
+ self.citations,
+ (software.citation for software in self._all_software()
+ if software.citation)))
+
+ def _all_software(self):
+ """Utility method used by ihm.dumper to get all Software. To initially
+ populate this list from all Software referenced in the system,
+ use _all_ref_software() instead."""
+ return self.software
+
+ def _all_ref_software(self):
+ """Iterate over all Software in the system.
+ This includes all Software referenced from other objects, plus
+ any referenced from the top-level system.
+ Duplicates may be present."""
+ def _all_software_in_groups():
+ for sg in self.software_groups:
+ if isinstance(sg, Software):
+ yield sg
+ else:
+ for s in sg:
+ if isinstance(s, SoftwareWithParameters):
+ yield s.software
+ else:
+ yield s
+
+ def _all_entities():
+ return itertools.chain(
+ self.entities, (t.entity for t in self.templates))
+
+ def _all_descriptor_software():
+ comps = frozenset(comp for e in _all_entities()
+ for comp in e.sequence)
+ for comp in comps:
+ if hasattr(comp, 'descriptors') and comp.descriptors:
+ for desc in comp.descriptors:
+ if desc.software:
+ yield desc.software
+ return (itertools.chain(
+ self.software, _all_software_in_groups(),
+ _all_descriptor_software()))
+
+ def _all_assemblies(self):
+ """Iterate over all Assemblies in the system.
+ This includes all Assemblies referenced from other objects, plus
+ any orphaned Assemblies. Duplicates may be present."""
+ return itertools.chain(
+ self.assemblies,
+ (model.assembly for group, model in self._all_models()
+ if model.assembly))
+
+ def _all_asym_units(self):
+ def _all_asym_in_assemblies():
+ for asmb in self.assemblies:
+ for a in asmb:
+ yield a.asym if hasattr(a, 'asym') else a
+ return itertools.chain(
+ self.asym_units, _all_asym_in_assemblies())
+
+ def _all_entities(self):
+ return itertools.chain(
+ self.entities,
+ (asym.entity for asym in self.asym_units))
+
+ def _all_model_groups(self, only_in_states=True):
+ return self.model_groups
+
+ def _all_data(self):
+ def _all_data_in_groups():
+ for dg in self.data_groups:
+ if isinstance(dg, list):
+ for data in dg:
+ yield data
+
+ def _all_data_in_files():
+ for repo in self.repositories:
+ for f in repo.files:
+ if f.data:
+ yield f.data
+ if hasattr(f, 'files'):
+ for subf in f.files:
+ if subf.data:
+ yield subf.data
+ return itertools.chain(
+ self.data,
+ self.templates,
+ self.entities,
+ self.alignments,
+ (model for group, model in self._all_models()),
+ _all_data_in_groups(),
+ _all_data_in_files())
+
+ def _all_data_groups(self):
+ """Return all DataGroup (or singleton Data) objects"""
+ return itertools.chain(
+ self.data_groups,
+ (step.input_data for p in self.protocols for step in p.steps
+ if step.input_data),
+ (step.output_data for p in self.protocols for step in p.steps
+ if step.output_data))
+
+ def _all_software_groups(self):
+ """Return all SoftwareGroup (or singleton Software) objects"""
+ return itertools.chain(
+ self.software_groups,
+ (aln.software for aln in self.alignments if aln.software),
+ (step.software for p in self.protocols for step in p.steps
+ if step.software),
+ (metric.software for group, model in self._all_models()
+ for metric in model.qa_metrics if metric.software))
+
+ def _all_features(self):
+ """Return all Feature objects"""
+ for _, model in self._all_models():
+ for m in model.qa_metrics:
+ if hasattr(m, '_all_features'):
+ for f in m._all_features:
+ yield f
+
+
+# Provide ma-specific docs for Entity
+Entity.__doc__ = """Represent a unique molecular sequence.
+
+This can be used both for template sequences (in which case the Entity is
+then used in a :class:`Template` object) or for target (model) sequences
+(where it is used in a :class:`AsymUnit` object).
+
+(Note that template sequence Entity objects are not written out to the
+entity, entity_poly etc. tables in the mmCIF/BinaryCIF file by default.
+Instead, sequence information is captured in template-specific categories.)
+
+:param sequence sequence: The primary sequence, as a sequence of
+ :class:`ihm.ChemComp` objects, and/or codes looked up in `alphabet`.
+ See `ihm.Entity <https://python-ihm.readthedocs.io/en/latest/main.html#ihm.Entity>`_ for examples.
+:param alphabet: The mapping from code to chemical components to use
+ (it is not necessary to instantiate this class).
+:type alphabet: :class:`ihm.Alphabet`
+:param str description: A short text name for the sequence.
+:param str details: Longer text describing the sequence.
+:param source: The method by which the sample for this entity was produced.
+:type source: :class:`ihm.source.Source`
+:param references: For a target (model) sequence, information about this
+ entity stored in external databases (for example the sequence in
+ UniProt). For references to structure databases for templates,
+ see :class:`Template` instead.
+:type references: sequence of :class:`reference.TargetReference` objects
+
+See `ihm.Entity <https://python-ihm.readthedocs.io/en/latest/main.html#ihm.Entity>`_ for more information.
+""" # noqa: E501
+
+
+# Provide ma-specific docs for Software
+Software.__doc__ = """Software used as part of the modeling protocol.
+
+:param str name: The name of the software.
+:param str classification: The major function of the software, for
+ example 'model building', 'sample preparation', 'data collection'.
+:param str description: A longer text description of the software.
+:param str location: Place where the software can be found (e.g. URL).
+:param str type: Type of software (program/package/library/other).
+:param str version: The version used.
+:param citation: Publication describing the software.
+:type citation: :class:`ihm.Citation`
+
+Generally these objects are added to groups (see :class:`SoftwareGroup`)
+which can then be used to describe the software used in various parts of the
+modeling (``Software`` objects can also be used any place
+:class:`SoftwareGroup` are accepted, in which case they will act as if a group
+containing only a single member was used).
+
+See also :attr:`System.software`.
+"""
+
+# Provide ma-specific docs for Assembly
+Assembly.__doc__ = """A collection of parts of the system that were modeled
+together.
+
+:param sequence elements: Initial set of parts of the system.
+:param str name: Short text name of this assembly.
+:param str description: Longer text that describes this assembly.
+
+This is implemented as a simple list of asymmetric units (or parts of
+them), i.e. a list of :class:`AsymUnit` and/or :class:`AsymUnitRange`
+objects. An Assembly is typically passed to the :class:`modelcif.model.Model`
+constructor.
+
+Note that the ModelCIF dictionary has deprecated the corresponding
+``ma_struct_assembly`` category, so any name or description of the assembly
+will not be written to the mmCIF file. The ModelCIF dictionary requires that
+all models have the same composition.
+"""
+
+
+class Database:
+ """Information about a System that is part of an official database.
+
+ If a :class:`System` is part of an official database (e.g. SwissModel,
+ ModBase), this class contains details of the database identifiers.
+ It should be passed to the :class:`System` constructor.
+
+ :param str id: Abbreviated name of the database (e.g. PDB)
+ :param str code: Identifier from the database (e.g. 1abc)
+ """
+ def __init__(self, id, code):
+ self.id, self.code = id, code
+
+
+class SoftwareGroup(list):
+ """A number of :class:`Software` and/or :class:`SoftwareWithParameters`
+ objects that are grouped together.
+
+ This class can be used to group together multiple :class:`Software`
+ objects if multiple pieces of software were used together to generate
+ a single alignment (see :class:`modelcif.alignment.AlignmentMode`), to
+ run a modeling step (see :class:`modelcif.protocol.Step`), or to
+ calculate a model quality score (see :mod:`modelcif.qa_metric`).
+ It behaves like a regular Python list.
+
+ :class:`SoftwareWithParameters` allows including both a piece of
+ software, and the parameters with which it was used, in the group.
+
+ :param sequence elements: Initial set of :class:`Software`
+ and/or :class:`SoftwareWithParameters` objects.
+ """
+
+ def __init__(self, elements=(), parameters=None):
+ super(SoftwareGroup, self).__init__(elements)
+ if parameters:
+ warnings.warn(
+ "Parameters for SofwareGroup are ignored. To specify "
+ "parameters for a piece of software, use the "
+ "SoftwareWithParameters class.")
+ self.parameters = [] if parameters is None else parameters
+
+
+class SoftwareWithParameters:
+ """A piece of software and the parameters with which it was used.
+
+ See :class:`SoftwareGroup`.
+
+ :param software: The software that was used.
+ :type software: :class:`modelcif.Software`
+ :param sequence parameters: sequence of parameters for the software,
+ as :class:`SoftwareParameter` objects.
+ """
+ def __init__(self, software, parameters=None):
+ self.software = software
+ self.parameters = [] if parameters is None else parameters
+
+ # Pass Software-specific fields through
+ name = property(lambda self: self.software.name)
+ classification = property(lambda self: self.software.classification)
+ description = property(lambda self: self.software.description)
+ location = property(lambda self: self.software.location)
+ type = property(lambda self: self.software.type)
+ version = property(lambda self: self.software.version)
+ citation = property(lambda self: self.software.citation)
+
+
+class SoftwareParameter:
+ """A single parameter given to software used in modeling.
+
+ See :class:`SoftwareWithParameters`, :class:`SoftwareGroup`.
+
+ :param str name: A short name for this parameter.
+ :param value: Parameter value.
+ :type value: ``int``, ``float``, ``str``, ``bool``, list of ``int``,
+ or list of ``float``.
+ :param str description: A longer description of the parameter.
+ """
+ def __init__(self, name, value, description=None):
+ self.name, self.value = name, value
+ self.description = description
+
+ def __repr__(self):
+ return ("<SoftwareParameter(name=%r, value=%r)>"
+ % (self.name, self.value))
+
+
+class Transformation:
+ """Rotation and translation applied to an object.
+
+ These objects are generally used to record the transformation that
+ was applied to a :class:`Template` to generate the starting structure
+ used in modeling.
+
+ :param rot_matrix: Rotation matrix (as a 3x3 array of floats) that
+ places the object in its final position.
+ :param tr_vector: Translation vector (as a 3-element float list) that
+ places the object in its final position.
+ """
+ def __init__(self, rot_matrix, tr_vector):
+ self.rot_matrix, self.tr_vector = rot_matrix, tr_vector
+
+ """Return the identity transformation.
+
+ :return: A new identity Transformation.
+ :rtype: :class:`Transformation`
+ """
+ @classmethod
+ def identity(cls):
+ if not hasattr(cls, '_identity_obj'):
+ cls._identity_obj = cls(
+ [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]], [0., 0., 0.])
+ return cls._identity_obj
+
+
+class TemplateSegment:
+ """An aligned part of a template (see :class:`modelcif.alignment.Pair`).
+
+ Usually these objects are created from
+ a :class:`Template` using :meth:`Template.segment`, e.g. to get a
+ segment covering residues 1 through 3 in `tmpl` use::
+
+ tmpl = modelcif.Template(entity, ...)
+ seg = tmpl.segment('--ACG', 1, 3)
+ """
+ def __init__(self, template, gapped_sequence, seq_id_begin, seq_id_end):
+ self.template = template
+ self.gapped_sequence = gapped_sequence
+ self.seq_id_range = (seq_id_begin, seq_id_end)
+
+
+class _TemplateBase(modelcif.data.Data):
+ """Base class for all templates; use Template or CustomTemplate"""
+
+ data_content_type = "template structure"
+
+ def __init__(self, entity, asym_id, model_num, transformation,
+ name=None, strand_id=None, entity_id=None):
+ super(_TemplateBase, self).__init__(name)
+ self.entity = entity
+ self.asym_id, self.model_num = asym_id, model_num
+ self.transformation = transformation
+ self._strand_id = strand_id
+ self.entity_id = entity_id
+
+ def segment(self, gapped_sequence, seq_id_begin, seq_id_end):
+ """Get an object representing the alignment of part of this sequence.
+
+ :param str gapped_sequence: Sequence of the segment, including gaps.
+ :param int seq_id_begin: Start of the segment.
+ :param int seq_id_end: End of the segment.
+ """
+ # todo: cache so we return the same object for same parameters
+ return TemplateSegment(self, gapped_sequence, seq_id_begin, seq_id_end)
+
+ seq_id_range = property(lambda self: self.entity.seq_id_range,
+ doc="Sequence range")
+
+ template = property(lambda self: self)
+
+ strand_id = property(lambda self: self._strand_id or self.asym_id,
+ doc="PDB or author-provided strand/chain ID")
+
+
+class Template(_TemplateBase):
+ """A single database chain that was used as a template structure
+ for modeling.
+
+ After creating a polymer template, use :meth:`segment` to denote the
+ part of its sequence used in any modeling alignments
+ (see :class:`modelcif.alignment.Pair`).
+
+ Non-polymer templates do not have alignments, and should instead be
+ passed to one or more :class:`NonPolymerFromTemplate` objects.
+
+ Template objects can also be used as inputs or outputs in modeling
+ protocol steps; see :class:`modelcif.protocol.Step`.
+
+ This class is intended for templates that were taken from reference
+ databases such as PDB. For a non-deposited "custom" template,
+ use the :class:`CustomTemplate` class instead.
+
+ :param entity: The sequence of the chain.
+ :type entity: :class:`Entity`
+ :param str asym_id: The asym or chain ID in the template structure.
+ :param int model_num: The model number of the template structure.
+ :param transformation: Rotation and translation applied to the original
+ template structure to get the starting model used in modeling.
+ :type transformation: :class:`Transformation`
+ :param str name: A short name for this template.
+ :param references: A list of pointers to reference databases (such as
+ PDB) from which the template structure was taken.
+ :type references: list of :class:`modelcif.reference.TemplateReference`
+ objects
+ :param str strand_id: PDB or "author-provided" strand/chain ID.
+ If not specified, it will be the same as the regular asym_id.
+ :param str entity_id: If known, the ID of the entity for this template
+ in its own mmCIF file.
+ """
+
+ def __init__(self, entity, asym_id, model_num, transformation,
+ name=None, references=[], strand_id=None, entity_id=None):
+ super(Template, self).__init__(
+ entity=entity, asym_id=asym_id, model_num=model_num,
+ transformation=transformation, name=name, strand_id=strand_id,
+ entity_id=entity_id)
+ self.references = []
+ self.references.extend(references)
+
+
+class CustomTemplate(_TemplateBase):
+ """A chain that was used as a template structure for modeling.
+
+ This class is intended for templates that have not been deposited
+ in a database such as PDB (for deposited templates, use the
+ :class:`Template` class instead). The coordinates of the atoms
+ in these "custom" templates will be included in the mmCIF file;
+ see the :attr:`atoms` member.
+
+ :param str details: Information on how the template was created.
+
+ See :class:`Template` for a description of the other parameters.
+ """
+ def __init__(self, entity, asym_id, model_num, transformation,
+ name=None, strand_id=None, entity_id=None, details=None):
+ super(CustomTemplate, self).__init__(
+ entity=entity, asym_id=asym_id, model_num=model_num,
+ transformation=transformation, name=name, strand_id=strand_id,
+ entity_id=entity_id)
+ self.details = details
+
+ #: Coordinates of all atoms as :class:`TemplateAtom` objects
+ self.atoms = []
+
+
+class TemplateAtom:
+ """Coordinates of a single atom in a custom template.
+
+ This provides the coordinates for a template that has not been
+ deposited in a database. See :class:`CustomTemplate` for more
+ information. These objects are added to the
+ :attr:`CustomTemplate.atoms` list.
+
+ :param int seq_id: The sequence ID of the residue represented by this
+ atom. This should generally be a number starting at 1 for any
+ polymer chain, water, or oligosaccharide. For ligands, a seq_id
+ is not needed (as a given asym can only contain a single ligand),
+ so either 1 or None can be used.
+ :param str atom_id: The name of the atom in the residue
+ :param str type_symbol: Element name
+ :param float x: x coordinate of the atom
+ :param float y: y coordinate of the atom
+ :param float z: z coordinate of the atom
+ :param bool het: True for HETATM sites, False (default) for ATOM
+ :param float biso: Temperature factor or equivalent (if applicable)
+ :param float occupancy: Fraction of the atom type present
+ (if applicable)
+ :param float charge: Formal charge (if applicable)
+ :param int auth_seq_id: Author-provided sequence ID (if applicable;
+ this is optional for polymers but required for ligands).
+ :param str auth_atom_id: Author-provided atom name (if needed)
+ :param str auth_comp_id: Author-provided residue name (if needed)
+ """
+
+ # Reduce memory usage
+ __slots__ = ['seq_id', 'atom_id', 'type_symbol', 'x', 'y', 'z', 'het',
+ 'biso', 'occupancy', 'charge', 'auth_seq_id', 'auth_atom_id',
+ 'auth_comp_id']
+
+ def __init__(self, seq_id, atom_id, type_symbol, x, y, z,
+ het=False, biso=None, occupancy=None, charge=None,
+ auth_seq_id=None, auth_atom_id=None, auth_comp_id=None):
+ self.seq_id, self.atom_id = seq_id, atom_id
+ self.type_symbol = type_symbol
+ self.x, self.y, self.z = x, y, z
+ self.het, self.biso = het, biso
+ self.occupancy, self.charge = occupancy, charge
+ self.auth_seq_id = auth_seq_id
+ self.auth_atom_id, self.auth_comp_id = auth_atom_id, auth_comp_id
+
+
+class NonPolymerFromTemplate(AsymUnit):
+ """A non-polymer (e.g. ligand) in the model that is modeled from
+ a non-polymer template.
+
+ These objects act just like :class:`AsymUnit` and should be added
+ to :class:`Assembly`.
+
+ To represent a non-polymer that is modeled without a template, just
+ use a regular :class:`AsymUnit`.
+
+ :param template: The non-polymer template used to model
+ this non-polymer.
+ :type template: :class:`Template`
+ :param bool explicit: True iff the conformation of the template is
+ allowed to change (e.g. bond relaxation, flexible fitting)
+ during the modeling, or False if the template is treated as
+ a single rigid body.
+
+ For the other parameters, see :class:`AsymUnit`.
+ """
+
+ def __init__(self, template, explicit, details=None, auth_seq_id_map=0,
+ id=None, strand_id=None):
+ super(NonPolymerFromTemplate, self).__init__(
+ template.entity, details=details, auth_seq_id_map=auth_seq_id_map,
+ id=id, strand_id=strand_id)
+ self.template, self.explicit = template, explicit
+
+
+class ReferenceDatabase(modelcif.data.Data):
+ """A reference database used in the modeling. This is typically a
+ sequence database used for template search, alignments, etc.
+ These objects are passed as input or output to
+ :class:`modelcif.protocol.Step`. See also :class:`modelcif.data.Data`
+ for more details.
+
+ Compare with :class:`modelcif.reference.TargetReference`, which pertains
+ to just the modeled sequence itself; this class describes *multiple*
+ sequences.
+
+ :param str name: Name of the database.
+ :param str url: Location of the database.
+ :param str version: Version of the database.
+ :param release_date: Release date of the specified version.
+ :type release_date: :class:`datetime.date`
+ """
+ data_content_type = "reference database"
+
+ def __init__(self, name, url, version=None, release_date=None):
+ super(ReferenceDatabase, self).__init__(name)
+ self.url, self.version, self.release_date = url, version, release_date
+
+
+class Feature:
+ """Base class for selecting parts of the system.
+ This class should not be used itself; instead,
+ see :class:`AtomFeature`, :class:`PolyResidueFeature`,
+ and :class:`EntityInstanceFeature`.
+
+ Generally it is expected that the entities selected by a given
+ feature are all of the same type. For example, a feature should
+ not select both a ligand and a polymer.
+
+ Features are typically used in QA metrics, passed to
+ :class:`modelcif.qa_metric.Feature` or
+ :class:`modelcif.qa_metric.FeaturePairwise` objects.
+ """
+ details = None
+ type = ihm.unknown
+
+ def _get_entity_type(self, check=False):
+ return ihm.unknown
+
+ def _check_entity_types(self, types, check):
+ if check:
+ if len(types) > 1:
+ raise ValueError(
+ "Feature %r selects entities of multiple types: %s"
+ % (self, list(types)))
+ elif len(types) == 0:
+ raise ValueError("Feature %r doesn't select anything" % self)
+ return list(types)[0] if len(types) == 1 else 'other'
+
+
+class AtomFeature(Feature):
+ """Selection of one or more atoms from the system. See :class:`Feature`
+ for more information.
+
+ Note that currently support for atom features in python-modelcif
+ is rather rudimentary. They must be selected by their "id", not by
+ the Atom Python object.
+
+ :param sequence atoms: A list of atom indices (usually integers).
+ :param str details: Additional text describing this feature.
+ """
+
+ type = 'atom'
+
+ def __init__(self, atoms, details=None):
+ self.atoms, self.details = atoms, details
+
+ def _get_entity_type(self, check=False):
+ # We currently can't tell what type entity the atom IDs refer to
+ return 'other'
+
+ def _signature(self):
+ return tuple(self.atoms)
+
+
+class PolyResidueFeature(Feature):
+ """Selection of one or more polymer residues from the system.
+ See :class:`Feature` for more information.
+
+ :param sequence residues: A list of :class:`Residue` objects.
+ :param str details: Additional text describing this feature.
+ """
+ type = 'residue'
+
+ def __init__(self, residues, details=None):
+ self.residues, self.details = residues, details
+
+ def _get_entity_type(self, check=False):
+ types = frozenset(x.entity.type for x in self.residues)
+ return self._check_entity_types(types, check)
+
+ def _signature(self):
+ return tuple(self.residues)
+
+
+class EntityInstanceFeature(Feature):
+ """Selection of one or more asyms from the system.
+ See :class:`Feature` for more information.
+
+ :param sequence asym_units: A list of :class:`AsymUnit` objects.
+ :param str details: Additional text describing this feature.
+ """
+ type = 'entity instance'
+
+ def __init__(self, asym_units, details=None):
+ self.asym_units, self.details = asym_units, details
+
+ def _get_entity_type(self, check=False):
+ types = frozenset(x.entity.type for x in self.asym_units)
+ return self._check_entity_types(types, check)
+
+ def _signature(self):
+ return tuple(self.asym_units)