Fitting the parameters in STO-LG

In computational chemistry, the Slater-type orbital (STO) more accurately describes the qualitative features of the molecular orbitals than Gaussian functions (GF). However, calculating the two-electron integral using STO can be costly. On the other hand, integrating GFs is relatively cheap. One way to solve this problem is to use a linear combination of GFs to approximate a STO. Such linear combination of Gaussian functions is called contracted Gaussian functions (CGF).

$$\phi_\mu^{CGF}(\vec{r}-\vec{R}_A) = \sum_{p=1}^L d_{p\mu} \phi_p^{GF} (\alpha_{p\mu}, \vec{r} - \vec{R}_A)$$

where L is the length of the contraction, $d_{p\mu}$ and $\alpha_{p\mu}$ are contraction coefficients and contraction exponents, respectively. Hence, the so-called STO-LG strategy uses L Gaussian-type orbitals to approximate one STO function.

Approximating 1s Slater-type function using STO-LG

The expressions for 1s STO and GF are

\begin{align} \phi_{1s}^{STO} (\zeta, \vec{r}) = \left( \frac{\zeta^3}{\pi} \right)^{1/2} e^{-\zeta |\vec{r}-{\vec{R_A}}|} \notag \\ \phi_{1s}^{GF}(\alpha, \vec{r}) = \left(\frac{2\alpha}{\pi}\right)^{3/4} e^{-\alpha |\vec{r}-{\vec{R _A}}|^2} \notag \end{align}

where both orbitals have their corresponding parameters. The goal is to find the $d_{p}$ and $\alpha_{p}$ in the following equation

$$ \phi_{1s}^{STO} (\zeta, \vec{r}) = \sum_p^L d_{p}\phi_{1s}^{GF}(\alpha_p, \vec{r}) $$

STO-1G with $\zeta = 1.0$

In the first case, we will show the process of fitting the simplest function STO-1G by assuming $\zeta=1.0$ in the STO. Basically we will solve the following equation for $\alpha_{11}$

$$ \phi_{1s}^{STO} (\zeta=1.0, \vec{r}) = \phi_{1s}^{GF}(\alpha_{11}, \vec{r}) $$

from sympy import *
import numpy as np

zeta, r, alpha, a11 = symbols("zeta r alpha a11", positive=True)  # define the variables

sto = (zeta **3 / pi) ** (1/2) * exp(-zeta * r)  # general expression for one STO
gf = (2 * alpha /pi) ** (3/4) * exp(-alpha * r**2)  # general expression for one GF

sto_1 = sto.subs(zeta, 1.0)  # zeta = 1.0
gf_1 = gto.subs(alpha, a11)  # alpha = a11

Instead of minimize the differences between the two functions, we will maximize the overlap between the GF and the STO following Szabo’s book.

S = integrate(sto_1 * gf_1 * r**2, (r, 0, oo)) * 4 * pi  # the overlap between STO(1.0, r) and GF(alpha, r)

We will maximize this overlap in terms of $a_{11}$. Since we will use scipy, we turn the maximization problem into minimzation of the negative of the overlap S.

# function to minimize
def func(a):
    res = S.subs(a11, a[0]).evalf()
    return -res
from scipy.optimize import minimize
res = minimize(func, x0=[0.2], bounds=[(0.1, 1)])
res.x[0]
0.2709502078346618

We get the $\alpha_{11}$ value as 0.2709497296298158. This is almost identical to the result from Szabo’s book.

Show the STO-1G plot

import matplotlib.pyplot as plt
%matplotlib inline

plt.rcParams['font.size'] = 22
plt.rcParams['font.family'] = 'Arial'
gf_a1 = lambdify(r, gf_1.subs(a11, res.x[0]), "numpy")
sto_1_np = lambdify(r, sto_1, "numpy")
r_np = np.linspace(0, 10, 101)

plt.plot(r_np, gf_a1(r_np), label='STO-1G')
plt.plot(r_np, sto_1_np(r_np), label='STO')
plt.legend()
plt.xlabel("$r$")
<matplotlib.text.Text at 0x7fcc2089b898>

The results are reasonably good.

STO-LG

We will code the general procedure to calculate $L>1$ CGFs.

from IPython.display import display, Math

