1#!/usr/bin/env python
2
3from __future__ import division
4
5import itertools
6import time, sys
7from collections import OrderedDict, defaultdict
8from copy import deepcopy
9
10import networkx as nx
11import numpy as np
12from numpy.linalg import multi_dot
13
14from geometric.molecule import Elements, Radii
15from geometric.nifty import click, commadash, ang2bohr, bohr2ang, logger, pvec1d, pmat2d
16from geometric.rotate import get_expmap, get_expmap_der, is_linear
17
18
19## Some vector calculus functions
20def unit_vector(a):
21    """
22    Vector function: Given a vector a, return the unit vector
23    """
24    return a / np.linalg.norm(a)
25
26def d_unit_vector(a, ndim=3):
27    term1 = np.eye(ndim)/np.linalg.norm(a)
28    term2 = np.outer(a, a)/(np.linalg.norm(a)**3)
29    answer = term1-term2
30    return answer
31
32def d_cross(a, b):
33    """
34    Given two vectors a and b, return the gradient of the cross product axb w/r.t. a.
35    (Note that the answer is independent of a.)
36    Derivative is on the first axis.
37    """
38    d_cross = np.zeros((3, 3), dtype=float)
39    for i in range(3):
40        ei = np.zeros(3, dtype=float)
41        ei[i] = 1.0
42        d_cross[i] = np.cross(ei, b)
43    return d_cross
44
45def d_cross_ab(a, b, da, db):
46    """
47    Given two vectors a, b and their derivatives w/r.t. a parameter, return the derivative
48    of the cross product
49    """
50    answer = np.zeros((da.shape[0], 3), dtype=float)
51    for i in range(da.shape[0]):
52        answer[i] = np.cross(a, db[i]) + np.cross(da[i], b)
53    return answer
54
55def ncross(a, b):
56    """
57    Scalar function: Given vectors a and b, return the norm of the cross product
58    """
59    cross = np.cross(a, b)
60    return np.linalg.norm(cross)
61
62def d_ncross(a, b):
63    """
64    Return the gradient of the norm of the cross product w/r.t. a
65    """
66    ncross = np.linalg.norm(np.cross(a, b))
67    term1 = a * np.dot(b, b)
68    term2 = -b * np.dot(a, b)
69    answer = (term1+term2)/ncross
70    return answer
71
72def nudot(a, b):
73    r"""
74    Given two vectors a and b, return the dot product (\hat{a}).b.
75    """
76    ev = a / np.linalg.norm(a)
77    return np.dot(ev, b)
78
79def d_nudot(a, b):
80    r"""
81    Given two vectors a and b, return the gradient of
82    the norm of the dot product (\hat{a}).b w/r.t. a.
83    """
84    return np.dot(d_unit_vector(a), b)
85
86def ucross(a, b):
87    r"""
88    Given two vectors a and b, return the cross product (\hat{a})xb.
89    """
90    ev = a / np.linalg.norm(a)
91    return np.cross(ev, b)
92
93def d_ucross(a, b):
94    r"""
95    Given two vectors a and b, return the gradient of
96    the cross product (\hat{a})xb w/r.t. a.
97    """
98    ev = a / np.linalg.norm(a)
99    return np.dot(d_unit_vector(a), d_cross(ev, b))
100
101def nucross(a, b):
102    r"""
103    Given two vectors a and b, return the norm of the cross product (\hat{a})xb.
104    """
105    ev = a / np.linalg.norm(a)
106    return np.linalg.norm(np.cross(ev, b))
107
108def d_nucross(a, b):
109    r"""
110    Given two vectors a and b, return the gradient of
111    the norm of the cross product (\hat{a})xb w/r.t. a.
112    """
113    ev = a / np.linalg.norm(a)
114    return np.dot(d_unit_vector(a), d_ncross(ev, b))
115## End vector calculus functions
116
117class CartesianX(object):
118    def __init__(self, a, w=1.0):
119        self.a = a
120        self.w = w
121        self.isAngular = False
122        self.isPeriodic = False
123
124    def __repr__(self):
125        #return "Cartesian-X %i : Weight %.3f" % (self.a+1, self.w)
126        return "Cartesian-X %i" % (self.a+1)
127
128    def __eq__(self, other):
129        if type(self) is not type(other): return False
130        eq = self.a == other.a
131        if eq and self.w != other.w:
132            logger.warning("Warning: CartesianX same atoms, different weights (%.4f %.4f)\n" % (self.w, other.w))
133        return eq
134
135    def __ne__(self, other):
136        return not self.__eq__(other)
137
138    def value(self, xyz):
139        xyz = xyz.reshape(-1,3)
140        a = self.a
141        return xyz[a][0]*self.w
142
143    def derivative(self, xyz):
144        xyz = xyz.reshape(-1,3)
145        derivatives = np.zeros_like(xyz)
146        derivatives[self.a][0] = self.w
147        return derivatives
148
149    def second_derivative(self, xyz):
150        xyz = xyz.reshape(-1,3)
151        deriv2 = np.zeros((xyz.shape[0], xyz.shape[1], xyz.shape[0], xyz.shape[1]))
152        return deriv2
153
154class CartesianY(object):
155    def __init__(self, a, w=1.0):
156        self.a = a
157        self.w = w
158        self.isAngular = False
159        self.isPeriodic = False
160
161    def __repr__(self):
162        # return "Cartesian-Y %i : Weight %.3f" % (self.a+1, self.w)
163        return "Cartesian-Y %i" % (self.a+1)
164
165    def __eq__(self, other):
166        if type(self) is not type(other): return False
167        eq = self.a == other.a
168        if eq and self.w != other.w:
169            logger.warning("Warning: CartesianY same atoms, different weights (%.4f %.4f)\n" % (self.w, other.w))
170        return eq
171
172    def __ne__(self, other):
173        return not self.__eq__(other)
174
175    def value(self, xyz):
176        xyz = xyz.reshape(-1,3)
177        a = self.a
178        return xyz[a][1]*self.w
179
180    def derivative(self, xyz):
181        xyz = xyz.reshape(-1,3)
182        derivatives = np.zeros_like(xyz)
183        derivatives[self.a][1] = self.w
184        return derivatives
185
186    def second_derivative(self, xyz):
187        xyz = xyz.reshape(-1,3)
188        deriv2 = np.zeros((xyz.shape[0], xyz.shape[1], xyz.shape[0], xyz.shape[1]))
189        return deriv2
190
191class CartesianZ(object):
192    def __init__(self, a, w=1.0):
193        self.a = a
194        self.w = w
195        self.isAngular = False
196        self.isPeriodic = False
197
198    def __repr__(self):
199        # return "Cartesian-Z %i : Weight %.3f" % (self.a+1, self.w)
200        return "Cartesian-Z %i" % (self.a+1)
201
202    def __eq__(self, other):
203        if type(self) is not type(other): return False
204        eq = self.a == other.a
205        if eq and self.w != other.w:
206            logger.warning("Warning: CartesianZ same atoms, different weights (%.4f %.4f)\n" % (self.w, other.w))
207        return eq
208
209    def __ne__(self, other):
210        return not self.__eq__(other)
211
212    def value(self, xyz):
213        xyz = xyz.reshape(-1,3)
214        a = self.a
215        return xyz[a][2]*self.w
216
217    def derivative(self, xyz):
218        xyz = xyz.reshape(-1,3)
219        derivatives = np.zeros_like(xyz)
220        derivatives[self.a][2] = self.w
221        return derivatives
222
223    def second_derivative(self, xyz):
224        xyz = xyz.reshape(-1,3)
225        deriv2 = np.zeros((xyz.shape[0], xyz.shape[1], xyz.shape[0], xyz.shape[1]))
226        return deriv2
227
228class TranslationX(object):
229    def __init__(self, a, w):
230        self.a = a
231        self.w = w
232        assert len(a) == len(w)
233        self.isAngular = False
234        self.isPeriodic = False
235
236    def __repr__(self):
237        # return "Translation-X %s : Weights %s" % (' '.join([str(i+1) for i in self.a]), ' '.join(['%.2e' % i for i in self.w]))
238        return "Translation-X %s" % (commadash(self.a))
239
240    def __eq__(self, other):
241        if type(self) is not type(other): return False
242        eq = set(self.a) == set(other.a)
243        if eq and np.sum((self.w-other.w)**2) > 1e-6:
244            logger.warning("Warning: TranslationX same atoms, different weights\n")
245            eq = False
246        return eq
247
248    def __ne__(self, other):
249        return not self.__eq__(other)
250
251    def value(self, xyz):
252        xyz = xyz.reshape(-1,3)
253        a = np.array(self.a)
254        return np.sum(xyz[a,0]*self.w)
255
256    def derivative(self, xyz):
257        xyz = xyz.reshape(-1,3)
258        derivatives = np.zeros_like(xyz)
259        for i, a in enumerate(self.a):
260            derivatives[a][0] = self.w[i]
261        return derivatives
262
263    def second_derivative(self, xyz):
264        xyz = xyz.reshape(-1,3)
265        deriv2 = np.zeros((xyz.shape[0], xyz.shape[1], xyz.shape[0], xyz.shape[1]))
266        return deriv2
267
268class TranslationY(object):
269    def __init__(self, a, w):
270        self.a = a
271        self.w = w
272        assert len(a) == len(w)
273        self.isAngular = False
274        self.isPeriodic = False
275
276    def __repr__(self):
277        # return "Translation-Y %s : Weights %s" % (' '.join([str(i+1) for i in self.a]), ' '.join(['%.2e' % i for i in self.w]))
278        return "Translation-Y %s" % (commadash(self.a))
279
280    def __eq__(self, other):
281        if type(self) is not type(other): return False
282        eq = set(self.a) == set(other.a)
283        if eq and np.sum((self.w-other.w)**2) > 1e-6:
284            logger.warning("Warning: TranslationY same atoms, different weights\n")
285            eq = False
286        return eq
287
288    def __ne__(self, other):
289        return not self.__eq__(other)
290
291    def value(self, xyz):
292        xyz = xyz.reshape(-1,3)
293        a = np.array(self.a)
294        return np.sum(xyz[a,1]*self.w)
295
296    def derivative(self, xyz):
297        xyz = xyz.reshape(-1,3)
298        derivatives = np.zeros_like(xyz)
299        for i, a in enumerate(self.a):
300            derivatives[a][1] = self.w[i]
301        return derivatives
302
303    def second_derivative(self, xyz):
304        xyz = xyz.reshape(-1,3)
305        deriv2 = np.zeros((xyz.shape[0], xyz.shape[1], xyz.shape[0], xyz.shape[1]))
306        return deriv2
307
308class TranslationZ(object):
309    def __init__(self, a, w):
310        self.a = a
311        self.w = w
312        assert len(a) == len(w)
313        self.isAngular = False
314        self.isPeriodic = False
315
316    def __repr__(self):
317        # return "Translation-Z %s : Weights %s" % (' '.join([str(i+1) for i in self.a]), ' '.join(['%.2e' % i for i in self.w]))
318        return "Translation-Z %s" % (commadash(self.a))
319
320    def __eq__(self, other):
321        if type(self) is not type(other): return False
322        eq = set(self.a) == set(other.a)
323        if eq and np.sum((self.w-other.w)**2) > 1e-6:
324            logger.warning("Warning: TranslationZ same atoms, different weights\n")
325            eq = False
326        return eq
327
328    def __ne__(self, other):
329        return not self.__eq__(other)
330
331    def value(self, xyz):
332        xyz = xyz.reshape(-1,3)
333        a = np.array(self.a)
334        return np.sum(xyz[a,2]*self.w)
335
336    def derivative(self, xyz):
337        xyz = xyz.reshape(-1,3)
338        derivatives = np.zeros_like(xyz)
339        for i, a in enumerate(self.a):
340            derivatives[a][2] = self.w[i]
341        return derivatives
342
343    def second_derivative(self, xyz):
344        xyz = xyz.reshape(-1,3)
345        deriv2 = np.zeros((xyz.shape[0], xyz.shape[1], xyz.shape[0], xyz.shape[1]))
346        return deriv2
347
348class Rotator(object):
349
350    def __init__(self, a, x0):
351        self.a = list(tuple(sorted(a)))
352        x0 = x0.reshape(-1, 3)
353        self.x0 = x0.copy()
354        self.stored_valxyz = np.zeros_like(x0)
355        self.stored_value = None
356        self.stored_derxyz = np.zeros_like(x0)
357        self.stored_deriv = None
358        self.stored_deriv2xyz = np.zeros_like(x0)
359        self.stored_deriv2 = None
360        self.stored_norm = 0.0
361        # Extra variables to account for the case of linear molecules
362        # The reference axis used for computing dummy atom position
363        self.e0 = None
364        # Dot-squared measures alignment of molecule long axis with reference axis.
365        # If molecule becomes parallel with reference axis, coordinates must be reset.
366        self.stored_dot2 = 0.0
367        # Flag that records linearity of molecule
368        self.linear = False
369
370    def reset(self, x0):
371        x0 = x0.reshape(-1, 3)
372        self.x0 = x0.copy()
373        self.stored_valxyz = np.zeros_like(x0)
374        self.stored_value = None
375        self.stored_derxyz = np.zeros_like(x0)
376        self.stored_deriv = None
377        self.stored_deriv2xyz = np.zeros_like(x0)
378        self.stored_deriv2 = None
379        self.stored_norm = 0.0
380        self.e0 = None
381        self.stored_dot2 = 0.0
382        self.linear = False
383
384    def __eq__(self, other):
385        if type(self) is not type(other): return False
386        eq = set(self.a) == set(other.a)
387        if eq and np.sum((self.x0-other.x0)**2) > 1e-6:
388            logger.warning("Warning: Rotator same atoms, different reference positions\n")
389        return eq
390
391    def __repr__(self):
392        return "Rotator %s" % commadash(self.a)
393
394    def __ne__(self, other):
395        return not self.__eq__(other)
396
397    def calc_e0(self):
398        """
399        Compute the reference axis for adding dummy atoms.
400        Only used in the case of linear molecules.
401
402        We first find the Cartesian axis that is "most perpendicular" to the molecular axis.
403        Next we take the cross product with the molecular axis to create a perpendicular vector.
404        Finally, this perpendicular vector is normalized to make a unit vector.
405        """
406        ysel = self.x0[self.a, :]
407        vy = ysel[-1]-ysel[0]
408        ev = vy / np.linalg.norm(vy)
409        # Cartesian axes.
410        ex = np.array([1.0,0.0,0.0])
411        ey = np.array([0.0,1.0,0.0])
412        ez = np.array([0.0,0.0,1.0])
413        self.e0 = np.cross(vy, [ex, ey, ez][np.argmin([np.dot(i, ev)**2 for i in [ex, ey, ez]])])
414        self.e0 /= np.linalg.norm(self.e0)
415
416    def value(self, xyz):
417        xyz = xyz.reshape(-1, 3)
418        if np.max(np.abs(xyz-self.stored_valxyz)) < 1e-12:
419            return self.stored_value
420        else:
421            xsel = xyz[self.a, :]
422            ysel = self.x0[self.a, :]
423            xmean = np.mean(xsel,axis=0)
424            ymean = np.mean(ysel,axis=0)
425            if not self.linear and is_linear(xsel, ysel):
426                # print "Setting linear flag for", self
427                self.linear = True
428            if self.linear:
429                # Handle linear molecules.
430                vx = xsel[-1]-xsel[0]
431                vy = ysel[-1]-ysel[0]
432                # Calculate reference axis (if needed)
433                if self.e0 is None: self.calc_e0()
434                #log.debug(vx)
435                ev = vx / np.linalg.norm(vx)
436                # Measure alignment of molecular axis with reference axis
437                self.stored_dot2 = np.dot(ev, self.e0)**2
438                # Dummy atom is located one Bohr from the molecular center, direction
439                # given by cross-product of the molecular axis with the reference axis
440                xdum = np.cross(vx, self.e0)
441                ydum = np.cross(vy, self.e0)
442                exdum = xdum / np.linalg.norm(xdum)
443                eydum = ydum / np.linalg.norm(ydum)
444                xsel = np.vstack((xsel, exdum+xmean))
445                ysel = np.vstack((ysel, eydum+ymean))
446            answer = get_expmap(xsel, ysel)
447            self.stored_norm = np.linalg.norm(answer)
448            self.stored_valxyz = xyz.copy()
449            self.stored_value = answer.copy()
450            return answer
451
452    def derivative(self, xyz):
453        xyz = xyz.reshape(-1, 3)
454        if np.max(np.abs(xyz-self.stored_derxyz)) < 1e-12:
455            return self.stored_deriv
456        else:
457            xsel = xyz[self.a, :]
458            ysel = self.x0[self.a, :]
459            xmean = np.mean(xsel,axis=0)
460            ymean = np.mean(ysel,axis=0)
461            if not self.linear and is_linear(xsel, ysel):
462                # print "Setting linear flag for", self
463                self.linear = True
464            if self.linear:
465                vx = xsel[-1]-xsel[0]
466                vy = ysel[-1]-ysel[0]
467                if self.e0 is None: self.calc_e0()
468                xdum = np.cross(vx, self.e0)
469                ydum = np.cross(vy, self.e0)
470                exdum = xdum / np.linalg.norm(xdum)
471                eydum = ydum / np.linalg.norm(ydum)
472                xsel = np.vstack((xsel, exdum+xmean))
473                ysel = np.vstack((ysel, eydum+ymean))
474            deriv_raw = get_expmap_der(xsel, ysel)
475            if self.linear:
476                # Chain rule is applied to get terms from
477                # dummy atom derivatives
478                nxdum = np.linalg.norm(xdum)
479                dxdum = d_cross(vx, self.e0)
480                dnxdum = d_ncross(vx, self.e0)
481                # Derivative of dummy atom position w/r.t. molecular axis vector
482                dexdum = (dxdum*nxdum - np.outer(dnxdum,xdum))/nxdum**2
483                # Here we may compute finite difference derivatives to check
484                # h = 1e-6
485                # fdxdum = np.zeros((3, 3), dtype=float)
486                # for i in range(3):
487                #     vx[i] += h
488                #     dPlus = np.cross(vx, self.e0)
489                #     dPlus /= np.linalg.norm(dPlus)
490                #     vx[i] -= 2*h
491                #     dMinus = np.cross(vx, self.e0)
492                #     dMinus /= np.linalg.norm(dMinus)
493                #     vx[i] += h
494                #     fdxdum[i] = (dPlus-dMinus)/(2*h)
495                # if np.linalg.norm(dexdum - fdxdum) > 1e-6:
496                #     print dexdum - fdxdum
497                #     raise Exception()
498                # Apply terms from chain rule
499                deriv_raw[0]  -= np.dot(dexdum, deriv_raw[-1])
500                for i in range(len(self.a)):
501                    deriv_raw[i]  += np.dot(np.eye(3), deriv_raw[-1])/len(self.a)
502                deriv_raw[-2] += np.dot(dexdum, deriv_raw[-1])
503                deriv_raw = deriv_raw[:-1]
504            derivatives = np.zeros((xyz.shape[0], 3, 3), dtype=float)
505            for i, a in enumerate(self.a):
506                derivatives[a, :, :] = deriv_raw[i, :, :]
507            self.stored_derxyz = xyz.copy()
508            self.stored_deriv = derivatives.copy()
509            return derivatives
510
511    def second_derivative(self, xyz):
512        xyz = xyz.reshape(-1, 3)
513        if np.max(np.abs(xyz-self.stored_deriv2xyz)) < 1e-12:
514            return self.stored_deriv2
515        else:
516            xsel = xyz[self.a, :]
517            ysel = self.x0[self.a, :]
518            xmean = np.mean(xsel,axis=0)
519            ymean = np.mean(ysel,axis=0)
520            if not self.linear and is_linear(xsel, ysel):
521                # print "Setting linear flag for", self
522                self.linear = True
523            if self.linear:
524                vx = xsel[-1]-xsel[0]
525                vy = ysel[-1]-ysel[0]
526                if self.e0 is None: self.calc_e0()
527                xdum = np.cross(vx, self.e0)
528                ydum = np.cross(vy, self.e0)
529                exdum = xdum / np.linalg.norm(xdum)
530                eydum = ydum / np.linalg.norm(ydum)
531                xsel = np.vstack((xsel, exdum+xmean))
532                ysel = np.vstack((ysel, eydum+ymean))
533            deriv_raw, deriv2_raw = get_expmap_der(xsel, ysel, second=True)
534            if self.linear:
535                # Chain rule is applied to get terms from dummy atom derivatives
536                def dexdum_(vx_):
537                    xdum_ = np.cross(vx_, self.e0)
538                    nxdum_ = np.linalg.norm(xdum_)
539                    dxdum_ = d_cross(vx_, self.e0)
540                    dnxdum_ = d_ncross(vx_, self.e0)
541                    dexdum_ = (dxdum_*nxdum_ - np.outer(dnxdum_,xdum_))/nxdum_**2
542                    return dexdum_.copy()
543                # First indices: elements of vx that are being differentiated w/r.t.
544                # Last index: elements of exdum itself
545                dexdum = dexdum_(vx)
546                dexdum2 = np.zeros((3, 3, 3), dtype=float)
547                h = 1.0e-3
548                for i in range(3):
549                    vx[i] += h
550                    dPlus = dexdum_(vx)
551                    vx[i] -= 2*h
552                    dMinus = dexdum_(vx)
553                    vx[i] += h
554                    dexdum2[i] = (dPlus-dMinus)/(2*h)
555                # Build arrays that contain derivative of dummy atom position
556                # w/r.t. real atom positions
557                ddum1 = np.zeros((len(self.a), 3, 3), dtype=float)
558                ddum1[0] = -dexdum
559                ddum1[-1] = dexdum
560                for i in range(len(self.a)):
561                    ddum1[i] += np.eye(3)/len(self.a)
562                ddum2 = np.zeros((len(self.a), 3, len(self.a), 3, 3), dtype=float)
563                ddum2[ 0, : , 0, :] =  dexdum2
564                ddum2[-1, : , 0, :] = -dexdum2
565                ddum2[ 0, :, -1, :] = -dexdum2
566                ddum2[-1, :, -1, :] =  dexdum2
567                # =====
568                # Do not delete - reference codes using loops for chain rule terms
569                # for j in range(len(self.a)): # Loop over atom 1
570                #     for m in range(3):       # Loop over xyz of atom 1
571                #         for k in range(len(self.a)): # Loop over atom 2
572                #             for n in range(3):       # Loop over xyz of atom 2
573                #                 for i in range(3):   # Loop over elements of exponential map
574                #                     for p in range(3): # Loop over xyz of dummy atom
575                #                         deriv2_raw[j, m, k, n, i] += deriv2_raw[j, m, -1, p, i] * ddum1[k, n, p]
576                #                         deriv2_raw[j, m, k, n, i] += deriv2_raw[-1, p, k, n, i] * ddum1[j, m, p]
577                #                         deriv2_raw[j, m, k, n, i] += deriv_raw[-1, p, i] * ddum2[j, m, k, n, p]
578                #                         for q in range(3):
579                #                             deriv2_raw[j, m, k, n, i] += deriv2_raw[-1, p, -1, q, i] * ddum1[j, m, p] * ddum1[k, n, q]
580                # =====
581                deriv2_raw[:-1, :, :-1, :] += np.einsum('jmpi,knp->jmkni', deriv2_raw[:-1, :, -1, :, :], ddum1, optimize=True)
582                deriv2_raw[:-1, :, :-1, :] += np.einsum('pkni,jmp->jmkni', deriv2_raw[-1, :, :-1, :, :], ddum1, optimize=True)
583                deriv2_raw[:-1, :, :-1, :] += np.einsum('pi,jmknp->jmkni', deriv_raw[-1, :, :], ddum2, optimize=True)
584                deriv2_raw[:-1, :, :-1, :] += np.einsum('pqi,jmp,knq->jmkni', deriv2_raw[-1, :, -1, :, :], ddum1, ddum1, optimize=True)
585                deriv2_raw = deriv2_raw[:-1, :, :-1, :, :]
586            second_derivatives = np.zeros((xyz.shape[0], 3, xyz.shape[0], 3, 3), dtype=float)
587            for i, a in enumerate(self.a):
588                for j, b in enumerate(self.a):
589                    second_derivatives[a, :, b, :, :] = deriv2_raw[i, :, j, :, :]
590            return second_derivatives
591
592class RotationA(object):
593    def __init__(self, a, x0, Rotators, w=1.0):
594        self.a = tuple(sorted(a))
595        self.x0 = x0
596        self.w = w
597        if self.a not in Rotators:
598            Rotators[self.a] = Rotator(self.a, x0)
599        self.Rotator = Rotators[self.a]
600        self.isAngular = True
601        self.isPeriodic = False
602
603    def __repr__(self):
604        # return "Rotation-A %s : Weight %.3f" % (' '.join([str(i+1) for i in self.a]), self.w)
605        return "Rotation-A %s" % (commadash(self.a))
606
607    def __eq__(self, other):
608        if type(self) is not type(other): return False
609        eq = set(self.a) == set(other.a)
610        # if eq and np.sum((self.w-other.w)**2) > 1e-6:
611        #     print "Warning: RotationA same atoms, different weights"
612        # if eq and np.sum((self.x0-other.x0)**2) > 1e-6:
613        #     print "Warning: RotationA same atoms, different reference positions"
614        return eq
615
616    def __ne__(self, other):
617        return not self.__eq__(other)
618
619    def value(self, xyz):
620        return self.Rotator.value(xyz)[0]*self.w
621
622    def derivative(self, xyz):
623        der_all = self.Rotator.derivative(xyz)
624        derivatives = der_all[:, :, 0]*self.w
625        return derivatives
626
627    def second_derivative(self, xyz):
628        deriv2_all = self.Rotator.second_derivative(xyz)
629        second_derivatives = deriv2_all[:, :, :, :, 0]*self.w
630        return second_derivatives
631
632class RotationB(object):
633    def __init__(self, a, x0, Rotators, w=1.0):
634        self.a = tuple(sorted(a))
635        self.x0 = x0
636        self.w = w
637        if self.a not in Rotators:
638            Rotators[self.a] = Rotator(self.a, x0)
639        self.Rotator = Rotators[self.a]
640        self.isAngular = True
641        self.isPeriodic = False
642
643    def __repr__(self):
644        # return "Rotation-B %s : Weight %.3f" % (' '.join([str(i+1) for i in self.a]), self.w)
645        return "Rotation-B %s" % (commadash(self.a))
646
647    def __eq__(self, other):
648        if type(self) is not type(other): return False
649        eq = set(self.a) == set(other.a)
650        # if eq and np.sum((self.w-other.w)**2) > 1e-6:
651        #     print "Warning: RotationB same atoms, different weights"
652        # if eq and np.sum((self.x0-other.x0)**2) > 1e-6:
653        #     print "Warning: RotationB same atoms, different reference positions"
654        return eq
655
656    def __ne__(self, other):
657        return not self.__eq__(other)
658
659    def value(self, xyz):
660        return self.Rotator.value(xyz)[1]*self.w
661
662    def derivative(self, xyz):
663        der_all = self.Rotator.derivative(xyz)
664        derivatives = der_all[:, :, 1]*self.w
665        return derivatives
666
667    def second_derivative(self, xyz):
668        deriv2_all = self.Rotator.second_derivative(xyz)
669        second_derivatives = deriv2_all[:, :, :, :, 1]*self.w
670        return second_derivatives
671
672class RotationC(object):
673    def __init__(self, a, x0, Rotators, w=1.0):
674        self.a = tuple(sorted(a))
675        self.x0 = x0
676        self.w = w
677        if self.a not in Rotators:
678            Rotators[self.a] = Rotator(self.a, x0)
679        self.Rotator = Rotators[self.a]
680        self.isAngular = True
681        self.isPeriodic = False
682
683    def __repr__(self):
684        # return "Rotation-C %s : Weight %.3f" % (' '.join([str(i+1) for i in self.a]), self.w)
685        return "Rotation-C %s" % (commadash(self.a))
686
687    def __eq__(self, other):
688        if type(self) is not type(other): return False
689        eq = set(self.a) == set(other.a)
690        # if eq and np.sum((self.w-other.w)**2) > 1e-6:
691        #     print "Warning: RotationC same atoms, different weights"
692        # if eq and np.sum((self.x0-other.x0)**2) > 1e-6:
693        #     print "Warning: RotationC same atoms, different reference positions"
694        return eq
695
696    def __ne__(self, other):
697        return not self.__eq__(other)
698
699    def value(self, xyz):
700        return self.Rotator.value(xyz)[2]*self.w
701
702    def derivative(self, xyz):
703        der_all = self.Rotator.derivative(xyz)
704        derivatives = der_all[:, :, 2]*self.w
705        return derivatives
706
707    def second_derivative(self, xyz):
708        deriv2_all = self.Rotator.second_derivative(xyz)
709        second_derivatives = deriv2_all[:, :, :, :, 2]*self.w
710        return second_derivatives
711
712class Distance(object):
713    def __init__(self, a, b):
714        self.a = a
715        self.b = b
716        if a == b:
717            raise RuntimeError('a and b must be different')
718        self.isAngular = False
719        self.isPeriodic = False
720
721    def __repr__(self):
722        return "Distance %i-%i" % (self.a+1, self.b+1)
723
724    def __eq__(self, other):
725        if type(self) is not type(other): return False
726        if self.a == other.a:
727            if self.b == other.b:
728                return True
729        if self.a == other.b:
730            if self.b == other.a:
731                return True
732        return False
733
734    def __ne__(self, other):
735        return not self.__eq__(other)
736
737    def value(self, xyz):
738        xyz = xyz.reshape(-1,3)
739        a = self.a
740        b = self.b
741        return np.sqrt(np.sum((xyz[a]-xyz[b])**2))
742
743    def derivative(self, xyz):
744        xyz = xyz.reshape(-1,3)
745        derivatives = np.zeros_like(xyz)
746        m = self.a
747        n = self.b
748        u = (xyz[m] - xyz[n]) / np.linalg.norm(xyz[m] - xyz[n])
749        derivatives[m, :] = u
750        derivatives[n, :] = -u
751        return derivatives
752
753    def second_derivative(self, xyz):
754        xyz = xyz.reshape(-1,3)
755        deriv2 = np.zeros((xyz.shape[0], xyz.shape[1], xyz.shape[0], xyz.shape[1]))
756        m = self.a
757        n = self.b
758        l = np.linalg.norm(xyz[m] - xyz[n])
759        u = (xyz[m] - xyz[n]) / l
760        mtx = (np.outer(u, u) - np.eye(3))/l
761        deriv2[m, :, m, :] = -mtx
762        deriv2[n, :, n, :] = -mtx
763        deriv2[m, :, n, :] = mtx
764        deriv2[n, :, m, :] = mtx
765        return deriv2
766
767class Angle(object):
768    def __init__(self, a, b, c):
769        self.a = a
770        self.b = b
771        self.c = c
772        self.isAngular = True
773        self.isPeriodic = False
774        if len({a, b, c}) != 3:
775            raise RuntimeError('a, b, and c must be different')
776
777    def __repr__(self):
778        return "Angle %i-%i-%i" % (self.a+1, self.b+1, self.c+1)
779
780    def __eq__(self, other):
781        if type(self) is not type(other): return False
782        if self.b == other.b:
783            if self.a == other.a:
784                if self.c == other.c:
785                    return True
786            if self.a == other.c:
787                if self.c == other.a:
788                    return True
789        return False
790
791    def __ne__(self, other):
792        return not self.__eq__(other)
793
794    def value(self, xyz):
795        xyz = xyz.reshape(-1,3)
796        a = self.a
797        b = self.b
798        c = self.c
799        # vector from first atom to central atom
800        vector1 = xyz[a] - xyz[b]
801        # vector from last atom to central atom
802        vector2 = xyz[c] - xyz[b]
803        # norm of the two vectors
804        norm1 = np.sqrt(np.sum(vector1**2))
805        norm2 = np.sqrt(np.sum(vector2**2))
806        dot = np.dot(vector1, vector2)
807        # Catch the edge case that very rarely this number is -1.
808        if dot / (norm1 * norm2) <= -1.0:
809            if (np.abs(dot / (norm1 * norm2)) + 1.0) < -1e-6:
810                raise RuntimeError('Encountered invalid value in angle')
811            return np.pi
812        if dot / (norm1 * norm2) >= 1.0:
813            if (np.abs(dot / (norm1 * norm2)) - 1.0) > 1e-6:
814                raise RuntimeError('Encountered invalid value in angle')
815            return 0.0
816        return np.arccos(dot / (norm1 * norm2))
817
818    def normal_vector(self, xyz):
819        xyz = xyz.reshape(-1,3)
820        a = self.a
821        b = self.b
822        c = self.c
823        # vector from first atom to central atom
824        vector1 = xyz[a] - xyz[b]
825        # vector from last atom to central atom
826        vector2 = xyz[c] - xyz[b]
827        # norm of the two vectors
828        norm1 = np.sqrt(np.sum(vector1**2))
829        norm2 = np.sqrt(np.sum(vector2**2))
830        crs = np.cross(vector1, vector2)
831        crs /= np.linalg.norm(crs)
832        return crs
833
834    def derivative(self, xyz):
835        xyz = xyz.reshape(-1,3)
836        derivatives = np.zeros_like(xyz)
837        m = self.a
838        o = self.b
839        n = self.c
840        # Unit displacement vectors
841        u_prime = (xyz[m] - xyz[o])
842        u_norm = np.linalg.norm(u_prime)
843        v_prime = (xyz[n] - xyz[o])
844        v_norm = np.linalg.norm(v_prime)
845        u = u_prime / u_norm
846        v = v_prime / v_norm
847        VECTOR1 = np.array([1, -1, 1]) / np.sqrt(3)
848        VECTOR2 = np.array([-1, 1, 1]) / np.sqrt(3)
849        if np.linalg.norm(u + v) < 1e-10 or np.linalg.norm(u - v) < 1e-10:
850            # if they're parallel
851            if ((np.linalg.norm(u + VECTOR1) < 1e-10) or
852                    (np.linalg.norm(u - VECTOR2) < 1e-10)):
853                # and they're parallel o [1, -1, 1]
854                w_prime = np.cross(u, VECTOR2)
855            else:
856                w_prime = np.cross(u, VECTOR1)
857        else:
858            w_prime = np.cross(u, v)
859        w = w_prime / np.linalg.norm(w_prime)
860        term1 = np.cross(u, w) / u_norm
861        term2 = np.cross(w, v) / v_norm
862        derivatives[m, :] = term1
863        derivatives[n, :] = term2
864        derivatives[o, :] = -(term1 + term2)
865        return derivatives
866
867    def second_derivative(self, xyz):
868        xyz = xyz.reshape(-1,3)
869        deriv2 = np.zeros((xyz.shape[0], xyz.shape[1], xyz.shape[0], xyz.shape[1]))
870        m = self.a
871        o = self.b
872        n = self.c
873        # Unit displacement vectors
874        u_prime = (xyz[m] - xyz[o])
875        u_norm = np.linalg.norm(u_prime)
876        v_prime = (xyz[n] - xyz[o])
877        v_norm = np.linalg.norm(v_prime)
878        u = u_prime / u_norm
879        v = v_prime / v_norm
880        # Deriv2 derivatives are set to zero in the case of parallel or antiparallel vectors
881        if np.linalg.norm(u + v) < 1e-10 or np.linalg.norm(u - v) < 1e-10:
882            return deriv2
883        # cosine and sine of the bond angle
884        cq = np.dot(u, v)
885        sq = np.sqrt(1-cq**2)
886        uu = np.outer(u, u)
887        uv = np.outer(u, v)
888        vv = np.outer(v, v)
889        de = np.eye(3)
890        term1 = (uv + uv.T - (3*uu - de)*cq)/(u_norm**2*sq)
891        term2 = (uv + uv.T - (3*vv - de)*cq)/(v_norm**2*sq)
892        term3 = (uu + vv - uv*cq   - de)/(u_norm*v_norm*sq)
893        term4 = (uu + vv - uv.T*cq - de)/(u_norm*v_norm*sq)
894        der1 = self.derivative(xyz)
895        def zeta(a_, m_, n_):
896            return (int(a_==m_) - int(a_==n_))
897        for a in [m, n, o]:
898            for b in [m, n, o]:
899                deriv2[a, :, b, :] = (zeta(a, m, o)*zeta(b, m, o)*term1
900                                      + zeta(a, n, o)*zeta(b, n, o)*term2
901                                      + zeta(a, m, o)*zeta(b, n, o)*term3
902                                      + zeta(a, n, o)*zeta(b, m, o)*term4
903                                      - (cq/sq) * np.outer(der1[a], der1[b]))
904        return deriv2
905
906class LinearAngle(object):
907    def __init__(self, a, b, c, axis):
908        self.a = a
909        self.b = b
910        self.c = c
911        self.axis = axis
912        self.isAngular = False
913        self.isPeriodic = False
914        if len({a, b, c}) != 3:
915            raise RuntimeError('a, b, and c must be different')
916        self.e0 = None
917        self.stored_dot2 = 0.0
918
919    def reset(self, xyz):
920        xyz = xyz.reshape(-1,3)
921        a = self.a
922        b = self.b
923        c = self.c
924        # Unit vector pointing from a to c.
925        v = xyz[c] - xyz[a]
926        ev = v / np.linalg.norm(v)
927        # Cartesian axes.
928        ex = np.array([1.0,0.0,0.0])
929        ey = np.array([0.0,1.0,0.0])
930        ez = np.array([0.0,0.0,1.0])
931        self.e0 = [ex, ey, ez][np.argmin([np.dot(i, ev)**2 for i in [ex, ey, ez]])]
932        self.stored_dot2 = 0.0
933
934    def __repr__(self):
935        return "LinearAngle%s %i-%i-%i" % (["X","Y"][self.axis], self.a+1, self.b+1, self.c+1)
936
937    def __eq__(self, other):
938        if not hasattr(other, 'axis'): return False
939        if self.axis is not other.axis: return False
940        if type(self) is not type(other): return False
941        if self.b == other.b:
942            if self.a == other.a:
943                if self.c == other.c:
944                    return True
945            if self.a == other.c:
946                if self.c == other.a:
947                    return True
948        return False
949
950    def __ne__(self, other):
951        return not self.__eq__(other)
952
953    def value(self, xyz):
954        """
955        This function measures the displacement of the BA and BC unit
956        vectors in the linear angle "ABC". The displacements are measured
957        along two axes that are perpendicular to the AC unit vector.
958        """
959        xyz = xyz.reshape(-1,3)
960        a = self.a
961        b = self.b
962        c = self.c
963        # Unit vector pointing from a to c.
964        v = xyz[c] - xyz[a]
965        ev = v / np.linalg.norm(v)
966        if self.e0 is None: self.reset(xyz)
967        e0 = self.e0
968        self.stored_dot2 = np.dot(ev, e0)**2
969        # Now make two unit vectors that are perpendicular to this one.
970        c1 = np.cross(ev, e0)
971        e1 = c1 / np.linalg.norm(c1)
972        c2 = np.cross(ev, e1)
973        e2 = c2 / np.linalg.norm(c2)
974        # BA and BC unit vectors in ABC angle
975        vba = xyz[a]-xyz[b]
976        eba = vba / np.linalg.norm(vba)
977        vbc = xyz[c]-xyz[b]
978        ebc = vbc / np.linalg.norm(vbc)
979        if self.axis == 0:
980            answer = np.dot(eba, e1) + np.dot(ebc, e1)
981        else:
982            answer = np.dot(eba, e2) + np.dot(ebc, e2)
983        return answer
984
985    def derivative(self, xyz):
986        xyz = xyz.reshape(-1,3)
987        a = self.a
988        b = self.b
989        c = self.c
990        derivatives = np.zeros_like(xyz)
991        ## Finite difference derivatives
992        ## fderivatives = np.zeros_like(xyz)
993        ## h = 1e-6
994        ## for u in range(xyz.shape[0]):
995        ##     for v in range(3):
996        ##         xyz[u, v] += h
997        ##         vPlus = self.value(xyz)
998        ##         xyz[u, v] -= 2*h
999        ##         vMinus = self.value(xyz)
1000        ##         xyz[u, v] += h
1001        ##         fderivatives[u, v] = (vPlus-vMinus)/(2*h)
1002        # Unit vector pointing from a to c.
1003        v = xyz[c] - xyz[a]
1004        ev = v / np.linalg.norm(v)
1005        if self.e0 is None: self.reset(xyz)
1006        e0 = self.e0
1007        c1 = np.cross(ev, e0)
1008        e1 = c1 / np.linalg.norm(c1)
1009        c2 = np.cross(ev, e1)
1010        e2 = c2 / np.linalg.norm(c2)
1011        # BA and BC unit vectors in ABC angle
1012        vba = xyz[a]-xyz[b]
1013        eba = vba / np.linalg.norm(vba)
1014        vbc = xyz[c]-xyz[b]
1015        ebc = vbc / np.linalg.norm(vbc)
1016        # Derivative terms
1017        de0 = np.zeros((3, 3), dtype=float)
1018        dev = d_unit_vector(v)
1019        dc1 = d_cross_ab(ev, e0, dev, de0)
1020        de1 = np.dot(dc1, d_unit_vector(c1))
1021        dc2 = d_cross_ab(ev, e1, dev, de1)
1022        de2 = np.dot(dc2, d_unit_vector(c2))
1023        deba = d_unit_vector(vba)
1024        debc = d_unit_vector(vbc)
1025        if self.axis == 0:
1026            derivatives[a, :] = np.dot(deba, e1) + np.dot(-de1, eba) + np.dot(-de1, ebc)
1027            derivatives[b, :] = np.dot(-deba, e1) + np.dot(-debc, e1)
1028            derivatives[c, :] = np.dot(de1, eba) + np.dot(de1, ebc) + np.dot(debc, e1)
1029        else:
1030            derivatives[a, :] = np.dot(deba, e2) + np.dot(-de2, eba) + np.dot(-de2, ebc)
1031            derivatives[b, :] = np.dot(-deba, e2) + np.dot(-debc, e2)
1032            derivatives[c, :] = np.dot(de2, eba) + np.dot(de2, ebc) + np.dot(debc, e2)
1033        ## Finite difference derivatives
1034        ## if np.linalg.norm(derivatives - fderivatives) > 1e-6:
1035        ##     print np.linalg.norm(derivatives - fderivatives)
1036        ##     raise Exception()
1037        return derivatives
1038
1039    def second_derivative(self, xyz):
1040        xyz = xyz.reshape(-1,3)
1041        a = self.a
1042        b = self.b
1043        c = self.c
1044        deriv2 = np.zeros((xyz.shape[0], 3, xyz.shape[0], 3), dtype=float)
1045        h = 1.0e-3
1046        for i in range(3):
1047            for j in range(3):
1048                ii = [a, b, c][i]
1049                xyz[ii, j] += h
1050                FPlus = self.derivative(xyz)
1051                xyz[ii, j] -= 2*h
1052                FMinus = self.derivative(xyz)
1053                xyz[ii, j] += h
1054                fderiv = (FPlus-FMinus)/(2*h)
1055                deriv2[ii, j, :, :] = fderiv
1056        return deriv2
1057
1058class MultiAngle(object):
1059    def __init__(self, a, b, c):
1060        if type(a) is int:
1061            a = (a,)
1062        if type(c) is int:
1063            c = (c,)
1064        self.a = tuple(a)
1065        self.b = b
1066        self.c = tuple(c)
1067        self.isAngular = True
1068        self.isPeriodic = False
1069        if len({a, b, c}) != 3:
1070            raise RuntimeError('a, b, and c must be different')
1071
1072    def __repr__(self):
1073        stra = ("("+','.join(["%i" % (i+1) for i in self.a])+")") if len(self.a) > 1 else "%i" % (self.a[0]+1)
1074        strc = ("("+','.join(["%i" % (i+1) for i in self.c])+")") if len(self.c) > 1 else "%i" % (self.c[0]+1)
1075        return "%sAngle %s-%i-%s" % ("Multi" if (len(self.a) > 1 or len(self.c) > 1) else "", stra, self.b+1, strc)
1076
1077    def __eq__(self, other):
1078        if type(self) is not type(other): return False
1079        if self.b == other.b:
1080            if set(self.a) == set(other.a):
1081                if set(self.c) == set(other.c):
1082                    return True
1083            if set(self.a) == set(other.c):
1084                if set(self.c) == set(other.a):
1085                    return True
1086        return False
1087
1088    def __ne__(self, other):
1089        return not self.__eq__(other)
1090
1091    def value(self, xyz):
1092        xyz = xyz.reshape(-1,3)
1093        a = np.array(self.a)
1094        b = self.b
1095        c = np.array(self.c)
1096        xyza = np.mean(xyz[a], axis=0)
1097        xyzc = np.mean(xyz[c], axis=0)
1098        # vector from first atom to central atom
1099        vector1 = xyza - xyz[b]
1100        # vector from last atom to central atom
1101        vector2 = xyzc - xyz[b]
1102        # norm of the two vectors
1103        norm1 = np.sqrt(np.sum(vector1**2))
1104        norm2 = np.sqrt(np.sum(vector2**2))
1105        dot = np.dot(vector1, vector2)
1106        # Catch the edge case that very rarely this number is -1.
1107        if dot / (norm1 * norm2) <= -1.0:
1108            if (np.abs(dot / (norm1 * norm2)) + 1.0) < -1e-6:
1109                raise RuntimeError('Encountered invalid value in angle')
1110            return np.pi
1111        return np.arccos(dot / (norm1 * norm2))
1112
1113    def normal_vector(self, xyz):
1114        xyz = xyz.reshape(-1,3)
1115        a = np.array(self.a)
1116        b = self.b
1117        c = np.array(self.c)
1118        xyza = np.mean(xyz[a], axis=0)
1119        xyzc = np.mean(xyz[c], axis=0)
1120        # vector from first atom to central atom
1121        vector1 = xyza - xyz[b]
1122        # vector from last atom to central atom
1123        vector2 = xyzc - xyz[b]
1124        # norm of the two vectors
1125        norm1 = np.sqrt(np.sum(vector1**2))
1126        norm2 = np.sqrt(np.sum(vector2**2))
1127        crs = np.cross(vector1, vector2)
1128        crs /= np.linalg.norm(crs)
1129        return crs
1130
1131    def derivative(self, xyz):
1132        xyz = xyz.reshape(-1,3)
1133        derivatives = np.zeros_like(xyz)
1134        m = np.array(self.a)
1135        o = self.b
1136        n = np.array(self.c)
1137        xyzm = np.mean(xyz[m], axis=0)
1138        xyzn = np.mean(xyz[n], axis=0)
1139        # Unit displacement vectors
1140        u_prime = (xyzm - xyz[o])
1141        u_norm = np.linalg.norm(u_prime)
1142        v_prime = (xyzn - xyz[o])
1143        v_norm = np.linalg.norm(v_prime)
1144        u = u_prime / u_norm
1145        v = v_prime / v_norm
1146        VECTOR1 = np.array([1, -1, 1]) / np.sqrt(3)
1147        VECTOR2 = np.array([-1, 1, 1]) / np.sqrt(3)
1148        if np.linalg.norm(u + v) < 1e-10 or np.linalg.norm(u - v) < 1e-10:
1149            # if they're parallel
1150            if ((np.linalg.norm(u + VECTOR1) < 1e-10) or
1151                    (np.linalg.norm(u - VECTOR2) < 1e-10)):
1152                # and they're parallel o [1, -1, 1]
1153                w_prime = np.cross(u, VECTOR2)
1154            else:
1155                w_prime = np.cross(u, VECTOR1)
1156        else:
1157            w_prime = np.cross(u, v)
1158        w = w_prime / np.linalg.norm(w_prime)
1159        term1 = np.cross(u, w) / u_norm
1160        term2 = np.cross(w, v) / v_norm
1161        for i in m:
1162            derivatives[i, :] = term1/len(m)
1163        for i in n:
1164            derivatives[i, :] = term2/len(n)
1165        derivatives[o, :] = -(term1 + term2)
1166        return derivatives
1167
1168    def second_derivative(self, xyz):
1169        raise NotImplementedError("Second derivatives have not been implemented for IC type %s" % self.__name__)
1170
1171class Dihedral(object):
1172    def __init__(self, a, b, c, d):
1173        self.a = a
1174        self.b = b
1175        self.c = c
1176        self.d = d
1177        self.isAngular = True
1178        self.isPeriodic = True
1179        if len({a, b, c, d}) != 4:
1180            raise RuntimeError('a, b, c and d must be different')
1181
1182    def __repr__(self):
1183        return "Dihedral %i-%i-%i-%i" % (self.a+1, self.b+1, self.c+1, self.d+1)
1184
1185    def __eq__(self, other):
1186        if type(self) is not type(other): return False
1187        if self.a == other.a:
1188            if self.b == other.b:
1189                if self.c == other.c:
1190                    if self.d == other.d:
1191                        return True
1192        if self.a == other.d:
1193            if self.b == other.c:
1194                if self.c == other.b:
1195                    if self.d == other.a:
1196                        return True
1197        return False
1198
1199    def __ne__(self, other):
1200        return not self.__eq__(other)
1201
1202    def value(self, xyz):
1203        xyz = xyz.reshape(-1,3)
1204        a = self.a
1205        b = self.b
1206        c = self.c
1207        d = self.d
1208        vec1 = xyz[b] - xyz[a]
1209        vec2 = xyz[c] - xyz[b]
1210        vec3 = xyz[d] - xyz[c]
1211        cross1 = np.cross(vec2, vec3)
1212        cross2 = np.cross(vec1, vec2)
1213        arg1 = np.sum(np.multiply(vec1, cross1)) * \
1214               np.sqrt(np.sum(vec2**2))
1215        arg2 = np.sum(np.multiply(cross1, cross2))
1216        answer = np.arctan2(arg1, arg2)
1217        return answer
1218
1219    def derivative(self, xyz):
1220        xyz = xyz.reshape(-1,3)
1221        derivatives = np.zeros_like(xyz)
1222        m = self.a
1223        o = self.b
1224        p = self.c
1225        n = self.d
1226        u_prime = (xyz[m] - xyz[o])
1227        w_prime = (xyz[p] - xyz[o])
1228        v_prime = (xyz[n] - xyz[p])
1229        u_norm = np.linalg.norm(u_prime)
1230        w_norm = np.linalg.norm(w_prime)
1231        v_norm = np.linalg.norm(v_prime)
1232        u = u_prime / u_norm
1233        w = w_prime / w_norm
1234        v = v_prime / v_norm
1235        if (1 - np.dot(u, w)**2) < 1e-6:
1236            term1 = np.cross(u, w) * 0
1237            term3 = np.cross(u, w) * 0
1238        else:
1239            term1 = np.cross(u, w) / (u_norm * (1 - np.dot(u, w)**2))
1240            term3 = np.cross(u, w) * np.dot(u, w) / (w_norm * (1 - np.dot(u, w)**2))
1241        if (1 - np.dot(v, w)**2) < 1e-6:
1242            term2 = np.cross(v, w) * 0
1243            term4 = np.cross(v, w) * 0
1244        else:
1245            term2 = np.cross(v, w) / (v_norm * (1 - np.dot(v, w)**2))
1246            term4 = np.cross(v, w) * np.dot(v, w) / (w_norm * (1 - np.dot(v, w)**2))
1247        # term1 = np.cross(u, w) / (u_norm * (1 - np.dot(u, w)**2))
1248        # term2 = np.cross(v, w) / (v_norm * (1 - np.dot(v, w)**2))
1249        # term3 = np.cross(u, w) * np.dot(u, w) / (w_norm * (1 - np.dot(u, w)**2))
1250        # term4 = np.cross(v, w) * np.dot(v, w) / (w_norm * (1 - np.dot(v, w)**2))
1251        derivatives[m, :] = term1
1252        derivatives[n, :] = -term2
1253        derivatives[o, :] = -term1 + term3 - term4
1254        derivatives[p, :] = term2 - term3 + term4
1255        return derivatives
1256
1257    def second_derivative(self, xyz):
1258        xyz = xyz.reshape(-1,3)
1259        deriv2 = np.zeros((xyz.shape[0], xyz.shape[1], xyz.shape[0], xyz.shape[1]))
1260        m = self.a
1261        o = self.b
1262        p = self.c
1263        n = self.d
1264        u_prime = (xyz[m] - xyz[o])
1265        w_prime = (xyz[p] - xyz[o])
1266        v_prime = (xyz[n] - xyz[p])
1267        lu = np.linalg.norm(u_prime)
1268        lw = np.linalg.norm(w_prime)
1269        lv = np.linalg.norm(v_prime)
1270        u = u_prime / lu
1271        w = w_prime / lw
1272        v = v_prime / lv
1273        cu = np.dot(u, w)
1274        su = (1 - np.dot(u, w)**2)**0.5
1275        su4 = su**4
1276        cv = np.dot(v, w)
1277        sv = (1 - np.dot(v, w)**2)**0.5
1278        sv4 = sv**4
1279        if su < 1e-6 or sv < 1e-6 : return deriv2
1280
1281        uxw = np.cross(u, w)
1282        vxw = np.cross(v, w)
1283
1284        term1 = np.outer(uxw, w*cu - u)/(lu**2*su4)
1285        term2 = np.outer(vxw, -w*cv + v)/(lv**2*sv4)
1286        term3 = np.outer(uxw, w - 2*u*cu + w*cu**2)/(2*lu*lw*su4)
1287        term4 = np.outer(vxw, w - 2*v*cv + w*cv**2)/(2*lv*lw*sv4)
1288        term5 = np.outer(uxw, u + u*cu**2 - 3*w*cu + w*cu**3)/(2*lw**2*su4)
1289        term6 = np.outer(vxw,-v - v*cv**2 + 3*w*cv - w*cv**3)/(2*lw**2*sv4)
1290        term1 += term1.T
1291        term2 += term2.T
1292        term3 += term3.T
1293        term4 += term4.T
1294        term5 += term5.T
1295        term6 += term6.T
1296        def mk_amat(vec):
1297            amat = np.zeros((3,3))
1298            for i in range(3):
1299                for j in range(3):
1300                    if i == j: continue
1301                    k = 3 - i - j
1302                    amat[i, j] = vec[k] * (j-i) * ((-0.5)**np.abs(j-i))
1303            return amat
1304        term7 = mk_amat((-w*cu + u)/(lu*lw*su**2))
1305        term8 = mk_amat(( w*cv - v)/(lv*lw*sv**2))
1306        def zeta(a_, m_, n_):
1307            return (int(a_==m_) - int(a_==n_))
1308        # deriv2_terms = [np.zeros_like(deriv2) for i in range(9)]
1309        # Accumulate the second derivative
1310        for a in [m, n, o, p]:
1311            for b in [m, n, o, p]:
1312                deriv2[a, :, b, :] = (zeta(a, m, o)*zeta(b, m, o)*term1 +
1313                                      zeta(a, n, p)*zeta(b, n, p)*term2 +
1314                                      (zeta(a, m, o)*zeta(b, o, p) + zeta(a, p, o)*zeta(b, o, m))*term3 +
1315                                      (zeta(a, n, p)*zeta(b, p, o) + zeta(a, p, o)*zeta(b, n, p))*term4 +
1316                                      zeta(a, o, p)*zeta(b, p, o)*term5 +
1317                                      zeta(a, p, o)*zeta(b, o, p)*term6)
1318                if a != b:
1319                    deriv2[a, :, b, :] += ((zeta(a, m, o)*zeta(b, p, o) + zeta(a, p, o)*zeta(b, o, m))*term7 +
1320                                           (zeta(a, n, o)*zeta(b, p, o) + zeta(a, p, o)*zeta(b, o, n))*term8)
1321        return deriv2
1322
1323        # Accumulate a dictionary of contributions to the second derivatives by term (for debugging)
1324        #             deriv2_terms[7][a, :, b, :] = (zeta(a, m, o)*zeta(b, p, o) + zeta(a, p, o)*zeta(b, o, m))*term7
1325        #             deriv2_terms[8][a, :, b, :] = (zeta(a, n, o)*zeta(b, p, o) + zeta(a, p, o)*zeta(b, o, n))*term8
1326        #         deriv2_terms[1][a, :, b, :] = zeta(a, m, o)*zeta(b, m, o)*term1
1327        #         deriv2_terms[2][a, :, b, :] = zeta(a, n, p)*zeta(b, n, p)*term2
1328        #         deriv2_terms[3][a, :, b, :] = (zeta(a, m, o)*zeta(b, o, p) + zeta(a, p, o)*zeta(b, o, m))*term3
1329        #         deriv2_terms[4][a, :, b, :] = (zeta(a, n, p)*zeta(b, p, o) + zeta(a, p, o)*zeta(b, n, p))*term4
1330        #         deriv2_terms[5][a, :, b, :] = zeta(a, o, p)*zeta(b, p, o)*term5
1331        #         deriv2_terms[6][a, :, b, :] = zeta(a, p, o)*zeta(b, o, p)*term6
1332        # deriv2_terms[0] = deriv2.copy()
1333        #
1334        #=======
1335        # Term-by-term checking of the second derivative.
1336        # Produces output such as:
1337        # 1x1x a:  0.0000 n:  0.0000 e:  0.0000 Terms: NNNNNNNN  0.0000  0.0000 -0.0000 -0.0000 -0.0000 -0.0000  0.0000  0.0000
1338        # 1x1y a:  0.3337 n:  0.3337 e:  0.0000 Terms: YNNNNNNN  0.3337  0.0000 -0.0000 -0.0000 -0.0000 -0.0000  0.0000  0.0000
1339        # 1x1z a:  0.0590 n:  0.0590 e: -0.0000 Terms: YNNNNNNN  0.0590  0.0000 -0.0000  0.0000 -0.0000  0.0000  0.0000  0.0000
1340        #
1341        # def printTerm(strin, num):
1342        #     i = int(strin[0])-1
1343        #     j = 'xyz'.index(strin[1])
1344        #     k = int(strin[2])-1
1345        #     l = 'xyz'.index(strin[3])
1346        #     ana = deriv2_terms[0][i,j,k,l]
1347        #     err = ana-num
1348        #     correct = np.abs(num-ana) < 1e-5
1349        #     color = '\x1b[92m' if correct else '\x1b[91m'
1350        #     print('%i%s%i%s a: % .4f n: % .4f e: % .4f Terms: ' % (i+1, 'xyz'[j], k+1, 'xyz'[l], ana, num, err) +
1351        #           ''.join(["Y" if np.abs(deriv2_terms[m][i,j,k,l]) > 1e-5 else "N" for m in range(1, 9)]) + ' ' +
1352        #           ' '.join(["%s% .4f\x1b[0m" % (color if np.abs(deriv2_terms[m][i,j,k,l]) > 1e-5 else '',
1353        #                                         deriv2_terms[m][i,j,k,l]) for m in range(1, 9)]))
1354        # print("LP checking single term:")
1355        # printTerm('1x1x',  5.55112e-09)
1356        # printTerm('1x1y',  3.33702e-01)
1357        # printTerm('1x1z',  5.90389e-02)
1358
1359class MultiDihedral(object):
1360    def __init__(self, a, b, c, d):
1361        if type(a) is int:
1362            a = (a, )
1363        if type(d) is int:
1364            d = (d, )
1365        self.a = tuple(a)
1366        self.b = b
1367        self.c = c
1368        self.d = tuple(d)
1369        self.isAngular = True
1370        self.isPeriodic = True
1371        if len({a, b, c, d}) != 4:
1372            raise RuntimeError('a, b, c and d must be different')
1373
1374    def __repr__(self):
1375        stra = ("("+','.join(["%i" % (i+1) for i in self.a])+")") if len(self.a) > 1 else "%i" % (self.a[0]+1)
1376        strd = ("("+','.join(["%i" % (i+1) for i in self.d])+")") if len(self.d) > 1 else "%i" % (self.d[0]+1)
1377        return "%sDihedral %s-%i-%i-%s" % ("Multi" if (len(self.a) > 1 or len(self.d) > 1) else "", stra, self.b+1, self.c+1, strd)
1378
1379    def __eq__(self, other):
1380        if type(self) is not type(other): return False
1381        if set(self.a) == set(other.a):
1382            if self.b == other.b:
1383                if self.c == other.c:
1384                    if set(self.d) == set(other.d):
1385                        return True
1386        if set(self.a) == set(other.d):
1387            if self.b == other.c:
1388                if self.c == other.b:
1389                    if set(self.d) == set(other.a):
1390                        return True
1391        return False
1392
1393    def __ne__(self, other):
1394        return not self.__eq__(other)
1395
1396    def value(self, xyz):
1397        xyz = xyz.reshape(-1,3)
1398        a = np.array(self.a)
1399        b = self.b
1400        c = self.c
1401        d = np.array(self.d)
1402        xyza = np.mean(xyz[a], axis=0)
1403        xyzd = np.mean(xyz[d], axis=0)
1404
1405        vec1 = xyz[b] - xyza
1406        vec2 = xyz[c] - xyz[b]
1407        vec3 = xyzd - xyz[c]
1408        cross1 = np.cross(vec2, vec3)
1409        cross2 = np.cross(vec1, vec2)
1410        arg1 = np.sum(np.multiply(vec1, cross1)) * \
1411               np.sqrt(np.sum(vec2**2))
1412        arg2 = np.sum(np.multiply(cross1, cross2))
1413        answer = np.arctan2(arg1, arg2)
1414        return answer
1415
1416    def derivative(self, xyz):
1417        xyz = xyz.reshape(-1,3)
1418        derivatives = np.zeros_like(xyz)
1419        m = np.array(self.a)
1420        o = self.b
1421        p = self.c
1422        n = np.array(self.d)
1423        xyzm = np.mean(xyz[m], axis=0)
1424        xyzn = np.mean(xyz[n], axis=0)
1425
1426        u_prime = (xyzm - xyz[o])
1427        w_prime = (xyz[p] - xyz[o])
1428        v_prime = (xyzn - xyz[p])
1429        u_norm = np.linalg.norm(u_prime)
1430        w_norm = np.linalg.norm(w_prime)
1431        v_norm = np.linalg.norm(v_prime)
1432        u = u_prime / u_norm
1433        w = w_prime / w_norm
1434        v = v_prime / v_norm
1435        if (1 - np.dot(u, w)**2) < 1e-6:
1436            term1 = np.cross(u, w) * 0
1437            term3 = np.cross(u, w) * 0
1438        else:
1439            term1 = np.cross(u, w) / (u_norm * (1 - np.dot(u, w)**2))
1440            term3 = np.cross(u, w) * np.dot(u, w) / (w_norm * (1 - np.dot(u, w)**2))
1441        if (1 - np.dot(v, w)**2) < 1e-6:
1442            term2 = np.cross(v, w) * 0
1443            term4 = np.cross(v, w) * 0
1444        else:
1445            term2 = np.cross(v, w) / (v_norm * (1 - np.dot(v, w)**2))
1446            term4 = np.cross(v, w) * np.dot(v, w) / (w_norm * (1 - np.dot(v, w)**2))
1447        # term1 = np.cross(u, w) / (u_norm * (1 - np.dot(u, w)**2))
1448        # term2 = np.cross(v, w) / (v_norm * (1 - np.dot(v, w)**2))
1449        # term3 = np.cross(u, w) * np.dot(u, w) / (w_norm * (1 - np.dot(u, w)**2))
1450        # term4 = np.cross(v, w) * np.dot(v, w) / (w_norm * (1 - np.dot(v, w)**2))
1451        for i in self.a:
1452            derivatives[i, :] = term1/len(self.a)
1453        for i in self.d:
1454            derivatives[i, :] = -term2/len(self.d)
1455        derivatives[o, :] = -term1 + term3 - term4
1456        derivatives[p, :] = term2 - term3 + term4
1457        return derivatives
1458
1459    def second_derivative(self, xyz):
1460        raise NotImplementedError("Second derivatives have not been implemented for IC type %s" % self.__name__)
1461
1462class OutOfPlane(object):
1463    def __init__(self, a, b, c, d):
1464        self.a = a
1465        self.b = b
1466        self.c = c
1467        self.d = d
1468        self.isAngular = True
1469        self.isPeriodic = True
1470        if len({a, b, c, d}) != 4:
1471            raise RuntimeError('a, b, c and d must be different')
1472
1473    def __repr__(self):
1474        return "Out-of-Plane %i-%i-%i-%i" % (self.a+1, self.b+1, self.c+1, self.d+1)
1475
1476    def __eq__(self, other):
1477        if type(self) is not type(other): return False
1478        if self.a == other.a:
1479            if {self.b, self.c, self.d} == {other.b, other.c, other.d}:
1480                if [self.b, self.c, self.d] != [other.b, other.c, other.d]:
1481                    logger.warning("Warning: OutOfPlane atoms are the same, ordering is different\n")
1482                return True
1483        #     if self.b == other.b:
1484        #         if self.c == other.c:
1485        #             if self.d == other.d:
1486        #                 return True
1487        # if self.a == other.d:
1488        #     if self.b == other.c:
1489        #         if self.c == other.b:
1490        #             if self.d == other.a:
1491        #                 return True
1492        return False
1493
1494    def __ne__(self, other):
1495        return not self.__eq__(other)
1496
1497    def value(self, xyz):
1498        xyz = xyz.reshape(-1,3)
1499        a = self.a
1500        b = self.b
1501        c = self.c
1502        d = self.d
1503        vec1 = xyz[b] - xyz[a]
1504        vec2 = xyz[c] - xyz[b]
1505        vec3 = xyz[d] - xyz[c]
1506        cross1 = np.cross(vec2, vec3)
1507        cross2 = np.cross(vec1, vec2)
1508        arg1 = np.sum(np.multiply(vec1, cross1)) * \
1509               np.sqrt(np.sum(vec2**2))
1510        arg2 = np.sum(np.multiply(cross1, cross2))
1511        answer = np.arctan2(arg1, arg2)
1512        return answer
1513
1514    def derivative(self, xyz):
1515        xyz = xyz.reshape(-1,3)
1516        derivatives = np.zeros_like(xyz)
1517        m = self.a
1518        o = self.b
1519        p = self.c
1520        n = self.d
1521        u_prime = (xyz[m] - xyz[o])
1522        w_prime = (xyz[p] - xyz[o])
1523        v_prime = (xyz[n] - xyz[p])
1524        u_norm = np.linalg.norm(u_prime)
1525        w_norm = np.linalg.norm(w_prime)
1526        v_norm = np.linalg.norm(v_prime)
1527        u = u_prime / u_norm
1528        w = w_prime / w_norm
1529        v = v_prime / v_norm
1530        if (1 - np.dot(u, w)**2) < 1e-6:
1531            term1 = np.cross(u, w) * 0
1532            term3 = np.cross(u, w) * 0
1533        else:
1534            term1 = np.cross(u, w) / (u_norm * (1 - np.dot(u, w)**2))
1535            term3 = np.cross(u, w) * np.dot(u, w) / (w_norm * (1 - np.dot(u, w)**2))
1536        if (1 - np.dot(v, w)**2) < 1e-6:
1537            term2 = np.cross(v, w) * 0
1538            term4 = np.cross(v, w) * 0
1539        else:
1540            term2 = np.cross(v, w) / (v_norm * (1 - np.dot(v, w)**2))
1541            term4 = np.cross(v, w) * np.dot(v, w) / (w_norm * (1 - np.dot(v, w)**2))
1542        # term1 = np.cross(u, w) / (u_norm * (1 - np.dot(u, w)**2))
1543        # term2 = np.cross(v, w) / (v_norm * (1 - np.dot(v, w)**2))
1544        # term3 = np.cross(u, w) * np.dot(u, w) / (w_norm * (1 - np.dot(u, w)**2))
1545        # term4 = np.cross(v, w) * np.dot(v, w) / (w_norm * (1 - np.dot(v, w)**2))
1546        derivatives[m, :] = term1
1547        derivatives[n, :] = -term2
1548        derivatives[o, :] = -term1 + term3 - term4
1549        derivatives[p, :] = term2 - term3 + term4
1550        return derivatives
1551
1552    def second_derivative(self, xyz):
1553        xyz = xyz.reshape(-1,3)
1554        a = self.a
1555        b = self.b
1556        c = self.c
1557        d = self.d
1558        deriv2 = np.zeros((xyz.shape[0], 3, xyz.shape[0], 3), dtype=float)
1559        h = 1.0e-3
1560        for i in range(4):
1561            for j in range(3):
1562                ii = [a, b, c, d][i]
1563                xyz[ii, j] += h
1564                FPlus = self.derivative(xyz)
1565                xyz[ii, j] -= 2*h
1566                FMinus = self.derivative(xyz)
1567                xyz[ii, j] += h
1568                fderiv = (FPlus-FMinus)/(2*h)
1569                deriv2[ii, j, :, :] = fderiv
1570        return deriv2
1571
1572CacheWarning = False
1573
1574class InternalCoordinates(object):
1575    def __init__(self):
1576        self.stored_wilsonB = OrderedDict()
1577
1578    def addConstraint(self, cPrim, cVal):
1579        raise NotImplementedError("Constraints not supported with Cartesian coordinates")
1580
1581    def haveConstraints(self):
1582        raise NotImplementedError("Constraints not supported with Cartesian coordinates")
1583
1584    def augmentGH(self, xyz, G, H):
1585        raise NotImplementedError("Constraints not supported with Cartesian coordinates")
1586
1587    def calcGradProj(self, xyz, gradx):
1588        raise NotImplementedError("Constraints not supported with Cartesian coordinates")
1589
1590    def clearCache(self):
1591        self.stored_wilsonB = OrderedDict()
1592
1593    def wilsonB(self, xyz):
1594        """
1595        Given Cartesian coordinates xyz, return the Wilson B-matrix
1596        given by dq_i/dx_j where x is flattened (i.e. x1, y1, z1, x2, y2, z2)
1597        """
1598        global CacheWarning
1599        t0 = time.time()
1600        xhash = hash(xyz.tostring())
1601        ht = time.time() - t0
1602        if xhash in self.stored_wilsonB:
1603            ans = self.stored_wilsonB[xhash]
1604            return ans
1605        WilsonB = []
1606        Der = self.derivatives(xyz)
1607        for i in range(Der.shape[0]):
1608            WilsonB.append(Der[i].flatten())
1609        self.stored_wilsonB[xhash] = np.array(WilsonB)
1610        if len(self.stored_wilsonB) > 1000 and not CacheWarning:
1611            logger.warning("\x1b[91mWarning: more than 1000 B-matrices stored, memory leaks likely\x1b[0m\n")
1612            CacheWarning = True
1613        ans = np.array(WilsonB)
1614        return ans
1615
1616    def GMatrix(self, xyz):
1617        """
1618        Given Cartesian coordinates xyz, return the G-matrix
1619        given by G = BuBt where u is an arbitrary matrix (default to identity)
1620        """
1621        Bmat = self.wilsonB(xyz)
1622        BuBt = np.dot(Bmat,Bmat.T)
1623        return BuBt
1624
1625    def GInverse_SVD(self, xyz):
1626        xyz = xyz.reshape(-1,3)
1627        # Perform singular value decomposition
1628        click()
1629        loops = 0
1630        while True:
1631            try:
1632                G = self.GMatrix(xyz)
1633                time_G = click()
1634                U, S, VT = np.linalg.svd(G)
1635                time_svd = click()
1636            except np.linalg.LinAlgError:
1637                logger.warning("\x1b[1;91m SVD fails, perturbing coordinates and trying again\x1b[0m\n")
1638                xyz = xyz + 1e-2*np.random.random(xyz.shape)
1639                loops += 1
1640                if loops == 10:
1641                    raise RuntimeError('SVD failed too many times')
1642                continue
1643            break
1644        # print "Build G: %.3f SVD: %.3f" % (time_G, time_svd),
1645        V = VT.T
1646        UT = U.T
1647        Sinv = np.zeros_like(S)
1648        LargeVals = 0
1649        for ival, value in enumerate(S):
1650            # print "%.5e % .5e" % (ival,value)
1651            if np.abs(value) > 1e-6:
1652                LargeVals += 1
1653                Sinv[ival] = 1/value
1654        # print "%i atoms; %i/%i singular values are > 1e-6" % (xyz.shape[0], LargeVals, len(S))
1655        Sinv = np.diag(Sinv)
1656        Inv = multi_dot([V, Sinv, UT])
1657        return Inv
1658
1659    def GInverse_EIG(self, xyz):
1660        xyz = xyz.reshape(-1,3)
1661        click()
1662        G = self.GMatrix(xyz)
1663        time_G = click()
1664        Gi = np.linalg.inv(G)
1665        time_inv = click()
1666        # print "G-time: %.3f Inv-time: %.3f" % (time_G, time_inv)
1667        return Gi
1668
1669    def checkFiniteDifferenceGrad(self, xyz):
1670        xyz = xyz.reshape(-1,3)
1671        Analytical = self.derivatives(xyz)
1672        FiniteDifference = np.zeros_like(Analytical)
1673        h = 1e-5
1674        for i in range(xyz.shape[0]):
1675            for j in range(3):
1676                x1 = xyz.copy()
1677                x2 = xyz.copy()
1678                x1[i,j] += h
1679                x2[i,j] -= h
1680                PMDiff = self.calcDiff(x1,x2)
1681                FiniteDifference[:,i,j] = PMDiff/(2*h)
1682        logger.info("-=# Now checking first derivatives of internal coordinates w/r.t. Cartesians #=-\n")
1683        for i in range(Analytical.shape[0]):
1684            title = "%20s : %20s" % ("IC %i/%i" % (i+1, Analytical.shape[0]), self.Internals[i])
1685            lines = [title]
1686            maxerr = 0.0
1687            for j in range(Analytical.shape[1]):
1688                lines.append("Atom %i" % (j+1))
1689                for k in range(Analytical.shape[2]):
1690                    error = Analytical[i,j,k] - FiniteDifference[i,j,k]
1691                    if np.abs(error) > 1e-5:
1692                        color = "\x1b[91m"
1693                    else:
1694                        color = "\x1b[92m"
1695                    lines.append("%s % .5e % .5e %s% .5e\x1b[0m" % ("xyz"[k], Analytical[i,j,k], FiniteDifference[i,j,k], color, Analytical[i,j,k] - FiniteDifference[i,j,k]))
1696                    if maxerr < np.abs(error):
1697                        maxerr = np.abs(error)
1698            if maxerr > 1e-5:
1699                logger.info('\n'.join(lines)+'\n')
1700            logger.info("%s : Max Error = %.5e\n" % (title, maxerr))
1701        logger.info("Finite-difference Finished\n")
1702        return FiniteDifference
1703
1704    def checkFiniteDifferenceHess(self, xyz):
1705        xyz = xyz.reshape(-1,3)
1706        Analytical = self.second_derivatives(xyz)
1707        FiniteDifference = np.zeros_like(Analytical)
1708        h = 1e-4
1709        verbose = False
1710        logger.info("-=# Now checking second derivatives of internal coordinates w/r.t. Cartesians #=-\n")
1711        for j in range(xyz.shape[0]):
1712            for m in range(3):
1713                for k in range(xyz.shape[0]):
1714                    for n in range(3):
1715                        x1 = xyz.copy()
1716                        x2 = xyz.copy()
1717                        x3 = xyz.copy()
1718                        x4 = xyz.copy()
1719                        x1[j, m] += h
1720                        x1[k, n] += h # (+, +)
1721                        x2[j, m] += h
1722                        x2[k, n] -= h # (+, -)
1723                        x3[j, m] -= h
1724                        x3[k, n] += h # (-, +)
1725                        x4[j, m] -= h
1726                        x4[k, n] -= h # (-, -)
1727                        PMDiff1 = self.calcDiff(x1, x2)
1728                        PMDiff2 = self.calcDiff(x4, x3)
1729                        FiniteDifference[:, j, m, k, n] += (PMDiff1+PMDiff2)/(4*h**2)
1730        #                 print('\r%i %i' % (j, k), end='')
1731        # print()
1732        for i in range(Analytical.shape[0]):
1733            title = "%20s : %20s" % ("IC %i/%i" % (i+1, Analytical.shape[0]), self.Internals[i])
1734            lines = [title]
1735            if verbose: logger.info(title+'\n')
1736            maxerr = 0.0
1737            numerr = 0
1738            for j in range(Analytical.shape[1]):
1739                for m in range(Analytical.shape[2]):
1740                    for k in range(Analytical.shape[3]):
1741                        for n in range(Analytical.shape[4]):
1742                            ana = Analytical[i,j,m,k,n]
1743                            fin = FiniteDifference[i,j,m,k,n]
1744                            error = ana - fin
1745                            message = "Atom %i %s %i %s a: % 12.5e n: % 12.5e e: % 12.5e %s" % (j+1, 'xyz'[m], k+1, 'xyz'[n], ana, fin,
1746                                                                                                error, 'X' if np.abs(error)>1e-5 else '')
1747                            if np.abs(error)>1e-5:
1748                                numerr += 1
1749                            if (ana != 0.0 or fin != 0.0) and verbose:
1750                                logger.info(message+'\n')
1751                            lines.append(message)
1752                            if maxerr < np.abs(error):
1753                                maxerr = np.abs(error)
1754            if maxerr > 1e-5 and not verbose:
1755                logger.info('\n'.join(lines)+'\n')
1756            logger.info("%s : Max Error = % 12.5e (%i above threshold)\n" % (title, maxerr, numerr))
1757        logger.info("Finite-difference Finished\n")
1758        return FiniteDifference
1759
1760    def calcGrad(self, xyz, gradx):
1761        q0 = self.calculate(xyz)
1762        Ginv = self.GInverse(xyz)
1763        Bmat = self.wilsonB(xyz)
1764        # Internal coordinate gradient
1765        # Gq = np.matrix(Ginv)*np.matrix(Bmat)*np.matrix(gradx).T
1766        Gq = multi_dot([Ginv, Bmat, gradx.T])
1767        return Gq.flatten()
1768
1769    def calcHess(self, xyz, gradx, hessx):
1770        """
1771        Compute the internal coordinate Hessian.
1772        Expects Cartesian coordinates to be provided in a.u.
1773        """
1774        xyz = xyz.flatten()
1775        q0 = self.calculate(xyz)
1776        Ginv = self.GInverse(xyz)
1777        Bmat = self.wilsonB(xyz)
1778        Gq = self.calcGrad(xyz, gradx)
1779        deriv2 = self.second_derivatives(xyz)
1780        Bmatp = deriv2.reshape(deriv2.shape[0], xyz.shape[0], xyz.shape[0])
1781        Hx_BptGq = hessx - np.einsum('pmn,p->mn',Bmatp,Gq)
1782        Hq = np.einsum('ps,sm,mn,nr,rq', Ginv, Bmat, Hx_BptGq, Bmat.T, Ginv, optimize=True)
1783        return Hq
1784
1785    def readCache(self, xyz, dQ):
1786        if not hasattr(self, 'stored_xyz'):
1787            return None
1788        xyz = xyz.flatten()
1789        dQ = dQ.flatten()
1790        if np.linalg.norm(self.stored_xyz - xyz) < 1e-10:
1791            if np.linalg.norm(self.stored_dQ - dQ) < 1e-10:
1792                return self.stored_newxyz
1793        return None
1794
1795    def writeCache(self, xyz, dQ, newxyz):
1796        xyz = xyz.flatten()
1797        dQ = dQ.flatten()
1798        newxyz = newxyz.flatten()
1799        self.stored_xyz = xyz.copy()
1800        self.stored_dQ = dQ.copy()
1801        self.stored_newxyz = newxyz.copy()
1802
1803    def newCartesian(self, xyz, dQ, verbose=True):
1804        cached = self.readCache(xyz, dQ)
1805        if cached is not None:
1806            # print "Returning cached result"
1807            return cached
1808        xyz1 = xyz.copy()
1809        dQ1 = dQ.copy()
1810        # Iterate until convergence:
1811        microiter = 0
1812        ndqs = []
1813        rmsds = []
1814        self.bork = False
1815        # Damping factor
1816        damp = 1.0
1817        # Function to exit from loop
1818        def finish(microiter, rmsdt, ndqt, xyzsave, xyz_iter1):
1819            if ndqt > 1e-1:
1820                if verbose: logger.info("Failed to obtain coordinates after %i microiterations (rmsd = %.3e |dQ| = %.3e)\n" % (microiter, rmsdt, ndqt))
1821                self.bork = True
1822                self.writeCache(xyz, dQ, xyz_iter1)
1823                return xyz_iter1.flatten()
1824            elif ndqt > 1e-3:
1825                if verbose: logger.info("Approximate coordinates obtained after %i microiterations (rmsd = %.3e |dQ| = %.3e)\n" % (microiter, rmsdt, ndqt))
1826            else:
1827                if verbose: logger.info("Cartesian coordinates obtained after %i microiterations (rmsd = %.3e |dQ| = %.3e)\n" % (microiter, rmsdt, ndqt))
1828            self.writeCache(xyz, dQ, xyzsave)
1829            return xyzsave.flatten()
1830        fail_counter = 0
1831        while True:
1832            microiter += 1
1833            Bmat = self.wilsonB(xyz1)
1834            Ginv = self.GInverse(xyz1)
1835            # Get new Cartesian coordinates
1836            dxyz = damp*multi_dot([Bmat.T,Ginv,dQ1.T])
1837            xyz2 = xyz1 + np.array(dxyz).flatten()
1838            if microiter == 1:
1839                xyzsave = xyz2.copy()
1840                xyz_iter1 = xyz2.copy()
1841            # Calculate the actual change in internal coordinates
1842            dQ_actual = self.calcDiff(xyz2, xyz1)
1843            rmsd = np.sqrt(np.mean((np.array(xyz2-xyz1).flatten())**2))
1844            ndq = np.linalg.norm(dQ1-dQ_actual)
1845            if len(ndqs) > 0:
1846                if ndq > ndqt:
1847                    if verbose: logger.info("Iter: %i Err-dQ (Best) = %.5e (%.5e) RMSD: %.5e Damp: %.5e (Bad)\n" % (microiter, ndq, ndqt, rmsd, damp))
1848                    damp /= 2
1849                    fail_counter += 1
1850                    # xyz2 = xyz1.copy()
1851                else:
1852                    if verbose: logger.info("Iter: %i Err-dQ (Best) = %.5e (%.5e) RMSD: %.5e Damp: %.5e (Good)\n" % (microiter, ndq, ndqt, rmsd, damp))
1853                    fail_counter = 0
1854                    damp = min(damp*1.2, 1.0)
1855                    rmsdt = rmsd
1856                    ndqt = ndq
1857                    xyzsave = xyz2.copy()
1858            else:
1859                if verbose: logger.info("Iter: %i Err-dQ = %.5e RMSD: %.5e Damp: %.5e\n" % (microiter, ndq, rmsd, damp))
1860                rmsdt = rmsd
1861                ndqt = ndq
1862            ndqs.append(ndq)
1863            rmsds.append(rmsd)
1864            # Check convergence / fail criteria
1865            if rmsd < 1e-6 or ndq < 1e-6:
1866                return finish(microiter, rmsdt, ndqt, xyzsave, xyz_iter1)
1867            if fail_counter >= 5:
1868                return finish(microiter, rmsdt, ndqt, xyzsave, xyz_iter1)
1869            if microiter == 50:
1870                return finish(microiter, rmsdt, ndqt, xyzsave, xyz_iter1)
1871            # Figure out the further change needed
1872            dQ1 = dQ1 - dQ_actual
1873            xyz1 = xyz2.copy()
1874
1875class PrimitiveInternalCoordinates(InternalCoordinates):
1876    def __init__(self, molecule, connect=False, addcart=False, constraints=None, cvals=None, **kwargs):
1877        super(PrimitiveInternalCoordinates, self).__init__()
1878        self.connect = connect
1879        self.addcart = addcart
1880        self.Internals = []
1881        self.cPrims = []
1882        self.cVals = []
1883        self.Rotators = OrderedDict()
1884        self.elem = molecule.elem
1885        for i in range(len(molecule)):
1886            self.makePrimitives(molecule[i], connect, addcart)
1887        # Assume we're using the first image for constraints
1888        self.makeConstraints(molecule[0], constraints, cvals)
1889        # Reorder primitives for checking with cc's code in TC.
1890        # Note that reorderPrimitives() _must_ be updated with each new InternalCoordinate class written.
1891        self.reorderPrimitives()
1892
1893    def makePrimitives(self, molecule, connect, addcart):
1894        molecule.build_topology()
1895        if 'resid' in molecule.Data.keys():
1896            frags = []
1897            current_resid = -1
1898            for i in range(molecule.na):
1899                if molecule.resid[i] != current_resid:
1900                    frags.append([i])
1901                    current_resid = molecule.resid[i]
1902                else:
1903                    frags[-1].append(i)
1904        else:
1905            frags = [m.nodes() for m in molecule.molecules]
1906        # coordinates in Angstrom
1907        coords = molecule.xyzs[0].flatten()
1908        # Make a distance matrix mapping atom pairs to interatomic distances
1909        AtomIterator, dxij = molecule.distance_matrix(pbc=False)
1910        D = {}
1911        for i, j in zip(AtomIterator, dxij[0]):
1912            assert i[0] < i[1]
1913            D[tuple(i)] = j
1914        dgraph = nx.Graph()
1915        for i in range(molecule.na):
1916            dgraph.add_node(i)
1917        for k, v in D.items():
1918            dgraph.add_edge(k[0], k[1], weight=v)
1919        mst = sorted(list(nx.minimum_spanning_edges(dgraph, data=False)))
1920        # Build a list of noncovalent distances
1921        noncov = []
1922        # Connect all non-bonded fragments together
1923        for edge in mst:
1924            if edge not in list(molecule.topology.edges()):
1925                # print "Adding %s from minimum spanning tree" % str(edge)
1926                if connect:
1927                    molecule.topology.add_edge(edge[0], edge[1])
1928                    noncov.append(edge)
1929        if not connect:
1930            if addcart:
1931                for i in range(molecule.na):
1932                    self.add(CartesianX(i, w=1.0))
1933                    self.add(CartesianY(i, w=1.0))
1934                    self.add(CartesianZ(i, w=1.0))
1935            else:
1936                for i in frags:
1937                    if len(i) >= 2:
1938                        self.add(TranslationX(i, w=np.ones(len(i))/len(i)))
1939                        self.add(TranslationY(i, w=np.ones(len(i))/len(i)))
1940                        self.add(TranslationZ(i, w=np.ones(len(i))/len(i)))
1941                        # Reference coordinates are given in Bohr.
1942                        sel = coords.reshape(-1,3)[i,:] * ang2bohr
1943                        sel -= np.mean(sel, axis=0)
1944                        rg = np.sqrt(np.mean(np.sum(sel**2, axis=1)))
1945                        self.add(RotationA(i, coords * ang2bohr, self.Rotators, w=rg))
1946                        self.add(RotationB(i, coords * ang2bohr, self.Rotators, w=rg))
1947                        self.add(RotationC(i, coords * ang2bohr, self.Rotators, w=rg))
1948                    else:
1949                        for j in i:
1950                            self.add(CartesianX(j, w=1.0))
1951                            self.add(CartesianY(j, w=1.0))
1952                            self.add(CartesianZ(j, w=1.0))
1953        add_tr = False
1954        if add_tr:
1955            i = range(molecule.na)
1956            self.add(TranslationX(i, w=np.ones(len(i))/len(i)))
1957            self.add(TranslationY(i, w=np.ones(len(i))/len(i)))
1958            self.add(TranslationZ(i, w=np.ones(len(i))/len(i)))
1959            # Reference coordinates are given in Bohr.
1960            sel = coords.reshape(-1,3)[i,:] * ang2bohr
1961            sel -= np.mean(sel, axis=0)
1962            rg = np.sqrt(np.mean(np.sum(sel**2, axis=1)))
1963            self.add(RotationA(i, coords * ang2bohr, self.Rotators, w=rg))
1964            self.add(RotationB(i, coords * ang2bohr, self.Rotators, w=rg))
1965            self.add(RotationC(i, coords * ang2bohr, self.Rotators, w=rg))
1966
1967        # # Build a list of noncovalent distances
1968        # noncov = []
1969        # # Connect all non-bonded fragments together
1970        # while True:
1971        #     # List of disconnected fragments
1972        #     subg = list(nx.connected_component_subgraphs(molecule.topology))
1973        #     # Break out of loop if all fragments are connected
1974        #     if len(subg) == 1: break
1975        #     # Find the smallest interatomic distance between any pair of fragments
1976        #     minD = 1e10
1977        #     for i in range(len(subg)):
1978        #         for j in range(i):
1979        #             for a in subg[i].nodes():
1980        #                 for b in subg[j].nodes():
1981        #                     if D[(min(a,b), max(a,b))] < minD:
1982        #                         minD = D[(min(a,b), max(a,b))]
1983        #     # Next, create one connection between pairs of fragments that have a
1984        #     # close-contact distance of at most 1.2 times the minimum found above
1985        #     for i in range(len(subg)):
1986        #         for j in range(i):
1987        #             tminD = 1e10
1988        #             conn = False
1989        #             conn_a = None
1990        #             conn_b = None
1991        #             for a in subg[i].nodes():
1992        #                 for b in subg[j].nodes():
1993        #                     if D[(min(a,b), max(a,b))] < tminD:
1994        #                         tminD = D[(min(a,b), max(a,b))]
1995        #                         conn_a = min(a,b)
1996        #                         conn_b = max(a,b)
1997        #                     if D[(min(a,b), max(a,b))] <= 1.3*minD:
1998        #                         conn = True
1999        #             if conn:
2000        #                 molecule.topology.add_edge(conn_a, conn_b)
2001        #                 noncov.append((conn_a, conn_b))
2002
2003        # Add an internal coordinate for all interatomic distances
2004        for (a, b) in molecule.topology.edges():
2005            self.add(Distance(a, b))
2006
2007        # Add an internal coordinate for all angles
2008        # LinThre = 0.99619469809174555
2009        # LinThre = 0.999
2010        # This number works best for the iron complex
2011        LinThre = 0.95
2012        AngDict = defaultdict(list)
2013        for b in molecule.topology.nodes():
2014            for a in molecule.topology.neighbors(b):
2015                for c in molecule.topology.neighbors(b):
2016                    if a < c:
2017                        # if (a, c) in molecule.topology.edges() or (c, a) in molecule.topology.edges(): continue
2018                        Ang = Angle(a, b, c)
2019                        nnc = (min(a, b), max(a, b)) in noncov
2020                        nnc += (min(b, c), max(b, c)) in noncov
2021                        # if nnc >= 2: continue
2022                        # logger.info("LPW: cosine of angle", a, b, c, "is", np.abs(np.cos(Ang.value(coords))))
2023                        if np.abs(np.cos(Ang.value(coords))) < LinThre:
2024                            self.add(Angle(a, b, c))
2025                            AngDict[b].append(Ang)
2026                        elif connect or not addcart:
2027                            # logger.info("Adding linear angle")
2028                            # Add linear angle IC's
2029                            # LPW 2019-02-16: Linear angle ICs work well for "very" linear angles in molecules (e.g. HCCCN)
2030                            # but do not work well for "almost" linear angles in noncovalent systems (e.g. H2O6).
2031                            # Bringing back old code to use "translations" for the latter case, but should be investigated
2032                            # more deeply in the future.
2033                            if nnc == 0:
2034                                self.add(LinearAngle(a, b, c, 0))
2035                                self.add(LinearAngle(a, b, c, 1))
2036                            else:
2037                                # Unit vector connecting atoms a and c
2038                                nac = molecule.xyzs[0][c] - molecule.xyzs[0][a]
2039                                nac /= np.linalg.norm(nac)
2040                                # Dot products of this vector with the Cartesian axes
2041                                dots = [np.abs(np.dot(ei, nac)) for ei in np.eye(3)]
2042                                # Functions for adding Cartesian coordinate
2043                                # carts = [CartesianX, CartesianY, CartesianZ]
2044                                trans = [TranslationX, TranslationY, TranslationZ]
2045                                w = np.array([-1.0, 2.0, -1.0])
2046                                # Add two of the most perpendicular Cartesian coordinates
2047                                for i in np.argsort(dots)[:2]:
2048                                    self.add(trans[i]([a, b, c], w=w))
2049
2050        for b in molecule.topology.nodes():
2051            for a in molecule.topology.neighbors(b):
2052                for c in molecule.topology.neighbors(b):
2053                    for d in molecule.topology.neighbors(b):
2054                        if a < c < d:
2055                            nnc = (min(a, b), max(a, b)) in noncov
2056                            nnc += (min(b, c), max(b, c)) in noncov
2057                            nnc += (min(b, d), max(b, d)) in noncov
2058                            # if nnc >= 1: continue
2059                            for i, j, k in sorted(list(itertools.permutations([a, c, d], 3))):
2060                                Ang1 = Angle(b,i,j)
2061                                Ang2 = Angle(i,j,k)
2062                                if np.abs(np.cos(Ang1.value(coords))) > LinThre: continue
2063                                if np.abs(np.cos(Ang2.value(coords))) > LinThre: continue
2064                                if np.abs(np.dot(Ang1.normal_vector(coords), Ang2.normal_vector(coords))) > LinThre:
2065                                    self.delete(Angle(i, b, j))
2066                                    self.add(OutOfPlane(b, i, j, k))
2067                                    break
2068
2069        # Find groups of atoms that are in straight lines
2070        atom_lines = [list(i) for i in molecule.topology.edges()]
2071        while True:
2072            # For a line of two atoms (one bond):
2073            # AB-AC
2074            # AX-AY
2075            # i.e. AB is the first one, AC is the second one
2076            # AX is the second-to-last one, AY is the last one
2077            # AB-AC-...-AX-AY
2078            # AB-(AC, AX)-AY
2079            atom_lines0 = deepcopy(atom_lines)
2080            for aline in atom_lines:
2081                # Imagine a line of atoms going like ab-ac-ax-ay.
2082                # Our job is to extend the line until there are no more
2083                ab = aline[0]
2084                ay = aline[-1]
2085                for aa in molecule.topology.neighbors(ab):
2086                    if aa not in aline:
2087                        # If the angle that AA makes with AB and ALL other atoms AC in the line are linear:
2088                        # Add AA to the front of the list
2089                        if all([np.abs(np.cos(Angle(aa, ab, ac).value(coords))) > LinThre for ac in aline[1:] if ac != ab]):
2090                            aline.insert(0, aa)
2091                for az in molecule.topology.neighbors(ay):
2092                    if az not in aline:
2093                        if all([np.abs(np.cos(Angle(ax, ay, az).value(coords))) > LinThre for ax in aline[:-1] if ax != ay]):
2094                            aline.append(az)
2095            if atom_lines == atom_lines0: break
2096        atom_lines_uniq = []
2097        for i in atom_lines:    #
2098            if tuple(i) not in set(atom_lines_uniq):
2099                atom_lines_uniq.append(tuple(i))
2100        lthree = [l for l in atom_lines_uniq if len(l) > 2]
2101        # TODO: Perhaps should reduce the times this is printed out in reaction paths
2102        # if len(lthree) > 0:
2103        #     print "Lines of three or more atoms:", ', '.join(['-'.join(["%i" % (i+1) for i in l]) for l in lthree])
2104
2105        # Normal dihedral code
2106        for aline in atom_lines_uniq:
2107            # Go over ALL pairs of atoms in a line
2108            for (b, c) in itertools.combinations(aline, 2):
2109                if b > c: (b, c) = (c, b)
2110                # Go over all neighbors of b
2111                for a in molecule.topology.neighbors(b):
2112                    # Go over all neighbors of c
2113                    for d in molecule.topology.neighbors(c):
2114                        # Make sure the end-atoms are not in the line and not the same as each other
2115                        if a not in aline and d not in aline and a != d:
2116                            nnc = (min(a, b), max(a, b)) in noncov
2117                            nnc += (min(b, c), max(b, c)) in noncov
2118                            nnc += (min(c, d), max(c, d)) in noncov
2119                            # print aline, a, b, c, d
2120                            Ang1 = Angle(a,b,c)
2121                            Ang2 = Angle(b,c,d)
2122                            # Eliminate dihedrals containing angles that are almost linear
2123                            # (should be eliminated already)
2124                            if np.abs(np.cos(Ang1.value(coords))) > LinThre: continue
2125                            if np.abs(np.cos(Ang2.value(coords))) > LinThre: continue
2126                            self.add(Dihedral(a, b, c, d))
2127
2128        ### Following are codes that evaluate angles and dihedrals involving entire lines-of-atoms
2129        ### as single degrees of freedom
2130        ### Unfortunately, they do not seem to improve the performance
2131        #
2132        # def pull_lines(a, front=True, middle=False):
2133        #     """
2134        #     Given an atom, pull all lines-of-atoms that it is in, e.g.
2135        #     e.g.
2136        #               D
2137        #               C
2138        #               B
2139        #           EFGHAIJKL
2140        #     returns (B, C, D), (H, G, F, E), (I, J, K, L).
2141        #
2142        #     A is the implicit first item in the list.
2143        #     Set front to False to make A the implicit last item in the list.
2144        #     Set middle to True to return lines where A is in the middle e.g. (H, G, F, E) and (I, J, K, L).
2145        #     """
2146        #     answer = []
2147        #     for l in atom_lines_uniq:
2148        #         if l[0] == a:
2149        #             answer.append(l[:][1:])
2150        #         elif l[-1] == a:
2151        #             answer.append(l[::-1][1:])
2152        #         elif middle and a in l:
2153        #             answer.append(l[l.index(a):][1:])
2154        #             answer.append(l[:l.index(a)][::-1])
2155        #     if front: return answer
2156        #     else: return [l[::-1] for l in answer]
2157        #
2158        # def same_line(al, bl):
2159        #     for l in atom_lines_uniq:
2160        #         if set(al).issubset(set(l)) and set(bl).issubset(set(l)):
2161        #             return True
2162        #     return False
2163        #
2164        # ## Multiple angle code; does not improve performance for Fe4N system.
2165        # for b in molecule.topology.nodes():
2166        #     for al in pull_lines(b, front=False, middle=True):
2167        #         for cl in pull_lines(b, front=True, middle=True):
2168        #             if al[0] == cl[-1]: continue
2169        #             if al[-1] == cl[0]: continue
2170        #             self.delete(Angle(al[-1], b, cl[0]))
2171        #             self.delete(Angle(cl[0], b, al[-1]))
2172        #             if len(set(al).intersection(set(cl))) > 0: continue
2173        #             if same_line(al, cl):
2174        #                 continue
2175        #             if al[-1] < cl[0]:
2176        #                 self.add(MultiAngle(al, b, cl))
2177        #             else:
2178        #                 self.add(MultiAngle(cl[::-1], b, al[::-1]))
2179        #
2180        ## Multiple dihedral code
2181        ## Note: This suffers from a problem where it cannot rebuild the Cartesian coordinates,
2182        ## possibly due to a bug in the MultiDihedral class.
2183        # for aline in atom_lines_uniq:
2184        #     for (b, c) in itertools.combinations(aline, 2):
2185        #         if b > c: (b, c) = (c, b)
2186        #         for al in pull_lines(b, front=False, middle=True):
2187        #             if same_line(al, aline): continue
2188        #                 # print "Same line:", al, aline
2189        #             for dl in pull_lines(c, front=True, middle=True):
2190        #                 if same_line(dl, aline): continue
2191        #                     # print "Same line:", dl, aline
2192        #                     # continue
2193        #                 # if same_line(dl, al): continue
2194        #                 if al[-1] == dl[0]: continue
2195        #                 # if len(set(al).intersection(set(dl))) > 0: continue
2196        #                 # print MultiDihedral(al, b, c, dl)
2197        #                 self.delete(Dihedral(al[-1], b, c, dl[0]))
2198        #                 self.add(MultiDihedral(al, b, c, dl))
2199
2200    def makeConstraints(self, molecule, constraints, cvals):
2201        # Add the list of constraints.
2202        xyz = molecule.xyzs[0].flatten() * ang2bohr
2203        if constraints is not None:
2204            if len(constraints) != len(cvals):
2205                raise RuntimeError("List of constraints should be same length as constraint values")
2206            for cons, cval in zip(constraints, cvals):
2207                self.addConstraint(cons, cval, xyz)
2208
2209    def __repr__(self):
2210        lines = ["Internal coordinate system (atoms numbered from 1):"]
2211        typedict = OrderedDict()
2212        for Internal in self.Internals:
2213            lines.append(Internal.__repr__())
2214            if str(type(Internal)) not in typedict:
2215                typedict[str(type(Internal))] = 1
2216            else:
2217                typedict[str(type(Internal))] += 1
2218        if len(lines) > 100:
2219            # Print only summary if too many
2220            lines = []
2221        for k, v in typedict.items():
2222            lines.append("%s : %i" % (k, v))
2223        return '\n'.join(lines)
2224
2225    def __eq__(self, other):
2226        answer = True
2227        for i in self.Internals:
2228            if i not in other.Internals:
2229                answer = False
2230        for i in other.Internals:
2231            if i not in self.Internals:
2232                answer = False
2233        return answer
2234
2235    def __ne__(self, other):
2236        return not self.__eq__(other)
2237
2238    def update(self, other):
2239        Changed = False
2240        for i in self.Internals:
2241            if i not in other.Internals:
2242                if hasattr(i, 'inactive'):
2243                    i.inactive += 1
2244                else:
2245                    i.inactive = 0
2246                if i.inactive == 1:
2247                    logger.info("Deleting:" + str(i) + "\n")
2248                    self.Internals.remove(i)
2249                    Changed = True
2250            else:
2251                i.inactive = 0
2252        for i in other.Internals:
2253            if i not in self.Internals:
2254                logger.info("Adding:  " + str(i) + "\n")
2255                self.Internals.append(i)
2256                Changed = True
2257        return Changed
2258
2259    def join(self, other):
2260        Changed = False
2261        for i in other.Internals:
2262            if i not in self.Internals:
2263                logger.info("Adding:  " + str(i) + "\n")
2264                self.Internals.append(i)
2265                Changed = True
2266        return Changed
2267
2268    def repr_diff(self, other):
2269        if hasattr(other, 'Prims'):
2270            output = ['Primitive -> Delocalized']
2271            otherPrims = other.Prims
2272        else:
2273            output = []
2274            otherPrims = other
2275        alines = ["-- Added: --"]
2276        for i in otherPrims.Internals:
2277            if i not in self.Internals:
2278                alines.append(i.__repr__())
2279        dlines = ["-- Deleted: --"]
2280        for i in self.Internals:
2281            if i not in otherPrims.Internals:
2282                dlines.append(i.__repr__())
2283        if len(alines) > 1:
2284            output += alines
2285        if len(dlines) > 1:
2286            output += dlines
2287        return '\n'.join(output)
2288
2289    def resetRotations(self, xyz):
2290        for Internal in self.Internals:
2291            if type(Internal) is LinearAngle:
2292                Internal.reset(xyz)
2293        for rot in self.Rotators.values():
2294            rot.reset(xyz)
2295
2296    def largeRots(self):
2297        for Internal in self.Internals:
2298            if type(Internal) is LinearAngle:
2299                if Internal.stored_dot2 > 0.75:
2300                    # Linear angle is almost parallel to reference axis
2301                    return True
2302            if type(Internal) in [RotationA, RotationB, RotationC]:
2303                if Internal in self.cPrims:
2304                    continue
2305                if Internal.Rotator.stored_norm > 0.9*np.pi:
2306                    # Molecule has rotated by almost pi
2307                    return True
2308                if Internal.Rotator.stored_dot2 > 0.9:
2309                    # Linear molecule is almost parallel to reference axis
2310                    return True
2311        return False
2312
2313    def calculate(self, xyz):
2314        answer = []
2315        for Internal in self.Internals:
2316            answer.append(Internal.value(xyz))
2317        return np.array(answer)
2318
2319    def calculateDegrees(self, xyz):
2320        answer = []
2321        for Internal in self.Internals:
2322            value = Internal.value(xyz)
2323            if Internal.isAngular:
2324                value *= 180/np.pi
2325            answer.append(value)
2326        return np.array(answer)
2327
2328    def getRotatorNorms(self):
2329        rots = []
2330        for Internal in self.Internals:
2331            if type(Internal) in [RotationA]:
2332                rots.append(Internal.Rotator.stored_norm)
2333        return rots
2334
2335    def getRotatorDots(self):
2336        dots = []
2337        for Internal in self.Internals:
2338            if type(Internal) in [RotationA]:
2339                dots.append(Internal.Rotator.stored_dot2)
2340        return dots
2341
2342    def printRotations(self, xyz):
2343        rotNorms = self.getRotatorNorms()
2344        if len(rotNorms) > 0:
2345            logger.info("Rotator Norms: " + " ".join(["% .4f" % i for i in rotNorms]) + "\n")
2346        rotDots = self.getRotatorDots()
2347        if len(rotDots) > 0 and np.max(rotDots) > 1e-5:
2348            logger.info("Rotator Dots : " + " ".join(["% .4f" % i for i in rotDots]) + "\n")
2349        linAngs = [ic.value(xyz) for ic in self.Internals if type(ic) is LinearAngle]
2350        if len(linAngs) > 0:
2351            logger.info("Linear Angles: " + " ".join(["% .4f" % i for i in linAngs]) + "\n")
2352
2353    def derivatives(self, xyz):
2354        self.calculate(xyz)
2355        answer = []
2356        for Internal in self.Internals:
2357            answer.append(Internal.derivative(xyz))
2358        # This array has dimensions:
2359        # 1) Number of internal coordinates
2360        # 2) Number of atoms
2361        # 3) 3
2362        return np.array(answer)
2363
2364    def second_derivatives(self, xyz):
2365        self.calculate(xyz)
2366        answer = []
2367        for Internal in self.Internals:
2368            answer.append(Internal.second_derivative(xyz))
2369        # This array has dimensions:
2370        # 1) Number of internal coordinates
2371        # 2) Number of atoms
2372        # 3) 3
2373        # 4) Number of atoms
2374        # 5) 3
2375        return np.array(answer)
2376
2377    def calcDiff(self, coord1, coord2):
2378        """ Calculate difference in internal coordinates (coord1-coord2), accounting for changes in 2*pi of angles. """
2379        Q1 = self.calculate(coord1)
2380        Q2 = self.calculate(coord2)
2381        PMDiff = (Q1-Q2)
2382        for k in range(len(PMDiff)):
2383            if self.Internals[k].isPeriodic:
2384                Plus2Pi = PMDiff[k] + 2*np.pi
2385                Minus2Pi = PMDiff[k] - 2*np.pi
2386                if np.abs(PMDiff[k]) > np.abs(Plus2Pi):
2387                    PMDiff[k] = Plus2Pi
2388                if np.abs(PMDiff[k]) > np.abs(Minus2Pi):
2389                    PMDiff[k] = Minus2Pi
2390        return PMDiff
2391
2392    def GInverse(self, xyz):
2393        return self.GInverse_SVD(xyz)
2394
2395    def add(self, dof):
2396        if dof not in self.Internals:
2397            self.Internals.append(dof)
2398
2399    def delete(self, dof):
2400        for ii in range(len(self.Internals))[::-1]:
2401            if dof == self.Internals[ii]:
2402                del self.Internals[ii]
2403
2404    def addConstraint(self, cPrim, cVal=None, xyz=None):
2405        if cVal is None and xyz is None:
2406            raise RuntimeError('Please provide either cval or xyz')
2407        if cVal is None:
2408            # If coordinates are provided instead of a constraint value,
2409            # then calculate the constraint value from the positions.
2410            # If both are provided, then the coordinates are ignored.
2411            cVal = cPrim.value(xyz)
2412        if cPrim in self.cPrims:
2413            iPrim = self.cPrims.index(cPrim)
2414            if np.abs(cVal - self.cVals[iPrim]) > 1e-6:
2415                logger.info("Updating constraint value to %.4e\n" % cVal)
2416            self.cVals[iPrim] = cVal
2417        else:
2418            if cPrim not in self.Internals:
2419                self.Internals.append(cPrim)
2420            self.cPrims.append(cPrim)
2421            self.cVals.append(cVal)
2422
2423    def reorderPrimitives(self):
2424        # Reorder primitives to be in line with cc's code
2425        newPrims = []
2426        for cPrim in self.cPrims:
2427            newPrims.append(cPrim)
2428        for typ in [Distance, Angle, LinearAngle, MultiAngle, OutOfPlane, Dihedral, MultiDihedral, CartesianX, CartesianY, CartesianZ, TranslationX, TranslationY, TranslationZ, RotationA, RotationB, RotationC]:
2429            for p in self.Internals:
2430                if type(p) is typ and p not in self.cPrims:
2431                    newPrims.append(p)
2432        if len(newPrims) != len(self.Internals):
2433            raise RuntimeError("Not all internal coordinates have been accounted for. You may need to add something to reorderPrimitives()")
2434        self.Internals = newPrims
2435
2436    def getConstraints_from(self, other):
2437        if other.haveConstraints():
2438            for cPrim, cVal in zip(other.cPrims, other.cVals):
2439                self.addConstraint(cPrim, cVal)
2440        self.reorderPrimitives()
2441
2442    def haveConstraints(self):
2443        return len(self.cPrims) > 0
2444
2445    def getConstraintViolation(self, xyz):
2446        nc = len(self.cPrims)
2447        maxdiff = 0.0
2448        for ic, c in enumerate(self.cPrims):
2449            w = c.w if type(c) in [RotationA, RotationB, RotationC] else 1.0
2450            current = c.value(xyz)/w
2451            reference = self.cVals[ic]/w
2452            diff = (current - reference)
2453            if c.isPeriodic:
2454                if np.abs(diff-2*np.pi) < np.abs(diff):
2455                    diff -= 2*np.pi
2456                if np.abs(diff+2*np.pi) < np.abs(diff):
2457                    diff += 2*np.pi
2458            if type(c) in [TranslationX, TranslationY, TranslationZ, CartesianX, CartesianY, CartesianZ, Distance]:
2459                factor = bohr2ang
2460            elif c.isAngular:
2461                factor = 180.0/np.pi
2462            if np.abs(diff*factor) > maxdiff:
2463                maxdiff = np.abs(diff*factor)
2464        return maxdiff
2465
2466    def printConstraints(self, xyz, thre=1e-5):
2467        nc = len(self.cPrims)
2468        out_lines = []
2469        header = "Constraint                         Current      Target       Diff."
2470        for ic, c in enumerate(self.cPrims):
2471            w = c.w if type(c) in [RotationA, RotationB, RotationC] else 1.0
2472            current = c.value(xyz)/w
2473            reference = self.cVals[ic]/w
2474            diff = (current - reference)
2475            if c.isPeriodic:
2476                if np.abs(diff-2*np.pi) < np.abs(diff):
2477                    diff -= 2*np.pi
2478                if np.abs(diff+2*np.pi) < np.abs(diff):
2479                    diff += 2*np.pi
2480            if type(c) in [TranslationX, TranslationY, TranslationZ, CartesianX, CartesianY, CartesianZ, Distance]:
2481                factor = bohr2ang
2482            elif c.isAngular:
2483                factor = 180.0/np.pi
2484            if np.abs(diff*factor) > thre:
2485                out_lines.append("%-30s  % 10.5f  % 10.5f  % 10.5f" % (str(c), current*factor, reference*factor, diff*factor))
2486        if len(out_lines) > 0:
2487            logger.info(header + "\n")
2488            logger.info('\n'.join(out_lines) + "\n")
2489            # if type(c) in [RotationA, RotationB, RotationC]:
2490            #     print c, c.value(xyz)
2491
2492    def getConstraintTargetVals(self):
2493        nc = len(self.cPrims)
2494        cNames = []
2495        cVals = []
2496        for ic, c in enumerate(self.cPrims):
2497            w = c.w if type(c) in [RotationA, RotationB, RotationC] else 1.0
2498            reference = self.cVals[ic]/w
2499            if type(c) in [TranslationX, TranslationY, TranslationZ, CartesianX, CartesianY, CartesianZ, Distance]:
2500                factor = bohr2ang
2501            elif c.isAngular:
2502                factor = 180.0/np.pi
2503            cNames.append(str(c))
2504            cVals.append(reference*factor)
2505        return(cNames, cVals)
2506
2507    def guess_hessian(self, coords):
2508        """
2509        Build a guess Hessian that roughly follows Schlegel's guidelines.
2510        """
2511        xyzs = coords.reshape(-1,3)*bohr2ang
2512        Hdiag = []
2513        def covalent(a, b):
2514            r = np.linalg.norm(xyzs[a]-xyzs[b])
2515            rcov = Radii[Elements.index(self.elem[a])-1] + Radii[Elements.index(self.elem[b])-1]
2516            return r/rcov < 1.2
2517
2518        for ic in self.Internals:
2519            if type(ic) is Distance:
2520                r = np.linalg.norm(xyzs[ic.a]-xyzs[ic.b]) * ang2bohr
2521                elem1 = min(Elements.index(self.elem[ic.a]), Elements.index(self.elem[ic.b]))
2522                elem2 = max(Elements.index(self.elem[ic.a]), Elements.index(self.elem[ic.b]))
2523                A = 1.734
2524                if elem1 < 3:
2525                    if elem2 < 3:
2526                        B = -0.244
2527                    elif elem2 < 11:
2528                        B = 0.352
2529                    else:
2530                        B = 0.660
2531                elif elem1 < 11:
2532                    if elem2 < 11:
2533                        B = 1.085
2534                    else:
2535                        B = 1.522
2536                else:
2537                    B = 2.068
2538                if covalent(ic.a, ic.b):
2539                    Hdiag.append(A/(r-B)**3)
2540                else:
2541                    Hdiag.append(0.1)
2542            elif type(ic) in [Angle, LinearAngle, MultiAngle]:
2543                if type(ic) in [Angle, LinearAngle]:
2544                    a = ic.a
2545                    c = ic.c
2546                else:
2547                    a = ic.a[-1]
2548                    c = ic.c[0]
2549                if min(Elements.index(self.elem[a]),
2550                       Elements.index(self.elem[ic.b]),
2551                       Elements.index(self.elem[c])) < 3:
2552                    A = 0.160
2553                else:
2554                    A = 0.250
2555                if covalent(a, ic.b) and covalent(ic.b, c):
2556                    Hdiag.append(A)
2557                else:
2558                    Hdiag.append(0.1)
2559            elif type(ic) in [Dihedral, MultiDihedral]:
2560                r = np.linalg.norm(xyzs[ic.b]-xyzs[ic.c])
2561                rcov = Radii[Elements.index(self.elem[ic.b])-1] + Radii[Elements.index(self.elem[ic.c])-1]
2562                # Hdiag.append(0.1)
2563                Hdiag.append(0.023)
2564            elif type(ic) is OutOfPlane:
2565                r1 = xyzs[ic.b]-xyzs[ic.a]
2566                r2 = xyzs[ic.c]-xyzs[ic.a]
2567                r3 = xyzs[ic.d]-xyzs[ic.a]
2568                d = 1 - np.abs(np.dot(r1,np.cross(r2,r3))/np.linalg.norm(r1)/np.linalg.norm(r2)/np.linalg.norm(r3))
2569                # Hdiag.append(0.1)
2570                if covalent(ic.a, ic.b) and covalent(ic.a, ic.c) and covalent(ic.a, ic.d):
2571                    Hdiag.append(0.045)
2572                else:
2573                    Hdiag.append(0.023)
2574            elif type(ic) in [CartesianX, CartesianY, CartesianZ]:
2575                Hdiag.append(0.05)
2576            elif type(ic) in [TranslationX, TranslationY, TranslationZ]:
2577                Hdiag.append(0.05)
2578            elif type(ic) in [RotationA, RotationB, RotationC]:
2579                Hdiag.append(0.05)
2580            else:
2581                raise RuntimeError('Failed to build guess Hessian matrix. Make sure all IC types are supported')
2582        return np.diag(Hdiag)
2583
2584
2585class DelocalizedInternalCoordinates(InternalCoordinates):
2586    def __init__(self, molecule, imagenr=0, build=False, connect=False, addcart=False, constraints=None, cvals=None, remove_tr=False, cart_only=False, conmethod=0):
2587        super(DelocalizedInternalCoordinates, self).__init__()
2588        # cart_only is just because of how I set up the class structure.
2589        if cart_only: return
2590        # Set the algorithm for constraint satisfaction.
2591        # 0 - Original algorithm implemented in 2016, constraints are satisfied slowly unless "enforce" is enabled
2592        # 1 - Updated algorithm implemented on 2019-03-20, constraints are satisfied instantly, "enforce" is not needed
2593        self.conmethod = conmethod
2594        # HDLC is given by (connect = False, addcart = True)
2595        # Standard DLC is given by (connect = True, addcart = False)
2596        # TRIC is given by (connect = False, addcart = False)
2597        # Build a minimum spanning tree
2598        self.connect = connect
2599        # Add Cartesian coordinates to all.
2600        self.addcart = addcart
2601        # The DLC contains an instance of primitive internal coordinates.
2602        self.Prims = PrimitiveInternalCoordinates(molecule, connect=connect, addcart=addcart, constraints=constraints, cvals=cvals)
2603        self.na = molecule.na
2604        # Whether constraints have been enforced previously
2605        self.enforced = False
2606        self.enforce_fail_printed = False
2607        # Build the DLC's. This takes some time, so we have the option to turn it off.
2608        xyz = molecule.xyzs[imagenr].flatten() * ang2bohr
2609        if build:
2610            self.build_dlc(xyz)
2611        self.remove_tr = remove_tr
2612        if self.remove_tr:
2613            self.remove_TR(xyz)
2614
2615    def clearCache(self):
2616        super(DelocalizedInternalCoordinates, self).clearCache()
2617        self.Prims.clearCache()
2618
2619    def __repr__(self):
2620        return self.Prims.__repr__()
2621
2622    def update(self, other):
2623        return self.Prims.update(other.Prims)
2624
2625    def join(self, other):
2626        return self.Prims.join(other.Prims)
2627
2628    def addConstraint(self, cPrim, cVal, xyz):
2629        self.Prims.addConstraint(cPrim, cVal, xyz)
2630
2631    def getConstraints_from(self, other):
2632        self.Prims.getConstraints_from(other.Prims)
2633
2634    def haveConstraints(self):
2635        return len(self.Prims.cPrims) > 0
2636
2637    def getConstraintViolation(self, xyz):
2638        return self.Prims.getConstraintViolation(xyz)
2639
2640    def printConstraints(self, xyz, thre=1e-5):
2641        self.Prims.printConstraints(xyz, thre=thre)
2642
2643    def getConstraintTargetVals(self):
2644        return self.Prims.getConstraintTargetVals()
2645
2646    def augmentGH(self, xyz, G, H):
2647        """
2648        Add extra dimensions to the gradient and Hessian corresponding to the constrained degrees of freedom.
2649        The Hessian becomes:  H  c
2650                              cT 0
2651        where the elements of cT are the first derivatives of the constraint function
2652        (typically a single primitive minus a constant) with respect to the DLCs.
2653
2654        Since we picked a DLC to represent the constraint (cProj), we only set one element
2655        in each row of cT to be nonzero. Because cProj = a_i * Prim_i + a_j * Prim_j, we have
2656        d(Prim_c)/d(cProj) = 1.0/a_c where "c" is the index of the primitive being constrained.
2657
2658        The extended elements of the Gradient are equal to the constraint violation.
2659
2660        Parameters
2661        ----------
2662        xyz : np.ndarray
2663            Flat array containing Cartesian coordinates in atomic units
2664        G : np.ndarray
2665            Flat array containing internal coordinate gradient
2666        H : np.ndarray
2667            Square array containing internal coordinate Hessian
2668
2669        Returns
2670        -------
2671        GC : np.ndarray
2672            Flat array containing gradient extended by constraint violations
2673        HC : np.ndarray
2674            Square matrix extended by partial derivatives d(Prim)/d(cProj)
2675        """
2676        # Number of internals (elements of G)
2677        ni = len(G)
2678        # Number of constraints
2679        nc = len(self.Prims.cPrims)
2680        # Total dimension
2681        nt = ni+nc
2682        # Lower block of the augmented Hessian
2683        cT = np.zeros((nc, ni), dtype=float)
2684        c0 = np.zeros(nc, dtype=float)
2685        for ic, c in enumerate(self.Prims.cPrims):
2686            # Look up the index of the primitive that is being constrained
2687            iPrim = self.Prims.Internals.index(c)
2688            # The DLC corresponding to the constrained primitive (a.k.a. cProj) is self.Vecs[self.cDLC[ic]].
2689            # For a differential change in the DLC, the primitive that we are constraining changes by:
2690            cT[ic, self.cDLC[ic]] = 1.0/self.Vecs[iPrim, self.cDLC[ic]]
2691            # Calculate the further change needed in this constrained variable
2692            c0[ic] = self.Prims.cVals[ic] - c.value(xyz)
2693            if c.isPeriodic:
2694                Plus2Pi = c0[ic] + 2*np.pi
2695                Minus2Pi = c0[ic] - 2*np.pi
2696                if np.abs(c0[ic]) > np.abs(Plus2Pi):
2697                    c0[ic] = Plus2Pi
2698                if np.abs(c0[ic]) > np.abs(Minus2Pi):
2699                    c0[ic] = Minus2Pi
2700            # The new constraint algorithm satisfies constraints too quickly and could cause
2701            # the energy to blow up. Thus, constraint steps are restricted to 0.1 au/radian
2702            if self.conmethod == 1:
2703                if c0[ic] < -0.1:
2704                    c0[ic] = -0.1
2705                if c0[ic] > 0.1:
2706                    c0[ic] = 0.1
2707        # Construct augmented Hessian
2708        HC = np.zeros((nt, nt), dtype=float)
2709        HC[0:ni, 0:ni] = H[:,:]
2710        HC[ni:nt, 0:ni] = cT[:,:]
2711        HC[0:ni, ni:nt] = cT.T[:,:]
2712        # Construct augmented gradient
2713        GC = np.zeros(nt, dtype=float)
2714        GC[0:ni] = G[:]
2715        GC[ni:nt] = -c0[:]
2716        return GC, HC
2717
2718    def applyConstraints(self, xyz):
2719        """
2720        Pass in Cartesian coordinates and return new coordinates that satisfy the constraints exactly.
2721        """
2722        xyz1 = xyz.copy()
2723        niter = 0
2724        xyzs = []
2725        ndqs = []
2726        while True:
2727            dQ = np.zeros(len(self.Internals), dtype=float)
2728            for ic, c in enumerate(self.Prims.cPrims):
2729                # Look up the index of the primitive that is being constrained
2730                iPrim = self.Prims.Internals.index(c)
2731                # Look up the index of the DLC that corresponds to the constraint
2732                iDLC = self.cDLC[ic]
2733                # Calculate the further change needed in this constrained variable
2734                dQ[iDLC] = (self.Prims.cVals[ic] - c.value(xyz1))
2735                if c.isPeriodic:
2736                    Plus2Pi = dQ[iDLC] + 2*np.pi
2737                    Minus2Pi = dQ[iDLC] - 2*np.pi
2738                    if np.abs(dQ[iDLC]) > np.abs(Plus2Pi):
2739                        dQ[iDLC] = Plus2Pi
2740                    if np.abs(dQ[iDLC]) > np.abs(Minus2Pi):
2741                        dQ[iDLC] = Minus2Pi
2742                dQ[iDLC] /= self.Vecs[iPrim, iDLC]
2743            xyzs.append(xyz1.copy())
2744            ndqs.append(np.linalg.norm(dQ))
2745            # print("applyConstraints calling newCartesian (%i), |dQ| = %.3e" % (niter, np.linalg.norm(dQ)))
2746            xyz2 = self.newCartesian(xyz1, dQ, verbose=False)
2747            if np.linalg.norm(dQ) < 1e-6:
2748                return xyz2
2749            if niter > 1 and np.linalg.norm(dQ) > np.linalg.norm(dQ0):
2750                xyz1 = xyzs[np.argmin(ndqs)]
2751                if not self.enforce_fail_printed:
2752                    logger.warning("Warning: Failed to enforce exact constraint satisfaction. Please remove possible redundant constraints. See below:\n")
2753                    self.printConstraints(xyz1, thre=0.0)
2754                    self.enforce_fail_printed = True
2755                return xyz1
2756            xyz1 = xyz2.copy()
2757            niter += 1
2758            dQ0 = dQ.copy()
2759
2760    def newCartesian_withConstraint(self, xyz, dQ, thre=0.1, verbose=False):
2761        xyz2 = self.newCartesian(xyz, dQ, verbose)
2762        constraintSmall = len(self.Prims.cPrims) > 0
2763        for ic, c in enumerate(self.Prims.cPrims):
2764            w = c.w if type(c) in [RotationA, RotationB, RotationC] else 1.0
2765            current = c.value(xyz)/w
2766            reference = self.Prims.cVals[ic]/w
2767            diff = (current - reference)
2768            if np.abs(diff-2*np.pi) < np.abs(diff):
2769                diff -= 2*np.pi
2770            if np.abs(diff+2*np.pi) < np.abs(diff):
2771                diff += 2*np.pi
2772            if np.abs(diff) > thre:
2773                constraintSmall = False
2774        if constraintSmall:
2775            xyz2 = self.applyConstraints(xyz2)
2776            if not self.enforced:
2777                logger.info("<<< Enforcing constraint satisfaction >>>\n")
2778            self.enforced = True
2779        else:
2780            self.enforced = False
2781        return xyz2
2782
2783    def calcGradProj(self, xyz, gradx):
2784        """
2785        Project out the components of the internal coordinate gradient along the
2786        constrained degrees of freedom. This is used to calculate the convergence
2787        criteria for constrained optimizations.
2788
2789        Parameters
2790        ----------
2791        xyz : np.ndarray
2792            Flat array containing Cartesian coordinates in atomic units
2793        gradx : np.ndarray
2794            Flat array containing gradient in Cartesian coordinates
2795
2796        Returns
2797        -------
2798        np.ndarray
2799            Flat array containing gradient in Cartesian coordinates with forces
2800            along constrained directions projected out
2801        """
2802        if len(self.Prims.cPrims) == 0:
2803            return gradx
2804        q0 = self.calculate(xyz)
2805        Ginv = self.GInverse(xyz)
2806        Bmat = self.wilsonB(xyz)
2807        # Internal coordinate gradient
2808        # Gq = np.matrix(Ginv)*np.matrix(Bmat)*np.matrix(gradx).T
2809        Gq = multi_dot([Ginv, Bmat, gradx.T])
2810        Gqc = np.array(Gq).flatten()
2811        # Remove the directions that are along the DLCs that we are constraining
2812        for i in self.cDLC:
2813            Gqc[i] = 0.0
2814        # Gxc = np.array(np.matrix(Bmat.T)*np.matrix(Gqc).T).flatten()
2815        Gxc = multi_dot([Bmat.T, Gqc.T]).flatten()
2816        return Gxc
2817
2818    def build_dlc_0(self, xyz):
2819        """
2820        Build the delocalized internal coordinates (DLCs) which are linear
2821        combinations of the primitive internal coordinates. Each DLC is stored
2822        as a column in self.Vecs.
2823
2824        In short, each DLC is an eigenvector of the G-matrix, and the number of
2825        nonzero eigenvalues of G should be equal to 3*N.
2826
2827        After creating the DLCs, we construct special ones corresponding to primitive
2828        coordinates that are constrained (cProj).  These are placed in the front (i.e. left)
2829        of the list of DLCs, and then we perform a Gram-Schmidt orthogonalization.
2830
2831        This function is called at the end of __init__ after the coordinate system is already
2832        specified (including which primitives are constraints).
2833
2834        Parameters
2835        ----------
2836        xyz : np.ndarray
2837            Flat array containing Cartesian coordinates in atomic units
2838        """
2839        # Perform singular value decomposition
2840        click()
2841        G = self.Prims.GMatrix(xyz)
2842        # Manipulate G-Matrix to increase weight of constrained coordinates
2843        if self.haveConstraints():
2844            for ic, c in enumerate(self.Prims.cPrims):
2845                iPrim = self.Prims.Internals.index(c)
2846                G[:, iPrim] *= 1.0
2847                G[iPrim, :] *= 1.0
2848        ncon = len(self.Prims.cPrims)
2849        # Water Dimer: 100.0, no check -> -151.1892668451
2850        time_G = click()
2851        L, Q = np.linalg.eigh(G)
2852        time_eig = click()
2853        # print "Build G: %.3f Eig: %.3f" % (time_G, time_eig)
2854        LargeVals = 0
2855        LargeIdx = []
2856        for ival, value in enumerate(L):
2857            # print ival, value
2858            if np.abs(value) > 1e-6:
2859                LargeVals += 1
2860                LargeIdx.append(ival)
2861        Expect = 3*self.na
2862        # print "%i atoms (expect %i coordinates); %i/%i singular values are > 1e-6" % (self.na, Expect, LargeVals, len(L))
2863        # if LargeVals <= Expect:
2864        self.Vecs = Q[:, LargeIdx]
2865
2866        # Vecs has number of rows equal to the number of primitives, and
2867        # number of columns equal to the number of delocalized internal coordinates.
2868        if self.haveConstraints():
2869            click()
2870            # print "Projecting out constraints...",
2871            V = []
2872            for ic, c in enumerate(self.Prims.cPrims):
2873                # Look up the index of the primitive that is being constrained
2874                iPrim = self.Prims.Internals.index(c)
2875                # Pick a row out of the eigenvector space. This is a linear combination of the DLCs.
2876                cVec = self.Vecs[iPrim, :]
2877                cVec = np.array(cVec)
2878                cVec /= np.linalg.norm(cVec)
2879                # This is a "new DLC" that corresponds to the primitive that we are constraining
2880                cProj = np.dot(self.Vecs,cVec.T)
2881                cProj /= np.linalg.norm(cProj)
2882                V.append(np.array(cProj).flatten())
2883                # print c, cProj[iPrim]
2884            # V contains the constraint vectors on the left, and the original DLCs on the right
2885            V = np.hstack((np.array(V).T, np.array(self.Vecs)))
2886            # Apply Gram-Schmidt to V, and produce U.
2887            # The Gram-Schmidt process should produce a number of orthogonal DLCs equal to the original number
2888            thre = 1e-6
2889            while True:
2890                U = []
2891                for iv in range(V.shape[1]):
2892                    v = V[:, iv].flatten()
2893                    U.append(v.copy())
2894                    for ui in U[:-1]:
2895                        U[-1] -= ui * np.dot(ui, v)
2896                    if np.linalg.norm(U[-1]) < thre:
2897                        U = U[:-1]
2898                        continue
2899                    U[-1] /= np.linalg.norm(U[-1])
2900                if len(U) > self.Vecs.shape[1]:
2901                    thre *= 10
2902                elif len(U) == self.Vecs.shape[1]:
2903                    break
2904                elif len(U) < self.Vecs.shape[1]:
2905                    raise RuntimeError('Gram-Schmidt orthogonalization has failed (expect %i length %i)' % (self.Vecs.shape[1], len(U)))
2906            # print "Gram-Schmidt completed with thre=%.0e" % thre
2907            self.Vecs = np.array(U).T
2908            # Constrained DLCs are on the left of self.Vecs.
2909            self.cDLC = [i for i in range(len(self.Prims.cPrims))]
2910        # Now self.Internals is no longer a list of InternalCoordinate objects but only a list of strings.
2911        # We do not create objects for individual DLCs but
2912        self.Internals = ["Constraint-DLC" if i < ncon else "DLC" + " %i" % (i+1) for i in range(self.Vecs.shape[1])]
2913
2914    def build_dlc_1(self, xyz):
2915        """
2916        Build the delocalized internal coordinates (DLCs) which are linear
2917        combinations of the primitive internal coordinates. Each DLC is stored
2918        as a column in self.Vecs.
2919
2920        After some thought, build_dlc_0 did not implement constraint satisfaction
2921        in the most correct way. Constraint satisfaction was rather slow and
2922        the --enforce 0.1 may be passed to improve performance. Rethinking how the
2923        G matrix is constructed provides a more fundamental solution.
2924
2925        In the new approach implemented here, constrained primitive ICs (PICs) are
2926        first set aside from the rest of the PICs. Next, a G-matrix is constructed
2927        from the rest of the PICs and diagonalized to form DLCs, called "residual" DLCs.
2928        The union of the cPICs and rDLCs forms a generalized set of DLCs, but the
2929        cPICs are not orthogonal to each other or to the rDLCs.
2930
2931        A set of orthogonal DLCs is constructed by carrying out Gram-Schmidt
2932        on the generalized set. Orthogonalization is carried out on the cPICs in order.
2933        Next, orthogonalization is carried out on the rDLCs using a greedy algorithm
2934        that carries out projection for each cPIC, then keeps the one with the largest
2935        remaining norm. This way we avoid keeping rDLCs that are almost redundant with
2936        the cPICs. The longest projected rDLC is added to the set and continued until
2937        the expected number is reached.
2938
2939        One special note in orthogonalization is that the "overlap" between internal
2940        coordinates corresponds to the G matrix element. Thus, for DLCs that's a linear
2941        combination of PICs, then the overlap is given by:
2942
2943        v_i * B * B^T * v_j = v_i * G * v_j
2944
2945        Notes on usage: 1) When this is activated, constraints tend to be satisfied very
2946        rapidly even if the current coordinates are very far from the constraint values,
2947        leading to possible blowing up of the energies. In augment_GH, maximum steps in
2948        constrained degrees of freedom are restricted to 0.1 a.u./radian for this reason.
2949
2950        2) Although the performance of this method is generally superior to the old method,
2951        the old method with --enforce 0.1 is more extensively tested and recommended.
2952        Thus, this method isn't enabled by default but provided as an optional feature.
2953
2954        Parameters
2955        ----------
2956        xyz : np.ndarray
2957            Flat array containing Cartesian coordinates in atomic units
2958        """
2959        click()
2960        G = self.Prims.GMatrix(xyz)
2961        nprim = len(self.Prims.Internals)
2962        cPrimIdx = []
2963        if self.haveConstraints():
2964            for ic, c in enumerate(self.Prims.cPrims):
2965                iPrim = self.Prims.Internals.index(c)
2966                cPrimIdx.append(iPrim)
2967        ncon = len(self.Prims.cPrims)
2968        if cPrimIdx != list(range(ncon)):
2969            raise RuntimeError("The constraint primitives should be at the start of the list")
2970        # Form a sub-G-matrix that doesn't include the constrained primitives and diagonalize it to form DLCs.
2971        Gsub = G[ncon:, ncon:]
2972        time_G = click()
2973        L, Q = np.linalg.eigh(Gsub)
2974        # Sort eigenvalues and eigenvectors in descending order (for cleanliness)
2975        L = L[::-1]
2976        Q = Q[:, ::-1]
2977        time_eig = click()
2978        # print "Build G: %.3f Eig: %.3f" % (time_G, time_eig)
2979        # Figure out which eigenvectors from the G submatrix to include
2980        LargeVals = 0
2981        LargeIdx = []
2982        GEigThre = 1e-6
2983        for ival, value in enumerate(L):
2984            if np.abs(value) > GEigThre:
2985                LargeVals += 1
2986                LargeIdx.append(ival)
2987        # This is the number of nonredundant DLCs that we expect to have at the end
2988        Expect = np.sum(np.linalg.eigh(G)[0] > 1e-6)
2989
2990        if (ncon + len(LargeIdx)) < Expect:
2991            raise RuntimeError("Expected at least %i delocalized coordinates, but got only %i" % (Expect, ncon + len(LargeIdx)))
2992        # print("%i atoms (expect %i coordinates); %i/%i singular values are > 1e-6" % (self.na, Expect, LargeVals, len(L)))
2993
2994        # Create "generalized" DLCs where the first six columns are the constrained primitive ICs
2995        # and the other columns are the DLCs formed from the rest
2996        self.Vecs = np.zeros((nprim, ncon+LargeVals), dtype=float)
2997        for i in range(ncon):
2998            self.Vecs[i, i] = 1.0
2999        self.Vecs[ncon:, ncon:ncon+LargeVals] = Q[:, LargeIdx]
3000
3001        # Perform Gram-Schmidt orthogonalization
3002        def ov(vi, vj):
3003            return multi_dot([vi, G, vj])
3004        if self.haveConstraints():
3005            click()
3006            V = self.Vecs.copy()
3007            nv = V.shape[1]
3008            Vnorms = np.array([np.sqrt(ov(V[:,ic], V[:, ic])) for ic in range(nv)])
3009            # U holds the Gram-Schmidt orthogonalized DLCs
3010            U = np.zeros((V.shape[0], Expect), dtype=float)
3011            Unorms = np.zeros(Expect, dtype=float)
3012
3013            for ic in range(ncon):
3014                # At the top of the loop, V columns are orthogonal to U columns up to ic.
3015                # Copy V column corresponding to the next constraint to U.
3016                U[:, ic] = V[:, ic].copy()
3017                ui = U[:, ic]
3018                Unorms[ic] = np.sqrt(ov(ui, ui))
3019                if Unorms[ic]/Vnorms[ic] < 0.1:
3020                    logger.warning("Constraint %i is almost redundant; after projection norm is %.3f of original\n" % (ic, Unorms[ic]/Vnorms[ic]))
3021                V0 = V.copy()
3022                # Project out newest U column from all remaining V columns.
3023                for jc in range(ic+1, nv):
3024                    vj = V[:, jc]
3025                    vj -= ui * ov(ui, vj)/Unorms[ic]**2
3026
3027            for ic in range(ncon, Expect):
3028                # Pick out the V column with the largest norm
3029                norms = np.array([np.sqrt(ov(V[:, jc], V[:, jc])) for jc in range(ncon, nv)])
3030                imax = ncon+np.argmax(norms)
3031                # Add this column to U
3032                U[:, ic] = V[:, imax].copy()
3033                ui = U[:, ic]
3034                Unorms[ic] = np.sqrt(ov(ui, ui))
3035                # Project out the newest U column from all V columns
3036                for jc in range(ncon, nv):
3037                    V[:, jc] -= ui * ov(ui, V[:, jc])/Unorms[ic]**2
3038
3039            # self.Vecs contains the linear combination coefficients that are our new DLCs
3040            self.Vecs = U.copy()
3041            # Constrained DLCs are on the left of self.Vecs.
3042            self.cDLC = [i for i in range(len(self.Prims.cPrims))]
3043
3044        self.Internals = ["Constraint" if i < ncon else "DLC" + " %i" % (i+1) for i in range(self.Vecs.shape[1])]
3045        # # LPW: Coefficients of DLC's are in each column and DLCs corresponding to constraints should basically be like (0 1 0 0 0 ..)
3046        # pmat2d(self.Vecs, format='f', precision=2)
3047        # B = self.Prims.wilsonB(xyz)
3048        # Bdlc = np.einsum('ji,jk->ik', self.Vecs, B)
3049        # Gdlc = np.dot(Bdlc, Bdlc.T)
3050        # # Expect to see a diagonal matrix here
3051        # print("Gdlc")
3052        # pmat2d(Gdlc, format='e', precision=2)
3053        # # Expect to see "large" eigenvalues here (no less than 0.1 ideally)
3054        # print("L, Q")
3055        # L, Q = np.linalg.eigh(Gdlc)
3056        # print(L)
3057
3058    def build_dlc(self, xyz):
3059        if self.conmethod == 1:
3060            return self.build_dlc_1(xyz)
3061        elif self.conmethod == 0:
3062            return self.build_dlc_0(xyz)
3063        else:
3064            raise RuntimeError("Unsupported value of conmethod %i" % self.conmethod)
3065
3066    def remove_TR(self, xyz):
3067        """
3068        Project overall translation and rotation out of the DLCs.
3069        This feature is intended to be used when an optimization job appears
3070        to contain slow rotations of the whole system, which sometimes happens.
3071        Uses the same logic as build_dlc_1.
3072        """
3073        # Create three translation and three rotation primitive ICs for the whole system
3074        na = int(len(xyz)/3)
3075        alla = range(na)
3076        sel = xyz.reshape(-1,3).copy()
3077        TRPrims = []
3078        TRPrims.append(TranslationX(alla, w=np.ones(na)/na))
3079        TRPrims.append(TranslationY(alla, w=np.ones(na)/na))
3080        TRPrims.append(TranslationZ(alla, w=np.ones(na)/na))
3081        sel -= np.mean(sel, axis=0)
3082        rg = np.sqrt(np.mean(np.sum(sel**2, axis=1)))
3083        TRPrims.append(RotationA(alla, xyz, self.Prims.Rotators, w=rg))
3084        TRPrims.append(RotationB(alla, xyz, self.Prims.Rotators, w=rg))
3085        TRPrims.append(RotationC(alla, xyz, self.Prims.Rotators, w=rg))
3086        # If these primitive ICs are already there, then move them to the front
3087        primorder = []
3088        addPrims = []
3089        for prim in TRPrims:
3090            if prim in self.Prims.Internals:
3091                primorder.append(self.Prims.Internals.index(prim))
3092            else:
3093                addPrims.append(prim)
3094        for iprim, prim in enumerate(self.Prims.Internals):
3095            if prim not in TRPrims:
3096                primorder.append(iprim)
3097        self.Prims.Internals = addPrims + [self.Prims.Internals[p] for p in primorder]
3098        self.Vecs = np.vstack((np.zeros((len(addPrims), self.Vecs.shape[1]), dtype=float), self.Vecs[np.array(primorder), :]))
3099
3100        self.clearCache()
3101        # Build DLCs with six extra in the front corresponding to the overall translations and rotations
3102        subVecs = self.Vecs.copy()
3103        self.Vecs = np.zeros((self.Vecs.shape[0], self.Vecs.shape[1]+6), dtype=float)
3104        self.Vecs[:6, :6] = np.eye(6)
3105        self.Vecs[:, 6:] = subVecs.copy()
3106        # pmat2d(self.Vecs, precision=3, format='f')
3107        # sys.exit()
3108        # This is the number of nonredundant DLCs that we expect to see
3109        G = self.Prims.GMatrix(xyz)
3110        Expect = np.sum(np.linalg.eigh(G)[0] > 1e-6)
3111
3112        # Carry out Gram-Schmidt orthogonalization
3113        # Define a function for computing overlap
3114        def ov(vi, vj):
3115            return multi_dot([vi, G, vj])
3116        V = self.Vecs.copy()
3117        nv = V.shape[1]
3118        Vnorms = np.array([np.sqrt(ov(V[:, ic], V[:, ic])) for ic in range(nv)])
3119        # G_tmp = np.array([[ov(V[:, ic], V[:, jc]) for jc in range(nv)] for ic in range(nv)])
3120        # pmat2d(G_tmp, precision=3, format='f')
3121        # U holds the Gram-Schmidt orthogonalized DLCs
3122        U = np.zeros((V.shape[0], Expect), dtype=float)
3123        Unorms = np.zeros(Expect, dtype=float)
3124        ncon = len(self.Prims.cPrims)
3125        # Keep translations, rotations, and any constraints in sequential order
3126        # and project them out of the remaining DLCs
3127        for ic in range(6+ncon):
3128            U[:, ic] = V[:, ic].copy()
3129            ui = U[:, ic]
3130            Unorms[ic] = np.sqrt(ov(ui, ui))
3131            if Unorms[ic]/Vnorms[ic] < 0.1:
3132                logger.warning("Constraint %i is almost redundant; after projection norm is %.3f of original\n" % (ic-6, Unorms[ic]/Vnorms[ic]))
3133            V0 = V.copy()
3134            # Project out newest U column from all remaining V columns.
3135            for jc in range(ic+1, nv):
3136                vj = V[:, jc]
3137                vj -= ui * ov(ui, vj)/Unorms[ic]**2
3138        # Now keep the remaining DLC with the largest norm, perform projection,
3139        # then repeat until the expected number is found
3140        shift = 6+ncon
3141        for ic in range(shift, Expect):
3142            # Pick out the V column with the largest norm
3143            norms = np.array([np.sqrt(ov(V[:, jc], V[:, jc])) for jc in range(shift, nv)])
3144            imax = shift+np.argmax(norms)
3145            # Add this column to U
3146            U[:, ic] = V[:, imax].copy()
3147            ui = U[:, ic]
3148            Unorms[ic] = np.sqrt(ov(ui, ui))
3149            # Project out the newest U column from all V columns
3150            for jc in range(ncon, nv):
3151                V[:, jc] -= ui * ov(ui, V[:, jc])/Unorms[ic]**2
3152        # self.Vecs contains the linear combination coefficients that are our new DLCs
3153        self.Vecs = U[:, 6:].copy()
3154        self.Internals = ["Constraint" if i < ncon else "DLC" + " %i" % (i+1) for i in range(self.Vecs.shape[1])]
3155
3156    def weight_vectors(self, xyz):
3157        """
3158        Not used anymore: Multiply each DLC by a constant so that a small displacement along each produces the
3159        same Cartesian displacement. Otherwise, some DLCs "move by a lot" and others only "move by a little".
3160
3161        Parameters
3162        ----------
3163        xyz : np.ndarray
3164            Flat array containing Cartesian coordinates in atomic units
3165        """
3166        Bmat = self.wilsonB(xyz)
3167        Ginv = self.GInverse(xyz, None)
3168        eps = 1e-6
3169        dxdq = np.zeros(len(self.Internals))
3170        for i in range(len(self.Internals)):
3171            dQ = np.zeros(len(self.Internals), dtype=float)
3172            dQ[i] = eps
3173            dxyz = multi_dot([Bmat.T, Ginv , dQ.T])
3174            rmsd = np.sqrt(np.mean(np.sum(np.array(dxyz).reshape(-1,3)**2, axis=1)))
3175            dxdq[i] = rmsd/eps
3176        dxdq /= np.max(dxdq)
3177        for i in range(len(self.Internals)):
3178            self.Vecs[:, i] *= dxdq[i]
3179
3180    def __eq__(self, other):
3181        return self.Prims == other.Prims
3182
3183    def __ne__(self, other):
3184        return not self.__eq__(other)
3185
3186    def largeRots(self):
3187        """ Determine whether a molecule has rotated by an amount larger than some threshold (hardcoded in Prims.largeRots()). """
3188        return self.Prims.largeRots()
3189
3190    def calcDiff(self, coord1, coord2):
3191        """ Calculate difference in internal coordinates (coord1-coord2), accounting for changes in 2*pi of angles. """
3192        PMDiff = self.Prims.calcDiff(coord1, coord2)
3193        Answer = np.dot(PMDiff, self.Vecs)
3194        return np.array(Answer).flatten()
3195
3196    def calculate(self, coords):
3197        """ Calculate the DLCs given the Cartesian coordinates. """
3198        PrimVals = self.Prims.calculate(coords)
3199        Answer = np.dot(PrimVals, self.Vecs)
3200        # To obtain the primitive coordinates from the delocalized internal coordinates,
3201        # simply multiply self.Vecs*Answer.T where Answer.T is the column vector of delocalized
3202        # internal coordinates. That means the "c's" in Equation 23 of Schlegel's review paper
3203        # are simply the rows of the Vecs matrix.
3204        # print np.dot(np.array(self.Vecs[0,:]).flatten(), np.array(Answer).flatten())
3205        # print PrimVals[0]
3206        # raw_input()
3207        return np.array(Answer).flatten()
3208
3209    def derivatives(self, coords):
3210        """ Obtain the change of the DLCs with respect to the Cartesian coordinates. """
3211        PrimDers = self.Prims.derivatives(coords)
3212        # The following code does the same as "tensordot"
3213        # print PrimDers.shape
3214        # print self.Vecs.shape
3215        # Answer = np.zeros((self.Vecs.shape[1], PrimDers.shape[1], PrimDers.shape[2]), dtype=float)
3216        # for i in range(self.Vecs.shape[1]):
3217        #     for j in range(self.Vecs.shape[0]):
3218        #         Answer[i, :, :] += self.Vecs[j, i] * PrimDers[j, :, :]
3219        # print Answer.shape
3220        Answer1 = np.tensordot(self.Vecs, PrimDers, axes=(0, 0))
3221        return np.array(Answer1)
3222
3223    def second_derivatives(self, coords):
3224        """ Obtain the second derivatives of the DLCs with respect to the Cartesian coordinates. """
3225        PrimDers = self.Prims.second_derivatives(coords)
3226        Answer2 = np.tensordot(self.Vecs, PrimDers, axes=(0, 0))
3227        return np.array(Answer2)
3228
3229    def GInverse(self, xyz):
3230        return self.GInverse_SVD(xyz)
3231
3232    def repr_diff(self, other):
3233        if hasattr(other, 'Prims'):
3234            return self.Prims.repr_diff(other.Prims)
3235        else:
3236            if self.Prims.repr_diff(other) == '':
3237                return 'Delocalized -> Primitive'
3238            else:
3239                return 'Delocalized -> Primitive\n' + self.Prims.repr_diff(other)
3240
3241    def guess_hessian(self, coords):
3242        """ Build the guess Hessian, consisting of a diagonal matrix
3243        in the primitive space and changed to the basis of DLCs. """
3244        Hprim = self.Prims.guess_hessian(coords)
3245        return multi_dot([self.Vecs.T,Hprim,self.Vecs])
3246
3247    def resetRotations(self, xyz):
3248        """ Reset the reference geometries for calculating the orientational variables. """
3249        self.Prims.resetRotations(xyz)
3250
3251class CartesianCoordinates(PrimitiveInternalCoordinates):
3252    """
3253    Cartesian coordinate system, written as a kind of internal coordinate class.
3254    This one does not support constraints, because that requires adding some
3255    primitive internal coordinates.
3256    """
3257    def __init__(self, molecule, **kwargs):
3258        super(CartesianCoordinates, self).__init__(molecule)
3259        self.Internals = []
3260        self.cPrims = []
3261        self.cVals = []
3262        self.elem = molecule.elem
3263        for i in range(molecule.na):
3264            self.add(CartesianX(i, w=1.0))
3265            self.add(CartesianY(i, w=1.0))
3266            self.add(CartesianZ(i, w=1.0))
3267        if kwargs.get('remove_tr', False):
3268            raise RuntimeError('Do not use remove_tr with Cartesian coordinates')
3269        if 'constraints' in kwargs and kwargs['constraints'] is not None:
3270            raise RuntimeError('Do not use constraints with Cartesian coordinates')
3271
3272    def guess_hessian(self, xyz):
3273        return 0.5*np.eye(len(xyz.flatten()))
3274