lightrider
Quantum circuits, IQM cloud jobs, attested entropy, quantum random numbers, and QRNG-driven synthetic data in one Python SDK.
lightrider provides three capabilities:
| Capability | Entry point | What it does |
|---|---|---|
| Circuit simulation & cloud jobs | Circuit, get_backend |
Build arbitrary circuits with a Qiskit-style API and run them on a fast local statevector simulator, a Stim-style stabilizer simulator, or IQM hardware in the cloud |
| Quantum random numbers | quantum_rng, IQM_sirius |
A numpy.random-style generator whose every draw comes from real IQM hardware bits — fully offline, with a selectable entropy backend |
| Synthetic data with provenance | Synthesizer |
Generate tabular synthetic data where every random draw is quantum, certified by a signed manifest |
Local simulators and the bundled QRNG pool run without network access. Cloud execution and live attested entropy use the same installed SDK and activate only when their clients are called.
The canonical Python namespace is lightrider. Attested entropy is grouped
under lightrider.entropy; quantum circuits and backends remain at the SDK
root:
from lightrider import Circuit, get_backend
from lightrider.entropy import EntropyClient, Policy
Installation
pip install lightrider # core (NumPy + httpx)
pip install "lightrider[pandas]" # + pandas DataFrame support
pip install "lightrider[pqc]" # + ML-DSA-65 (FIPS 204) receipt verification
Requires Python ≥ 3.9.
Live EMS entropy is part of the same SDK under lightrider.entropy; no
second Python package is required.
from lightrider.entropy import EntropyClient, Policy
Migrating from
lr_entropy? The legacylr_entropypackage is retired and has been removed from the repository.lightrider.entropyis a drop-in replacement for its client and receipt APIs — changefrom lr_entropy import ...tofrom lightrider.entropy import .... The oldQuantumClientis superseded byget_backend("iqm", ...)(see Running on IQM hardware).
Quickstart
from lightrider import Circuit, get_backend
# 1. Build a Bell-pair circuit
circ = Circuit(2)
circ.h(0)
circ.cx(0, 1)
circ.measure_all()
# 2. Run it on the local statevector simulator
job = get_backend("statevector").run(circ, shots=1000, seed=42)
# 3. Read the counts (Qiskit convention: clbit 0 is the rightmost character)
print(job.result().counts) # {'00': 507, '11': 493}
Measure before you run. Counts are samples of measured classical bits, so every circuit needs
measure_all()(or explicitmeasure()calls) beforerun()— otherwiserun()raisesBackendError: circuit has no measurements. In notebooks, build and run the circuit in the same cell:Circuitmethods mutate in place, so re-running only therun()cell reuses whatever state the circuit last had.
Quantum circuits
Building circuits
Circuit follows Qiskit's builder conventions — gate methods take parameters
first, then qubits, and calls chain:
from lightrider import Circuit
circ = Circuit(3) # 3 qubits, 3 classical bits
circ.h(0)
circ.rx(0.5, 1) # params first, qubits last
circ.ccx(0, 1, 2)
circ.measure_all()
The primitive gate set:
| Group | Gates |
|---|---|
| Single-qubit | id x y z h s sdg t tdg sx |
| Single-qubit, parameterized | rx ry rz p r u |
| Two-qubit | cx cy cz ch swap cp rxx ryy rzz |
| Three-qubit | ccx cswap |
Composite gates are defined as macros that expand to primitives at append time:
from lightrider import custom_gate
@custom_gate(num_qubits=2)
def bell_pair(c, qubits, params):
a, b = qubits
c.h(a)
c.cx(a, b)
circ = Circuit(3)
circ.append(bell_pair, [0, 1])
Choosing a backend
Every backend declares the gate set it supports, and run() validates the
circuit up front — a job that submits will also execute. Inspect all backends
programmatically with list_backends().
| Backend name | Aliases | Where | Gate set | Best for |
|---|---|---|---|---|
lightrider_statevector |
statevector, sv |
local | full | Exact simulation up to 24 qubits. Shots are sampled in one vectorized pass, so large shot counts are effectively free (1M shots of a 20-qubit circuit in ~1.4 s) |
lightrider_stabilizer |
stabilizer, stim |
local | Clifford subset (x y z h s sdg sx cx cy cz swap) |
Clifford circuits at hundreds of qubits; supports mid-circuit measurement |
iqm |
cloud |
cloud | full, transpiled server-side to IQM-native r (prx) + cz |
Real-hardware runs via the Light Rider IQM proxy |
Running locally
from lightrider import get_backend
result = get_backend("statevector").run(circ, shots=10_000, seed=7).result()
result.counts # {'000': 4980, '111': 5020}
result.probabilities() # {'000': 0.498, '111': 0.502}
The stabilizer backend trades gate-set generality for scale — a 100-qubit GHZ state samples at ~6 ms/shot:
n = 100
ghz = Circuit(n)
ghz.h(0)
for q in range(n - 1):
ghz.cx(q, q + 1)
ghz.measure_all()
counts = get_backend("stabilizer").run(ghz, shots=1000).result().counts
Submitting a non-Clifford gate to the stabilizer backend (or an unsupported
gate to any backend) raises UnsupportedGateError before anything runs.
Stabilizer noise and surface-code QEC
The local stabilizer backend includes the Stim-style operations needed for circuit-level QEC experiments:
| Kind | Light Rider circuit methods | Stim text |
|---|---|---|
| Pauli noise | x_error, y_error, z_error |
X_ERROR, Y_ERROR, Z_ERROR |
| Depolarizing noise | depolarize1, depolarize2 |
DEPOLARIZE1, DEPOLARIZE2 |
| General 1q Pauli channel | pauli_channel_1 |
PAULI_CHANNEL_1 |
| Basis measurement | measure, measure_x, measure_y |
M, MX, MY |
| Basis reset | reset, reset_x, reset_y |
R, RX, RY |
from lightrider import Circuit, get_backend
circuit = Circuit(1)
circuit.h(0)
circuit.depolarize1(1e-4, 0)
circuit.measure_x(0)
counts = get_backend("stim").run(
circuit, shots=100_000, seed=7
).result().counts
SurfaceCode9 implements the measurement-free, fault-tolerant
[[9,1,3]] encoder of Goto, Ho, and Kanao,
Phys. Rev. Research 5, 043137 (2023). It includes the exact
two-stage encoder, transversal logical Hadamard with virtual 90-degree
relabeling, X/Z syndrome decoding, and batched Pauli-frame Monte Carlo:
from lightrider import PauliNoiseModel, SurfaceCode9
code = SurfaceCode9()
result = code.simulate_logical_h(
PauliNoiseModel(one_qubit_error=1e-4, two_qubit_error=1e-4),
shots=1_000_000,
seed=7,
noisy_encoder=True,
)
print(result.as_dict())
The complete three-part reproduction is
examples/stabilizer_surface_code_demo.py:
PYTHONPATH=lightrider python3 \
lightrider/examples/stabilizer_surface_code_demo.py
The SDK implements these core stabilizer/QEC operations natively; it does not
yet claim wire-format compatibility with every advanced Stim annotation such
as DETECTOR, OBSERVABLE_INCLUDE, or detector error models.
Running on IQM hardware
Cloud jobs go through the Light Rider IQM proxy and authenticate with a Light
Rider lr_ API key — you never handle IQM credentials directly. The circuit
is transpiled to the QPU's native gates server-side.
Getting a key: lr_ API keys are issued internally by Light Rider —
request one from your administrator. There is intentionally no public
self-registration; IQMBackend.register() exists for administrators only and
requires the deployment's admin token.
iqm = get_backend("iqm_garnet",
endpoint="https://lightriderapp.vercel.app/api/quantum",
api_key="lr_...", # Garnet-scoped LR key
backend_id="iqm_garnet")
job = iqm.run(circ, shots=100) # low-cost Bell smoke test; returns immediately
job.status() # WAITING | PROCESSING | COMPLETED | FAILED | ABORTED
result = job.result() # counts + receipt in result.metadata["receipt"]
job.receipt() # provider credits + Light Rider token charge
Mock deployments. If the proxy is backed by one of IQM's
:mockQPU endpoints,run()emits aMockBackendWarning: mock QPUs execute the full job lifecycle but return canned mock entropy (all measured bits set to one coin flip) instead of running your circuit. Use the local simulators when the counts need to be physically meaningful.
Serialization
Circuits serialize to the lr-circuit/v1 JSON payload shared with the Light
Rider proxy and the rest of the SDK, and to a Stim-flavored text format:
payload = circ.to_payload() # dict, JSON-safe
circ2 = Circuit.from_payload(payload)
print(circ.to_text()) # H 0 / CX 0 1 / M 0 -> 0 ...
circ3 = Circuit.from_text(circ.to_text())
Quantum random numbers
numpy-style: quantum_rng()
quantum_rng() is the quantum counterpart of numpy.random.default_rng() —
the same calling conventions, but every draw comes from a quantum entropy
source, with no PRNG in the sampling path:
from lightrider import quantum_rng
rng = quantum_rng() # default source: "iqm_sirius"
rng.random(5) # uniform floats in [0, 1)
rng.integers(1, 6, size=10, endpoint=True) # quantum dice
rng.normal(loc=0.0, scale=1.0, size=100) # Box–Muller on quantum uniforms
rng.choice(["a", "b", "c"], 5, p=[0.5, 0.3, 0.2])
rng.shuffle(my_list) # quantum Fisher–Yates
rng.bytes(32) # raw quantum entropy
The entropy backend is selectable. "iqm_sirius" (default) is the bundled
IQM hardware pool; any object with a uniform(shape) method also works —
pass an EntropySource for live, signed EMS entropy, or a BundledQrng to
record every draw on a provenance manifest:
from lightrider import BundledQrng, quantum_rng
provider = BundledQrng(dataset_id="my_experiment")
rng = quantum_rng(provider) # draws are logged on provider.manifest
numpy interop: when you need numpy's full distribution zoo or bulk PRNG
throughput, rng.numpy_generator() returns a genuine
numpy.random.Generator seeded from quantum bytes — quantum-seeded rather
than quantum-drawn, and the honest label matters:
g = rng.numpy_generator() # a real np.random.Generator
g.binomial(10, 0.5, size=100_000) # anything numpy can do
Two deliberate design points: there is no seed parameter (the stream is
physical entropy, not a reproducible algorithm — for reproducibility, seed a
numpy_generator() and store the seed), and the bundled pool cycles after
~1.9M bits, so it is statistically quantum but not suitable for
cryptographic key material.
Classic: IQM_sirius
IQM_sirius draws from the same bundled pool (~2 million bits captured from
IQM hardware: Hadamard coin-flip circuits across 10 qubits, SHA-256
debiased) — no network required. Output is unbiased on any range via
rejection sampling.
from lightrider import IQM_sirius
IQM_sirius(5, 1, 100) # 5 quantum random ints in [1, 100]
IQM_sirius(3, 0.0, 1.0, step=0.1) # 3 quantum random floats on a 0.1 grid
Capture metadata for the bundled pool lives in the repository under
iqm_capture_20260507_181448/metadata.json.
Synthetic data with provenance
Synthesizer fits a Gaussian copula to tabular data and generates new rows
whose every random draw comes from a quantum source. Each dataset ships with
a provenance manifest binding it to the entropy that produced it.
from lightrider import Synthesizer
synth = Synthesizer(dataset_id="customers_v3").fit(df) # DataFrame / dict / records
rows = synth.generate(10_000)
synth.manifest.write("customers_v3.provenance.json")
print(synth.certificate())
How it works
fit: data ─▶ marginals (empirical CDF / category freqs)
─▶ normal scores z = Φ⁻¹(rank)
─▶ correlation Σ = corr(z), Cholesky Σ = L Lᵀ
gen: QRNG ─▶ U(0,1) (quantum draws, recorded on the manifest)
─▶ Z₀ = Φ⁻¹(U) (iid standard normals)
─▶ Z = Z₀ Lᵀ (impose learned correlation)
─▶ U' = Φ(Z) (back to uniform, per column)
─▶ x = F⁻¹(U') (inverse marginal → synthetic value)
The copula reproduces each column's marginal distribution and inter-column
correlations; the randomness selecting each synthetic row is quantum, not a
PRNG. The full mathematical treatment is in the repository under
docs/qrng-synthetic-data.pdf.
Entropy modes
| Mode | Provider | Provenance |
|---|---|---|
| bundled-qrng (default) | BundledQrng over the packaged IQM pool |
Real quantum bits, SHA-256 debiased, offline, unsigned |
| live-attested | EntropySource against a Light Rider EMS |
Multi-source extraction over GF(2¹²⁸), SP 800-90B health-tested, post-quantum-signed receipts |
from lightrider import EntropySource, Synthesizer
src = EntropySource("http://localhost:7081", dataset_id="customers_v3")
synth = Synthesizer(entropy=src).fit(df)
rows = synth.generate(10_000) # every draw carries a signed receipt
EntropySource(allow_failover=True) (the default) falls back to the OS
CSPRNG on any EMS error so a long job never blocks. Failover draws are
flagged in the manifest and excluded from the certificate's source list — the
certificate never overstates its provenance.
The manifest
{
"dataset_id": "customers_v3", "model": "qrng-copula",
"rows": 10000, "columns": ["age", "income", "tier", "region"],
"entropy_mode": "live-attested", "fully_attested": true,
"signature_alg": "ML-DSA-65", "post_quantum_signed": true,
"sources_used": ["curby_q_jila_001", "qispace_kds_001"],
"min_quality_score": 90, "health_all_pass": true,
"extractors": ["SHAKE256"], "failover_used": false
}
In the offline default the same manifest reports
entropy_mode: "bundled-qrng" and post_quantum_signed: false — honest by
construction.
Demo and development
# generate a synthetic dataset with its provenance certificate
# (installed as the `lightrider-demo` console script)
python -m lightrider.demo --rows 2000 --out synthetic.csv --manifest cert.json
# against a live EMS
python -m lightrider.demo --endpoint http://localhost:7081 --rows 2000
# run the test suite
pytest tests -q
Repository layout
| Path | Contents |
|---|---|
lightrider/ |
The package itself (data/ holds the bundled QRNG pool shipped in the wheel) |
examples/ |
Runnable examples: Bell circuit on Garnet, Colab cloud submission (script + notebook), surface-code QEC demo |
tests/ |
Offline test suite (pytest tests -q); live-EMS smoke tests gate on LR_EMS_LIVE=1 |
docs/ |
The QRNG-copula mathematical treatment (qrng-synthetic-data.pdf + LaTeX source) |
fibonacci_gates/ |
Fibonacci-gate research notebooks and captured results across IQM Garnet, Emerald, Sirius, and Cepheus |
iqm_capture_20260507_181448/ |
Capture record for the bundled pool: metadata, experiment log, and raw measurements |
License
Apache-2.0. Built by Light Rider.
Release files for lightrider 1.4.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| lightrider-1.4.2.tar.gz | 600.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lightrider-1.4.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.3 MB
Release files / lightrider-1.4.2.tar.gz
| Download URL | lightrider-1.4.2.tar.gz |
|---|---|
| Size | 600.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
24117f5303554ba0a8af4a1f5b752ad7bb5ba1aca829d42206ba407dacc434b9
|
|
BLAKE2b-256 checksum How to use checksums |
1234bef140177e9904f9313098977cf88d3623077a8770847c387768189ebcf1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / lightrider-1.4.2-py3-none-any.whl
| Download URL | lightrider-1.4.2-py3-none-any.whl |
|---|---|
| Size | 723.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
80112ebc01754e4b86f23d0f321f8606bc3f240fc9fd846952e6f75dc3957d1e
|
|
BLAKE2b-256 checksum How to use checksums |
c5e3f213696ff07f5b4345e6abbb0a837e86cbe3144a04c61a4a2dffc13df8a4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|