Skip to content

API Reference

Documentación generada automáticamente desde el código fuente de qiskit-data-reuploading.


qdr.models

DataReuploadingClassifier

qdr.models.classifier.DataReuploadingClassifier(n_qubits: int = 2, n_layers: int = 5, encoding: str = 'rx_ry_rz', entanglement: str = 'full', optimizer: str = 'COBYLA', backend: Any = None, shots: int | None = None, max_iter: int = 100, learning_rate: float = 0.01, seed: int | None = None)

Bases: BaseEstimator, ClassifierMixin

Quantum classifier based on the data re-uploading technique.

Implements a full sklearn-compatible estimator (fit, predict, predict_proba, score, save / load) backed by Qiskit 2.x V2 primitives.

Parameters

n_qubits : int, optional Number of qubits. Default 2. n_layers : int, optional Number of data-reuploading layers. Default 5. encoding : str, optional Rotation encoding — "rx", "ry", "rz", or "rx_ry_rz" (default). entanglement : str, optional Entanglement pattern — "none", "linear", "circular", "full" (default). optimizer : str, optional Optimizer — "SPSA", "COBYLA" (default), or "ADAM". backend : None, optional Must be None during fit. Hardware execution is exposed through :func:qdr.hardware.run_on_ibm_backend so that real-backend cost, authentication, transpilation, and batching are explicit. shots : int or None, optional None = exact statevector simulation. Positive int uses :class:~qiskit_aer.primitives.EstimatorV2 with that shot count. max_iter : int, optional Maximum optimiser iterations. Default 100. learning_rate : float, optional Learning rate (ADAM only). Default 0.01. seed : int or None, optional Random seed for weight initialisation and stochastic optimisers.

Attributes

weights_ : np.ndarray Trained parameters, shape (n_weights,). classes_ : np.ndarray Unique class labels seen during fit. loss_history_ : list[float] Per-iteration loss values recorded during training. n_features_in_ : int Number of features seen during fit.

Notes

The underlying circuit uses data re-uploading gates whose angles are

``angle = w[l, i, r] + x[feat_idx(l, i, r)]``

where feat_idx cycles modulo n_features in :class:qdr.circuits.DataReuploadingCircuit. Binary classification uses a single <Z_0> observable. Multiclass classification uses one local Z observable per class and requires n_qubits >= n_classes.

Source code in qiskit-data-reuploading-src/qdr/models/classifier.py
def __init__(
    self,
    n_qubits: int = 2,
    n_layers: int = 5,
    encoding: str = "rx_ry_rz",
    entanglement: str = "full",
    optimizer: str = "COBYLA",
    backend: Any = None,
    shots: int | None = None,
    max_iter: int = 100,
    learning_rate: float = 0.01,
    seed: int | None = None,
) -> None:
    self.n_qubits = n_qubits
    self.n_layers = n_layers
    self.encoding = encoding
    self.entanglement = entanglement
    self.optimizer = optimizer
    self.backend = backend
    self.shots = shots
    self.max_iter = max_iter
    self.learning_rate = learning_rate
    self.seed = seed

fit(X: np.ndarray, y: np.ndarray) -> 'DataReuploadingClassifier'

Fit the classifier to training data.

Parameters

X : array-like of shape (n_samples, n_features) Training features. y : array-like of shape (n_samples,) Class labels.

Returns

self

Raises

ValueError If X is not two-dimensional, contains non-finite values, y has the wrong shape or length, fewer than two classes are present, multiclass output requests more classes than qubits, the feature count exceeds the available data-uploading slots, or the optimizer or circuit configuration is invalid. ImportError If shots is a positive integer and qiskit-aer is not installed.

Source code in qiskit-data-reuploading-src/qdr/models/classifier.py
def fit(self, X: np.ndarray, y: np.ndarray) -> "DataReuploadingClassifier":
    """Fit the classifier to training data.

    Parameters
    ----------
    X : array-like of shape (n_samples, n_features)
        Training features.
    y : array-like of shape (n_samples,)
        Class labels.

    Returns
    -------
    self

    Raises
    ------
    ValueError
        If ``X`` is not two-dimensional, contains non-finite values, ``y``
        has the wrong shape or length, fewer than two classes are present,
        multiclass output requests more classes than qubits, the feature
        count exceeds the available data-uploading slots, or the optimizer
        or circuit configuration is invalid.
    ImportError
        If ``shots`` is a positive integer and ``qiskit-aer`` is not
        installed.
    """
    X = self._validate_X(X, reset=True)
    y = self._validate_y(y, X.shape[0])

    self._label_encoder_ = LabelEncoder()
    y_int = self._label_encoder_.fit_transform(y)
    self.classes_ = self._label_encoder_.classes_
    self.n_features_in_ = X.shape[1]
    n_classes = len(self.classes_)
    if n_classes < 2:
        raise ValueError(f"Classifier requires at least 2 classes, got n_classes={n_classes}.")
    self._validate_feature_capacity(self.n_features_in_)
    if n_classes > 2 and self._has_valid_qubit_count() and n_classes > self.n_qubits:
        raise ValueError(
            f"Para {n_classes} clases se necesitan al menos {n_classes} qubits. "
            f"Actual: n_qubits={self.n_qubits}."
        )

    # Map to {-1, +1} for binary, or one-hot-like for multiclass
    if n_classes == 2:
        y_mapped: np.ndarray = 2.0 * y_int - 1.0  # {0,1} → {-1,+1}
    else:
        # One-hot mapped to {-1, +1}
        y_one_hot = np.full((len(y_int), n_classes), -1.0)
        for i, label in enumerate(y_int):
            y_one_hot[i, label] = 1.0
        y_mapped = y_one_hot

    # Build circuit and estimator
    self._circuit_ = DataReuploadingCircuit(
        n_qubits=self.n_qubits,
        n_layers=self.n_layers,
        n_features=self.n_features_in_,
        encoding=self.encoding,
        entanglement=self.entanglement,
    )
    self._circuit_.build_circuit()
    self._estimator_ = self._build_estimator()
    self._observables_ = self._make_observable(n_classes)

    # Initialise weights
    rng = np.random.default_rng(self.seed)
    init_weights = rng.uniform(-np.pi, np.pi, self._circuit_.n_weights)

    self._last_loss_: float = 0.0

    def loss_fn(w: np.ndarray) -> float:
        val = self._loss(w, X, y_mapped, self._observables_)
        self._last_loss_ = val
        return val

    # Run optimisation
    self.loss_history_: list[float] = []

    def _record(w: np.ndarray) -> None:
        self.loss_history_.append(self._last_loss_)

    if self.optimizer == "COBYLA":
        opt = COBYLA(maxiter=self.max_iter)
        result = opt.minimize(loss_fn, init_weights, callback=_record)

    elif self.optimizer == "SPSA":
        opt = SPSA(maxiter=self.max_iter, seed=self.seed)
        result = opt.minimize(loss_fn, init_weights, callback=_record)

    elif self.optimizer == "ADAM":
        grad_fn = self._make_grad_fn(X, y_mapped, self._observables_)
        opt = ADAM(maxiter=self.max_iter, lr=self.learning_rate)
        result = opt.minimize(loss_fn, init_weights, gradient_fn=grad_fn, callback=_record)

    else:
        raise ValueError(
            f"optimizer must be one of ['COBYLA', 'SPSA', 'ADAM'], got '{self.optimizer}'"
        )

    self.weights_ = result.x
    if result.loss_history:
        self.loss_history_ = result.loss_history

    return self

load(path: str | Path) -> 'DataReuploadingClassifier' classmethod

Load a previously saved model.

Parameters

path : str or Path Path to a .pkl file created by :meth:save.

Returns

DataReuploadingClassifier

Raises

FileNotFoundError If path does not exist. ValueError If the saved parameters are inconsistent with the circuit or observable constraints.

Source code in qiskit-data-reuploading-src/qdr/models/classifier.py
@classmethod
def load(
    cls: type["DataReuploadingClassifier"],
    path: str | Path,
) -> "DataReuploadingClassifier":
    """Load a previously saved model.

    Parameters
    ----------
    path : str or Path
        Path to a ``.pkl`` file created by :meth:`save`.

    Returns
    -------
    DataReuploadingClassifier

    Raises
    ------
    FileNotFoundError
        If ``path`` does not exist.
    ValueError
        If the saved parameters are inconsistent with the circuit or
        observable constraints.
    """
    payload = pickle.loads(Path(path).read_bytes())
    model = cls(**payload["params"])
    # Reconstruct internal state without re-fitting
    model.weights_ = payload["weights_"]
    model.classes_ = payload["classes_"]
    model.n_features_in_ = payload["n_features_in_"]
    model.loss_history_ = payload["loss_history_"]
    model._label_encoder_ = LabelEncoder()
    model._label_encoder_.classes_ = model.classes_

    n_classes = len(model.classes_)
    model._circuit_ = DataReuploadingCircuit(
        n_qubits=model.n_qubits,
        n_layers=model.n_layers,
        n_features=model.n_features_in_,
        encoding=model.encoding,
        entanglement=model.entanglement,
    )
    model._circuit_.build_circuit()
    model._estimator_ = model._build_estimator()
    model._observables_ = model._make_observable(n_classes)
    return model

predict(X: np.ndarray) -> np.ndarray

Predict class labels.

Parameters

X : array-like of shape (n_samples, n_features)

Returns

np.ndarray of shape (n_samples,)

Raises

ValueError If X fails the same validation used by :meth:predict_proba. sklearn.exceptions.NotFittedError If the classifier has not been fitted.

Source code in qiskit-data-reuploading-src/qdr/models/classifier.py
def predict(self, X: np.ndarray) -> np.ndarray:
    """Predict class labels.

    Parameters
    ----------
    X : array-like of shape (n_samples, n_features)

    Returns
    -------
    np.ndarray of shape (n_samples,)

    Raises
    ------
    ValueError
        If ``X`` fails the same validation used by :meth:`predict_proba`.
    sklearn.exceptions.NotFittedError
        If the classifier has not been fitted.
    """
    proba = self.predict_proba(X)
    return self.classes_[np.argmax(proba, axis=1)]

