Source code for chemtools.topology.point

# -*- coding: utf-8 -*-
# ChemTools is a collection of interpretive chemical tools for
# analyzing outputs of the quantum chemistry calculations.
#
# Copyright (C) 2016-2019 The ChemTools Development Team
#
# This file is part of ChemTools.
#
# ChemTools is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# ChemTools is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>
#
# --
r"""Contains class responsible for calculating various descriptors based on eigenvalues."""


import warnings
import numpy as np


__all__ = ["EigenValueTool", "CriticalPoint"]


[docs]class EigenValueTool(object): r"""Class of descriptive tools based on eigenvalues.""" def __init__(self, eigenvalues, eps=1e-15): r"""Initialize class. Parameters ---------- eigenvalues : np.ndarray(N, 3) A 2-D array recording the eigenvalues at each :math:`N` point. eps : float, optional The error bound for being a zero eigenvalue. """ if not isinstance(eigenvalues, np.ndarray): raise TypeError("Eigenvalues should be a numpy array.") if not eigenvalues.ndim == 2: raise TypeError("Eigenvalues should be a two dimensional array.") self._eigenvalues = eigenvalues self._eps = eps @property def eigenvalues(self): r"""Eigenvalues.""" return self._eigenvalues @property def ellipticity(self): r"""Ellipticity. .. math:: \frac{\lambda_\text{min}}{\lambda_\text{min-1}} - 1 """ # get the two smallest eigenvalues index = np.argsort(self.eigenvalues, axis=1)[:, :2] min1 = self.eigenvalues[np.arange(self.eigenvalues.shape[0]), index[:, 0]] min2 = self.eigenvalues[np.arange(self.eigenvalues.shape[0]), index[:, 1]] # if np.abs(eigen2) < self._zero_eps: # warnings.warn("Second largest eigenvalue is zero.") # return np.inf return (min1 / min2) - 1. @property def bond_descriptor(self): r"""Bond descriptor which is the ratio of average of positive and negative eigenvalues. .. math:: \frac{\left(\frac{\sum_{\lambda_k > 0} \lambda_k}{\sum_{\lambda_k > 0} 1}\right)} {\left(\frac{\sum_{\lambda_k < 0} \lambda_k}{\sum_{\lambda_k < 0} 1}\right)} """ # compute numerator pos_mask = (self.eigenvalues > self._eps).astype(int) result = np.sum(self.eigenvalues * pos_mask, axis=1) / np.sum(pos_mask, axis=1) # compute denominator neg_mask = (self.eigenvalues < -self._eps).astype(int) result /= (np.sum(self.eigenvalues * neg_mask, axis=1) / np.sum(neg_mask, axis=1)) return result @property def eccentricity(self): r"""Eccentricity (essentially the condition number). .. math :: \sqrt{\frac{\lambda_\text{max}}{\lambda_\text{min}}} """ ratio = np.amax(self.eigenvalues, axis=1) / np.amin(self.eigenvalues, axis=1) # set negative values to None ratio[ratio < 0.] = np.nan return np.sqrt(ratio) @property def index(self): r"""Index which is the number of negative-curvature directions. .. math:: \sum_{\lambda_k < 0} 1 """ return np.sum(self._eigenvalues < -self._eps, axis=1) @property def rank(self): r"""Rank which is the number of positive eigenvalues. .. math:: \sum_{\lambda_i > 0} 1 """ return np.sum(np.abs(self._eigenvalues) > self._eps, axis=1) @property def signature(self): r"""Signature which is the difference of number of positive & negative eigenvalues. .. math:: \sum_{\lambda_k > 0.} 1 - \sum_{\lambda_k < 0.} 1 """ result = np.sum(self.eigenvalues > self._eps, axis=1) result -= np.sum(self.eigenvalues < -self._eps, axis=1) return result @property def morse(self): r"""Rank and signature. .. math:: \left(\sum_{\lambda_k > 0} 1, \sum_{\lambda_k > 0.}1 - \sum_{\lambda_k < 0.} 1\right) A system is degenerate if it has a zero eigenvalue and consequently, it's critical point is said to be "catastrophe". It returns a warning in this case. """ if np.any(np.abs(self.eigenvalues) < self._eps): warnings.warn("Near catastrophic eigenvalue (close to zero) been found.") return np.hstack([self.rank[:, np.newaxis], self.signature[:, np.newaxis]])
class CriticalPoint(EigenValueTool): """Critical Point Class.""" def __init__(self, coordinate, eigenvalues, eigenvectors, eps=1e-15): r"""Initialize class. Parameters ---------- coordinate : np.ndarray(3,) Cartesian coordinate of critical point. eigenvalues : np.ndarray(3,) Eigenvalues of hessian function evaluated at the critical point. eigenvectors : np.ndarray(3, 3) Eigenvectors of hessian function evaluated at the critical point. """ super(CriticalPoint, self).__init__(eigenvalues[np.newaxis, :], eps=eps) self._coord = coordinate self._eigenvectors = eigenvectors @property def coordinate(self): """Cartesian coordinate of critical point.""" return self._coord