"""
Options module for the propSAPT package.
Package-wide default values for keyword arguments that are otherwise hardcoded in
propSAPT routines. An explicitly passed keyword argument always takes precedence
over the value stored here. Use `prop_sapt.set_propsapt_options` and
`prop_sapt.get_propsapt_options` to modify and inspect the current values.
Available options:
default_functional (str): DFT functional used for the monomer calculations
with the RKS reference, if `functional` is not given to `Dimer`.
Defaults to "pbe0".
r_convergence (float): Residual convergence threshold of the CPSCF solver.
Defaults to 1.0e-10.
cpscf_maxiter (int): Maximum number of CPSCF iterations.
Defaults to 50.
cube_grid_step (float): Step size (in bohr) of the grid built by `make_cube`,
if no `grid` is given. Defaults to 0.2.
cube_grid_overage (float): Extent (in bohr) of the grid built by `make_cube`
beyond the outermost atoms, if no `grid` is given. Defaults to 4.0.
cube_iso_sum_level (float): Fraction of the density (or of the squared orbital)
enclosed by the isosurface, used for the isovalues reported in cube files.
Defaults to 0.85.
point_eval_chunk_size (int): Number of points for which basis function values are
computed at once by the `prop_sapt.evaluate` functions and `make_cube`; the
memory used is about `point_eval_chunk_size * nbf` floats. Defaults to 2000.
quadrature_radial_points (int): Number of radial points per atom of the molecular
quadrature grid built by `prop_sapt.partition.molecular_grid`. Defaults to 75.
quadrature_spherical_points (int): Number of Lebedev points per radial shell of that
grid. Defaults to 590.
quadrature_bs_radius_alpha (float): Scale of the Bragg-Slater radii setting the radial
extent of that grid, Psi4's `DFT_BS_RADIUS_ALPHA`. Psi4's default of 1.0 is too
short for products of diffuse basis functions. Defaults to 2.0.
check_symmetry_atol (float | None): Largest element of ``|D - D^T|`` allowed for a
density matrix by `prop_sapt.checks.check_symmetric`, which `calc_density_matrix`
and `save_densities` run; a larger one warns. None turns the check off.
Defaults to 1.0e-10.
check_partition_atol (float | None): Largest defect of fragment weights allowed by
`prop_sapt.checks.check_partition_of_unity`, which
`FragmentOperators.from_grid` runs: the largest element of
``sum_q w_q (sum_X w_X - 1) phi_mu phi_nu``. A larger one warns. None turns the
check off. Defaults to 1.0e-8.
check_closure_atol (float | None): Largest ``|sum_X Tr(D W_X) - Tr(D S)|`` in
electrons allowed by `prop_sapt.checks.check_charge_closure`, which
`FragmentOperators.population` and `charge` run; a larger one warns. None turns
the check off. Defaults to 1.0e-6.
"""
from collections.abc import Collection, Iterator
# Default option values
_OPTION_DEFAULTS = {
"default_functional": "pbe0",
"r_convergence": 1.0e-10, # CPSCF convergence threshold
"cpscf_maxiter": 50,
"cube_grid_step": 0.2, # in bohr
"cube_grid_overage": 4.0, # in bohr
"cube_iso_sum_level": 0.85,
"point_eval_chunk_size": 2000,
"quadrature_radial_points": 75,
"quadrature_spherical_points": 590,
"quadrature_bs_radius_alpha": 2.0,
"check_symmetry_atol": 1.0e-10,
"check_partition_atol": 1.0e-8,
"check_closure_atol": 1.0e-6,
}
_VALID_OPTION_KEYS = frozenset(_OPTION_DEFAULTS.keys())
[docs]
class PropsaptOptions:
"""
Container for the package-wide propSAPT options.
Provides read-only, dict-like access (`options[key]`, `key in options`,
iteration over keys, `len`). Options should be modified only through
`prop_sapt.set_propsapt_options`. The module-level `PROPSAPT_OPTIONS`
instance is the one used throughout the package.
"""
def __init__(self) -> None:
self._options_dict = dict(_OPTION_DEFAULTS)
[docs]
def __getitem__(self, key: str):
"""Returns the current value of the option `key`."""
if not isinstance(key, str):
# NOTE: this could raise `KeyError` immediately
# we only accept string keys
return self._options_dict[key]
return self._options_dict[key.lower()]
# NOTE: The `__setitem__` and `__delitem__` are NOT set purposefully
# to protect `_options_dict` from unwanted changes.
[docs]
def __contains__(self, key: str) -> bool:
"""Checks whether `key` is a known option."""
if not isinstance(key, str):
# NOTE: this could return `False` immediately
# we only accept string keys
return key in self._options_dict
return key.lower() in self._options_dict
[docs]
def __iter__(self) -> Iterator[str]:
"""Iterates over the option keys."""
return iter(self._options_dict)
[docs]
def __len__(self) -> int:
"""Returns the number of options."""
return len(self._options_dict)
def _update(self, options: dict):
"""Updates propSAPT options dict. This is the class protected method.
The `prop_sapt.set_propsapt_options` is the preferred way to set options.
Option keys are case-insensitive. Options not present in `options`
keep their current values.
Args:
options (dict): Dictionary containing `option_key: option_value` pairs.
Raises:
TypeError: Raised when a non-string `option_key` is found in `options`.
KeyError: Raised when invalid `option_key` is found in `options`.
"""
# check if given option keys are valid
for option_key in options:
check_if_valid_option(option_key)
# lowercase all option keys
options_lower = {key.lower(): value for key, value in options.items()}
self._options_dict.update(options_lower)
[docs]
def get_options_dict(self) -> dict:
"""Returns a copy of the current options.
Modifying the returned dictionary does not change the propSAPT options.
Returns:
dict: Dictionary containing `option_key: option_value` pairs.
"""
return dict(self._options_dict)
# Initialize the module with default option values
PROPSAPT_OPTIONS = PropsaptOptions()
[docs]
def check_if_valid_option(option_key: str) -> None:
"""Checks whether `option_key` is a valid propSAPT option key.
Args:
option_key (str): Option key to check (case-insensitive).
Raises:
TypeError: Raised when a non-string `option_key` is given.
KeyError: Raised when invalid `option_key` is given.
"""
if not isinstance(option_key, str):
raise TypeError(
f"Only strings are accepted as option keys! Received `{type(option_key)}`."
)
if option_key.lower() not in _VALID_OPTION_KEYS:
raise KeyError(
f"Invalid propSAPT option key! Option `{option_key}` not recognised."
)
[docs]
def set_propsapt_options(options: dict) -> None:
"""Sets package-wide propSAPT options.
The new values are used as defaults by all subsequent propSAPT calls, unless
overridden by an explicitly passed keyword argument. Option keys are
case-insensitive. Options not present in `options` keep their current values.
See the `prop_sapt.options` module docstring for the available options.
Example:
>>> prop_sapt.set_propsapt_options({"r_convergence": 1.0e-8, "cpscf_maxiter": 100})
Args:
options (dict): Dictionary containing `option_key: option_value` pairs.
Raises:
TypeError: Raised when a non-string `option_key` is found in `options`.
KeyError: Raised when invalid `option_key` is found in `options`.
"""
# NOTE: this is the ONLY place where `PropsaptOptions._update()` is ok.
PROPSAPT_OPTIONS._update(options)
[docs]
def get_propsapt_options() -> dict:
"""Returns the current package-wide propSAPT options.
Modifying the returned dictionary does not change the propSAPT options,
use `prop_sapt.set_propsapt_options` instead.
Returns:
dict: Dictionary containing `option_key: option_value` pairs.
"""
return PROPSAPT_OPTIONS.get_options_dict()
[docs]
def reset_propsapt_options(option_keys: Collection[str] | None = None) -> None:
"""Resets the package-wide propSAPT options to the default values.
Args:
option_keys (Collection[str] | None, optional): Option keys to be set to default
values (e.g. a list, tuple, set or dict), or a single key as a string.
If None, all options are being set to default values. Defaults to None.
Raises:
TypeError: Raised when `option_keys` is not a collection (e.g. a generator)
or a non-string option key is given.
KeyError: Raised when an invalid option key is given.
ValueError: Raised when `option_keys` is empty.
"""
# reset all options
if option_keys is None:
# NOTE: this is the ONLY place where `PropsaptOptions._options_dict` is ok.
PROPSAPT_OPTIONS._options_dict = dict(_OPTION_DEFAULTS)
# one-shot iterators (e.g. generators) are not accepted
elif not isinstance(option_keys, Collection):
raise TypeError(
"`option_keys` has to be a collection (e.g. list, tuple, set)! "
f"Received `{type(option_keys)}`."
)
# reset selected options
elif option_keys:
# a single option key
if isinstance(option_keys, str):
option_keys = [option_keys]
# check if given option keys are valid
for option_key in option_keys:
check_if_valid_option(option_key)
defaults_subdict = {
key.lower(): _OPTION_DEFAULTS[key.lower()] for key in option_keys
}
set_propsapt_options(defaults_subdict)
# something unexpected given
else:
raise ValueError(f"Invalid `option_keys` given! {option_keys}")