predict_proba(X: np.ndarray) -> np.ndarray

Predict class probabilities.

Parameters

X : array-like of shape (n_samples, n_features)

Returns

np.ndarray of shape (n_samples, n_classes)

Raises

ValueError If X contains non-finite values or its feature count differs from the data used in fit. sklearn.exceptions.NotFittedError If the classifier has not been fitted.

Source code in qiskit-data-reuploading-src/qdr/models/classifier.py
def predict_proba(self, X: np.ndarray) -> np.ndarray:
    """Predict class probabilities.

    Parameters
    ----------
    X : array-like of shape (n_samples, n_features)

    Returns
    -------
    np.ndarray of shape (n_samples, n_classes)

    Raises
    ------
    ValueError
        If ``X`` contains non-finite values or its feature count differs
        from the data used in ``fit``.
    sklearn.exceptions.NotFittedError
        If the classifier has not been fitted.
    """
    check_is_fitted(self, "weights_")
    X = self._validate_X(X, reset=False)
    evs = self._evaluate_batch(self.weights_, X, self._observables_)

    if evs.ndim == 1:
        # Binary: map [-1,1] → [0,1]
        p1 = np.clip((evs + 1.0) / 2.0, 0.0, 1.0)
        return np.column_stack([1.0 - p1, p1])

    # Multiclass: softmax of expectations
    def softmax(z: np.ndarray) -> np.ndarray:
        e = np.exp(z - z.max(axis=1, keepdims=True))
        return e / e.sum(axis=1, keepdims=True)

    return softmax(evs)

save(path: str | Path) -> None

Serialise the fitted model to a file.

Parameters

path : str or Path Destination file path (e.g. "model.pkl").

Raises

sklearn.exceptions.NotFittedError If the classifier has not been fitted.

Source code in qiskit-data-reuploading-src/qdr/models/classifier.py
def save(self, path: str | Path) -> None:
    """Serialise the fitted model to a file.

    Parameters
    ----------
    path : str or Path
        Destination file path (e.g. ``"model.pkl"``).

    Raises
    ------
    sklearn.exceptions.NotFittedError
        If the classifier has not been fitted.
    """
    check_is_fitted(self, "weights_")
    payload = {
        "params": self.get_params(),
        "weights_": self.weights_,
        "classes_": self.classes_,
        "n_features_in_": self.n_features_in_,
        "loss_history_": self.loss_history_,
    }
    Path(path).write_bytes(pickle.dumps(payload))

DataReuploadingRegressor

qdr.models.regressor.DataReuploadingRegressor(n_qubits: int = 2, n_layers: int = 5, encoding: str = 'rx_ry_rz', entanglement: str = 'full', optimizer: str = 'COBYLA', backend: Any = None, shots: int | None = None, max_iter: int = 100, learning_rate: float = 0.01, seed: int | None = None)

Bases: BaseEstimator, RegressorMixin

Quantum regressor based on the data re-uploading technique.

Predicts a continuous scalar output as the expectation value of a Z observable, scaled to the target range observed during training.

Parameters

n_qubits : int, optional Number of qubits. Default 2. n_layers : int, optional Number of data-reuploading layers. Default 5. encoding : str, optional Rotation encoding. Default "rx_ry_rz". entanglement : str, optional Entanglement pattern. Default "full". optimizer : str, optional "SPSA", "COBYLA" (default), or "ADAM". backend : None, optional Must be None during fit. Hardware execution is exposed through :func:qdr.hardware.run_on_ibm_backend so that real-backend cost, authentication, transpilation, and batching are explicit. shots : int or None, optional None = exact statevector simulation. Positive int uses :class:~qiskit_aer.primitives.EstimatorV2 with that shot count. max_iter : int, optional Maximum optimiser iterations. Default 100. learning_rate : float, optional Learning rate for ADAM. Default 0.01. seed : int or None, optional Random seed.

Attributes

weights_ : np.ndarray Trained parameters. loss_history_ : list[float] Per-iteration loss values. n_features_in_ : int Number of features seen during fit. y_min_ : float Minimum target value seen during training. y_max_ : float Maximum target value seen during training.

Notes

The underlying circuit uses gates whose angles are

``angle = w[l, i, r] + x[feat_idx(l, i, r)]``

and predicts one scalar from a single <Z_0> observable. Outputs are rescaled from [-1, 1] to the target range observed during fit. This estimator is intentionally single-output; multi-output regression requires a separate observable design and is not implemented.

Source code in qiskit-data-reuploading-src/qdr/models/regressor.py
def __init__(
    self,
    n_qubits: int = 2,
    n_layers: int = 5,
    encoding: str = "rx_ry_rz",
    entanglement: str = "full",
    optimizer: str = "COBYLA",
    backend: Any = None,
    shots: int | None = None,
    max_iter: int = 100,
    learning_rate: float = 0.01,
    seed: int | None = None,
) -> None:
    self.n_qubits = n_qubits
    self.n_layers = n_layers
    self.encoding = encoding
    self.entanglement = entanglement
    self.optimizer = optimizer
    self.backend = backend
    self.shots = shots
    self.max_iter = max_iter
    self.learning_rate = learning_rate
    self.seed = seed

fit(X: np.ndarray, y: np.ndarray) -> 'DataReuploadingRegressor'

Fit the regressor.

Parameters

X : array-like of shape (n_samples, n_features) y : array-like of shape (n_samples,)

Returns

self

Raises

ValueError If X is not two-dimensional, contains non-finite values, y has the wrong shape or length, targets contain non-finite values, the feature count exceeds the available data-uploading slots, or the optimizer or circuit configuration is invalid. ImportError If shots is a positive integer and qiskit-aer is not installed.

Source code in qiskit-data-reuploading-src/qdr/models/regressor.py
def fit(self, X: np.ndarray, y: np.ndarray) -> "DataReuploadingRegressor":
    """Fit the regressor.

    Parameters
    ----------
    X : array-like of shape (n_samples, n_features)
    y : array-like of shape (n_samples,)

    Returns
    -------
    self

    Raises
    ------
    ValueError
        If ``X`` is not two-dimensional, contains non-finite values, ``y``
        has the wrong shape or length, targets contain non-finite values,
        the feature count exceeds the available data-uploading slots, or
        the optimizer or circuit configuration is invalid.
    ImportError
        If ``shots`` is a positive integer and ``qiskit-aer`` is not
        installed.
    """
    X = self._validate_X(X, reset=True)
    y = self._validate_y(y, X.shape[0])

    self.n_features_in_ = X.shape[1]
    self._validate_feature_capacity(self.n_features_in_)
    self.y_min_ = float(y.min())
    self.y_max_ = float(y.max())
    y_scaled = self._scale_from_target(y)  # ∈ [-1, 1]

    self._circuit_ = DataReuploadingCircuit(
        n_qubits=self.n_qubits,
        n_layers=self.n_layers,
        n_features=self.n_features_in_,
        encoding=self.encoding,
        entanglement=self.entanglement,
    )
    self._circuit_.build_circuit()
    self._estimator_ = self._build_estimator()
    self._obs_ = self._observable()

    rng = np.random.default_rng(self.seed)
    init_weights = rng.uniform(-np.pi, np.pi, self._circuit_.n_weights)

    self._last_loss_: float = 0.0

    def loss_fn(w: np.ndarray) -> float:
        evs = self._evaluate_batch(w, X)
        val = float(np.mean((evs - y_scaled) ** 2))
        self._last_loss_ = val
        return val

    self.loss_history_: list[float] = []

    def _record(w: np.ndarray) -> None:
        self.loss_history_.append(self._last_loss_)

    if self.optimizer == "COBYLA":
        result = COBYLA(maxiter=self.max_iter).minimize(loss_fn, init_weights, callback=_record)
    elif self.optimizer == "SPSA":
        result = SPSA(maxiter=self.max_iter, seed=self.seed).minimize(
            loss_fn, init_weights, callback=_record
        )
    elif self.optimizer == "ADAM":
        from qdr.training.gradients import ParameterShiftGradient

        psr = ParameterShiftGradient(self._circuit_, self._obs_, estimator=self._estimator_)

        def grad_fn(w: np.ndarray) -> np.ndarray:
            return psr.compute(w, X, y_scaled)

        result = ADAM(maxiter=self.max_iter, lr=self.learning_rate).minimize(
            loss_fn, init_weights, gradient_fn=grad_fn, callback=_record
        )
    else:
        raise ValueError(f"Unknown optimizer '{self.optimizer}'")

    self.weights_ = result.x
    if result.loss_history:
        self.loss_history_ = result.loss_history
    return self

load(path: str | Path) -> 'DataReuploadingRegressor' classmethod

Load a saved model.

Parameters

path : str or Path Path to a file created by :meth:save.

Returns

DataReuploadingRegressor

Raises

FileNotFoundError If path does not exist. ValueError If the saved parameters are inconsistent with the circuit constraints.

Source code in qiskit-data-reuploading-src/qdr/models/regressor.py
@classmethod
def load(
    cls: type["DataReuploadingRegressor"],
    path: str | Path,
) -> "DataReuploadingRegressor":
    """Load a saved model.

    Parameters
    ----------
    path : str or Path
        Path to a file created by :meth:`save`.

    Returns
    -------
    DataReuploadingRegressor

    Raises
    ------
    FileNotFoundError
        If ``path`` does not exist.
    ValueError
        If the saved parameters are inconsistent with the circuit
        constraints.
    """
    payload = pickle.loads(Path(path).read_bytes())
    model = cls(**payload["params"])
    model.weights_ = payload["weights_"]
    model.n_features_in_ = payload["n_features_in_"]
    model.y_min_ = payload["y_min_"]
    model.y_max_ = payload["y_max_"]
    model.loss_history_ = payload["loss_history_"]
    model._circuit_ = DataReuploadingCircuit(
        n_qubits=model.n_qubits,
        n_layers=model.n_layers,
        n_features=model.n_features_in_,
        encoding=model.encoding,
        entanglement=model.entanglement,
    )
    model._circuit_.build_circuit()
    model._estimator_ = model._build_estimator()
    model._obs_ = model._observable()
    return model

