Skip to content

API Reference

API Overview

This section is auto-generated from the Python docstrings using mkdocstrings. It always reflects the latest installed version of quantumuq.

Core modules

DeepEnsemble dataclass

Bases: UncertaintyMethod

Deep ensemble over a list of already trained predictors.

Source code in quantumuq/core/methods.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@dataclass
class DeepEnsemble(UncertaintyMethod):
    """Deep ensemble over a list of already trained predictors."""

    predictors: Sequence[Predictor]

    def __post_init__(self) -> None:
        if not self.predictors:
            raise ValueError("predictors list must not be empty")
        first_task = self.predictors[0].task
        if any(p.task != first_task for p in self.predictors[1:]):
            raise ValueError("all predictors in an ensemble must share the same task")

    def __call__(
        self, predictor: Predictor, X: np.ndarray, shots: Optional[int] = None
    ) -> PredictiveDistribution:
        # Ignore the predictor argument and use the ensemble instead.
        X_arr = np.asarray(X)
        per_model: List[np.ndarray] = []
        if self.predictors[0].task == "classification":
            for p in self.predictors:
                per_model.append(np.asarray(p.predict_proba(X_arr, shots=shots)))
        else:
            for p in self.predictors:
                per_model.append(np.asarray(p.predict(X_arr, shots=shots)))
        return stack_ensemble_samples(per_model)

NoiseProfile dataclass

Probe prediction stability as a function of shots.

For each value in sweep_shots, the predictor is evaluated n_repeats times and stability statistics are computed.

Source code in quantumuq/core/methods.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
@dataclass
class NoiseProfile:
    """Probe prediction stability as a function of shots.

    For each value in ``sweep_shots``, the predictor is evaluated ``n_repeats``
    times and stability statistics are computed.
    """

    sweep_shots: Sequence[int]
    n_repeats: int = 5

    def __post_init__(self) -> None:
        if not self.sweep_shots:
            raise ValueError("sweep_shots must not be empty")
        if self.n_repeats <= 0:
            raise ValueError("n_repeats must be positive")

    def __call__(self, predictor: Predictor, X: np.ndarray) -> Dict[int, Dict[str, np.ndarray]]:
        if predictor.task != "classification":
            raise ValueError("NoiseProfile currently supports classification predictors only")

        X_arr = np.asarray(X)
        results: Dict[int, Dict[str, np.ndarray]] = {}
        for shots in self.sweep_shots:
            probs_list: List[np.ndarray] = []
            entropies: List[np.ndarray] = []
            for _ in range(self.n_repeats):
                probs = np.asarray(predictor.predict_proba(X_arr, shots=shots))
                probs = np.clip(probs, 1e-12, 1.0)
                probs = probs / probs.sum(axis=-1, keepdims=True)
                probs_list.append(probs)
                ent = -np.sum(probs * np.log(probs), axis=-1)
                entropies.append(ent)

            stacked_probs = np.stack(probs_list, axis=0)  # (R, N, C)
            stacked_ent = np.stack(entropies, axis=0)  # (R, N)

            results[int(shots)] = {
                "mean_prob": stacked_probs.mean(axis=0),
                "std_prob": stacked_probs.std(axis=0),
                "mean_entropy": stacked_ent.mean(axis=0),
                "std_entropy": stacked_ent.std(axis=0),
            }

        return results

ShotBootstrap dataclass

Bases: UncertaintyMethod

Repeated forward passes with (optionally) varying shots.

This method is fully model-agnostic and works for both PennyLane and Qiskit predictors, as long as they implement the :class:Predictor protocol.

Source code in quantumuq/core/methods.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@dataclass
class ShotBootstrap(UncertaintyMethod):
    """Repeated forward passes with (optionally) varying shots.

    This method is fully model-agnostic and works for both PennyLane and Qiskit
    predictors, as long as they implement the :class:`Predictor` protocol.
    """

    n_samples: int
    shots: Optional[int] = None
    shots_jitter: Optional[int] = None
    seed: Optional[int] = None

    def __post_init__(self) -> None:
        if self.n_samples <= 0:
            raise ValueError("n_samples must be positive")
        self._rng = np.random.default_rng(self.seed)

    def _sample_shots(self, base_shots: Optional[int]) -> Optional[int]:
        if base_shots is None and self.shots is None:
            return None
        base = self.shots if self.shots is not None else base_shots
        if base is None:
            return None
        if self.shots_jitter is None or self.shots_jitter <= 0:
            return base
        low = max(1, base - self.shots_jitter)
        high = base + self.shots_jitter + 1
        return int(self._rng.integers(low, high))

    def __call__(
        self, predictor: Predictor, X: np.ndarray, shots: Optional[int] = None
    ) -> PredictiveDistribution:
        X_arr = np.asarray(X)
        samples: List[np.ndarray] = []
        for _ in range(self.n_samples):
            sample_shots = self._sample_shots(shots)
            if predictor.task == "classification":
                y = predictor.predict_proba(X_arr, shots=sample_shots)
            else:
                y = predictor.predict(X_arr, shots=sample_shots)
            samples.append(np.asarray(y))
        stacked = np.stack(samples, axis=0)
        mean = stacked.mean(axis=0)
        std = stacked.std(axis=0)
        return PredictiveDistribution(samples=stacked, mean=mean, std=std)

