Quantum Error Correction (QEC)
qudit provides an end-to-end QEC workflow using PyTorch Gate-based channels throughout:
- Define a code subspace (built-in or from stabilizers)
- Apply a noise channel to encoded states
- Construct a recovery map
- Measure how well recovery works
Main imports:
from qudit.qec import Recovery
from qudit.qec.lib import Dutta3, Leung, Perfect
from qudit.noise import ProcessCode representation (Code)
A Code wraps a torch.Tensor of shape (k, d^n) where each row is one logical codeword as a statevector in the
| Property/Method | Description |
|---|---|
code.codewords | Raw torch.Tensor of shape (k, d^n) |
code.dim | Number of logical codewords |
code.dits | Number of physical qudits |
code[i] | |
code.toTensor() | Returns all codewords as a tensor (indexable) |
Code.isValid(codewords) | Assert normalization and mutual orthogonality |
Code.fromStabilizers(stabs, d=2) | Construct code from stabilizer strings for local dimension d |
Built-in codes
Standard codes are provided in qudit.qec.lib:
| Code | Description |
|---|---|
Dutta3() | 3-qubit permutation-invariant code. Smallest single-AD-error-correcting code |
Leung() | 4-qubit code. Standard single-AD-error correction |
Perfect() | [[5,1,3]] 5-qubit perfect code. corrects any single-qubit error |
Qutrit3() | 3-qutrit repetition code. codewords |
GottesmanD(d) | CSS-type code: 1 logical qudit in |
Surface(m, n, d=2, edge, start) | m×n surface code for any local dimension d |
from qudit.qec.lib import Leung, Dutta3, Perfect
code = Leung()
state0, state1 = code.toTensor() # two codeword tensors
print(state0.shape) # torch.Size([16]) (2^4 = 16)
print(state1.norm()) # 1.0From stabilizers
Code.fromStabilizers constructs the codespace projector from a list of Pauli stabilizer generators and extracts codewords via SVD or randomized range finding. The optional d parameter sets the local qudit dimension (default 2). for d>2, X and Z are the clock and shift operators from Gategen(d).
from qudit.qec.codes import Code
code = Code.fromStabilizers(["ZZZII", "IIZZZ", "XIXXI", "IXXIX"])
print(len(code)) # 2 (= 2^(5-4))
print(code.codewords.shape) # (2, 32)from qudit.qec.codes import Code
code = Code.fromStabilizers(["ZI", "IZ"], d=3)
print(len(code)) # 1 (= 3^(2-2))
print(code.codewords.shape) # (1, 9)The Surface factory wraps fromStabilizers for lattice surface codes:
from qudit.qec.lib import Surface
qubit_code = Surface(3, 1, d=2) # 1×3 qubit surface code: 2 codewords, dim 8
qutrit_code = Surface(3, 1, d=3) # 1×3 qutrit surface code: 3 codewords, dim 27Noise channels (Process)
Process builds multi-qudit noise channels as Channel objects with correctable subsets pre-labeled. The channel applies Gate operators embedded in the full Hilbert space.
from qudit.noise import Process
noise = Process.AD(d=2, n=4, Y=0.1, order=3)| Process | Description |
|---|---|
Process.AD(d, n, Y, order) | Amplitude damping. Only lowering operators |
Process.GAD(d, n, Y, p, order) | Generalized AD. Lowering + raising operators |
Process.Pauli(n, paulis, p, order) | Pauli channel over {I,X,Y,Z} words |
Process.Depolarising(d, n, p) | Weyl-Heisenberg depolarising for any |
Process.PhaseDamp(d, n, p) | Phase damping (dephasing) for any |
Process.BitFlip(d, n, p) | Cyclic shift |
Process.PhaseFlip(d, n, p) | Clock operator |
Process.Reset(d, n, p) | Collapse to $ |
Process.ThermalRelax(n, T1, T2, t) |
See Noise Channels for full channel documentation and IID variants.
Applying a channel to a density matrix:
import torch as pt
def to_rho(psi):
N = psi.numel()
return (psi.view(N, 1) @ psi.view(1, N).conj()).to(pt.complex64)
state0, state1 = Leung().toTensor()
rho0 = to_rho(state0)
noisy0 = noise.run(rho0)Recovery maps (Recovery)
Recovery constructs a recovery Channel from a noise channel and a list of codeword tensors. All three constructors have the same signature:
Recovery.petz(channel, codewords) -> Channel
Recovery.leung(channel, codewords) -> Channel
Recovery.cafaro(channel, codewords) -> Channelwhere codewords is List[pt.Tensor] (each codeword as a 1D tensor).
Petz recovery
The Petz recovery map minimizes a distinguishability measure and is given by:
where
state0, state1 = Leung().toTensor()
rho0 = to_rho(state0)
noise = Process.AD(d=2, n=4, Y=0.1, order=3)
noisy0 = noise.run(rho0)
rec = Recovery.petz(noise, [state0, state1])
clean0 = rec.run(noisy0)
fid = pt.real(pt.trace(rho0 @ clean0)).item()
print(f"Fidelity after recovery: {fid:.4f}") # ~0.9889from qudit.qec import Recovery
from qudit.qec.lib import Leung
from qudit.noise import Process
import torch as pt
def to_rho(psi):
N = psi.numel()
return (psi.view(N, 1) @ psi.view(1, N).conj()).to(pt.complex64)Leung recovery
The Leung map uses a polar decomposition: for each
rec = Recovery.leung(noise, [state0, state1])Cafaro recovery
The Cafaro map normalizes codeword projections by the overlap
rec = Recovery.cafaro(noise, [state0, state1])End-to-end example
state0, state1 = Leung().toTensor()
rho0, rho1 = to_rho(state0), to_rho(state1)
noise = Process.AD(d=2, n=4, Y=0.1, order=3)
noisy0 = noise.run(rho0)
noisy1 = noise.run(rho1)
rec = Recovery.petz(noise, [state0, state1])
clean0 = rec.run(noisy0)
clean1 = rec.run(noisy1)
fid = lambda r, s: pt.real(pt.trace(r @ s)).item()
print("Noisy fidelity |0L>:", fid(rho0, noisy0))
print("Recovered fidelity |0L>:", fid(rho0, clean0))
print("Noisy fidelity |1L>:", fid(rho1, noisy1))
print("Recovered fidelity |1L>:", fid(rho1, clean1))from qudit.qec import Recovery
from qudit.qec.lib import Leung
from qudit.noise import Process
import torch as pt
def to_rho(psi):
N = psi.numel()
return (psi.view(N, 1) @ psi.view(1, N).conj()).to(pt.complex64)NOTE
Recovery returns a Channel object. Apply it via .run(rho) exactly as you would any noise channel.
Practical notes
- Codeword tensors must be
torch.Tensor(1D, on the same device). Call.toTensor()on aCodeand unpack the rows. Process.AD(order=3)labels correctable Kraus words up to 3-photon-loss order.Recovery.petzuses all Kraus words regardless ofcorrectables.- For large codes the Petz pseudo-inverse can be slow. Try
Recovery.cafaroas a faster approximation. - To validate a custom code before running QEC, use
Code.isValid(code.toTensor()).