Source code for vayesta.core.qemb.self_energy

"""Routines to reconstruct the full system self-energy from cluster spectral moments"""

import numpy as np

from vayesta.core.util import NotCalculatedError, Object, dot, einsum
try:
    from dyson import Lehmann, MBLGF, MixedMBLGF, NullLogger, AuxiliaryShift
except ImportError as e:
    print(e)
    print("Dyson required for self-energy calculations")

[docs]def make_self_energy_moments(emb, n_se_mom, use_sym=True, proj=1, eta=1e-2): """ Construct full system self-energy moments from cluster spectral moments Parameters ---------- emb : EWF object Embedding object n_se_mom : int Number of self-energy moments use_sym : bool Use symmetry to reconstruct self-energy proj : int Number of projectors to use (1 or 2) eta : float Broadening factor for static potential Returns ------- self_energy_moms : ndarry (n_se_mom, nmo, nmo) Full system self-energy moments (MO basis) static_self_energy : ndarray (nmo,nmo) Static part of self-energy (MO basis) static_potential : ndarray (nao,nao) Static potential (AO basis) """ fock = emb.get_fock() static_self_energy = np.zeros_like(fock) static_potential = np.zeros_like(fock) self_energy_moms = np.zeros((n_se_mom, fock.shape[1], fock.shape[1])) fragments = emb.get_fragments(sym_parent=None) if use_sym else emb.get_fragments() for i, f in enumerate(fragments): # Calculate self energy from cluster moments th, tp = f.results.moms solverh = MBLGF(th, log=NullLogger()) solverp = MBLGF(tp, log=NullLogger()) solver = MixedMBLGF(solverh, solverp) solver.kernel() se = solver.get_self_energy() se_moms_clus = [se.moment(i) for i in range(n_se_mom)] mc = f.get_overlap('mo|cluster') mf = f.get_overlap('mo|frag') fc = f.get_overlap('frag|cluster') cfc = fc.T @ fc # Fock matrix in cluster basis fock_cls = f.cluster.c_active.T @ fock @ f.cluster.c_active e_cls = np.diag(fock_cls) if proj == 1: # Static potential v_cls = se.as_static_potential(e_cls, eta=eta) # Static potential (used to update MF for the self-consistnecy) v_frag = cfc @ v_cls v_frag = 0.5 * (v_frag + v_frag.T) static_potential += f.cluster.c_active @ v_frag @ f.cluster.c_active.T # Static self-energy static_se_cls = th[1] + tp[1] - fock_cls static_self_energy_frag = cfc @ static_se_cls static_self_energy_frag = 0.5 * (static_self_energy_frag + static_self_energy_frag.T) static_self_energy += mc @ static_self_energy_frag @ mc.T # Self-energy moments se_moms_frag = [0.5*(cfc @ mom + mom @ cfc) for mom in se_moms_clus] self_energy_moms += np.array([mc @ mom @ mc.T for mom in se_moms_frag]) if use_sym: for child in f.get_symmetry_children(): static_potential += child.cluster.c_active @ v_frag @ child.cluster.c_active.T mc_child = child.get_overlap('mo|cluster') static_self_energy += mc_child @ static_self_energy_frag @ mc_child.T self_energy_moms += np.array([mc_child @ mom @ mc_child.T for mom in se_moms_frag]) elif proj == 2: # Static potential v_cls = se.as_static_potential(e_cls, eta=eta) v_frag = fc @ v_cls @ fc.T static_potential += f.c_frag @ v_frag @ f.c_frag.T # Static self-energy static_se_cls = th[1] + tp[1] - fock_cls static_se_frag = fc @ static_se_cls @ fc.T static_self_energy += mf @ static_se_frag @ mf.T # Self-energy moments se_moms_frag = [0.5*(fc @ mom @ fc.T) for mom in se_moms_clus] self_energy_moms += np.array([mf @ mom @ mf.T for mom in se_moms_frag]) if use_sym: for child in f.get_symmetry_children(): static_potential += child.c_frag @ v_frag @ child.c_frag.T mf_child = child.get_overlap('mo|frag') fc_child = child.get_overlap('frag|cluster') static_self_energy += mf_child @ static_se_frag @ mf_child.T self_energy_moms += np.array([mf_child @ mom @ mf_child.T for mom in se_moms_frag]) return self_energy_moms, static_self_energy, static_potential
[docs]def make_self_energy_1proj(emb, use_sym=True, use_svd=True, eta=1e-2, aux_shift_frag=False, se_degen_tol=1e-4, se_eval_tol=1e-6, drop_non_causal=False): """ Construct full system self-energy in Lehmann representation from cluster spectral moments using 1 projector TODO: MPI, SVD Parameters ---------- emb : EWF object Embedding object use_sym : bool Use symmetry to reconstruct self-energy use_svd : bool Use SVD to decompose the self-energy as outer product eta : float Broadening factor for static potential se_degen_tol : float Tolerance for degeneracy in Lehmann representation se_eval_tol : float Tolerance for self-energy eigenvalues assumed to be kept drop_non_causal : bool Drop non-causal poles (negative eigenvalues) of self-energy Returns ------- self_energy : Lehmann object Reconstructed self-energy in Lehmann representation (MO basis) static_self_energy : ndarray (nmo,nmo) Static part of self-energy (MO basis) static_potential : ndarray (nao,nao) Static potential (AO basis) """ fock = emb.get_fock() static_self_energy = np.zeros_like(fock) static_potential = np.zeros_like(fock) energies = [] if use_svd: couplings_l, couplings_r = [], [] else: couplings = [] fragments = emb.get_fragments(sym_parent=None) if use_sym else emb.get_fragments() for i, f in enumerate(fragments): # Calculate self energy from cluster moments th, tp = f.results.moms solverh = MBLGF(th, log=emb.log) solverp = MBLGF(tp, log=emb.log) solver = MixedMBLGF(solverh, solverp) solver.kernel() se = solver.get_self_energy() gf = solver.get_greens_function() dm = gf.occupied().moment(0) * 2 nelec = np.trace(dm) emb.log.info("Fragment %s: Electron target %f %f without shift"%(f.id, f.nelectron, nelec)) if aux_shift_frag: aux = AuxiliaryShift(th[0]+tp[0], se, f.nelectron, occupancy=2, log=emb.log) aux.kernel() se = aux.get_self_energy() gf = aux.get_greens_function() dm = gf.occupied().moment(0) * 2 nelec = np.trace(dm) emb.log.info("Fragment %s: Electron target %f %f with shift"%(f.id, f.nelectron, nelec)) mc = f.get_overlap('mo|cluster') fc = f.get_overlap('frag|cluster') cfc = fc.T @ fc # Fock matrix in cluster basis fock_cls = f.cluster.c_active.T @ fock @ f.cluster.c_active e_cls = np.diag(fock_cls) # Static potential v_cls = se.as_static_potential(e_cls, eta=eta) # Static potential (used to update MF for the self-consistnecy) v_frag = cfc @ v_cls v_frag = 0.5 * (v_frag + v_frag.T) static_potential += f.cluster.c_active @ v_frag @ f.cluster.c_active.T # Static self-energy static_se_cls = th[1] + tp[1] - fock_cls static_self_energy_frag = cfc @ static_se_cls static_self_energy_frag = 0.5 * (static_self_energy_frag + static_self_energy_frag.T) static_self_energy += mc @ static_self_energy_frag @ mc.T # Dynamic self-energy coup_l, coup_r = se._unpack_couplings() sym_coup = 0.5*(einsum('pa,qa->apq', cfc @ coup_l , coup_r) + einsum('pa,qa->apq', coup_l , cfc @ coup_r)) if use_svd: couplings_l_frag, couplings_r_frag, energies_frag = [], [], [] for a in range(sym_coup.shape[0]): m = sym_coup[a] U, s, Vt = np.linalg.svd(m) idx = np.abs(s) > se_eval_tol assert idx.sum() <= 2 u = U[:,idx] @ np.diag(np.sqrt(s[idx])) v = Vt.conj().T[:,idx] @ np.diag(np.sqrt(s[idx])) couplings_l_frag.append(u) couplings_r_frag.append(v) energies_frag += [se.energies[a] for e in range(idx.sum())] couplings_l_frag, couplings_r_frag = np.hstack(couplings_l_frag), np.hstack(couplings_r_frag) couplings_l.append(mc @ couplings_l_frag) couplings_r.append(mc @ couplings_r_frag) energies.append(energies_frag) else: couplings_frag, energies_frag = [], [] for a in range(sym_coup.shape[0]): m = sym_coup[a] val, vec = np.linalg.eigh(m) idx = np.abs(val) > se_eval_tol assert idx.sum() <= 2 w = vec[:,idx] @ np.diag(np.sqrt(val[idx], dtype=np.complex64)) couplings_frag.append(w) energies_frag += [se.energies[a] for e in range(idx.sum())] couplings_frag = np.hstack(couplings_frag) couplings.append(mc @ couplings_frag) energies.append(energies_frag) if use_sym: for child in f.get_symmetry_children(): static_potential += child.cluster.c_active @ v_frag @ child.cluster.c_active.T mc_child = child.get_overlap('mo|cluster') static_self_energy += mc_child @ static_self_energy_frag @ mc_child.T energies.append(energies_frag) if use_svd: couplings_l.append(mc_child @ couplings_l_frag) couplings_r.append(mc_child @ couplings_r_frag) else: couplings.append(mc_child @ couplings_frag) energies = np.concatenate(energies) if use_svd: couplings = np.hstack(couplings_l), np.hstack(couplings_r) else: couplings = np.hstack(couplings) self_energy = Lehmann(energies, couplings) self_energy = remove_se_degeneracy(emb, self_energy, dtol=se_degen_tol, etol=se_eval_tol, drop_non_causal=drop_non_causal) return self_energy, static_self_energy, static_potential
[docs]def make_self_energy_2proj(emb, use_sym=True, eta=1e-2): """ Construct full system self-energy in Lehmann representation from cluster spectral moments using 2 projectors TODO: MPI, SVD Parameters ---------- emb : EWF object Embedding object use_sym : bool Use symmetry to reconstruct self-energy eta : float Broadening factor for static potential Returns ------- self_energy : Lehmann object Reconstructed self-energy in Lehmann representation (MO basis) static_self_energy : ndarray (nmo,nmo) Static part of self-energy (MO basis) static_potential : ndarray (nao,nao) Static potential (AO basis) """ fock = emb.get_fock() static_self_energy = np.zeros_like(fock) static_potential = np.zeros_like(fock) couplings, energies = [], [] fragments = emb.get_fragments(sym_parent=None) if use_sym else emb.get_fragments() for i, f in enumerate(fragments): # Calculate self energy from cluster moments th, tp = f.results.moms solverh = MBLGF(th, log=NullLogger()) solverp = MBLGF(tp, log=NullLogger()) solver = MixedMBLGF(solverh, solverp) solver.kernel() se = solver.get_self_energy() mf = f.get_overlap('mo|frag') fc = f.get_overlap('frag|cluster') # Fock matrix in cluster basis fock_cls = f.cluster.c_active.T @ fock @ f.cluster.c_active e_cls = np.diag(fock_cls) # Static potential v_cls = se.as_static_potential(e_cls, eta=eta) v_frag = fc @ v_cls @ fc.T static_potential += f.c_frag @ v_frag @ f.c_frag.T # Static self-energy static_se_cls = th[1] + tp[1] - fock_cls static_se_frag = fc @ static_se_cls @ fc.T static_self_energy += mf @ static_se_frag @ mf.T # Dynamic self-energy if type(se.couplings) is tuple: couplings_l, couplings_r = se.couplings couplings_l = mf @ fc @ couplings_l couplings_r = mf @ fc @ couplings_r couplings.append((couplings_l, couplings_r)) else: couplings.append(mf @ fc @ se.couplings) energies.append(se.energies) if use_sym: for child in f.get_symmetry_children(): static_potential += child.c_frag @ v_frag @ child.c_frag.T mf_child = child.get_overlap('mo|frag') fc_child = child.get_overlap('frag|cluster') static_self_energy += mf_child @ static_se_frag @ mf_child.T if type(se.couplings) is tuple: couplings_l, couplings_r = se.couplings couplings_l = mf_child @ fc_child @ couplings_l couplings_r = mf_child @ fc_child @ couplings_r couplings.append((couplings_l, couplings_r)) else: couplings.append(mf_child @ fc_child @ se.couplings) energies.append(se.energies) if type(couplings[0]) is tuple: couplings_l, couplings_r = zip(*couplings) couplings = np.hstack(couplings_l), np.hstack(couplings_r) else: couplings = np.hstack(couplings) energies = np.concatenate(energies) self_energy = Lehmann(energies, couplings) #self_energy = remove_se_degeneracy(emb, self_energy)#, dtol=se_degen_tol, etol=se_eval_tol, drop_non_causal=drop_non_causal) return self_energy, static_self_energy, static_potential
[docs]def remove_se_degeneracy(emb, se, dtol=1e-8, etol=1e-6, drop_non_causal=False): emb.log.debug("Removing degeneracy in self-energy - degenerate energy tol=%e evec tol=%e"%(dtol, etol)) e = se.energies couplings_l, couplings_r = se._unpack_couplings() e_new, slices = get_unique(e, atol=dtol)# emb.log.debug("Number of energies = %d, unique = %d"%(len(e),len(e_new))) energies, couplings = [], [] warn_non_causal = False for i, s in enumerate(slices): mat = np.einsum('pa,qa->pq', couplings_l[:,s], couplings_r[:,s]).real val, vec = np.linalg.eigh(mat) if drop_non_causal: idx = val > etol else: idx = np.abs(val) > etol if np.sum(val[idx] < -etol) > 0: warn_non_causal = True w = vec[:,idx] @ np.diag(np.sqrt(val[idx], dtype=np.complex64)) couplings.append(w) energies += [e_new[i] for _ in range(idx.sum())] emb.log.debug(" | E = %e << %s"%(e_new[i],e[s])) emb.log.debug(" evals: %s"%val) emb.log.debug(" kept: %s"%(val[idx])) if warn_non_causal: emb.log.warning("Non-causal poles found in self-energy") couplings = np.hstack(couplings).real return Lehmann(np.array(energies), np.array(couplings))
[docs]def get_unique(array, atol=1e-15): # Find elements of a sorted float array which are unique up to a tolerance assert len(array.shape) == 1 i = 0 slices = [] while i < len(array): j = 1 idxs = [i] while i+j < len(array): if np.abs(array[i] - array[i+j]) < atol: idxs.append(i+j) j += 1 else: break i = i + j slices.append(np.s_[idxs[0]:idxs[-1]+1]) new_array = np.array([array[s].mean() for s in slices]) return new_array, slices
[docs]def fit_hermitian(se): """ Fit a causal self-energy Parameters ---------- se : Lehmann Self-energy in Lehmann representation Returns ------- se : Lehmann Fitted causal self-energy """ energies = se.energies.copy() couplings_l, couplings_r = se._unpack_couplings() couplings_l, couplings_r = couplings_l.copy(), couplings_r.copy() def f(w): denom = 1 / (1j*w - energies + 1j * eta) return np.einsum('pa,qa,a->pq', couplings_l, couplings_r, denom) def obj(x): x = x.reshape(shape) V, e = x[:-1], x[-1] def integrand(w): denom = 1 / (1j*w - energies) a = np.einsum('pa,qa,a->pq', couplings_l, couplings_r, denom) denom = 1 / (1j*w - e) b = np.einsum('pa,qa,a->pq', V, V, denom) c = (np.abs(a - b) ** 2).sum() #print(c) return c lim = np.inf val, err = scipy.integrate.quad(integrand, -lim, lim) print("obj: %s err: %s"%(val, err)) return val def grad(x): x = x.reshape(shape) V, e = x[:-1], x[-1] def integrand_V(w): a = np.einsum('pa,qa,a->pq', couplings_l, couplings_r, 1 / (1j*w - energies)) b = np.einsum('pa,qa,a->pq', V, V, 1 / (1j*w - e)) d = b - a omegaRe = e/(w**2 + e**2) omegaIm = w/(w**2 + e**2) ret = np.einsum('rq,qb,b->rb', d.real, V, omegaRe) ret += np.einsum('pr,pb,b->rb', d.real, V, omegaRe) ret += np.einsum('rq,qb,b->rb', d.imag, V, omegaIm) ret += np.einsum('pr,pb,b->rb', d.imag, V, omegaIm) return -2 * ret def integrand_e(w): a = np.einsum('pa,qa,a->pq', couplings_l, couplings_r, 1 / (1j*w - energies)) b = np.einsum('pa,qa,a->pq', V, V, 1 / (1j*w - e)) d = b - a omegaRe = (e**2 - w**2)/(w**2 + e**2)**2 omegaIm = 2*e*w/(w**2 + e**2)**2 #print(omegaIm) ret = 2*np.einsum('pq,pb,qb,b->b', d.real, V, V, omegaRe) ret += 2*np.einsum('pq,pb,qb,b->b', d.imag, V, V, omegaIm) return ret integrand = lambda w: np.hstack([integrand_V(w).flatten(), integrand_e(w)]) lim = np.inf jac, err_V = scipy.integrate.quad_vec(lambda x: integrand(x), -lim, lim) print('grad norm: %s err: %s'%(np.linalg.norm(jac),err_V)) #print(grad) return jac x0 = np.vstack([couplings_l, energies]) shape = x0.shape x0 = x0.flatten() xgrad = grad(x0) print(shape) print("obj(x0) = %s"%obj(x0)) print('grad(x0)') print(xgrad) #x0 = np.random.randn(*x0.shape) #* 1e-2 #x = xgrad.reshape(x0.shape) #return xgrad print(shape) res = scipy.optimize.minimize(obj, x0, jac=grad, method='BFGS') #res = scipy.optimize.basinhopping(obj, x0.flatten(), niter=10, minimizer_kwargs=dict(method='BFGS')) print("Sucess %s, Integral = %s"%(res.success, res.x)) x = res.x.reshape(shape) return Lehmann(x[-1], x[:-1])