brier(y_true, p_pred)

Multiclass Brier score.

Source code in quantumuq/core/metrics.py
17
18
19
20
21
22
23
24
25
def brier(y_true: np.ndarray, p_pred: np.ndarray) -> float:
    """Multiclass Brier score."""

    y_true = np.asarray(y_true, dtype=int)
    p_pred = np.asarray(p_pred, dtype=float)
    n_classes = p_pred.shape[1]
    one_hot = np.eye(n_classes)[y_true]
    diff = p_pred - one_hot
    return float(np.mean(np.sum(diff * diff, axis=1)))

ece(y_true, p_pred, n_bins=15)

Expected calibration error with equal-width bins.

Source code in quantumuq/core/metrics.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def ece(y_true: np.ndarray, p_pred: np.ndarray, n_bins: int = 15) -> float:
    """Expected calibration error with equal-width bins."""

    y_true = np.asarray(y_true, dtype=int)
    p_pred = np.asarray(p_pred, dtype=float)
    confidences = p_pred.max(axis=1)
    predictions = p_pred.argmax(axis=1)
    accuracies = predictions == y_true

    bin_edges = np.linspace(0.0, 1.0, n_bins + 1)
    ece_val = 0.0
    for i in range(n_bins):
        mask = (confidences >= bin_edges[i]) & (confidences < bin_edges[i + 1])
        if not np.any(mask):
            continue
        bin_conf = confidences[mask].mean()
        bin_acc = accuracies[mask].mean()
        weight = mask.mean()
        ece_val += weight * abs(bin_acc - bin_conf)
    return float(ece_val)

gaussian_nll(y_true, mean, std, eps=1e-08)

Gaussian negative log-likelihood for regression.

Source code in quantumuq/core/metrics.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def gaussian_nll(
    y_true: np.ndarray,
    mean: np.ndarray,
    std: np.ndarray,
    eps: float = 1e-8,
) -> float:
    """Gaussian negative log-likelihood for regression."""

    y_true = np.asarray(y_true, dtype=float)
    mean = np.asarray(mean, dtype=float)
    std = np.asarray(std, dtype=float)
    var = np.clip(std, eps, None) ** 2
    per_point = 0.5 * np.log(2.0 * np.pi * var) + 0.5 * ((y_true - mean) ** 2) / var
    return float(np.mean(per_point))

nll(y_true, p_pred, eps=1e-12)

Multiclass negative log-likelihood.

Source code in quantumuq/core/metrics.py
 6
 7
 8
 9
10
11
12
13
14
def nll(y_true: np.ndarray, p_pred: np.ndarray, eps: float = 1e-12) -> float:
    """Multiclass negative log-likelihood."""

    y_true = np.asarray(y_true, dtype=int)
    p_pred = np.asarray(p_pred, dtype=float)
    p_pred = np.clip(p_pred, eps, 1.0)
    idx = (np.arange(y_true.shape[0]), y_true)
    log_p = np.log(p_pred[idx])
    return float(-log_p.mean())

predictive_entropy(p_pred, eps=1e-12)

Predictive entropy from class probabilities.

Source code in quantumuq/core/metrics.py
50
51
52
53
54
55
56
def predictive_entropy(p_pred: np.ndarray, eps: float = 1e-12) -> np.ndarray:
    """Predictive entropy from class probabilities."""

    p_pred = np.asarray(p_pred, dtype=float)
    p_pred = np.clip(p_pred, eps, 1.0)
    p_pred = p_pred / p_pred.sum(axis=-1, keepdims=True)
    return -np.sum(p_pred * np.log(p_pred), axis=-1)

rmse(y_true, y_pred)

Root mean squared error.

