"""
pdeco.py
Functions associated with PDE-Constrained Optimization.
"""
import numpy as np
from opt_einsum import contract
from scipy.optimize import minimize
try:
from psi4.core import BasisSet as psi4_basiset
from psi4.core import MintsHelper as psi4_mintshelper
except:
pass
[docs]class PDECO():
"""
Performs Optimization as in: 10.1063/1.1535422 - Qin Wu + Weitao Yang
Attributes:
-----------
lambda_rgl: {None, float}. If float, lambda-regularization is added with lambda=lambda_rgl.
"""
regul_norm = None # Regularization norm: ||v||^2
lambda_reg = None # Regularization constant
[docs] def pdeco(self, opt_max_iter, reg=None, gtol=1e-3,
opt_method='L-BFGS-B', opt=None):
"""
Calls scipy minimizer to minimize lagrangian.
"""
self.lambda_reg = reg
self.lambda_reg = reg
if opt is None:
opt = {"disp": False}
opt['maxiter'] = opt_max_iter
opt['gtol'] = gtol
# Initialization for D and C
self._diagonalize_with_potential_pbs(self.v_pbs)
if self.S4 is None:
self.S4 = self.fouroverlap()
if opt_method.lower() == 'bfgs' or opt_method.lower() == 'l-bfgs-b':
opt_results = minimize( fun = self.lagrangian_pbeco,
x0 = self.v_pbs,
jac = self.gradient_pbeco,
method = opt_method,
options = opt
)
else:
raise ValueError(F'{opt_method} is not supported. Only BFGS '
F'and L-BFGS is supported.')
if opt_results.success == False:
self.v_pbs = opt_results.x
self.opt_info = opt_results
raise ValueError("Optimization was unsucessful (|grad|=%.2e) within %i iterations, "
"try a different initial guess. %s"% (np.linalg.norm(opt_results.jac), opt_results.nit, opt_results.message)
)
else:
print(f"Optimization Successful within {opt_results.nit} iterations! |grad|={np.linalg.norm(opt_results.jac):.2e}." )
self.v_pbs = opt_results.x
self.opt_info = opt_results
[docs] def fouroverlap(self, wfn=None):
"""
Calculates four overlap integral with Density Fitting method.
S4_{ijkl} = \int dr \phi_i(r)*\phi_j(r)*\phi_k(r)*\phi_l(r)
Parameters
----------
wfn: psi4.core.Wavefunction
Wavefunction object of molecule
Return
------
S4
"""
if wfn is None:
wfn = self.wfn
print(f"4-AO-Overlap tensor will take about {self.nbf **4 / 8 * 1e-9:f} GB.")
mints = psi4_mintshelper( self.basis )
aux_basis = psi4_basiset.build(wfn.molecule(), "DF_BASIS_SCF", "",
"JKFIT", wfn.basisset().name())
S_Pmn = np.squeeze(mints.ao_3coverlap(aux_basis, wfn.basisset(),
wfn.basisset()))
S_PQ = np.array(mints.ao_overlap(aux_basis, aux_basis))
S_PQinv = np.linalg.pinv(S_PQ, rcond=1e-9)
S4 = np.einsum('Pmn,PQ,Qrs->mnrs', S_Pmn, S_PQinv, S_Pmn, optimize=True)
return S4
[docs] def lagrangian_pbeco(self, v):
"""
Lagrangian to be minimized wrt external potential
Equation (5) of main reference
"""
# If v is not updated, will not re-calculate.
if not np.allclose(v, self.v_pbs, atol=1e-15):
self._diagonalize_with_potential_pbs(v)
# self._diagonalize_with_potential_pbs(v)
if self.ref == 1:
L = 4 * contract("ijkl,ij,kl", self.S4, self.Da - self.Dt[0], self.Da- self.Dt[0])
else:
L = contract("ijkl,ij,kl", self.S4, self.Da+self.Db-self.Dt[0]-self.Dt[1], self.Da+self.Db-self.Dt[0]-self.Dt[1])
# Add lambda-regularization
if self.lambda_reg is not None:
T = self.T_pbs
if self.ref == 1:
norm = 2 * (v[:self.npbs] @ T @ v[:self.npbs])
else:
norm = (v[self.npbs:] @ T @ v[self.npbs:]) + (v[:self.npbs] @ T @ v[:self.npbs])
L += norm * self.lambda_reg
self.regul_norm = norm
return L
[docs] def gradient_pbeco(self, v):
"""
Calculates gradient wrt target density
Equation (11) of main reference
"""
# If v is not updated, will not re-calculate.
if not np.allclose(v, self.v_pbs, atol=1e-15):
self._diagonalize_with_potential_pbs(v)
if self.ref == 1:
grad_temp = np.zeros((self.nbf, self.nbf))
g = 8 * contract("ijkl,ij,km->lm", self.S4, 2 * (self.Dt[0] - self.Da), self.Coca) # shape (ao, mo)
u = 0.5 * contract("lm,lm->m", self.Coca, g) # shape (mo, )
g -= 2 * contract('m,ij,jm->im', u, self.S2, self.Coca) # shape (ao, mo)
for idx in range(self.nalpha):
LHS = self.Fock - self.S2 * self.eigvecs_a[idx]
p_i = np.linalg.solve(LHS, g[:, idx])
# Gram–Schmidt rotation
p_i -= np.sum(p_i * np.dot(self.S2, self.Coca[:,idx])) * self.Coca[:,idx]
assert np.allclose([np.sum(p_i * (self.S2 @ self.Coca[:,idx])), np.linalg.norm(np.dot(LHS,p_i)-g[:, idx]), np.sum(g[:, idx]*self.Coca[:,idx])], 0, atol=1e-4)
grad_temp += p_i[:, np.newaxis] * self.Coca[:,idx]
self.grad = contract("ij,ijk->k", grad_temp, self.S3)
else:
grad_temp_a = np.zeros((self.nbf, self.nbf))
g_a = 4 * contract("ijkl,ij,km->lm", self.S4, (self.Dt[0] - self.Da) + (self.Dt[1] - self.Db), self.Coca) # shape (ao, mo)
u_a = 0.5 * contract("lm,lm->m", self.Coca, g_a) # shape (mo, )
g_a -= 2 * contract('m,ij,jm->im', u_a, self.S2, self.Coca) # shape (ao, mo)
for idx in range(self.nalpha):
LHS = self.Fock[0] - self.S2 * self.eigvecs_a[idx]
p_i = np.linalg.solve(LHS, g_a[:, idx])
# Gram–Schmidt rotation
p_i -= np.sum(p_i * np.dot(self.S2, self.Coca[:,idx])) * self.Coca[:,idx]
assert np.allclose([np.sum(p_i * (self.S2 @ self.Coca[:,idx])), np.linalg.norm(np.dot(LHS,p_i)-g_a[:, idx]), np.sum(g_a[:, idx]*self.Coca[:,idx])], 0, atol=1e-4)
grad_temp_a += p_i[:, np.newaxis] * self.Coca[:,idx]
grad_temp_b = np.zeros((self.nbf, self.nbf))
g_b = 4 * contract("ijkl,ij,km->lm", self.S4, (self.Dt[0] - self.Da) + (self.Dt[1] - self.Db), self.Cocb) # shape (ao, mo)
u_b = 0.5 * contract("lm,lm->m", self.Cocb, g_b) # shape (mo, )
g_b -= 2 * contract('m,ij,jm->im', u_b, self.S2, self.Cocb) # shape (ao, mo)
for idx in range(self.nbeta):
LHS = self.Fock[1] - self.S2 * self.eigvecs_b[idx]
p_i = np.linalg.solve(LHS, g_b[:, idx])
# Gram–Schmidt rotation
p_i -= np.sum(p_i * np.dot(self.S2, self.Cocb[:,idx])) * self.Cocb[:,idx]
assert np.allclose([np.sum(p_i * (self.S2 @ self.Cocb[:,idx])), np.linalg.norm(np.dot(LHS,p_i)-g_b[:, idx]), np.sum(g_b[:, idx]*self.Cocb[:,idx])], 0, atol=1e-4)
grad_temp_b += p_i[:, np.newaxis] * self.Cocb[:,idx]
self.grad = np.concatenate((contract("ij,ijk->k", grad_temp_a, self.S3), contract("ij,ijk->k", grad_temp_b, self.S3)))
if self.lambda_reg is not None:
T = self.T_pbs
if self.ref == 1:
rgl_vector = 4 * self.lambda_reg*np.dot(T, v[:self.npbs])
self.grad += rgl_vector
else:
self.grad[:self.npbs] += 2 * self.lambda_reg*np.dot(T, v[:self.npbs])
self.grad[self.npbs:] += 2 * self.lambda_reg*np.dot(T, v[self.npbs:])
return self.grad
[docs] def find_regularization_constant_pdeco(self, opt_max_iter, opt_method="L-BFGS-B", gtol=1e-3,
opt=None, lambda_list=None):
"""
Finding regularization constant lambda.
Note: it is recommend to set a specific convergence criteria by opt or tol,
in order to control the same convergence
for different lambda value.
After the calculation is done, one can plot the returns to select a good lambda.
Parameters:
-----------
guide_potential_components: a list of string
the components for guide potential v_pbs.
see Inverter.generate_components() for details.
opt_method: string default: "trust-krylov"
opt_methods available in scipy.optimize.minimize
tol: float
Tolerance for termination. See scipy.optimize.minimize for details.
opt: dictionary, optional
if given:
scipy.optimize.minimize(method=opt_method, options=opt)
lambda_list: np.ndarray, optional
A array of lambda to search; otherwise, it will be 10 ** np.linspace(-1, -7, 7).
Returns:
--------
lambda_list: np.ndarray
A array of lambda searched.
P_list: np.ndarray
The value defined by [Bulat, Heaton-Burgess, Cohen, Yang 2007] eqn (21).
Corresponding to lambda in lambda_list.
error_list: np.ndarray
The Ts value for each lambda.
"""
error_list = []
v_norm_list = []
if lambda_list is None:
lambda_list = 10 ** np.linspace(-3, -9, 7)
if opt is None:
opt = {"disp" : False}
opt['maxiter'] = opt_max_iter
opt['gtol'] = gtol
self.lambda_reg = None
# Initial calculation with no regularization
# Initialization for D and C
self._diagonalize_with_potential_pbs(self.v_pbs)
if opt_method.lower() == 'bfgs' or opt_method.lower() == 'l-bfgs-b':
initial_result = minimize(fun=self.lagrangian_pbeco,
x0=self.v_pbs,
jac=self.gradient_pbeco,
method=opt_method,
options=opt
)
else:
raise ValueError(F'{opt_method} is not supported. Only BFGS '
F'and L-BFGS is supported.')
if initial_result.success == False:
raise ValueError("Optimization was unsucessful (|grad|=%.2e) within %i iterations, "
"try a different intitial guess"% (np.linalg.norm(initial_result.jac), initial_result.nit)
+ initial_result.message)
else:
error0 = -initial_result.fun
initial_v_pbs = initial_result.x # This is used as the initial guess for with regularization calculation.
for reg in lambda_list:
self.lambda_reg = reg
if opt_method.lower() == 'bfgs' or opt_method.lower() == 'l-bfgs-b':
opt_results = minimize(fun=self.lagrangian_pbeco,
x0=initial_v_pbs,
jac=self.gradient_pbeco,
method=opt_method,
options=opt
)
else:
raise ValueError(F'{opt_method} is not supported. Only BFGS '
F'and L-BFGS is supported.')
v_norm_list.append(self.regul_norm)
error_list.append(opt_results.fun - self.lambda_reg * self.regul_norm)
P_list = lambda_list * np.array(v_norm_list) / (np.array(error_list) - error0)
return lambda_list, P_list, np.array(error_list)