Source code for pydft_qmmm.interfaces.qmmm_pme_psi4.psi4_interface

"""The QM/MM/PME-hacked Psi4 software interface.
"""
from __future__ import annotations

from dataclasses import asdict
from functools import lru_cache
from typing import TYPE_CHECKING

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

from pydft_qmmm.common import BOHR_PER_ANGSTROM
from pydft_qmmm.common import KJMOL_PER_EH
from pydft_qmmm.interfaces.psi4.psi4_interface import Psi4Interface

if TYPE_CHECKING:
    from pydft_qmmm.interfaces import QMSettings
    from pydft_qmmm.interfaces.psi4.psi4_utils import Psi4Context
    from .psi4_utils import PMEPsi4Options

# psi4.core.be_quiet()


[docs] class PMEPsi4Interface(Psi4Interface): """A software interface wrapping Psi4 functionality. Args: settings: The settings used to build the Psi4 interface. options: The Psi4 global options derived from the settings. functional: The name of the functional to use for exchange-correlation calculations. context: An object which holds system information to feed into Psi4. """ def __init__( self, settings: QMSettings, options: PMEPsi4Options, functional: str, context: Psi4Context, ) -> None: self._settings = settings self._options = options self._functional = functional self._context = context self._quad_extd_pot = None self._nuc_extd_pot = None self._nuc_extd_grad = None
[docs] def compute_forces(self) -> NDArray[np.float64]: r"""Compute the forces on the system using Psi4. Returns: The forces (:math:`\mathrm{kJ\;mol^{-1}\;\mathring{A}^{-1}}`) acting on atoms in the system. """ wfn = self._generate_wavefunction() psi4.set_options(asdict(self._options)) kwargs: dict[str, NDArray[np.float64]] = {} if self._quad_extd_pot is not None: kwargs["quad_extd_pot"] = self._quad_extd_pot if self._nuc_extd_pot is not None: kwargs["nuc_extd_pot"] = self._nuc_extd_pot if self._nuc_extd_grad is not None: kwargs["nuc_extd_grad"] = self._nuc_extd_grad forces = psi4.gradient( self._functional, ref_wfn=wfn, **kwargs, ) forces = forces.to_array() * -KJMOL_PER_EH * BOHR_PER_ANGSTROM forces_temp = np.zeros(self._context.positions.shape) qm_indices = sorted(self._context.atoms) forces_temp[qm_indices, :] = forces[0:len(qm_indices)] if self._context.generate_external_potential(): embed_indices = sorted(self._context.embedding) forces = ( wfn.external_pot().gradient_on_charges().to_array() * -KJMOL_PER_EH * BOHR_PER_ANGSTROM ) forces_temp[embed_indices, :] = forces forces = forces_temp return forces
@lru_cache def _generate_wavefunction(self) -> psi4.core.Wavefunction: """Generate the Psi4 Wavefunction object for use by Psi4. Returns: The Psi4 Wavefunction object, which contains the energy and coefficients determined through SCF. """ molecule = self._context.generate_molecule() psi4.set_options(asdict(self._options)) kwargs: dict[str, NDArray[np.float64]] = {} if self._quad_extd_pot is not None: kwargs["quad_extd_pot"] = self._quad_extd_pot if self._nuc_extd_pot is not None: kwargs["nuc_extd_pot"] = self._nuc_extd_pot if self._nuc_extd_grad is not None: kwargs["nuc_extd_grad"] = self._nuc_extd_grad _, wfn = psi4.energy( self._functional, return_wfn=True, molecule=molecule, external_potentials=self._context.generate_external_potential(), **kwargs, ) wfn.to_file( wfn.get_scratch_filename(180), ) return wfn
[docs] def compute_quadrature(self) -> NDArray[np.float64]: """Build a reference quadrature. Returns: A reference quadrature constructed from the Psi4 Geometry object. """ molecule = self._context.generate_molecule() sup_func = psi4.driver.dft.build_superfunctional( self._functional, True, )[0] c1_molecule = molecule.clone() c1_molecule._initial_cartesian = molecule._initial_cartesian.clone() c1_molecule.set_geometry(c1_molecule._initial_cartesian) c1_molecule.reset_point_group("c1") c1_molecule.fix_orientation(True) c1_molecule.fix_com(True) c1_molecule.update_geometry() basis = psi4.core.BasisSet.build( c1_molecule, "ORBITAL", self._options.basis, ) vbase = psi4.core.VBase.build(basis, sup_func, "RV") vbase.initialize() quadrature = np.concatenate( tuple([ coord.reshape(-1, 1) for coord in vbase.get_np_xyzw()[0:3] ]), axis=1, ) return quadrature
[docs] def update_quad_extd_pot(self, quad_extd_pot: NDArray[np.float64]) -> None: r"""Update the external potential at the quadrature grid points. Args: quad_ext_pot: A potential (:math:`\mathrm{a.u.}\;e^{-1}`) evaluated at the quadrature grid points. """ self._quad_extd_pot = quad_extd_pot self._generate_wavefunction.cache_clear()
[docs] def update_nuc_extd_pot(self, nuc_extd_pot: NDArray[np.float64]) -> None: r"""Update the external potential at the nuclear coordinates. Args: nuc_ext_pot: A potential (:math:`\mathrm{a.u.}\;e^{-1}`) evaluated at the nuclear coordinates. """ self._nuc_extd_pot = nuc_extd_pot self._generate_wavefunction.cache_clear()
[docs] def update_nuc_extd_grad(self, nuc_extd_grad: NDArray[np.float64]) -> None: r"""Update the gradient of the external potential at the nuclei. Args: nuc_ext_pot: A gradient of the potential (:math:`\mathrm{a.u.}\;e^{-1}\;\mathrm{Bohr^{-1}}`) evaluated at the nuclear coordinates. """ self._nuc_extd_grad = nuc_extd_grad self._generate_wavefunction.cache_clear()