Two-Electron Integrals
Four Centers
For the atomic orbitals $\chi_\mu, \chi_\nu, \chi_\lambda, \chi_\sigma$, the two-electron four-center repulsion integral, in chemist's notation, is calculated as:
\[(\mu\nu|\lambda\sigma) = \iint \chi_\mu(\mathbf{r}_1)\chi_\nu(\mathbf{r}_1)\, \frac{1}{|\mathbf{r}_1-\mathbf{r}_2|}\, \chi_\lambda(\mathbf{r}_2)\chi_\sigma(\mathbf{r}_2)\, d\mathbf{r}_1 d\mathbf{r}_2\]
This integral obeys an 8-fold permutational symmetry, $(\mu\nu|\lambda\sigma) = (\nu\mu|\lambda\sigma) = (\mu\nu|\sigma\lambda) = (\lambda\sigma|\mu\nu) = \ldots$, and its full dense tensor scales as $\mathcal{O}(n_\text{bas}^4)$ in memory – prohibitive for anything but small systems.
A full-dense tensor can be calculated directly from a basis set
julia> bset = BasisSet("sto-3g", """
O 0.000000000000 -0.143225816552 0.000000000000
H 1.638036840407 1.136548822547 -0.000000000000
H -1.638036840407 1.136548822547 -0.000000000000
""")
julia> I = ERI_2e4c(bset)
julia> size(I)
(7, 7, 7, 7)
julia> I[1, 1, 1, 1] # (1s_O 1s_O | 1s_O 1s_O)
4.785065752279989
julia> a,b,c,d = 1, 3, 6, 4 # The array `I` has 8-fold symmetry
julia> I[a,b,c,d] == I[b,a,c,d] == I[a,b,d,c] == I[b,a,d,c] &&
I[c,d,a,b] == I[d,c,a,b] == I[c,d,b,a] == I[d,c,b,a]
trueAn in-place (mutating) version of the full-dense tensor can also be used:
julia> out = zeros(bset.nbas, bset.nbas, bset.nbas, bset.nbas)
julia> ERI_2e4c!(out, bset)
julia> out == I
trueFor large basis sets, the full dense tensor quickly becomes intractable. sparseERI_2e4c provides a middle ground: Cauchy-Schwarz shell-pair screening discards quartets below a magnitude cutoff up front, and only the permutationally-unique surviving elements are stored, rather than the full dense tensor.
julia> idx, vals = sparseERI_2e4c(bset)
julia> n = findfirst(==((1, 2, 2, 2)), idx) # For example reproducibility.
julia> vals[n] # A non-zero integral value...
0.25663337137335623
julia> vals[n] ≈ I[idx[n]...]
true
julia> I[1,1,5,7] # Zero or near zero elements are not contained in `idx` and `vals`
0.0
julia> (1,1,5,7) in idx
false
julia> I[1,3,6,7]
-8.470329472543003e-22
julia> (1,3,6,7) in idx
false
julia> I[1,3,7,7] # All non zero elements (up to a threshold) are returned...
-0.0025845696440600086
julia> (1,3,7,7) in idx
true
julia> (3,1,7,7) in idx # But only one canonical permutation is stored.
false
julia> length(idx) # number of unique, screened-surviving elements
228
julia> length(I) / length(idx) # Fraction of non-zero elements
10.530701754385966The full ERI is thus ~10x larger than its sparse, screened counterpart.
The ordering within
idxandvalsreturned fromsparseERI_2e4cis arbitrary.
In many cases, storing the full ERI array, even in sparse form, becomes impossible. In these cases, the per-shell-quartet option is also available:
julia> I4443 = ERI_2e4c(bset, 4, 4, 4, 3) # 4 and 3 are shell indexes
1×1×1×3 Array{Float64, 4}:
[:, :, 1, 1] =
0.026307122405698283
[:, :, 1, 2] =
0.02055337661033345
[:, :, 1, 3] =
0.0For maximum efficiency, you may need to use the most primitive call, mutating and using a pre-allocated output
julia> out = zeros(num_basis(bset[4]), num_basis(bset[4]), num_basis(bset[4]), num_basis(bset[3]))
julia> ERI_2e4c!(out, bset, 4, 4, 4, 3)
julia> out == I4443
trueGaussianBasis.ERI_2e4c — Function
ERI_2e4c(BS::BasisSet) -> Array{Float64,4}
ERI_2e4c(BS::BasisSet, i, j, k, l) -> Array{Float64,4}Compute the two-electron four-center integral tensor (ij|kl) (chemist's notation), respecting the standard 8-fold permutational symmetry ((ij|kl)=(ji|kl)=(ij|lk)=(kl|ij)=...).
Methods
ERI_2e4c(BS): full, densenbas × nbas × nbas × nbastensor forBS. For large basis sets prefersparseERI_2e4c, which screens and stores only the unique elements.ERI_2e4c(BS, i, j, k, l): just the(Ni,Nj,Nk,Nl)block for shellsi,j,k,lofBS(shell indices, not AO indices).
For repeated calls (e.g. in a hot loop), see ERI_2e4c!, which writes into a preallocated array instead of allocating.
GaussianBasis.ERI_2e4c! — Function
ERI_2e4c!(out, BS::BasisSet, i, j, k, l)
ERI_2e4c!(out, BS::BasisSet)Mutating counterpart of ERI_2e4c: writes into the caller-supplied out instead of allocating. This shell-quartet form is the primitive the full-tensor form builds on.
Methods
ERI_2e4c!(out, BS, i, j, k, l):outmust be(Ni,Nj,Nk,Nl), the(ij|kl)block (chemist's notation) for shellsi,j,k,lofBS(shell indices, not AO indices).ERI_2e4c!(out, BS):outmust be a densenbas × nbas × nbas × nbasarray.
GaussianBasis.sparseERI_2e4c — Function
sparseERI_2e4c(BS::BasisSet, cutoff=1e-12) -> (indexes, values)Compute the unique, permutationally non-redundant two-electron four-center integrals (ij|kl) for BS (chemist's notation), applying Cauchy-Schwarz shell-pair screening and discarding any integral with abs(value) <= cutoff.
Returns a pair (indexes, values): indexes is a Vector{NTuple{4,Int16}} of (I,J,K,L) AO indices (1-based) and values the corresponding Vector{Float64} of integral values, indexes[n] paired with values[n]. Only one representative of each permutationally-equivalent AO quartet is returned. Element order is not sorted or otherwise guaranteed (it depends on how the underlying work is split across threads) – callers that need a canonical unique integral once (e.g. Fock builds) don't care about order, but shouldn't rely on any particular one either.
Three Centers
For the atomic orbitals $\chi_\mu, \chi_\nu$ of a "regular" orbital basis and an auxiliary/fitting basis function $\varphi_P$, the two-electron three-center integral is calculated as:
\[(\mu\nu|P) = \iint \chi_\mu(\mathbf{r}_1)\chi_\nu(\mathbf{r}_1) \frac{1}{|\mathbf{r}_1-\mathbf{r}_2|} \varphi_P(\mathbf{r}_2)\, d\mathbf{r}_1 d\mathbf{r}_2\]
This is one of the two building blocks for density fitting (also called resolution-of-the-identity), which approximates the 4-center ERI by expanding each electron's charge density in an auxiliary basis instead of computing it exactly. It scales as $\mathcal{O}(n_\text{bas}^2 n_\text{aux})$, far more modestly than the $\mathcal{O}(n_\text{bas}^4)$ full 4-center tensor – which is what makes density fitting an attractive approximation for large systems.
A full-dense tensor can be calculated directly from an orbital basis set and an auxiliary basis set
julia> auxbset = BasisSet("cc-pvdz-rifit", """
O 0.000000000000 -0.143225816552 0.000000000000
H 1.638036840407 1.136548822547 -0.000000000000
H -1.638036840407 1.136548822547 -0.000000000000
""")
julia> B = ERI_2e3c(bset, auxbset)
julia> size(B)
(7, 7, 84)
julia> B[1, 1, 1]
0.7085956990587061Alternatively, an in-place (mutating) version can be used:
julia> out = zeros(bset.nbas, bset.nbas, auxbset.nbas)
julia> ERI_2e3c!(out, bset, auxbset)
julia> out == B
trueA per-shell call is also available, but it requires a single basis set. Hence, both your regular basis and your auxiliary basis must be merged, and you must keep track of shell indexes carefully: shells 1:bset.nshells of the merged basis are the regular shells (unchanged), and shells beyond that are the auxiliary ones, offset by bset.nshells.
julia> merged = merge_basis(bset, auxbset)
julia> out = zeros(num_basis(bset[1]), num_basis(bset[1]), num_basis(auxbset[1]))
julia> ERI_2e3c!(out, merged, 1, 1, bset.nshells + 1) # shell 1 of auxbset is shell (bset.nshells + 1) of merged
julia> out[1, 1, 1] == B[1, 1, 1] # matches the full dense array from the example above
trueGaussianBasis.ERI_2e3c — Function
ERI_2e3c(BS1::BasisSet, BS2::BasisSet) -> Array{Float64,3}Compute the full two-electron three-center integral tensor (μν|P), with μ,ν running over BS1's AOs (the "regular" orbital basis) and P over BS2's (the auxiliary/fitting basis) – the building block for density fitting / resolution-of-the-identity approximations. Returns a dense BS1.nbas × BS1.nbas × BS2.nbas array, symmetric under μ↔ν swap. For repeated calls, see ERI_2e3c!, which writes into a preallocated array instead of allocating.
GaussianBasis.ERI_2e3c! — Function
ERI_2e3c!(out, BS::BasisSet, i, j, k)
ERI_2e3c!(out, BS1::BasisSet, BS2::BasisSet)Mutating counterpart of ERI_2e3c: writes into the caller-supplied out instead of allocating.
Methods
ERI_2e3c!(out, BS, i, j, k):outmust be(Ni,Nj,Nk), the(ij|k)block for shellsi,j,kofBS(shell indices, not AO indices). This is the shell-triple primitive the full-tensor form builds on – but it takes a singleBasisSetwith the regular and auxiliary shells already merged together, since libcint's 3-center kernel resolves shell indices against one basis. See Three Centers for how to build one and map shell indices into it.ERI_2e3c!(out, BS1, BS2):outmust be a denseBS1.nbas × BS1.nbas × BS2.nbasarray.
Two Centers
For two auxiliary/fitting basis functions $\varphi_P, \varphi_Q$, the two-electron two-center integral is calculated as:
\[(P|Q) = \iint \varphi_P(\mathbf{r}_1) \frac{1}{|\mathbf{r}_1-\mathbf{r}_2|} \varphi_Q(\mathbf{r}_2)\, d\mathbf{r}_1 d\mathbf{r}_2\]
This is the other density fitting building block: (P|Q) is the Coulomb metric $J_{PQ}$ used to fit the auxiliary-basis expansion coefficients against the three-center integrals above. It only involves the auxiliary basis, so it scales as $\mathcal{O}(n_\text{aux}^2)$, far cheaper than either the 3- or 4-center integral.
A full-dense matrix can be calculated directly from an auxiliary basis set
julia> Jmetric = ERI_2e2c(auxbset)
julia> size(Jmetric)
(84, 84)
julia> Jmetric[1, 1]
0.1148022639511714Alternatively, an in-place (mutating) version can be used:
julia> out = zeros(auxbset.nbas, auxbset.nbas)
julia> ERI_2e2c!(out, auxbset)
julia> out == Jmetric
trueFor maximum efficiency, you may need to use the most primitive call, mutating and using a pre-allocated output
julia> out = zeros(num_basis(auxbset[3]), num_basis(auxbset[2]))
julia> ERI_2e2c!(out, auxbset, 3, 2) # 3 and 2 are shell indexes
1×1 Matrix{Float64}:
0.7584111125059806GaussianBasis.ERI_2e2c — Function
ERI_2e2c(BS::BasisSet) -> Matrix{Float64}Compute the full two-electron two-center integral matrix (P|Q) for BS (the auxiliary/fitting basis) – the Coulomb metric J_{PQ} used in density fitting / resolution-of-the-identity approximations. Returns a dense, symmetric nbas × nbas matrix. For repeated calls, see ERI_2e2c!, which writes into a preallocated array instead of allocating.
GaussianBasis.ERI_2e2c! — Function
ERI_2e2c!(out, BS::BasisSet, i, j)
ERI_2e2c!(out, BS::BasisSet)Mutating counterpart of ERI_2e2c: writes into the caller-supplied out instead of allocating. This shell-pair form is the primitive the full-tensor form builds on.
Methods
ERI_2e2c!(out, BS, i, j):outmust be(Ni,Nj), the(P|Q)block for shellsi,jofBS(shell indices, not AO indices).ERI_2e2c!(out, BS):outmust be a densenbas × nbasmatrix.