Source code for pydft_qmmm.interfaces.psi4.psi4_utils

"""Functionality for building the Psi4 interface and some helper classes.
"""
from __future__ import annotations

from dataclasses import dataclass
from functools import lru_cache
from typing import TYPE_CHECKING

import numpy as np
import psi4.core
from numpy.typing import NDArray

from .psi4_interface import Psi4Interface
from pydft_qmmm.common import BOHR_PER_ANGSTROM

if TYPE_CHECKING:
    from pydft_qmmm.interfaces import QMSettings


[docs] class Psi4Context: r"""A wrapper class for managing Psi4 Geometry object generation. Args: atoms: The indices of atoms that are treated at the QM level of theory. embedding: The indices of atoms that are electrostatically embedded. elements: The element symbols of the atoms in the system. positions: The positions (:math:`\mathrm{\mathring{A}}`) of the atoms within the system. charges: The partial charges (:math:`e`) of the atoms in the system. charge: The net charge (:math:`e`) of the system represented at the QM level of theory. spin: The net spin of the system represented by the QM level of theory. """ def __init__( self, atoms: set[int], embedding: set[int], elements: list[str], positions: NDArray[np.float64], charges: NDArray[np.float64], charge: int, spin: int, ) -> None: self.atoms = atoms self.embedding = embedding self.elements = elements self.positions = positions self.charges = charges self.charge = charge self.spin = spin self.do_embedding = True
[docs] @lru_cache def generate_molecule(self) -> psi4.core.Molecule: """Create the Geometry object for Psi4 calculations. Returns: The Psi4 Geometry object, which contains the positions, net charge, and net spin of atoms treated at the QM level of theory. """ geometrystring = """\n""" for atom in sorted(self.atoms): position = self.positions[atom] element = self.elements[atom] geometrystring = ( geometrystring + str(element) + " " + str(position[0]) + " " + str(position[1]) + " " + str(position[2]) + "\n" ) geometrystring += str(self.charge) + " " geometrystring += str(self.spin) + "\n" # geometrystring += "symmetry c1\n" geometrystring += "noreorient\nnocom\n" return psi4.geometry(geometrystring)
[docs] def generate_external_potential(self) -> list[list[int | list[int]]] | None: r"""Create the data structures read by Psi4 to perform embedding. Returns: The list of coordinates (:math:`\mathrm{a.u.}`) and charges (:math:`e`) that will be read by Psi4 during calculations and electrostatically embedded. """ external_potential = [] for i in sorted(self.embedding): external_potential.append( [ self.charges[i], [ self.positions[i, 0] * BOHR_PER_ANGSTROM, self.positions[i, 1] * BOHR_PER_ANGSTROM, self.positions[i, 2] * BOHR_PER_ANGSTROM, ], ], ) if not external_potential: return None if not self.do_embedding: return None return external_potential
[docs] def update_positions(self, positions: NDArray[np.float64]) -> None: r"""Set the atomic positions used by Psi4. Args: positions: The positions (:math:`\mathrm{\mathring{A}}`) of the atoms within the system. """ self.positions = positions self.generate_molecule.cache_clear()
[docs] def update_charges(self, charges: NDArray[np.float64]) -> None: """Set the atomic partial charges used by Psi4 for embedding. Args: charges: The partial charges (:math:`e`) of the atoms. """ self.charges = charges self.generate_molecule.cache_clear()
[docs] def update_embedding(self, embedding: set[int]) -> None: """Set the atoms are electrostatically embedded. Args: embedding: The indices of atoms that are electrostatically embedded. """ self.embedding = embedding
[docs] @dataclass(frozen=True) class Psi4Options: """An immutable wrapper class for storing Psi4 global options. Args: basis: The name of the basis set to be used by Psi4. dft_spherical_points: The number of spherical Lebedev points to use in the DFT quadrature. dft_radial_points: The number of radial points to use in the DFT quadrature. scf_type: The name of the type of SCF to perform, as in the JK build algorithms as in Psi4. scf__reference: The name of the reference to use, including restricted Kohn-Sham or unrestricted Kohn-Sham. scf__guess: The name of the algorithm used to generate the initial guess at the start of an SCF procedure. """ basis: str dft_spherical_points: int dft_radial_points: int scf_type: str scf__reference: str scf__guess: str
[docs] def psi4_interface_factory(settings: QMSettings) -> Psi4Interface: """Build the interface to Psi4 given the settings. Args: settings: The settings used to build the Psi4 interface. Returns: The Psi4 interface. """ basis = settings.basis_set if "assign" not in settings.basis_set: basis = "assign " + settings.basis_set.strip() psi4.basis_helper(basis, name="default") options = _build_options(settings) functional = settings.functional context = _build_context(settings) wrapper = Psi4Interface( settings, options, functional, context, ) # Register observer functions. settings.system.charges.register_notifier(wrapper.update_charges) settings.system.positions.register_notifier(wrapper.update_positions) settings.system.subsystems.register_notifier(wrapper.update_subsystems) return wrapper
def _build_options(settings: QMSettings) -> Psi4Options: """Build the Psi4Options object. Args: settings: The settings used to build the Psi4 interface. Returns: The global options used by Psi4 in each calculation. """ options = Psi4Options( "default", settings.quadrature_spherical, settings.quadrature_radial, settings.scf_type, "uks" if settings.spin > 1 else "rks", "read" if settings.read_guess else "auto", ) return options def _build_context(settings: QMSettings) -> Psi4Context: """Build the Psi4Context object. Args: settings: The settings used to build the Psi4 interface. Returns: The geometry and embedding manager for Psi4. """ context = Psi4Context( set(settings.system.select("subsystem I")), set(), list(settings.system.elements), settings.system.positions, settings.system.charges, settings.charge, settings.spin, ) return context