Source code for bonafide.features.morfeus_local_force
"""Local force constant features from ``MORFEUS``."""
import logging
import pickle
from typing import Optional, Tuple
from morfeus import LocalForce
from bonafide.utils.base_featurizer import BaseFeaturizer
from bonafide.utils.constants import ELECTRONIC_STRUCTURE_DATA_FILE_EXTENSIONS_MORFEUS_LOCAL_FORCE
from bonafide.utils.helper_functions import get_function_or_method_name
[docs]
class _Morfeus3DBondLocalForce(BaseFeaturizer):
"""Parent feature factory for the 3D bond MORFEUS local force features.
For details, please refer to the MORFEUS documentation
(https://digital-chemistry-laboratory.github.io/morfeus/index.html, last accessed on
09.09.2025).
"""
local_force_: LocalForce
electronic_struc_type_n: str
imag_cutoff: float
method: str
project_imag: bool
save_hessian: bool
def __init__(self) -> None:
self.extraction_mode = "multi"
super().__init__()
[docs]
def calculate(self) -> None:
"""Calculate the ``morfeus3D-bond-local_force_constant`` and
``morfeus3D-bond-local_frequency`` feature.
"""
# Initial logging because frequency data is required
_namespace = self.conformer_name[::-1].split("__", 1)[-1][::-1]
_loc = f"{self.__class__.__name__}.{get_function_or_method_name()}"
logging.warning(
f"'{_namespace}' | {_loc}()\nThe calculation of the '{self.feature_name}' feature "
"requires the presence of data from a frequency calculation in the respective "
"electronic structure data file (*.log, *.fchk, or *.hessian). Ensure that the "
"provided file contains this data."
)
# Determine program and file type
program, file_type = self._get_program_filetype()
if program is None:
return
# Initialize local force constant calculation
assert self.coordinates is not None # for type checker
assert isinstance(self.electronic_struc_n, str) # for type checker
assert isinstance(file_type, str) # for type checker
self.local_force_ = LocalForce(elements=self.elements, coordinates=self.coordinates)
self.local_force_.load_file(
file=self.electronic_struc_n, program=program, filetype=file_type
)
# Get normal mode analysis if Hessian is provided
if file_type == "hessian":
self.local_force_.normal_mode_analysis(save_hessian=self.save_hessian)
# Add all bonds of the molecule as internal coordinates (if coords are already present,
# this is not harmful)
for bond in self.mol.GetBonds():
self.local_force_.add_internal_coordinate(
atoms=[bond.GetBeginAtomIdx() + 1, bond.GetEndAtomIdx() + 1]
)
# Compute desired data
if self.method == "local_modes":
self.local_force_.compute_local(project_imag=self.project_imag, cutoff=self.imag_cutoff)
if self.method == "compliance":
self.local_force_.compute_compliance()
self.local_force_.compute_frequencies()
# Save the values to the results dictionary
self._save_values()
# Save data
with open(f"Morfeus3DBondLocalForce__{self.conformer_name}.pkl", "wb") as f:
pickle.dump(self.local_force_, f)
[docs]
def _get_program_filetype(self) -> Tuple[Optional[str], Optional[str]]:
"""Determine the program and file type based on the electronic structure file extension.
Returns
-------
Tuple[Optional[str], Optional[str]]
A tuple containing the program name and file type. If the file type is invalid,
returns (``None``, ``None``).
"""
# Check if valid file was provided
if (
self.electronic_struc_type_n
not in ELECTRONIC_STRUCTURE_DATA_FILE_EXTENSIONS_MORFEUS_LOCAL_FORCE
):
self._err = (
f"Invalid electronic structure data file type: "
f"'{self.electronic_struc_type_n}'. Supported types are: "
f"{ELECTRONIC_STRUCTURE_DATA_FILE_EXTENSIONS_MORFEUS_LOCAL_FORCE}"
)
return None, None
# Determine program and file type
if self.electronic_struc_type_n == "fchk":
return "gaussian", "fchk"
elif self.electronic_struc_type_n == "log":
return "gaussian", "log"
return "xtb", "hessian"
[docs]
def _save_values(self) -> None:
"""Loop over all bonds and get the local force constant and frequency.
Returns
-------
None
"""
for bond in self.mol.GetBonds():
_atom1_idx = bond.GetBeginAtomIdx() + 1
_atom2_idx = bond.GetEndAtomIdx() + 1
force_constant = self.local_force_.get_local_force_constant(
atoms=[_atom1_idx, _atom2_idx]
)
local_frequency = self.local_force_.get_local_frequency(atoms=[_atom1_idx, _atom2_idx])
self.results[bond.GetIdx()] = {
"morfeus3D-bond-local_force_constant": float(force_constant),
"morfeus3D-bond-local_frequency": float(local_frequency),
}
[docs]
class Morfeus3DBondLocalForceConstant(_Morfeus3DBondLocalForce):
"""Feature factory for the 3D bond feature "local_force_constant", calculated with morfeus.
The index of this feature is 198 (see the ``list_atom_features()`` and
``list_bond_features()`` method). The corresponding configuration settings can be found
under "morfeus.local_force" in the _feature_config.toml file.
"""
def __init__(self) -> None:
super().__init__()
# This feature is automatically calculated in _Morfeus3DBondLocalForce
[docs]
class Morfeus3DBondLocalFrequency(_Morfeus3DBondLocalForce):
"""Feature factory for the 3D bond feature "local_frequency", calculated with morfeus.
The index of this feature is 199 (see the ``list_atom_features()`` and
``list_bond_features()`` method). The corresponding configuration settings can be found
under "morfeus.local_force" in the _feature_config.toml file.
"""
def __init__(self) -> None:
super().__init__()
# This feature is automatically calculated in _Morfeus3DBondLocalForce