Source code for bonafide.utils.base_single_point

"""Base class for single-point energy calculations with different computational engines."""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any, List, Optional, Tuple

from bonafide.utils.base_mixin import _BaseMixin
from bonafide.utils.helper_functions import get_function_or_method_name

if TYPE_CHECKING:
    import numpy as np
    from numpy.typing import NDArray

    from bonafide.utils.molecule_vault import MolVault


[docs] class BaseSinglePoint(_BaseMixin): """Run single-point energy calculations with different computational engines. All conformers in the molecule vault are processed sequentially. Attributes ---------- _keep_output_files : bool If ``True``, all output files created during the feature calculations are kept. If ``False``, they are removed when the calculation is done. charge : int The total charge of the molecule. conformer_name : str The name of the conformer. coordinates : NDArray[np.float64] The cartesian coordinates of the conformer. elements : NDArray[np.str\_] The element symbols of the molecule. engine_name : str The name of the computational engine (must be set in the child class). mol_vault : MolVault The dataclass for storing all relevant data on the molecule. multiplicity : int The spin multiplicity of the molecule. """ _keep_output_files: bool charge: int conformer_name: str coordinates: NDArray[np.float64] elements: NDArray[np.str_] engine_name: str method: str mol_vault: MolVault multiplicity: int solvent: str state: str def __init__(self, **kwargs: Any) -> None: # Set all attributes required for the single-point energy calculation for attr_name, value in kwargs.items(): setattr(self, attr_name, value) # Check if single-point energy class is correctly implemented self._check_requirements()
[docs] def _check_requirements(self) -> None: """Check if the respective single-point energy class (child class) implements the ``calculate()`` method and sets the ``engine_name`` attribute. Returns ------- None """ _loc = f"{self.__class__.__name__}.{get_function_or_method_name()}" # Check if child class has mandatory calculate() method method_names = [ attr for attr in dir(self) if callable(getattr(self, attr)) is True and not attr.startswith("__") ] if "calculate" not in method_names: _errmsg = ( "calculate() method must be implemented in engine-specific single-point " f"energy class '{self.__class__.__name__}'." ) logging.error(f"'None' | {_loc}()\n{_errmsg}") raise NotImplementedError(_errmsg) # Check if child class sets engine_name attribute if "engine_name" not in vars(self): _errmsg = ( "Attribute 'engine_name' must be set in engine-specific single-point energy class " f"'{self.__class__.__name__}'." ) logging.error(f"'None' | {_loc}()\n{_errmsg}") raise AttributeError(f"{_loc}(): {_errmsg}")
[docs] def run( self, state: str, write_el_struc_file: bool = True ) -> Tuple[List[Tuple[Optional[float], str]], List[Optional[str]]]: """Run a single-point energy calculation for all conformers of the molecule in the molecule vault. Parameters ---------- state : str The redox state of the molecule to consider, either "n", "n+1", or "n-1". write_el_struc_file : bool, optional Whether to write the calculated electronic structure of the molecule to an electronic structure data file, by default ``True``. Returns ------- Tuple[List[Tuple[Optional[float], str]], List[Optional[str]]] A tuple containing the data for each conformer: * A list of tuples with the electronic energy in kJ/mol (value, unit pair). In case the calculation failed, the energy is ``None``. * A list of paths to the electronic structure data files. If they were not requested, the paths are ``None``. """ _loc = f"{self.__class__.__name__}.{get_function_or_method_name()}" # Define additional attributes # It is ensured that the molecule vault contains the required data for the single-point # energy calculation. self.elements = self.mol_vault.elements # type: ignore[assignment] self.charge = self.mol_vault.charge # type: ignore[assignment] self.multiplicity = self.mol_vault.multiplicity # type: ignore[assignment] energies = [] electronic_strucs = [] # Loop over all conformers in the molecule vault for conf_idx, mol in enumerate(self.mol_vault.mol_objects): # Setup up working directory and change to it self.conformer_name = self.mol_vault.conformer_names[conf_idx] _namespace = self.conformer_name[::-1].split("__", 1)[-1][::-1] self._setup_work_dir() # Initialize results as None energy = None molden_file_path = None # Skip conformers that were labeled as invalid if self.mol_vault.is_valid[conf_idx] is False and state == "n": logging.warning( f"'{_namespace}' | {_loc}()\nSkipping conformer with index {conf_idx} for " f"single-point energy calculation for state '{state}' with {self.engine_name} " "because it is invalid." ) else: logging.info( ( f"'{_namespace}' | {_loc}()\nRunning {self.engine_name} single " f"point-energy calculation for conformer with index {conf_idx} for " f"state '{state}'." ) ) self.coordinates = mol.GetConformer(0).GetPositions() # Try to run the calculation try: # self._check_requirements() ensures that the child class implements the # calculate() method; mypy does not recognize this, so we ignore the type # error here energy, molden_file_path = self.calculate( # type: ignore[attr-defined] write_el_struc_file=write_el_struc_file ) except Exception as e: _errmsg = ( f"Single-point energy calculation with {self.engine_name} failed for " f"conformer with index {conf_idx} for state '{state}': {str(e)}." ) if _errmsg.endswith(".."): _errmsg = _errmsg[:-1] logging.error(f"'{_namespace}' | {_loc}()\n{_errmsg}") # Collect and store data energies.append((energy, "kj_mol")) electronic_strucs.append(molden_file_path) self._save_output_files() return (energies, electronic_strucs)