Skip to content

Taxonomy

Offline ChEBI classification with the optional Chebifier ensemble. See Chebifier taxonomy for installation.

provesid.taxonomy

Chemical taxonomy classification backends for PROVESID.

This module provides high-level "give me chemical-class labels for these structures" capabilities on top of PROVESID's identifier resolution. The first backend implemented here is ChebifierClassifier, a wrapper around the offline, AI-based ChEB-AI chebifier ensemble that assigns ChEBI ontology classes to molecules.

chebifier is a heavy, optional dependency (it pulls in PyTorch and, for the graph models, part of the PyG stack). It is therefore not a core requirement of PROVESID; install it with::

bash scripts/install_chebifier.sh

which wraps pip install 'chebifier[models]' plus the torch_scatter wheel the graph models need from the PyG index.

See docs/guide/chebifier.md for the full installation story and known issues, and plans/2026-07-02-chemical-taxonomy-classyfire-chebifier.md (§10) for the design rationale.

Key design points (mirrors the PROVESID conventions):

  • Optional + lazily imported. Importing provesid.taxonomy never requires PyTorch. chebifier is imported only when a ChebifierClassifier actually needs it, and a missing install raises a clear ChebifierMissingError rather than a raw ModuleNotFoundError.
  • Systemwide model storage. Model weights are redirected to the shared per-user PROVESID dataset directory (provesid.utils.user_dataset_path, overridable with PROVESID_DATA_DIR) so a single copy is reused across all virtual environments on the machine, exactly like the other large PROVESID datasets.
  • InChIKey-keyed, resumable cache. Each structure is classified once and cached on disk (keyed by InChIKey + chebifier version + configuration), so a re-run over the same chemicals hits the cache and never reloads the model.
  • Self-healing checkpoint compatibility. chebifier's graph checkpoints require chebai-graph's property index vocabularies in a specific (older) state. chebifier[models]==1.2.2 pins a matching chebai-graph, and ensure_v244_indices restores the indices if a drifted version is installed over it.

Attributes

CHEBIFIER_PINNED_VERSION module-attribute

chebifier release this backend is validated against (see docs/guide/chebifier.md).

TAXONOMY_COLUMNS module-attribute

Columns of the tidy taxonomy table returned by classify (shared with the planned ClassyFire backend, hence the ClassyFire-only level columns).

Classes

ChebifierError

Bases: Exception

Base class for errors raised by the chebifier taxonomy backend.

Raised on its own for a bad ensemble configuration, such as an exclude_models name the ensemble does not have.

Examples:

>>> ChebifierClassifier(exclude_models=["no_such_model"]).ensemble
Traceback (most recent call last):
...
provesid.taxonomy.ChebifierError: exclude_models names not in the ensemble configuration: ['no_such_model']. ...
Source code in src/provesid/taxonomy.py
103
104
105
106
107
108
109
110
111
112
113
114
115
class ChebifierError(Exception):
    """
    Base class for errors raised by the chebifier taxonomy backend.

    Raised on its own for a bad ensemble configuration, such as an
    ``exclude_models`` name the ensemble does not have.

    Examples:
        >>> ChebifierClassifier(exclude_models=["no_such_model"]).ensemble  # doctest: +SKIP
        Traceback (most recent call last):
        ...
        provesid.taxonomy.ChebifierError: exclude_models names not in the ensemble configuration: ['no_such_model']. ...
    """

ChebifierMissingError

Bases: ChebifierError

Raised when the optional chebifier dependency is not installed.

Raised when the ensemble is first built, not on import or construction; check chebifier_available first to avoid it.

Examples:

>>> issubclass(ChebifierMissingError, ChebifierError)
True
Source code in src/provesid/taxonomy.py
118
119
120
121
122
123
124
125
126
127
128
129
class ChebifierMissingError(ChebifierError):
    """
    Raised when the optional ``chebifier`` dependency is not installed.

    Raised when the ensemble is first built, not on import or construction;
    check [`chebifier_available`][provesid.taxonomy.chebifier_available] first
    to avoid it.

    Examples:
        >>> issubclass(ChebifierMissingError, ChebifierError)
        True
    """

ChebifierClassifier

Classify molecules into ChEBI ontology classes with the chebifier ensemble.

Wraps the ChEB-AI chebifier ensemble behind PROVESID conventions: systemwide model storage, an InChIKey-keyed resumable cache, and a tidy pandas.DataFrame output. The (expensive) ensemble is constructed lazily on first use and reused for the lifetime of the instance.

Parameters:

Name Type Description Default
data_dir Optional[str]

Base directory for chebifier model storage. Defaults to user_dataset_path("chebifier") (honors PROVESID_DATA_DIR).

None
use_cache bool

When True (default), classified structures are cached on disk by InChIKey and reused on subsequent calls.

True
resolve_names bool

When True, resolve predicted ChEBI IDs to names via the online provesid.ChEBI client (cached). Defaults to False to keep classification fast and dependency-light.

False
model_configs Optional[Union[str, Dict[str, Any]]]

Optional custom chebifier ensemble configuration (path or dict) passed straight to BaseEnsemble. When None, chebifier's default ensemble is used.

None
patch_indices bool

When True (default), call ensure_v244_indices before loading the ensemble so the graph models load correctly.

True
with_scores bool

When True, populate the confidence column with the ensemble's smoothed net score for each predicted class instead of leaving it empty. See predict_with_scores.

False
exclude_models Optional[Sequence[str]]

Model names to drop from the ensemble configuration, e.g. ["electra_chebi50-3star_v244"] to build a fallback ensemble for structures that crash the transformer's tokenizer (see classify). Applied on top of model_configs.

None

Raises:

Type Description
ChebifierMissingError

If chebifier is not installed (raised when the ensemble is first constructed).

Examples:

>>> from provesid.taxonomy import ChebifierClassifier
>>> clf = ChebifierClassifier()
>>> df = clf.classify(["c1ccccc1", "CCO"])
>>> df["inchikey"].tolist()
['UHOVQNZJYSORNB-UHFFFAOYSA-N', 'LFQSCWFLJHTTHZ-UHFFFAOYSA-N']
>>> "30879" in df.loc[1, "chebi_ids"].split("|")
True

which takes the better part of a minute; later calls for the same structures come from the cache.

