Source code for psipose.encoders.amplitude
"""Amplitude encoding: encode a vector as quantum state amplitudes."""
import numpy as np
import pennylane as qml
from ._base import BaseEncoder
[docs]
class AmplitudeEncoder(BaseEncoder):
"""Amplitude encoding of a classical vector into quantum amplitudes.
Maps a classical feature vector :math:`\\mathbf{x}` to the amplitudes
of a quantum state :math:`|\\phi(\\mathbf{x})\\rangle`.
Requires :math:`n_{\\text{qubits}}` such that
:math:`2^{n_{\\text{qubits}}} \\ge n_{\\text{features}}`.
This implements the state preparation method from:
- Möttönen, Vartiainen, Bergholm & Salomaa (2004),
"Quantum circuit for preparing arbitrary states" (arXiv:quant-ph/0407010).
The :func:`qml.AmplitudeEmbedding` template uses this algorithm.
- Schuld & Petruccione (2018), "Machine Learning with Quantum Computers",
Springer. Chapter 6 covers amplitude encoding as a feature map.
Mathematically, for a normalized feature vector
:math:`\\mathbf{x} = (x_1, \\dots, x_d)` with :math:`\\|\\mathbf{x}\\| = 1`:
.. math::
|\\phi(\\mathbf{x})\\rangle = \\sum_{i=1}^{d} x_i |i-1\\rangle
where :math:`d = 2^{n_{\\text{qubits}}}`. If the input dimension
is not a power of 2, it is padded with zeros to the next power of 2.
Parameters
----------
n_qubits : int, optional
Number of qubits to use. If None, the smallest power of 2
that can accommodate the feature dimension is used.
normalize : bool, default=True
Whether to normalize the input vector before encoding.
Attributes
----------
n_features_ : int
The original number of features (set after fit).
Examples
--------
>>> encoder = AmplitudeEncoder()
>>> encoder.fit(X_train) # determines n_qubits_ based on n_features
>>> # X_train.shape[1] must be <= 2**n_qubits
"""
def __init__(self, n_qubits=None, normalize=True):
super().__init__(n_qubits)
self.normalize = normalize
self.n_features_ = None
[docs]
def fit(self, X):
"""Determine the number of qubits needed.
For amplitude encoding, we need
:math:`2^{n_{\\text{qubits}}} \\ge n_{\\text{features}}`.
If n_qubits is not specified, choose the smallest that works.
"""
n_features = X.shape[1]
self.n_features_ = n_features
if self.n_qubits is None:
# Find smallest n such that :math:`2^n \ge n_{\\text{features}}`
self.n_qubits_ = int(np.ceil(np.log2(max(1, n_features))))
else:
if 2**self.n_qubits < n_features:
raise ValueError(
f"n_qubits={self.n_qubits} is too small for {n_features} features. "
f"Need at least {int(np.ceil(np.log2(n_features)))} qubits."
)
self.n_qubits_ = self.n_qubits
return self
[docs]
def encode(self, x, wires):
"""Apply amplitude encoding to the given wires.
Pads or truncates the input to match
:math:`2^{\\text{len(wires)}}`.
Note: ``normalize=False`` is passed to ``qml.AmplitudeEmbedding``
because normalization is handled manually above (using ``self.normalize``)
to support the zero-vector edge case.
"""
n_wires = len(wires)
dim = 2**n_wires
# Prepare the amplitude vector
x_padded = np.zeros(dim)
n_to_use = min(len(x), dim)
x_padded[:n_to_use] = x[:n_to_use]
if self.normalize:
norm = np.linalg.norm(x_padded)
if norm > 0:
x_padded = x_padded / norm
else:
x_padded[0] = 1.0 # |0> state for zero vector
# Use AmplitudeEmbedding with normalize=False (handled manually above)
qml.AmplitudeEmbedding(x_padded, wires=wires, normalize=False, pad_with=0.0)