Source code for pydft_qmmm.plugins.pme.pme
"""A plugin organizing the QM/MM/PME algorithm for calculations.
"""
from __future__ import annotations
from typing import Callable
from typing import TYPE_CHECKING
import numpy as np
from openmm import NonbondedForce
from simtk.unit import nanometer
from .pme_utils import pme_components
from pydft_qmmm.calculators import InterfaceCalculator
from pydft_qmmm.interfaces.qmmm_pme_openmm.openmm_interface import (
PMEOpenMMInterface,
)
from pydft_qmmm.interfaces.qmmm_pme_psi4.psi4_interface import (
PMEPsi4Interface,
)
from pydft_qmmm.plugins.plugin import CompositeCalculatorPlugin
# This is bad practice and should be removed when the hacked versions
# of Psi4 and OpenMM are deprecated.
if TYPE_CHECKING:
from pydft_qmmm.calculators import CompositeCalculator
from pydft_qmmm.common import Results
import mypy_extensions
CalculateMethod = Callable[
[
mypy_extensions.DefaultArg(
bool | None,
"return_forces", # noqa: F821
),
mypy_extensions.DefaultArg(
bool | None,
"return_components", # noqa: F821
),
],
Results,
]
[docs]
class PME(CompositeCalculatorPlugin):
"""Perform the QM/MM/PME algorithm during calculations.
"""
[docs]
def modify(
self,
calculator: CompositeCalculator,
) -> None:
"""Modify the functionality of a calculator.
Args:
calculator: The calculator whose functionality will be
modified by the plugin.
"""
self._modifieds.append(type(calculator).__name__)
self.system = calculator.system
for calc in calculator.calculators:
if isinstance(calc, InterfaceCalculator):
if isinstance(calc.interface, PMEOpenMMInterface):
nonbonded_forces = [
force for force in
calc.interface._base_context.getSystem().getForces()
if isinstance(force, NonbondedForce)
]
(
self.pme_alpha, self.pme_gridnumber, _, _,
) = nonbonded_forces[0].getPMEParameters()
self.pme_alpha *= nanometer
self.mm_interface = calc.interface
if isinstance(calc.interface, PMEPsi4Interface):
self.qm_interface = calc.interface
calculator.calculate = self._modify_calculate(calculator.calculate)
def _modify_calculate(
self,
calculate: CalculateMethod,
) -> CalculateMethod:
"""Modify the calculate routine to perform QM/MM/PME.
Args:
calculate: The calculation routine to modify.
Returns:
The modified calculation routine which implements
QM/MM/PME and all requisite operations.
"""
def inner(
return_forces: bool | None = True,
return_components: bool | None = True,
) -> Results:
pme_potential = self.mm_interface.compute_recip_potential()
quadrature = self.qm_interface.compute_quadrature()
(
reciprocal_energy, quadrature_pme_potential,
nuclei_pme_potential, nuclei_pme_gradient,
) = pme_components(
self.system,
quadrature,
pme_potential,
self.pme_gridnumber,
self.pme_alpha,
)
self.qm_interface.update_quad_extd_pot(
np.array(tuple(quadrature_pme_potential)),
)
self.qm_interface.update_nuc_extd_pot(
np.array(tuple(nuclei_pme_potential)),
)
self.qm_interface.update_nuc_extd_grad(
np.array(
tuple(
[tuple(x) for x in nuclei_pme_gradient],
),
),
)
results = calculate(return_forces, return_components)
results.energy += reciprocal_energy
results.components.update(
{"Correction Reciprocal-Space Energy": reciprocal_energy},
)
return results
return inner