Source code for psi4_step.derivatives

# -*- coding: utf-8 -*-

"""Which Psi4 calculations have analytic gradients, and how the integrals are done.

Psi4 (1.11) picks the code for a gradient from the method, the reference, the
algorithm of the correlation integrals (``mp2_type``, ``mp_type`` or ``cc_type``:
conventional, density fitted or Cholesky decomposed) and ``qc_module``. When no code
fits, Psi4 does not stop: it quietly differentiates energies instead, which for a
correlated method costs many times more -- every energy of a Hessian, roughly the
square of the number of coordinates. So the step decides here, writes the options
Psi4 needs, asks for energies explicitly where there is no analytic gradient, and
says so in the output.

The rules follow ``psi4/driver/procrouting/proc.py`` (the ``select_*`` functions):

* conventional CCSD(T): ``ccenergy``, RHF or UHF, only with ``qc_module ccenergy``
  set, and all-electron (``ccenergy`` refuses frozen core);
* conventional CCSD and EOM-CCSD: ``ccenergy``, all-electron;
* conventional MP2, MP2.5, MP3 and LCCD: ``occ``, RHF or UHF, all-electron;
* conventional CCD: no gradient (nor energy);
* density-fitted MP2, MP2.5, MP3, LCCD, CCD, CCSD and CCSD(T): RHF or UHF, frozen
  core or not (``dfmp2`` or ``dfocc``). The step sets ``qc_module occ`` for
  DF-CCSD and DF-CCSD(T), so that the energy comes from the same code as the
  gradient (Psi4 would otherwise take an RHF energy from ``fnocc``) and UHF
  DF-CCSD(T) works at all;
* Cholesky-decomposed integrals: no analytic gradient for any correlated method;
* the orbital-optimized methods: analytic, conventional or density fitted;
* HF and DFT: analytic.
"""

from dataclasses import dataclass, field

import psi4_step


