Source code for pytc.scf


import numpy as np
import scipy.linalg
from pyscf import scf
from pyscf.scf import hf
import jax
import jax.numpy as jnp
from .tc import TC, ISDFTC

[docs] class TCSCF(hf.RHF): """ Transcorrelated Self-Consistent Field (TC-SCF) solver. Inherits from PySCF's RHF class to leverage standard integral engines. Adds TC effective potentials to H_core and V_eff. """ def __init__(self, mol, jastrow_factor, jastrow_params, tc_obj=None): super().__init__(mol) self.jastrow_factor = jastrow_factor self.jastrow_params = jastrow_params if tc_obj is None: # Initialize TC object in AO basis (mo_coeff=Identity) # We use a dummy mean-field to initialize TC from pyscf # but we need to ensure mo_coeff is Identity. # TC.from_pyscf uses mf.mo_coeff. # Create a dummy MF with identity coeffs dummy_mf = scf.RHF(mol) dummy_mf.mo_coeff = np.eye(mol.nao_nr()) dummy_mf.mo_occ = np.zeros(mol.nao_nr()) # Dummy # We need to ensure grid level is sufficient self.tc_obj = TC.from_pyscf(dummy_mf, jastrow_factor, mo_coeff=dummy_mf.mo_coeff, grid_lvl=3) else: self.tc_obj = tc_obj
[docs] def isdf(self, n_rank=None): """Enable ISDF approximation.""" self.tc_obj = ISDFTC.from_tc(self.tc_obj, n_rank=n_rank) # Precompute kernels and L_aux self.tc_obj = self.tc_obj.isdf(self.jastrow_params) return self
def _get_init_occ(self, n_orb): """Get initial occupation numbers.""" nocc = self.mol.nelectron // 2 mo_occ = np.zeros(n_orb) mo_occ[:nocc] = 2 return mo_occ
[docs] def get_hcore(self, mol=None): """Get core Hamiltonian including 1-body TC corrections.""" if mol is None: mol = self.mol h_core = super().get_hcore(mol) # Add TC 1-body correction # Note: get_1b_fock returns JAX array, convert to numpy h_tc = np.array(self.tc_obj.get_1b_fock(self.jastrow_params)) return h_core + h_tc
[docs] def get_veff(self, mol=None, dm=None, dm_last=0, vhf_last=0, hermi=1): """Get effective potential including 2-body and 3-body TC corrections.""" if mol is None: mol = self.mol if dm is None: dm = self.make_rdm1() # Standard HF potential (Coulomb + Exchange) v_hf = super().get_veff(mol, dm, dm_last, vhf_last, hermi) # Convert DM to JAX dm_jax = jnp.array(dm) v_tc_2b = np.array(self.tc_obj.get_2b_fock(self.jastrow_params, dm_jax)) v_tc_3b = np.array(self.tc_obj.get_3b_fock(self.jastrow_params, dm_jax)) return v_hf + v_tc_2b + v_tc_3b
[docs] def eig(self, h, s, **kwargs): """Solver for generalized eigenvalue problem F C = S C e. Overridden to handle non-Hermitian F and orthonormalize orbitals. Newer pyscf passes extra kwargs (e.g. x=x_orth); accept and ignore them. """ # Use scipy.linalg.eig for non-Hermitian matrices # h is F, s is S e, c = scipy.linalg.eig(h, s) # Sort eigenvalues (real part) idx = np.argsort(e.real) e = e[idx] c = c[:, idx] # Orthonormalize orbitals with respect to S # The TC-SCF procedure requires the density to be built from # orthonormalized orbitals (Gram-Schmidt). # We use QR decomposition of S^1/2 C to get orthonormal C_orth. # C_orth = S^-1/2 Q # Compute S^1/2 # S is Hermitian, so we can use eigh s_e, s_v = scipy.linalg.eigh(s) # Handle small eigenvalues if any (though S should be positive definite) s_e = np.maximum(s_e, 1e-15) s_half = s_v @ np.diag(np.sqrt(s_e)) @ s_v.T s_inv_half = s_v @ np.diag(1.0/np.sqrt(s_e)) @ s_v.T # Transform C c_trans = s_half @ c q, r = scipy.linalg.qr(c_trans, mode='economic') # Transform back c_orth = s_inv_half @ q return e, c_orth
[docs] def get_grad(self, mo_coeff, mo_occ, fock=None): """ Gradient of the objective function. For non-Hermitian/TC-SCF, this might need adjustment, but for now we rely on the default which checks [F, P]. """ return super().get_grad(mo_coeff, mo_occ, fock)