predict(X: np.ndarray) -> np.ndarray

Predict continuous target values.

Parameters

X : array-like of shape (n_samples, n_features)

Returns

np.ndarray of shape (n_samples,)

Raises

ValueError If X contains non-finite values or its feature count differs from the data used in fit. sklearn.exceptions.NotFittedError If the regressor has not been fitted.

Source code in qiskit-data-reuploading-src/qdr/models/regressor.py
def predict(self, X: np.ndarray) -> np.ndarray:
    """Predict continuous target values.

    Parameters
    ----------
    X : array-like of shape (n_samples, n_features)

    Returns
    -------
    np.ndarray of shape (n_samples,)

    Raises
    ------
    ValueError
        If ``X`` contains non-finite values or its feature count differs
        from the data used in ``fit``.
    sklearn.exceptions.NotFittedError
        If the regressor has not been fitted.
    """
    check_is_fitted(self, "weights_")
    X = self._validate_X(X, reset=False)
    evs = self._evaluate_batch(self.weights_, X)
    return self._scale_to_target(evs)

save(path: str | Path) -> None

Serialise the fitted model.

Parameters

path : str or Path Destination file path.

Raises

sklearn.exceptions.NotFittedError If the regressor has not been fitted.

Source code in qiskit-data-reuploading-src/qdr/models/regressor.py
def save(self, path: str | Path) -> None:
    """Serialise the fitted model.

    Parameters
    ----------
    path : str or Path
        Destination file path.

    Raises
    ------
    sklearn.exceptions.NotFittedError
        If the regressor has not been fitted.
    """
    check_is_fitted(self, "weights_")
    payload = {
        "params": self.get_params(),
        "weights_": self.weights_,
        "n_features_in_": self.n_features_in_,
        "y_min_": self.y_min_,
        "y_max_": self.y_max_,
        "loss_history_": self.loss_history_,
    }
    Path(path).write_bytes(pickle.dumps(payload))

qdr.circuits

DataReuploadingCircuit

qdr.circuits.data_reuploading.DataReuploadingCircuit(n_qubits: int, n_layers: int, n_features: int, encoding: str = 'rx_ry_rz', entanglement: str = 'full')

Parameterized quantum circuit implementing the data re-uploading technique.

Each layer applies rotation gates whose angles are the sum of a trainable weight and a (cyclically indexed) input feature, followed by entanglement gates. Stacking n_layers of such blocks allows the circuit to act as a universal quantum classifier (Perez-Salinas et al., 2020).

Parameters

n_qubits : int Number of qubits. n_layers : int Number of data-reuploading layers. n_features : int Number of classical input features. encoding : str, optional Rotation scheme — "rx", "ry", "rz", or "rx_ry_rz" (default). entanglement : str, optional Entanglement pattern — "none", "linear", "circular", or "full" (default).

Notes

The angle of each rotation gate in layer l, qubit i, and rotation axis r is

``angle = w[l, i, r] + x[feat_idx(l, i, r)]``

where feat_idx cycles modulo n_features; see :meth:build_circuit for the exact assignment rule. The circuit requires n_features <= n_layers * n_qubits * n_rotations so every declared input feature is represented by at least one gate.

Source code in qiskit-data-reuploading-src/qdr/circuits/data_reuploading.py
def __init__(
    self,
    n_qubits: int,
    n_layers: int,
    n_features: int,
    encoding: str = "rx_ry_rz",
    entanglement: str = "full",
) -> None:
    n_qubits = self._validate_positive_int("n_qubits", n_qubits)
    n_layers = self._validate_positive_int("n_layers", n_layers)
    n_features = self._validate_positive_int("n_features", n_features)
    if encoding not in self._VALID_ENCODINGS:
        raise ValueError(
            f"encoding must be one of {sorted(self._VALID_ENCODINGS)}, got '{encoding}'"
        )
    if entanglement not in self._VALID_ENTANGLEMENTS:
        raise ValueError(
            f"entanglement must be one of {sorted(self._VALID_ENTANGLEMENTS)}, got '{entanglement}'"
        )
    self.n_qubits = n_qubits
    self.n_layers = n_layers
    self.n_features = n_features
    self.encoding = encoding
    self.entanglement = entanglement

    self._rotations: tuple[str, ...] = ENCODING_GATES[encoding]
    self._n_rots: int = len(self._rotations)

    # n_weights = layers × qubits × rotations-per-qubit
    self.n_weights: int = n_layers * n_qubits * self._n_rots
    if n_features > self.n_weights:
        raise ValueError(
            f"n_features={n_features} exceeds the number of encoding slots "
            f"({self.n_weights}). Increase n_layers, n_qubits, or encoding "
            "richness so every feature is uploaded at least once."
        )

    # 'w' sorts before 'x', so weights precede inputs in circuit.parameters
    self.weight_params: ParameterVector = ParameterVector("w", self.n_weights)
    self.input_params: ParameterVector = ParameterVector("x", n_features)

    self._circuit: QuantumCircuit | None = None

circuit: QuantumCircuit property

The built :class:~qiskit.QuantumCircuit, building it on first access.

build_circuit() -> QuantumCircuit

Build and return the parameterized quantum circuit.

Features are assigned deterministically and cyclically to the encoding slots:

``feat_idx = slot_idx % n_features``

where slot_idx = layer * n_qubits * n_rots + qubit * n_rots + rot_idx. If n_features < n_slots, features are reused. For example, n_features=4 and n_slots=18 uses each feature 4 or 5 times. With n_features=30 and n_slots=36, features 0-5 are used twice and features 6-29 once, so the first features receive one extra slot. Configurations with n_features > n_slots are rejected at construction time because they would leave trailing features outside the circuit.

Returns

QuantumCircuit The data re-uploading circuit (no measurements).

Notes

The angle of each rotation gate is angle = w + x. Data and trainable weights are mixed inside the same gate, which is the defining feature of data re-uploading.

Source code in qiskit-data-reuploading-src/qdr/circuits/data_reuploading.py
def build_circuit(self) -> QuantumCircuit:
    """Build and return the parameterized quantum circuit.

    Features are assigned deterministically and cyclically to the encoding
    slots:

        ``feat_idx = slot_idx % n_features``

    where ``slot_idx = layer * n_qubits * n_rots + qubit * n_rots + rot_idx``.
    If ``n_features < n_slots``, features are reused. For example,
    ``n_features=4`` and ``n_slots=18`` uses each feature 4 or 5 times.
    With ``n_features=30`` and ``n_slots=36``, features 0-5 are used twice
    and features 6-29 once, so the first features receive one extra slot.
    Configurations with ``n_features > n_slots`` are rejected at
    construction time because they would leave trailing features outside
    the circuit.

    Returns
    -------
    QuantumCircuit
        The data re-uploading circuit (no measurements).

    Notes
    -----
    The angle of each rotation gate is ``angle = w + x``. Data and
    trainable weights are mixed inside the same gate, which is the defining
    feature of data re-uploading.
    """
    qc = QuantumCircuit(self.n_qubits)
    weight_idx = 0

    for layer in range(self.n_layers):
        # --- encoding block ---
        for qubit in range(self.n_qubits):
            for rot_idx, rot in enumerate(self._rotations):
                feat_idx = (
                    layer * self.n_qubits * self._n_rots
                    + qubit * self._n_rots
                    + rot_idx
                ) % self.n_features
                angle = self.weight_params[weight_idx] + self.input_params[feat_idx]
                getattr(qc, rot)(angle, qubit)
                weight_idx += 1

        # --- entanglement block ---
        if self.n_qubits > 1:
            self._add_entanglement(qc)

    self._circuit = qc
    return qc

draw(**kwargs) -> Any

Draw the circuit (delegates to :meth:QuantumCircuit.draw).

Source code in qiskit-data-reuploading-src/qdr/circuits/data_reuploading.py
def draw(self, **kwargs) -> Any:
    """Draw the circuit (delegates to :meth:`QuantumCircuit.draw`)."""
    return self.circuit.draw(**kwargs)

make_param_array(weights: np.ndarray, x: np.ndarray) -> np.ndarray

Return a parameter array for a single sample in circuit.parameters order.

Parameters

weights : np.ndarray Shape (n_weights,). x : np.ndarray Shape (n_features,).

Returns

np.ndarray Shape (n_params,).

Raises

ValueError If weights or x has the wrong shape or contains non-finite values.

Source code in qiskit-data-reuploading-src/qdr/circuits/data_reuploading.py
def make_param_array(self, weights: np.ndarray, x: np.ndarray) -> np.ndarray:
    """Return a parameter array for a *single* sample in ``circuit.parameters`` order.

    Parameters
    ----------
    weights : np.ndarray
        Shape ``(n_weights,)``.
    x : np.ndarray
        Shape ``(n_features,)``.

    Returns
    -------
    np.ndarray
        Shape ``(n_params,)``.

    Raises
    ------
    ValueError
        If ``weights`` or ``x`` has the wrong shape or contains non-finite values.
    """
    weights = self._validate_weights(weights)
    x = self._validate_input_vector(x)
    if self._circuit is None:
        self.build_circuit()
    sorted_params = list(self._circuit.parameters)
    w_dict = dict(zip(self.weight_params, weights))
    x_dict = dict(zip(self.input_params, x))
    full_dict = {**w_dict, **x_dict}
    return np.array([full_dict[p] for p in sorted_params], dtype=float)

