Source code for psipose.feature_maps.pauli

"""Pauli-Z feature map for quantum kernels.

This module implements a compact Havlicek-style feature map using
single-qubit Z rotations and two-qubit ZZ interactions. It is designed
to work with PennyLane's adjoint transform so it can be used by
``QuantumKernel`` for fidelity kernel matrices.
"""

from itertools import combinations

import numpy as np
import pennylane as qml

from .base import FeatureMap


[docs] class PauliFeatureMap(FeatureMap): """Pauli-based feature map from Havlíček et al. (2019). This feature map implements the quantum kernel approach from the landmark paper: - Havlíček et al. (2019), "Supervised learning with quantum-inspired kernel" (arXiv:1904.01567, Nature 567, 209-212). The Pauli feature map applies Pauli rotations in a layered structure with entangling gates, creating a rich non-linear embedding. Mathematically, for an input vector :math:`\\mathbf{x}` and qubits, the encoding circuit applies: .. math:: U_\\phi(\\mathbf{x}) = \\exp\\left(i \\sum_{S \\subseteq [n], |S| \\geq 2} \\phi_S(\\mathbf{x}) \\prod_{i \\in S} Z_i\\right) \\prod_{j=1}^n \\exp\\left(i x_j Z_j\\right) where :math:`\\phi_S` are functions of the input features. The implemented circuit is a Z/ZZ feature map with repeated layers: 1. Hadamard gates prepare a superposition. 2. Single-qubit RZ rotations encode individual features. 3. Two-qubit MultiRZ rotations encode pairwise feature interactions. Parameters ---------- n_qubits : int, optional Number of qubits. reps : int, default=2 Number of feature-map repetitions. entanglement : {"full", "linear", "circular", "none"}, default="full" Which two-qubit feature interactions to include. alpha : float, default=2.0 Global multiplier for all encoded angles. data_map_func : callable, optional Function called as ``data_map_func(left, right)`` for pairwise interaction angles. Defaults to ``(pi - left) * (pi - right)``. Attributes ---------- n_features_ : int Number of features seen during fit. Examples -------- >>> feature_map = PauliFeatureMap(n_qubits=2, reps=1) >>> feature_map.fit(X_train) >>> feature_map.encode(X_train[0], wires=range(feature_map.n_qubits_)) """ _VALID_ENTANGLEMENTS = {"full", "linear", "circular", "none"} def __init__( self, n_qubits=None, reps=2, entanglement="full", alpha=2.0, data_map_func=None, ): super().__init__(n_qubits) if reps < 1: raise ValueError(f"reps must be >= 1, got {reps}") if alpha <= 0: raise ValueError(f"alpha must be > 0, got {alpha}") if entanglement not in self._VALID_ENTANGLEMENTS: valid = ", ".join(sorted(self._VALID_ENTANGLEMENTS)) raise ValueError( f"entanglement must be one of {valid}, got {entanglement!r}" ) self.reps = reps self.entanglement = entanglement self.alpha = alpha self.data_map_func = data_map_func self.n_features_ = None
[docs] def encode(self, x, wires): """Apply Pauli-Z feature-map encoding to the given wires. Extra input features are truncated. Missing features are padded with zero so custom wire counts remain usable in sklearn pipelines. """ wires = list(wires) values = self._feature_values(x, len(wires)) for _ in range(self.reps): for wire in wires: qml.Hadamard(wires=wire) for feature, wire in zip(values, wires, strict=True): qml.RZ(self.alpha * feature, wires=wire) for left, right in self._entanglement_pairs(len(wires)): angle = self.alpha * self._data_map(values[left], values[right]) qml.MultiRZ(angle, wires=[wires[left], wires[right]])
[docs] def fit(self, X): """Determine qubit count and record the observed feature dimension.""" X = np.asarray(X) if X.ndim != 2: raise ValueError(f"X must be 2-dimensional, got shape {X.shape}") self.n_features_ = X.shape[1] return super().fit(X)
def _feature_values(self, x, n_wires): """Return one numeric feature value per wire.""" x = np.asarray(x, dtype=float).ravel() values = np.zeros(n_wires, dtype=float) n_to_copy = min(len(x), n_wires) values[:n_to_copy] = x[:n_to_copy] return values def _data_map(self, left, right): """Compute the pairwise feature interaction angle.""" if self.data_map_func is not None: return self.data_map_func(left, right) return (np.pi - left) * (np.pi - right) def _entanglement_pairs(self, n_wires): """Return wire index pairs for the configured entanglement mode.""" if n_wires < 2 or self.entanglement == "none": return [] if self.entanglement == "full": return list(combinations(range(n_wires), 2)) pairs = [(wire, wire + 1) for wire in range(n_wires - 1)] if self.entanglement == "circular" and n_wires > 2: pairs.append((n_wires - 1, 0)) return pairs