"""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