Source code for n2v.methods.oucarter

"""
oucarter.py

Functions associated with Ou-Carter inversion
"""

import numpy as np
from opt_einsum import contract
try:
    import psi4
except:
    pass

[docs]class OC(): """ Ou-Carter density to potential inversion [1]. [1] [J. Chem. Theory Comput. 2018, 14, 5680−5689] """ def _get_l_kinetic_energy_density_directly(self, D, C, grid_info=None): """ Calculate $\frac{\tau_L^{KS}}{\rho^{KS}}-\frac{\tau_P^{KS}}{\rho^{KS}}$: laplace_rho_temp: $\frac{\nabla^2 \rho}{4}$; tauW_temp: $\frac{|\napla \rho|^2}{8|\rho|}$; tauLmP_rho: $\frac{|\napla \rho|^2}{8|\rho|^2} - \frac{\nabla^2 \rho}{4\rho}$. (i.e. the 2dn and 3rd term in eqn. (17) in [1] over $\rho$.): """ if grid_info is None: tauLmP_rho = np.zeros(self.Vpot.grid().npoints()) nblocks = self.Vpot.nblocks() points_func = self.Vpot.properties()[0] blocks = None else: blocks, npoints, points_func = grid_info tauLmP_rho = np.zeros(npoints) nblocks = len(blocks) points_func.set_deriv(2) iw = 0 for l_block in range(nblocks): # Obtain general grid information if blocks is None: l_grid = self.Vpot.get_block(l_block) else: l_grid = blocks[l_block] l_npoints = l_grid.npoints() points_func.compute_points(l_grid) l_lpos = np.array(l_grid.functions_local_to_global()) if len(l_lpos) == 0: iw += l_npoints continue l_phi = np.array(points_func.basis_values()["PHI"])[:l_npoints, :l_lpos.shape[0]] l_phi_x = np.array(points_func.basis_values()["PHI_X"])[:l_npoints, :l_lpos.shape[0]] l_phi_y = np.array(points_func.basis_values()["PHI_Y"])[:l_npoints, :l_lpos.shape[0]] l_phi_z = np.array(points_func.basis_values()["PHI_Z"])[:l_npoints, :l_lpos.shape[0]] l_phi_xx = np.array(points_func.basis_values()["PHI_XX"])[:l_npoints, :l_lpos.shape[0]] l_phi_yy = np.array(points_func.basis_values()["PHI_YY"])[:l_npoints, :l_lpos.shape[0]] l_phi_zz = np.array(points_func.basis_values()["PHI_ZZ"])[:l_npoints, :l_lpos.shape[0]] lD = D[(l_lpos[:, None], l_lpos)] # lC = C[l_lpos, :] rho = contract('pm,mn,pn->p', l_phi, lD, l_phi) rho_inv = 1/rho # Calculate the second term laplace_rho_temp = contract('ab,pa,pb->p', lD, l_phi, l_phi_xx + l_phi_yy + l_phi_zz) # laplace_rho_temp += contract('pm, mn, pn->p', l_phi_x,lD, l_phi_x) # laplace_rho_temp += contract('pm, mn, pn->p', l_phi_y,lD, l_phi_y) # laplace_rho_temp += contract('pm, mn, pn->p', l_phi_z,lD, l_phi_z) laplace_rho_temp += np.sum((l_phi_x @ lD) * l_phi_x, axis=1) laplace_rho_temp += np.sum((l_phi_y @ lD) * l_phi_y, axis=1) laplace_rho_temp += np.sum((l_phi_z @ lD) * l_phi_z, axis=1) laplace_rho_temp *= 0.25 * 2 # Calculate the third term tauW_temp = contract('pm, mn, pn->p', l_phi, lD, l_phi_x) ** 2 tauW_temp += contract('pm, mn, pn->p', l_phi, lD, l_phi_y) ** 2 tauW_temp += contract('pm, mn, pn->p', l_phi, lD, l_phi_z) ** 2 tauW_temp *= rho_inv * 0.125 * 4 tauLmP_rho[iw: iw + l_npoints] = (-laplace_rho_temp + tauW_temp) * rho_inv iw += l_npoints assert iw == tauLmP_rho.shape[0], "Somehow the whole space is not fully integrated." return tauLmP_rho def _get_optimized_external_potential(self, grid_info, average_alpha_beta=False): """ $ v^{~}{ext}(r) = \epsilon^{-LDA}(r) - \frac{\tau^{LDA}{L}}{n^{LDA}(r)} - v_{H}^{LDA}(r) - v_{xc}^{LDA}(r) $ (22) in [1]. """ Nalpha = self.nalpha Nbeta = self.nbeta # SVWN calculation wfn_LDA = psi4.energy("SVWN/" + self.basis_str, molecule=self.mol, return_wfn=True)[1] Da_LDA = wfn_LDA.Da().np Db_LDA = wfn_LDA.Db().np Ca_LDA = wfn_LDA.Ca().np Cb_LDA = wfn_LDA.Cb().np epsilon_a_LDA = wfn_LDA.epsilon_a().np epsilon_b_LDA = wfn_LDA.epsilon_b().np self.Vpot = wfn_LDA.V_potential() vxc_LDA_DFT = self.on_grid_vxc(Da=Da_LDA, Db=Db_LDA, Vpot=self.Vpot) vxc_LDA = self.on_grid_vxc(Da=Da_LDA, Db=Db_LDA, grid=grid_info) if self.ref != 1: assert vxc_LDA.shape[-1] == 2 vxc_LDA_beta = vxc_LDA[:,1] vxc_LDA = vxc_LDA[:, 0] vxc_LDA_DFT_beta = vxc_LDA_DFT[:, 1] vxc_LDA_DFT = vxc_LDA_DFT[:, 0] # _average_local_orbital_energy() taken from mrks.py. e_bar_DFT = self._average_local_orbital_energy(Da_LDA, Ca_LDA[:,:Nalpha], epsilon_a_LDA[:Nalpha]) e_bar = self._average_local_orbital_energy(Da_LDA, Ca_LDA[:, :Nalpha], epsilon_a_LDA[:Nalpha], grid_info=grid_info) tauLmP_rho_DFT = self._get_l_kinetic_energy_density_directly(Da_LDA, Ca_LDA[:,:Nalpha]) tauLmP_rho = self._get_l_kinetic_energy_density_directly(Da_LDA, Ca_LDA[:,:Nalpha], grid_info=grid_info) tauP_rho_DFT = self._pauli_kinetic_energy_density(Da_LDA, Ca_LDA[:,:Nalpha]) tauP_rho = self._pauli_kinetic_energy_density(Da_LDA, Ca_LDA[:,:Nalpha], grid_info=grid_info) tauL_rho_DFT = tauLmP_rho_DFT + tauP_rho_DFT tauL_rho = tauLmP_rho + tauP_rho vext_opt_no_H_DFT = e_bar_DFT - tauL_rho_DFT - vxc_LDA_DFT vext_opt_no_H = e_bar - tauL_rho - vxc_LDA J = self.form_jk(Ca_LDA[:,:Nalpha], Cb_LDA[:,:Nbeta])[0] vext_opt_no_H_DFT_Fock = self.dft_grid_to_fock(vext_opt_no_H_DFT, self.Vpot) vext_opt_DFT_Fock = vext_opt_no_H_DFT_Fock - J[0] - J[1] # # Does vext_opt need a shift? # Fock_LDA = self.T + vext_opt_DFT_Fock + J[0] + J[1] + self.dft_grid_to_fock(vxc_LDA_DFT, self.Vpot) # eigvecs_a = self.diagonalize(Fock_LDA, self.nalpha)[-1] # shift = eigvecs_a[Nalpha-1] - epsilon_a_LDA[Nalpha-1] # vext_opt_DFT_Fock -= shift * self.S2 # print("LDA shift:", shift, eigvecs_a[Nalpha-1], epsilon_a_LDA[Nalpha-1]) vH = self.on_grid_esp(grid=grid_info, wfn=wfn_LDA)[1] vext_opt = vext_opt_no_H - vH # vext_opt -= shift if self.ref != 1: e_bar_DFT_beta = self._average_local_orbital_energy(Db_LDA, Cb_LDA[:,:Nbeta], epsilon_b_LDA[:Nbeta]) e_bar_beta = self._average_local_orbital_energy(Db_LDA, Cb_LDA[:, :Nbeta], epsilon_b_LDA[:Nbeta], grid_info=grid_info) tauLmP_rho_DFT_beta = self._get_l_kinetic_energy_density_directly(Db_LDA, Cb_LDA[:,:Nbeta]) tauLmP_rho_beta = self._get_l_kinetic_energy_density_directly(Db_LDA, Cb_LDA[:,:Nalpha], grid_info=grid_info) tauP_rho_DFT_beta = self._pauli_kinetic_energy_density(Db_LDA, Cb_LDA[:,:Nbeta]) tauP_rho_beta = self._pauli_kinetic_energy_density(Db_LDA, Cb_LDA[:,:Nbeta], grid_info=grid_info) tauL_rho_DFT_beta = tauLmP_rho_DFT_beta + tauP_rho_DFT_beta tauL_rho_beta = tauLmP_rho_beta + tauP_rho_beta vext_opt_no_H_DFT_beta = e_bar_DFT_beta - tauL_rho_DFT_beta - vxc_LDA_DFT_beta vext_opt_no_H_beta = e_bar_beta - tauL_rho_beta - vxc_LDA_beta vext_opt_no_H_DFT_Fock_beta = self.dft_grid_to_fock(vext_opt_no_H_DFT_beta, self.Vpot) vext_opt_DFT_Fock_beta = vext_opt_no_H_DFT_Fock_beta - J[0] - J[1] vext_opt_beta = vext_opt_no_H_beta - vH # vext_opt_DFT_Fock = (vext_opt_DFT_Fock + vext_opt_DFT_Fock_beta) * 0.5 # vext_opt = (vext_opt + vext_opt_beta) * 0.5 return (vext_opt_DFT_Fock, vext_opt_DFT_Fock_beta), (vext_opt, vext_opt_beta) return vext_opt_DFT_Fock, vext_opt
[docs] def oucarter(self, maxiter, vxc_grid, D_tol=1e-7, eig_tol=1e-4, frac_old=0.5, init="scan"): """ (23) in [1]. [1] [J. Chem. Theory Comput. 2018, 14, 5680−5689] parameters: ---------------------- maxiter: int same as opt_max_iter vxc_grid: np.ndarray of shape (3, num_grid_points) The final result will be represented on this grid default: 1e-4 D_tol: float, opt convergence criteria for density matrices. default: 1e-7 eig_tol: float, opt convergence criteria for occupied eigenvalue spectrum. default: 1e-4 frac_old: float, opt Linear mixing parameter for current vxc and old vxc. If 0, no old vxc is mixed in. Should be in [0,1) default: 0.5. init: string, opt Initial guess method. default: "SCAN" 1) If None, input wfn info will be used as initial guess. 2) If "continue" is given, then it will not initialize but use the densities and orbitals stored. Meaningly, one can run a quick WY calculation as the initial guess. This can also be used to user speficified initial guess by setting Da, Coca, eigvec_a. 3) If it's not continue, it would be expecting a method name string that works for psi4. A separate psi4 calculation would be performed. """ # if self.ref != 1: # raise ValueError("Currently only supports Spin-Restricted " # "calculations since Spin-Unrestricted CI " # "is not supported by Psi4.") grid_info = self.grid_to_blocks(vxc_grid) if self.ref == 1: grid_info[-1].set_pointers(self.wfn.Da()) else: grid_info[-1].set_pointers(self.wfn.Da(), self.wfn.Db()) if self.ref == 1: vext_opt_Fock, vext_opt = self._get_optimized_external_potential(grid_info) else: (vext_opt_Fock, vext_opt_Fock_beta), (vext_opt, vext_opt_beta) = self._get_optimized_external_potential(grid_info) assert self.Vpot is not None vH0_Fock = self.va Nalpha = self.nalpha Nbeta = self.nbeta # Initialization. if init is None: self.Da = np.copy(self.Dt[0]) self.Coca = np.copy(self.ct[0]) self.eigvecs_a = self.wfn.epsilon_a().np[:Nalpha] self.Db = np.copy(self.Dt[1]) self.Cocb = np.copy(self.ct[1]) self.eigvecs_b = self.wfn.epsilon_b().np[:Nbeta] elif init.lower()=="continue": pass else: wfn_temp = psi4.energy(init+"/" + self.basis_str, molecule=self.mol, return_wfn=True)[1] self.Da = np.array(wfn_temp.Da()) self.Coca = np.array(wfn_temp.Ca())[:, :Nalpha] self.eigvecs_a = np.array(wfn_temp.epsilon_a()) self.Db = np.array(wfn_temp.Db()) self.Cocb = np.array(wfn_temp.Cb())[:, :Nbeta] self.eigvecs_b = np.array(wfn_temp.epsilon_b()) del wfn_temp # nerror = self.on_grid_density(Da=self.Dt[0]-self.Da, Db=self.Dt[1]-self.Da, Vpot=self.Vpot) # w = self.Vpot.get_np_xyzw()[-1] # nerror = np.sum(np.abs(nerror.T) * w) # print("nerror", nerror) vxc_old = 0.0 vxc_old_beta = 0.0 Da_old = 0.0 eig_old = 0.0 tauLmP_rho = self._get_l_kinetic_energy_density_directly(self.Dt[0], self.ct[0][:, :Nalpha]) if self.ref != 1: tauLmP_rho_beta = self._get_l_kinetic_energy_density_directly(self.Dt[1], self.ct[1][:, :Nbeta]) for OC_step in range(1, maxiter+1): tauP_rho = self._pauli_kinetic_energy_density(self.Da, self.Coca) # _average_local_orbital_energy() taken from mrks.py. e_bar = self._average_local_orbital_energy(self.Da, self.Coca, self.eigvecs_a[:Nalpha]) shift = self.eigvecs_a[Nalpha - 1] - self.wfn.epsilon_a().np[Nalpha - 1] # vxc + vext_opt + vH0 vxc_extH = e_bar - tauLmP_rho - tauP_rho - shift if self.ref != 1: tauP_rho_beta = self._pauli_kinetic_energy_density(self.Db, self.Cocb) e_bar_beta = self._average_local_orbital_energy(self.Db, self.Cocb, self.eigvecs_b[:Nbeta]) shift_beta = self.eigvecs_b[Nbeta - 1] - self.wfn.epsilon_b().np[Nbeta - 1] # vxc + vext_opt + vH0 vxc_extH_beta = e_bar_beta - tauLmP_rho_beta - tauP_rho_beta - shift_beta Derror = np.linalg.norm(self.Da - Da_old) / self.nbf ** 2 eerror = (np.linalg.norm(self.eigvecs_a[:Nalpha] - eig_old) / Nalpha) if (Derror < D_tol) and (eerror < eig_tol): print("KSDFT stops updating.") break # linear Mixture if OC_step != 1: vxc_extH = vxc_extH * (1 - frac_old) + vxc_old * frac_old if self.ref != 1: vxc_extH_beta = vxc_extH_beta * (1 - frac_old) + vxc_old_beta * frac_old vxc_old = np.copy(vxc_extH) if self.ref != 1: vxc_old_beta = np.copy(vxc_extH_beta) # Save old data. Da_old = np.copy(self.Da) eig_old = np.copy(self.eigvecs_a[:Nalpha]) Vxc_extH_Fock = self.dft_grid_to_fock(vxc_extH, self.Vpot) Vxc_Fock = Vxc_extH_Fock - vext_opt_Fock - vH0_Fock if self.ref != 1: Vxc_extH_Fock_beta = self.dft_grid_to_fock(vxc_extH_beta, self.Vpot) Vxc_Fock_beta = Vxc_extH_Fock_beta - vext_opt_Fock_beta - vH0_Fock if self.ref == 1: self._diagonalize_with_potential_vFock(v=Vxc_Fock) else: self._diagonalize_with_potential_vFock(v=(Vxc_Fock, Vxc_Fock_beta)) print(f"Iter: {OC_step}, Density Change: {Derror:2.2e}, Eigenvalue Change: {eerror:2.2e}.") # nerror = self.on_grid_density(Da=self.Dt[0] - self.Da, Db=self.Dt[1] - self.Da, Vpot=self.Vpot) # nerror = np.sum(np.abs(nerror.T) * w) # print("nerror", nerror) # Calculate vxc on grid vH0 = self.on_grid_esp(grid=grid_info)[1] tauLmP_rho = self._get_l_kinetic_energy_density_directly(self.Dt[0], self.ct[0][:, :Nalpha], grid_info=grid_info) tauP_rho = self._pauli_kinetic_energy_density(self.Da, self.Coca, grid_info=grid_info) shift = self.eigvecs_a[Nalpha - 1] - self.wfn.epsilon_a().np[Nalpha - 1] e_bar = self._average_local_orbital_energy(self.Da, self.Coca, self.eigvecs_a[:Nalpha], grid_info=grid_info) if self.ref != 1: tauLmP_rho_beta = self._get_l_kinetic_energy_density_directly(self.Dt[1], self.ct[1][:, :Nbeta], grid_info=grid_info) tauP_rho_beta = self._pauli_kinetic_energy_density(self.Db, self.Cocb, grid_info=grid_info) shift_beta = self.eigvecs_b[Nbeta - 1] - self.wfn.epsilon_b().np[Nbeta - 1] e_bar_beta = self._average_local_orbital_energy(self.Db, self.Cocb, self.eigvecs_b[:Nbeta], grid_info=grid_info) if self.ref == 1: self.grid.vxc = e_bar - tauLmP_rho - tauP_rho - vext_opt - vH0 - shift return self.grid.vxc, e_bar, tauLmP_rho, tauP_rho, vext_opt, vH0, shift else: self.grid.vxc = np.array((e_bar - tauLmP_rho - tauP_rho - vext_opt - vH0 - shift, e_bar_beta - tauLmP_rho_beta - tauP_rho_beta - vext_opt_beta - vH0 - shift_beta )) return self.grid.vxc, (e_bar, e_bar_beta), (tauLmP_rho, tauLmP_rho_beta), \ (tauP_rho,tauP_rho_beta), (vext_opt, vext_opt_beta), vH0, (shift, shift_beta)