Source code in src/provesid/taxonomy.py
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
class ChebifierClassifier:
    """Classify molecules into ChEBI ontology classes with the chebifier ensemble.

    Wraps the ChEB-AI ``chebifier`` ensemble behind PROVESID conventions:
    systemwide model storage, an InChIKey-keyed resumable cache, and a tidy
    `pandas.DataFrame` output. The (expensive) ensemble is constructed
    lazily on first use and reused for the lifetime of the instance.

    Args:
        data_dir: Base directory for chebifier model storage. Defaults to
            ``user_dataset_path("chebifier")`` (honors ``PROVESID_DATA_DIR``).
        use_cache: When ``True`` (default), classified structures are cached on
            disk by InChIKey and reused on subsequent calls.
        resolve_names: When ``True``, resolve predicted ChEBI IDs to names via the
            online [`provesid.ChEBI`][provesid.chebi.ChEBI] client (cached).
            Defaults to ``False`` to keep classification fast and
            dependency-light.
        model_configs: Optional custom chebifier ensemble configuration (path or
            dict) passed straight to ``BaseEnsemble``. When ``None``, chebifier's
            default ensemble is used.
        patch_indices: When ``True`` (default), call
            [`ensure_v244_indices`][provesid.taxonomy.ensure_v244_indices]
            before loading the ensemble so the graph models load correctly.
        with_scores: When ``True``, populate the ``confidence`` column with the
            ensemble's smoothed net score for each predicted class instead of
            leaving it empty. See
            [`predict_with_scores`][provesid.taxonomy.ChebifierClassifier.predict_with_scores].
        exclude_models: Model names to drop from the ensemble configuration, e.g.
            ``["electra_chebi50-3star_v244"]`` to build a fallback ensemble for
            structures that crash the transformer's tokenizer (see
            [`classify`][provesid.taxonomy.ChebifierClassifier.classify]).
            Applied on top of ``model_configs``.

    Raises:
        ChebifierMissingError: If ``chebifier`` is not installed (raised when the
            ensemble is first constructed).

    Examples:
        >>> from provesid.taxonomy import ChebifierClassifier
        >>> clf = ChebifierClassifier()
        >>> df = clf.classify(["c1ccccc1", "CCO"])                 # doctest: +SKIP
        >>> df["inchikey"].tolist()                                # doctest: +SKIP
        ['UHOVQNZJYSORNB-UHFFFAOYSA-N', 'LFQSCWFLJHTTHZ-UHFFFAOYSA-N']
        >>> "30879" in df.loc[1, "chebi_ids"].split("|")           # doctest: +SKIP
        True

        CHEBI:30879 is "alcohol". The first call loads the whole ensemble,
        which takes the better part of a minute; later calls for the same
        structures come from the cache.
    """

    _CACHE_FUNC_NAME = "provesid.taxonomy.ChebifierClassifier"

    def __init__(
        self,
        data_dir: Optional[str] = None,
        use_cache: bool = True,
        resolve_names: bool = False,
        model_configs: Optional[Union[str, Dict[str, Any]]] = None,
        patch_indices: bool = True,
        with_scores: bool = False,
        exclude_models: Optional[Sequence[str]] = None,
    ) -> None:
        self.data_dir = _configure_chebifier_storage(data_dir)
        self.use_cache = use_cache
        self.resolve_names = resolve_names
        self.model_configs = model_configs
        self.patch_indices = patch_indices
        self.with_scores = with_scores
        self.exclude_models = list(exclude_models or ())
        self._ensemble = None
        self._chebi = None
        self._cache = get_service_cache("chebifier")

    # -- lazy resources ----------------------------------------------------
    @property
    def chebifier_version(self) -> str:
        """
        Installed chebifier version (falls back to the pinned version).

        Part of every cache key, so upgrading chebifier re-classifies.

        Examples:
            >>> ChebifierClassifier().chebifier_version            # doctest: +SKIP
            '1.2.2'
        """
        try:
            return importlib.import_module("chebifier").__version__
        except Exception:
            return CHEBIFIER_PINNED_VERSION

    @property
    def ensemble(self):
        """The lazily-constructed, reused ``BaseEnsemble`` instance.

        Note:
            The ensemble is built with
            [`data_dir`][provesid.taxonomy.ChebifierClassifier] as the working
            directory, because ``chemlog_extra`` resolves its element-class
            mapping files relative to the working directory (see
            [`ensure_element_class_mappings`][provesid.taxonomy.ensure_element_class_mappings]).
            The previous directory is always restored.

        Raises:
            ChebifierMissingError: If ``chebifier`` is not installed.

        Examples:
            >>> type(ChebifierClassifier().ensemble).__name__      # doctest: +SKIP
            'BaseEnsemble'
        """
        if self._ensemble is None:
            base_ensemble_cls = _load_chebifier()
            if self.patch_indices:
                ensure_v244_indices()
            ensure_element_class_mappings(self.data_dir)
            logger.info(
                "Loading chebifier ensemble (weights cache: %s). First run "
                "downloads model weights.",
                os.environ.get("HF_HOME", "<default>"),
            )
            with contextlib.chdir(self.data_dir):
                configs = self._resolve_model_configs()
                if configs is not None:
                    self._ensemble = base_ensemble_cls(model_configs=configs)
                else:
                    self._ensemble = base_ensemble_cls()
        return self._ensemble

    def _resolve_model_configs(self):
        """The ensemble configuration to build, after applying ``exclude_models``.

        Returns ``None`` (meaning "chebifier's default") when nothing has to be
        customised, so the common path stays byte-identical to upstream.
        """
        if not self.exclude_models:
            return self.model_configs
        if self.model_configs is None:
            from chebifier.utils import get_default_configs

            config = dict(get_default_configs())
        elif isinstance(self.model_configs, dict):
            config = dict(self.model_configs)
        else:
            import yaml

            with open(self.model_configs, "r") as fh:
                config = yaml.safe_load(fh)
        unknown = [m for m in self.exclude_models if m not in config]
        if unknown:
            raise ChebifierError(
                f"exclude_models names not in the ensemble configuration: {unknown}. "
                f"Available: {sorted(config)}"
            )
        for name in self.exclude_models:
            config.pop(name)
        if not config:
            raise ChebifierError("exclude_models removed every model from the ensemble")
        return config

    def _get_chebi(self):
        """Lazily construct an online ChEBI client for name resolution."""
        if self._chebi is None:
            from .chebi import ChEBI

            self._chebi = ChEBI()
        return self._chebi

    # -- caching helpers ---------------------------------------------------
    def _cache_key(self, inchikey: str) -> str:
        """Build the cache key for a structure (InChIKey + version + config).

        ``with_scores`` and ``exclude_models`` change what a prediction contains,
        so they are part of the key: a cached score-less entry must never be
        served to a caller that asked for scores, or a reduced-ensemble entry to
        a caller using the full ensemble.
        """
        config_sig = "default" if self.model_configs is None else "custom"
        if self.exclude_models:
            config_sig += "-x" + ",".join(sorted(self.exclude_models))
        if self.with_scores:
            config_sig += "+scores"
        return f"{inchikey}::{self.chebifier_version}::{config_sig}"

    @staticmethod
    def _normalize_prediction(prediction: Any) -> Dict[str, Optional[float]]:
        """Normalise one chebifier prediction into ``{chebi_id: confidence}``.

        chebifier's ``predict_smiles_list`` returns, per molecule, either a list
        of predicted ChEBI id strings or a mapping of id to score (depending on
        version/config); ``None`` for structures it could not classify. This
        collapses all shapes into an ordered ``{id: confidence}`` dict (with
        ``None`` confidence when the backend does not provide one).
        """
        if prediction is None:
            return {}
        if isinstance(prediction, dict):
            return {str(k): (float(v) if v is not None else None)
                    for k, v in prediction.items()}
        # list/tuple/set of ids
        return {str(cid): None for cid in prediction}

    def predict_with_scores(self, smiles_list: Sequence[str]) -> List[Optional[Dict[str, float]]]:
        """Predict ChEBI classes *and* the score behind each decision.

        ``BaseEnsemble.predict_smiles_list`` returns only the surviving class
        ids; the smoothed net score it thresholds at 0 to get them is computed
        and then discarded. This runs the same four steps
        (``gather_predictions`` -> ``consolidate_predictions`` ->
        ``smoother`` -> ``> 0``) and keeps the score, so callers get a per-label
        confidence for free -- the models are run exactly once, no differently
        and no more often than upstream does.

        Args:
            smiles_list: Structures to classify.

        Returns:
            One entry per input: a ``{chebi_id: smoothed_score}`` mapping, or
            ``None`` for a structure no model could classify (upstream's
            "complete failure", which ``predict_smiles_list`` also reports as
            ``None``).

        Note:
            Verified to reproduce ``predict_smiles_list``'s label sets exactly.
            The scores are the *smoothed* ones, i.e. the values the ontology
            consistency pass actually thresholds, so ``score > 0`` holds for
            every returned label. Not cached;
            [`classify`][provesid.taxonomy.ChebifierClassifier.classify] with
            ``with_scores=True`` is.

        Examples:
            >>> scores = ChebifierClassifier().predict_with_scores(["CCO"])[0]  # doctest: +SKIP
            >>> scores["30879"], round(scores["2571"], 3)                        # doctest: +SKIP
            (1.0, 0.866)
        """
        import torch

        ens = self.ensemble
        with contextlib.chdir(self.data_dir):
            logits, classes = ens.gather_predictions(list(smiles_list))
            class_idx = {cls: i for i, cls in enumerate(classes)}
            weights = ens.calculate_classwise_weights(class_idx)
            net_score, has_valid = ens.consolidate_predictions(logits, weights)
            if ens.smoother is not None:
                ens.smoother.set_label_names(list(classes))
                score = ens.smoother(net_score)
            else:
                score = net_score
            decisions = (score > 0) & has_valid
            failures = torch.all(~has_valid, dim=1)

        out: List[Optional[Dict[str, float]]] = []
        for i in range(len(smiles_list)):
            if bool(failures[i]):
                out.append(None)
                continue
            out.append({
                classes[j]: float(score[i, j])
                for j in torch.nonzero(decisions[i], as_tuple=True)[0].tolist()
            })
        return out

    # -- core API ----------------------------------------------------------
    def classify(
        self,
        smiles: Union[str, Sequence[str]],
        inchikeys: Optional[Sequence[Optional[str]]] = None,
    ) -> pd.DataFrame:
        """Classify one or more structures into ChEBI ontology classes.

        Args:
            smiles: A SMILES string or a sequence of SMILES strings.
            inchikeys: Optional pre-computed InChIKeys aligned with ``smiles``.
                When omitted, InChIKeys are derived from the SMILES with RDKit.
                The InChIKey is the cache key, so supplying canonical values makes
                caching consistent across equivalent SMILES.

        Returns:
            A `pandas.DataFrame` with one row per input structure and the
            columns in
            [`TAXONOMY_COLUMNS`][provesid.taxonomy.TAXONOMY_COLUMNS]. For this
            backend the ClassyFire level columns
            (``kingdom``/``superclass``/``class``/``subclass``) are ``None``;
            ``chebi_ids`` holds the ``|``-joined predicted ChEBI ids,
            ``chebi_names`` the ``|``-joined names when ``resolve_names`` is
            set, ``source`` is ``"chebifier"``, and ``confidence`` the
            ``|``-joined per-label scores when available.

        Raises:
            ChebifierMissingError: If ``chebifier`` is not installed.
            ValueError: If ``inchikeys`` is not the length of ``smiles``.

        Examples:
            >>> clf = ChebifierClassifier(with_scores=True)
            >>> row = clf.classify("CCO").iloc[0]                  # doctest: +SKIP
            >>> row["source"], row["chebi_ids"].split("|")[:3]     # doctest: +SKIP
            ('chebifier', ['134179', '23367', '24431'])
            >>> row["confidence"].split("|")[:3]                   # doctest: +SKIP
            ['1', '1', '1']
        """
        if isinstance(smiles, str):
            smiles_list: List[str] = [smiles]
        else:
            smiles_list = list(smiles)

        if inchikeys is not None and len(inchikeys) != len(smiles_list):
            raise ValueError(
                "inchikeys must be the same length as smiles "
                f"({len(inchikeys)} != {len(smiles_list)})"
            )

        # Resolve InChIKeys (cache keys) for every input.
        resolved_keys: List[Optional[str]] = []
        for idx, smi in enumerate(smiles_list):
            key = inchikeys[idx] if inchikeys is not None else None
            if not key:
                key = normalize_structure(smi).get("inchikey")
            resolved_keys.append(key)

        # Look up cache; collect the structures that still need the model.
        predictions: List[Optional[Dict[str, Optional[float]]]] = [None] * len(smiles_list)
        to_run_idx: List[int] = []
        for idx, key in enumerate(resolved_keys):
            if self.use_cache and key:
                found, value = self._cache.get(
                    self._CACHE_FUNC_NAME, (self._cache_key(key),), {}
                )
                if found:
                    predictions[idx] = value
                    continue
            to_run_idx.append(idx)

        # Run the ensemble on the cache misses (single batched call).
        if to_run_idx:
            batch = [smiles_list[i] for i in to_run_idx]
            if self.with_scores:
                raw = self.predict_with_scores(batch)
            else:
                # chdir for the same reason the ensemble is *built* under it: the
                # models resolve some data paths (chebi_v244/, disjoint_*.csv)
                # relative to the working directory, and without this they are
                # written into whatever directory the caller happened to run in.
                with contextlib.chdir(self.data_dir):
                    raw = self.ensemble.predict_smiles_list(batch)
            for pos, idx in enumerate(to_run_idx):
                norm = self._normalize_prediction(raw[pos])
                predictions[idx] = norm
                key = resolved_keys[idx]
                if self.use_cache and key:
                    self._cache.set(
                        self._CACHE_FUNC_NAME, (self._cache_key(key),), {}, norm
                    )

        rows = [
            self._build_row(smiles_list[i], resolved_keys[i], predictions[i] or {})
            for i in range(len(smiles_list))
        ]
        return pd.DataFrame(rows, columns=TAXONOMY_COLUMNS)

    def _build_row(
        self,
        smiles: str,
        inchikey: Optional[str],
        prediction: Dict[str, Optional[float]],
    ) -> Dict[str, Any]:
        """Assemble one tidy-schema row from a normalised prediction."""
        chebi_ids = list(prediction.keys())
        confidences = [prediction[cid] for cid in chebi_ids]
        names = self._resolve_names(chebi_ids) if self.resolve_names else None
        has_conf = any(c is not None for c in confidences)
        return {
            "inchikey": inchikey,
            "smiles": smiles,
            "kingdom": None,
            "superclass": None,
            "class": None,
            "subclass": None,
            "chebi_ids": "|".join(chebi_ids) if chebi_ids else None,
            "chebi_names": "|".join(names) if names else None,
            "source": "chebifier",
            "confidence": (
                "|".join("" if c is None else f"{c:g}" for c in confidences)
                if has_conf
                else None
            ),
        }

    def _resolve_names(self, chebi_ids: Sequence[str]) -> List[str]:
        """Resolve ChEBI ids to names via the ChEBI client (best-effort)."""
        names: List[str] = []
        for cid in chebi_ids:
            names.append(self._resolve_single_name(cid))
        return names

    def _resolve_single_name(self, chebi_id: str) -> str:
        """Resolve one ChEBI id to a name, caching and degrading gracefully."""
        cid = chebi_id if str(chebi_id).upper().startswith("CHEBI:") else f"CHEBI:{chebi_id}"
        found, value = self._cache.get("provesid.taxonomy.chebi_name", (cid,), {})
        if found:
            return value
        name = cid
        try:
            entity = self._get_chebi().get_compound(cid)
            if isinstance(entity, dict):
                name = entity.get("chebiAsciiName") or entity.get("name") or cid
        except Exception as exc:  # network / lookup failures must not break classify
            logger.debug("ChEBI name lookup failed for %s: %s", cid, exc)
        self._cache.set("provesid.taxonomy.chebi_name", (cid,), {}, name)
        return name

    @staticmethod
    def to_labels(df: pd.DataFrame, level: str = "chebi_ids") -> Dict[str, Optional[str]]:
        """Collapse a taxonomy table to a ``{inchikey: label}`` mapping.

        Args:
            df: A taxonomy table as returned by
                [`classify`][provesid.taxonomy.ChebifierClassifier.classify].
            level: The column to use as the label (e.g. ``"chebi_ids"`` or
                ``"chebi_names"``).

        Returns:
            Mapping from InChIKey to the chosen label (``None`` when absent).

        Raises:
            KeyError: If ``level`` is not a column of ``df``.

        Examples:
            >>> table = pd.DataFrame({"inchikey": ["LFQSCWFLJHTTHZ-UHFFFAOYSA-N"],
            ...                       "chebi_ids": ["30879|33822"]})
            >>> ChebifierClassifier.to_labels(table)
            {'LFQSCWFLJHTTHZ-UHFFFAOYSA-N': '30879|33822'}
        """
        if level not in df.columns:
            raise KeyError(f"Unknown level {level!r}; available: {list(df.columns)}")
        return dict(zip(df["inchikey"], df[level]))