Source code in quantumuq/core/metrics.py
59
60
61
62
63
64
def rmse(y_true: np.ndarray, y_pred: np.ndarray) -> float:
    """Root mean squared error."""

    y_true = np.asarray(y_true, dtype=float)
    y_pred = np.asarray(y_pred, dtype=float)
    return float(np.sqrt(np.mean((y_true - y_pred) ** 2)))

PredictiveDistribution dataclass

Container for predictive samples and summary statistics.

Attributes:

Name Type Description
samples ndarray

Array of samples with shape (S, N, C) for classification or (S, N, D) for regression.

mean ndarray

Mean over the sample dimension, shape (N, C) or (N, D).

std ndarray

Standard deviation over the sample dimension, same shape as mean.

Source code in quantumuq/core/predictors.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
@dataclass
class PredictiveDistribution:
    """Container for predictive samples and summary statistics.

    Attributes
    ----------
    samples:
        Array of samples with shape ``(S, N, C)`` for classification or
        ``(S, N, D)`` for regression.
    mean:
        Mean over the sample dimension, shape ``(N, C)`` or ``(N, D)``.
    std:
        Standard deviation over the sample dimension, same shape as ``mean``.
    """

    samples: np.ndarray
    mean: np.ndarray
    std: np.ndarray

    def interval(self, alpha: float) -> tuple[np.ndarray, np.ndarray]:
        """Return central prediction interval for given ``alpha``.

        Parameters
        ----------
        alpha:
            Confidence level in (0, 1). E.g. ``0.95`` for a 95% interval.
        """

        if not 0.0 < alpha < 1.0:
            raise ValueError("alpha must be in (0, 1)")

        lower_q = (1.0 - alpha) / 2.0
        upper_q = 1.0 - lower_q
        lower = np.quantile(self.samples, lower_q, axis=0)
        upper = np.quantile(self.samples, upper_q, axis=0)
        return lower, upper

    def entropy(self) -> np.ndarray:
        """Predictive entropy for classification tasks.

        Uses the mean class probabilities over samples and returns entropy
        per data point with shape ``(N,)``.
        """

        # Assume last dimension is class dimension.
        mean_probs = self.mean
        # Normalize defensively.
        mean_probs = np.clip(mean_probs, 1e-12, 1.0)
        mean_probs = mean_probs / mean_probs.sum(axis=-1, keepdims=True)
        return -np.sum(mean_probs * np.log(mean_probs), axis=-1)

entropy()

Predictive entropy for classification tasks.

Uses the mean class probabilities over samples and returns entropy per data point with shape (N,).

Source code in quantumuq/core/predictors.py
68
69
70
71
72
73
74
75
76
77
78
79
80
def entropy(self) -> np.ndarray:
    """Predictive entropy for classification tasks.

    Uses the mean class probabilities over samples and returns entropy
    per data point with shape ``(N,)``.
    """

    # Assume last dimension is class dimension.
    mean_probs = self.mean
    # Normalize defensively.
    mean_probs = np.clip(mean_probs, 1e-12, 1.0)
    mean_probs = mean_probs / mean_probs.sum(axis=-1, keepdims=True)
    return -np.sum(mean_probs * np.log(mean_probs), axis=-1)

interval(alpha)

Return central prediction interval for given alpha.

Parameters:

Name Type Description Default
alpha float

Confidence level in (0, 1). E.g. 0.95 for a 95% interval.

required
Source code in quantumuq/core/predictors.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def interval(self, alpha: float) -> tuple[np.ndarray, np.ndarray]:
    """Return central prediction interval for given ``alpha``.

    Parameters
    ----------
    alpha:
        Confidence level in (0, 1). E.g. ``0.95`` for a 95% interval.
    """

    if not 0.0 < alpha < 1.0:
        raise ValueError("alpha must be in (0, 1)")

    lower_q = (1.0 - alpha) / 2.0
    upper_q = 1.0 - lower_q
    lower = np.quantile(self.samples, lower_q, axis=0)
    upper = np.quantile(self.samples, upper_q, axis=0)
    return lower, upper

Predictor

Bases: Protocol

Protocol for quantum predictors used by UQ methods.

Implementations must expose:

  • task: either "classification" or "regression".
  • predict(X, shots=None): point predictions.
  • predict_proba(X, shots=None): class probabilities for classification.