def get_gto(d, alpha):
    return d * (2 * alpha /pi) ** (3/4) * exp(-alpha * r**2)

def get_symbols(L):
    ds = symbols(f'd:{L}', positive=True)
    alphas = symbols(f'alpha:{L}', positive=True)
    return ds, alphas

class STOLG:
    def __init__(self, L=3, zta=1.0):
        self.L = L
        self.ds, self.alphas = get_symbols(L)
        self.GFs = [get_gto(d, a) for d, a in zip(self.ds, self.alphas)]
        self.GF_sum = sum(self.GFs)
        self.gg_int = integrate(self.GF_sum * self.GF_sum * r**2, (r, 0, oo)) * 4 * pi
        self.gg_int = self.gg_int.evalf()
        self.sto = sto.subs(zeta, zta)
        self.S = integrate(self.GF_sum * self.sto * r**2, (r, 0, oo)) * 4 * pi
        
    def fit(self):
        def _func(x):
            subs = {i: j for i, j in zip(self.ds[1:]+self.alphas, x)}
            d0_val = solve(self.gg_int.subs(subs) - 1)
            subs[self.ds[0]] = d0_val[0]
            val = self.S.subs(subs).evalf()
            # print(subs)
            self.subs = subs
            return -float(val)
          
        # initial guesses
        d_vals = np.linspace(0.1, 0.9, self.L-1).tolist()
        alpha_vals = np.linspace(0.1, 0.9, self.L).tolist()
        self.res = minimize(_func, x0=d_vals + alpha_vals, bounds=[(0, 10)] * (2 * self.L - 1))
        return self.res
    
    @property
    def expression(self):
        expr = []
        for i, j in zip(self.ds, self.alphas):
            expr.append(r"%.6f\phi^{GF}(%.6f)" % (self.subs[i], self.subs[j]))
        return display(Math('+'.join(expr)))
    
    @property
    def func(self):
        return lambdify(r, self.GF_sum.subs(self.subs), "numpy")
    
    @property
    def funcs(self):
        return [lambdify(r, i.subs(self.subs), "numpy") for i in self.GFs]
    
    def __call__(self, r):
        return self.func(r)
    
    def plot(self, r):
        plt.plot(r, self(r), '-', label=f'STO-{self.L}G')
        for i in range(self.L):
            plt.plot(r, self.funcs[i](r), '--', label=f'GF-{i}')
        plt.plot(r, sto_1_np(r), '-', label='STO')
        plt.legend()

STO-2G

sto2g = STOLG(L=2)
sto2g.fit()
      fun: -0.9984197028799346
 hess_inv: <3x3 LbfgsInvHessProduct with dtype=float64>
      jac: array([ 0.00000000e+00,  2.40918396e-06, -4.10782519e-07])
  message: b'CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL'
     nfev: 96
      nit: 19
   status: 0
  success: True
        x: array([0.43013353, 0.15162213, 0.85180271])

The overlap has reached 0.998 with only two GFs.

sto2g.expression  

$$0.678908\phi^{GF}(0.151622)+0.430134\phi^{GF}(0.851803)$$

The expression above matches with Equation (3.220) in Szabo’s book.

sto2g.plot(r_np)

STO-3G

sto3g = STOLG(3)
sto3g.fit()
      fun: -0.9998347361981794
 hess_inv: <5x5 LbfgsInvHessProduct with dtype=float64>
      jac: array([5.09592368e-06, 1.04027897e-05, 1.17794663e-05, 5.55111512e-06,
       2.22044605e-08])
  message: b'CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH'
     nfev: 276
      nit: 39
   status: 0
  success: True
        x: array([0.53532369, 0.15432918, 0.10982016, 0.40578573, 2.22784233])
sto3g.expression

$$0.444642\phi^{GF}(0.109820)+0.535324\phi^{GF}(0.405786)+0.154329\phi^{GF}(2.227842)$$

Again the STO-3G expression matches with Equation (3.221) in Szabo’s book.

sto3g.plot(r_np)

Summary

In this notebook, I show how we can fit the parameters in the contracted Gaussian functions. The results are relatively sensitive to the initial guesses given the optimizers. I believe it will be more so if L further increases.

I also see that with changing $\zeta$, the optimizer gives me results different from the scaling relationships. It will be interesting to further investigate the cause.