make_param_batch(weights: np.ndarray, X: np.ndarray) -> np.ndarray

Return a batched parameter array for multiple samples.

Parameters

weights : np.ndarray Shape (n_weights,). X : np.ndarray Shape (n_samples, n_features).

Returns

np.ndarray Shape (n_samples, n_params).

Raises

ValueError If weights or X has the wrong shape or contains non-finite values.

Source code in qiskit-data-reuploading-src/qdr/circuits/data_reuploading.py
def make_param_batch(self, weights: np.ndarray, X: np.ndarray) -> np.ndarray:
    """Return a batched parameter array for *multiple* samples.

    Parameters
    ----------
    weights : np.ndarray
        Shape ``(n_weights,)``.
    X : np.ndarray
        Shape ``(n_samples, n_features)``.

    Returns
    -------
    np.ndarray
        Shape ``(n_samples, n_params)``.

    Raises
    ------
    ValueError
        If ``weights`` or ``X`` has the wrong shape or contains non-finite values.
    """
    weights = self._validate_weights(weights)
    X = self._validate_input_batch(X)
    if self._circuit is None:
        self.build_circuit()
    sorted_params = list(self._circuit.parameters)
    n_samples, n_params = X.shape[0], len(sorted_params)
    param_values = np.empty((n_samples, n_params), dtype=float)

    param_to_col = {param: col for col, param in enumerate(sorted_params)}
    weight_cols = [param_to_col[param] for param in self.weight_params]
    input_items = [
        (feature_idx, param_to_col[param])
        for feature_idx, param in enumerate(self.input_params)
        if param in param_to_col
    ]

    param_values[:, weight_cols] = np.asarray(weights, dtype=float)
    if input_items:
        feature_indices, input_cols = zip(*input_items)
        param_values[:, input_cols] = X[:, feature_indices]
    return param_values

ReuploadingFeatureMap

qdr.circuits.feature_maps.ReuploadingFeatureMap(n_qubits: int, n_layers: int, n_features: int, encoding: str = 'rx_ry_rz', entanglement: str = 'full', seed: int | None = None)

Feature map based on data re-uploading, compatible with QML pipelines.

Unlike :class:~qdr.circuits.DataReuploadingCircuit this class treats the weights as fixed random parameters (frozen at construction time) and exposes only the input parameters — matching the interface expected by kernel-based methods and :class:qiskit_machine_learning.kernels.FidelityQuantumKernel.

Parameters

n_qubits : int Number of qubits. n_layers : int Number of data-reuploading layers. n_features : int Number of classical input features. encoding : str, optional Rotation scheme. Default "rx_ry_rz". entanglement : str, optional Entanglement pattern. Default "full". seed : int or None, optional Random seed for weight initialisation.

Notes

The feature map keeps the original data re-uploading angle structure,

``angle = fixed_w[l, i, r] + x[feat_idx(l, i, r)]``

where fixed_w is sampled once at construction time and then frozen. Only the input parameters x remain symbolic in the returned circuit. The feature map requires enough encoding slots to upload every declared feature at least once.

Source code in qiskit-data-reuploading-src/qdr/circuits/feature_maps.py
def __init__(
    self,
    n_qubits: int,
    n_layers: int,
    n_features: int,
    encoding: str = "rx_ry_rz",
    entanglement: str = "full",
    seed: int | None = None,
) -> None:
    n_qubits = self._validate_positive_int("n_qubits", n_qubits)
    n_layers = self._validate_positive_int("n_layers", n_layers)
    n_features = self._validate_positive_int("n_features", n_features)
    if encoding not in self._VALID_ENCODINGS:
        raise ValueError(f"Invalid encoding '{encoding}'")
    if entanglement not in self._VALID_ENTANGLEMENTS:
        raise ValueError(f"Invalid entanglement '{entanglement}'")

    self.n_qubits = n_qubits
    self.n_layers = n_layers
    self.n_features = n_features
    self.encoding = encoding
    self.entanglement = entanglement

    self._rotations: tuple[str, ...] = ENCODING_GATES[encoding]
    self._n_rots: int = len(self._rotations)
    self.n_weights: int = n_layers * n_qubits * self._n_rots
    if n_features > self.n_weights:
        raise ValueError(
            f"n_features={n_features} exceeds the number of encoding slots "
            f"({self.n_weights}). Increase n_layers, n_qubits, or encoding "
            "richness so every feature is uploaded at least once."
        )

    rng = np.random.default_rng(seed)
    self._fixed_weights: np.ndarray = rng.uniform(-np.pi, np.pi, self.n_weights)

    self.input_params: ParameterVector = ParameterVector("x", n_features)
    self._sorted_input_params_: list[Parameter] = []
    self._sorted_input_indices_: list[int] = []
    self._circuit: QuantumCircuit | None = None

circuit: QuantumCircuit property

The feature-map circuit (built on first access).

bind(x: np.ndarray) -> QuantumCircuit

Return a fully bound (non-parameterized) circuit for a single input.

Parameters

x : np.ndarray Feature vector of shape (n_features,).

Returns

QuantumCircuit Circuit with all parameters assigned.

Raises

ValueError If x has the wrong shape or contains non-finite values.

Source code in qiskit-data-reuploading-src/qdr/circuits/feature_maps.py
def bind(self, x: np.ndarray) -> QuantumCircuit:
    """Return a fully bound (non-parameterized) circuit for a single input.

    Parameters
    ----------
    x : np.ndarray
        Feature vector of shape ``(n_features,)``.

    Returns
    -------
    QuantumCircuit
        Circuit with all parameters assigned.

    Raises
    ------
    ValueError
        If ``x`` has the wrong shape or contains non-finite values.
    """
    x = self._validate_input_vector(x)
    if self._circuit is None:
        self.build_circuit()
    binding = dict(zip(self._sorted_input_params_, x[self._sorted_input_indices_]))
    return self._circuit.assign_parameters(binding)

build_circuit() -> QuantumCircuit

Build and return the feature-map circuit with fixed weights.

Returns

QuantumCircuit Parameterized only by x (input parameters).

Notes

The circuit parameters are the same objects stored in :attr:input_params; this keeps external QML code from seeing a different ParameterVector than the one actually used by the circuit.

Source code in qiskit-data-reuploading-src/qdr/circuits/feature_maps.py
def build_circuit(self) -> QuantumCircuit:
    """Build and return the feature-map circuit with fixed weights.

    Returns
    -------
    QuantumCircuit
        Parameterized only by *x* (input parameters).

    Notes
    -----
    The circuit parameters are the same objects stored in
    :attr:`input_params`; this keeps external QML code from seeing a
    different ParameterVector than the one actually used by the circuit.
    """
    from qdr.circuits.data_reuploading import DataReuploadingCircuit

    base = DataReuploadingCircuit(
        n_qubits=self.n_qubits,
        n_layers=self.n_layers,
        n_features=self.n_features,
        encoding=self.encoding,
        entanglement=self.entanglement,
    )
    base.build_circuit()

    # Bind weights to constants and remap base x parameters to this
    # object's ParameterVector so public input_params matches the circuit.
    binding = dict(zip(base.weight_params, self._fixed_weights))
    base_circuit_params = set(base.circuit.parameters)
    binding.update(
        {
            base_param: self.input_params[idx]
            for idx, base_param in enumerate(base.input_params)
            if base_param in base_circuit_params
        }
    )
    self._circuit = base.circuit.assign_parameters(binding)
    # After binding, self._circuit.parameters contains only the x params
    # Store them in sorted order for consistent binding in bind()
    self._sorted_input_params_ = list(self._circuit.parameters)
    param_to_feature = {param: idx for idx, param in enumerate(self.input_params)}
    self._sorted_input_indices_ = [
        param_to_feature[param] for param in self._sorted_input_params_
    ]
    return self._circuit

draw(**kwargs) -> Any

Draw the feature-map circuit.

Source code in qiskit-data-reuploading-src/qdr/circuits/feature_maps.py
def draw(self, **kwargs) -> Any:
    """Draw the feature-map circuit."""
    return self.circuit.draw(**kwargs)

qdr.training

ParameterShiftGradient

qdr.training.gradients.ParameterShiftGradient(circuit_obj: DataReuploadingCircuit, observable: SparsePauliOp, estimator: Any | None = None, shift: float = np.pi / 2)

Parameter-shift gradient of expectation-value circuits.

For a circuit :math:U(\theta) and observable :math:O, the derivative is:

.. math::

\frac{\partial}{\partial \theta_i}\langle O \rangle =
\frac{\langle O \rangle_{\theta_i + s}
- \langle O \rangle_{\theta_i - s}}{2\sin(s)}

The default s = pi/2 recovers the usual 0.5 * (f+ - f-) rule. With an exact estimator this produces analytic gradients for the Pauli rotations used by :class:qdr.circuits.DataReuploadingCircuit. With finite-shot estimators it produces a stochastic gradient estimate.

For an MSE gradient call this implementation evaluates the current predictions once and then performs two shifted evaluations per trainable parameter, i.e. 2 * n_weights + 1 batched estimator calls.

Parameters

circuit_obj : DataReuploadingCircuit The parameterized circuit (already built). observable : SparsePauliOp Observable to differentiate. estimator : object or None, optional Qiskit V2 estimator. A fresh :class:~qiskit.primitives.StatevectorEstimator is created when None. shift : float, optional Shift value s in radians. Default pi/2.

Raises

ValueError If shift is non-finite or makes sin(shift) numerically zero.

