#!/usr/bin/env python
# Copyright 2014-2023 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.
#
# Author: Xiaojie Wu <wxj6000@gmail.com>
#
'''
dispersion correction for HF and DFT
'''
import warnings
from functools import lru_cache
from pyscf.lib import logger
from pyscf import scf
# supported dispersion corrections
DISP_VERSIONS = ['d3bj', 'd3zero', 'd3bjm', 'd3zerom', 'd3op', 'd4']
XC_MAP = {'wb97m-d3bj': 'wb97m',
'b97m-d3bj': 'b97m',
'wb97x-d3bj': 'wb97x',
'wb97m-v': 'wb97m',
'b97m-v': 'b97m',
'wb97x-v': 'wb97x'
}
# special cases:
# - wb97x-d is not supported yet
# - wb97*-d3bj is wb97*-v with d3bj
# - wb97x-d3 is not supported yet
# - 3c method is not supported yet
# These xc functionals need special treatments
_white_list = {
'wb97m-d3bj': ('wb97m-v', False, 'd3bj'),
'b97m-d3bj': ('b97m-v', False, 'd3bj'),
'wb97x-d3bj': ('wb97x-v', False, 'd3bj'),
}
# These xc functionals are not supported yet
_black_list = {
'wb97x-d', 'wb97x-d3',
'wb97m-d3bj2b', 'wb97m-d3bjatm',
'b97m-d3bj2b', 'b97m-d3bjatm',
}
[docs]
@lru_cache(128)
def parse_dft(xc_code):
'''
Extract (xc, nlc, disp) from xc_code
'''
if not isinstance(xc_code, str):
return xc_code, '', None
method_lower = xc_code.lower()
if method_lower in _black_list:
raise NotImplementedError(f'{method_lower} is not supported yet.')
if method_lower in _white_list:
return _white_list[method_lower]
if method_lower.endswith('-3c'):
raise NotImplementedError('*-3c methods are not supported yet.')
if '-d3' in method_lower or '-d4' in method_lower:
xc, disp = method_lower.split('-')
else:
xc, disp = method_lower, None
return xc, '', disp
[docs]
@lru_cache(128)
def parse_disp(dft_method):
'''Decode the disp parameters based on the xc code.
Returns xc_code_for_dftd3, disp_version, with_3body
Example: b3lyp-d3bj2b -> (b3lyp, d3bj, False)
wb97x-d3bj -> (wb97x, d3bj, False)
'''
if dft_method == 'hf':
return 'hf', None, False
dft_lower = dft_method.lower()
xc, nlc, disp = parse_dft(dft_lower)
if dft_lower in XC_MAP:
xc = XC_MAP[dft_lower]
if disp is None:
return xc, None, False
disp_lower = disp.lower()
if disp_lower.endswith('2b'):
return xc, disp_lower.replace('2b', ''), False
elif disp_lower.endswith('atm'):
return xc, disp_lower.replace('atm', ''), True
else:
return xc, disp_lower, False
[docs]
def check_disp(mf, disp=None):
'''Check whether to apply dispersion correction based on the xc attribute.
If dispersion is allowed, return the DFTD3 disp version, such as d3bj,
d3zero, d4.
'''
if disp is None:
disp = mf.disp
if disp == 0: # disp = False
return False
# To prevent mf.do_disp() triggering the SCF.__getattr__ method, do not use
# method = getattr(mf, 'xc', 'hf').
if isinstance(mf, scf.hf.KohnShamDFT):
method = mf.xc
else:
# Set the disp method for both HF and MCSCF to 'hf'
method = 'hf'
disp_version = parse_disp(method)[1]
if disp is None: # Using the disp version decoded from the mf.xc attribute
if disp_version is None:
return False
elif disp_version is None: # Using the version specified by mf.disp
disp_version = disp
elif disp != disp_version:
raise RuntimeError(f'mf.disp {disp} conflicts with mf.xc {method}')
if disp_version not in DISP_VERSIONS:
raise NotImplementedError
return disp_version
[docs]
def get_dispersion(mf, disp=None, with_3body=None, verbose=None):
disp_version = check_disp(mf, disp)
if not disp_version:
return 0.
try:
from pyscf.dispersion import dftd3, dftd4
except ImportError:
print('dftd3 and dftd4 not available. Install them with `pip install pyscf-dispersion`')
raise
mol = mf.mol
method = getattr(mf, 'xc', 'hf')
method, _, disp_with_3body = parse_disp(method)
if with_3body is not None:
with_3body = disp_with_3body
# for dftd3
if disp_version[:2].upper() == 'D3':
logger.info(mf, "Calc dispersion correction with DFTD3.")
logger.info(mf, f"Parameters: xc={method}, version={disp_version}, atm={with_3body}")
d3_model = dftd3.DFTD3Dispersion(mol, xc=method, version=disp_version, atm=with_3body)
res = d3_model.get_dispersion()
e_d3 = res.get('energy')
mf.scf_summary['dispersion'] = e_d3
return e_d3
# for dftd4
elif disp_version[:2].upper() == 'D4':
logger.info(mf, "Calc dispersion correction with DFTD4.")
logger.info(mf, f"Parameters: xc={method}, atm={with_3body}")
d4_model = dftd4.DFTD4Dispersion(mol, xc=method, atm=with_3body)
res = d4_model.get_dispersion()
e_d4 = res.get('energy')
mf.scf_summary['dispersion'] = e_d4
return e_d4
else:
raise RuntimeError(f'dispersion correction: {disp_version} is not supported.')