Encoding¶
Getting classical data into a circuit, and the scaling decisions that come first.
qmlkit.encoding.angle¶
angle ¶
Angle and basis encoding — getting classical numbers into a circuit.
angle_encode ¶
angle_encode(
x: Sequence[float],
rotation: str = "ry",
trainable: bool = False,
) -> CircuitSpec
One feature per qubit, written into a rotation angle.
trainable=False bakes the values in as literals. trainable=True makes
them circuit parameters instead — which is what lets the same shift rule
deliver df/dx, the gradient a classical pre-net needs in a hybrid stack.
Source code in src/qmlkit/encoding/angle.py
basis_encode ¶
basis_encode(bits: Sequence[int]) -> CircuitSpec
Computational-basis encoding: flip a qubit wherever the bit is 1.
Source code in src/qmlkit/encoding/angle.py
basis_index ¶
n_qubits_for ¶
Qubits needed to hold n_values amplitudes: ceil(log2 N).
qmlkit.encoding.amplitude¶
amplitude ¶
Amplitude encoding — :math:n qubits hold :math:2^n numbers.
Built from uniformly-controlled rotations, not from a backend state-preparation primitive. That matters: the resulting circuit is made of ordinary registered gates, so it runs identically on every backend, can be drawn and transpiled, and its resource cost is visible rather than hidden inside an SDK call.
The construction is the standard one. Magnitudes come from a binary tree of partial
norms, each level applying a uniformly-controlled Ry; phases, when the data is
complex, come from a second cascade of uniformly-controlled Rz.
A uniformly-controlled rotation decomposes recursively:
.. code-block:: text
UCR(theta, [c0, ...], t) = UCR(alpha, [...], t) . CX(c0, t) . UCR(beta, [...], t) . CX(c0, t)
alpha_j = (theta_j + theta_{j + h}) / 2 beta_j = (theta_j - theta_{j + h}) / 2
which costs 2**m rotations and 2**m CX gates for m controls — the
exponential price of loading exponentially many numbers.
Global phase. The phase cascade reproduces every relative phase exactly and
drops one overall factor, which is unobservable. If you embed an amplitude-encoded
block inside a larger controlled circuit, that factor stops being global; use
check=True to assert the prepared state matches your target up to phase.
pad_to_power_of_two ¶
Zero-pad a vector up to the next power of two.
Source code in src/qmlkit/encoding/amplitude.py
uniformly_controlled_rotation ¶
uniformly_controlled_rotation(
qc: QCircuit,
rotation: str,
angles: NDArray[Any],
controls: Sequence[int],
target: int,
) -> None
Apply R(angles[k]) to target for each control basis state k.
controls[0] is the most significant bit of k. Emits only Ry/Rz
and CX, so it works on any backend.
Source code in src/qmlkit/encoding/amplitude.py
state_preparation_angles ¶
state_preparation_angles(
amplitudes: NDArray[Any],
) -> tuple[list[NDArray[Any]], list[NDArray[Any]]]
Ry angles per level (magnitudes) and Rz angles per level (phases).
Source code in src/qmlkit/encoding/amplitude.py
amplitude_encode ¶
amplitude_encode(
vec: Sequence[float] | NDArray[Any],
normalize: bool = True,
pad: bool = True,
check: bool = False,
) -> CircuitSpec
Encode a vector into the amplitudes of ceil(log2 len(vec)) qubits.
Only the direction of the vector survives — amplitudes must be normalised, so
the magnitude is lost. normalize=False refuses a vector that is not already
a unit vector rather than silently rescaling it.
check=True re-simulates the circuit and asserts it prepares the intended
state (up to global phase). Cheap insurance while you are getting a pipeline
working; leave it off in a training loop.
Source code in src/qmlkit/encoding/amplitude.py
qmlkit.encoding.feature_maps¶
feature_maps ¶
Pauli feature maps.
Each term :math:S of a feature map contributes
.. math:: \exp!\left(-i\,\phi_S(x) \prod_{j \in S} P_j\right)
realised as W . (CX ladder, Rz(2 phi), CX ladder) . W^dagger, where W is the
basis change that sends each Pauli to Z. The default data map is the standard
one: :math:\phi_{\{i\}}(x) = x_i for singletons and
:math:\phi_S(x) = \prod_{j \in S}(\pi - x_j) for higher-order terms.
A Pauli feature map needs two pieces that are usually left implicit: the basis
change that diagonalises each Pauli string, and the data map that turns features into
angles. Both are public here — :func:basis_change and :func:default_data_map — so
either can be replaced without rewriting the map, and the map is tested against the
analytic kernel it is supposed to induce rather than against itself.
FeatureMap ¶
Turns a feature vector into a circuit.
Subclasses implement :meth:build. adjoint comes free from the IR, which
is what the fidelity kernel's compute-uncompute test needs.
build ¶
build(x: ArrayLike) -> CircuitSpec
angles ¶
build_parametric ¶
build_parametric(offset: int = 0) -> CircuitSpec
The circuit with each encoding angle as a free parameter.
This is what lets a gradient flow back to the data: the circuit is
differentiated with respect to its angles, and the chain rule to x is
finished classically by :meth:angle_jacobian.
Source code in src/qmlkit/encoding/feature_maps.py
angle_jacobian ¶
d(angle) / d(feature), shape (n_angles, n_features).
The default differences the classical data map — no circuits involved, so it costs nothing quantum. Override it when a closed form is available.
Source code in src/qmlkit/encoding/feature_maps.py
adjoint ¶
adjoint(x: Sequence[float]) -> CircuitSpec
resources ¶
PauliFeatureMap ¶
PauliFeatureMap(
n_features: int,
paulis: Sequence[str] = ("Z", "ZZ"),
reps: int = 2,
entanglement: str = "linear",
data_map: DataMap | None = None,
)
Bases: FeatureMap
The general Pauli feature map, for any set of Pauli strings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_features
|
int
|
One qubit per feature. |
required |
paulis
|
Sequence[str]
|
Pauli strings to include, e.g. |
('Z', 'ZZ')
|
reps
|
int
|
How many times to repeat the whole block. More reps means higher reachable frequencies, at proportional depth. |
2
|
entanglement
|
str
|
Pattern for two-body terms: |
'linear'
|
data_map
|
DataMap | None
|
Override the default :func: |
None
|
Source code in src/qmlkit/encoding/feature_maps.py
n_angles
property
¶
One angle per term. Reps reuse the same angles, so they add depth only.
angle_jacobian ¶
Closed form for the standard data map; falls back to differencing otherwise.
Source code in src/qmlkit/encoding/feature_maps.py
ZFeatureMap ¶
Bases: PauliFeatureMap
First-order, no entanglement — so its kernel factorises over features.
Source code in src/qmlkit/encoding/feature_maps.py
ZZFeatureMap ¶
Bases: PauliFeatureMap
First order plus entangling ZZ couplings — the kernel stops factorising.
Source code in src/qmlkit/encoding/feature_maps.py
AngleFeatureMap ¶
AngleFeatureMap(
n_features: int,
rotation: str = "ry",
entangle: bool = True,
entanglement: str = "chain",
reps: int = 1,
)
Bases: FeatureMap
One rotation per feature, optionally followed by an entangling layer.
The plainest map there is, and the one whose kernel has a closed form:
cos^2((x - x')/2) per feature when entangle=False.
Source code in src/qmlkit/encoding/feature_maps.py
angle_jacobian ¶
default_data_map ¶
The standard data map: x_i for one index, prod(pi - x_j) for more.
The product form is what makes higher-order terms nonlinear in the features — a linear map there would leave the kernel factorisable and the entanglers pointless.
Source code in src/qmlkit/encoding/feature_maps.py
basis_change ¶
Gates that rotate pauli into the Z basis, and the gates that undo it.
Returns (forward, inverse) as gate-name tuples applied in circuit order.
X = H Z H so W = H; Y = (SH) Z (SH)^dagger so W = H S^dagger,
which in circuit order is sdg then h.
Source code in src/qmlkit/encoding/feature_maps.py
pauli_terms ¶
pauli_terms(
paulis: Sequence[str],
n_features: int,
entanglement: str = "linear",
) -> list[tuple[tuple[int, ...], str]]
Expand Pauli strings into concrete (qubit indices, pauli string) terms.
A one-character string like "Z" becomes one term per qubit. A two-character
string like "ZZ" follows the entanglement pattern. Longer strings enumerate
combinations of that size.
Source code in src/qmlkit/encoding/feature_maps.py
qmlkit.encoding.hamiltonian¶
hamiltonian ¶
Hamiltonian (IQP-style) encoding and data re-uploading.
Hamiltonian encoding evolves the register under a data-dependent Hamiltonian
:math:H(x) = \sum_i x_i Z_i + \sum_{(i,j)} x_i x_j Z_i Z_j for a time t,
Trotterised into steps slices. Because every term commutes here, the Trotter
split is exact at any number of steps — steps changes the circuit depth and
nothing else. That is worth knowing before anyone tunes it hoping for accuracy.
Data re-uploading interleaves the encoding with trainable blocks. Each repeat
widens the reachable Fourier spectrum: L uploads reach frequencies 0..L,
which is the knob that decides which functions the model can represent at all,
separately from the ansatz that picks the coefficients.
DataReuploadEncoder ¶
DataReuploadEncoder(
n_features: int,
n_uploads: int = 3,
rotations: Sequence[str] = ("rz", "ry", "rz"),
encoding_rotation: str = "ry",
entanglement: str | None = "chain",
trainable_input: bool = False,
)
One convenient re-uploading shape: angle encoding, rotations, entangler.
.. note::
Re-uploading is a pattern, not a structure — any feature map, any
trainable block, any interleaving. This class fixes one convenient choice.
For anything else use :func:qmlkit.reupload, or compose
:class:~qmlkit.ansatz.blocks.EncodingLayer directly with the block
vocabulary. This remains for the plain angle-encoding case.
The circuit alternates S(x) — an angle encoding — with W(theta), a
trainable rotation block, n_uploads times. Data enters as literals by
default; pass trainable_input=True to make the features circuit parameters
too, which is what yields df/dx for a classical pre-net.
The parameter vector is laid out as (n_uploads, n_qubits, len(rotations)),
flattened, with the input parameters (if trainable) appended after it.
Source code in src/qmlkit/encoding/hamiltonian.py
n_params
property
¶
Total circuit parameters — weights, plus inputs when they are trainable.
build ¶
build(x: Sequence[float] | None = None) -> CircuitSpec
Build the circuit.
With trainable_input=False (default) x is required and baked in.
With trainable_input=True x is ignored: the features become
parameters, supplied later alongside the weights.
Source code in src/qmlkit/encoding/hamiltonian.py
trotter_rz_angle ¶
Single-qubit Rz angle per Trotter step: 2 * x_i * t / steps.
Source code in src/qmlkit/encoding/hamiltonian.py
trotter_zz_angle ¶
Two-qubit coupling angle per Trotter step: 2 * x_i * x_j * t / steps.
Source code in src/qmlkit/encoding/hamiltonian.py
hamiltonian_encode ¶
hamiltonian_encode(
x: Sequence[float],
t: float = 1.0,
steps: int = 3,
entanglement: str = "chain",
initial_hadamard: bool = True,
) -> CircuitSpec
Evolve under a data-dependent Ising Hamiltonian.
initial_hadamard=True starts in the uniform superposition, which is what
makes the Z-diagonal evolution do anything observable — without it the register
stays in a computational basis state and only picks up a global phase.
Source code in src/qmlkit/encoding/hamiltonian.py
n_reachable_frequencies ¶
L uploads reach frequencies 0..L -- so L + 1 of them.
This holds only when the trainable block does not commute with the encoding
rotation. If it does, the uploads merge into one rotation and the model reaches
a single frequency instead. :class:DataReuploadEncoder warns when you build
such a pairing.
Source code in src/qmlkit/encoding/hamiltonian.py
qmlkit.encoding.pipeline¶
Standardise, reduce to n_qubits columns, scale into rotation angles — one
scikit-learn-clonable object, used in every case study.
pipeline ¶
Getting a real dataset onto a small number of qubits, once and reproducibly.
Almost every quantum model starts the same way: standardise the features, reduce them to as many columns as you have qubits, and scale those into rotation angles. Done by hand that is three objects to keep in sync and one easy mistake — fitting the reducer on the test set — so it is one object here.
pipeline = FeaturePipeline(n_qubits=4).fit(X_train)
Z_train, Z_test = pipeline.transform(X_train), pipeline.transform(X_test)
fit sees only the training data, and transform reuses exactly what it learned.
:attr:FeaturePipeline.explained_variance_ reports what the reduction cost, because a
model that never saw 20% of the variance is not underperforming — it was never shown
the data.
Everything here is duck-typed to scikit-learn's estimator protocol (get_params /
set_params / fit / transform), so it drops into Pipeline and
GridSearchCV without qmlkit depending on scikit-learn.
SklearnCompatible ¶
get_params / set_params, read off the constructor signature.
scikit-learn duck-types: clone, Pipeline and GridSearchCV need these
two methods, not a base class. Implementing them directly is what lets a qmlkit
estimator sit in a scikit-learn workflow while scikit-learn stays an optional
dependency — which matters, because the NumPy backend is meant to work alone.
The one rule this imposes: an __init__ parameter must be stored on an
attribute of the same name, unchanged.
FeaturePipeline ¶
FeaturePipeline(
n_qubits: int,
method: str = "pca",
standardize: bool = True,
angle_range: tuple[float, float] = (0.0, 2 * pi),
)
Bases: SklearnCompatible
Standardise, reduce to n_qubits columns, and scale into rotation angles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_qubits
|
int
|
How many columns to come out with — one rotation angle per qubit. |
required |
method
|
str
|
|
'pca'
|
standardize
|
bool
|
Centre and scale to unit variance first. PCA without this is dominated by whichever feature happens to be measured in the largest units. |
True
|
angle_range
|
tuple[float, float]
|
Where the output lands. The default |
(0.0, 2 * pi)
|
Source code in src/qmlkit/encoding/pipeline.py
fit ¶
fit(X: NDArray[Any], y: Any = None) -> FeaturePipeline
Learn every step from the training data alone.
Source code in src/qmlkit/encoding/pipeline.py
transform ¶
Apply the fitted steps. Never re-fits — that is the whole point.
Source code in src/qmlkit/encoding/pipeline.py
qmlkit.encoding.scaling¶
scaling ¶
Getting real data into the range and width an encoding needs.
Two problems every quantum model hits before any quantum step happens: features arrive on arbitrary scales when rotations want radians, and there are usually more features than qubits.
These are preprocessing, not classical baselines — no model is fitted here, and there is no sklearn dependency. The PCA reduction is a plain SVD.
AngleScaler
dataclass
¶
AngleScaler(
lo: float = 0.0,
hi: float = 2 * pi,
data_min: NDArray[Any] | None = None,
data_max: NDArray[Any] | None = None,
)
Fit-then-transform angle scaling, so train and test share one range.
PCAReducer
dataclass
¶
PCAReducer(
n_components: int,
mean_: NDArray[Any] | None = None,
components_: NDArray[Any] | None = None,
explained_variance_ratio_: NDArray[Any] | None = None,
)
Project features onto their leading principal components, via SVD.
Angle encoding needs one qubit per feature, so a 64-feature dataset needs 64
qubits — usually out of reach. Reducing first is the ordinary way through, and
explained_variance_ratio_ says how much you gave up doing it.
to_angle_range ¶
to_angle_range(
x: NDArray[Any],
lo: float = 0.0,
hi: float = 2 * pi,
data_min: NDArray[Any] | None = None,
data_max: NDArray[Any] | None = None,
) -> NDArray[Any]
Rescale features into an angle window, per column.
Fit the range on training data and reuse it on test data by passing
data_min/data_max explicitly — otherwise each call rescales to its own
extremes, which silently makes train and test incomparable. A constant column
maps to the middle of the window rather than dividing by zero.
Source code in src/qmlkit/encoding/scaling.py
reduce_to_qubits ¶
reduce_to_qubits(
x: NDArray[Any],
n_qubits: int,
method: str = "pca",
to_angles: bool = True,
lo: float = 0.0,
hi: float = 2 * pi,
) -> NDArray[Any]
Reduce a feature matrix to n_qubits columns, ready for angle encoding.
method="pca" keeps the leading principal components; method="truncate"
keeps the first n_qubits columns unchanged, which is only sensible when the
features are already ordered by importance.