[docs] @dataclass class GradientRoute: """How Psi4 gets the gradient: analytically or from energies.""" analytic: bool options: list = field(default_factory=list) # Psi4 'set' lines it needs reason: str = "" # why there is no analytic gradient
ALWAYS = {"hf", "scf", "dft", "efp"} ORBITAL_OPTIMIZED = {"omp2", "omp3", "omp2.5", "olccd", "oremp2"} CONV_ALL_ELECTRON = { # method: references with an analytic all-electron conventional gradient "mp2": {"rhf", "uhf"}, "mp2.5": {"rhf", "uhf"}, "mp3": {"rhf", "uhf"}, "lccd": {"rhf", "uhf"}, "ccsd": {"rhf", "uhf", "rohf"}, "eom-ccsd": {"rhf", "uhf", "rohf"}, "ccsd(t)": {"rhf", "uhf"}, } DF_RHF_UHF = {"mp2", "mp2.5", "mp3", "lccd", "ccd", "ccsd", "ccsd(t)"} NEEDS_CCENERGY = {"ccsd(t)"} # Psi4 routes it to ccenergy only if asked NEEDS_OCC = {"ccsd", "ccsd(t)"} # DF/CD: dfocc for the energy too, and for UHF NONE = {"ccd": "Psi4 has CCD only with density-fitted or Cholesky integrals"} #: The Psi4 option that sets each method's correlation integrals, and its default. #: (Psi4 treats MP2.5 and MP3 as density fitted unless mp_type is set.) TYPE_KEYWORD = { "mp2": ("mp2_type", "DF"), "mp2.5": ("mp_type", "DF"), "mp3": ("mp_type", "DF"), "lccd": ("cc_type", "CONV"), "ccd": ("cc_type", "CONV"), "ccsd": ("cc_type", "CONV"), "ccsd(t)": ("cc_type", "CONV"), "omp2": ("mp2_type", "DF"), "omp2.5": ("mp_type", "CONV"), "omp3": ("mp_type", "CONV"), "olccd": ("cc_type", "CONV"), } #: The step's choices for the correlation integrals, as Psi4's values. INTEGRAL_CHOICES = { "conventional": "CONV", "density fitting": "DF", "Cholesky decomposition": "CD", } #: How the model name shows integrals other than the method's default. MODEL_PREFIX = {"CONV": "CONV-", "DF": "DF-", "CD": "CD-"}
[docs] def integral_type(method, choice="default"): """Psi4's correlation-integral type for ``method``: "CONV", "DF" or "CD". Parameters ---------- method : str Psi4's name for the method. choice : str The step's setting: "default" (Psi4's default for the method), "conventional", "density fitting" or "Cholesky decomposition". Returns ------- str or None None for methods without correlation integrals (HF, DFT, ...). """ method = method.lower() if method not in TYPE_KEYWORD: return None if choice in INTEGRAL_CHOICES: return INTEGRAL_CHOICES[choice] return TYPE_KEYWORD[method][1]
[docs] def model_prefix(method, choice="default"): """The prefix for the model name ("DF-", "CD-", "CONV-") when the integrals are not the method's default, so results of different integrals are kept apart.""" method = method.lower() integrals = integral_type(method, choice) if integrals is None or integrals == TYPE_KEYWORD[method][1]: return "" return MODEL_PREFIX[integrals]
[docs] def integral_options(method, reference, choice="default", fitting_basis="default"): """The Psi4 'set' lines for the correlation integrals of ``method``.""" method = method.lower() integrals = integral_type(method, choice) if integrals is None: return [] lines = [] if choice in INTEGRAL_CHOICES: lines.append(f"set {TYPE_KEYWORD[method][0]} {integrals.lower()}") if integrals in ("DF", "CD") and method in NEEDS_OCC: lines.append("set qc_module occ") if integrals == "DF" and fitting_basis not in ("default", "", None): lines.append(f"set df_basis_cc {fitting_basis}") return lines
[docs] def gradient_route(method, reference, frozen_core, integrals=None): """The route Psi4 takes to the gradient of ``method``. Parameters ---------- method : str Psi4's name for the method, e.g. "ccsd(t)" or "dft". reference : str "rhf", "uhf", "rohf", "rks" or "uks". frozen_core : bool Whether the core orbitals are frozen. integrals : str, optional "CONV", "DF" or "CD" (see :func:`integral_type`); by default Psi4's default for the method. Returns ------- GradientRoute """ method = method.lower() reference = reference.lower() if method in ALWAYS or reference in ("rks", "uks"): return GradientRoute(True) if integrals is None: integrals = integral_type(method) if integrals == "CD": return GradientRoute( False, reason=( "Psi4 has no analytic gradients with Cholesky-decomposed " "integrals; use density fitting for analytic gradients" ), ) if method in ORBITAL_OPTIMIZED: return GradientRoute(True) if integrals == "DF" and method in DF_RHF_UHF: if reference in ("rhf", "uhf"): return GradientRoute(True) return GradientRoute( False, reason=f"Psi4 has no analytic {reference.upper()} gradient for it" ) if method in NONE: return GradientRoute(False, reason=NONE[method]) if method in CONV_ALL_ELECTRON: if reference not in CONV_ALL_ELECTRON[method]: return GradientRoute( False, reason=f"Psi4 has no analytic {reference.upper()} gradient for it", ) if frozen_core: extra = ( ", or density-fitted integrals, which allow a frozen core" if method in DF_RHF_UHF else "" ) return GradientRoute( False, reason=( "Psi4's analytic gradient for it with conventional integrals is " "all-electron only; set 'Freeze core orbitals' to 'no' for " f"analytic gradients{extra}" ), ) options = ["set qc_module ccenergy"] if method in NEEDS_CCENERGY else [] return GradientRoute(True, options=options) # Anything else: what the metadata says. for record in psi4_step.methods.values(): if record["method"] == method: if record.get("gradients") == "analytic": return GradientRoute(True) break return GradientRoute(False, reason="Psi4 has no analytic gradient for it")