def __init__(self, atomlist, freeze=[], explicit_bonds=[], verbose=0): """ setup system of internal coordinates using valence bonds, angles and dihedrals Parameters ---------- atomlist : list of tuples (Z,[x,y,z]) with molecular geometry, connectivity defines the valence coordinates Optional -------- freeze : list of tuples of atom indices (starting at 0) corresponding to internal coordinates that should be frozen explicit_bonds : list of pairs of atom indices (starting at 0) between which artificial bonds should be inserted, i.e. [(0,1), (10,20)]. This allows to connect separate fragments. verbose : write out additional information if > 0 """ self.verbose = verbose self.atomlist = atomlist self.masses = AtomicData.atomlist2masses(self.atomlist) # Bonds, angles and torsions are constructed by the force field. # Atom types, partial charges and lattice vectors # all don't matter, so we assign atom type 6 (C_R, carbon in resonance) # to all atoms. atomtypes = [6 for atom in atomlist] partial_charges = [0.0 for atom in atomlist] lattice_vectors = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] # But since the covalent radii are wrong, we have to provide # the connectivity matrix conmat = XYZ.connectivity_matrix(atomlist, hydrogen_bonds=True) # insert artificial bonds for (I, J) in explicit_bonds: print("explicit bond between atoms %d-%d" % (I + 1, J + 1)) conmat[I, J] = 1 # Internal coordinates only work if the molecule does not # contain disconnected fragments, since there is no way how the # interfragment distance could be expressed in terms of internal coordinates. # We need to check that there is only a single fragment. fragment_graphs = MolecularGraph.atomlist2graph(self.atomlist, conmat=conmat) nr_fragments = len(fragment_graphs) error_msg = "The molecule consists of %d disconnected fragments.\n" % nr_fragments error_msg += "Internal coordinates only work if all atoms in the molecular graph are connected.\n" error_msg += "Disconnected fragments may be joined via an artificial bond using the\n" error_msg += "`explicit_bonds` option.\n" assert nr_fragments == 1, error_msg # Frozen degrees of freedom do not necessarily correspond to physical bonds # or angles. For instance we can freeze the H-H distance in water although there # is no bond between the hydrogens. To allow the definition of such 'unphysical' # internal coordinates, we have to modify the connectivity matrix and introduce # artificial bonds. for IJKL in freeze: if len(IJKL) == 2: I, J = IJKL # create artificial bond between atoms I and J conmat[I, J] = 1 elif len(IJKL) == 3: I, J, K = IJKL # create artifical bonds I-J and J-K so that the valence angle I-J-K exists conmat[I, J] = 1 conmat[J, K] = 1 elif len(IJKL) == 4: I, J, K, L = IJKL # create artifical bonds I-J, J-K and K-L so that the dihedral angle I-J-K-L # exists conmat[I, J] = 1 conmat[J, K] = 1 conmat[K, L] = 1 # cutoff for small singular values when solving the # linear system of equations B.dx = dq in a least square # sense. self.cond_threshold = 1.0e-10 self.force_field = PeriodicForceField(atomlist, atomtypes, partial_charges, lattice_vectors, [], connectivity_matrix=conmat, verbose=1) x0 = XYZ.atomlist2vector(atomlist) # shift molecule to center of mass self.x0 = MolCo.shift_to_com(x0, self.masses) self._selectActiveInternals(freeze=freeze)
class InternalValenceCoords: def __init__(self, atomlist, freeze=[], explicit_bonds=[], verbose=0): """ setup system of internal coordinates using valence bonds, angles and dihedrals Parameters ---------- atomlist : list of tuples (Z,[x,y,z]) with molecular geometry, connectivity defines the valence coordinates Optional -------- freeze : list of tuples of atom indices (starting at 0) corresponding to internal coordinates that should be frozen explicit_bonds : list of pairs of atom indices (starting at 0) between which artificial bonds should be inserted, i.e. [(0,1), (10,20)]. This allows to connect separate fragments. verbose : write out additional information if > 0 """ self.verbose = verbose self.atomlist = atomlist self.masses = AtomicData.atomlist2masses(self.atomlist) # Bonds, angles and torsions are constructed by the force field. # Atom types, partial charges and lattice vectors # all don't matter, so we assign atom type 6 (C_R, carbon in resonance) # to all atoms. atomtypes = [6 for atom in atomlist] partial_charges = [0.0 for atom in atomlist] lattice_vectors = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] # But since the covalent radii are wrong, we have to provide # the connectivity matrix conmat = XYZ.connectivity_matrix(atomlist, hydrogen_bonds=True) # insert artificial bonds for (I, J) in explicit_bonds: print("explicit bond between atoms %d-%d" % (I + 1, J + 1)) conmat[I, J] = 1 # Internal coordinates only work if the molecule does not # contain disconnected fragments, since there is no way how the # interfragment distance could be expressed in terms of internal coordinates. # We need to check that there is only a single fragment. fragment_graphs = MolecularGraph.atomlist2graph(self.atomlist, conmat=conmat) nr_fragments = len(fragment_graphs) error_msg = "The molecule consists of %d disconnected fragments.\n" % nr_fragments error_msg += "Internal coordinates only work if all atoms in the molecular graph are connected.\n" error_msg += "Disconnected fragments may be joined via an artificial bond using the\n" error_msg += "`explicit_bonds` option.\n" assert nr_fragments == 1, error_msg # Frozen degrees of freedom do not necessarily correspond to physical bonds # or angles. For instance we can freeze the H-H distance in water although there # is no bond between the hydrogens. To allow the definition of such 'unphysical' # internal coordinates, we have to modify the connectivity matrix and introduce # artificial bonds. for IJKL in freeze: if len(IJKL) == 2: I, J = IJKL # create artificial bond between atoms I and J conmat[I, J] = 1 elif len(IJKL) == 3: I, J, K = IJKL # create artifical bonds I-J and J-K so that the valence angle I-J-K exists conmat[I, J] = 1 conmat[J, K] = 1 elif len(IJKL) == 4: I, J, K, L = IJKL # create artifical bonds I-J, J-K and K-L so that the dihedral angle I-J-K-L # exists conmat[I, J] = 1 conmat[J, K] = 1 conmat[K, L] = 1 # cutoff for small singular values when solving the # linear system of equations B.dx = dq in a least square # sense. self.cond_threshold = 1.0e-10 self.force_field = PeriodicForceField(atomlist, atomtypes, partial_charges, lattice_vectors, [], connectivity_matrix=conmat, verbose=1) x0 = XYZ.atomlist2vector(atomlist) # shift molecule to center of mass self.x0 = MolCo.shift_to_com(x0, self.masses) self._selectActiveInternals(freeze=freeze) def _selectActiveInternals(self, freeze=[], use_internal_types=['B', 'A', 'D']): """ Here a redundant set of active internal coordinates is selected. The coordinates are included if they are of one of the types specified in `use_internal_types` (B - bond, A - bending angle, D - dihedral, I - inversion) With use_internal_types=['B','A','D'] all internal coordinates are used except to inversions. """ nat = len(self.atomlist) # compute internal coordinates # qprim - list all internal coordinates (bonds, angles, dihedrals, inversions) in bohr or radians # Bprim - Wilson's B-matrix, derivatives of all internal coordinates w/r/t cartesian coordinates # Bprim[i,j] = dqprim(i)/dx(j) i = 1,...,nred j=1,...,3*Nat qprim, Bprim = self.force_field.getRedundantInternalCoordinates( self.x0, 0) # number of redundant internal coordinates nred = len(qprim) # Perform singular value decomposition of B.B^T. There should be exactly 3*Nat-6 or 3*Nat-5 # singular values > 0, and 6 or 5 singular values = 0 G = np.dot(Bprim, Bprim.transpose()) U, s, Vh = la.svd(G) nint = len(s[s > 1.0e-8]) error_msg = "There should be 3*Nat-6= %d or 3*Nat-5= %d non-zero eigenvalues of matrix G = B.B^T, but got %d ones!" % ( 3 * nat - 6, 3 * nat - 5, nint) #assert (nint == 3*nat-6) or (nint == 3*nat-5), error_msg self.coord_types, self.atom_indices = self.force_field.getInternalCoordinateDefinitions( ) # Here we select a subset of the internal coordinates self.active_internals = [] for k, ktype in enumerate(self.coord_types): # select internal coordinate k if it has the right type if not (ktype in use_internal_types): continue # Internal coordinate k has passed tests, so we add it to the active set. self.active_internals.append(k) # Indices into set of active coordinate which are frozen. The # gradient along these coordinates is set to 0 when transforming # the cartesian gradient to internal coordinates. self.frozen_internals = [] for IJKL in freeze: self.freeze(IJKL) # In the following we prepare the columns and row labels for printing # internal coordinates nicely # labels for internal coordinates and cartesian coordinates self.internal_labels = [] max_width = 0 for iact, k in enumerate(self.active_internals): # shift indices by 1 atoms = [I + 1 for I in self.atom_indices[k]] label = self.coord_types[k] + "(" + "-".join( [str(i) for i in atoms]) + ")" self.internal_labels.append(label) max_width = max(max_width, len(label)) # all labels should have the same width for i in range(0, len(self.internal_labels)): self.internal_labels[i] = self.internal_labels[i].ljust( max_width, " ") # labels for cartesian positions, e.g. C1x, C1y, C1z, ... self.cartesian_labels = [] for i, (Z, pos) in enumerate(self.atomlist): at = AtomicData.atom_names[Z - 1].upper() xyz_labels = [ at + str(i + 1) + "x", at + str(i + 1) + "y", at + str(i + 1) + "z" ] self.cartesian_labels += xyz_labels # values of active coordinates and Wilson's B-matrix restricted to active set q = qprim[self.active_internals] B = Bprim[self.active_internals, :] if self.verbose > 0: # table with internal coordinates, atom indices and current values print(self._table_internal_coords(q)) print(self._table_Bmatrix(B)) def _internal_from_indices(self, IJKL): """ find the internal coordinate corresponding to the atom indices IJKL. """ for k, ktype in enumerate(self.coord_types): # compare atom indices in any order if set(self.atom_indices[k]) == set(IJKL): break else: raise ValueError( "The tuple of atom indices %s does not correspond to an existing bond, angle or dihedral!" % [I + 1 for I in IJKL]) if self.verbose > 1: print( "Atom indices %s correspond to the %d-th internal coordinate, which is of type %s." % (IJKL, k + 1, ktype)) return k def freeze(self, IJKL): """ freezes the internal coordinate defined by the atom indices IJKL. The atom indices do not necessarily have to correspond to a "physical" bond, angle or dihedral that is actually present in the molecule. So, for instance, you can also freeze the distance between two atoms that are not bonded. Parameters ---------- IJKL: tuple of 2, 3 or 4 atom indices (starting at 0) (I,J) - bond between atoms I and J (I,J,K) - valence angle I-J-K (I,J,K,L) - dihedral angle between the bonds I-J, J-K and K-L """ k = self._internal_from_indices(IJKL) if not k in self.active_internals: print("Internal coordinate %d does not belong to the active set") return kact = self.active_internals.index(k) if not kact in self.frozen_internals: self.frozen_internals.append(kact) def coordinate_value(self, x, IJKL): """ current value of internal coordinate defined by atom indices IJKL Parameters ---------- x : vector of cartesian coordinates IJKL : tuple of 2,3 or 4 atom indices (starting from 0) Returns ------- val : value of selected internal coordinate, bond lengths in Angstrom, angles in degrees """ k = self._internal_from_indices(IJKL) # determine internal coordinates q ~ x qprim, Bprim = self.force_field.getRedundantInternalCoordinates(x, 0) val = qprim[k] # convert units if len(IJKL) == 2: # bond val *= AtomicData.bohr_to_angs elif len(IJKL) in [3, 4]: # angles val *= 180.0 / np.pi return val def cartesian2internal(self, x): """ convert cartesian to redundant internal coordinates Parameters ---------- x : cartesian coordinates, array of length 3*Nat Returns ------- q : redundant internal coordinates of length n >= 3*nat-6 """ if self.verbose > 1: print("cartesian -> internal") x = MolCo.shift_to_com(x, self.masses) # values of internal coordinates at cartesian position x qprim, Bprim = self.force_field.getRedundantInternalCoordinates(x, 0) #test_wilson_bmatrix(self.force_field, x) # q = qprim[self.active_internals] B = Bprim[self.active_internals, :] # At the cartesian position x0 we know the internal coordinates q0 exactly. # We need this information later to deduce the new cartesian coordinates x (close to x0) # which corresponds to the new internal coordinates q (also close to q0). self.q0 = q self.x0 = x self.B0 = B # projector self.P0 = self.feasibility_projector(B) return q def internal2cartesian(self, q, max_iter=1000): """ transform internal coordinates back to cartesians. Since the internal coordinates are curvilinear the transformation has to be done iteratively and depends on having a closeby point q0 for which we know the cartesian coordinates x0. If the displacement dq = q-q0 is too large, the iteration will not converge. Given the initial point x0 ~ q0 we wish to find the cartesian coordinate x that corresponds to q x ~ q q = q0 + dq Parameters ---------- q : redundant internal coordinates, should not be too far from the coordinates obtained by the last call to `cartesian2delocalized(...)` Returns ------- x : cartesian coordinates corresponding to q, x ~ q Optional -------- max_iter : maximum number of iterative refinements of cartesian position """ # If the geometry did not change, there is nothing to do, # just return the old cartesian coordinates. if la.norm(q - self.q0) < 1.0e-15: return self.x0 if self.verbose > 0: print("internal -> cartesian") print(" The internal coordinates are iteratively transformed") print(" transformed back to cartesian coordinates.") # bending and torsion angles should be in the range [0,2*pi] and # inversions in the range [-pi/2,pi/2] q = self._wrap_angles(q) if self.verbose > 1: print("previous internal coordinates q0") print(self._table_internal_coords(self.q0)) print("current internal coordinates q") print(self._table_internal_coords(q)) # By how much did the internal coordinates change relative # to the reference geometry, where we know the transformation # x0 ~ q0 ? dq = q - self.q0 # solve B(q0).dx = dq for qx ret = sla.lstsq(self.B0, dq, cond=self.cond_threshold) dx = ret[0] # Initial quess for updated cartesian coordinates xi = self.x0 + dx if self.verbose > 2: err_dx = la.norm(np.dot(self.B0, dx) - dq) print("error |B0.dx - (q-q0)|= %e |dq|= %e" % (err_dx, la.norm(dq))) if self.verbose > 0: print(" Iteration= %4.1d |dx|= %e |q-q0|= %e" % (0, la.norm(dx), la.norm(dq))) # The initial guess xi is refined iteratively for i in range(0, max_iter): qi_prim, Bi_prim = self.force_field.getRedundantInternalCoordinates( xi, 0) qi = qi_prim[self.active_internals] Bi = Bi_prim[self.active_internals, :] if self.verbose > 2: print("intermediate internal coordinates q(%d)" % (i + 1)) print(self._table_internal_coords(qi)) print(self._table_Bmatrix(Bi)) ddq = q - qi # solve B(xi).dx = q-qi for refinement dx ret = sla.lstsq(Bi, ddq, cond=self.cond_threshold) dx = ret[0] xi += dx # error of least square solution err_dx = la.norm(np.dot(Bi, dx) - ddq) if self.verbose > 2: print("changes in internal coordinates q-q(%d)" % (i + 1)) print(self._table_internal_coords(ddq)) if self.verbose > 0: print( " Iteration= %4.1d |B.dx-dq|= %e |dx|= %e |q-q(%d)|= %e" % (i + 1, err_dx, la.norm(dx), i + 1, la.norm(ddq))) # If qi has converged to q or the desired accuracy cannot # be reached, because the solution of B.dx = q-qi has a # large error anyway, we accept the final cartesian coordinates xi. # The factor 1.01 is to avoid an endless loop if |dq| == err_dx. if la.norm(ddq) < 1.01 * max(err_dx, 1.0e-10): break # Abort if new cartesian coordinates make no sense. dx_norm = la.norm(dx) if (dx_norm > 1.0e6): raise RuntimeError( "change in cartesian coordinates too large |dx|= %e !" % dx_norm) else: dq_norm = la.norm(q - qi) if dq_norm < 0.5: print( "WARNING: Internal->cartesian transformation did not converge!" ) print( " But |dq|= %e is not too large, so we try to continue anyway." % la.norm(q - qi)) else: raise NotConvergedError( "ERROR: internal->cartesian transformation did not converge! |q-qi|= %e" % dq_norm) # We have succesfully found the cartesian coordinates x ~ q # and use them as the new reference geometry. self.q0 = np.copy(qi) self.x0 = np.copy(xi) self.B0 = np.copy(Bi) return xi def transform_gradient(self, x, g_cart, max_iter=200): """ transform cartesian gradient g_cart = dE/dx to redundant internal coordinates according to B^T g_intern = g_cart The underdetermined system of linear equations is solved in a least square sense. """ if self.verbose > 0: print("transform gradient to internal coordinates") print(" dE/dx -> dE/dq") # Since the energy of a molecule depends only on the internal # coordinates q it should be possible to transform the cartesian # gradient exactly into internal coordinates according to # dE/dx(i) = sum_j dE/dq(j) dq(j)/dx(i) # = sum_j (B^T)_{i,j} dE/dq(j) # # cartesian force f_cart = -g_cart # cartesian accelerations a_cart = f_cart / self.masses # cartesian velocity dt = 1.0 vel = a_cart * dt # shift position to center of mass x = MolCo.shift_to_com(x, self.masses) # eliminate rotation and translation from a_cart I = MolCo.inertial_tensor(self.masses, x) L = MolCo.angular_momentum(self.masses, x, vel) P = MolCo.linear_momentum(self.masses, vel) omega = MolCo.angular_velocity(L, I) vel = MolCo.eliminate_translation(self.masses, vel, P) vel = MolCo.eliminate_rotation(vel, omega, x) # Check that the total linear momentum and angular momentum # indeed vanish. assert la.norm(MolCo.linear_momentum(self.masses, vel)) < 1.0e-14 assert la.norm(MolCo.angular_momentum(self.masses, x, vel)) < 1.0e-14 # go back from velocities to gradient g_cart = -vel / dt * self.masses # solve B^T . g_intern = g_cart by linear least squares. ret = sla.lstsq(self.B0.transpose(), g_cart, cond=self.cond_threshold) g_intern = ret[0] if self.verbose > 1: print(self._table_gradients(g_cart, g_intern)) # check solution err = la.norm(np.dot(self.B0.transpose(), g_intern) - g_cart) assert err < 1.0e-10, "|B^T.g_intern - g_cart|= %e" % err # Abort if gradients are not reasonable gnorm = la.norm(g_intern) if gnorm > 1.0e5: raise RuntimeError( "ERROR: Internal gradient is too large |grad|= %e" % gnorm) # Components of gradient belonging to frozen internal # coordinates are zeroed to avoid changing them. g_intern[self.frozen_internals] = 0.0 # Simply setting some components of the gradient to zero # will most likely lead to inconsistencies if the coordinates # are coupled (Maybe it's impossible to change internal coordinate X # without changing coordinate Y at the same time). These # inconsistencies are removed by applying the projection # operator repeatedly until the components of the frozen # internal coordinates have converged to 0. if self.verbose > 0: print(" apply projector P=G.G- to internal gradient") # The projector is updated everytime cartesian2internal(...) # is called. proj = self.P0 for i in range(0, max_iter): g_intern = np.dot(proj, g_intern) # gradient along frozen coordinates should be zero gnorm_frozen = la.norm(g_intern[self.frozen_internals]) if self.verbose > 0: print(" Iteration= %4.1d |grad(frozen)|= %s" % (i, gnorm_frozen)) if gnorm_frozen < 1.0e-10: break else: g_intern[self.frozen_internals] = 0.0 else: if gnorm_frozen < 1.0e-5: print( "WARNING: Projection of gradient vector in internal coordinates did not converge!" ) print( " But |grad(frozen)|= %e is not too large, so let's continue anyway." % gnorm_frozen) else: raise RuntimeError( "ERROR: Projection of gradient vector in internal coordinates did not converge! |grad(frozen)|= %e" % gnorm_frozen) if self.verbose > 1: print( "gradients after applying projector (only internal gradient changes)" ) print(self._table_gradients(g_cart, g_intern)) return g_intern def feasibility_projector(self, B): """ compute the projector P into the space of feasible displacement vectors Not all displacements in internal coordinates are feasible, since internal coordinates often depend on each other so that changing one coordinate requires changing another coordinate at the same time. A displacement that would change only one of them, would be impossible to realize. Given a displacement vector `g`, the projection matrix P resolves these dependencies, so that `g_prj = P.g` is a feasible displacement vector. Parameters ---------- B : Wilson's B-matrix for active set of internal coordinates Returns ------- P : projector onto feasible directions """ # n is the number of active internal coordinates n, ncart = B.shape assert n == len(self.active_internals) # G = B.B^T G = np.dot(B, B.transpose()) # compute pseudo-inverse G^- Gm = la.pinv(G) # projector due to coupling of internal coordinates P = np.dot(G, Gm) return P def internal_step(self, x0, IJKL, incr, max_iter=200): """ take a step of size `dq` along the internal coordinate defined by the atom indices IJKL starting from the cartesian coordinates `x0`. Parameters ---------- x0 : initial cartesian coordinates IJKL : tuple of 2, 3 or 4 atom indices (starting at 0) (I,J) - bond between atoms I and J (I,J,K) - valence angle I-J-K (I,J,K,L) - dihedral angle between the bonds I-J, J-K and K-L incr : displacement, in bohr for bond lengths and in radians for angles Optional -------- max_iter : maximum number of interators for projecting the step into the feasible subspace Returns ------- x1 : cartesian coordinates corresponding to the displaced geometry x0 ~ q0, x1 ~ q0+incr*e_IJKL """ if self.verbose > 0: print("take internal step along coordinate %s" % [I + 1 for I in IJKL]) # Which internal coordinate should be changed? k = self._internal_from_indices(IJKL) # 1) transform cartesian to internal coordinates, x0 ~ q0 q0 = self.cartesian2internal(x0) # 2) take a step along the internal coordinate k dq = np.zeros(len(q0)) dq[k] = incr # 3) Since internal coordinates are coupled it's not # always possible to change only one coordinate without # changing others. Therefore the displacement dq has to be # projected into the space of displacements that are compatible # with the dependencies between internal coordinates. # For instance the angles A-B-C, A-B-D and C-B-D are coupled. # If we increase the angle A-B-C by 2 degrees we have # to decrease the angles A-B-D and C-B-D each by 1 degree. This is # accomplished by the projection. if self.verbose > 0: print(" apply projector P=G.G- to step dq in internal coordinates") # projector onto feasible displacements proj = self.P0 for i in range(0, max_iter): dq = np.dot(proj, dq) ddq = dq[k] - incr if self.verbose > 0: print(" Iteration= %4.1d |dq[k]-incr|= %s" % (i, abs(ddq))) if abs(ddq) < 1.0e-10: break else: dq[k] = incr else: if abs(ddq) < 1.0e-6: print( "WARNING: Projection of displacement vector in internal coordinates did not converge! |dq[k]-incr|= %e" % abs(ddq)) print( " But the deviation is not too large, so let's try to continue anyway." ) else: raise RuntimeError( "ERROR: Projection of displacement vector in internal coordinates did not converge! |dq[k]-incr|= %e" % abs(ddq)) q1 = q0 + dq if self.verbose > 0: print("initial coordinates") print(self._table_internal_coords(q0)) print("coordinates after step along %d-th coordinate" % (k + 1)) print(self._table_internal_coords(q1)) # 4) transform back x1 ~ q1 x1 = self.internal2cartesian(q1) # verify that x really corresponds to internal coordinate q q_test = self.cartesian2internal(x1) err = la.norm(q1 - q_test) # assert err < 1.0e-10, "|q(x(q)) - q|= %e" % err if self.verbose > 0: print("coordinates after step (determined from cartesians)") print(self._table_internal_coords(q_test)) return x1 def _wrap_angles(self, q): """ Bending angles and dihedral angles have to be in the range [0,pi], while inversion angles have to be in the range [-pi/2,pi/2]. Angles outside these ranges are wrapped back to the equivalent angle inside the range. Parameters ---------- q : values of active internal coordinates with angles outside the valid ranges Returns ------- q_wrap : values of internal coordinates with all angles inside valid ranges """ # types of the active coordinates act_coord_types = self.coord_types[self.active_internals] # select internal coordinates which are angles bending_indices = np.where(act_coord_types == 'A')[0] torsion_indices = np.where(act_coord_types == 'D')[0] inversion_indices = np.where(act_coord_types == 'I')[0] bending = q[bending_indices] torsion = q[torsion_indices] inversion = q[inversion_indices] # Shifting angles by multiples of 2*pi has no effect # for any type of angle. bending = bending % (2 * np.pi) torsion = torsion % (2 * np.pi) inversion = inversion % (2 * np.pi) # wrap bending and torsion angles back into the range [0,pi] bending[bending > np.pi] = 2 * np.pi - bending[bending > np.pi] torsion[torsion > np.pi] = 2 * np.pi - torsion[torsion > np.pi] # wrap inversion angle back into the range [-pi/2,pi/2] # shift interval [pi,2*pi] to [-pi,0] inversion[inversion > np.pi] = inversion[inversion > np.pi] - 2 * np.pi # wrap [-pi,-pi/2] to [-pi/2,0] # and [pi/2,pi] to [0, pi/2] inversion[inversion > np.pi / 2] = np.pi - inversion[inversion > np.pi / 2] inversion[inversion < -np.pi / 2] = -np.pi - inversion[inversion < -np.pi / 2] # update internal coordinates with wrapped angles q[bending_indices] = bending q[torsion_indices] = torsion q[inversion_indices] = inversion return q def _table_internal_coords(self, q): """ make table with internal coordinates, atom indices and current values """ txt = "\n" txt += " ============================================================================= \n" txt += " Selected Active Internal Coordinates \n" txt += " \n" txt += " Internal Coordinate Atom Indices Current Value Frozen \n" txt += " Nr. Type (starting at 1) \n" txt += " ============================================================================= \n" for iact, k in enumerate(self.active_internals): # If this coordinate frozen? if iact in self.frozen_internals: frozen_flag = "Y" else: frozen_flag = "N" ktype = self.coord_types[k] if ktype == 'B': I, J = self.atom_indices[k] txt += " %4.1d BOND %3.1d %3.1d %+8.5f Ang " % ( iact + 1, I + 1, J + 1, q[iact] * AtomicData.bohr_to_angs) elif ktype == 'A': I, J, K = self.atom_indices[k] txt += " %4.1d ANGLE %3.1d %3.1d %3.1d %+12.5f degrees" % ( iact + 1, I + 1, J + 1, K + 1, q[iact] * 180.0 / np.pi) elif ktype == 'D': I, J, K, L = self.atom_indices[k] txt += " %4.1d DIHEDRAL %3.1d %3.1d %3.1d %3.1d %+12.5f degrees" % ( iact + 1, I + 1, J + 1, K + 1, L + 1, q[iact] * 180.0 / np.pi) elif ktype == 'I': I, J, K, L = self.atom_indices[k] txt += " %4.1d INVERSION %3.1d %3.1d %3.1d %3.1d %+12.5f degrees" % ( iact + 1, I + 1, J + 1, K + 1, L + 1, q[iact] * 180.0 / np.pi) txt += " %s \n" % frozen_flag txt += "\n" return txt def _table_gradients(self, g_cart, g_intern): """ make tables with gradient vectors in cartesian and internal coordinates """ txt = "" txt += " =========================== \n" txt += " Cartesian Gradient dE/dx(i) \n" txt += " =========================== \n" txt += utils.annotated_matrix(np.reshape(g_cart, (len(g_cart), 1)), self.cartesian_labels, ["grad E"]) txt += " cartesian gradient norm : %e \n" % la.norm(g_cart) txt += "\n" txt += " ========================== \n" txt += " Internal Gradient dE/dq(i) \n" txt += " ========================== \n" txt += utils.annotated_matrix(np.reshape(g_intern, (len(g_intern), 1)), self.internal_labels, ["grad E"]) txt += " internal gradient norm : %e \n" % la.norm(g_intern) txt += "\n" return txt def _table_Bmatrix(self, B): """ make table with rows of Wilson's B-matrix belonging to the active internal coordinates """ txt = " ======================================================================\n" txt += " Wilson's B-matrix B(i,j) = dq(i)/dx(j) for active internal coordinates\n" txt += " B - bond, A - valence angle, D - dihedral, I - inversion \n" txt += " ======================================================================\n" txt += utils.annotated_matrix(B, self.internal_labels, self.cartesian_labels) txt += "\n" return txt
parser.add_option("--spectrum_file", dest="spectrum_file", type=str, default="exciton_spectrum.dat", help="Save excitonic absorption and circular dichroism spectra to this file [default: %default]") parser.add_option("--state", dest="state", type=int, default=0, help="Excitonic state for which the gradient should be calculated (0 for ground state). [default: %default]") parser.add_option("--plot_spectra", dest="plot_spectra", type=str, default="", help="Choose units ('nm', 'Hartree', 'cm-1' or 'eV') for plotting absorption and circular dichroism spectra. [default: %default]") (opts,args) = parser.parse_args() if len(args) < 1: print usage exit(-1) ff_file = args[0] #"h2.ff" #"ethene.ff" #"pyrene_crystal_expanded.ff" # # read force field definition atomlist, atomtypes, partial_charges, lattice_vectors = read_force_field(ff_file) # read transition charge for exciton model (if available) if opts.transition_charges != "": chromophores = list(read_transition_charges(opts.transition_charges)) else: chromophores = [] pff = PeriodicForceField(atomlist, atomtypes, partial_charges, lattice_vectors, chromophores) coords = XYZ.atomlist2vector(atomlist) # evaluate force field once energy, grad = pff.getEnergyAndGradient(coords, state=opts.state) print "Total energy: %s" % energy print "|gradient| : %s" % la.norm(grad) # compute exciton spectrum en, T, M = pff.getTransitionDipoles(verbose=1) save_exciton_spectrum(opts.spectrum_file, en, T, M) # if opts.plot_spectra != "": plot_exciton_spectrum(en, T, M, units=opts.plot_spectra)
def __init__(self, atomlist_full, inner_indeces, embedding="electrostatic", pff_file=None, verbose=0): """ Parameters: =========== atomlist_full: list of tuples (Zi,[xi,yi,zi]) for all atoms (QM + MM) inner_indeces: list of the indeces which belong to the QM atoms """ assert embedding in ["mechanical", "electrostatic"] if "-" in inner_indeces: # index list contains ranges such as "9-14" inner_indeces = parseAtomTags(inner_indeces) else: inner_indeces = list(eval(inner_indeces)) inner_indeces = list(set(inner_indeces)) # remove duplicate indeces inner_indeces.sort() if verbose > 0: print("Indeces of QM atoms:") print(inner_indeces) print("number of QM atoms: %d" % len(inner_indeces)) # counting in the partitioning file starts at 1 inner_indeces = np.asarray(inner_indeces, dtype=int)-1 Nat = len(atomlist_full) all_indeces = set(range(0, Nat)) outer_indeces = list(all_indeces - set(inner_indeces)) outer_indeces.sort() self.inner_indeces = inner_indeces self.outer_indeces = outer_indeces # sort atoms into inner and outer region self.atomlist_full = atomlist_full self.atomlist_inner = [self.atomlist_full[i] for i in self.inner_indeces] self.atomlist_outer = [self.atomlist_full[i] for i in self.outer_indeces] if (pff_file == None): # # prepare the drivers for the UFF calculations # E^MM(I+O) self.FF_full = Gaussian.UFF_handler(self.atomlist_full, embedding=embedding, verbose=verbose, unique_tmp=True) # E^MM(I) self.FF_inner = Gaussian.UFF_handler(self.atomlist_inner, embedding=embedding, verbose=verbose, unique_tmp=True) else: # load definitions for periodic force field if verbose > 0: print("periodic MM calculations with DREIDING") atomlist_full_ff, atomtypes_full, charges_full, lattice_vectors = read_force_field(pff_file) atomtypes_inner = [atomtypes_full[i] for i in self.inner_indeces] charges_inner = [charges_full[i] for i in self.inner_indeces] if (len(atomlist_full_ff) != Nat): raise ValueError("Wrong number of atoms in '%s'. Expected %d atoms but got %d !" \ % (pff_file, Nat, len(atomlist))) # prepare drivers for DREIDING calculations # E^MM(I+O) self.FF_full = PeriodicForceField(self.atomlist_full, atomtypes_full, charges_full, lattice_vectors, [], verbose=verbose) # E^MM(I) self.FF_inner = PeriodicForceField(self.atomlist_inner, atomtypes_inner, charges_inner, lattice_vectors, [], verbose=verbose) self.charge = 0
class QMMM: def __init__(self, atomlist_full, inner_indeces, embedding="electrostatic", pff_file=None, verbose=0): """ Parameters: =========== atomlist_full: list of tuples (Zi,[xi,yi,zi]) for all atoms (QM + MM) inner_indeces: list of the indeces which belong to the QM atoms """ assert embedding in ["mechanical", "electrostatic"] if "-" in inner_indeces: # index list contains ranges such as "9-14" inner_indeces = parseAtomTags(inner_indeces) else: inner_indeces = list(eval(inner_indeces)) inner_indeces = list(set(inner_indeces)) # remove duplicate indeces inner_indeces.sort() if verbose > 0: print("Indeces of QM atoms:") print(inner_indeces) print("number of QM atoms: %d" % len(inner_indeces)) # counting in the partitioning file starts at 1 inner_indeces = np.asarray(inner_indeces, dtype=int)-1 Nat = len(atomlist_full) all_indeces = set(range(0, Nat)) outer_indeces = list(all_indeces - set(inner_indeces)) outer_indeces.sort() self.inner_indeces = inner_indeces self.outer_indeces = outer_indeces # sort atoms into inner and outer region self.atomlist_full = atomlist_full self.atomlist_inner = [self.atomlist_full[i] for i in self.inner_indeces] self.atomlist_outer = [self.atomlist_full[i] for i in self.outer_indeces] if (pff_file == None): # # prepare the drivers for the UFF calculations # E^MM(I+O) self.FF_full = Gaussian.UFF_handler(self.atomlist_full, embedding=embedding, verbose=verbose, unique_tmp=True) # E^MM(I) self.FF_inner = Gaussian.UFF_handler(self.atomlist_inner, embedding=embedding, verbose=verbose, unique_tmp=True) else: # load definitions for periodic force field if verbose > 0: print("periodic MM calculations with DREIDING") atomlist_full_ff, atomtypes_full, charges_full, lattice_vectors = read_force_field(pff_file) atomtypes_inner = [atomtypes_full[i] for i in self.inner_indeces] charges_inner = [charges_full[i] for i in self.inner_indeces] if (len(atomlist_full_ff) != Nat): raise ValueError("Wrong number of atoms in '%s'. Expected %d atoms but got %d !" \ % (pff_file, Nat, len(atomlist))) # prepare drivers for DREIDING calculations # E^MM(I+O) self.FF_full = PeriodicForceField(self.atomlist_full, atomtypes_full, charges_full, lattice_vectors, [], verbose=verbose) # E^MM(I) self.FF_inner = PeriodicForceField(self.atomlist_inner, atomtypes_inner, charges_inner, lattice_vectors, [], verbose=verbose) self.charge = 0 def getGeometryFull(self): return self.atomlist_full def partitionGeometry(self, atomlist): """ divides the geometry into an inner and an outer region Parameters: =========== atomlist: atomlist contains the inner and outer region. Returns: ======== atomlist_inner: a list of the atoms that should belong to the inner region. This is the atomlist that the DFTB modules will see and work with. """ self.atomlist_full = atomlist # sort atoms into inner and outer region self.atomlist_inner = [self.atomlist_full[i] for i in self.inner_indeces] self.atomlist_outer = [self.atomlist_full[i] for i in self.outer_indeces] return self.atomlist_inner def setCharge(self, nelec, charge): if nelec % 2 == 0: self.multiplicity = 1 else: self.multiplicity = 2 self.charge = charge def getEnergy(self): """ """ self.FF_full.calc(self.atomlist_full, charge=self.charge, multiplicity=self.multiplicity) self.FF_inner.calc(self.atomlist_inner, charge=self.charge, multiplicity=self.multiplicity) # en_MM_full = self.FF_full.get_MM_Energy() en_MM_inner = self.FF_inner.get_MM_Energy() # total energy E^QMMM = E^MM(I+O) - E^MM(I) en_QMMM = en_MM_full - en_MM_inner return en_QMMM def getGradientFull(self, grad_QM_inner): """ """ grad_MM_full = self.FF_full.get_MM_Gradient() grad_MM_inner = self.FF_inner.get_MM_Gradient() grad = grad_MM_full # grad E = grad E^MM(I+O) + grad E^QM(I) - grad E^MM(I) for i,iI in enumerate(self.inner_indeces): grad[3*iI:3*(iI+1)] += grad_QM_inner[3*i:3*(i+1)] - grad_MM_inner[3*i:3*(i+1)] return grad
(opts, args) = parser.parse_args() if len(args) < 1: print usage exit(-1) ff_file = args[0] # read force field definition atomlist, atomtypes, partial_charges, lattice_vectors = read_force_field( ff_file) # read transition charge for exciton model (if available) if opts.transition_charges != "": chromophores = list(read_transition_charges(opts.transition_charges)) else: chromophores = [] force_field = PeriodicForceField(atomlist, atomtypes, partial_charges, lattice_vectors, chromophores) # convert geometry to a vector x0 = XYZ.atomlist2vector(atomlist) # FIND ENERGY MINIMUM # f is the objective function that should be minimized # it returns (f(x), f'(x)) def f(x): # also save geometries from line searches #save_xyz(x) # energy, grad = force_field.getEnergyAndGradient(x, state=opts.state) print "E = %2.7f |grad| = %2.7f" % (energy, la.norm(grad)) # return energy, grad