#!/usr/bin/env python
# Copyright 2024 The PySCF Developers. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
'''
The AutoAux algorithm by ORCA
Ref:
JCTC, 13 (2016), 554
'''
from math import factorial
import numpy as np
from pyscf import gto
F_LAUX = np.array([20 , 7.0, 4.0, 4.0, 3.5, 2.5, 2.0, 2.0])
BETA_BIG = np.array([1.8, 2.0, 2.2, 2.2, 2.2, 2.3, 3.0, 3.0])
BETA_SMALL = 1.8
def _primitive_emin_emax(basis):
l_max = max(b[0] for b in basis)
emin_by_l = [1e99] * (l_max+1)
emax_by_l = [0] * (l_max+1)
e_eff_by_l = [0] * (l_max+1)
for b in basis:
l = b[0]
if isinstance(b[1], int):
e_c = np.array(b[2:])
else:
e_c = np.array(b[1:])
es = e_c[:,0]
emax_by_l[l] = max(es.max(), emax_by_l[l])
emin_by_l[l] = min(es.min(), emin_by_l[l])
cs = e_c[:,1:]
cs = np.einsum('pi,p->pi', cs, gto.gto_norm(l, es)) # normalize GTOs
cs = gto.mole._nomalize_contracted_ao(l, es, cs) # prefactors in r_ints
ee = es[:,None] + es
r_ints = gto.gaussian_int(l*2+3, ee) # \int \chi^2 r dr
r_exp = np.einsum('pi,pq,qi->i', cs, r_ints, cs)
k = 2**(2*l+1) * factorial(l+1)**2 / factorial(2*l+2)
# Eq (9) in the paper, e_eff = 2 * k**2 / (np.pi * r_exp) is a typo.
# See also https://github.com/MolSSI-BSE/basis_set_exchange/issues/317
# For primitive functions, following expression leads to
# e_eff = exponent of the basis
e_eff = 2 * k**2 / (np.pi * r_exp**2)
# For primitive functions, e_eff may be slightly different to the
# exponent due to the rounding errors in gaussian_int function.
# When a particular shell has only one primitive function, one auxiliary
# function should be generated. This error can introduce an additional
# auxiliary function.
# Slightly reduce e_eff to remove the extra auxiliary functions.
e_eff -= 1e-8
e_eff_by_l[l] = max(e_eff.max(), e_eff_by_l[l])
return np.array(emax_by_l), np.array(emin_by_l), np.array(e_eff_by_l)
def _auto_aux_element(Z, basis, ecp_core=0):
a_max_by_l, a_min_by_l, a_eff_by_l = _primitive_emin_emax(basis)
a_min_prim = a_min_by_l[:,None] + a_min_by_l
a_max_prim = a_max_by_l[:,None] + a_max_by_l
a_max_aux = a_eff_by_l[:,None] + a_eff_by_l
l_max1 = a_max_by_l.size
l_max = l_max1 - 1
# TODO: handle ECP
if Z <= 2:
l_val = 0
elif Z <= 20:
l_val = 1
elif Z <= 56:
l_val = 2
else:
l_val = 3
l_inc = 1
if Z > 18:
l_inc = 2
l_max_aux = min(max(l_val*2, l_max+l_inc), l_max*2)
liljsum = np.arange(l_max1)[:,None] + np.arange(l_max1)
liljsub = abs(np.arange(l_max1)[:,None] - np.arange(l_max1))
a_min_by_l = [a_min_prim[(liljsub<=ll) & (ll<=liljsum)].min() for ll in range(l_max_aux+1)]
a_max_by_l = [a_max_prim[(liljsub<=ll) & (ll<=liljsum)].max() for ll in range(l_max_aux+1)]
a_aux_by_l = [a_max_aux [(liljsub<=ll) & (ll<=liljsum)].max() for ll in range(l_max_aux+1)]
a_max_adjust = [min(F_LAUX[l] * a_aux_by_l[l], a_max_by_l[l])
for l in range(l_val*2+1)]
a_max_adjust = a_max_adjust + a_aux_by_l[l_val*2+1 : l_max_aux+1]
emin = np.array(a_min_by_l)
emax = np.array(a_max_adjust)
ns_small = np.log(a_max_adjust[:l_val*2+1] / emin[:l_val*2+1]) / np.log(BETA_SMALL)
etb = []
# ns_small+1 to ensure the largest exponent in etb > emax
for l, n in enumerate(np.ceil(ns_small).astype(int) + 1):
if n > 0:
etb.append((l, n, emin[l], BETA_SMALL))
if l_max_aux > l_val*2:
ns_big = (np.log(emax[l_val*2+1:] / emin[l_val*2+1:])
/ np.log(BETA_BIG[l_val*2+1:l_max_aux+1]))
for l, n in enumerate(np.ceil(ns_big).astype(int) + 1):
if n > 0:
l = l + l_val*2+1
beta = BETA_BIG[l]
etb.append((l, n, emin[l], beta))
return etb
[docs]
def autoaux(mol):
'''
Create an auxiliary basis set for the given orbital basis set using
the Auto-Aux algorithm.
See also: G. L. Stoychev, A. A. Auer, and F. Neese
Automatic Generation of Auxiliary Basis Sets
J. Chem. Theory Comput. 13, 554 (2017)
http://doi.org/10.1021/acs.jctc.6b01041
'''
from pyscf.gto.basis import bse
def expand(symb):
Z = gto.charge(symb)
etb = _auto_aux_element(Z, mol._basis[symb])
if etb:
return gto.expand_etbs(etb)
raise RuntimeError(f'Failed to generate even-tempered auxbasis for {symb}')
uniq_atoms = {a[0] for a in mol._atom}
if bse.basis_set_exchange is None:
return {symb: expand(symb) for symb in uniq_atoms}
if isinstance(mol.basis, str):
try:
elements = [gto.charge(symb) for symb in uniq_atoms]
newbasis = bse.autoaux(mol.basis, elements)
except KeyError:
newbasis = {symb: expand(symb) for symb in uniq_atoms}
else:
newbasis = {}
for symb in uniq_atoms:
if symb in mol.basis and isinstance(mol.basis[symb], str):
try:
auxbs = bse.autoaux(mol.basis[symb], gto.charge(symb))
newbasis[symb] = next(iter(auxbs.values()))
except KeyError:
newbasis[symb] = expand(symb)
else:
newbasis[symb] = expand(symb)
return newbasis
[docs]
def autoabs(mol):
'''
Create a Coulomb fitting basis set for the given orbital basis set.
See also:
R. Yang, A. P. Rendell, and M. J. Frisch
Automatically generated Coulomb fitting basis sets: Design and accuracy for systems containing H to Kr
J. Chem. Phys. 127, 074102 (2007)
http://doi.org/10.1063/1.2752807
'''
from pyscf.gto.basis import bse
if bse is None:
print('Package basis-set-exchange not available')
raise ImportError
uniq_atoms = {a[0] for a in mol._atom}
if isinstance(mol.basis, str):
elements = [gto.charge(symb) for symb in uniq_atoms]
newbasis = bse.autoabs(mol.basis, elements)
else:
newbasis = {}
for symb in uniq_atoms:
if symb in mol.basis and isinstance(mol.basis[symb], str):
auxbs = bse.autoabs(mol.basis[symb], gto.charge(symb))
newbasis[symb] = next(iter(auxbs.values()))
else:
raise NotImplementedError
return newbasis