Source code in qiskit-data-reuploading-src/qdr/training/gradients.py
def __init__(
    self,
    circuit_obj: DataReuploadingCircuit,
    observable: SparsePauliOp,
    estimator: Any | None = None,
    shift: float = np.pi / 2,
) -> None:
    if isinstance(shift, bool) or not isinstance(shift, Real) or not np.isfinite(shift):
        raise ValueError(f"shift must be a finite real number, got {shift!r}.")
    scale_denominator = 2.0 * np.sin(float(shift))
    if np.isclose(scale_denominator, 0.0):
        raise ValueError(
            f"shift={shift!r} is invalid for parameter-shift because sin(shift) is zero."
        )
    self.circuit_obj = circuit_obj
    self.observable = observable
    self.estimator = estimator if estimator is not None else StatevectorEstimator()
    self.shift = float(shift)
    self._shift_scale = 1.0 / scale_denominator

compute(weights: np.ndarray, X: np.ndarray, y: np.ndarray, loss_fn: Callable[..., float] | None = None) -> np.ndarray

Compute the gradient of the MSE loss with respect to weights.

.. math::

\nabla_\theta \mathcal{L} =
\frac{2}{N} \sum_i (\hat{y}_i - y_i) \nabla_\theta \hat{y}_i
Parameters

weights : np.ndarray Current weight vector, shape (n_weights,). X : np.ndarray Input matrix, shape (n_samples, n_features). y : np.ndarray Target values in the same scale as the measured expectation values, shape (n_samples,). loss_fn : callable or None Ignored; kept for API compatibility with possible future gradient engines.

Returns

np.ndarray Gradient vector, shape (n_weights,).

Raises

TypeError If loss_fn is provided and is not callable. ValueError If weights, X, y, or estimator outputs have invalid shape or non-finite values.

Source code in qiskit-data-reuploading-src/qdr/training/gradients.py
def compute(
    self,
    weights: np.ndarray,
    X: np.ndarray,
    y: np.ndarray,
    loss_fn: Callable[..., float] | None = None,
) -> np.ndarray:
    """Compute the gradient of the MSE loss with respect to ``weights``.

    .. math::

        \\nabla_\\theta \\mathcal{L} =
        \\frac{2}{N} \\sum_i (\\hat{y}_i - y_i) \\nabla_\\theta \\hat{y}_i

    Parameters
    ----------
    weights : np.ndarray
        Current weight vector, shape ``(n_weights,)``.
    X : np.ndarray
        Input matrix, shape ``(n_samples, n_features)``.
    y : np.ndarray
        Target values in the same scale as the measured expectation values,
        shape ``(n_samples,)``.
    loss_fn : callable or None
        Ignored; kept for API compatibility with possible future gradient
        engines.

    Returns
    -------
    np.ndarray
        Gradient vector, shape ``(n_weights,)``.

    Raises
    ------
    TypeError
        If ``loss_fn`` is provided and is not callable.
    ValueError
        If ``weights``, ``X``, ``y``, or estimator outputs have invalid
        shape or non-finite values.
    """
    if loss_fn is not None and not callable(loss_fn):
        raise TypeError("loss_fn must be callable or None.")
    weights, X = self._validate_weights_X(weights, X)
    y = self._validate_targets(y, X.shape[0])
    n_weights = weights.shape[0]
    n_samples = X.shape[0]
    grad = np.zeros(n_weights)
    evs_0 = self._eval(weights, X, validate=False)

    for i in range(n_weights):
        w_plus = weights.copy()
        w_minus = weights.copy()
        w_plus[i] += self.shift
        w_minus[i] -= self.shift

        evs_plus = self._eval(w_plus, X, validate=False)
        evs_minus = self._eval(w_minus, X, validate=False)

        # The generalized prefactor keeps non-default shifts mathematically correct.
        dO_dtheta_i = self._shift_scale * (evs_plus - evs_minus)

        # Chain rule for MSE: dL/dtheta_i = (2/N) sum_j residual_j * d ev_j/dtheta_i.
        grad[i] = (2.0 / n_samples) * np.dot(evs_0 - y, dO_dtheta_i)

    return grad

compute_jacobian(weights: np.ndarray, X: np.ndarray) -> np.ndarray

Compute the Jacobian matrix d y_hat_i / d theta_j.

Parameters

weights : np.ndarray Shape (n_weights,). X : np.ndarray Shape (n_samples, n_features).

Returns

np.ndarray Jacobian of shape (n_samples, n_weights).

Raises

ValueError If weights, X, or estimator outputs have invalid shape or non-finite values.

Source code in qiskit-data-reuploading-src/qdr/training/gradients.py
def compute_jacobian(self, weights: np.ndarray, X: np.ndarray) -> np.ndarray:
    """Compute the Jacobian matrix ``d y_hat_i / d theta_j``.

    Parameters
    ----------
    weights : np.ndarray
        Shape ``(n_weights,)``.
    X : np.ndarray
        Shape ``(n_samples, n_features)``.

    Returns
    -------
    np.ndarray
        Jacobian of shape ``(n_samples, n_weights)``.

    Raises
    ------
    ValueError
        If ``weights``, ``X``, or estimator outputs have invalid shape or
        non-finite values.
    """
    weights, X = self._validate_weights_X(weights, X)
    n_weights = weights.shape[0]
    n_samples = X.shape[0]
    jacobian = np.zeros((n_samples, n_weights))

    for i in range(n_weights):
        w_plus = weights.copy()
        w_minus = weights.copy()
        w_plus[i] += self.shift
        w_minus[i] -= self.shift
        jacobian[:, i] = self._shift_scale * (
            self._eval(w_plus, X, validate=False)
            - self._eval(w_minus, X, validate=False)
        )

    return jacobian

qdr.benchmarks

BenchmarkRunner

qdr.benchmarks.runner.BenchmarkRunner(test_size: float = 0.2, cv_folds: int = 5, random_state: int | None = 42, verbose: bool = True)

Compare a :class:~qdr.models.DataReuploadingClassifier against baselines.

The runner performs one stratified train/test split and, when cv_folds > 1, stratified cross-validation on the full dataset. Labels are encoded once with :class:~sklearn.preprocessing.LabelEncoder so all baselines, including XGBoost, receive the same target representation.

Parameters

test_size : float, optional Fraction of data held out for evaluation. Default 0.2. cv_folds : int, optional Number of stratified cross-validation folds. Default 5. Set to 0 or 1 to skip CV. random_state : int or None, optional Controls train/test split and classical model seeds. verbose : bool, optional Print progress updates. Default True.

Raises

ValueError If constructor parameters are outside their valid ranges.

Notes

BenchmarkRunner does not perform angle scaling for the quantum model. Pass the feature representation intended for the circuit, usually scaled to a compact angular range such as [-pi, pi].

Source code in qiskit-data-reuploading-src/qdr/benchmarks/runner.py
def __init__(
    self,
    test_size: float = 0.2,
    cv_folds: int = 5,
    random_state: int | None = 42,
    verbose: bool = True,
) -> None:
    self.test_size = self._validate_test_size(test_size)
    self.cv_folds = self._validate_cv_folds(cv_folds)
    self.random_state = self._validate_random_state(random_state)
    self.verbose = self._validate_include_flag("verbose", verbose)
    self._results: list[BenchmarkResult] = []

results: list[BenchmarkResult] property

Raw list of :class:BenchmarkResult objects.

run(X: np.ndarray, y: np.ndarray, qdr_model: Any | None = None, include_logreg: bool = True, include_svm: bool = True, include_mlp: bool = True, include_rf: bool = True, include_xgboost: bool = False) -> 'BenchmarkRunner'

Run the full benchmark suite.

Parameters

X : np.ndarray Feature matrix of shape (n_samples, n_features). y : np.ndarray Class labels, shape (n_samples,). qdr_model : DataReuploadingClassifier or None Configured but unfitted quantum model. If None, a default model is created with enough qubits for the number of classes and enough layers to upload all features at least once. include_logreg : bool, optional Whether to benchmark scaled logistic regression. Default True. include_svm : bool, optional Whether to benchmark a scaled RBF-SVM. Default True. include_mlp : bool, optional Whether to benchmark a small scaled MLP. Default True. include_rf : bool, optional Whether to benchmark a Random Forest. Default True. include_xgboost : bool, optional Whether to benchmark XGBoost. Default False because it requires the optional xgboost package.

Returns

BenchmarkRunner Allows chaining: runner.run(...).summary().

Raises

ValueError If data, resampling settings, or include flags are invalid. ImportError If include_xgboost=True and xgboost is not installed.

