Source code for qsarkit.representation.fingerprints._base

"""Shared plumbing for the RDKit-backed fingerprint transformers.

Every fingerprint in :mod:`qsarkit.representation.fingerprints` is a
stateless :class:`~qsarkit.base.MoleculeTransformer`: it consumes
``Iterable[rdkit.Chem.Mol]`` and returns a dense ``numpy.ndarray`` of shape
``(n_molecules, n_features_out)``. Subclasses only have to say how wide they
are (:attr:`BaseFingerprintTransformer.n_features_out`) and how to encode a
single molecule (``_fingerprint``); batching, ``None`` handling, dtype
selection and ``get_feature_names_out`` are provided here.

References
----------
- Pedregosa et al. (2011). "Scikit-learn: Machine Learning in Python."
  Journal of Machine Learning Research, 12, 2825-2830.
  https://jmlr.org/papers/v12/pedregosa11a.html
- RDKit fingerprint documentation:
  https://www.rdkit.org/docs/GettingStartedInPython.html#fingerprinting-and-molecular-similarity
"""

from __future__ import annotations

from abc import abstractmethod
from typing import TYPE_CHECKING, List, Optional, Sequence

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__ = ["BaseFingerprintTransformer", "fold_on_bits"]


[docs] class BaseFingerprintTransformer(MoleculeTransformer): """Abstract base for dense, fixed-width molecular fingerprints. Notes ----- ``None`` entries in the input (molecules that failed an earlier parsing or standardization step) are encoded as an all-zero row so that the output stays positionally aligned with the input, mirroring the convention used by :class:`qsarkit.chemistry.MolecularStandardizer`. Output arrays are always ``float64`` so that fingerprints compose directly with downstream scikit-learn estimators/scalers without an implicit cast; the intermediate RDKit bit/count vectors are generated in their natural ``uint8``/``uint32`` dtype for efficiency and cast on assignment into the output matrix. References ---------- - RDKit documentation, "Fingerprinting and Molecular Similarity": https://www.rdkit.org/docs/GettingStartedInPython.html#fingerprinting-and-molecular-similarity - scikit-learn transformer API: https://scikit-learn.org/stable/developers/develop.html """ #: Prefix used to build feature names (overridden by subclasses). _feature_prefix: str = "fp" @property @abstractmethod def n_features_out(self) -> int: """Width of the produced feature matrix.""" @abstractmethod def _fingerprint(self, mol: "Mol") -> npt.NDArray[np.float64]: """Encode one RDKit ``Mol`` as a 1-D array of length ``n_features_out``.""" def _transform(self, mols: List[Optional["Mol"]]) -> npt.NDArray[np.float64]: width = self.n_features_out out = np.zeros((len(mols), width), dtype=np.float64) for i, mol in enumerate(mols): if mol is None: continue out[i] = self._fingerprint(mol) return out
[docs] def get_feature_names_out( self, input_features: Optional[Sequence[str]] = None ) -> npt.NDArray[np.object_]: """Return ``n_features_out`` bit/count names. 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.fingerprints import MACCSKeysFingerprint >>> len(MACCSKeysFingerprint().get_feature_names_out()) 167 """ return np.asarray( [f"{self._feature_prefix}_{i}" for i in range(self.n_features_out)], dtype=object, )
[docs] def fold_on_bits(on_bits: Sequence[int], n_bits: int) -> npt.NDArray[np.float64]: """Fold a sparse list of set bit indices into a dense ``n_bits`` vector. Parameters ---------- on_bits : sequence of int Indices of the set bits in a (possibly very large) sparse bit vector. n_bits : int Width of the folded output. Returns ------- numpy.ndarray ``float64`` array of length ``n_bits`` holding 0/1 values. Notes ----- Modulo folding is the standard RDKit approach for reducing sparse fingerprints (e.g. the 39 972-bit Gobbi 2D pharmacophore keys) to a machine-learning friendly width; it trades bit collisions for a fixed, dense representation. Examples -------- Bits 0, 5 and 10 fold to 0, 1 and 2 modulo 4: >>> fold_on_bits([0, 5, 10], 4).tolist() [1.0, 1.0, 1.0, 0.0] Collisions are silent, which is the cost of folding -- bits 1 and 5 land on the same output position: >>> fold_on_bits([1, 5], 4).tolist() [0.0, 1.0, 0.0, 0.0] References ---------- - RDKit documentation, "Fingerprinting and Molecular Similarity": https://www.rdkit.org/docs/GettingStartedInPython.html#fingerprinting-and-molecular-similarity """ vector = np.zeros(n_bits, dtype=np.float64) for bit in on_bits: vector[bit % n_bits] = 1.0 return vector