Source code for qsarkit.representation.descriptors._base
"""Shared plumbing for dense, named scalar molecular-descriptor blocks.
Every descriptor transformer in :mod:`qsarkit.representation.descriptors` is
a stateless :class:`~qsarkit.base.MoleculeTransformer` built from a list of
``(name, function)`` pairs, where each ``function`` maps one RDKit ``Mol`` to
a single float. Batching, ``None``-molecule handling, per-descriptor failure
handling and ``get_feature_names_out`` are provided here so every concrete
descriptor class is NaN-safe by construction: many RDKit descriptor
functions raise (rather than return NaN) on unusual valences, disconnected
fragments or zero-heavy-atom molecules, and a batch calculator must not let
one such molecule abort the whole design matrix.
References
----------
- RDKit documentation, "List of Available Descriptors":
https://www.rdkit.org/docs/GettingStartedInPython.html#list-of-available-descriptors
- Todeschini, R. & Consonni, V. (2009). "Molecular Descriptors for
Chemoinformatics." Wiley-VCH. https://doi.org/10.1002/9783527628766
"""
from __future__ import annotations
from abc import abstractmethod
from typing import TYPE_CHECKING, Callable, List, Optional, Sequence, Tuple
import numpy as np
import numpy.typing as npt
from qsarkit.base import MoleculeTransformer
if TYPE_CHECKING: # pragma: no cover
from rdkit.Chem import Mol
__all__ = ["BaseDescriptorTransformer"]
[docs]
class BaseDescriptorTransformer(MoleculeTransformer):
"""Abstract base for dense, named scalar molecular-descriptor blocks.
Notes
-----
``missing_value`` (a constructor argument on every concrete subclass,
following the sklearn convention of storing constructor arguments
verbatim) is substituted whenever a descriptor function raises or
returns a non-finite value, and for every column of a ``None`` input
molecule -- keeping the output positionally aligned with the input, as
elsewhere in :mod:`qsarkit.representation`.
Catching a bare ``Exception`` around each descriptor call is a
deliberate exception to the "no defensive try/except around internal
calls" rule: the functions dispatched here are heterogeneous,
third-party (RDKit) callables applied to arbitrary user molecules, and
a handful of them (e.g. ``Ipc`` on large fused-ring systems, most 3-D
descriptors on disconnected inputs) are documented to raise rather than
return NaN. This is exactly the kind of untrusted-input boundary the
project style guide carves out for explicit error handling.
References
----------
- RDKit documentation, "List of Available Descriptors":
https://www.rdkit.org/docs/GettingStartedInPython.html#list-of-available-descriptors
"""
#: Value substituted for a descriptor that raised or was non-finite.
#: Every concrete subclass declares this as a constructor argument
#: (default ``nan``) and stores it verbatim, per the sklearn estimator
#: convention; declared here so base-class methods can reference it.
missing_value: float
@abstractmethod
def _descriptor_functions(self) -> List[Tuple[str, Callable[["Mol"], float]]]:
"""Return the ``(name, function)`` pairs computed by this block."""
def _compute_row(
self,
mol: "Mol",
functions: Sequence[Tuple[str, Callable[["Mol"], float]]],
) -> npt.NDArray[np.float64]:
"""Evaluate every descriptor function on ``mol``, NaN-safe."""
row = np.full(len(functions), self.missing_value, dtype=np.float64)
for j, (_, fn) in enumerate(functions):
try:
value = float(fn(mol))
except Exception: # noqa: BLE001 - heterogeneous third-party callables
continue
row[j] = value if np.isfinite(value) else self.missing_value
return row
def _transform(self, mols: List[Optional["Mol"]]) -> npt.NDArray[np.float64]:
functions = self._descriptor_functions()
out = np.full(
(len(mols), len(functions)),
self.missing_value,
dtype=np.float64,
)
for i, mol in enumerate(mols):
if mol is None:
continue
out[i] = self._compute_row(mol, functions)
return out
[docs]
def get_feature_names_out(
self, input_features: Optional[Sequence[str]] = None
) -> npt.NDArray[np.object_]:
"""Return the descriptor names produced by this transformer.
Parameters
----------
input_features : sequence of str, optional
Ignored; present for scikit-learn API compatibility.
Returns
-------
numpy.ndarray
Array of ``str`` names, one per output column.
Examples
--------
>>> from qsarkit.representation.descriptors import LipinskiDescriptors
>>> "MolWt" in LipinskiDescriptors().get_feature_names_out()
True
"""
return np.asarray(
[name for name, _ in self._descriptor_functions()], dtype=object
)