Source code in qiskit-data-reuploading-src/qdr/benchmarks/runner.py
def run(
    self,
    X: np.ndarray,
    y: np.ndarray,
    qdr_model: Any | None = None,
    include_logreg: bool = True,
    include_svm: bool = True,
    include_mlp: bool = True,
    include_rf: bool = True,
    include_xgboost: bool = False,
) -> "BenchmarkRunner":
    """Run the full benchmark suite.

    Parameters
    ----------
    X : np.ndarray
        Feature matrix of shape ``(n_samples, n_features)``.
    y : np.ndarray
        Class labels, shape ``(n_samples,)``.
    qdr_model : DataReuploadingClassifier or None
        Configured but unfitted quantum model. If ``None``, a default model
        is created with enough qubits for the number of classes and enough
        layers to upload all features at least once.
    include_logreg : bool, optional
        Whether to benchmark scaled logistic regression. Default ``True``.
    include_svm : bool, optional
        Whether to benchmark a scaled RBF-SVM. Default ``True``.
    include_mlp : bool, optional
        Whether to benchmark a small scaled MLP. Default ``True``.
    include_rf : bool, optional
        Whether to benchmark a Random Forest. Default ``True``.
    include_xgboost : bool, optional
        Whether to benchmark XGBoost. Default ``False`` because it requires
        the optional ``xgboost`` package.

    Returns
    -------
    BenchmarkRunner
        Allows chaining: ``runner.run(...).summary()``.

    Raises
    ------
    ValueError
        If data, resampling settings, or include flags are invalid.
    ImportError
        If ``include_xgboost=True`` and ``xgboost`` is not installed.
    """
    include_logreg = self._validate_include_flag("include_logreg", include_logreg)
    include_svm = self._validate_include_flag("include_svm", include_svm)
    include_mlp = self._validate_include_flag("include_mlp", include_mlp)
    include_rf = self._validate_include_flag("include_rf", include_rf)
    include_xgboost = self._validate_include_flag("include_xgboost", include_xgboost)

    xgb_classifier_cls: Any | None = None
    if include_xgboost:
        try:
            from xgboost import XGBClassifier
        except ImportError as exc:
            raise ImportError(
                "include_xgboost=True requires the optional xgboost package."
            ) from exc
        xgb_classifier_cls = XGBClassifier

    X, y_encoded, class_labels = self._validate_data(X, y)
    self._validate_resampling(y_encoded)
    self.classes_ = class_labels
    self.n_features_in_ = X.shape[1]
    cv = self._cv_splitter()

    X_train, X_test, y_train, y_test = train_test_split(
        X,
        y_encoded,
        test_size=self.test_size,
        random_state=self.random_state,
        stratify=y_encoded,
    )
    self._log(f"Benchmark: {len(X_train)} train / {len(X_test)} test samples")

    results: list[BenchmarkResult] = []

    # --- Quantum model ---
    if qdr_model is None:
        qdr_model = self._default_qdr_model(
            n_features=X.shape[1],
            n_classes=len(class_labels),
            random_state=self.random_state,
        )
    qdr_result = self._benchmark_one(
        "DataReuploadingClassifier",
        qdr_model,
        X_train,
        X_test,
        y_train,
        y_test,
        X,
        y_encoded,
        cv,
    )
    if hasattr(qdr_model, "loss_history_"):
        qdr_result.extra["loss_history"] = qdr_model.loss_history_
    qdr_result.extra["classes"] = class_labels.tolist()
    results.append(qdr_result)

    # --- Logistic Regression ---
    if include_logreg:
        logreg = Pipeline(
            [
                ("scaler", StandardScaler()),
                (
                    "logreg",
                    LogisticRegression(
                        max_iter=1000,
                        random_state=self.random_state,
                    ),
                ),
            ]
        )
        results.append(
            self._benchmark_one(
                "Logistic Regression",
                logreg,
                X_train,
                X_test,
                y_train,
                y_test,
                X,
                y_encoded,
                cv,
            )
        )

    # --- SVM ---
    if include_svm:
        svm = Pipeline(
            [
                ("scaler", StandardScaler()),
                ("svc", SVC(kernel="rbf", random_state=self.random_state, probability=True)),
            ]
        )
        results.append(
            self._benchmark_one(
                "SVM (RBF)",
                svm,
                X_train,
                X_test,
                y_train,
                y_test,
                X,
                y_encoded,
                cv,
            )
        )

    # --- Random Forest ---
    if include_rf:
        rf = RandomForestClassifier(
            n_estimators=300,
            random_state=self.random_state,
            # Keep benchmark execution portable in constrained CI/sandboxed
            # Windows environments where worker pools may be unavailable.
            n_jobs=1,
        )
        results.append(
            self._benchmark_one(
                "Random Forest",
                rf,
                X_train,
                X_test,
                y_train,
                y_test,
                X,
                y_encoded,
                cv,
            )
        )

    # --- XGBoost ---
    if include_xgboost:
        xgb_kwargs: dict[str, Any] = {
            "n_estimators": 300,
            "max_depth": 4,
            "learning_rate": 0.05,
            "subsample": 0.9,
            "colsample_bytree": 0.9,
            "random_state": self.random_state,
            "n_jobs": 1,
            "verbosity": 0,
        }
        if len(class_labels) == 2:
            xgb_kwargs.update({"objective": "binary:logistic", "eval_metric": "logloss"})
        else:
            xgb_kwargs.update(
                {
                    "objective": "multi:softprob",
                    "eval_metric": "mlogloss",
                    "num_class": len(class_labels),
                }
            )
        xgb = xgb_classifier_cls(**xgb_kwargs)
        results.append(
            self._benchmark_one(
                "XGBoost",
                xgb,
                X_train,
                X_test,
                y_train,
                y_test,
                X,
                y_encoded,
                cv,
            )
        )

    # --- MLP ---
    if include_mlp:
        mlp = Pipeline(
            [
                ("scaler", StandardScaler()),
                (
                    "mlp",
                    MLPClassifier(
                        hidden_layer_sizes=(32, 16),
                        max_iter=300,
                        random_state=self.random_state,
                    ),
                ),
            ]
        )
        results.append(
            self._benchmark_one(
                "MLP (32-16)",
                mlp,
                X_train,
                X_test,
                y_train,
                y_test,
                X,
                y_encoded,
                cv,
            )
        )

    self._results = results
    return self

summary() -> pd.DataFrame

Return benchmark results as a :class:~pandas.DataFrame.

Returns

pd.DataFrame Indexed by model name with columns accuracy, f1, train_time_s, predict_time_s, cv_mean, and cv_std. Values are returned at full floating-point precision; callers can round only for presentation.

Raises

ValueError If called before :meth:run has produced results.

Source code in qiskit-data-reuploading-src/qdr/benchmarks/runner.py
def summary(self) -> pd.DataFrame:
    """Return benchmark results as a :class:`~pandas.DataFrame`.

    Returns
    -------
    pd.DataFrame
        Indexed by model name with columns ``accuracy``, ``f1``,
        ``train_time_s``, ``predict_time_s``, ``cv_mean``, and ``cv_std``.
        Values are returned at full floating-point precision; callers can
        round only for presentation.

    Raises
    ------
    ValueError
        If called before :meth:`run` has produced results.
    """
    if not self._results:
        raise ValueError("No benchmark results available. Call run() before summary().")
    rows = [
        {
            "model": r.model_name,
            "accuracy": r.accuracy,
            "f1": r.f1,
            "train_time_s": r.train_time_s,
            "predict_time_s": r.predict_time_s,
            "cv_mean": r.cv_mean,
            "cv_std": r.cv_std,
        }
        for r in self._results
    ]
    df = pd.DataFrame(rows).set_index("model")
    if self.verbose:
        print("\n" + df.to_string())
    return df

qdr.visualization

qdr.visualization.plots

Visualization utilities for trained QDR models and benchmarks.

plot_benchmark_comparison(summary_df: Any, metric: str = 'accuracy', ax: 'Axes | None' = None, title: str | None = None) -> 'Figure'

Plot a bar chart comparing benchmark metrics across models.

Parameters

summary_df : pandas.DataFrame Output of :meth:qdr.benchmarks.BenchmarkRunner.summary, indexed by model name. metric : str, optional Numeric column to plot. Default "accuracy". ax : Axes or None, optional Existing axes to plot into. If None, a new figure is created. title : str or None, optional Plot title. If None, a default title is used.

Returns

Figure Matplotlib figure containing the benchmark comparison.

Raises

ValueError If the summary is empty, the metric is missing, or metric values are non-numeric/non-finite.

Source code in qiskit-data-reuploading-src/qdr/visualization/plots.py
def plot_benchmark_comparison(
    summary_df: Any,
    metric: str = "accuracy",
    ax: "Axes | None" = None,
    title: str | None = None,
) -> "Figure":
    """Plot a bar chart comparing benchmark metrics across models.

    Parameters
    ----------
    summary_df : pandas.DataFrame
        Output of :meth:`qdr.benchmarks.BenchmarkRunner.summary`, indexed by
        model name.
    metric : str, optional
        Numeric column to plot. Default ``"accuracy"``.
    ax : Axes or None, optional
        Existing axes to plot into. If ``None``, a new figure is created.
    title : str or None, optional
        Plot title. If ``None``, a default title is used.

    Returns
    -------
    Figure
        Matplotlib figure containing the benchmark comparison.

    Raises
    ------
    ValueError
        If the summary is empty, the metric is missing, or metric values are
        non-numeric/non-finite.
    """
    import matplotlib.pyplot as plt

    if getattr(summary_df, "empty", False):
        raise ValueError("summary_df must contain at least one benchmark result.")
    if metric not in getattr(summary_df, "columns", []):
        raise ValueError(f"metric '{metric}' is not present in summary_df.")

    models = [str(model) for model in summary_df.index.tolist()]
    try:
        values = np.asarray(summary_df[metric].to_numpy(dtype=float), dtype=float)
    except (TypeError, ValueError):
        raise ValueError(f"metric '{metric}' must contain numeric values.") from None
    if values.ndim != 1 or values.size == 0:
        raise ValueError(f"metric '{metric}' must contain at least one value.")
    if np.any(~np.isfinite(values)):
        raise ValueError(f"metric '{metric}' contains NaN or Inf values.")

    if ax is None:
        fig, ax = plt.subplots(figsize=(7, 4))
    else:
        fig = ax.figure

    palette = [
        "#4C72B0",
        "#DD8452",
        "#55A868",
        "#C44E52",
        "#8172B2",
        "#937860",
        "#64B5CD",
    ]
    colors = [palette[i % len(palette)] for i in range(len(models))]

    bars = ax.bar(models, values, color=colors, edgecolor="black", linewidth=0.5)
    ax.bar_label(bars, fmt="%.3f", padding=3)
    if values.min() >= 0 and values.max() <= 1:
        ax.set_ylim(0, 1.05)
    else:
        margin = max(0.05 * (values.max() - values.min()), 1e-12)
        if values.min() >= 0:
            ax.set_ylim(0, values.max() + margin)
        elif values.max() <= 0:
            ax.set_ylim(values.min() - margin, 0)
        else:
            ax.set_ylim(values.min() - margin, values.max() + margin)
    ax.set_ylabel(metric)
    ax.set_title(title or f"Model comparison - {metric}")
    ax.grid(axis="y", alpha=0.3)
    fig.tight_layout()
    return fig

plot_bloch_sphere(statevector: np.ndarray, ax: 'Axes | None' = None, title: str = 'Bloch Sphere') -> 'Figure'

