def normalize(self, atomlist): """ compute and set the normalization constant so that the orbital is normalized to 1. Parameters ---------- atomlist : molecule geometry for defining Becke's grid for numerical integration numerical Returns ------- norm : norm of wavefunction <wfn|wfn>^{1/2} before normalization """ # norm^2 of wavefunction <wfn|wfn> nrm2 = integral(atomlist, lambda x, y, z: abs(self.__call__(x, y, z))**2) nrm = np.sqrt(nrm2) # update normalization constant self.norm *= nrm return nrm
def improve_continuum_orbital(atomlist, phi0, potential_ee, E, max_iter=100, thresh=1.0e-6): """ improves an initial guess phi0 for a continuum orbital with energy E by repeatedly computing orbital corrections phi_{i+1} = a_i * phi_{i} + b_i * dphi_{i} The orbital correction delta_phi is obtained as the solution of the inhomogeneous Schroedinger equation (H-E) dphi = -(H-E) phi i i The Schroedinger equation is solved approximately by replacing the effective potential with a spherical average around each atom. The best linear combination of phi and the correction is chosen. The coefficients a_i and b_i minimize the expectation value of the residual 2 R = (H-E) 2 a ,b = argmin <phi |(H-E) |phi > i i i+1 i+1 Parameters ========== atomlist : list of tuples (Zat,[x,y,z]) with atomic numbers and positions phi0 : callable, phi0(x,y,z) evaluates the initial orbital potential_ee : callable, potential_ee(x,y,z) evaluates the effective molecular Kohn-Sham potential WITHOUT the nuclear attraction, Vee = Vcoul + Vxc E : float, energy of continuum orbital Optional ======== max_iter : int, maximum number of orbital correction thresh : float, the iteration is stopped as soon as the weight of the orbital correction falls below this threshold: 2 b_i <= thresh Returns ======= phi : callable, phi(x,y,z) evaluates the improved orbital """ # attraction potential between nuclei and electrons nuclear_potential = nuclear_potential_func(atomlist) # effective potential (electron-electron interaction + nuclei-electrons) def potential(x, y, z): return potential_ee(x, y, z) + nuclear_potential(x, y, z) print("orbital corrections...") phi = phi0 for i in range(0, max_iter): residual = residual_ee_func(atomlist, phi, potential_ee, E) def source(x, y, z): return -residual(x, y, z) # orbital correction delta_phi = inhomogeneous_schroedinger(atomlist, potential, source, E) a, b = variational_mixture_continuum(atomlist, phi, delta_phi, potential_ee, E) # The error is estimated as the norm^2 of the orbital correction: # error = <delta_phi|delta_phi> # For large radii, we cannot expect the solution to be correct since # the density of points is far too low. Therefore we exclude all points # outside the radius `rlarge` from the integration. rlarge = 10.0 def error_density(x, y, z): err = abs(delta_phi(x, y, z))**2 r = np.sqrt(x**2 + y**2 + z**2) err[rlarge < r] = 0.0 return err error = integral(atomlist, error_density) print(" iteration i= %d |dphi|^2= %e b^2= %e (threshold= %e )" % (i + 1, error, b**2, thresh)) # The solution is converged, when the weight of the # correction term b**2 is small enough. if b**2 < thresh: print("CONVERGED") break # next approximation for phi phi_next = add_two_functions(atomlist, phi, delta_phi, a, b) ### DEBUG import matplotlib.pyplot as plt plt.clf() # plot cuts along z-axis r = np.linspace(-40.0, 40.0, 10000) x = 0.0 * r y = 0.0 * r z = r plt.title("Iteration i=%d" % (i + 1)) plt.xlabel("z / bohr") plt.ylim((-0.6, +0.6)) plt.plot(r, phi(x, y, z), label=r"$\phi$") plt.plot(r, delta_phi(x, y, z), ls="--", label=r"$\Delta \phi$") plt.plot(r, phi_next(x, y, z), label=r"$a \phi + b \Delta \phi$") plt.plot(r, residual(x, y, z), ls="-.", label="residual $(H-E)\phi$") plt.legend() plt.savefig("/tmp/iteration_%3.3d.png" % (i + 1)) # plt.show() ### # prepare for next iteration phi = phi_next else: msg = "Orbital corrections did not converge in '%s' iterations!" % max_iter print("WARNING: %s" % msg) #raise RuntimeError(msg) return phi
def variational_mixture_continuum(atomlist, phi, delta_phi, potential_ee, E, rsmall=0.0, rlarge=10.0): """ Since the orbital correction is only approximate due to the spherical averaging, for the improved wavefunction we make the ansatz psi(r) = a phi(r) + b delta_phi(r) where the coefficients a and b are optimized variationally. a,b are the coefficients belonging to the smallest in magnitude eigenvalue of the generalized eigenvalue equation R v = S v or ( Raa Rab ) (a) ( Saa Sab ) ( a ) ( ) ( ) = E ( ) ( ) ( Rba Rbb ) (b) ( Sba Sbb ) ( b ) where Raa = <phi|(H-E)^2|phi>, Rab = <phi|(H-E)^2|delta_phi>, etc. are the matrix elements of the residual operator (H-E)^2 and Sab is the overlap between the two wavefunctions integrated over one one wavelength at a large distance. Parameters ========== atomlist : list of tuples (Zat,[x,y,z]) with atomic numbers and positions phi : callable, phi(x,y,z) evaluates the initial orbital delta_phi : callable, delta_phi(x,y,z) evaluates the orbital correction potential_ee : callable, potential_ee(x,y,z) evaluates the effective molecular Kohn-Sham potential WITHOUT the nuclear attraction, Vee = Vcoul + Vxc E : float, energy of continuum orbital, E=1/2 k^2 Optional ======== rsmall : regions closer than `rsmall` to any nucleus are excluded from the integration rlarge : regions further away from the origin than `rlarge` are also excluded, the overlap matrix is compute on the interval [rlarge, rlarge+2*pi/k] Returns ======= a,b : floats, mixing coefficients """ # (H-E)phi residual_a = residual_ee_func(atomlist, phi, potential_ee, E) # (H-E)delta_phi residual_b = residual_ee_func(atomlist, delta_phi, potential_ee, E) residuals = [residual_a, residual_b] n = len(residuals) R = np.zeros((n, n)) for a in range(0, n): for b in range(0, n): def integrand(x, y, z): integ = residuals[a](x, y, z) * residuals[b](x, y, z) # Around the nuclei, it is very difficult to accuratly # calculate the residual (H-E)phi, because both the nuclear attraction (negative) # and the kinetic energy (positive) are very large, but cancel mostly, # so that we get very large numerical errors. To avoid this, we excise a circular # region of radius rsmall around each nucleus and set the integrand # to zero in this region, so as to exclude regions with large errors from # the calculation. for i, (Zi, posi) in enumerate(atomlist): # distance of point (x,y,z) from nucleus i xi, yi, zi = posi ri = np.sqrt((x - xi)**2 + (y - yi)**2 + (z - zi)**2) integ[ri < rsmall] = 0.0 # Since density of the grid decreases very rapidly far away from the # molecule, we cannot expect the residual to be accurate at large # distances. Therefore we exclude all points outside the radius `rlarge` # from the integraion r = np.sqrt(x**2 + y**2 + z**2) integ[rlarge < r] = 0.0 return integ # compute the integrals R_ab = <a|(H-E)(H-E)|b> where |a> and |b> # are phi or delta_phi print("integration R[%d,%d]" % (a, b)) R[a, b] = integral(atomlist, integrand) print("matrix elements of R = (H-E)^2") print(R) # S = continuum_overlap([phi, delta_phi], E, rlarge=rlarge) print("continuum overlap matrix") print(S) # solve generalized eigenvalue problem eigvals, eigvecs = sla.eigh(R, S) print("eigenvalues") print(eigvals) print("eigenvectors") print(eigvecs) # select eigenvector with smallest (in magnitude) eigenvalue imin = np.argmin(abs(eigvals)) a, b = eigvecs[:, imin] # Eigenvectors come out with arbitrary signs, # the phase of phi should always be positive. if a < 0.0: a *= -1.0 b *= -1.0 return a, b
def residual2_matrix(atomlist, potential, ps, bs): """ compute matrix elements of (H-E)^2 2 <u |(H-E) |u > = <u |(V-V )(V-V )|u > i j i i j j Parameters ---------- atomlist : list of tuples (Zat,[x,y,z]) with atomic numbers and positions potential : callable, potential(x,y,z) evaluates the effective molecular Kohn-Sham potential V ps : instance of `AtomicPotentialSet` with the effective atomic Kohn-Sham potentials V_i bs : instance of `AtomicScatteringBasisSet` with the atomic continuum orbitals |ui> (all for the same energy) """ # number of basis functions nb = len(bs.bfs) R = np.zeros((nb, nb)) for i in range(0, nb): # i-th basis function ui = bs.bfs[i] # effective potential of the atom to which the basis function belongs I = ui.atom_index poti = ps.pots[I] for j in range(i, nb): print("computing R[%d,%d] = " % (i, j), end=' ') uj = bs.bfs[j] J = uj.atom_index potj = ps.pots[J] ### DEBUG debug = 0 if debug > 0: import matplotlib.pyplot as plt r = np.linspace(-5.0, 5.0, 100000) x = 0.0 * r y = 0.0 * r z = r plt.plot(r, potential(x, y, z), label=r"$V$") plt.plot(r, poti(x, y, z), label=r"$V_{%d}$" % (I + 1)) plt.plot(r, potj(x, y, z), ls="--", label=r"$V_{%d}$" % (J + 1)) plt.plot(r, (potential(x, y, z) - poti(x, y, z)) * (potential(x, y, z) - potj(x, y, z)), ls="-.", label=r"$(V-V_{%d})(V-V_{%d})$" % (I + 1, J + 1)) VmVi = potential(x, y, z) - poti(x, y, z) xi, yi, zi = poti.center ri = np.sqrt((x - xi)**2 + (y - yi)**2 + (z - zi)**2) VmVi[ri < 2 * poti.rmin] = 0.0 VmVj = potential(x, y, z) - potj(x, y, z) xj, yj, zj = potj.center rj = np.sqrt((x - xj)**2 + (y - yj)**2 + (z - zj)**2) VmVi[rj < 2 * potj.rmin] = 0.0 plt.plot(r, VmVi * VmVj, ls="--", label=r"$(V-V_{%d})(V-V_{%d})$ (outer region only)" % (I + 1, J + 1)) plt.ylim((-100.0, +100.0)) plt.legend() plt.show() ### def integrand(x, y, z): V = potential(x, y, z) Vi = poti(x, y, z) Vj = potj(x, y, z) # Very close to each nucleus, the molecular Kohn-Sham potential # should be dominated by the nuclear attraction potential -Z/r. # In this region the atomic and molecular Kohn-Sham potentials should # cancel exactly, V-Vi = 0. However, subtracting two large numbers that # tend to infinity, will never give exactly 0 due to numerical errors. # Therefore a circle of radius 2*rmin is excised around atom i, where # V-Vi is explicitly set to zero. VmVi = V - Vi xi, yi, zi = poti.center ri = np.sqrt((x - xi)**2 + (y - yi)**2 + (z - zi)**2) VmVi[ri < 2 * poti.rmin] = 0.0 VmVj = V - Vj xj, yj, zj = potj.center rj = np.sqrt((x - xj)**2 + (y - yj)**2 + (z - zj)**2) VmVi[rj < 2 * potj.rmin] = 0.0 # ui (V-Vi) (V-Vj) uj return ui(x, y, z) * VmVi * VmVj * uj(x, y, z) # return ui(x,y,z)*(V-Vi)*(V-Vj)*uj(x,y,z) R[i, j] = integral(atomlist, integrand) R[j, i] = R[i, j] print(R[i, j]) return R
def fvvm_matrix_elements(atomlist, bfs, potential, E, r0): """ compute matrix elements of eqns. (8) and (9) in Ref. [1] which are needed for the finite-volume variational method. The integration is limited to a sphere of radius r0 around the origin. Parameters ========== atomlist : list of tuples (Z,[x,y,z]) with atomic numbers and positions bfs : list of callables, `n` real basis functions potential : callable, potential(x,y,z) evaluates effective potential E : float, energy of continuum orbital r0 : float, radius of sphere which defines the integration volume Returns ======= A : n x n matrix D : n x n matrix S : n x n matrix with overlap in finite volume """ # number of basis functions nbfs = len(bfs) print " %d basis functions" % nbfs # volume integral # __ __ # A = <\/chi |\/chi > + 2 <chi |(V-E)|chi > # ij i j V i j V A = np.zeros((nbfs, nbfs)) # # surface integral # D = <chi |chi > # ij i j surface of V D = np.zeros((nbfs, nbfs)) # # overlap inside volume # # S = <chi |chi > # ij i j V S = np.zeros((nbfs, nbfs)) # angular grid for surface integral Lmax, (th, ph, angular_weights) = select_angular_grid(settings.lebedev_order) # cartesian coordinates of Lebedev points on sphere of # radius r0 xS = r0 * np.sin(th) * np.cos(ph) yS = r0 * np.sin(th) * np.sin(ph) zS = r0 * np.cos(th) for i in range(0, nbfs): # basis function i chi_i = bfs[i] for j in range(i, nbfs): # basis function j print " integrals between basis functions i= %d and j= %d" % ( i + 1, j + 1) chi_j = bfs[j] # volume integral A_ij grad_prod_func = gradient_product(atomlist, chi_i, chi_j) def integrand(x, y, z): # (grad chi_i).(grad chi_j) integ = grad_prod_func(x, y, z) # 2 * chi_i * chi_j (V-E) integ += 2 * chi_i(x, y, z) * chi_j( x, y, z) * (potential(x, y, z) - E) # Outside the integration volume the integrand # is set to zero. r = np.sqrt(x * x + y * y + z * z) integ[r0 < r] = 0.0 return integ A[i, j] = integral(atomlist, integrand) # A is symmetric A[j, i] = A[i, j] # surface integral D_ij D[i, j] = 4.0 * np.pi * r0**2 * np.sum( angular_weights * chi_i(xS, yS, zS) * chi_j(xS, yS, zS)) # D is symmetric D[j, i] = D[i, j] # overlap integral S_ij inside the volume def integrand(x, y, z): integ = chi_i(x, y, z) * chi_j(x, y, z) # Outside the integration volume the integrand # is set to zero. r = np.sqrt(x * x + y * y + z * z) integ[r0 < r] = 0.0 return integ S[i, j] = integral(atomlist, integrand) # S is symmetric S[j, i] = S[i, j] return A, D, S
def hmi_variational_kohn(): # H2^+ # bond length in bohr R = 2.0 # positions of protons posH1 = (0.0, 0.0, -R/2.0) posH2 = (0.0, 0.0, +R/2.0) # center of charge center = (0.0, 0.0, 0.0) atomlist = [(1, posH1), (1, posH2)] charge = +2 # choose resolution of multicenter grids for continuum orbitals settings.radial_grid_factor = 10 # controls size of radial grid settings.lebedev_order = 21 # controls size of angular grid # energy of continuum orbital (in Hartree) E = 10.0 # angular momentum quantum numbers l,m = 0,0 rho = None xc = None print "effective potential..." V = effective_potential_func(atomlist, rho, xc, nelec=0) # basis functions u1 = regular_coulomb_func(E, +1, l, m, 0.0, center=posH1) eta1 = 0.0 u2 = regular_coulomb_func(E, +1, l, m, 0.0, center=posH2) eta2 = 0.0 # asymptotic solutions s0 = regular_coulomb_func(E, charge, l, m, 0.0, center=center) # c0 = irregular_coulomb_func(E, charge, l, m, 0.0, # center=center) c0 = regular_coulomb_func(E, charge, l, m, np.pi/2.0, center=center) # switching function # g(0) = 0 g(oo) = 1 def g(r): r0 = 3.0 # radius at which the switching happens gr = 0*r gr[r > 0] = 1.0/(1.0+np.exp(-(1-r0/r[r > 0]))) return gr def s(x,y,z): r = np.sqrt(x*x+y*y+z*z) return g(r) * s0(x,y,z) def c(x,y,z): r = np.sqrt(x*x+y*y+z*z) return g(r) * c0(x,y,z) free = [s,c] # basis potentials def V1(x,y,z): r2 = (x-posH1[0])**2 + (y-posH1[1])**2 + (z-posH1[2])**2 return -1.0/np.sqrt(r2) def V2(x,y,z): r2 = (x-posH2[0])**2 + (y-posH2[1])**2 + (z-posH2[2])**2 return -1.0/np.sqrt(r2) basis = [u1,u2] basis_potentials = [V1,V2] # number of basis functions nb = len(basis) # matrix elements between basis functions Lbb = np.zeros((nb,nb)) for i in range(0, nb): ui = basis[i] for j in range(0, nb): print "i= %d j= %d" % (i,j) uj = basis[j] Vj = basis_potentials[j] def integrand(x,y,z): return ui(x,y,z) * (V(x,y,z) - Vj(x,y,z)) * uj(x,y,z) Lbb[i,j] = integral(atomlist, integrand) print Lbb # matrix elements between unbound functions Lff = np.zeros((2,2)) for i in range(0, 2): fi = free[i] for j in range(0, 2): print "i= %d j= %d" % (i,j) fj = free[j] Lff[i,j] = kinetic(atomlist, fi,fj) + nuclear(atomlist, fi,fj) - E * overlap(atomlist, fi,fj) print Lff # matrix elements between bound and unbound functions Lbf = np.zeros((nb,2)) Lfb = np.zeros((2,nb)) for i in range(0, nb): ui = basis[i] for j in range(0, 2): fj = free[j] print "i= %d j= %d" % (i,j) Lbf[i,j] = kinetic(atomlist, ui,fj) + nuclear(atomlist, ui,fj) - E * overlap(atomlist, ui,fj) Lfb[j,i] = kinetic(atomlist, fj,ui) + nuclear(atomlist, fj,ui) - E * overlap(atomlist, fj,ui) print Lbf print Lfb # coeffs = -np.dot(la.inv(Lbb), Lbf) M = Lff - np.dot(Lfb, np.dot(la.inv(Lbb), Lbf)) print "M" print M # The equation # # M. (1) = 0 # (t) # cannot be fulfilled, usually. t0 = -M[0,0]/M[0,1] tans = np.array([np.tan(eta1), np.tan(eta2)]) t = (t0 + np.sum(coeffs[:,0]*tans) + t0*np.sum(coeffs[:,1]*tans)) \ / (1 + np.sum(coeffs[:,0]) + t0*np.sum(coeffs[:,1])) eta = np.arctan(t) # Because a global phase is irrelevant, the phase shift is only determined # modulo pi. sin(pi+eta) = -sin(eta) while eta < 0.0: eta += np.pi print "eta= %s" % eta bc0 = linear_combination(atomlist, basis, coeffs[:,0]) bc1 = linear_combination(atomlist, basis, coeffs[:,1]) #u = s + np.dot(basis, coeffs[:,0]) + t0 * (c + np.dot(basis, coeffs[:,1])) orbitals = [s, bc0, c, bc1] nrm = (1 + np.sum(coeffs[:,0]) + t0*np.sum(coeffs[:,1])) orb_coeffs = np.array([ 1.0, 1.0, t0, t0 ]) / nrm phi = linear_combination(atomlist, orbitals, orb_coeffs) residual = residual_func(atomlist, phi, V, E) import matplotlib.pyplot as plt r = np.linspace(-50.0, 50.0, 5000) x = 0.0*r y = 0.0*r z = r plt.plot(r, phi(x,y,z), label=r"$\phi$") plt.plot(r, residual(x,y,z), label=r"residual $(H-E)\phi$") plt.plot(r, V(x,y,z), label=r"potential") phi_lcao = linear_combination(atomlist, basis, [1.0/np.sqrt(2.0), 1.0/np.sqrt(2.0)]) residual_lcao = residual_func(atomlist, phi_lcao, V, E) plt.plot(r, phi_lcao(x,y,z), label=r"$\phi_{LCAO}$") plt.plot(r, residual_lcao(x,y,z), label=r"residual $(H-E)\phi_{LCAO}$") plt.legend() plt.show()