Source code for bonafide.features.morfeus_bv

"""Buried volume features from ``MORFEUS``."""

from __future__ import annotations

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

import numpy as np
from morfeus import BuriedVolume

from bonafide.utils.base_featurizer import BaseFeaturizer

if TYPE_CHECKING:
    from numpy.typing import NDArray


[docs] class _Morfeus3DAtomBV(BaseFeaturizer): """Parent feature factory for the 3D atom MORFEUS buried volume features. For details, please refer to the MORFEUS documentation (https://digital-chemistry-laboratory.github.io/morfeus/index.html, last accessed on 09.09.2025). """ bv_: BuriedVolume density: float distal_volume_method: str distal_volume_sasa_density: float excluded_atoms: Optional[List[int]] include_hs: bool radii: Optional[Union[List[float], NDArray[np.float64]]] radii_scale: float radii_type: str radius: float xz_plane_atoms: Optional[List[int]] z_axis_atoms: Optional[List[int]] def __init__(self) -> None: self.extraction_mode = "single" super().__init__()
[docs] def _run_morfeus(self, do_octant_analysis: bool = False) -> bool: """Run MORFEUS and populate the buried volume attribute (``bv_``). Parameters ---------- do_octant_analysis : bool, optional Whether to calculate the decomposition of the buried-volume features in octants and quadrants, by default ``False``. 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.z_axis_atoms == []: self.z_axis_atoms = None else: assert self.z_axis_atoms is not None # for type checker self.z_axis_atoms = self._validate_atom_indices( atom_indices_list=self.z_axis_atoms, parameter_name="z_axis_atoms", all_indices=_atom_indices, ) if self.z_axis_atoms is None: return False if self.xz_plane_atoms == []: self.xz_plane_atoms = None else: assert self.xz_plane_atoms is not None # for type checker self.xz_plane_atoms = self._validate_atom_indices( atom_indices_list=self.xz_plane_atoms, parameter_name="xz_plane_atoms", all_indices=_atom_indices, ) if self.xz_plane_atoms is None: return False # Run morfeus assert self.coordinates is not None # for type checker self.bv_ = BuriedVolume( elements=self.elements, coordinates=self.coordinates, metal_index=self.atom_bond_idx + 1, # morfeus is 1-indexed excluded_atoms=self.excluded_atoms, radii=self.radii, include_hs=self.include_hs, radius=self.radius, radii_type=self.radii_type, radii_scale=self.radii_scale, density=self.density, z_axis_atoms=self.z_axis_atoms, xz_plane_atoms=self.xz_plane_atoms, ) # Perform octant analysis if requested if do_octant_analysis is True: self.bv_.octant_analysis() # Save data with open(f"{self.__class__.__name__}__{self.conformer_name}.pkl", "wb") as f: pickle.dump(self.bv_, 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 Morfeus3DAtomBuriedVolume(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "buried_volume", calculated with morfeus. The index of this feature is 167 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-buried_volume`` feature.""" _save_data = self._run_morfeus() if _save_data is True: self.results[self.atom_bond_idx] = {self.feature_name: self.bv_.buried_volume}
[docs] class Morfeus3DAtomBuriedVolumeOctants(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "buried_volume_octants", calculated with morfeus. The index of this feature is 168 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-buried_volume_octants`` feature.""" _save_data = self._run_morfeus(do_octant_analysis=True) if _save_data is True: val = ",".join([str(v) for v in self.bv_.octants["buried_volume"].values()]) self.results[self.atom_bond_idx] = {self.feature_name: val}
[docs] class Morfeus3DAtomBuriedVolumeQuadrants(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "buried_volume_quadrants", calculated with morfeus. The index of this feature is 169 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-buried_volume_quadrants`` feature.""" _save_data = self._run_morfeus(do_octant_analysis=True) if _save_data is True: val = ",".join([str(v) for v in self.bv_.quadrants["buried_volume"].values()]) self.results[self.atom_bond_idx] = {self.feature_name: val}
[docs] class Morfeus3DAtomDistalVolume(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "distal_volume", calculated with morfeus. The index of this feature is 174 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-distal_volume`` feature.""" _save_data = self._run_morfeus() if _save_data is True: self.bv_.compute_distal_volume( method=self.distal_volume_method, octants=False, sasa_density=self.distal_volume_sasa_density, ) val = float(self.bv_.distal_volume) if val < 0: val = 0.0 self.results[self.atom_bond_idx] = {self.feature_name: val} # Save data with open(f"{self.__class__.__name__}__{self.conformer_name}.pkl", "wb") as f: pickle.dump(self.bv_, f)
[docs] class Morfeus3DAtomDistalVolumeOctants(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "distal_volume_octants", calculated with morfeus. The index of this feature is 175 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-distal_volume_octants`` feature.""" _save_data = self._run_morfeus(do_octant_analysis=True) if _save_data is True: self.bv_.compute_distal_volume( method="buried_volume", octants=True, sasa_density=self.distal_volume_sasa_density, ) vals = [0.0 if v < 0 else v for v in self.bv_.octants["distal_volume"].values()] val = ",".join([str(v) for v in vals]) self.results[self.atom_bond_idx] = {self.feature_name: val} # Save data with open(f"{self.__class__.__name__}__{self.conformer_name}.pkl", "wb") as f: pickle.dump(self.bv_, f)
[docs] class Morfeus3DAtomDistalVolumeQuadrants(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "distal_volume_quadrants", calculated with morfeus. The index of this feature is 176 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-distal_volume_quadrants`` feature.""" _save_data = self._run_morfeus(do_octant_analysis=True) if _save_data is True: self.bv_.compute_distal_volume( method="buried_volume", octants=True, sasa_density=self.distal_volume_sasa_density, ) vals = [0.0 if v < 0 else v for v in self.bv_.quadrants["distal_volume"].values()] val = ",".join([str(v) for v in vals]) self.results[self.atom_bond_idx] = {self.feature_name: val} # Save data with open(f"{self.__class__.__name__}__{self.conformer_name}.pkl", "wb") as f: pickle.dump(self.bv_, f)
[docs] class Morfeus3DAtomFractionBuriedVolume(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "fraction_buried_volume", calculated with morfeus. The index of this feature is 177 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-fraction_buried_volume`` feature.""" _save_data = self._run_morfeus() if _save_data is True: self.results[self.atom_bond_idx] = {self.feature_name: self.bv_.fraction_buried_volume}
[docs] class Morfeus3DAtomFractionBuriedVolumeOctants(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "fraction_buried_volume_octants", calculated with morfeus. The index of this feature is 178 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-fraction_buried_volume_octants`` feature.""" _save_data = self._run_morfeus(do_octant_analysis=True) if _save_data is True: val = ",".join( [str(v / 100) for v in self.bv_.octants["percent_buried_volume"].values()] ) self.results[self.atom_bond_idx] = {self.feature_name: val}
[docs] class Morfeus3DAtomFractionBuriedVolumeQuadrants(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "fraction_buried_volume_quadrants", calculated with morfeus. The index of this feature is 179 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-fraction_buried_volume_quadrants`` feature.""" _save_data = self._run_morfeus(do_octant_analysis=True) if _save_data is True: val = ",".join( [str(v / 100) for v in self.bv_.quadrants["percent_buried_volume"].values()] ) self.results[self.atom_bond_idx] = {self.feature_name: val}
[docs] class Morfeus3DAtomFractionFreeVolume(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "fraction_free_volume", calculated with morfeus. The index of this feature is 180 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-fraction_free_volume`` feature.""" _save_data = self._run_morfeus() if _save_data is True: self.results[self.atom_bond_idx] = { self.feature_name: 1 - self.bv_.fraction_buried_volume }
[docs] class Morfeus3DAtomFractionFreeVolumeOctants(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "fraction_free_volume_octants", calculated with morfeus. The index of this feature is 181 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-fraction_free_volume_octants`` feature.""" _save_data = self._run_morfeus(do_octant_analysis=True) if _save_data is True: val = ",".join( [str(1 - v / 100) for v in self.bv_.octants["percent_buried_volume"].values()] ) self.results[self.atom_bond_idx] = {self.feature_name: val}
[docs] class Morfeus3DAtomFractionFreeVolumeQuadrants(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "fraction_free_volume_quadrants", calculated with morfeus. The index of this feature is 182 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-fraction_free_volume_quadrants`` feature.""" _save_data = self._run_morfeus(do_octant_analysis=True) if _save_data is True: val = ",".join( [str(1 - v / 100) for v in self.bv_.quadrants["percent_buried_volume"].values()] ) self.results[self.atom_bond_idx] = {self.feature_name: val}
[docs] class Morfeus3DAtomFreeVolume(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "free_volume", calculated with morfeus. The index of this feature is 183 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-free_volume`` feature.""" _save_data = self._run_morfeus() if _save_data is True: self.results[self.atom_bond_idx] = {self.feature_name: self.bv_.free_volume}
[docs] class Morfeus3DAtomFreeVolumeOctants(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "free_volume_octants", calculated with morfeus. The index of this feature is 184 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-free_volume_octants`` feature.""" _save_data = self._run_morfeus(do_octant_analysis=True) if _save_data is True: val = ",".join([str(v) for v in self.bv_.octants["free_volume"].values()]) self.results[self.atom_bond_idx] = {self.feature_name: val}
[docs] class Morfeus3DAtomFreeVolumeQuadrants(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "free_volume_quadrants", calculated with morfeus. The index of this feature is 185 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-free_volume_quadrants`` feature.""" _save_data = self._run_morfeus(do_octant_analysis=True) if _save_data is True: val = ",".join([str(v) for v in self.bv_.quadrants["free_volume"].values()]) self.results[self.atom_bond_idx] = {self.feature_name: val}
[docs] class Morfeus3DAtomMolecularVolumeOctants(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "molecular_volume_octants", calculated with morfeus. The index of this feature is 186 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-molecular_volume_octants`` feature.""" _save_data = self._run_morfeus(do_octant_analysis=True) if _save_data is True: self.bv_.compute_distal_volume( method="buried_volume", octants=True, sasa_density=self.distal_volume_sasa_density, ) val = ",".join([str(v) for v in self.bv_.octants["molecular_volume"].values()]) self.results[self.atom_bond_idx] = {self.feature_name: val} # Save data with open(f"{self.__class__.__name__}__{self.conformer_name}.pkl", "wb") as f: pickle.dump(self.bv_, f)
[docs] class Morfeus3DAtomMolecularVolumeQuadrants(_Morfeus3DAtomBV): """Feature factory for the 3D atom feature "molecular_volume_quadrants", calculated with morfeus. The index of this feature is 187 (see the ``list_atom_features()`` and ``list_bond_features()`` method). The corresponding configuration settings can be found under "morfeus.buried_volume" in the _feature_config.toml file. """ def __init__(self) -> None: super().__init__()
[docs] def calculate(self) -> None: """Calculate the ``morfeus3D-atom-molecular_volume_quadrants`` feature.""" _save_data = self._run_morfeus(do_octant_analysis=True) if _save_data is True: self.bv_.compute_distal_volume( method="buried_volume", octants=True, sasa_density=self.distal_volume_sasa_density, ) val = ",".join([str(v) for v in self.bv_.quadrants["molecular_volume"].values()]) self.results[self.atom_bond_idx] = {self.feature_name: val} # Save data with open(f"{self.__class__.__name__}__{self.conformer_name}.pkl", "wb") as f: pickle.dump(self.bv_, f)