Source code for vermouth.processors.locate_charge_dummies
# 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 processor that generates positions for every charge dummy.
"""
import itertools
import operator
import numpy as np
from .processor import Processor
DEFAULT_DUMMY_ATTRIBUTE = 'charge_dummy'
[docs]
def fibonacci_sphere(n_samples):
"""
Place points near-evenly distributed on a sphere.
Use the Fibonacci sphere algorithm to place 'n_samples' points at the
surface of a sphere of radius 1, centered on the origin.
Parameters
----------
n_samples: int
Number of points to place.
Returns
-------
numpy.ndarray
3D coordinates of the points.
"""
offset = 2 / n_samples
increment = np.pi * (3 - np.sqrt(5))
sample_idx = np.arange(n_samples)
y = (sample_idx * offset - 1) + offset / 2 # pylint: disable=invalid-name
r = np.sqrt(1 - y * y) # pylint: disable=invalid-name
phi = (sample_idx % n_samples) * increment
x = np.cos(phi) * r # pylint: disable=invalid-name
z = np.sin(phi) * r # pylint: disable=invalid-name
return np.stack([x, y, z]).T
[docs]
def colinear_pair():
"""
Build two points on a line around the origin at a random orientation.
"""
vector = np.random.rand(3)
vector /= np.linalg.norm(vector)
points = np.stack([np.zeros((3, )), vector])
points -= vector/2
return points
[docs]
def find_anchor(molecule, node_key, attribute_tag=DEFAULT_DUMMY_ATTRIBUTE):
"""
Find the non-dummy bead to which a charge dummy is anchored.
Each charge dummy has to be attached to exactly one non-dummy atom. This
function returns the node key for that non-dummy atom.
Parameters
----------
molecule: networkx.Graph
The molecule to work on.
node_key:
The node key of the charge dummy.
attribute_tag: str
The name of the atom attribute used to describe charge dummies.
Returns
-------
collections.abc.Hashable
The node key of the anchor in the molecule graph.
Raises
------
ValueError
Raised if there are no anchor, or more than one anchor, found. Raised
also if the charge dummy is not a charge dummy.
"""
dummy = molecule.nodes[node_key]
if dummy.get(attribute_tag, None) is None:
msg = 'Node "{}" is not a charge dummy. Check the "{}" node attribute.'
raise ValueError(msg.format(node_key, attribute_tag))
# There should be only one anchor, and that anchor is the (hopefully) only
# neighbor that is not a dummy.
potential_anchors = [
neighbor
for neighbor in molecule.neighbors(node_key)
if molecule.nodes[neighbor].get(attribute_tag, None) is None
]
if not potential_anchors:
raise ValueError('No anchor found for dummy bead "{}": {}.'
.format(node_key, molecule.nodes[node_key]))
elif len(potential_anchors) > 1:
raise ValueError('Too many potential anchors found for dummy "{}" ({} found).'
.format(node_key, len(potential_anchors)))
return potential_anchors[0]
[docs]
def locate_dummy(molecule, anchor_key, dummy_keys, attribute_tag=DEFAULT_DUMMY_ATTRIBUTE):
"""
Set the position of a group of charge dummies around a non-dummy anchor.
The molecule is modified in-place.
The charge dummies are placed at a distance to the anchor defined in nm by
their charge dummy attribute, the name of which is given in the
'attribute_tag' argument.
Parameters
----------
molecule: vermouth.molecule.Molecule
The molecule to work on.
anchor_key:
The key of the non-dummy anchor all the charge dummies are connected to.
dummy_keys: collections.abc.Iterable
A collection of atom keys for charge dummies to position.
attribute_tag: str
Name of the atom attribute that describe charge dummies.
"""
anchor_position = molecule.nodes[anchor_key].get('position')
if anchor_position is None:
msg = 'The anchor of the "{}" dummy ("{}") does not have a position.'
raise ValueError(msg.format(anchor_key, anchor_position[0]))
distances = []
distance_error_keys = []
for dummy_key in dummy_keys:
try:
distances.append(float(molecule.nodes[dummy_key].get(attribute_tag)))
except ValueError:
distance_error_keys.append(dummy_key)
if distance_error_keys:
msg = ('The following charge dummies have an invalid for their {} '
'attribute: {}. The values have to be numbers.'
.format(attribute_tag, ', '.join(distance_error_keys)))
raise ValueError(msg)
distances = np.array(distances)
if len(dummy_keys) == 2:
points = colinear_pair()
else:
points = fibonacci_sphere(len(dummy_keys))
points *= distances[:, None]
for dummy_key, position in zip(dummy_keys, points):
molecule.nodes[dummy_key]['position'] = position + anchor_position
[docs]
def locate_all_dummies(molecule, attribute_tag=DEFAULT_DUMMY_ATTRIBUTE):
"""
Set the position of all charge dummies of a molecule.
The molecule is modified in-place.
The charge dummies are placed at a distance to the anchor defined in nm by
their charge dummy attribute, the name of which is given in the
'attribute_tag' argument.
Parameters
----------
molecule: vermouth.molecule.Molecule
The molecule to work on.
attribute_tag: str
Name of the atom attribute that describe charge dummies.
"""
dummies = [
(find_anchor(molecule, dummy_key, attribute_tag), dummy_key)
for dummy_key in molecule.nodes
if molecule.nodes[dummy_key].get(attribute_tag, None) is not None
]
dummies.sort() # Sort primarily on the anchor key as it appears first
grouped_by_anchor = itertools.groupby(dummies, key=operator.itemgetter(0))
for anchor_key, dummy_pairs in grouped_by_anchor:
dummy_keys = [pair[1] for pair in dummy_pairs]
locate_dummy(molecule, anchor_key, dummy_keys, attribute_tag)
[docs]
class LocateChargeDummies(Processor):
def __init__(self, attribute_tag=DEFAULT_DUMMY_ATTRIBUTE):
super().__init__()
self.attribute_tag = attribute_tag
[docs]
def run_molecule(self, molecule):
locate_all_dummies(molecule, attribute_tag=self.attribute_tag)
return molecule