Source code for pydft_qmmm.wrappers.optimization
"""The class for organizing and running optimizations.
"""
from __future__ import annotations
__all__ = ["Optimization"]
import logging
import os
import tempfile
from typing import TYPE_CHECKING
import numpy as np
from pydft_qmmm.calculators import CalculatorPlugin
from pydft_qmmm.utils import Loggable
from pydft_qmmm.utils import DependencyImportError
from pydft_qmmm.plugins import CalculatorCenter
from pydft_qmmm.plugins import CalculatorWrap
if TYPE_CHECKING:
from typing import Any
from pydft_qmmm.calculators import Calculator
from pydft_qmmm.hamiltonians import StandaloneHamiltonian
from pydft_qmmm.system import System
logger = logging.getLogger(__name__)
logger.setLevel(20)
[docs]
class Optimization(Loggable):
"""Organizes and performs optimizations.
Args:
query: A VMD-like selection query describing the atoms to
optimize.
system: The system containing the atoms to optimize.
hamiltonian: The Hamiltonian describing how calculations are
performed.
calculator: A user-defined energy and force calculator, which
may be provided in place of a Hamiltonian object.
plugins: A list of plugins to apply to the calculator object
before optimization.
kwargs: Additional options to provide to the logging
functionality, see :class:`pydft_qmmm.utils.logging.Loggable`
for a list of options.
Attributes:
_frame: The current frame of the optimization, defaults to zero
upon instantiation.
"""
_frame: int = 0
def __init__(
self,
query: str,
system: System,
hamiltonian: StandaloneHamiltonian | None = None,
calculator: Calculator | None = None,
plugins: list[CalculatorPlugin] | None = None,
**kwargs: Any,
) -> None:
# Perform logging setup.
super().__init__(**kwargs)
for handler in self.handlers:
logger.addHandler(handler)
# Set calculator and build if necessary.
if calculator is not None:
self.calculator = calculator
elif hamiltonian is not None:
self.calculator = hamiltonian.build_calculator(system)
else:
raise TypeError
# Perform additional simulation setup.
self.query = query
self._offset = np.zeros(system.positions.shape)
# Todo: See if this is desirable or necessary.
if system.box.any():
self.calculator.register_plugin(CalculatorWrap(), 0)
if system.select("subsystem I"):
self.calculator.register_plugin(CalculatorCenter(), 0)
self.system = system
# Apply plugins.
if plugins is not None:
self._register_plugins(plugins)
[docs]
def optimize(self) -> None:
"""Perform the optimization using geomeTRIC."""
try:
import geometric
import geometric.molecule
except ImportError:
raise DependencyImportError(
"geomeTRIC",
"performing geometry optimizations",
"https://github.com/leeping/geomeTRIC",
)
opt_indices = sorted(self.system.select(self.query))
# Define objective function.
def model(x, *args):
# geomeTRIC provides coordinates in a.u.
y = x/1.88973
self.system.positions[opt_indices, :] = (
y - self._offset[opt_indices, :]
)
self.calculate_energy_forces()
# geomeTRIC wants energies and gradients in a.u.
e = self.energy["Total Energy"] / 2625.5
G = -self.system.forces / 2625.5 / 1.88973
self._frame += 1
return e, G[opt_indices, :]
# Define Geometric engine.
class CustomEngine(geometric.engine.Engine):
def __init__(self, molecule):
super(CustomEngine, self).__init__(molecule)
def calc_new(self, coords, dirname):
energy, gradient = model(coords.reshape(-1, 3))
return {'energy': energy, 'gradient': gradient.ravel()}
# Instantiate optimizer.
molecule = geometric.molecule.Molecule()
molecule.elem = self.system.elements[np.ix_(opt_indices)].tolist()
molecule.xyzs = [self.system.positions[opt_indices, :]]
customengine = CustomEngine(molecule)
# Run the optimizer.
fd, tmp_path = tempfile.mkstemp(suffix=".tmp", text=True)
os.close(fd)
try:
m = geometric.optimize.run_optimizer(
customengine=customengine,
check=1,
input=tmp_path,
)
finally:
os.remove(tmp_path)
# Set positions ot the optimized positions.
self.system.positions[opt_indices, :] = m.xyzs[-1]
[docs]
def calculate_energy_forces(self) -> None:
"""Update total system energy and forces on atoms in the system.
"""
unwrapped_positions = self.system.positions.base + self._offset
logger.info(
"",
extra={"frame": self._frame,
"positions": unwrapped_positions,
"box": self.system.box.base},
)
temp = self.system.positions.base.copy()
results = self.calculator.calculate()
self.system.forces = results.forces
energy = {
"Total Energy": results.energy,
".": results.components,
}
self.energy = energy
self._offset += temp - self.system.positions.base
logger.info("", extra={"frame": self._frame, "energy": self.energy})
def _register_plugins(self, plugins: list[CalculatorPlugin]) -> None:
"""Dynamically load plugins for calculators.
Args:
plugins: A list of plugins to apply to the calculator
object before optimization.
"""
for plugin in plugins:
if isinstance(plugin, CalculatorPlugin):
self.calculator.register_plugin(plugin)
else:
raise TypeError