Render a normalized single-qubit state on a Bloch sphere.

Parameters

statevector : np.ndarray Complex array with shape (2,) representing alpha |0> + beta |1>. ax : Axes or None, optional Existing 3D axes created with projection="3d". If None, a new 3D axes is created. title : str, optional Plot title.

Returns

Figure Matplotlib figure containing the Bloch sphere.

Raises

ValueError If statevector is not a finite normalized single-qubit state, or if ax is provided but is not a 3D axes.

Notes

The plotted vector is (<X>, <Y>, <Z>) for the supplied pure state. The state must be normalized because otherwise these expectation values are not physically valid Bloch coordinates.

Source code in qiskit-data-reuploading-src/qdr/visualization/plots.py
def plot_bloch_sphere(
    statevector: np.ndarray,
    ax: "Axes | None" = None,
    title: str = "Bloch Sphere",
) -> "Figure":
    """Render a normalized single-qubit state on a Bloch sphere.

    Parameters
    ----------
    statevector : np.ndarray
        Complex array with shape ``(2,)`` representing
        ``alpha |0> + beta |1>``.
    ax : Axes or None, optional
        Existing 3D axes created with ``projection="3d"``. If ``None``, a new
        3D axes is created.
    title : str, optional
        Plot title.

    Returns
    -------
    Figure
        Matplotlib figure containing the Bloch sphere.

    Raises
    ------
    ValueError
        If ``statevector`` is not a finite normalized single-qubit state, or if
        ``ax`` is provided but is not a 3D axes.

    Notes
    -----
    The plotted vector is ``(<X>, <Y>, <Z>)`` for the supplied pure state. The
    state must be normalized because otherwise these expectation values are not
    physically valid Bloch coordinates.
    """
    import matplotlib.pyplot as plt

    state = np.asarray(statevector, dtype=complex)
    if state.shape != (2,):
        raise ValueError(f"statevector must have shape (2,), got {state.shape}.")
    if np.any(~np.isfinite(state.real)) or np.any(~np.isfinite(state.imag)):
        raise ValueError("statevector contains NaN or Inf values.")
    norm = float(np.linalg.norm(state))
    if not np.isclose(norm, 1.0, rtol=1e-7, atol=1e-9):
        raise ValueError(f"statevector must be normalized to norm 1, got norm={norm}.")

    alpha, beta = state[0], state[1]
    bx = 2.0 * np.real(np.conj(alpha) * beta)
    by = 2.0 * np.imag(np.conj(alpha) * beta)
    bz = float(np.abs(alpha) ** 2 - np.abs(beta) ** 2)

    if ax is None:
        fig = plt.figure(figsize=(5, 5))
        ax = fig.add_subplot(111, projection="3d")
    else:
        if not hasattr(ax, "get_zlim"):
            raise ValueError("ax must be a 3D axes created with projection='3d'.")
        fig = ax.figure

    u = np.linspace(0, 2 * np.pi, 40)
    v = np.linspace(0, np.pi, 20)
    xs = np.outer(np.cos(u), np.sin(v))
    ys = np.outer(np.sin(u), np.sin(v))
    zs = np.outer(np.ones(np.size(u)), np.cos(v))
    ax.plot_wireframe(xs, ys, zs, color="lightgrey", alpha=0.3, linewidth=0.4)

    for start, end, label_axis in [
        ([0, 0, -1.3], [0, 0, 1.4], "Z"),
        ([-1.3, 0, 0], [1.4, 0, 0], "X"),
        ([0, -1.3, 0], [0, 1.4, 0], "Y"),
    ]:
        ax.plot(*zip(start, end), color="grey", linewidth=0.8)
        ax.text(*end, label_axis, fontsize=9, ha="center")

    ax.quiver(0, 0, 0, bx, by, bz, color="royalblue", linewidth=2, arrow_length_ratio=0.15)
    ax.set_title(title)
    ax.set_box_aspect((1, 1, 1))
    ax.set_axis_off()
    fig.tight_layout()
    return fig

plot_decision_boundary(model: Any, X: np.ndarray, y: np.ndarray, resolution: int = 50, feature_indices: tuple[int, int] = (0, 1), ax: 'Axes | None' = None, title: str = 'Decision Boundary') -> 'Figure'

Plot a 2D decision boundary for a fitted classifier.

Parameters

model : fitted classifier Any object with a predict(X) method. X : np.ndarray Feature matrix with shape (n_samples, n_features). y : np.ndarray True labels with shape (n_samples,). resolution : int, optional Grid resolution, in points per axis. Default 50. feature_indices : tuple[int, int], optional Two feature indices to plot. Default (0, 1). ax : Axes or None, optional Existing axes to plot into. If None, a new figure is created. title : str, optional Plot title.

Returns

Figure Matplotlib figure containing the decision boundary.

Raises

ValueError If inputs have invalid shape, contain non-finite values, or if model predictions cannot be aligned with the plotted grid.

Notes

For n_features > 2, the non-plotted features are fixed to their empirical mean in X. This draws a well-defined 2D slice of the fitted model instead of silently moving selected features into the wrong columns.

Source code in qiskit-data-reuploading-src/qdr/visualization/plots.py
def plot_decision_boundary(
    model: Any,
    X: np.ndarray,
    y: np.ndarray,
    resolution: int = 50,
    feature_indices: tuple[int, int] = (0, 1),
    ax: "Axes | None" = None,
    title: str = "Decision Boundary",
) -> "Figure":
    """Plot a 2D decision boundary for a fitted classifier.

    Parameters
    ----------
    model : fitted classifier
        Any object with a ``predict(X)`` method.
    X : np.ndarray
        Feature matrix with shape ``(n_samples, n_features)``.
    y : np.ndarray
        True labels with shape ``(n_samples,)``.
    resolution : int, optional
        Grid resolution, in points per axis. Default ``50``.
    feature_indices : tuple[int, int], optional
        Two feature indices to plot. Default ``(0, 1)``.
    ax : Axes or None, optional
        Existing axes to plot into. If ``None``, a new figure is created.
    title : str, optional
        Plot title.

    Returns
    -------
    Figure
        Matplotlib figure containing the decision boundary.

    Raises
    ------
    ValueError
        If inputs have invalid shape, contain non-finite values, or if model
        predictions cannot be aligned with the plotted grid.

    Notes
    -----
    For ``n_features > 2``, the non-plotted features are fixed to their
    empirical mean in ``X``. This draws a well-defined 2D slice of the fitted
    model instead of silently moving selected features into the wrong columns.
    """
    import matplotlib.pyplot as plt

    X = _validate_finite_2d("X", X)
    y = np.asarray(y)
    if y.ndim != 1:
        raise ValueError(f"y must be a 1D array, got y.ndim={y.ndim}.")
    if y.shape[0] != X.shape[0]:
        raise ValueError(f"y must have length {X.shape[0]}, got {y.shape[0]}.")
    resolution = _validate_resolution(resolution)
    i, j = _validate_feature_indices(feature_indices, X.shape[1])

    X2 = X[:, [i, j]]
    x_min, x_max = X2[:, 0].min() - 0.3, X2[:, 0].max() + 0.3
    y_min, y_max = X2[:, 1].min() - 0.3, X2[:, 1].max() + 0.3

    xx, yy = np.meshgrid(
        np.linspace(x_min, x_max, resolution),
        np.linspace(y_min, y_max, resolution),
    )
    grid = np.tile(X.mean(axis=0), (xx.size, 1))
    grid[:, i] = xx.ravel()
    grid[:, j] = yy.ravel()

    Z = np.asarray(model.predict(grid))
    if Z.shape != (xx.size,):
        raise ValueError(
            f"model.predict must return shape ({xx.size},), got {Z.shape}."
        )
    Z = Z.reshape(xx.shape)

    classes = _ordered_unique(getattr(model, "classes_", np.array([], dtype=object)), y, Z)
    if len(classes) < 1:
        raise ValueError("At least one class is required to plot a decision boundary.")
    cmap = _class_cmap(plt, len(classes))
    Z_int = _label_indices(Z, classes)
    y_int = _label_indices(y, classes)

    if ax is None:
        fig, ax = plt.subplots(figsize=(6, 5))
    else:
        fig = ax.figure

    levels = np.arange(len(classes) + 1) - 0.5
    ax.contourf(xx, yy, Z_int, levels=levels, alpha=0.35, cmap=cmap)
    scatter = ax.scatter(
        X2[:, 0],
        X2[:, 1],
        c=y_int,
        cmap=cmap,
        vmin=-0.5,
        vmax=len(classes) - 0.5,
        edgecolors="k",
        s=40,
    )
    ax.set_xlabel(f"Feature {i}")
    ax.set_ylabel(f"Feature {j}")
    ax.set_title(title)
    cbar = fig.colorbar(scatter, ax=ax, ticks=np.arange(len(classes)))
    cbar.ax.set_yticklabels([str(label) for label in classes])
    cbar.set_label("Class")
    fig.tight_layout()
    return fig

plot_loss_curve(loss_history: Sequence[float], ax: 'Axes | None' = None, title: str = 'Training Loss', label: str = 'loss') -> 'Figure'

Plot an optimization loss curve.

Parameters

loss_history : Sequence[float] Per-iteration loss values. ax : Axes or None, optional Existing axes to plot into. If None, a new figure is created. title : str, optional Plot title. label : str, optional Legend label for the curve.

Returns

Figure Matplotlib figure containing the loss curve.

Raises

ValueError If loss_history is empty, not one-dimensional, or contains non-finite values.

