Every quantum SDK has its own circuit objects. A model written for PennyLane has to be rewritten to run on Qiskit Aer, and rewritten again for an IBM or IQM device. Comparing backends on your own workload turns into integration work.
We built eigenkit to remove that step. It is an open-source Python library (Apache-2.0) that describes a circuit once, as data, and lets each backend compile that description into its own SDK. Moving to another simulator or a QPU means changing the provider= argument.
pip install eigenkit
The core install is numpy, scipy and pydantic. PyTorch, scikit-learn and every quantum SDK are optional extras.
One circuit, four simulators
A small IQP circuit: Hadamards, a ring of ZZ rotations, Hadamards again.
from eigenkit import Circuit, CircuitBuilder, Z
def iqp(n, edges, angles):
qc = CircuitBuilder(n_qubits=n)
for q in range(n):
qc.h(q)
for (i, j), angle in zip(edges, angles):
qc.pauli_exponential((("Z", i), ("Z", j)), angle)
for q in range(n):
qc.h(q)
return qc.build()
spec = iqp(4, [(0, 1), (1, 2), (2, 3), (3, 0)], [0.7, 1.1, 0.4, 1.9])
observables = [Z(0) * Z(1), Z(1) * Z(2), Z(0) * Z(1) * Z(2) * Z(3)]
for provider in ["numpy", "pennylane", "qiskit-aer", "qulacs"]:
print(provider, Circuit(spec, observables=observables, provider=provider)([]))
numpy [0.46548538 0.11841725 1. ]
pennylane [0.46548538 0.11841725 1. ]
qiskit-aer [0.46548538 0.11841725 1. ]
qulacs [0.46548538 0.11841725 1. ]
numpy is the built-in simulator and needs nothing extra. The others come with pip install "eigenkit[pennylane,qiskit,qulacs]".
The current list of simulators: numpy, PennyLane, Qiskit, Qiskit Aer (statevector and MPS), Qulacs, qsim, TensorCircuit, CUDA-Q and Amazon Braket.
The same code on hardware
On a QPU the provider becomes a device config. The rest of the program stays the same:
from eigenkit import IQMConfig, QiskitRuntimeConfig
devices = {
"IBM Heron": QiskitRuntimeConfig(backend_name="ibm_kingston", shots=8192, primitive="sampler"),
"IQM Emerald": IQMConfig(quantum_computer="emerald", shots=8192),
}
for name, device in devices.items():
print(name, Circuit(spec, observables=observables, provider=device)([]))
Hardware access goes through IBM Qiskit Runtime, IQM Resonance and the QPUs on Amazon Braket. Photonic circuits run on Quandela's Perceval.
Real hardware costs money, so the library guards against accidental spending. Compiling for a paid device raises a CostWarning, and dry_run_info() previews a job before it is submitted. Gradients on hardware are opt-in (enable_gradients=True), since one backward pass runs at least two jobs per weight.
A circuit fits the classical stack
Circuits can also be assembled from ready-made blocks. With the default interface a circuit is a numpy function, and the gradient is one call away:
import numpy as np
from eigenkit import Circuit, Encoding, Variational, all_z
circuit = Circuit(
blocks=[Encoding(in_features=4), Variational(depth=2)],
observables=all_z(4),
)
weights = np.zeros(circuit.spec.n_weights)
x = np.random.uniform(0, np.pi, size=(8, 4)) # a batch of 8 samples
y = circuit(weights, x) # shape (8, 4)
grad = circuit.jacobian(weights, x[0]) # no torch needed
With interface="torch" the same circuit becomes a torch.nn.Module that owns its weights and trains inside a larger model with a standard optimizer. The ready-made models (hybrid networks, quantum kernels, reservoir computing) follow scikit-learn's fit / predict, so they work with Pipeline and GridSearchCV.
Checking the result against classical models
A quantum model is only interesting next to a good classical baseline. compare() trains quantum and classical models on the same data over several seeds and reports mean and spread:
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from eigenkit import QSVC
from eigenkit.benchmark import compare
result = compare(
{"QSVC": QSVC(), "SVC (RBF)": SVC(), "Random forest": RandomForestClassifier()},
dataset="iris",
n_seeds=5,
)
print(result.table())
On our run:
┌───────────────┬───────────────┬───────────────┬──────────────────┬───────────────────┐
│ Model │ Accuracy │ F1 │ Fit time (s) │ Predict time (s) │
├───────────────┼───────────────┼───────────────┼──────────────────┼───────────────────┤
│ QSVC │ 0.974 ± 0.039 │ 0.973 ± 0.040 │ 0.0022 ± 0.00039 │ 0.00075 ± 2e-05 │
│ SVC (RBF) │ 0.965 ± 0.036 │ 0.965 ± 0.037 │ 0.0012 ± 9.3e-05 │ 0.00023 ± 3.2e-05 │
│ Random forest │ 0.957 ± 0.053 │ 0.956 ± 0.054 │ 0.095 ± 0.0091 │ 0.0049 ± 0.00017 │
└───────────────┴───────────────┴───────────────┴──────────────────┴───────────────────┘
The quantum kernel has the highest mean accuracy. result.significance_test("accuracy") runs a Wilcoxon test across seeds, and every pairwise p-value comes out at 0.5 or higher. On iris the three models are statistically tied, and the classical SVC predicts about three times faster. That is the kind of answer compare() is there to give.
The qml and benchmark extras are needed for this example. eigenkit.datasets ships 12 datasets for classification, regression and time series.
What else is inside
- Optimization: QUBO / Ising, Max-Cut, QAOA.
- Chemistry: Jordan-Wigner and Bravyi-Kitaev mappings, UCCSD, VQE, molecular Hamiltonians through PySCF and OpenFermion.
- HHL, iterative amplitude estimation (IQAE) and portfolio risk (VaR / CVaR).
- Orthogonal networks of RBS gates for orthogonal fine-tuning and certified robustness.
Optimization and chemistry run on the core install. A four-node Max-Cut takes three lines:
from eigenkit.optimization import QAOA, max_cut
res = QAOA(layers=1, provider="numpy", random_state=0).solve(max_cut([(0, 1), (1, 2), (2, 3), (3, 0)]))
print(res.best_bitstring, res.best_energy) # (1, 0, 1, 0) -4.0
Links
- GitHub: github.com/eigenorbit/eigenkit
- Docs: docs.kit.eigenorbit.com
- Paper: arXiv:2611.02979
- PyPI: pypi.org/project/eigenkit
Which backend or dataset should we test next? Tell us in the comments.
Top comments (0)