Source code for psipose.kernels.fidelity

"""Fidelity-based quantum kernel.

Computes the kernel matrix as

:math:`K(x_i, x_j) = |\\langle \\phi(x_i) | \\phi(x_j) \\rangle|^2`,

where :math:`\\phi` is a quantum state prepared by an encoder circuit.

This implements the quantum kernel approach from:

- Havlíček et al. (2019), "Supervised learning with
  quantum-enhanced feature spaces" (Nature 567, 209-212).
  Introduces quantum kernels via the fidelity between
  quantum-encoded feature maps, combined with classical SVM.
- Schuld & Killoran (2019), "Quantum machine learning in
  feature Hilbert spaces" (PRL 122, 040504).
  Frames quantum input encoding as a nonlinear map to Hilbert
  space and shows how quantum kernels can be used with
  classical ML methods.
- Schuld, Bocharov, Svore & Wiebe (2018), "Circuit-centric
  quantum classifiers" (arXiv:1804.00633). Uses angle encoding
  as the feature map :math:`\\phi` for quantum kernels.

Mathematically, the quantum kernel is defined as:

.. math::
    K(x_i, x_j) = |\\langle \\phi(x_i) | \\phi(x_j) \\rangle|^2
    = |\\langle 0^{\\otimes n}|
    U_{\\text{enc}}^\\dagger(x_j)
    U_{\\text{enc}}(x_i)
    |0^{\\otimes n}\\rangle|^2

where :math:`|\\phi(x)\\rangle = U_{\\text{enc}}(x)|0^{\\otimes n}\\rangle`
is the quantum feature map implemented by the encoder.

The fidelity is computed by measuring the probability of :math:`|0\\rangle^{\\otimes n}`
in the combined circuit :math:`U_{\\text{enc}}^\\dagger(x_j) U_{\\text{enc}}(x_i)`:

.. math::
    |\\langle \\phi(x_i) | \\phi(x_j) \\rangle|^2
    = \\langle \\phi(x_j) | \\phi(x_i) \\rangle
    \\langle \\phi(x_i) | \\phi(x_j) \\rangle
    = \\text{Prob}(00\\dots 0 | U_{\\text{enc}}^\\dagger(x_j)
    U_{\\text{enc}}(x_i) |00\\dots 0\\rangle)

Computationally, this is implemented as:

1. Encode :math:`x_1`: :math:`U_{\\text{enc}}(x_1) |0^{\\otimes n}\\rangle`
2. Apply adjoint: :math:`U_{\\text{enc}}^\\dagger(x_2)`
3. Measure :math:`\\text{Prob}(|0\\rangle^{\\otimes n})` via :func:`qml.probs`
"""

import numpy as np
import pennylane as qml
from sklearn.base import BaseEstimator
from sklearn.utils.validation import check_is_fitted

from psipose.encoders import AngleEncoder, BaseEncoder


[docs] class FidelityQuantumKernel(BaseEstimator): """Fidelity-based quantum kernel matrix calculator. Computes the Gram matrix where each entry is the fidelity (squared inner product) between quantum-encoded data points: :math:`K[i, j] = |\\langle \\phi(x_i) | \\phi(x_j) \\rangle|^2` The quantum state :math:`|\\phi(x)\\rangle` is prepared by the given encoder circuit. This implements the quantum kernel approach from: - Havlíček et al. (2019), "Supervised learning with quantum-enhanced feature spaces" (Nature 567, 209-212). - Schuld & Killoran (2019), "Quantum machine learning in feature Hilbert spaces" (PRL 122, 040504). - Schuld, Bocharov, Svore & Wiebe (2018), "Circuit-centric quantum classifiers" (arXiv:1804.00633). Mathematically, the quantum kernel is defined as: .. math:: K(x_i, x_j) = |\\langle \\phi(x_i) | \\phi(x_j) \\rangle|^2 where :math:`|\\phi(x)\\rangle = U_{\\text{enc}}(x)|0^{\\otimes n}\\rangle`. Parameters ---------- encoder : BaseEncoder, optional The quantum encoding circuit. If None, defaults to AngleEncoder(). n_qubits : int, default=4 Number of qubits (only used if encoder is None). device : str, default="default.qubit" PennyLane device to use for simulation. batch_size : int, optional Number of circuits to evaluate in parallel. If None, evaluates all. Attributes ---------- encoder_ : BaseEncoder Fitted encoder. n_qubits_ : int Number of qubits used. Examples -------- >>> from psipose import FidelityQuantumKernel, AngleEncoder >>> kernel = FidelityQuantumKernel(encoder=AngleEncoder(n_qubits=4)) >>> K = kernel.compute_matrix(X_train) >>> # Use with sklearn SVC: >>> from sklearn.svm import SVC >>> svc = SVC(kernel="precomputed") >>> svc.fit(K, y_train) """ def __init__( self, encoder: BaseEncoder | None = None, n_qubits: int = 4, device: str = "default.qubit", batch_size: int | None = None, ): self.encoder = encoder self.n_qubits = n_qubits self.device = device self.batch_size = batch_size
[docs] def fit(self, X): """Fit the kernel by determining the encoder parameters. Parameters ---------- X : array-like, shape (n_samples, n_features) Training data. Returns ------- self : FidelityQuantumKernel """ if self.encoder is None: self.encoder_ = AngleEncoder(n_qubits=self.n_qubits) else: self.encoder_ = self.encoder self.encoder_.fit(X) self.n_qubits_ = self.encoder_.n_qubits_ return self
def _compute_overlap(self, x1, x2): """Compute fidelity between two encoded states.""" dev = qml.device(self.device, wires=self.n_qubits_) @qml.qnode(dev, interface="autograd") def circuit(): self.encoder_.encode(x1, wires=range(self.n_qubits_)) qml.adjoint(self.encoder_.encode)(x2, wires=range(self.n_qubits_)) return qml.probs(wires=range(self.n_qubits_)) # Probability of all zeros = :math:`|\\langle x1|x2 \\rangle|^2` probs = circuit() return probs[0]
[docs] def compute_matrix(self, X, Y=None): """Compute the kernel matrix. Parameters ---------- X : array-like, shape (n_samples_X, n_features) First set of vectors. Y : array-like, shape (n_samples_Y, n_features), optional Second set of vectors. If None, uses X. Returns ------- K : ndarray, shape (n_samples_X, n_samples_Y) Kernel matrix. """ check_is_fitted(self, ["encoder_", "n_qubits_"]) if Y is None: Y = X symmetric = True else: symmetric = False n_samples_X = len(X) n_samples_Y = len(Y) K = np.zeros((n_samples_X, n_samples_Y)) # Simple double loop with optional row batching if self.batch_size is None: # Original: no batching for i in range(n_samples_X): for j in range(n_samples_Y): if symmetric and j < i: K[i, j] = K[j, i] else: K[i, j] = self._compute_overlap(X[i], Y[j]) else: # Row-based batching for i_start in range(0, n_samples_X, self.batch_size): i_end = min(i_start + self.batch_size, n_samples_X) for i in range(i_start, i_end): for j in range(n_samples_Y): if symmetric and j < i: K[i, j] = K[j, i] else: K[i, j] = self._compute_overlap(X[i], Y[j]) return K
def __call__(self, X, Y=None): """Compute kernel matrix (makes object callable like a sklearn kernel).""" return self.compute_matrix(X, Y)