Source code in quantumuq/core/predictors.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@runtime_checkable
class Predictor(Protocol):
    """Protocol for quantum predictors used by UQ methods.

    Implementations must expose:

    - ``task``: either ``"classification"`` or ``"regression"``.
    - ``predict(X, shots=None)``: point predictions.
    - ``predict_proba(X, shots=None)``: class probabilities for classification.
    """

    task: TaskType

    def predict(self, X: np.ndarray, shots: Optional[int] = None) -> np.ndarray:  # pragma: no cover - protocol
        ...

    def predict_proba(self, X: np.ndarray, shots: Optional[int] = None) -> np.ndarray:  # pragma: no cover - protocol
        ...

UQModel

Wrap a base predictor with an uncertainty method.

Parameters:

Name Type Description Default
base_predictor Predictor

Object implementing the :class:Predictor protocol.

required
method 'UncertaintyMethod'

Callable that given (predictor, X, shots) returns a :class:PredictiveDistribution.

required
Source code in quantumuq/core/predictors.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
class UQModel:
    """Wrap a base predictor with an uncertainty method.

    Parameters
    ----------
    base_predictor:
        Object implementing the :class:`Predictor` protocol.
    method:
        Callable that given ``(predictor, X, shots)`` returns a
        :class:`PredictiveDistribution`.
    """

    def __init__(self, base_predictor: Predictor, method: "UncertaintyMethod") -> None:
        self.base_predictor = base_predictor
        self.method = method

    @property
    def task(self) -> TaskType:
        return self.base_predictor.task

    def predict(self, X: np.ndarray, shots: Optional[int] = None) -> np.ndarray:
        return self.base_predictor.predict(X, shots=shots)

    def predict_proba(self, X: np.ndarray, shots: Optional[int] = None) -> np.ndarray:
        return self.base_predictor.predict_proba(X, shots=shots)

    def predict_dist(
        self, X: np.ndarray, shots: Optional[int] = None
    ) -> PredictiveDistribution:
        return self.method(self.base_predictor, X, shots=shots)

UncertaintyMethod

Bases: Protocol

Protocol for uncertainty methods compatible with :class:UQModel.

Source code in quantumuq/core/predictors.py
115
116
117
118
119
120
121
class UncertaintyMethod(Protocol):
    """Protocol for uncertainty methods compatible with :class:`UQModel`."""

    def __call__(
        self, predictor: Predictor, X: np.ndarray, shots: Optional[int] = None
    ) -> PredictiveDistribution:  # pragma: no cover - protocol
        ...

stack_ensemble_samples(samples)

Utility to convert a sequence of per-model predictions into a distribution.

Parameters:

Name Type Description Default
samples Sequence[ndarray]

Sequence of arrays of identical shape (N, C) or (N, D).

required
Source code in quantumuq/core/predictors.py
124
125
126
127
128
129
130
131
132
133
134
135
136
def stack_ensemble_samples(samples: Sequence[np.ndarray]) -> PredictiveDistribution:
    """Utility to convert a sequence of per-model predictions into a distribution.

    Parameters
    ----------
    samples:
        Sequence of arrays of identical shape ``(N, C)`` or ``(N, D)``.
    """

    arr = np.stack(samples, axis=0)
    mean = arr.mean(axis=0)
    std = arr.std(axis=0)
    return PredictiveDistribution(samples=arr, mean=mean, std=std)

Adapters

wrap_qnode(qnode, task, n_classes=None, params=None, postprocess=None, batched=False)

Wrap a PennyLane QNode as a QuantumUQ predictor.

Parameters:

Name Type Description Default
qnode Any

PennyLane QNode. For classification it should return probabilities, logits, or expectations convertible to probabilities.

required
task Literal['classification', 'regression']

Either "classification" or "regression".

required
n_classes Optional[int]

Number of classes for classification tasks.

None
params Optional[Any]

Optional trainable parameters; the QNode is expected to have signature qnode(features, params) when this is provided.

None
postprocess Optional[PostprocessFn]

Optional function mapping raw outputs to probabilities (classification) or predictions (regression). If not provided for classification, a defensive normalization is applied, or softmax when outputs look like logits.

None
batched bool

If True, the QNode is expected to handle a batch of inputs at once.

