Source code for mmtfPyspark.interactions.coordinationGeometry

#!/user/bin/env python
'''coordinateGeometry.py

This class calculates distances, angles, and orientational order parameters
for atoms coordinated to a central atom

'''
__author__ = "Mars (Shih-Cheng) Huang"
__maintainer__ = "Mars (Shih-Cheng) Huang"
__email__ = "marshuang80@gmail.com"
__version__ = "0.2.0"
__status__ = "done"

import numpy as np
import itertools as it
import math


[docs]class CoordinateGeometry(object): '''A coordinate geoetry class that calcualtes distances angles and orientational order parameters. Attributes ---------- center : list coordinates of the center atom neighbors : list coordinates of neighbor atoms ''' def __init__(self, center, neighbors): self.center = center self.neighbors = neighbors self._calc_distances() self._calc_angles() self._calc_dot_products()
[docs] def get_distance(self): '''Returns an array of distances from the coordination center to the neighbor atoms. Returns ------- :obj:`list <numpy.ndarray>` Numpy array of pairwise angles ''' return self.distances
[docs] def get_angles(self): '''Returns all pairwise angles between a center and pairs of neighbor atoms. Returns ------- :obj:`list <numpy.ndarray>` Numpy array of pairwise angles ''' return self.angles
[docs] def q3(self): '''Returns a normalized trigonal orientational order parameter q3. The orientational order parameter q3 measures the extent to which a molecule and its three nearest neighbors adopt a trigonal arrangement. It is equal to 0 for a random arrangement and equals 1 in a perfectly trigonal arrangement. References ---------- - Richard H. Henchman and Stuart J. Cockramb (2013), Water’s Non-Tetrahedral Side, Faraday Discuss., 167, 529. https://dx.doi.org/10.1039/c3fd00080j Returns ------- float trigonal orientational order parameter ''' if len(self.neighbors) < 3: raise ValueError(f"ERROR: Trigonality calculation requires at least \ 3 neighbors, but found: {len(self.neighbors)}") total = 0.0 for i, j in it.combinations(range(3), 2): total += (self.dotProducts[i][j] + 0.5) ** 2 return float(1.0 - 4.0 / 7.0 * total)
[docs] def q4(self): '''Returns a normalized tetrahedral orientational order parameter q4. The orientational order parameter q4 measures the extent to which a central atom and its four nearest neighbors adopt a tetrahedral arrangement. It is equal to 0 for a random arrangement and equals 1 in a perfectly tetrahedral arrangement. It can reach a minimum value of -3 for unusual arrangements. References ---------- Jeffrey R. Errington & Pablo G. Debenedetti (2001) Relationship between structural order and the anomalies of liquid water, Nature 409, 318-321. <a href="https://dx.doi.org/10.1038/35053024">doi:10.1038/35053024</a> P.-L. Chau & A. J. Hardwick (1998) A new order parameter for tetrahedral configurations, Molecular Physics, 93:3, 511-518. <a href"https://dx.doi.org/10.1080/002689798169195">doi:10.1080/002689798169195</a> Returns ------- float tetrahedra orientational order parameter ''' if len(self.neighbors) < 4: raise ValueError(f"ERROR: Tetrahedral calculation requires at least \ 4 neighbors, but found: {len(self.neighbors)}") total = 0.0 for i, j in it.combinations(range(4), 2): total += (self.dotProducts[i][j] + 1.0 / 3.0) ** 2 return float(1.0 - 3.0 / 8.0 * total)
[docs] def q5(self): '''Returns a normalized trigonal bipyramidal orientational order parameter q5. The first three nearest atoms to the center atom are used to define the equatorial positions. The next two nearest atoms are used to specify the axial positions. The orientational order parameter q5 measures the extent to which the five nearest atoms adopt a trigonal bipyramidal arrangement. It is equal to 0 for a random arrangement and equals 1 in a perfectly trigonal bipyramidal arrangement. It can reach negative values for certain arrangements. References ---------- - `Richard H. Henchman and Stuart J. Cockramb (2013), Water’s Non-Tetrahedral Side, Faraday Discuss., 167, 529. <https://dx.doi.org/10.1039/c3fd00080j>`_ Note, the summations in equation (3) in this paper is incorrect. This method uses the corrected version (R. Henchman, personal communication). Returns ------- float trigonal bipyramidal orientational order parameter ''' if len(self.neighbors) < 5: raise ValueError(f"ERROR: Tetrahedral calculation requires at least \ 5 neighbors, but found: {len(self.neighbors)}") # 3 equatorial-equatorial angles (120 deg: cos(120) = -1/2) ee = [(0, 1), (0, 2), (1, 2)] sum1 = sum([(self.dotProducts[i][j] + 1.0 / 2.0) ** 2 for i, j in ee]) # 6 equatorial-axial angles (90 deg: cos(90) = 0) ea = [(0, 3), (0, 4), (1, 3), (1, 4), (2, 3), (2, 4)] sum2 = sum([self.dotProducts[i][j] ** 2 for i, j in ea]) # 1 axial-axial angle (180 deg: cos(180) = -1) sum3 = (self.dotProducts[3][4] + 1) ** 2 return float(1.0 - 6.0 / 35.0 * sum1 - 3.0 / 10.0 * sum2 - 3.0 / 40.0 * sum3)
[docs] def q6(self): '''Returns a normalized octahedra orientational order parameter q6. The orientational order parameter q6 measures the extent to which a central atom and its six nearest neighbors adopt an octahedral arrangement. It is equal to 0 for a random arrangement and equals 1 in a perfectly octahedralhedral arrangement. It can reach negative values for certain arrangements. References ---------- - `Richard H. Henchman and Stuart J. Cockramb (2013), Water’s Non-Tetrahedral Side, Faraday Discuss., 167, 529. <https://dx.doi.org/10.1039/c3fd00080j>`_ The same method as described in this paper was used to derive the q6 parameter (R. Henchman, personal communication). Returns ------- float Octahedral orientational order parameter ''' if len(self.neighbors) < 6: raise ValueError(f"ERROR: Octrahedrality calculation requires at \ least 5 neighbors, but found: {len(self.neighbors)}") # 4 angles between positions in equatorial plane equatorial = [(0, 1), (1, 2), (2, 3), (3, 0)] sum1 = sum([self.dotProducts[i][j]**2 for i, j in equatorial]) # 4 angles between axial position 1 and equatorial plane axial_1 = [(0, 4), (1, 4), (2, 4), (3, 4)] sum2 = sum([self.dotProducts[i][j]**2 for i, j in axial_1]) # 4 angles between axial position 2 and equatorial plane axial_2 = [(0, 5), (1, 5), (2, 5), (3, 5)] sum3 = sum([self.dotProducts[i][j]**2 for i, j in axial_2]) return float(1.0 - 1.0 / 4.0 * sum([sum1, sum2, sum3]))
def _calc_distances(self): '''Calcualte and set the distances between the center and it's neighbors ''' dist = [float(np.linalg.norm(self.center - n)) for n in self.neighbors] self.distances = dist def _calc_angles(self): '''Calcualte and set the angles between the center and it's neighbors ''' self._get_vectors() self.angles = [0.0] * \ int((len(self.neighbors) * len(self.neighbors) - 1) / 2) n = 0 for i in range(len(self.vectors) - 1): for j in range(i + 1, len(self.vectors)): self.angles[n] = self._angle(self.vectors[i], self.vectors[j]) n += 1 def _calc_dot_products(self): '''Calcualte and set the dot products between the center and it's neighbors ''' index = self._get_index_by_distance(self.distances) vectors = np.array([(self.center - self.neighbors[index[i]]) / np.linalg.norm( self.center - self.neighbors[index[i]]) for i in range(len(self.neighbors))]) self.dotProducts = np.zeros((len(vectors), len(vectors))) for i in range(len(vectors) - 1): for j in range(i + 1, len(vectors)): self.dotProducts[i, j] = float(np.dot(vectors[i], vectors[j])) def _get_vectors(self): '''Set the center to neighbors vectors ''' self.vectors = [self.center - n for n in self.neighbors] def _angle(self, a, b): '''Calculate the angle between two points Parameters ---------- a : list point a b : list oint b ''' arccosInput = np.dot(a, b) / np.linalg.norm(a) / np.linalg.norm(b) arccosInput = 1.0 if arccosInput > 1.0 else arccosInput arccosInput = -1.0 if arccosInput < -1.0 else arccosInput return float(math.acos(arccosInput)) def _get_index_by_distance(self, values): '''Get a list of indices sorted by their calculated distance ''' indexed_values = [(index, value) for index, value in enumerate(values)] indexed_values.sort(key=lambda x: x[1]) return [tup[0] for tup in indexed_values]