def test_dftb_eigenvector_derivative(): """compare analytical and numerical gradients of MO coefficients and orbital energies""" from DFTB.XYZ import read_xyz, extract_keywords_xyz from DFTB.DFTB2 import DFTB2 from DFTB.Analyse.Cube import CubeExporterEx from DFTB.Molden import MoldenExporter from DFTB import utils from DFTB.LR_TDDFTB import LR_TDDFTB from DFTB.ExcGradients import Gradients import sys import os usage = "Usage: %s <xyz-file>\n" % sys.argv[0] usage += " --help option will give more information\n" parser = utils.OptionParserFuncWrapper([\ DFTB2.__init__, DFTB2.runSCC, \ LR_TDDFTB.getEnergies, LR_TDDFTB.saveAbsorptionSpectrum, LR_TDDFTB.analyseParticleHole, \ LR_TDDFTB.graphical_analysis, \ CubeExporterEx.exportCubes, MoldenExporter.export, \ Gradients.getGradients], \ usage) (options, args) = parser.parse_args(DFTB2.__init__) if len(args) < 1: print(usage) exit(-1) xyz_file = args[0] atomlist = read_xyz(xyz_file)[0] kwds = extract_keywords_xyz(xyz_file) tddftb = LR_TDDFTB(atomlist, **options) (options, args) = parser.parse_args(tddftb.getEnergies) (scf_options, args) = parser.parse_args(tddftb.dftb2.runSCC) options.update(scf_options) tddftb.setGeometry(atomlist, charge=kwds.get("charge", 0.0)) tddftb.getEnergies(**options) grad = Gradients(tddftb) grad.gradient(I=0, save_intermediates_CPKS=1) dE_an, dX_an = grad.getMOgradients() dftb2 = tddftb.dftb2 E = dftb2.getKSEnergies() X = dftb2.getKSCoefficients() dE_num, dX_num = dftb_numerical_mo_gradients(dftb2, atomlist) print("eigenvalues") print(E) print("numerical eigenvalue gradients") print(dE_num) print("analytical eigenvalue gradients") print(dE_an) err_dE = la.norm(dE_num - dE_an) err_dX = la.norm(dX_num - dX_an) assert err_dE < 1.0e-3, "err(dE) = %s" % err_dE
def test_dftb_charge_derivative(): """compare analytical and numerical gradients of Mulliken charges""" from DFTB.XYZ import read_xyz, extract_keywords_xyz from DFTB.DFTB2 import DFTB2 from DFTB.Analyse.Cube import CubeExporterEx from DFTB.Molden import MoldenExporter from DFTB import utils from DFTB.LR_TDDFTB import LR_TDDFTB from DFTB.ExcGradients import Gradients import sys import os usage = "Usage: %s <xyz-file>\n" % sys.argv[0] usage += " --help option will give more information\n" parser = utils.OptionParserFuncWrapper([\ DFTB2.__init__, DFTB2.runSCC, \ LR_TDDFTB.getEnergies, LR_TDDFTB.saveAbsorptionSpectrum, LR_TDDFTB.analyseParticleHole, \ LR_TDDFTB.graphical_analysis, \ CubeExporterEx.exportCubes, MoldenExporter.export, \ Gradients.getGradients], \ usage) (options, args) = parser.parse_args(DFTB2.__init__) if len(args) < 1: print usage exit(-1) xyz_file = args[0] atomlist = read_xyz(xyz_file)[0] kwds = extract_keywords_xyz(xyz_file) tddftb = LR_TDDFTB(atomlist, **options) (options, args) = parser.parse_args(tddftb.getEnergies) (scf_options, args) = parser.parse_args(tddftb.dftb2.runSCC) options.update(scf_options) tddftb.setGeometry(atomlist, charge=kwds.get("charge", 0.0)) tddftb.getEnergies(**options) grad = Gradients(tddftb) grad.gradient(I=0, save_intermediates_CPKS=1) dQdp_ana = grad.getChargeGradients() dftb2 = tddftb.dftb2 dQdp_num = dftb_numerical_charge_gradients(dftb2, atomlist) print "partial Mulliken charges" print dftb2.getPartialCharges() print "numerical charge gradients" print dQdp_num print "analytical charge gradients" print dQdp_ana print "difference" print dQdp_num - dQdp_ana err_dQ = la.norm(dQdp_num - dQdp_ana) print "err(dQdp) = %s" % err_dQ #assert err_dQ < 1.0e-4, "err(dQdp) = %s" % err_dQ # show timings print T
class PotentialEnergySurfaces(object): def __init__(self, atomlist, Nst=2, **kwds): """ The DFTB module has many parameters which can be set from the command line using options, e.g. --scf_conv=1.0e-10. During the initialization - the command line arguments are parsed for options - Slater-Koster tables and repulsive potentials are loaded for the atom pair present in the molecule Parameters: =========== atomlist: list of tuples (Zi,[xi,yi,zi]) for each atom in the molecule Nst: number of electronic states (including ground state) """ usage = "Type --help to show all options for DFTB" parser = optparse.OptionParserFuncWrapper([ DFTB2.__init__, DFTB2.runSCC, SolventCavity.__init__, LR_TDDFTB.getEnergies ], usage, section_headers=["DFTBaby"], unknown_options="ignore") options, args = parser.parse_args(DFTB2.__init__) self.atomlist = atomlist self.tddftb = LR_TDDFTB(atomlist, **options) solvent_options, args = parser.parse_args(SolventCavity.__init__) solvent_cavity = SolventCavity(**solvent_options) self.tddftb.dftb2.setSolventCavity(solvent_cavity) self.grads = Gradients(self.tddftb) self.tddftb.setGeometry(atomlist, charge=kwds.get("charge", 0.0)) self.options, args = parser.parse_args(self.tddftb.getEnergies) self.scf_options, args = parser.parse_args(self.tddftb.dftb2.runSCC) self.options.update(self.scf_options) self.Nst = Nst # # always use iterative diagonalizer for lowest Nst-1 excited states self.options["nstates"] = Nst - 1 # save geometry, orbitals and TD-DFT coefficients from # last calculation self.last_calculation = None # save transition dipoles from last calculation self.tdip_old = None def getEnergies(self, x): """ This functions computes the adiabatic energies ONLY. It should not be used during a dynamics simulation """ self.atomlist = XYZ.vector2atomlist(x, self.atomlist) self.tddftb.setGeometry(self.atomlist, keep_charge=True, update_symmetry=False) self.tddftb.getEnergies(**self.options) # total ground state energy E0 = self.tddftb.dftb2.E_tot # excitation energies Exc = self.tddftb.getExcEnergies() # total energies of ground and excited states energies = np.hstack(([E0], E0 + Exc)) return energies[:self.Nst] def getEnergy_S0(self, x): """ This function compute the ground state energy ONLY. """ self.atomlist = XYZ.vector2atomlist(x, self.atomlist) self.tddftb.setGeometry(self.atomlist, keep_charge=True, update_symmetry=False) E0 = self.tddftb.dftb2.getEnergy(**self.scf_options) energies = np.array([E0]) # only ground state energy return energies @T.timer def getEnergiesAndGradient(self, x, gradient_state): """ This function computes the adiabatic electronic energies for the nuclear geometry contained in 'x' and the gradient of the total energy of state 'gradient_state'. Parameters: =========== x: a numpy array with the nuclear coordinates, x[3*i:3*(i+1)] should contain the cartesian coordinates of atom i gradient_state: index of the electronic state, whose gradient should be calculated. 0 stands for the ground state, 1 for the 1st excited state, etc. Returns: ======== energies: numpy array with total energies (in Hartree) for all electronic states gradTot: total gradient of the selected state, gradTot[3*i:3*(i+1)] contains the energy derivative w/r/t the 3 cartesian coordinates of atom i. """ self.atomlist = XYZ.vector2atomlist(x, self.atomlist) self.tddftb.setGeometry(self.atomlist, keep_charge=True, update_symmetry=False) self.tddftb.getEnergies(**self.options) # total ground state energy E0 = self.tddftb.dftb2.E_tot # excitation energies Exc = self.tddftb.getExcEnergies() # total energies of ground and excited states energies = np.hstack(([E0], E0 + Exc)) gradVrep, gradE0, gradExc = self.grads.gradient( I=gradient_state, save_intermediates_CPKS=1) gradTot = gradVrep + gradE0 + gradExc # QM/MM if self.tddftb.dftb2.qmmm != None: # expand gradient to the full system gradTot = self.tddftb.dftb2.qmmm.getGradientFull(gradTot) # confining cavity if self.tddftb.dftb2.cavity != None: if self.tddftb.dftb2.qmmm != None: atomlist = self.tddftb.dftb2.qmmm.getGeometryFull() else: atomlist = self.atomlist gradTot += self.tddftb.dftb2.cavity.getGradient(atomlist) return energies, gradTot def getEnergyAndGradient_S0(self, x, has_scf=False): """ obtain the ground state energy and gradient while skipping an excited state calculation. This function should be called with has_scf=True directly after a failed TD-DFTB calculation. Then the scf calculation is skipped as well, assuming that the converged ground state orbitals are available. Returns: ======== E0: ground state energy gradTot: gradient on ground state """ # ground state energy if has_scf == True: # SCF calculation has been done already E0 = self.tddftb.dftb2.E_tot else: self.atomlist = XYZ.vector2atomlist(x, self.atomlist) self.tddftb.setGeometry(self.atomlist, keep_charge=True, update_symmetry=False) # SCF calculation E0 = self.tddftb.dftb2.getEnergy(**self.scf_options) # try to compute gradient on ground state gradVrep, gradE0, gradExc = self.grads.gradient( I=0, save_intermediates_CPKS=1) gradTot = gradVrep + gradE0 + gradExc # QM/MM if self.tddftb.dftb2.qmmm != None: # expand gradient to the full system gradTot = self.tddftb.dftb2.qmmm.getGradientFull(gradTot) # confining cavity if self.tddftb.dftb2.cavity != None: if self.tddftb.dftb2.qmmm != None: atomlist = self.tddftb.dftb2.qmmm.getGeometryFull() else: atomlist = self.atomlist gradTot += self.tddftb.dftb2.cavity.getGradient(atomlist) return E0, gradTot def resetXY(self): # reset excitation vectors, so that in the next iteration # we start with a clean guess self.tddftb.XmY = None self.tddftb.XpY = None def resetSCF(self): # set Mulliken partial charges to zero and density matrix P to # reference density matrix P0, so that in the next iteration # we start with a clean guess self.tddftb.dftb2.dq *= 0.0 self.tddftb.dftb2.P = self.tddftb.dftb2.density_matrix_ref() def getScalarCouplings(self, threshold=0.01): """ Parameters: =========== threshold: smaller excitation coefficients are neglected in the calculation of overlaps Returns: ======== coupl: antisymmetric matrix with scalar non-adiabatic coupling coupl[A,B] ~ <Psi_A|d/dt Psi_B>*dt The coupling matrix has to be divided by the nuclear time step dt It is important that this function is called exactly once after calling 'getEnergiesAndGradient' """ atomlist2 = self.atomlist orbs2 = self.tddftb.dftb2.orbs C2 = self.tddftb.Cij[:self.Nst - 1, :, :] # only save the states of interest # off-diagonal elements of electronic hamiltonian SC = ScalarCoupling(self.tddftb) # retrieve last calculation if self.last_calculation == None: # This is the first calculation, so use the same data atomlist1, orbs1, C1 = atomlist2, orbs2, C2 else: atomlist1, orbs1, C1 = self.last_calculation Sci = SC.ci_overlap(atomlist1, orbs1, C1, atomlist2, orbs2, C2, self.Nst, threshold=threshold) # align phases # The eigenvalue solver produces vectors with arbitrary global phases # (+1 or -1). The orbitals of the ground state can also change their signs. # Eigen states from neighbouring geometries should change continuously. signs = np.sign(np.diag(Sci)) self.P = np.diag(signs) # align C2 with C1 C2 = np.tensordot(self.P[1:, :][:, 1:], C2, axes=(0, 0)) # align overlap matrix Sci = np.dot(Sci, self.P) # The relative signs for the overlap between the ground and excited states at different geometries # cannot be deduced from the diagonal elements of Sci. The phases are chosen such that the coupling # between S0 and S1-SN changes smoothly for most of the states. if hasattr(self, "last_coupl"): s = np.sign(self.last_coupl[0, 1:] / Sci[0, 1:]) w = np.abs(Sci[0, 1:] - self.last_coupl[0, 1:]) mean_sign = np.sign(np.sum(w * s) / np.sum(w)) sign0I = mean_sign for I in range(1, self.Nst): Sci[0, I] *= sign0I Sci[I, 0] *= sign0I # state_labels = [("S%d" % I) for I in range(0, self.Nst)] if self.tddftb.dftb2.verbose > 0: print "Overlap <Psi(t)|Psi(t+dt)>" print "==========================" print utils.annotated_matrix(Sci, state_labels, state_labels) # overlap between wavefunctions at different time steps olap = np.copy(Sci) coupl = Sci # coupl[A,B] = <Psi_A(t)|Psi_B(t+dt)> - delta_AB # ~ <Psi_A(t)|d/dR Psi_B(t)>*dR/dt dt # TEST # The scalar coupling matrix should be more or less anti-symmetric # provided the time-step is small enough # set diagonal elements of coupl to zero coupl[np.diag_indices_from(coupl)] = 0.0 err = np.sum(abs(coupl + coupl.transpose())) if err > 1.0e-1: print "WARNING: Scalar coupling matrix is not antisymmetric, error = %s" % err # # Because of the finite time-step it will not be completely antisymmetric, # so antisymmetrize it coupl = 0.5 * (coupl - coupl.transpose()) # save coupling for establishing relative phases self.last_coupl = coupl state_labels = [("S%d" % I) for I in range(0, self.Nst)] if self.tddftb.dftb2.verbose > 0: print "Scalar Coupling" print "===============" print utils.annotated_matrix(coupl, state_labels, state_labels) # save last calculation self.last_calculation = (atomlist2, orbs2, C2) return coupl, olap def saveLastCalculation(self): """ save geometry, MO coefficients and TD-DFTB coefficients of last calculation. This data is needed to align the phases between neighbouring steps. """ atomlist = self.atomlist orbs = self.tddftb.dftb2.orbs # only save the states of interest C = self.tddftb.Cij[:self.Nst - 1, :, :] # save last calculation self.last_calculation = (atomlist, orbs, C) def alignCoefficients(self, c): """ align phases of coefficients of adiabatic states """ if hasattr(self, "P"): c = np.dot(self.P[1:, :][:, 1:], c) return c def getTransitionDipoles(self): """ computes and aligns transition dipoles between all Nst states This function should be called only once directly after getScalarCouplings! """ tdip = self.tddftb.getTransitionDipolesExc(self.Nst) # eigenvectors can have arbitrary phases # align transition dipoles with old ones tdip_old = self.tdip_old if type(tdip_old) == type(None): tdip_old = tdip for A in range(0, self.Nst): for B in range(0, self.Nst): sign = np.dot(tdip_old[A, B, :], tdip[A, B, :]) if sign < 0.0: tdip[A, B, :] *= -1.0 self.tdip_old = tdip if self.tddftb.dftb2.verbose > 0: state_labels = [("S%d" % I) for I in range(0, self.Nst)] print "Transition Dipoles" print "==================" print "X-Component" print utils.annotated_matrix(tdip[:, :, 0], state_labels, state_labels) print "Y-Component" print utils.annotated_matrix(tdip[:, :, 1], state_labels, state_labels) print "Z-Component" print utils.annotated_matrix(tdip[:, :, 2], state_labels, state_labels) # return tdip
def calculate_charge_derivative(): """compute analytical gradients of Mulliken charges""" # # This long prologue serves for reading all options # from the command line or the dftbaby.cfg file. # The command-line option # # --cpks_solver='direct' or 'iterative' # # determines whether the full CPKS matrix should be constructed ('direct') # or whether the system of linear equations should be solved iteratively using the Krylov # subspace method. # from DFTB.XYZ import read_xyz, extract_keywords_xyz from DFTB.DFTB2 import DFTB2 from DFTB import utils from DFTB.LR_TDDFTB import LR_TDDFTB from DFTB.ExcGradients import Gradients import sys import os usage = "Usage: %s <xyz-file>\n" % sys.argv[0] usage += " --help option will give more information\n" parser = utils.OptionParserFuncWrapper([\ DFTB2.__init__, DFTB2.runSCC, \ LR_TDDFTB.getEnergies, LR_TDDFTB.saveAbsorptionSpectrum, LR_TDDFTB.analyseParticleHole, \ Gradients.getGradients], \ usage) (options, args) = parser.parse_args(DFTB2.__init__) if len(args) < 1: print usage exit(-1) xyz_file = args[0] atomlist = read_xyz(xyz_file)[0] # number of atoms Nat = len(atomlist) kwds = extract_keywords_xyz(xyz_file) tddftb = LR_TDDFTB(atomlist, **options) (options, args) = parser.parse_args(tddftb.getEnergies) (scf_options, args) = parser.parse_args(tddftb.dftb2.runSCC) options.update(scf_options) tddftb.setGeometry(atomlist, charge=kwds.get("charge", 0.0)) tddftb.getEnergies(**options) # # The important part starts here. # grad = Gradients(tddftb) # A CPKS calculation has to be preceeded by a gradient calculation. # The option `save_intermediates_CPKS=1` tells the program to save intermediate # variables that are needed later during the CPKS calculation. grad.gradient(I=0, save_intermediates_CPKS=1) # This runs a CPKS calculation and computes the gradients of the charges dQdp = grad.getChargeGradients() # The gradient of the charge on atom B w/r/t the position of atom A is # d(Q_B)/dR_A = dQdp[3*A:3*(A+1), B] A = 0 # first atom B = Nat - 1 # last atom print "Gradient of Mulliken charge on atom A=%d w/r/t nucleus B=%d d(Q_B)/dR_A = %s" \ % (A, B, dQdp[3*A:3*(A+1), B])