Source code for bonafide.features.morfeus_dispersion

"""Dispersion features from ``MORFEUS``."""

from __future__ import annotations

import pickle
from typing import TYPE_CHECKING, List, Optional, Union

import numpy as np
from morfeus import Dispersion

from bonafide.utils.base_featurizer import BaseFeaturizer

if TYPE_CHECKING:
    from numpy.typing import NDArray


[docs] class _Morfeus3DAtomDispersion(BaseFeaturizer): """Parent feature factory for the 3D atom MORFEUS dispersion features. For details, please refer to the MORFEUS documentation (https://digital-chemistry-laboratory.github.io/morfeus/index.html, last accessed on 09.09.2025). """ dispersion_: Dispersion density: float excluded_atoms: Optional[List[int]] included_atoms: Optional[List[int]] radii: Optional[Union[List[float], NDArray[np.float64]]] radii_type: str def __init__(self) -> None: self.extraction_mode = "multi" super().__init__()
[docs] def _run_morfeus(self) -> bool: """Run MORFEUS and populate the dispersion attribute (``dispersion_``). Returns ------- bool Whether MORFEUS ran (successfully). """ # Modify the user input if necessary to comply with MORFEUS requirements if self.radii == []: self.radii = None else: self.radii = np.array(self.radii) _atom_indices = list(range(len(self.elements))) if self.excluded_atoms == []: self.excluded_atoms = None else: assert self.excluded_atoms is not None # for type checker self.excluded_atoms = self._validate_atom_indices( atom_indices_list=self.excluded_atoms, parameter_name="excluded_atoms", all_indices=_atom_indices, ) if self.excluded_atoms is None: return False if self.included_atoms == []: self.included_atoms = None else: assert self.included_atoms is not None # for type checker self.included_atoms = self._validate_atom_indices( atom_indices_list=self.included_atoms, parameter_name="included_atoms", all_indices=_atom_indices, ) if self.included_atoms is None: return False # Run MORFEUS assert self.coordinates is not None # for type checker self.dispersion_ = Dispersion( elements=self.elements, coordinates=self.coordinates, radii=self.radii, radii_type=self.radii_type, density=self.density, excluded_atoms=self.excluded_atoms, included_atoms=self.included_atoms, ) # Save data with open(f"{self.__class__.__name__}__{self.conformer_name}.pkl", "wb") as f: pickle.dump(self.dispersion_, f) return True
[docs] def _validate_atom_indices( self, atom_indices_list: List[int], parameter_name: str, all_indices: List[int] ) -> Optional[List[int]]: """Validate user-provided atom indices. Parameters ---------- atom_indices_list : List[int] The list of atom indices to be validated. parameter_name : str The name of the parameter being validated (for error messages). all_indices : List[int] A list of all valid atom indices. Returns ------- Optional[List[int]] Returns the validated list of atom indices (converted to 1-indexed) or ``None`` if validation fails. """ for idx in atom_indices_list: if idx not in all_indices: self._err = ( f"Invalid input to '{parameter_name}': atom index {idx} is out of range." ) return None atom_indices_list = [idx + 1 for idx in atom_indices_list] # MORFEUS is 1-indexed return atom_indices_list
[docs] class Morfeus3DAtomPInt(_Morfeus3DAtomDispersion): """Feature factory for the 3D atom feature "p_int", calculated with morfeus. The index of this feature is 188 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.dispersion" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-p_int`` feature.""" _save_data = self._run_morfeus() if _save_data is True: for atom_idx, value in self.dispersion_.atom_p_int.items(): self.results[atom_idx - 1] = { self.feature_name: float(value) } # morfeus is 1-indexed
[docs] class Morfeus3DAtomPMax(_Morfeus3DAtomDispersion): """Feature factory for the 3D atom feature "p_max", calculated with morfeus. The index of this feature is 189 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.dispersion" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-p_max`` feature.""" _save_data = self._run_morfeus() if _save_data is True: for atom_idx, value in self.dispersion_.atom_p_max.items(): self.results[atom_idx - 1] = { self.feature_name: float(value) } # morfeus is 1-indexed
[docs] class Morfeus3DAtomPMin(_Morfeus3DAtomDispersion): """Feature factory for the 3D atom feature "p_min", calculated with morfeus. The index of this feature is 190 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.dispersion" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-p_min`` feature.""" _save_data = self._run_morfeus() if _save_data is True: for atom_idx, value in self.dispersion_.atom_p_min.items(): self.results[atom_idx - 1] = { self.feature_name: float(value) } # morfeus is 1-indexed