Source code in qiskit-data-reuploading-src/qdr/visualization/plots.py
def plot_loss_curve(
    loss_history: Sequence[float],
    ax: "Axes | None" = None,
    title: str = "Training Loss",
    label: str = "loss",
) -> "Figure":
    """Plot an optimization loss curve.

    Parameters
    ----------
    loss_history : Sequence[float]
        Per-iteration loss values.
    ax : Axes or None, optional
        Existing axes to plot into. If ``None``, a new figure is created.
    title : str, optional
        Plot title.
    label : str, optional
        Legend label for the curve.

    Returns
    -------
    Figure
        Matplotlib figure containing the loss curve.

    Raises
    ------
    ValueError
        If ``loss_history`` is empty, not one-dimensional, or contains
        non-finite values.
    """
    import matplotlib.pyplot as plt

    losses = np.asarray(loss_history, dtype=float)
    if losses.ndim != 1:
        raise ValueError(f"loss_history must be a 1D sequence, got ndim={losses.ndim}.")
    if losses.size == 0:
        raise ValueError("loss_history must contain at least one value.")
    if np.any(~np.isfinite(losses)):
        raise ValueError("loss_history contains NaN or Inf values.")

    if ax is None:
        fig, ax = plt.subplots(figsize=(6, 4))
    else:
        fig = ax.figure

    ax.plot(np.arange(losses.size), losses, label=label, color="royalblue", linewidth=1.5)
    ax.set_xlabel("Iteration")
    ax.set_ylabel("Loss")
    ax.set_title(title)
    ax.legend()
    ax.grid(True, alpha=0.3)
    fig.tight_layout()
    return fig

qdr.hardware

qdr.hardware.ibm_backend

Hardware integration: run data re-uploading circuits on IBM Quantum backends.

list_available_backends(token: str | None = None, channel: str = 'ibm_quantum', min_qubits: int = 2, operational: bool = True) -> list[dict[str, Any]]

Return accessible IBM Quantum backends.

Parameters

token : str or None, optional IBM Quantum API token. If None, uses the saved account. channel : str, optional Runtime channel. Default "ibm_quantum". min_qubits : int, optional Filter backends with fewer than this many qubits. Default 2. operational : bool, optional Only include currently operational backends. Default True.

Returns

list[dict[str, Any]] Each dict has name, n_qubits, status, and pending_jobs.

Raises

ImportError If qiskit-ibm-runtime is not installed. ValueError If inputs are malformed.

Source code in qiskit-data-reuploading-src/qdr/hardware/ibm_backend.py
def list_available_backends(
    token: str | None = None,
    channel: str = "ibm_quantum",
    min_qubits: int = 2,
    operational: bool = True,
) -> list[dict[str, Any]]:
    """Return accessible IBM Quantum backends.

    Parameters
    ----------
    token : str or None, optional
        IBM Quantum API token. If ``None``, uses the saved account.
    channel : str, optional
        Runtime channel. Default ``"ibm_quantum"``.
    min_qubits : int, optional
        Filter backends with fewer than this many qubits. Default 2.
    operational : bool, optional
        Only include currently operational backends. Default ``True``.

    Returns
    -------
    list[dict[str, Any]]
        Each dict has ``name``, ``n_qubits``, ``status``, and ``pending_jobs``.

    Raises
    ------
    ImportError
        If ``qiskit-ibm-runtime`` is not installed.
    ValueError
        If inputs are malformed.
    """
    token = _validate_optional_token(token)
    channel = _validate_nonempty_string("channel", channel)
    min_qubits = _validate_positive_int("min_qubits", min_qubits)
    if not isinstance(operational, bool):
        raise ValueError(f"operational must be a bool, got {operational!r}.")

    _, QiskitRuntimeService, _ = _load_runtime()
    service = QiskitRuntimeService(channel=channel, token=token)
    backends = service.backends(
        operational=operational,
        min_num_qubits=min_qubits,
    )
    rows: list[dict[str, Any]] = []
    for backend in backends:
        status = backend.status()
        rows.append(
            {
                "name": _backend_name(backend),
                "n_qubits": int(getattr(backend, "num_qubits")),
                "status": str(getattr(status, "status_msg", status)),
                "pending_jobs": getattr(status, "pending_jobs", None),
            }
        )
    return rows

run_on_ibm_backend(circuit: 'QuantumCircuit', observable: 'SparsePauliOp', parameter_values: np.ndarray, backend_name: str, token: str | None = None, channel: str = 'ibm_quantum', optimization_level: int = 1, resilience_level: int | None = 1, default_shots: int | None = None, seed_estimator: int | None = None, precision: float | None = None) -> np.ndarray

Evaluate expectation values on an IBM Quantum backend.

Requires the optional hardware dependency group::

pip install qiskit-data-reuploading[hardware]
Parameters

circuit : QuantumCircuit Parameterized circuit without measurements. observable : SparsePauliOp Observable to estimate. parameter_values : np.ndarray Parameter values. Shape (n_samples, n_params) for a batch, or (n_params,) for one sample. backend_name : str IBM Quantum backend name, e.g. "ibm_brisbane". token : str or None, optional IBM Quantum API token. If None, the token is read from the saved account configured with QiskitRuntimeService.save_account(...). channel : str, optional Runtime channel. Default "ibm_quantum". optimization_level : int, optional Transpiler optimization level, from 0 to 3. Default 1. resilience_level : int or None, optional Estimator resilience level. IBM Runtime supports 0, 1, and 2; None leaves the server default unset. Default 1. default_shots : int or None, optional Non-negative total shots per circuit/configuration. Mutually exclusive with precision. seed_estimator : int or None, optional Runtime estimator seed. precision : float or None, optional Target precision passed to EstimatorV2.run. Mutually exclusive with default_shots.

Returns

np.ndarray Expectation values of shape (n_samples,).

Raises

ImportError If qiskit-ibm-runtime is not installed. ValueError If inputs are malformed or estimator output has an unexpected shape.

Notes

This function submits real or runtime-managed backend jobs and can incur queue time and account cost. It intentionally does not train models; use it for explicit hardware evaluation of already-built circuits/observables.

Source code in qiskit-data-reuploading-src/qdr/hardware/ibm_backend.py
def run_on_ibm_backend(
    circuit: "QuantumCircuit",
    observable: "SparsePauliOp",
    parameter_values: np.ndarray,
    backend_name: str,
    token: str | None = None,
    channel: str = "ibm_quantum",
    optimization_level: int = 1,
    resilience_level: int | None = 1,
    default_shots: int | None = None,
    seed_estimator: int | None = None,
    precision: float | None = None,
) -> np.ndarray:
    """Evaluate expectation values on an IBM Quantum backend.

    Requires the optional ``hardware`` dependency group::

        pip install qiskit-data-reuploading[hardware]

    Parameters
    ----------
    circuit : QuantumCircuit
        Parameterized circuit without measurements.
    observable : SparsePauliOp
        Observable to estimate.
    parameter_values : np.ndarray
        Parameter values. Shape ``(n_samples, n_params)`` for a batch, or
        ``(n_params,)`` for one sample.
    backend_name : str
        IBM Quantum backend name, e.g. ``"ibm_brisbane"``.
    token : str or None, optional
        IBM Quantum API token. If ``None``, the token is read from the saved
        account configured with ``QiskitRuntimeService.save_account(...)``.
    channel : str, optional
        Runtime channel. Default ``"ibm_quantum"``.
    optimization_level : int, optional
        Transpiler optimization level, from 0 to 3. Default 1.
    resilience_level : int or None, optional
        Estimator resilience level. IBM Runtime supports 0, 1, and 2; ``None``
        leaves the server default unset. Default 1.
    default_shots : int or None, optional
        Non-negative total shots per circuit/configuration. Mutually exclusive with
        ``precision``.
    seed_estimator : int or None, optional
        Runtime estimator seed.
    precision : float or None, optional
        Target precision passed to ``EstimatorV2.run``. Mutually exclusive with
        ``default_shots``.

    Returns
    -------
    np.ndarray
        Expectation values of shape ``(n_samples,)``.

    Raises
    ------
    ImportError
        If ``qiskit-ibm-runtime`` is not installed.
    ValueError
        If inputs are malformed or estimator output has an unexpected shape.

    Notes
    -----
    This function submits real or runtime-managed backend jobs and can incur
    queue time and account cost. It intentionally does not train models; use it
    for explicit hardware evaluation of already-built circuits/observables.
    """
    (
        values,
        backend_name,
        token,
        channel,
        optimization_level,
        resilience_level,
        default_shots,
        seed_estimator,
        precision,
    ) = _validate_run_options(
        circuit,
        parameter_values,
        backend_name,
        token,
        channel,
        optimization_level,
        resilience_level,
        default_shots,
        seed_estimator,
        precision,
    )
    EstimatorV2, QiskitRuntimeService, EstimatorOptions = _load_runtime()

    from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

    service = QiskitRuntimeService(channel=channel, token=token)
    backend = service.backend(backend_name)

    pm = generate_preset_pass_manager(optimization_level=optimization_level, backend=backend)
    isa_circuit = pm.run(circuit)
    if _num_parameters(isa_circuit) != values.shape[1]:
        raise ValueError(
            "Transpilation changed the number of circuit parameters: "
            f"expected {values.shape[1]}, got {_num_parameters(isa_circuit)}."
        )
    isa_observable = observable.apply_layout(isa_circuit.layout)

    options = _estimator_options(
        EstimatorOptions,
        resilience_level=resilience_level,
        default_shots=default_shots,
        seed_estimator=seed_estimator,
    )

    estimator = EstimatorV2(mode=backend, options=options)
    pub = (isa_circuit, isa_observable, values)
    job = estimator.run([pub], precision=precision) if precision is not None else estimator.run([pub])

    result = job.result()
    evs = np.asarray(result[0].data.evs, dtype=float).reshape(-1)
    if evs.shape != (values.shape[0],):
        raise ValueError(
            f"Estimator returned evs with shape {evs.shape}, expected {(values.shape[0],)}."
        )
    if np.any(~np.isfinite(evs)):
        raise ValueError("Estimator returned NaN or Inf expectation values.")
    return evs