A quantum machine learning framework built on Qiskit, PyTorch and scikit-learn. It pulls together a few building blocks I keep reaching for: quantum kernels, a trainable quantum neural network layer, a couple of hybrid models, plus some tools for noise mitigation, circuit optimization, feature selection and plotting.
Python 3.10+ and Qiskit 2.3 or newer (both floors are what the test suite actually runs against). Install the dependencies with:
pip install -r requirements.txt
or install the package itself (which also registers the qmlf command):
pip install .
It uses the current Qiskit primitives (StatevectorEstimator) and the
zz_feature_map / real_amplitudes circuit builders, so it runs on Qiskit 2.x
without any of the old V1 shims.
Classify with a quantum kernel in three lines — no circuits, no Gram-matrix wiring, no qiskit imports:
from qmlf import QuantumClassifier
clf = QuantumClassifier() # qubits sized from the data
clf.fit(X_train, y_train) # bandwidth tuned by deterministic CV
clf.predict(X_test)Everything that used to require insider knowledge happens inside fit: the
encoding circuit is sized from the feature count, the fidelity Gram matrices
are computed in the right orientation and wired into an SVM, and bandwidth
— the knob that separates a working fidelity kernel from a degenerate one —
is selected by a leak-free, deterministic cross-validation sweep. On the
reference dataset this takes the zero-knowledge path from 0.40 (chance, the
old defaults) to 0.70+ with no arguments at all. The chosen value is in
clf.bandwidth_, the full sweep table in clf.cv_results_, and every knob
(mode, feature_map, entanglement, bandwidths, C) is still there for
power users.
Nothing must be chosen; everything can be. Every automatic decision has an override and reports what it decided:
| automatic | override | decision visible in |
|---|---|---|
| qubit count (wide data is PCA-reduced to fit the simulator) | max_qubits= / None |
n_qubits_, reduction_ |
| bandwidth sweep | bandwidth=0.05 |
bandwidth_, cv_results_ |
| sweep grid | bandwidths=(...) |
cv_results_ |
| encoding search | mode= / feature_map= (or "auto" to search) |
mode_, feature_map_ |
| safe normalisation for whitened modes | normalize= |
— |
With mode="auto", feature_map="auto" the encoding itself is selected by
cross-validation — entangled vs unentangled map, raw vs Mahalanobis-whitened
data — so the choice qiskit tutorials hand to the reader becomes a searched
hyperparameter. After fitting, clf.diagnose() reports whether the kernel is
concentrated (the classic silent fidelity-kernel failure), its spectrum, and
its centered kernel-target alignment in one call.
Or work with the kernel directly:
import numpy as np
from qmlf import QuantumKernel
X = np.random.rand(20, 4)
kernel = QuantumKernel().fit(X) # n_qubits inferred from the data
gram = kernel.compute_kernel_matrix(X) # 20 x 20 kernel matrix(n_qubits can still be passed explicitly; a mismatch with the data raises
an error naming both numbers.)
Tune the bandwidth. Fidelity kernels concentrate: as the encoding angles
span a wider range, off-diagonal similarities collapse toward zero, the Gram
matrix approaches the identity, and a downstream SVM memorises the training set
instead of generalising. bandwidth scales the angles down and is usually the
single most valuable thing to sweep — on a real benchmark it was worth roughly
0.3 AUROC:
kernel = QuantumKernel(n_qubits=4, mode="ZZ", bandwidth=0.1).fit(X)For mode="covariant" (alias "mahalanobis" — Mahalanobis/ZCA whitening then a
ZZ map, not the group-covariant kernel of Glick et al.) also pass
normalize="maxabs". Whitening cancels any scaling applied to its input, so
without normalisation the whitened angles can exceed [-pi, pi] and alias:
kernel = QuantumKernel(
n_qubits=4, mode="mahalanobis", bandwidth=0.5, normalize="maxabs"
).fit(X)Both are available from the CLI too (--bandwidth, --normalize). To run
against real hardware or a noisy simulator, pass fidelity= or sampler=; the
exact statevector fast path is used automatically only when neither is given.
Choose the encoding. The feature map and its entanglement structure are
hyperparameters, not fixed choices. Entanglement drives the concentration that
bandwidth compensates for, so the two interact:
QuantumKernel(n_qubits=8, feature_map="zz", entanglement="linear") # or "full", "circular"
QuantumKernel(n_qubits=8, feature_map="z") # no entanglement, much faster
QuantumKernel(n_qubits=8, feature_map=my_circuit) # any Qiskit QuantumCircuitOn at least one real dataset the unentangled "z" map matched or beat "zz"
while running 6-11x faster, so the entangled default is not automatically the
right choice — sweep it. CLI: --feature-map, --entanglement.
Don't hand
.fidelity_quantum_kerneltoQSVC. Doing so bypasses_prepare, silently discarding whitening,normalizeandbandwidth— somode="covariant"becomes indistinguishable frommode="ZZ"with no error. Usecompute_kernel_matrixwithSVC(kernel="precomputed")instead. Accessing the attribute now warns when it would change your answer.
Drop a trainable quantum layer into a torch model:
import torch
from qmlf import create_advanced_qnn_layer
layer = create_advanced_qnn_layer(n_qubits=4, output_dim=8)
layer.eval() # see the note below
y = layer(torch.randn(16, 4)) # (16, 8)Read-outs are exact by default. The quantum layers evaluate their observables straight off the statevector, so the same input gives the same answer every time. That is what you want when you are seeding an experiment or asserting on a number.
To emulate a finite-shot device instead, ask for it:
create_advanced_qnn_layer(n_qubits=4, precision=0.015625) # sampled, noisyBefore 1.2.1 the sampled path was the default, inherited from
qiskit-machine-learning, and there was no way to turn it off. On a circuit whose
exact expectation value was 0.032, five successive forward passes spanned 0.037
— more drift than signal — and training would not reproduce under a fixed seed.
If you were relying on that noise, pass precision=0.015625 to get it back.
One PyTorch footgun worth repeating, because the quantum layers carry dropout in
their classical heads: torch.no_grad() turns off gradients, not dropout.
Call .eval() for inference. QuantumPipeline.run() does this for you and
restores the layer's previous mode afterwards.
Or chain a kernel, a QNN and a Hilbert-space plot together in one call:
import numpy as np
from qmlf import create_quantum_pipeline
X = np.random.rand(20, 4)
result = create_quantum_pipeline().run(X, labels=None)
result["kernel_matrix"] # 20 x 20 quantum kernel
result["qnn_output"] # QNN read-out for each sampleThe number of qubits is taken from the data when it isn't given.
Installing the package puts a qmlf command on the path (or run it as
python -m qmlf). It works out of the box on a synthetic dataset, or point it
at your own CSV with --csv (the last column is the target unless you pass
--target):
qmlf benchmark --n-qubits 4
qmlf visualize --mode covariant --save kernel.html
models:QuantumClassifierandQuantumRegressor— three-line quantum-kernel estimators (auto-sized, auto-tuned, sklearn-compatible).analysis: the Huang-et-al. quantum-advantage screen (quantum_advantage_report,geometric_difference,model_complexity).ops: quantum fidelity kernel over a ZZ feature map, plus a QSVC vs. XGBoost benchmark.graph: graph-diffusion kernel, with an optional quantum kernel backend (use_quantum=True).qnn,hybrid,transformer: torch layers wrapping a realEstimatorQNN, read out from per-qubit Z expectation values.chem: charge-aware Coulomb-matrix descriptors, a model Hamiltonian, classical ground-state energy, and an optional VQE.nisq: transpiler-based circuit depth and gate reduction.noise: readout calibration, depolarizing-channel inversion, and zero-noise extrapolation.optimizers: quantum-inspired evolutionary feature selection.federated: sample-weighted federated averaging.viz: Plotly charts (kernels, Bloch sphere, training curves, barren-plateau diagnostics).integration: end-to-end pipelines, including a one-lineQuantumPipelinethat runs kernel -> QNN -> visualization.
Everything here runs on Qiskit, so nothing is impossible elsewhere — the point is what exists as a built-in versus what you would have to build:
- Projected quantum kernels (Huang et al. 2021) as a mode string:
QuantumKernel(kernel="projected")builds the RBF over per-qubit reduced density matrices, with a train-fitted median-heuristicgamma. This is the literature's answer to exponential fidelity-kernel concentration, and it measurably works: at 8 qubits and bandwidth 1.0 the fidelity Gram matrix collapses to 0.004 mean off-diagonal while the projected kernel holds 0.356 — a 100x difference on the exact failure mode. Not implemented anywhere in qiskit-machine-learning (verified: zero source references). - Per-feature (ARD) bandwidth.
bandwidth=np.array([...])scales each feature's encoding angle independently — and because it lands after whitening, it is a per-direction metric that no external rescaling of the input can reproduce (whitening cancels input scaling exactly). - CV-free bandwidth via the median heuristic.
bandwidth="median"fits the scale from the training angles' pairwise distances — one deterministic pass, no folds, no SVM fits. - Supervised (Fisher) whitening.
mode="fisher"whitens by the within-class scatter instead of the global covariance, so noise directions shrink and class-separating directions keep their scale — metric learning inside the quantum kernel, using the labels qiskit's kernels never see. - Bandwidth as a first-class, auto-tuned parameter.
FidelityQuantumKernelhas no bandwidth; the literature (and our measured sweeps: 0.40 -> 0.75+) says it is the single most important knob. Here it is a constructor argument and, by default, tuned for you. - Searchable encodings.
mode="auto", feature_map="auto"cross-validates entangled/unentangled maps and raw/whitened preprocessing jointly. QSVC asks you to hand-build the feature map before you start. - Mahalanobis-whitened fidelity kernels with leak-free, train-fitted normalisation — covariance-adapted quantum kernels as a mode string, not a research project.
- O(n) exact statevector fast path. Pure-state fidelity Gram matrices are computed from n statevectors and one matmul instead of n² circuit pairs — measured 6–11x faster than the pairwise path, and bit-for-bit reproducible.
- The quantum-advantage screen (Huang et al. 2021). One call —
quantum_advantage_report(X, y)— answers the question every quantum kernel project should ask before running a single circuit: can this quantum kernel beat a classical RBF on this data? Computes the paper's geometric difference g and both model complexities from the Gram matrices alone, with a conservative verdict. No framework ships this. - Nystrom kernel approximation with a circuit budget.
kernel.nystrom_features(X, n_landmarks=m)gives an explicit feature map usable with any linear model, cutting a hardware run from n(n-1)/2 fidelity circuits to ~n*m;kernel.circuit_budget(n)prints the arithmetic for every evaluation path. qiskit-ml has no kernel approximation and no resource accounting. - Alignment-selected bandwidth.
bandwidth="alignment"picks the scale that maximises centered kernel-target alignment — one Gram matrix per candidate, no folds, no SVM fits; a third selection principle beside CV and the median heuristic. - A regressor with the full auto stack. qiskit's QSVR exists but bare —
no bandwidth (the parameter does not exist there), no tuning, no
dimensionality handling.
QuantumRegressoris kernel ridge with everything above: auto-sized, auto-reduced, CV- or median-tuned, projected kernels included. - A post-fit concentration guard.
QuantumClassifierwarns at fit time when the kernel it just built is degenerate, instead of letting a memorising model reach the test set silently. - Kernel health diagnostics.
kernel_diagnostics()/clf.diagnose()detect concentration and report centered kernel-target alignment. Neither qiskit-machine-learning nor TFQ ships a built-in check for the failure mode their own kernels exhibit. - Exact-by-default QNN read-outs with shot noise as an explicit opt-in
(
precision=), where the upstream default silently samples. - Deterministic everything: same data, same answer, across processes — the property a benchmark or a paper actually needs.
The quantum kernels and the VQE run on simulators, and the cost grows with the number of circuit evaluations, so keep datasets and qubit counts small. The chemistry module works from a geometry-derived model Hamiltonian, not a full electronic-structure calculation.
n_qubits must equal the number of input features — the feature map encodes one
angle per feature. Passing a mismatch raises and tells you both numbers.
A few constructor arguments are declarative and do not resize anything:
AdvancedQIGASelector(n_features=...) does not request a subset of that size
(the selector keeps whatever scores best), and FederatedQML(num_clients=...)
and AdvancedQuantumChemistryLayer(num_atoms=...) are sized from the data you
pass. All three warn when what you declared disagrees with what arrived.
Torch, scikit-learn and xgboost each vendor their own OpenMP runtime, and more
than one thread pool in a process can kill the interpreter with no traceback.
import qmlf sets OMP_NUM_THREADS=1 unless you have already chosen a value.
See LICENSE.