Quantum Error and Noise Simulation SDK
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file qens-0.1.0.tar.gz.
File metadata
- Download URL: qens-0.1.0.tar.gz
- Upload date:
- Size: 638.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5cbc1bf2d96a645e0142fe4c33f7491ec99958176fa4d9218dd86978b37f10e9
|
|
| MD5 |
1ff76cf25ac42b9492592fc7d6f136fe
|
|
| BLAKE2b-256 |
918516e083b623c82bb2c88fd7c4c1e218e7dab069de20edd83c800ec0d44446
|
File details
Details for the file qens-0.1.0-py3-none-any.whl.
File metadata
- Download URL: qens-0.1.0-py3-none-any.whl
- Upload date:
- Size: 54.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
05785e266c55b29472fbb3b2660abafd52a5d169e73793cf53fac9251be39e90
|
|
| MD5 |
82d9613bc151307341d64072b239898b
|
|
| BLAKE2b-256 |
4c2c7412254f4366d986108687c365e66c50d661e8df4f6c4853c27f8d6bf1b9
|