Source code for pydft_qmmm.plugins.atom_partition.atom_partition

"""A plugin for performing atom-by-atom system partitioning.
"""
from __future__ import annotations

from typing import Callable
from typing import TYPE_CHECKING

import numpy as np

from pydft_qmmm.common import Subsystem
from pydft_qmmm.plugins.plugin import PartitionPlugin

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 AtomPartition(PartitionPlugin): """Partition subsystems atom-by-atom. Args: query: The VMD-like query representing the group of atoms whose subsystem membership will be determined on an atom-by-atom basis. """ def __init__( self, query: str, ) -> None: self._query = query
[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 self.cutoff = calculator.cutoff calculator.calculate = self._modify_calculate( calculator.calculate, )
def _modify_calculate( self, calculate: CalculateMethod, ) -> CalculateMethod: """Modify the calculate routine to perform atom-wise partitioning. Args: calculate: The calculation routine to modify. Returns: The modified calculation routine which implements atom-wise partitioning before calculation. """ def inner( return_forces: bool | None = True, return_components: bool | None = True, ) -> Results: self.generate_partition() results = calculate(return_forces, return_components) return results return inner
[docs] def generate_partition(self) -> None: """Perform the atom-wise system partitioning. """ qm_region = sorted(self.system.select("subsystem I")) qm_centroid = np.average( self.system.positions[qm_region, :], axis=0, ) region_ii: list[int] = [] selection = self.system.select(self._query) for atom in range(len(self.system)): atoms = sorted(frozenset((atom,)) & selection) not_qm = set(qm_region).isdisjoint({atoms}) if not_qm and atoms: nth_centroid = np.average( self.system.positions[atoms, :], axis=0, ) r_vector = nth_centroid - qm_centroid distance = np.sum(r_vector**2)**0.5 if distance < self.cutoff: region_ii.extend(atoms) # Update the topology with the current embedding atoms. temp = [Subsystem.III]*len(self.system) temp = [ Subsystem.II if i in region_ii else x for i, x in enumerate(temp) ] temp = [ Subsystem.I if i in qm_region else x for i, x in enumerate(temp) ] self.system.subsystems = np.array(temp)