QENS
Quantum Error and Noise Simulation SDK -- a Python-native toolkit for simulating quantum errors, decoding syndromes, and visualizing error-correcting codes.
QENS provides a layered API for researchers, educators, and engineers working with quantum error correction. It ships with built-in support for surface codes, repetition codes, and color codes, multiple decoder implementations, and publication-quality visualization -- all with only numpy and matplotlib as dependencies.
Installation
pip install -e .
For development (includes pytest, mypy, ruff):
pip install -e ".[dev]"
Requires Python 3.11+.
Documentation
Full documentation is in the docs/ directory:
| Guide | Description |
|---|---|
| Getting Started | Installation, first simulation, project structure |
| Core Concepts | QEC background, Pauli errors, CSS codes, Pauli frame model |
| Error Models | All 10 noise models with examples |
| QEC Codes | Repetition, surface, and color codes |
| Decoders | Lookup, MWPM, and union-find decoders |
| Simulation | Monte Carlo sampling, threshold sweeps, Pauli frame simulator |
| Visualization | Circuit diagrams, lattice views, decoding graphs, plots |
| Extending QENS | Custom error models, codes, decoders, visualizers |
| API Reference | Complete reference for every class and function |
| Architecture | Package design, dependency graph, simulation pipeline |
Quickstart
import qens
code = qens.RepetitionCode(distance=5)
noise = qens.DepolarizingError(p=0.05)
decoder = qens.LookupTableDecoder(code)
result = qens.ThresholdExperiment.single_point(
code=code, noise_model=noise, decoder=decoder, shots=10_000, seed=42
)
print(f"Logical error rate: {result.logical_error_rate:.4f}")
Detailed Usage Guide
1. Building Quantum Circuits
QENS provides a fluent builder API for constructing quantum circuits gate by gate:
from qens import Circuit, Gate, Moment
# Fluent API -- chain gates together
circuit = Circuit(num_qubits=3).h(0).cx(0, 1).cx(0, 2).measure_all()
print(circuit) # Circuit(num_qubits=3, depth=4)
# Or build manually with Gate and Moment objects
gate = Gate("H", qubits=(0,))
moment = Moment()
moment.add(gate)
circuit = Circuit(5)
circuit.append_moment(moment)
Available fluent gates: h(q), x(q), z(q), cx(ctrl, tgt), cz(q0, q1), measure(q), measure_all(), reset(q).
Circuit properties: num_qubits, depth, moments.
2. Error Models
Every error model subclasses ErrorModel and implements sample_errors(). All models support Pauli-frame sampling and can be composed.
Pauli Errors
from qens import BitFlipError, PhaseFlipError, DepolarizingError, PauliYError
import numpy as np
noise = DepolarizingError(p=0.01) # X, Y, Z each with prob p/3
rng = np.random.default_rng(42)
error = noise.sample_errors(num_qubits=5, affected_qubits=[0, 1, 2, 3, 4], rng=rng)
# Returns a PauliString: array of 0=I, 1=X, 2=Y, 3=Z
Measurement Errors
from qens import MeasurementError
# Asymmetric readout: different 0->1 and 1->0 flip rates
readout = MeasurementError(p_0to1=0.02, p_1to0=0.005)
# Only applies to measurement gates
readout.applies_to(Gate("M", (0,))) # True
readout.applies_to(Gate("H", (0,))) # False
Gate Errors
from qens import CoherentRotationError, CrosstalkError
# Gaussian over/under-rotation on every non-measurement gate
rotation = CoherentRotationError(angle_stddev=0.01)
# ZZ crosstalk between specific qubit pairs
crosstalk = CrosstalkError(coupling_map={(0, 1): 0.002, (1, 2): 0.001})
Correlated and Leakage Errors
from qens import CorrelatedPauliError, LeakageError
from qens.core.types import PauliOp
# Joint Pauli errors on qubit pairs
correlated = CorrelatedPauliError(joint_errors={
(0, 1): [(0.01, PauliOp.X, PauliOp.X)], # XX with prob 0.01
(2, 3): [(0.005, PauliOp.Z, PauliOp.Z)], # ZZ with prob 0.005
})
# Leakage to |2> state with relaxation back
leakage = LeakageError(p_leak=0.001, p_relax=0.1)
print(leakage.leaked_qubits) # Track which qubits are leaked
leakage.reset() # Clear leakage state
Composing Noise Models
Stack multiple error models into a single composed model. Each model's applies_to() filter is respected.
from qens import ComposedNoiseModel, DepolarizingError, MeasurementError, CrosstalkError
noise = ComposedNoiseModel([
DepolarizingError(p=0.001),
MeasurementError(p_0to1=0.01, p_1to0=0.005),
CrosstalkError(coupling_map={(0, 1): 0.002}),
])
# Sample errors for a specific gate (respects filters)
from qens import Gate
error = noise.sample_errors_for_gate(
num_qubits=5, gate=Gate("CX", (0, 1)), rng=rng
)
Kraus Representations
Pauli error models provide Kraus operator representations for density-matrix simulation:
channel = DepolarizingError(p=0.01).to_channel(affected_qubits=[0])
print(channel.validate()) # True (completeness relation holds)
print(channel.probabilities()) # [0.99, 0.0033, 0.0033, 0.0033]
3. Error-Correcting Codes
All codes subclass QECCode and provide stabilizers, logical operators, check matrices, and syndrome extraction circuits.
Repetition Code
from qens import RepetitionCode
code = RepetitionCode(distance=5)
print(code.num_data_qubits) # 5
print(code.num_ancilla_qubits) # 4
print(code.code_distance) # 5
# Stabilizer generators (ZZ on adjacent pairs)
for stab in code.stabilizer_generators():
print(f" {stab.stabilizer_type}: qubits {stab.qubits}")
# Parity check matrix
H = code.check_matrix() # shape (4, 5)
# Syndrome extraction circuit
circuit = code.syndrome_circuit(rounds=3)
Surface Code
from qens import SurfaceCode
import numpy as np
code = SurfaceCode(distance=3) # Rotated surface code
print(code.num_data_qubits) # 9
print(code.num_ancilla_qubits) # 8
# Inject an X error and compute the syndrome
error = np.zeros(9, dtype=np.uint8)
error[4] = 1 # X on center qubit
syndrome = code.compute_syndrome(error)
print(f"Syndrome: {syndrome}") # Non-trivial where Z-stabilizers detect X
# Check if an error is a logical error
residual = np.zeros(9, dtype=np.uint8)
residual[0] = 1; residual[3] = 1; residual[6] = 1 # X on left column = X_L
print(code.is_logical_error(residual)) # True
Color Code
from qens import ColorCode
# 4.8.8 lattice (default)
code_488 = ColorCode(distance=3, lattice_type="4.8.8")
print(code_488.supports_transversal_clifford) # True
# 6.6.6 lattice
code_666 = ColorCode(distance=5, lattice_type="6.6.6")
Qubit Coordinates
Every code provides coordinates for visualization:
coords = code.qubit_coordinates()
# {0: (0, 0), 1: (0, 1), ...} -- maps qubit index to (row, col)
4. Decoders
All decoders subclass Decoder and implement decode(syndrome) -> DecoderResult.
Lookup Table Decoder
Exact decoding via precomputed syndrome-to-correction table. Best for small codes (d <= 7).
from qens import RepetitionCode, LookupTableDecoder
import numpy as np
code = RepetitionCode(5)
decoder = LookupTableDecoder(code)
decoder.precompute() # Build lookup table
# Decode a syndrome
error = np.zeros(5, dtype=np.uint8)
error[2] = 1 # X error on qubit 2
syndrome = code.compute_syndrome(error)
result = decoder.decode(syndrome)
print(result.correction) # Correction to apply
print(result.metadata) # {"table_hit": True}
MWPM Decoder
Greedy minimum-weight perfect matching. Good balance of speed and accuracy.
from qens import SurfaceCode, MWPMDecoder
code = SurfaceCode(5)
decoder = MWPMDecoder(code)
decoder.precompute() # Build decoding graph
result = decoder.decode(syndrome)
print(result.metadata["matching"]) # List of matched defect pairs
# Access the decoding graph structure (for visualization)
graph = decoder.build_decoding_graph()
print(graph.keys()) # ['nodes', 'edges', 'boundary_nodes']
Union-Find Decoder
Fast approximate decoding with almost-linear time complexity.
from qens import UnionFindDecoder
decoder = UnionFindDecoder(code)
decoder.precompute()
result = decoder.decode(syndrome)
5. Running Simulations
Single-Point Simulation
Run a fixed number of Monte Carlo shots at one noise level:
from qens import (
RepetitionCode, DepolarizingError, MWPMDecoder,
ThresholdExperiment, NoisySampler,
)
code = RepetitionCode(5)
noise = DepolarizingError(p=0.05)
decoder = MWPMDecoder(code)
# Option A: via ThresholdExperiment convenience method
result = ThresholdExperiment.single_point(
code=code, noise_model=noise, decoder=decoder,
shots=10_000, seed=42
)
print(f"Logical error rate: {result.logical_error_rate:.4f}")
print(f"Logical errors: {result.logical_error_count} / {result.num_shots}")
# Option B: via NoisySampler directly
sampler = NoisySampler(seed=42)
result = sampler.run(code, noise, decoder, shots=10_000)
# Access individual shots
syn_0 = result.sample_syndrome(0)
err_0 = result.sample_error(0)
Sampling Without Decoding
Generate error samples and syndromes without the decoding step:
sampler = NoisySampler(seed=42)
result = sampler.sample_errors(code, noise, shots=1_000)
# result.syndromes and result.errors are populated
# result.corrections and result.logical_errors are empty
Threshold Sweep
Sweep physical error rates across multiple code distances to find the error threshold:
from qens import SurfaceCode, DepolarizingError, MWPMDecoder, ThresholdExperiment
experiment = ThresholdExperiment(
code_class=SurfaceCode,
distances=[3, 5, 7],
physical_error_rates=[0.001, 0.003, 0.005, 0.008, 0.01, 0.015, 0.02],
noise_model_factory=lambda p: DepolarizingError(p=p),
decoder_class=MWPMDecoder,
shots_per_point=10_000,
seed=42,
)
# Run with progress tracking
def on_progress(completed, total):
print(f" [{completed}/{total}] {100*completed/total:.0f}%")
result = experiment.run(progress_callback=on_progress)
# Access results
print(result.distances) # [3, 5, 7]
print(result.physical_error_rates) # [0.001, ..., 0.02]
print(result.logical_error_rates) # shape (3, 7) numpy array
Pauli Frame Simulator
Track Pauli errors through Clifford circuits efficiently (O(n) per gate):
from qens.simulation import PauliFrameSimulator
from qens import Circuit, PauliOp
import numpy as np
sim = PauliFrameSimulator(num_qubits=3)
# Inject an X error on qubit 0
error = np.array([PauliOp.X, PauliOp.I, PauliOp.I], dtype=np.uint8)
sim.apply_error(error)
# Propagate through a circuit
circuit = Circuit(3).h(0).cx(0, 1)
sim.propagate_circuit(circuit)
print(sim.frame) # See how the error propagated
print(sim.measure(0)) # 1 if X/Y error on qubit, else 0
6. Visualization
All visualization functions return a FigureHandle with .save(path), .show(), and .close() methods. Use matplotlib.use('Agg') for headless environments.
Circuit Diagrams
from qens import SurfaceCode, DepolarizingError, draw_circuit
code = SurfaceCode(3)
circuit = code.syndrome_circuit(rounds=1)
noise = DepolarizingError(p=0.01)
# Basic circuit diagram
fig = draw_circuit(circuit)
fig.save("circuit.png")
# With error annotations highlighting noise-prone gates
fig = draw_circuit(circuit, noise_model=noise, highlight_errors=True)
fig.save("noisy_circuit.png")
# With explicit error locations
fig = draw_circuit(circuit, error_locations=[(0, 2), (1, 5)])
fig.save("marked_circuit.png")
Lattice Views
from qens import RepetitionCode, SurfaceCode, draw_lattice
import numpy as np
code = SurfaceCode(3)
# Basic lattice
fig = draw_lattice(code)
# With syndrome overlay
error = np.zeros(9, dtype=np.uint8)
error[4] = 1 # X error
syndrome = code.compute_syndrome(error)
fig = draw_lattice(code, syndrome=syndrome, error=error, title="X error on center")
fig.save("lattice_with_error.png")
Decoding Graphs
from qens import SurfaceCode, MWPMDecoder, draw_decoding_graph
import numpy as np
code = SurfaceCode(3)
decoder = MWPMDecoder(code)
decoder.precompute()
error = np.zeros(9, dtype=np.uint8)
error[4] = 1
syndrome = code.compute_syndrome(error)
decode_result = decoder.decode(syndrome)
fig = draw_decoding_graph(
decoder, syndrome=syndrome, decode_result=decode_result,
show_matching=True, title="MWPM Decoding"
)
fig.save("decoding_graph.png")
Statistical Plots
from qens import plot_threshold, plot_logical_rates, plot_histogram
# Threshold plot (from a ThresholdExperiment result)
fig = plot_threshold(result, log_scale=True, title="Surface Code Threshold")
fig.save("threshold.pdf") # Publication-quality PDF
# Bar chart of logical error rates
fig = plot_logical_rates(
distances=[3, 5, 7, 9],
logical_rates=[0.05, 0.01, 0.002, 0.0004],
)
fig.save("rates_by_distance.png")
# Histogram of any simulation data
fig = plot_histogram(data, bins=50, xlabel="Error weight", title="Error Distribution")
fig.save("histogram.png")
7. Extending QENS
Every subsystem uses the ABC + Registry pattern. Extension points are marked with comments in the source.
Custom Error Model
from qens.noise.base import ErrorModel
from qens.noise import noise_registry
import numpy as np
class ThermalRelaxationError(ErrorModel):
def __init__(self, t1: float, t2: float, gate_time: float):
self.p_x = 1 - np.exp(-gate_time / t1)
self.p_z = 1 - np.exp(-gate_time / t2)
def sample_errors(self, num_qubits, affected_qubits, rng):
error = np.zeros(num_qubits, dtype=np.uint8)
for q in affected_qubits:
if rng.random() < self.p_x:
error[q] = 1 # X
elif rng.random() < self.p_z:
error[q] = 3 # Z
return error
def __repr__(self):
return f"ThermalRelaxationError(p_x={self.p_x:.4f}, p_z={self.p_z:.4f})"
noise_registry.register("thermal", ThermalRelaxationError)
Custom Decoder
from qens.decoders.base import Decoder, DecoderResult
from qens.decoders import decoder_registry
class MyDecoder(Decoder):
def decode(self, syndrome):
correction = np.zeros(self._code.num_data_qubits, dtype=np.uint8)
# ... your decoding logic ...
return DecoderResult(correction=correction, success=True)
decoder_registry.register("my_decoder", MyDecoder)
Registry Lookup
from qens.noise import noise_registry
from qens.codes import code_registry
from qens.decoders import decoder_registry
print(noise_registry.list_registered())
# ['bit_flip', 'coherent_rotation', 'correlated_pauli', 'crosstalk',
# 'depolarizing', 'leakage', 'measurement', 'pauli_y', 'phase_flip']
cls = noise_registry.get("depolarizing")
model = cls(p=0.01)
Architecture
qens/
core/ Types, Circuit, Gate, NoiseChannel, Registry
noise/ ErrorModel ABC + 8 built-in models + ComposedNoiseModel
codes/ QECCode ABC + RepetitionCode, SurfaceCode, ColorCode
decoders/ Decoder ABC + Lookup, UnionFind, MWPM
simulation/ NoisySampler, PauliFrameSimulator, ThresholdExperiment
viz/ Circuit diagrams, lattice views, decoding graphs, stats plots
utils/ Pauli algebra, GF(2) sparse matrices, seeded RNG
Examples
python3 examples/01_quickstart.py # Basic workflow
python3 examples/02_surface_code_threshold.py # Threshold sweep
python3 examples/03_custom_noise_model.py # Composed noise + visualization
python3 examples/04_visualization_gallery.py # All visualization types
Testing
pytest # 194 tests
ruff check src/qens/ # Lint
License
MIT
Metadata
Release files for qens 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| qens-0.1.0.tar.gz | 638.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| qens-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 692.7 kB
Release files / qens-0.1.0.tar.gz
| Download URL | qens-0.1.0.tar.gz |
|---|---|
| Size | 638.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5cbc1bf2d96a645e0142fe4c33f7491ec99958176fa4d9218dd86978b37f10e9
|
|
BLAKE2b-256 checksum How to use checksums |
918516e083b623c82bb2c88fd7c4c1e218e7dab069de20edd83c800ec0d44446
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|
Release files / qens-0.1.0-py3-none-any.whl
| Download URL | qens-0.1.0-py3-none-any.whl |
|---|---|
| Size | 54.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
05785e266c55b29472fbb3b2660abafd52a5d169e73793cf53fac9251be39e90
|
|
BLAKE2b-256 checksum How to use checksums |
4c2c7412254f4366d986108687c365e66c50d661e8df4f6c4853c27f8d6bf1b9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|