# 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.
"""
Read .ff files.
The FF file format describes molecule components for a given force field. It is
a test format devised for quick prototyping.
The format is built on top of a subset of the ITP format. Describing a block
is done in the same way an ITP file describes a molecule.
"""
import collections
import copy
import logging
import numbers
import json
from .molecule import (
Block, Link, Modification,
Interaction, DeleteInteraction,
Choice, NotDefinedOrNot,
ParamDistance, ParamAngle, ParamDihedral, ParamDihedralPhase,
)
from .parser_utils import (
SectionLineParser, _tokenize, _substitute_macros, _parse_macro
)
from .log_helpers import StyleAdapter, get_logger
import networkx as nx
# Python 3.4 does not raise JSONDecodeError but ValueError.
try:
from json import JSONDecodeError
except ImportError:
JSONDecodeError = ValueError
LOGGER = StyleAdapter(get_logger(__name__))
VALUE_PREDICATES = {
'not': NotDefinedOrNot,
}
PARAMETER_EFFECTORS = {
'dist': ParamDistance,
'angle': ParamAngle,
'dihedral': ParamDihedral,
'dihphase': ParamDihedralPhase,
}
[docs]
class FFDirector(SectionLineParser):
COMMENT_CHAR = ';'
interactions_natoms = {
'bonds': 2,
'angles': 3,
'dihedrals': 4,
'impropers': 4,
'constraints': 2,
'pairs': 2,
'pairs_nb': 2,
'SETTLE': 1,
'virtual_sites2': 3,
'virtual_sites3': 4,
'virtual_sites4': 5,
'position_restraints': 1,
'distance_restraints': 2,
'dihedral_restraints': 4,
'orientation_restraints': 2,
'angle_restraints': 4,
'angle_restraints_z': 2
}
def __init__(self, force_field):
super().__init__()
self.force_field = force_field
self.current_block = None
self.current_link = None
self.current_modification = None
self.blocks = collections.OrderedDict()
self.links = []
self.modifications = []
self.citations = set()
self.header_actions = {
('moleculetype', ): self._new_block,
('link', ): self._new_link,
('modification', ): self._new_modification,
}
[docs]
def finalize_section(self, previous_section, ended_section):
"""
Called once a section is finished. It appends the current_links list
to the links and update the block dictionary with current_block. Thereby it
finishes the reading a given section.
Parameters
---------
previous_section: list[str]
The last parsed section.
ended_section: list[str]
The sections that have been ended.
"""
if self.current_block is not None:
# add FF wide citations
self.current_block.citations.update(self.citations)
self.current_block.make_edges_from_interactions()
self.force_field.blocks[self.current_block.name] = self.current_block
if self.current_link is not None:
# add FF wide citations
self.current_link.citations.update(self.citations)
self.current_link.make_edges_from_interactions()
self.force_field.links.append(self.current_link)
if self.current_modification is not None:
# add FF wide citations
if self.current_modification and not nx.is_connected(self.current_modification):
LOGGER.error('Modification {} in force field {} is not a single connected component',
self.current_modification.name, self.force_field.name)
self.current_modification.citations.update(self.citations)
self.force_field.modifications[self.current_modification.name] = self.current_modification
[docs]
def get_context(self, context_type=''):
possible_contexts = {
'block': self.current_block,
'link': self.current_link,
'molmeta': self.current_link,
'modification': self.current_modification,
}
return possible_contexts[context_type]
[docs]
def has_context(self):
open_contexts = [
self.current_block, self.current_link, self.current_modification]
return open_contexts != ([None] * len(open_contexts))
def _new_block(self):
self.current_block = Block(force_field=self.force_field)
def _new_link(self):
self.current_link = Link(force_field=self.force_field)
def _new_modification(self):
self.current_modification = Modification(force_field=self.force_field)
@SectionLineParser.section_parser('variables')
def _variables(self, line, lineno=0):
if self.has_context():
raise IOError('The [variables] section must be defined '
'before the blocks, links, and modifications.')
tokens = _tokenize(line)
_parse_variables(tokens, self.force_field, 'variables')
@SectionLineParser.section_parser('moleculetype')
def _block(self, line, lineno=0):
name, nrexcl = line.split()
self.current_block.name = name
self.current_block.nrexcl = int(nrexcl)
@SectionLineParser.section_parser('moleculetype', 'atoms')
def _block_atoms(self, line, lineno=0):
tokens = collections.deque(_tokenize(line))
_parse_block_atom(tokens, self.current_block)
@SectionLineParser.section_parser('moleculetype', 'meta')
def _parse_block_meta(self, line, lineno=0):
"""
Parse the meta section and update the object current_block with meta information.
Allow the dictionnary value to be None (in case of flag), string, int or float.
Example :
[meta]
flag1
key value
key2 value1 0.37
will give :
{
'flag1' : None,
'key': 'value',
'key2': ['value1' , '0.37']
}
Parameters
----------
line: str
lineno: str
"""
split_line = line.split()
key = split_line[0]
# depend of the number of value(s)
if len(split_line[1:]) == 0 :
value = None
elif len(split_line[1:]) == 1 :
value = split_line[1]
else:
value = split_line[1:]
self.current_block.meta[key] = value
@SectionLineParser.section_parser('moleculetype', 'edges',
negate=False, context_type='block')
@SectionLineParser.section_parser('moleculetype', 'non-edges',
negate=True, context_type='block')
@SectionLineParser.section_parser('link', 'edges',
negate=False, context_type='link')
@SectionLineParser.section_parser('link', 'non-edges',
negate=True, context_type='link')
@SectionLineParser.section_parser('modification', 'edges',
negate=False, context_type='modification')
def _edges(self, line, lineno=0, negate=False, context_type=''):
context = self.get_context(context_type)
tokens = collections.deque(_tokenize(line))
_parse_edges(tokens, context, context_type, negate=negate)
@SectionLineParser.section_parser('moleculetype', 'bonds', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'angles', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'impropers', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'constraints', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'pairs', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'exclusions', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'pairs_nb', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'SETTLE', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'virtual_sites2', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'virtual_sites3', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'virtual_sites4', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'virtual_sitesn', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'position_restraints', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'distance_restraints', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'dihedral_restraints', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'orientation_restraints', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'angle_restraints', context_type='block')
@SectionLineParser.section_parser('moleculetype', 'angle_restraints_z', context_type='block')
@SectionLineParser.section_parser('link', 'bonds', context_type='link')
@SectionLineParser.section_parser('link', 'angles', context_type='link')
@SectionLineParser.section_parser('link', 'impropers', context_type='link')
@SectionLineParser.section_parser('link', 'constraints', context_type='link')
@SectionLineParser.section_parser('link', 'pairs', context_type='link')
@SectionLineParser.section_parser('link', 'exclusions', context_type='link')
@SectionLineParser.section_parser('link', 'pairs_nb', context_type='block')
@SectionLineParser.section_parser('link', 'SETTLE', context_type='link')
@SectionLineParser.section_parser('link', 'virtual_sites2', context_type='link')
@SectionLineParser.section_parser('link', 'virtual_sites3', context_type='link')
@SectionLineParser.section_parser('link', 'virtual_sites4', context_type='link')
@SectionLineParser.section_parser('link', 'virtual_sitesn', context_type='link')
@SectionLineParser.section_parser('link', 'position_restraints', context_type='link')
@SectionLineParser.section_parser('link', 'distance_restraints', context_type='link')
@SectionLineParser.section_parser('link', 'dihedral_restraints', context_type='link')
@SectionLineParser.section_parser('link', 'orientation_restraints', context_type='link')
@SectionLineParser.section_parser('link', 'angle_restraints', context_type='link')
@SectionLineParser.section_parser('link', 'angle_restraints_z', context_type='link')
@SectionLineParser.section_parser('link', '!bonds', context_type='link')
@SectionLineParser.section_parser('link', '!angles', context_type='link')
@SectionLineParser.section_parser('link', '!impropers', context_type='link')
@SectionLineParser.section_parser('link', '!constraints', context_type='link')
@SectionLineParser.section_parser('link', '!pairs', context_type='link')
@SectionLineParser.section_parser('link', '!exclusions', context_type='link')
@SectionLineParser.section_parser('link', '!pairs_nb', context_type='link')
@SectionLineParser.section_parser('link', '!SETTLE', context_type='link')
@SectionLineParser.section_parser('link', '!virtual_sites2', context_type='link')
@SectionLineParser.section_parser('link', '!virtual_sites3', context_type='link')
@SectionLineParser.section_parser('link', '!virtual_sites4', context_type='link')
@SectionLineParser.section_parser('link', '!virtual_sitesn', context_type='link')
@SectionLineParser.section_parser('link', '!position_restraints', context_type='link')
@SectionLineParser.section_parser('link', '!distance_restraints', context_type='link')
@SectionLineParser.section_parser('link', '!dihedral_restraints', context_type='link')
@SectionLineParser.section_parser('link', '!orientation_restraints', context_type='link')
@SectionLineParser.section_parser('link', '!angle_restraints', context_type='link')
@SectionLineParser.section_parser('link', '!angle_restraints_z', context_type='link')
@SectionLineParser.section_parser('modification', 'bonds', context_type='modification')
@SectionLineParser.section_parser('modification', 'angles', context_type='modification')
@SectionLineParser.section_parser('modification', 'impropers', context_type='modification')
@SectionLineParser.section_parser('modification', 'constraints', context_type='modification')
@SectionLineParser.section_parser('modification', 'pairs', context_type='modification')
@SectionLineParser.section_parser('modification', 'exclusions', context_type='modification')
@SectionLineParser.section_parser('modification', 'pairs_nb', context_type='modification')
@SectionLineParser.section_parser('modification', 'SETTLE', context_type='modification')
@SectionLineParser.section_parser('modification', 'virtual_sites2', context_type='modification')
@SectionLineParser.section_parser('modification', 'virtual_sites3', context_type='modification')
@SectionLineParser.section_parser('modification', 'virtual_sites4', context_type='modification')
@SectionLineParser.section_parser('modification', 'virtual_sitesn', context_type='modification')
@SectionLineParser.section_parser('modification', 'position_restraints', context_type='modification')
@SectionLineParser.section_parser('modification', 'distance_restraints', context_type='modification')
@SectionLineParser.section_parser('modification', 'dihedral_restraints', context_type='modification')
@SectionLineParser.section_parser('modification', 'orientation_restraints', context_type='modification')
@SectionLineParser.section_parser('modification', 'angle_restraints', context_type='modification')
@SectionLineParser.section_parser('modification', 'angle_restraints_z', context_type='modification')
def _interactions(self, line, lineno=0, context_type=''):
context = self.get_context(context_type)
interaction_name = self.section[-1]
delete = False
if interaction_name.startswith('!'):
interaction_name = interaction_name[1:]
delete = True
tokens = collections.deque(_tokenize(line))
if tokens[0] == '#meta':
_parse_meta(
tokens,
context,
context_type=context_type,
section=interaction_name,
)
else:
n_atoms = self.interactions_natoms.get(interaction_name)
_base_parser(
tokens,
context,
context_type=context_type,
section=interaction_name,
natoms=n_atoms,
delete=delete,
)
@SectionLineParser.section_parser('link', 'dihedrals', context_type='link')
@SectionLineParser.section_parser('link', '!dihedrals', context_type='link')
@SectionLineParser.section_parser('moleculetype', 'dihedrals', context_type='block')
@SectionLineParser.section_parser('modification', 'dihedrals', context_type='modification')
def _dih_interactions(self, line, lineno=0, context_type=''):
context = self.get_context(context_type)
interaction_name = self.section[-1]
delete = False
tokens = collections.deque(_tokenize(line))
if tokens[0] == '#meta':
_parse_meta(
tokens,
context,
context_type=context_type,
section=interaction_name,
)
else:
n_atoms = self.interactions_natoms.get(interaction_name)
_base_parser(
tokens,
context,
context_type=context_type,
section=interaction_name,
natoms=n_atoms,
delete=delete,
)
# Because of how they are described in gromacs, proper and improper
# dihedral angles are all under the [ dihedrals ] section. However
# the way they are treated differently in the library, at least on how
# they generate edges. Here we move the all the impropers into their own
# [ impropers ] section.
propers = []
impropers = context.interactions.get('impropers', [])
for dihedral in context.interactions.get('dihedrals', []):
if dihedral.parameters and dihedral.parameters[0] == '2':
impropers.append(dihedral)
else:
propers.append(dihedral)
context.interactions['dihedrals'] = propers
context.interactions['impropers'] = impropers
@SectionLineParser.section_parser('moleculetype', 'patterns')
@SectionLineParser.section_parser('moleculetype', 'features')
@SectionLineParser.section_parser('moleculetype', 'non-edge')
@SectionLineParser.section_parser('modification', 'non-edge')
def _invalid_out_of_link(self, line, lineno=0):
raise IOError('The "{}" section is only valid in links.'
.format(self.section[-1]))
@SectionLineParser.section_parser('link', context_type='link')
@SectionLineParser.section_parser('link', 'molmeta', context_type='molmeta')
def _link(self, line, lineno=0, context_type=''):
tokens = collections.deque(_tokenize(line))
_parse_link_attribute(tokens, self.current_link, context_type)
@SectionLineParser.section_parser('modification')
def _modification(self, line, lineno=0):
self.current_modification.name = line
@SectionLineParser.section_parser('link', 'atoms')
def _link_atoms(self, line, lineno=0):
tokens = collections.deque(_tokenize(line))
_parse_link_atom(tokens, self.current_link)
@SectionLineParser.section_parser('modification', 'atoms')
def _modification_atoms(self, line, lineno=0):
tokens = collections.deque(_tokenize(line))
_parse_link_atom(tokens, self.current_modification,
defaults={'PTM_atom': False},
treat_prefix=True)
@SectionLineParser.section_parser('link', 'patterns', context_type='link')
@SectionLineParser.section_parser('modification', 'patterns', context_type='modification')
def _link_patterns(self, line, lineno=0, context_type=''):
context = self.get_context(context_type)
tokens = collections.deque(_tokenize(line))
_parse_patterns(tokens, context, context_type)
@SectionLineParser.section_parser('link', 'features', context_type='link')
@SectionLineParser.section_parser('modification', 'features', context_type='modification')
def _link_features(self, line, lineno=0, context_type=''):
context = self.get_context(context_type)
tokens = collections.deque(_tokenize(line))
_parse_features(tokens, context, context_type)
@SectionLineParser.section_parser('moleculetype', 'citation', context_type='block')
@SectionLineParser.section_parser('link', 'citation', context_type='link')
@SectionLineParser.section_parser('modification', 'citation', context_type='modification')
def _parse_citation(self, line, lineno=0, context_type=""):
cite_keys = line.split()
self.get_context(context_type).citations.update(cite_keys)
@SectionLineParser.section_parser('citations')
def _pase_ff_citations(self, line, lineno=0):
# parses force-field wide citations
cite_keys = line.split()
self.citations.update(cite_keys)
@SectionLineParser.section_parser('moleculetype', 'debug', context_type='block')
@SectionLineParser.section_parser('link', 'debug', context_type='link')
@SectionLineParser.section_parser('modification', 'debug', context_type='modification')
@SectionLineParser.section_parser('moleculetype', 'info', context_type='block')
@SectionLineParser.section_parser('link', 'info', context_type='link')
@SectionLineParser.section_parser('modification', 'info', context_type='modification')
@SectionLineParser.section_parser('moleculetype', 'warning', context_type='block')
@SectionLineParser.section_parser('link', 'warning', context_type='link')
@SectionLineParser.section_parser('modification', 'warning', context_type='modification')
@SectionLineParser.section_parser('moleculetype', 'error', context_type='block')
@SectionLineParser.section_parser('link', 'error', context_type='link')
@SectionLineParser.section_parser('modification', 'error', context_type='modification')
def _parse_log_entry(self, line, lineno=0, context_type=''):
loglevel = logging.getLevelName(self.section[-1].upper())
self.get_context(context_type).log_entries[loglevel][line] = []
def _some_atoms_left(tokens, atoms, natoms):
"""
Return True if the token list expected to contain atoms.
If the number of atoms is known before hand, then the function compares the
number of already found atoms to the expected number. If the '--' token if
found, it is removed from the token list and there is no atom left.
Parameters
----------
tokens: collections.deque[str]
Deque of token to inspect. The deque **can be modified** in place.
atoms: list
List of already found atoms.
natoms: int or None
The number of expected atoms if known, else None.
Returns
-------
bool
"""
if not tokens:
return False
if tokens and tokens[0] == '--':
tokens.popleft()
return False
if natoms is not None and len(atoms) >= natoms:
return False
return True
def _parse_atom_attributes(token):
"""
Parse bracketed tokens.
Parameters
----------
token: str
Token in the form of a json dictionary.
Returns
-------
dict
"""
if not token.strip().startswith('{'):
raise ValueError('The token should start with a curly bracket.')
try:
attributes = json.loads(token)
except JSONDecodeError as error:
raise ValueError('The following value is not a valid atom attribute token: "{}".'
.format(token)) from error
modifications = {}
for key, value in attributes.items():
try:
if '|' in value:
modifications[key] = Choice(value.split('|'))
except TypeError:
pass
attributes.update(modifications)
return attributes
def _get_atoms(tokens, natoms):
atoms = []
while tokens and _some_atoms_left(tokens, atoms, natoms):
token = tokens.popleft()
if token.startswith('{'):
msg = 'Found atom attributes without an atom reference.'
raise IOError(msg)
if tokens:
next_token = tokens[0]
else:
next_token = ''
if next_token.startswith('{'):
atoms.append([token, _parse_atom_attributes(next_token)])
tokens.popleft()
else:
atoms.append([token, {}])
return atoms
def _treat_block_interaction_atoms(atoms, context, section):
atom_names = list(context.nodes)
all_references = []
for atom in atoms:
reference = atom[0]
if reference.isdigit():
# The indices in the file are 1-based
reference = int(reference) - 1
try:
reference = atom_names[reference]
except IndexError:
msg = ('There are {} atoms defined in the block "{}". '
'Interaction in section "{}" cannot refer to '
'atom index {}.')
raise IOError(msg.format(len(context), context.name,
section, reference + 1))
atom[0] = reference
else:
if reference not in context:
msg = ('There is no atom "{}" defined in the block "{}". '
'Section "{}" cannot refer to it.')
raise IOError(msg.format(reference, context.name, section))
if reference[0] in '+-<>':
msg = ('Atom names in blocks cannot be prefixed with + or -. '
'The name "{}", used in section "{}" of the block "{}" '
'is not valid in a block.')
raise IOError(msg.format(reference, section, context.name))
all_references.append(reference)
return all_references
def _split_node_key(key):
"""
Split a node key into a prefix and a base and validate the key validity.
"""
if not key:
raise IOError('A node key cannot be empty.')
# If the atom name is prefixed, we can get the order.
prefix_end = 0 # Make sure prefix_end is defined even if key is empty
for prefix_end, char in enumerate(key):
if char not in '+-><*':
break
else: # no break
# This branch could also be taken if 'key' was empty. However, we
# tested already that it was not the case.
msg = ('The atom key "{}" is not valid. There must be a name '
'following prefix.')
raise IOError(msg.format(key))
prefix = key[:prefix_end]
if len(set(prefix)) > 1:
msg = ('Atom name prefix cannot mix characters. Atom name "{}" '
'is not a valid key.')
raise IOError(msg.format(key))
base = key[prefix_end:]
return prefix, base
def _get_order_and_prefix_from_attributes(attributes):
prefix_from_attributes = ''
order_from_attributes = None
Sequence = collections.abc.Sequence # pylint: disable=invalid-name
if attributes.get('order') is not None:
order = attributes['order']
order_from_attributes = order
if isinstance(order, numbers.Integral) and not isinstance(order, bool):
# Boolean as abstract subclasses of number.Integral as they can be
# considered as 0 and 1. Yet, they yield unexpected results and
# should not be accepted as valid values for 'order'.
if order > 0:
prefix_char = '+'
else:
prefix_char = '-'
prefix_from_attributes = prefix_char * int(abs(order))
elif (isinstance(order, Sequence) # We can do the following operations
and len(set(order)) == 1 # It is homogeneous
and order[0] in '><*'): # The character is an expected one
prefix_from_attributes = order
else:
raise IOError('The order given in attribute ("{}") is not valid. '
'It must be am integer or a homogeneous series '
'of ">", "<", or "*".'
.format(order))
return prefix_from_attributes, order_from_attributes
def _get_order_and_prefix_from_prefix(prefix):
"""
Convert a prefix into a numerical value.
"""
prefix_from_prefix = None
order_from_prefix = 0
if not prefix:
return prefix_from_prefix, order_from_prefix
# It is already validated.
prefix_from_prefix = prefix
if prefix[0] == '+':
order_from_prefix = len(prefix)
elif prefix[0] == '-':
order_from_prefix = -len(prefix)
else:
order_from_prefix = prefix
return prefix_from_prefix, order_from_prefix
def _treat_atom_prefix(reference, attributes):
"""
Connect graph keys, order, and atom names.
In a link, the graph keys, the order attribute of the atoms, and the atom
names are interconnected. In most cases, the graph key is the atom name
prefixed in a way that represent the order attribute. It is possible to
define the order and the atom name from the graph key, or to set the graph
key to represent the order, depending on what is explicitly specified in
the file.
In a link node, the order can be an integer, a series of '>' (*e.g.* '>',
'>>', '>>>', ...), a series of '<', or a series of '*'. The series of '>',
'<', and '*' translate directly from the key prefix to the order attribute,
and *vice versa*. Numerical values of the order attribute, however, are
converted into series of '+' or '-' for the key prefix; there, the number
of '+' or '-' in the prefix corresponds to the value of the order
attribute.
The order can be specified either by the key prefix, or by the attribute.
If it is specified in the two places, then they have to match each other or
a :exc:`IOError` is raised.
If the atom name is explicitly specified, then it is not modified. If it is
not specified, then it is set from the node key. The base of the node key
(*i.e.* what follows the prefix) is not modified, but a prefix can be added
if there is none. The base of the node key and the atom name *can* differ.
The atom name is what will be use for the graph isomorphism. The base of
the key cannot be empty (*i.e.* '+++' or '*' are not valid keys); if it is,
then an :exc:`IOError` is raised.
Parameters
----------
reference: str
A node key for a link, as written in the file.
attributes: dict
The node attributes read fro the file.
Returns
-------
prefixed_reference: str
The node key with the appropriate prefix.
attributes: dict
A shalow copy of the node attribute dictionary with the 'order' and the
'atomname' attributes set as appropriate.
Raises
------
IOError
The node key, or an attribute value is invalid.
Examples
--------
>>> _treat_atom_prefix('+BB', {})
('+BB', {'order': 1, 'atomname': 'BB'})
>>> _treat_atom_prefix('BB', {'order': 1})
('+BB', {'order': 1, 'atomname': 'BB'})
>>> _treat_atom_prefix('--XX', {'atomname': 'BB'})
('+BB', {'order': -2, 'atomname': 'BB'})
>>> _treat_atom_prefix('>>BB', {})
('>>BB', {'order': '>>', 'atomname': 'BB'})
"""
prefix, base = _split_node_key(reference)
# Is the order specified in the attributes?
(prefix_from_attributes,
order_from_attributes) = _get_order_and_prefix_from_attributes(attributes)
# Is there a specified prefix?
(prefix_from_prefix,
order_from_prefix) = _get_order_and_prefix_from_prefix(prefix)
# If the order is defined twice, is it consistent?
if (order_from_attributes is not None
and prefix_from_prefix is not None
and order_from_attributes != order_from_prefix):
msg = ('The sequence order for atom "{}" of a '
'link is not consistent between the name prefix '
'(order={}) and the atom attributes (order={}).')
raise IOError(msg.format(reference, order_from_prefix, order_from_attributes))
return_attributes = copy.copy(attributes)
if order_from_attributes is None:
return_attributes['order'] = order_from_prefix
if 'atomname' not in return_attributes:
return_attributes['atomname'] = base
if prefix_from_prefix is None:
prefixed = prefix_from_attributes + base
else:
prefixed = reference
return prefixed, return_attributes
def _treat_link_interaction_atoms(atoms, context, section):
all_references = []
for reference, attributes in atoms:
intermediate = context._apply_to_all_nodes.copy()
intermediate.update(attributes)
attributes = intermediate
prefixed_reference, attributes = _treat_atom_prefix(reference, attributes)
all_references.append(prefixed_reference)
if prefixed_reference in context:
context_atom = context.nodes[prefixed_reference]
for key, value in attributes.items():
if key in context_atom and value != context_atom[key]:
msg = ('Attribute {} of atom {} conflicts in a link '
'between its definition in section "{}" '
'(value is "{}") and its previous definition '
'(value was "{}").')
raise IOError(msg.format(key, reference, section,
value, context_atom[key]))
context_atom.update(attributes)
else:
context.add_node(prefixed_reference, **attributes)
return all_references
def _parse_interaction_parameters(tokens):
parameters = []
for token in tokens:
if _is_param_effector(token):
effector_name, effector_param_str = token.split('(', 1)
effector_param_str = effector_param_str[:-1] # Remove the closing parenthesis
try:
effector_class = PARAMETER_EFFECTORS[effector_name]
except KeyError:
raise IOError('{} is not a known parameter effector.'
.format(effector_name))
if '|' in effector_param_str:
effector_param_str, effector_format = effector_param_str.split('|')
else:
effector_format = None
effector_param = [elem.strip() for elem in effector_param_str.split(',')]
parameter = effector_class(effector_param, format_spec=effector_format)
else:
parameter = token
parameters.append(parameter)
return parameters
def _is_param_effector(token):
return (
'(' in token
and not token.startswith('(')
and token.endswith(')')
)
def _base_parser(tokens, context, context_type, section, natoms=None, delete=False):
if context_type != 'link' and delete:
raise IOError('Interactions can only be removed in links.')
delimiter_count = tokens.count('--')
if delimiter_count > 1:
msg = 'There can be 0 or 1 "--" delimiter; {} found.'
raise IOError(msg.format(delimiter_count))
# Group the atoms and their attributes
atoms = _get_atoms(tokens, natoms)
if natoms is not None and len(atoms) != natoms:
raise IOError('Found {} atoms while {} were expected.'
.format(len(atoms), natoms))
# Normalize the atom references.
# Blocks and links treat these references differently.
# For blocks:
# * references can be written as indices or atom names
# * a reference cannot be prefixed by + or -
# * an interaction cannot create a new atom
# For links:
# * references must be atom names, but they can be prefixed with one or
# more + or - to signify the order in the sequence
# * interactions create nodes
if context_type == 'block':
treated_atoms = _treat_block_interaction_atoms(atoms, context, section)
elif context_type in ('link', 'modification'):
treated_atoms = _treat_link_interaction_atoms(atoms, context, section)
# Getting the atoms consumed the "--" delimiter if any. So what is left
# are the interaction parameters or the meta attributes.
if tokens and tokens[-1].startswith('{'):
token = tokens.pop()
meta = json.loads(token)
else:
meta = {}
parameters = _parse_interaction_parameters(tokens)
apply_to_all_interactions = context._apply_to_all_interactions[section]
meta = dict(collections.ChainMap(meta, apply_to_all_interactions))
if delete:
interaction = DeleteInteraction(
atoms=treated_atoms,
atom_attrs=[atom[1] for atom in atoms],
parameters=parameters,
meta=meta,
)
interaction_list = context.removed_interactions.get(section, [])
interaction_list.append(interaction)
context.removed_interactions[section] = interaction_list
else:
interaction = Interaction(
atoms=treated_atoms,
parameters=parameters,
meta=meta,
)
interaction_list = context.interactions.get(section, [])
interaction_list.append(interaction)
context.interactions[section] = interaction_list
def _parse_block_atom(tokens, context):
if tokens[-1].startswith('{'):
attributes = _parse_atom_attributes(tokens.pop())
else:
attributes = {}
# deque does not support slicing
first_six = (tokens.popleft() for _ in range(6))
_, atype, resid, resname, name, charge_group = first_six
if name in context:
msg = ('There is already an atom named "{}" in the block "{}". '
'Atom names must be unique within a block.')
raise IOError(msg.format(name, context.name))
atom = {
'atomname': name,
'atype': atype,
'resname': resname,
'resid': int(resid),
'charge_group': int(charge_group),
}
# charge and mass are optional, but charge has to be defined for mass to be
if tokens:
atom['charge'] = float(tokens.popleft())
if tokens:
atom['mass'] = float(tokens.popleft())
context.add_atom(dict(collections.ChainMap(attributes, atom)))
def _parse_link_atom(tokens, context, defaults=None, treat_prefix=True):
if len(tokens) > 2:
raise IOError('Unexpected column in link atom definition.')
if len(tokens) < 2:
raise IOError('Missing column in link atom definition.')
reference = tokens[0]
if defaults is None:
defaults = {}
attributes = _parse_atom_attributes(tokens[1])
if treat_prefix:
prefixed_reference, attributes = _treat_atom_prefix(reference, attributes)
else:
prefixed_reference = reference
attributes['atomname'] = reference
attributes = dict(collections.ChainMap(attributes, context._apply_to_all_nodes))
node_attributes = context.nodes.get(prefixed_reference, {})
for attr, value in attributes.items():
if value != node_attributes.get(attr, value):
msg = ('Conflict in an atom attributes in the definition of a '
'link node. Cannot set the attribute "{}" of atom "{}" '
'to "{}" because it is already defined as "{}".')
raise IOError(msg.format(attr, reference, value,
node_attributes[node_attributes[attr]]))
full_attributes = dict(collections.ChainMap(attributes, node_attributes, defaults))
if prefixed_reference in context.nodes:
context.nodes[prefixed_reference] = full_attributes
else:
context.add_node(prefixed_reference, **full_attributes)
def _parse_link_attribute(tokens, context, section):
if len(tokens) > 2:
raise IOError('Unexpected column in section "{}".'.format(section))
if len(tokens) < 2:
raise IOError('Missing column in section "{}".'.format(section))
key, value = tokens
if '|' in value:
value = Choice(json.loads(value).split('|'))
elif '(' in value and value.endswith(')') and not value.startswith('('):
open_pos = value.find('(')
function = value[:open_pos]
argument = json.loads(value[open_pos + 1:-1])
value = VALUE_PREDICATES[function](argument)
else:
value = json.loads(value)
if section == 'link':
context._apply_to_all_nodes[key] = value
elif section == 'molmeta':
context.molecule_meta[key] = value
else:
raise ValueError('Parser only defined for sections "link" and "molmeta".')
def _parse_meta(tokens, context, context_type, section):
"""
Parse lines starting with '#meta'.
The function expects 2 tokens. The first token is assumed to be '#meta' and
is ignored. The second must be a bracketed token. This second token is
parsed as a dictionary and updated the dictionary of attributes to add to
all the nodes involved in a given type of interaction for the context.
The type of interaction is set by the `section` argument.
The context is a :class:`vermouth.molecule.Block` or a subclass such as a
:class:`vermouth.molecule.Link`.
The `context_type` is a string version of the first level section (i.e
"link", "block", or "modification").
"""
if len(tokens) > 2:
msg = ('Unexpected column when defining meta attributes for section '
'"{}" of a {}. {} tokens read instead of 2.')
raise IOError(msg.format(section, context_type, len(tokens)))
if len(tokens) < 2:
msg = 'Missing column when defining meta attributes for section "{}" of a {}.'
raise IOError(msg.format(section, context_type))
attributes = json.loads(tokens[-1])
context._apply_to_all_interactions[section].update(attributes)
def _parse_edges(tokens, context, context_type, negate):
if negate and context_type != 'link':
raise IOError('The "non-edges" section is only valid in links.')
atoms = _get_atoms(tokens, natoms=2)
prefixed_atoms = []
for atom in atoms:
prefixed_reference, attributes = _treat_atom_prefix(*atom)
try:
apply_to_all_nodes = context._apply_to_all_nodes
except AttributeError:
apply_to_all_nodes = {}
full_attributes = dict(collections.ChainMap(attributes, apply_to_all_nodes))
prefixed_atoms.append([prefixed_reference, full_attributes])
if negate:
context.non_edges.append([prefixed_atoms[0][0], prefixed_atoms[1][1]])
else:
error_message = 'Atom with name {} not found for {} {}'
for prefixed_atom in prefixed_atoms:
atomname = prefixed_atom[0]
if atomname not in context and context_type == 'modification':
raise KeyError(error_message.format(atomname, context_type,
context.name))
context.add_edge(prefixed_atoms[0][0], prefixed_atoms[1][0])
def _parse_patterns(tokens, context, context_type):
if context_type != 'link':
raise IOError('The "partterns" section is only valid in links.')
atoms = _get_atoms(tokens, natoms=None)
context.patterns.append(atoms)
def _parse_variables(tokens, force_field, section):
if len(tokens) > 2:
raise IOError('Unexpected column in section "{}".'.format(section))
if len(tokens) < 2:
raise IOError('Missing column in section "{}".'.format(section))
key, value = tokens
try:
value = json.loads(value)
except JSONDecodeError:
value = str(value)
force_field.variables[key] = value
def _parse_features(tokens, context, context_type):
if context_type != 'link':
raise IOError('The "features" section is only valid in links.')
context.features.update(set(tokens))
[docs]
def read_ff(lines, force_field):
director = FFDirector(force_field)
return list(director.parse(iter(lines)))