qmlkit¶
A quantum machine learning library built to refuse a plausible wrong number.
What makes this field difficult is not writing the circuit. It is that a mistake here usually does not raise: a re-uploading model whose trainable rotations commute with its encoding trains happily and reaches one Fourier frequency instead of eight; a kernel concentrates until every pair of points looks alike and still returns a Gram matrix; a quantum model beats its classical baseline by less than the spread between folds and gets written up as a result. All three run. All three return numbers in the right range. All three are wrong.
So the tools that catch those are not an afterthought — they are the library:
# docs: requires torch
import qmlkit as qk
print(qk.plan(qk.hardware_efficient(3, 2))) # what the run will cost, first
print(qk.diagnose(qk.hardware_efficient(3, 2))) # what does not raise
diagnose names the failure and the edit that fixes it,
baseline puts the classical bar on identical folds and
refuses to call a lead inside the fold spread a result, progress
says how much of a run is left, and selfcheck compares every exact gradient route
against every other. The same instinct runs through the rest: adjoint
refuses on a noisy backend rather than quietly differentiating a
noiseless one, and an unknown gate is refused by name rather than approximated.
Why one diagnose can read any model¶
Because a circuit is data, not a backend object.
That one decision is what makes the layer above possible: diagnose walks
spec.ops, so it works on any ansatz, on any backend, whether or not the library has
ever seen the gates in it. The same structure serves a single gradient implementation
across every backend, lets inventing an ansatz take one line and inherit correct
gradients for free, and makes weight tying expressible at all — a parameter that fills
several slots, differentiated one occurrence at a time, which is the thing a shift rule
gets silently wrong when circuits are opaque objects.
import numpy as np
import qmlkit as qk
ansatz = qk.hardware_efficient(3, n_layers=2)
theta = ansatz.init(seed=0)
spec = ansatz.build()
observable = qk.Z(0) + 0.5 * qk.ZZ(0, 2)
print(f"<O> = {qk.expval(spec, observable, theta=theta):+.6f}")
print(f"gradient = {np.round(qk.grad(spec, theta, observable)[:4], 4)} ...")
print(f"cost = 1 pass (adjoint) vs {qk.gradient_cost(spec, 'parameter-shift')} circuits (parameter-shift)")
Where to start¶
-
Eight pages, start to finish. Every snippet is executed by the test suite, so none of them can quietly stop working.
-
Why parameter-shift is subtler than it looks, which gradient to reach for, and how to add your own gate, ansatz, backend or estimator.
-
Generated from the docstrings, so it cannot drift from the code.
-
301 cross-validation cases against PennyLane, and the four genuine convention differences that surfaced.
-
Eight whole problems, raw data to defensible number. In most of them the number is that the quantum model lost.
What is actually here¶
| Backends | NumPy (reference), SpinQit, Qiskit, Cirq, Torch — one circuit, one answer, checked against each other |
| Gradients | adjoint, backprop, Hadamard-test, parameter-shift, SPSA, finite differences, behind one grad() |
| Encoding | angle, amplitude, basis, Hamiltonian, and Pauli feature maps, with input gradients |
| Ansätze | a composable block vocabulary plus ten templates written in it |
| Kernels | three overlap estimators, PSD repair, QSVC/QSVR, trainable and projected kernels |
| PyTorch | QuantumLayer, VQC, VQRegressor, QCNN/QLSTM/MPS, dressed networks |
| Analysis | expressibility, Meyer–Wallach entanglement, barren-plateau scans, Fourier spectra, Fubini–Study geometry |
| Noise | cirq-density and qiskit-aer evolve a density matrix; shot noise stays separable from decoherence, and state-based gradients refuse |
| Interop | from_qasm, from_qiskit, from_pennylane, from_cirq — circuits come back in as well as out |
| Honesty | diagnose, selfcheck, baseline, evaluate, plan, fingerprint — the layer this library is actually for |
Three layers, and you pick where to stand¶
Nothing at a higher layer hides a lower one.
# docs: requires torch
import numpy as np
import qmlkit as qk
rng = np.random.default_rng(0)
X = rng.normal(size=(40, 4))
y = (X[:, 0] * X[:, 1] > 0).astype(int)
# 1. a ready-made model
model = qk.VQC(n_features=4, n_classes=2, seed=0).fit(X, y, epochs=5)
# 2. a torch layer, in any nn.Module you like
layer = qk.QuantumLayer(qk.ZZFeatureMap(3), qk.hardware_efficient(3, 2), [qk.Z(0)])
# 3. circuits and gradients directly
g = qk.grad(qk.hardware_efficient(3, 2).build(), qk.hardware_efficient(3, 2).init(seed=0))
Scope¶
Simulator-only for the whole 0.x line. That is a design constraint, not a
missing feature: it makes adjoint the right default gradient, makes shot noise
opt-in rather than unavoidable, and keeps the focus on properties of the model —
trainability, expressibility, concentration — rather than properties of a device.
Parameter-shift is still first-class, because it is the rule that stays valid when
you do move to hardware.
Apache-2.0. Source on GitHub.