Source code for arcade.classifiers.base

"""Base classifier class and registry for ARCADE.

Built-in discriminators register with :func:`register_classifier`. External
methods can either use the same decorator (if they import ARCADE) or be
referenced from YAML via a dotted path ``package.module.ClassName``.
"""

from __future__ import annotations

import difflib
import importlib
from abc import ABC, abstractmethod
from typing import Any, Literal

import numpy as np

_CLASSIFIER_REGISTRY: dict[str, type["BaseClassifier"]] = {}


[docs] def register_classifier(name: str) -> Any: """Register a :class:`BaseClassifier` subclass under *name*. The *name* is what appears in ``classifiers.run`` in YAML. Args: name: Short registry key (e.g. ``"fnn"``, ``"my_clf"``). Returns: A class decorator that stores the class in the registry. Example:: @register_classifier("my_clf") class MyClassifier(BaseClassifier): FEATURE_KIND = "filter_features" def fit(self, train_features, train_labels, **kwargs): return self def predict(self, features): ... """ def decorator(cls: type["BaseClassifier"]) -> type["BaseClassifier"]: _CLASSIFIER_REGISTRY[name] = cls cls.name = name return cls return decorator
[docs] def get_classifier(name: str) -> type["BaseClassifier"]: """Resolve a classifier class by registry name or dotted import path. Args: name: Either a registered key (``"lda"``) or ``"package.module.ClassName"`` (last segment is the class). Returns: The classifier **class** (not an instance). Call :meth:`BaseClassifier.from_config` to construct. Raises: KeyError: Unknown registry name (includes close-match hints). TypeError: Dotted path did not resolve to a ``BaseClassifier`` subclass. """ if name in _CLASSIFIER_REGISTRY: return _CLASSIFIER_REGISTRY[name] if "." in name: module_path, cls_name = name.rsplit(".", 1) mod = importlib.import_module(module_path) cls = getattr(mod, cls_name) if isinstance(cls, type) and issubclass(cls, BaseClassifier): return cls raise TypeError(f"{name} resolved to {cls!r}, not a BaseClassifier subclass.") close = difflib.get_close_matches(name, _CLASSIFIER_REGISTRY.keys(), n=3, cutoff=0.5) hint = f" Did you mean: {', '.join(close)}?" if close else "" raise KeyError( f"Unknown classifier {name!r}.{hint} " f"Registered: {sorted(_CLASSIFIER_REGISTRY)}. " f"For external classifiers, use a dotted import path." )
[docs] def list_classifiers() -> list[str]: """Return sorted registry names of all built-in classifiers. Triggers :func:`ensure_classifiers_registered` so the list is complete even before any classifier module was imported. """ ensure_classifiers_registered() return sorted(_CLASSIFIER_REGISTRY)
[docs] def iq_trace_classifier_names() -> frozenset[str]: """Return names of classifiers that consume IQ / raw traces (not MF scores).""" ensure_classifiers_registered() names = { n for n, cls in _CLASSIFIER_REGISTRY.items() if getattr(cls, "USES_IQ_TRACES", False) or getattr(cls, "FEATURE_KIND", "") == "iq_trace" } names |= {"path_signature", "ngrc", "remf"} return frozenset(names)
[docs] def seq_trace_classifier_names() -> frozenset[str]: """Return names of classifiers that consume sequential trace tensors.""" ensure_classifiers_registered() names = { n for n, cls in _CLASSIFIER_REGISTRY.items() if "trace_seq" in getattr(cls, "FEATURE_KIND", "") } names |= {"transformer"} return frozenset(names)
FILTER_SET_CLASSIFIERS = frozenset({"threshold", "leakage", "multilevel"})
[docs] def list_classifiers_with_descriptions() -> list[tuple[str, str, str]]: """Return ``(name, category, description)`` for every registered classifier.""" ensure_classifiers_registered() out: list[tuple[str, str, str]] = [] for name in sorted(_CLASSIFIER_REGISTRY): cls = _CLASSIFIER_REGISTRY[name] out.append( ( name, getattr(cls, "CATEGORY", "classical"), getattr(cls, "DESCRIPTION", ""), ) ) return out
[docs] def ensure_classifiers_registered() -> None: """Import built-in classifier packages so the registry is populated.""" import arcade.classifiers.classical # noqa: F401 import arcade.classifiers.ensemble # noqa: F401 import arcade.classifiers.hybrid # noqa: F401 import arcade.classifiers.neural # noqa: F401
[docs] class BaseClassifier(ABC): """Contract every ARCADE discriminator must satisfy. Class attributes control how the pipeline builds features and trains: - ``FEATURE_KIND`` — which feature matrix the classify stage builds (``filter_features``, ``iq_trace``, …). - ``CATEGORY`` — ``classical`` · ``neural`` · ``hybrid`` · ``ensemble``. - ``REQUIRES_NN_CONFIG`` — if true, YAML must supply ``config_file`` / NN dict. - ``CLASSIFICATION_MODE`` — joint vs per-qubit decoding hints for metrics. Subclass, set those attributes, implement :meth:`fit` / :meth:`predict`, and decorate with :func:`register_classifier`. Example:: @register_classifier("my_clf") class MyClassifier(BaseClassifier): CATEGORY = "classical" FEATURE_KIND = "filter_features" def fit(self, train_features, train_labels, **kwargs): ... return self def predict(self, features): ... return labels """ name: str = "base" DESCRIPTION: str = "" REQUIRES_NN_CONFIG: bool = False REQUIRES_TRAINING: bool = True CATEGORY: Literal["classical", "neural", "hybrid", "ensemble"] = "classical" CLASSIFICATION_MODE: str = "joint" FEATURE_KIND: str = "filter_features" SUPPORTED_FEATURE_KINDS: tuple[str, ...] | None = None USES_IQ_TRACES: bool = False TUNABLE: dict[str, Any] | bool = {}
[docs] def validate_feature_kind(self, kind: str) -> None: """Raise ``ValueError`` if *kind* is not supported by this classifier.""" if not self.supports_feature_kind(kind): allowed = self.SUPPORTED_FEATURE_KINDS or (self.FEATURE_KIND,) raise ValueError( f"{self.__class__.__name__} needs feature kind one of {allowed}, got {kind!r}", )
[docs] def supports_feature_kind(self, kind: str) -> bool: """Return True if this classifier can consume the given feature kind.""" allowed = self.SUPPORTED_FEATURE_KINDS if allowed is None: allowed = (self.FEATURE_KIND,) return kind in allowed
[docs] @abstractmethod def fit( self, train_features: np.ndarray, train_labels: np.ndarray, val_features: np.ndarray | None = None, val_labels: np.ndarray | None = None, **kwargs: Any, ) -> "BaseClassifier": """Train or fit the classifier on the prepared feature matrix. Args: train_features: Array shaped ``(n_train, feature_dim)`` (or method-specific). train_labels: Integer labels aligned with *train_features*. val_features: Optional validation features for early stopping / model selection. val_labels: Optional validation labels. **kwargs: Method-specific options (epochs, device, …). Returns: ``self``, for method chaining. """ ...
[docs] @abstractmethod def predict(self, features: np.ndarray) -> np.ndarray: """Return predicted integer labels for each row of *features*. Args: features: Feature matrix with the same layout used in :meth:`fit`. Returns: 1-D integer array of length ``n_shots``. """ ...
[docs] def predict_proba(self, features: np.ndarray) -> np.ndarray | None: """Return class probabilities, or ``None`` if unsupported. Args: features: Same layout as :meth:`predict`. Returns: Array shaped ``(n_shots, n_classes)``, or ``None``. """ return None
[docs] @classmethod def from_config(cls, config: dict[str, Any]) -> "BaseClassifier": """Construct an instance from a YAML / dict section for this classifier. Args: config: Merged options from the global classifiers block and any per-name subsection (plus pipeline-injected keys such as ``filter_set`` for some methods). Returns: A ready-to-:meth:`fit` instance. Raises: NotImplementedError: If the subclass does not override this method. """ raise NotImplementedError(f"{cls.__name__} does not support construction from config.")