Attributes
chebifier_version property

Installed chebifier version (falls back to the pinned version).

Part of every cache key, so upgrading chebifier re-classifies.

Examples:

>>> ChebifierClassifier().chebifier_version
'1.2.2'
ensemble property

The lazily-constructed, reused BaseEnsemble instance.

Note

The ensemble is built with data_dir as the working directory, because chemlog_extra resolves its element-class mapping files relative to the working directory (see ensure_element_class_mappings). The previous directory is always restored.

Raises:

Type Description
ChebifierMissingError

If chebifier is not installed.

Examples:

>>> type(ChebifierClassifier().ensemble).__name__
'BaseEnsemble'
Methods:
predict_with_scores(smiles_list)

Predict ChEBI classes and the score behind each decision.

BaseEnsemble.predict_smiles_list returns only the surviving class ids; the smoothed net score it thresholds at 0 to get them is computed and then discarded. This runs the same four steps (gather_predictions -> consolidate_predictions -> smoother -> > 0) and keeps the score, so callers get a per-label confidence for free -- the models are run exactly once, no differently and no more often than upstream does.

Parameters:

Name Type Description Default
smiles_list Sequence[str]

Structures to classify.

required

Returns:

Type Description
List[Optional[Dict[str, float]]]

One entry per input: a {chebi_id: smoothed_score} mapping, or None for a structure no model could classify (upstream's "complete failure", which predict_smiles_list also reports as None).

Note

Verified to reproduce predict_smiles_list's label sets exactly. The scores are the smoothed ones, i.e. the values the ontology consistency pass actually thresholds, so score > 0 holds for every returned label. Not cached; classify with with_scores=True is.

Examples:

>>> scores = ChebifierClassifier().predict_with_scores(["CCO"])[0]
>>> scores["30879"], round(scores["2571"], 3)
(1.0, 0.866)
Source code in src/provesid/taxonomy.py
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
def predict_with_scores(self, smiles_list: Sequence[str]) -> List[Optional[Dict[str, float]]]:
    """Predict ChEBI classes *and* the score behind each decision.

    ``BaseEnsemble.predict_smiles_list`` returns only the surviving class
    ids; the smoothed net score it thresholds at 0 to get them is computed
    and then discarded. This runs the same four steps
    (``gather_predictions`` -> ``consolidate_predictions`` ->
    ``smoother`` -> ``> 0``) and keeps the score, so callers get a per-label
    confidence for free -- the models are run exactly once, no differently
    and no more often than upstream does.

    Args:
        smiles_list: Structures to classify.

    Returns:
        One entry per input: a ``{chebi_id: smoothed_score}`` mapping, or
        ``None`` for a structure no model could classify (upstream's
        "complete failure", which ``predict_smiles_list`` also reports as
        ``None``).

    Note:
        Verified to reproduce ``predict_smiles_list``'s label sets exactly.
        The scores are the *smoothed* ones, i.e. the values the ontology
        consistency pass actually thresholds, so ``score > 0`` holds for
        every returned label. Not cached;
        [`classify`][provesid.taxonomy.ChebifierClassifier.classify] with
        ``with_scores=True`` is.

    Examples:
        >>> scores = ChebifierClassifier().predict_with_scores(["CCO"])[0]  # doctest: +SKIP
        >>> scores["30879"], round(scores["2571"], 3)                        # doctest: +SKIP
        (1.0, 0.866)
    """
    import torch

    ens = self.ensemble
    with contextlib.chdir(self.data_dir):
        logits, classes = ens.gather_predictions(list(smiles_list))
        class_idx = {cls: i for i, cls in enumerate(classes)}
        weights = ens.calculate_classwise_weights(class_idx)
        net_score, has_valid = ens.consolidate_predictions(logits, weights)
        if ens.smoother is not None:
            ens.smoother.set_label_names(list(classes))
            score = ens.smoother(net_score)
        else:
            score = net_score
        decisions = (score > 0) & has_valid
        failures = torch.all(~has_valid, dim=1)

    out: List[Optional[Dict[str, float]]] = []
    for i in range(len(smiles_list)):
        if bool(failures[i]):
            out.append(None)
            continue
        out.append({
            classes[j]: float(score[i, j])
            for j in torch.nonzero(decisions[i], as_tuple=True)[0].tolist()
        })
    return out
classify(smiles, inchikeys=None)

Classify one or more structures into ChEBI ontology classes.

Parameters:

Name Type Description Default
smiles Union[str, Sequence[str]]

A SMILES string or a sequence of SMILES strings.

required
inchikeys Optional[Sequence[Optional[str]]]

Optional pre-computed InChIKeys aligned with smiles. When omitted, InChIKeys are derived from the SMILES with RDKit. The InChIKey is the cache key, so supplying canonical values makes caching consistent across equivalent SMILES.

None

Returns:

Type Description
DataFrame

A pandas.DataFrame with one row per input structure and the columns in TAXONOMY_COLUMNS. For this backend the ClassyFire level columns (kingdom/superclass/class/subclass) are None; chebi_ids holds the |-joined predicted ChEBI ids, chebi_names the |-joined names when resolve_names is set, source is "chebifier", and confidence the |-joined per-label scores when available.

Raises:

Type Description
ChebifierMissingError

If chebifier is not installed.

ValueError

If inchikeys is not the length of smiles.

Examples:

>>> clf = ChebifierClassifier(with_scores=True)
>>> row = clf.classify("CCO").iloc[0]
>>> row["source"], row["chebi_ids"].split("|")[:3]
('chebifier', ['134179', '23367', '24431'])
>>> row["confidence"].split("|")[:3]
['1', '1', '1']
Source code in src/provesid/taxonomy.py
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
def classify(
    self,
    smiles: Union[str, Sequence[str]],
    inchikeys: Optional[Sequence[Optional[str]]] = None,
) -> pd.DataFrame:
    """Classify one or more structures into ChEBI ontology classes.

    Args:
        smiles: A SMILES string or a sequence of SMILES strings.
        inchikeys: Optional pre-computed InChIKeys aligned with ``smiles``.
            When omitted, InChIKeys are derived from the SMILES with RDKit.
            The InChIKey is the cache key, so supplying canonical values makes
            caching consistent across equivalent SMILES.

    Returns:
        A `pandas.DataFrame` with one row per input structure and the
        columns in
        [`TAXONOMY_COLUMNS`][provesid.taxonomy.TAXONOMY_COLUMNS]. For this
        backend the ClassyFire level columns
        (``kingdom``/``superclass``/``class``/``subclass``) are ``None``;
        ``chebi_ids`` holds the ``|``-joined predicted ChEBI ids,
        ``chebi_names`` the ``|``-joined names when ``resolve_names`` is
        set, ``source`` is ``"chebifier"``, and ``confidence`` the
        ``|``-joined per-label scores when available.

    Raises:
        ChebifierMissingError: If ``chebifier`` is not installed.
        ValueError: If ``inchikeys`` is not the length of ``smiles``.

    Examples:
        >>> clf = ChebifierClassifier(with_scores=True)
        >>> row = clf.classify("CCO").iloc[0]                  # doctest: +SKIP
        >>> row["source"], row["chebi_ids"].split("|")[:3]     # doctest: +SKIP
        ('chebifier', ['134179', '23367', '24431'])
        >>> row["confidence"].split("|")[:3]                   # doctest: +SKIP
        ['1', '1', '1']
    """
    if isinstance(smiles, str):
        smiles_list: List[str] = [smiles]
    else:
        smiles_list = list(smiles)

    if inchikeys is not None and len(inchikeys) != len(smiles_list):
        raise ValueError(
            "inchikeys must be the same length as smiles "
            f"({len(inchikeys)} != {len(smiles_list)})"
        )

    # Resolve InChIKeys (cache keys) for every input.
    resolved_keys: List[Optional[str]] = []
    for idx, smi in enumerate(smiles_list):
        key = inchikeys[idx] if inchikeys is not None else None
        if not key:
            key = normalize_structure(smi).get("inchikey")
        resolved_keys.append(key)

    # Look up cache; collect the structures that still need the model.
    predictions: List[Optional[Dict[str, Optional[float]]]] = [None] * len(smiles_list)
    to_run_idx: List[int] = []
    for idx, key in enumerate(resolved_keys):
        if self.use_cache and key:
            found, value = self._cache.get(
                self._CACHE_FUNC_NAME, (self._cache_key(key),), {}
            )
            if found:
                predictions[idx] = value
                continue
        to_run_idx.append(idx)

    # Run the ensemble on the cache misses (single batched call).
    if to_run_idx:
        batch = [smiles_list[i] for i in to_run_idx]
        if self.with_scores:
            raw = self.predict_with_scores(batch)
        else:
            # chdir for the same reason the ensemble is *built* under it: the
            # models resolve some data paths (chebi_v244/, disjoint_*.csv)
            # relative to the working directory, and without this they are
            # written into whatever directory the caller happened to run in.
            with contextlib.chdir(self.data_dir):
                raw = self.ensemble.predict_smiles_list(batch)
        for pos, idx in enumerate(to_run_idx):
            norm = self._normalize_prediction(raw[pos])
            predictions[idx] = norm
            key = resolved_keys[idx]
            if self.use_cache and key:
                self._cache.set(
                    self._CACHE_FUNC_NAME, (self._cache_key(key),), {}, norm
                )

    rows = [
        self._build_row(smiles_list[i], resolved_keys[i], predictions[i] or {})
        for i in range(len(smiles_list))
    ]
    return pd.DataFrame(rows, columns=TAXONOMY_COLUMNS)
to_labels(df, level='chebi_ids') staticmethod

Collapse a taxonomy table to a {inchikey: label} mapping.

Parameters:

Name Type Description Default
df DataFrame

A taxonomy table as returned by classify.

required
level str

The column to use as the label (e.g. "chebi_ids" or "chebi_names").

'chebi_ids'

Returns:

Type Description
Dict[str, Optional[str]]

Mapping from InChIKey to the chosen label (None when absent).

Raises:

Type Description
KeyError

If level is not a column of df.

Examples:

>>> table = pd.DataFrame({"inchikey": ["LFQSCWFLJHTTHZ-UHFFFAOYSA-N"],
...                       "chebi_ids": ["30879|33822"]})
>>> ChebifierClassifier.to_labels(table)
{'LFQSCWFLJHTTHZ-UHFFFAOYSA-N': '30879|33822'}
Source code in src/provesid/taxonomy.py
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
@staticmethod
def to_labels(df: pd.DataFrame, level: str = "chebi_ids") -> Dict[str, Optional[str]]:
    """Collapse a taxonomy table to a ``{inchikey: label}`` mapping.

    Args:
        df: A taxonomy table as returned by
            [`classify`][provesid.taxonomy.ChebifierClassifier.classify].
        level: The column to use as the label (e.g. ``"chebi_ids"`` or
            ``"chebi_names"``).

    Returns:
        Mapping from InChIKey to the chosen label (``None`` when absent).

    Raises:
        KeyError: If ``level`` is not a column of ``df``.

    Examples:
        >>> table = pd.DataFrame({"inchikey": ["LFQSCWFLJHTTHZ-UHFFFAOYSA-N"],
        ...                       "chebi_ids": ["30879|33822"]})
        >>> ChebifierClassifier.to_labels(table)
        {'LFQSCWFLJHTTHZ-UHFFFAOYSA-N': '30879|33822'}
    """
    if level not in df.columns:
        raise KeyError(f"Unknown level {level!r}; available: {list(df.columns)}")
    return dict(zip(df["inchikey"], df[level]))

Functions:

chebifier_available()

Return whether the optional chebifier package is importable.

This is a cheap check (it does not import PyTorch or load any model) suitable for feature-detection and for skipping tests when the extra is absent.

Returns:

Type Description
bool

True if chebifier can be imported, False otherwise.

Examples:

>>> from provesid.taxonomy import chebifier_available
>>> isinstance(chebifier_available(), bool)
True
Source code in src/provesid/taxonomy.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def chebifier_available() -> bool:
    """Return whether the optional ``chebifier`` package is importable.

    This is a cheap check (it does not import PyTorch or load any model) suitable
    for feature-detection and for skipping tests when the extra is absent.

    Returns:
        ``True`` if ``chebifier`` can be imported, ``False`` otherwise.

    Examples:
        >>> from provesid.taxonomy import chebifier_available
        >>> isinstance(chebifier_available(), bool)
        True
    """
    return importlib.util.find_spec("chebifier") is not None

missing_ensemble_modules()

Modules required by the default chebifier ensemble that are not installed.

Use this to decide whether a full classification run can succeed. chebifier_available only reports whether chebifier itself imports, which is not enough: the default ensemble also loads transformer, graph, rule-based and c3p models from separate packages, and a partial install fails only once prediction is attempted.

Returns:

Type Description
List[str]

The missing module names, in the order they appear in the ensemble. Empty when the full default ensemble can be constructed.

Examples:

>>> from provesid.taxonomy import missing_ensemble_modules
>>> missing_ensemble_modules()
['c3p']
Source code in src/provesid/taxonomy.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def missing_ensemble_modules() -> List[str]:
    """Modules required by the default chebifier ensemble that are not installed.

    Use this to decide whether a full classification run can succeed.
    [`chebifier_available`][provesid.taxonomy.chebifier_available] only reports
    whether ``chebifier`` itself imports, which is not enough: the default
    ensemble also loads transformer, graph, rule-based and c3p models from
    separate packages, and a partial install fails only once prediction is
    attempted.

    Returns:
        The missing module names, in the order they appear in the ensemble.
        Empty when the full default ensemble can be constructed.

    Examples:
        >>> from provesid.taxonomy import missing_ensemble_modules
        >>> missing_ensemble_modules()  # doctest: +SKIP
        ['c3p']
    """
    return [
        name
        for name in _DEFAULT_ENSEMBLE_MODULES
        if importlib.util.find_spec(name) is None
    ]

default_ensemble_available()

Whether every module the default chebifier ensemble needs is installed.

Returns:

Type Description
bool

True when missing_ensemble_modules is empty.

Examples:

>>> from provesid.taxonomy import default_ensemble_available
>>> default_ensemble_available() == (missing_ensemble_modules() == [])
True
Source code in src/provesid/taxonomy.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def default_ensemble_available() -> bool:
    """Whether every module the default chebifier ensemble needs is installed.

    Returns:
        ``True`` when
        [`missing_ensemble_modules`][provesid.taxonomy.missing_ensemble_modules]
        is empty.

    Examples:
        >>> from provesid.taxonomy import default_ensemble_available
        >>> default_ensemble_available() == (missing_ensemble_modules() == [])
        True
    """
    return not missing_ensemble_modules()

ensure_v244_indices()

Ensure chebai-graph's property indices match chebifier's v244 checkpoints.

Reverts the BondType/AtomNumHs/NumAtomBonds one-hot vocabularies inside the installed chebai_graph package to their pre-drift contents when a newer (drifted) version is present. Without this, chebifier's graph (GNN) models fail to load with a tensor-shape error. This is a safety net for a hand-upgraded chebai_graph: the version pinned by chebifier[models] (1.0.0) already matches, so every property reports "ok" on a clean install. The operation is idempotent and a no-op when the indices already match or when chebai_graph is not installed. See docs/guide/chebifier.md for the full root-cause writeup.

Returns:

Type Description
Dict[str, str]

Mapping of property name to a status string: "patched", "ok" (already matching), or "missing" (index file not found). Empty when chebai_graph is not installed.

Note

"patched" means a file inside the installed chebai_graph package was rewritten.

Examples:

>>> ensure_v244_indices()
{'BondType': 'ok', 'AtomNumHs': 'ok', 'NumAtomBonds': 'ok'}
Source code in src/provesid/taxonomy.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
def ensure_v244_indices() -> Dict[str, str]:
    """Ensure chebai-graph's property indices match chebifier's v244 checkpoints.

    Reverts the ``BondType``/``AtomNumHs``/``NumAtomBonds`` one-hot vocabularies
    inside the installed ``chebai_graph`` package to their pre-drift contents when
    a newer (drifted) version is present. Without this, chebifier's graph (GNN)
    models fail to load with a tensor-shape error. This is a safety net for a
    hand-upgraded ``chebai_graph``: the version pinned by ``chebifier[models]``
    (1.0.0) already matches, so every property reports ``"ok"`` on a clean
    install. The operation is idempotent and a no-op when the indices already
    match or when ``chebai_graph`` is not installed. See ``docs/guide/chebifier.md`` for
    the full root-cause writeup.

    Returns:
        Mapping of property name to a status string: ``"patched"``, ``"ok"``
        (already matching), or ``"missing"`` (index file not found). Empty
        when ``chebai_graph`` is not installed.

    Note:
        ``"patched"`` means a file inside the installed ``chebai_graph``
        package was rewritten.

    Examples:
        >>> ensure_v244_indices()                                  # doctest: +SKIP
        {'BondType': 'ok', 'AtomNumHs': 'ok', 'NumAtomBonds': 'ok'}
    """
    try:
        import chebai_graph  # noqa: F401 - only need its location
    except ImportError:
        logger.debug("chebai_graph not installed; skipping v244 index check")
        return {}

    bin_dir = os.path.join(
        os.path.dirname(chebai_graph.__file__), "preprocessing", "bin"
    )
    results: Dict[str, str] = {}
    for prop, tokens in _V244_PROPERTY_INDICES.items():
        path = os.path.join(bin_dir, prop, "indices_one_hot.txt")
        if not os.path.exists(path):
            logger.warning("chebai-graph index file not found: %s", path)
            results[prop] = "missing"
            continue
        with open(path, "r") as handle:
            current = [line.strip() for line in handle if line.strip()]
        if current == tokens:
            results[prop] = "ok"
            continue
        with open(path, "w") as handle:
            handle.write("\n".join(tokens) + "\n")
        logger.info(
            "Patched chebai-graph %s index to v244 state (%d -> %d tokens)",
            prop,
            len(current),
            len(tokens),
        )
        results[prop] = "patched"
    return results

ensure_element_class_mappings(data_dir=None, chebi_version=244)

Ensure chemlog_extra's element-to-ChEBI-class mapping files exist.

chemlog_extra's by-element classifiers read data/chebi_v<version>/<Classifier>_element_class_mapping.csv relative to the current working directory, and rebuild it from the ChEBI graph when the file is absent. That rebuild crashes on the current graph — 288 of its 205k nodes carry name: None, and the builder does " molecular entity" in properties["name"] — so the chemlog models cannot be constructed at all without these files, and the whole ensemble fails with TypeError: argument of type 'NoneType' is not iterable.

This writes both files into the PROVESID chebifier data directory using upstream's own derivation rules, skipping unnamed nodes. ChebifierClassifier.ensemble then builds the ensemble with that directory as the working directory, so the files are found without writing anything into the caller's working directory.

Parameters:

Name Type Description Default
data_dir Optional[str]

Base directory for chebifier data. When None, uses user_dataset_path("chebifier").

None
chebi_version int

ChEBI version the mappings are built for. Must match the version chemlog_extra asks for (its default, 244).

244

Returns:

Type Description
Dict[str, str]

Mapping of classifier name to a status string: "written", "ok" (already present), or "unavailable" (chebifier not installed, so the ChEBI graph could not be loaded).

Examples:

>>> from provesid.taxonomy import ensure_element_class_mappings
>>> ensure_element_class_mappings()
{'XMolecularEntityClassifier': 'ok', 'OrganoXCompoundClassifier': 'ok'}
Source code in src/provesid/taxonomy.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
def ensure_element_class_mappings(
    data_dir: Optional[str] = None, chebi_version: int = 244
) -> Dict[str, str]:
    """Ensure ``chemlog_extra``'s element-to-ChEBI-class mapping files exist.

    ``chemlog_extra``'s by-element classifiers read
    ``data/chebi_v<version>/<Classifier>_element_class_mapping.csv`` **relative to
    the current working directory**, and rebuild it from the ChEBI graph when the
    file is absent. That rebuild crashes on the current graph — 288 of its 205k
    nodes carry ``name: None``, and the builder does
    ``" molecular entity" in properties["name"]`` — so the chemlog models cannot
    be constructed at all without these files, and the whole ensemble fails with
    ``TypeError: argument of type 'NoneType' is not iterable``.

    This writes both files into the PROVESID chebifier data directory using
    upstream's own derivation rules, skipping unnamed nodes.
    [`ChebifierClassifier.ensemble`][provesid.taxonomy.ChebifierClassifier.ensemble]
    then builds the ensemble with that directory as the working directory, so
    the files are found without writing anything into the caller's working
    directory.

    Args:
        data_dir: Base directory for chebifier data. When ``None``, uses
            ``user_dataset_path("chebifier")``.
        chebi_version: ChEBI version the mappings are built for. Must match the
            version chemlog_extra asks for (its default, 244).

    Returns:
        Mapping of classifier name to a status string: ``"written"``, ``"ok"``
        (already present), or ``"unavailable"`` (chebifier not installed, so the
        ChEBI graph could not be loaded).

    Examples:
        >>> from provesid.taxonomy import ensure_element_class_mappings
        >>> ensure_element_class_mappings()  # doctest: +SKIP
        {'XMolecularEntityClassifier': 'ok', 'OrganoXCompoundClassifier': 'ok'}
    """
    base = data_dir or user_dataset_path("chebifier")
    target_dir = os.path.join(base, "data", f"chebi_v{chebi_version}")

    paths = {
        name: os.path.join(target_dir, f"{name}_element_class_mapping.csv")
        for name in ("XMolecularEntityClassifier", "OrganoXCompoundClassifier")
    }
    results = {name: "ok" for name, path in paths.items() if os.path.exists(path)}
    missing = [name for name in paths if name not in results]
    if not missing:
        return results

    try:
        from chebifier.utils import load_chebi_graph
    except ImportError:
        logger.debug("chebifier not installed; skipping element class mappings")
        return {name: "unavailable" for name in paths}

    chebi_graph = load_chebi_graph()
    os.makedirs(target_dir, exist_ok=True)
    for name in missing:
        mapping = _build_element_class_mapping(chebi_graph, name)
        with open(paths[name], "w", newline="") as handle:
            writer = csv.writer(handle)
            writer.writerow(["element_num", "chebi_id"])
            for element_num, chebi_id in mapping.items():
                writer.writerow([element_num, chebi_id])
        logger.info(
            "Wrote %s element class mapping (%d entries) to %s",
            name, len(mapping), paths[name],
        )
        results[name] = "written"
    return results

chebi_class_names(data_dir=None)

ChEBI id -> name for every term, read from chebifier's ontology snapshot.

chebifier already ships (and the ensemble already loads) a pickled networkx graph of the whole ChEBI hierarchy, whose nodes carry their names. Reading it is an offline alternative to resolving names one at a time through the online ChEBI client, which matters when labelling thousands of predicted classes at once.

Parameters:

Name Type Description Default
data_dir Optional[str]

Base directory for chebifier storage; defaults to the shared PROVESID dataset dir, exactly as ChebifierClassifier does.

None

Returns:

Type Description
Dict[str, str]

{"33659": "organic aromatic compound", ...} -- ids are bare, without the CHEBI: prefix, matching what the ensemble predicts.

Raises:

Type Description
ChebifierMissingError

If chebifier is not installed.

Examples:

>>> from provesid.taxonomy import chebi_class_names
>>> chebi_class_names()["33659"]
'organic aromatic compound'
Source code in src/provesid/taxonomy.py
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
def chebi_class_names(data_dir: Optional[str] = None) -> Dict[str, str]:
    """ChEBI id -> name for every term, read from chebifier's ontology snapshot.

    chebifier already ships (and the ensemble already loads) a pickled networkx
    graph of the whole ChEBI hierarchy, whose nodes carry their names. Reading it
    is an **offline** alternative to resolving names one at a time through the
    online ChEBI client, which matters when labelling thousands of predicted
    classes at once.

    Args:
        data_dir: Base directory for chebifier storage; defaults to the shared
            PROVESID dataset dir, exactly as
            [`ChebifierClassifier`][provesid.taxonomy.ChebifierClassifier]
            does.

    Returns:
        ``{"33659": "organic aromatic compound", ...}`` -- ids are bare, without
        the ``CHEBI:`` prefix, matching what the ensemble predicts.

    Raises:
        ChebifierMissingError: If ``chebifier`` is not installed.

    Examples:
        >>> from provesid.taxonomy import chebi_class_names
        >>> chebi_class_names()["33659"]  # doctest: +SKIP
        'organic aromatic compound'
    """
    if not chebifier_available():
        raise ChebifierMissingError(
            "Reading ChEBI class names requires the optional 'chebifier' extra. "
            "Install with:\n    bash scripts/install_chebifier.sh"
        )
    resolved = _configure_chebifier_storage(data_dir)
    from chebifier.utils import load_chebi_graph

    with contextlib.chdir(resolved):
        graph = load_chebi_graph()
    return {
        str(node): props["name"]
        for node, props in graph.nodes(data=True)
        if props.get("name")
    }

classify_chebifier(smiles, inchikeys=None, *, data_dir=None, use_cache=True, resolve_names=False)

Classify structures with the chebifier ensemble (convenience wrapper).

Thin functional wrapper over ChebifierClassifier for one-off calls. For repeated calls, construct a ChebifierClassifier once and reuse it so the model is loaded a single time.

Parameters:

Name Type Description Default
smiles Union[str, Sequence[str]]

A SMILES string or sequence of SMILES strings.

required
inchikeys Optional[Sequence[Optional[str]]]

Optional pre-computed InChIKeys aligned with smiles.

None
data_dir Optional[str]

Base directory for chebifier model storage.

None
use_cache bool

Whether to use the on-disk InChIKey cache.

True
resolve_names bool

Whether to resolve ChEBI ids to names.

False

Returns:

Type Description
DataFrame

A tidy taxonomy pandas.DataFrame (see ChebifierClassifier.classify).

Raises:

Type Description
ChebifierMissingError

If chebifier is not installed.

Examples:

>>> from provesid.taxonomy import classify_chebifier
>>> table = classify_chebifier("CCO")
>>> table.loc[0, "inchikey"]
'LFQSCWFLJHTTHZ-UHFFFAOYSA-N'
Source code in src/provesid/taxonomy.py
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
def classify_chebifier(
    smiles: Union[str, Sequence[str]],
    inchikeys: Optional[Sequence[Optional[str]]] = None,
    *,
    data_dir: Optional[str] = None,
    use_cache: bool = True,
    resolve_names: bool = False,
) -> pd.DataFrame:
    """Classify structures with the chebifier ensemble (convenience wrapper).

    Thin functional wrapper over
    [`ChebifierClassifier`][provesid.taxonomy.ChebifierClassifier] for one-off
    calls. For repeated calls, construct a
    [`ChebifierClassifier`][provesid.taxonomy.ChebifierClassifier] once and
    reuse it so the model is loaded a single time.

    Args:
        smiles: A SMILES string or sequence of SMILES strings.
        inchikeys: Optional pre-computed InChIKeys aligned with ``smiles``.
        data_dir: Base directory for chebifier model storage.
        use_cache: Whether to use the on-disk InChIKey cache.
        resolve_names: Whether to resolve ChEBI ids to names.

    Returns:
        A tidy taxonomy `pandas.DataFrame` (see
        [`ChebifierClassifier.classify`][provesid.taxonomy.ChebifierClassifier.classify]).

    Raises:
        ChebifierMissingError: If ``chebifier`` is not installed.

    Examples:
        >>> from provesid.taxonomy import classify_chebifier
        >>> table = classify_chebifier("CCO")                     # doctest: +SKIP
        >>> table.loc[0, "inchikey"]                              # doctest: +SKIP
        'LFQSCWFLJHTTHZ-UHFFFAOYSA-N'
    """
    classifier = ChebifierClassifier(
        data_dir=data_dir, use_cache=use_cache, resolve_names=resolve_names
    )
    return classifier.classify(smiles, inchikeys=inchikeys)