Skip to content

Quantum Error Correction (QEC)

qudit provides an end-to-end QEC workflow using PyTorch Gate-based channels throughout:

  1. Define a code subspace (built-in or from stabilizers)
  2. Apply a noise channel to encoded states
  3. Construct a recovery map
  4. Measure how well recovery works

Main imports:

python
from qudit.qec import Recovery
from qudit.qec.lib import Dutta3, Leung, Perfect
from qudit.noise import Process

Code 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 dn-dimensional physical Hilbert space.

Property/MethodDescription
code.codewordsRaw torch.Tensor of shape (k, d^n)
code.dimNumber of logical codewords k
code.ditsNumber of physical qudits n
code[i]i-th codeword as a 1D tensor
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:

CodeDescription
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 |000,|111,|222
GottesmanD(d)CSS-type code: 1 logical qudit in d physical qudits (any prime d)
Surface(m, n, d=2, edge, start)m×n surface code for any local dimension d
python
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.0

From 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).

python
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)
python
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:

python
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 27

Noise channels (Process)

Process builds multi-qudit noise channels as Channel objects with correctable subsets pre-labeled. The channel applies Φ(ρ)=kEkρEk where each Ek is a sequence of local Gate operators embedded in the full Hilbert space.

python
from qudit.noise import Process

noise = Process.AD(d=2, n=4, Y=0.1, order=3)
ProcessDescription
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 d
Process.PhaseDamp(d, n, p)Phase damping (dephasing) for any d
Process.BitFlip(d, n, p)Cyclic shift Xd with probability p
Process.PhaseFlip(d, n, p)Clock operator Zd with probability p
Process.Reset(d, n, p)Collapse to $
Process.ThermalRelax(n, T1, T2, t)T1/T2 thermal relaxation (qubits only)

See Noise Channels for full channel documentation and IID variants.

Applying a channel to a density matrix:

python
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:

python
Recovery.petz(channel, codewords)   -> Channel
Recovery.leung(channel, codewords)  -> Channel
Recovery.cafaro(channel, codewords) -> Channel

where 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:

RPetz:Rk=PEk[E(P)]1/2

where P=i|i¯i¯| is the code projector.

python
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.9889
python
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)

Leung recovery

The Leung map uses a polar decomposition: for each k, factor EkP=UkΣkVk and set Rk=PUk:

python
rec = Recovery.leung(noise, [state0, state1])

Cafaro recovery

The Cafaro map normalizes codeword projections by the overlap i¯|EkEk|i¯:

python
rec = Recovery.cafaro(noise, [state0, state1])

End-to-end example

python
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))
python
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 a Code and unpack the rows.
  • Process.AD(order=3) labels correctable Kraus words up to 3-photon-loss order. Recovery.petz uses all Kraus words regardless of correctables.
  • For large codes the Petz pseudo-inverse can be slow. Try Recovery.cafaro as a faster approximation.
  • To validate a custom code before running QEC, use Code.isValid(code.toTensor()).