False
Source code in quantumuq/adapters/pennylane_adapter.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def wrap_qnode(
    qnode: Any,
    task: Literal["classification", "regression"],
    n_classes: Optional[int] = None,
    params: Optional[Any] = None,
    postprocess: Optional[PostprocessFn] = None,
    batched: bool = False,
) -> _QNodePredictor:
    """Wrap a PennyLane QNode as a QuantumUQ predictor.

    Parameters
    ----------
    qnode:
        PennyLane QNode. For classification it should return probabilities,
        logits, or expectations convertible to probabilities.
    task:
        Either ``"classification"`` or ``"regression"``.
    n_classes:
        Number of classes for classification tasks.
    params:
        Optional trainable parameters; the QNode is expected to have signature
        ``qnode(features, params)`` when this is provided.
    postprocess:
        Optional function mapping raw outputs to probabilities (classification)
        or predictions (regression). If not provided for classification, a
        defensive normalization is applied, or softmax when outputs look like
        logits.
    batched:
        If ``True``, the QNode is expected to handle a batch of inputs at once.
    """

    if task not in ("classification", "regression"):
        raise ValueError('task must be "classification" or "regression"')
    if task == "classification" and n_classes is None:
        raise ValueError("n_classes must be provided for classification tasks")

    # Import lazily to avoid hard dependency at import time.
    try:
        import pennylane as qml  # type: ignore  # noqa: F401
    except Exception as exc:  # pragma: no cover - import guard
        raise RuntimeError(
            "wrap_qnode requires PennyLane to be installed: `pip install pennylane`"
        ) from exc

    if task == "classification" and postprocess is None:
        # Default classification behaviour: try probabilities, otherwise softmax.
        def default_postprocess(raw: np.ndarray) -> np.ndarray:
            arr = np.asarray(raw)
            # If already looks like probabilities (non-negative and sum ~ 1).
            if np.all(arr >= -1e-8):
                sums = arr.sum(axis=-1, keepdims=True)
                # Avoid division by zero.
                sums = np.clip(sums, 1e-12, None)
                return arr / sums
            return _softmax(arr)

        postprocess = default_postprocess

    return _QNodePredictor(
        qnode=qnode,
        task=task,
        n_classes=n_classes,
        params=params,
        postprocess=postprocess,
        batched=batched,
    )

wrap_qiskit_estimator(estimator, circuit, observables, task, n_classes=None, params=None, feature_map=None, postprocess=None)

Wrap a Qiskit Estimator primitive as a predictor.

For classification, use one observable per class to obtain logits, and apply a softmax by default. For regression, use a single observable per data point and return expectations directly.

Source code in quantumuq/adapters/qiskit_adapter.py
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
def wrap_qiskit_estimator(
    estimator: Any,
    circuit: Any,
    observables: Sequence[Any],
    task: Literal["classification", "regression"],
    n_classes: Optional[int] = None,
    params: Optional[Any] = None,
    feature_map: Optional[FeatureMapFn] = None,
    postprocess: Optional[PostprocessFn] = None,
) -> _QiskitEstimatorPredictor:
    """Wrap a Qiskit Estimator primitive as a predictor.

    For classification, use one observable per class to obtain logits, and
    apply a softmax by default. For regression, use a single observable per
    data point and return expectations directly.
    """

    if task == "classification" and n_classes is None:
        n_classes = len(observables)
    if task == "classification" and (n_classes is None or n_classes <= 1):
        raise ValueError("n_classes must be >= 2 for classification")

    _import_qiskit_primitives()

    if task == "classification" and postprocess is None:
        postprocess = _softmax

    return _QiskitEstimatorPredictor(
        estimator=estimator,
        circuit=circuit,
        observables=observables,
        task=task,
        n_classes=n_classes,
        params=params,
        feature_map=feature_map,
        postprocess=postprocess,
    )

wrap_qiskit_sampler(sampler, circuit, task='classification', n_classes=2, params=None, feature_map=None, bitstring_to_class=None)

Wrap a Qiskit Sampler primitive as a classification predictor.

Source code in quantumuq/adapters/qiskit_adapter.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
def wrap_qiskit_sampler(
    sampler: Any,
    circuit: Any,
    task: Literal["classification"] = "classification",
    n_classes: int = 2,
    params: Optional[Any] = None,
    feature_map: Optional[FeatureMapFn] = None,
    bitstring_to_class: Optional[BitstringToClassFn] = None,
) -> _QiskitSamplerPredictor:
    """Wrap a Qiskit Sampler primitive as a classification predictor."""

    if task != "classification":
        raise ValueError("wrap_qiskit_sampler currently supports classification only")
    if n_classes <= 1:
        raise ValueError("n_classes must be >= 2 for classification")

    # Lazy import to keep import-time surface small.
    _import_qiskit_primitives()

    return _QiskitSamplerPredictor(
        sampler=sampler,
        circuit=circuit,
        task=task,
        n_classes=n_classes,
        params=params,
        feature_map=feature_map,
        bitstring_to_class=bitstring_to_class,
    )