Source code for vermouth.forcefield

# Copyright 2018 University of Groningen
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Provides a class used to describe a forcefield and all associated data.
"""


import itertools
from glob import glob
import os
from .gmx.rtp import read_rtp
from .ffinput import read_ff
from .citation_parser import read_bib
from . import DATA_PATH

FORCE_FIELD_PARSERS = {'.rtp': read_rtp, '.ff': read_ff, '.bib': read_bib}

# Cache the force fields.
# It should only be used by the get_native_force_field function, else it would
# allow to request a "native" force field that is not actually native.
_FORCE_FIELDS = {}


[docs] class ForceField: """ Description of a force field. A force field can be created empty or read from a directory. In any case, a force field must be named. If read from a directory, the base name of the directory is used as force field name, unless the `name` attribute is provided. If the force field is created empty, then `name` must be provided. Parameters ---------- directory: str or pathlib.Path, optional A directory to read the force field from. name: str, optional The name of the force field. Attributes ---------- blocks: dict links: list modifications: dict renamed_residues: dict name: str variables: dict """ def __init__(self, directory=None, name=None): self.blocks = {} self.links = [] self.modifications = {} self.renamed_residues = {} self.variables = {} self.name = None self.citations = {} if directory is not None: self.read_from(directory) self.name = os.path.basename(str(directory)) if name is not None: self.name = name if self.name is None: msg = 'At least one of `directory` or `name` must be provided.' raise TypeError(msg)
[docs] def read_from(self, directory): """ Populate or update the force field from a directory. The provided directory must contain a subdirectory with the same name as the force field. """ source_files = iter_force_field_files(directory) for source in source_files: extension = os.path.splitext(source)[-1] with open(source) as infile: FORCE_FIELD_PARSERS[extension](infile, self)
@property def reference_graphs(self): """ Returns all known blocks. Returns ------- dict """ return self.blocks @property def features(self): """ List the features declared by the links. Returns ------- set """ return set(feature for link in self.links for feature in link.features)
[docs] def has_feature(self, feature): """ Test if a feature is declared by the links. Parameters ---------- feature: str The name of the feature of interest. Returns ------- bool """ return feature in self.features
[docs] def find_force_fields(directory, force_fields=None): """ Read all the force fields in the given directory. A force field is defined as a directory that contains at least one RTP file. The name of the force field is the base name of the directory. If the force field argument is not ``None``, then it must be a dictionary with force field names as keys and instances of :class:`ForceField` as values. The force fields in the dictionary will be updated if force fields with the same names are found in the directory. Parameters ---------- directory: pathlib.Path or str The path to the directory containing the force fields. force_fields: dict A dictionary of force fields to update. Returns ------- dict A dictionary of force fields read or updated. Keys are force field names as strings, and values are instances of :class:`ForceField`. If a dictionary was provided as the "force_fields" argument, then the returned dictionary is the same instance as the one provided but with updated content. """ if force_fields is None: force_fields = {} directory = str(directory) # Py<3.6 compliance for name in os.listdir(directory): path = os.path.join(directory, name) try: next(iter_force_field_files(path)) except StopIteration: pass else: try: if name not in force_fields: force_fields[name] = ForceField(path) else: force_fields[name].read_from(path) except IOError: msg = 'An error occured while reading the force field in "{}".' raise IOError(msg.format(path)) return force_fields
[docs] def iter_force_field_files(directory, extensions=FORCE_FIELD_PARSERS.keys()): """ Returns a generator over the path of all the force field files in the directory. """ return itertools.chain(*( glob(os.path.join(str(directory), '*' + extension)) for extension in extensions ))
[docs] def get_native_force_field(name): """ Get a force field from the distributed library knowing its name. Parameters ---------- name: str The name of the requested force field. Returns ------- ForceField Raises ------ KeyError There is no force field with the requested name in the distributed library. """ # This function is a *temporary* solution. It reads all the distributed # force fields and keep a cache of them. A better solution would only parse # the requested force field! There would still be a need to cache the read # force fields, though. Indeed, force field comparison is based on instance # identity,so we want each force field to be singleton. # TODO: Implement a better way to request a force field by name. global _FORCE_FIELDS try: return _FORCE_FIELDS[name] except KeyError: _FORCE_FIELDS = find_force_fields(os.path.join(DATA_PATH, 'force_fields')) return _FORCE_FIELDS[name]