Quantum Error Correction Library
Features • Installation • Quick start • Code distance • Automorphism groups
Qubitserf is a Python QEC library with a C++ backend, including GPU acceleration of certain algorithms. Its goal is to be fast, very fast.
- Code distance — CSS quantum codes, general (non-CSS) stabilizer codes, subsystem codes, and classical linear codes by multiple algorithms
- Operator weight — the minimum weight of a Pauli operator modulo the stabilizer or gauge group, i.e. the minimum-weight coset leader.
- Automorphism groups —
Aut(C)of a classical binary linear code or of a quantum CSS code with a full permutation-group toolkit (order, membership, enumeration, intersection). - Scales to real codes — connected-cluster certifies distances of qLDPC codes with up to hundreds of qubits and distances up to 20. Similarly, automorphism groups of sparse codes with hundreds of bits/qubits can be computed with the Brouwer-Zimmerman+nauty method.
Requirements:
- Python 3.9 or newer, and NumPy
- CMake 3.20 or newer, and a C++17 compiler
- Optional CUDA or Metal for the GPU backends, auto-detected by CMake
cd qubitserf # the unpacked package source directory
python -m pip install .Important
The automorphism engines additionally need system nauty 2.7+ (which provides dreadnaut) on
your PATH for the colored-graph solves. Install it with brew install nauty,
apt install nauty, or conda install -c conda-forge nauty. Distance finding needs no system
dependency.
To build the native library in-tree for local development instead:
./build.sh # then run with PYTHONPATH=pythonCertify the distance of IBM's [[144,12,12]] gross code and compute its automorphism group:
from qubitserf import distfind, codeaut
from qubitserf.distfind import codes
from qubitserf.codeaut import codes as acodes
Hx, Hz = codes.gross_code()
print(distfind.css_distance(Hx, Hz, method="cc")) # 12
a = codeaut.css_automorphisms(acodes.gross())
print(a.order()) # 144Every distfind entry point returns a plain int — -1 when the distance is not defined for
the input (an empty code, or a code with no logical qubits). The codeaut functions return the
full group or raise. Pass verbose=True to any of them to
stream progress to stderr. Standard code families ship with both modules (qubitserf.distfind.codes and qubitserf.codeaut.codes —
steane, shor, toric(L), surface(L), gross, bacon_shor(d), Reed–Muller, Hamming, …),
so every example below runs without external data.
import numpy as np
from qubitserf import distfind as df
Hx = np.loadtxt("Hx.txt", dtype=np.uint8) # any 0/1 check matrices
Hz = np.loadtxt("Hz.txt", dtype=np.uint8)
print(df.css_distance(Hx, Hz)) # method="bz" is the defaultchoose method depending on the codes: use "cc" (connected cluster) for sparse qLDPC codes — and "bz" (Brouwer-Zimmermann, the default, GPU-capable) for dense or random codes. "mitm" is an independent cross-check for
small codes.
from qubitserf.distfind import codes
Hx, Hz = codes.toric(10) # [[200, 2, 10]], sparse
print(df.css_distance(Hx, Hz, method="cc")) # 10, in milliseconds
print(df.css_distance(Hx, Hz, method="bz", backend="gpu")) # same answer the slow way;
# falls back to cpu without a GPU
d_z = df.css_distance(Hx, Hz, which="z", method="cc") # one side only
d_x = df.css_distance(Hx, Hz, which="x", method="cc")
print(d_z, d_x) # 10 10Useful keywords (shared by all distance functions): which="min"|"z"|"x",
backend="auto"|"cpu"|"gpu", threads=N (0 = all cores), verbose=True.
H = codes.hamming_parity(3) # parity-check matrix of the [7,4,3] Hamming code
print(df.classical_distance(H)) # 3The minimum weight of a Pauli modulo the stabilizer group — the Z-part is minimized over
z + rowspace(Gz) and the X-part over x + rowspace(Gx) independently, and the larger of the
two coset weights is returned:
Gx, Gz = codes.steane()
print(df.operator_weight(Gx, Gz, "ZZZZZZZ")) # 3 — a logical Z of Steane
print(df.operator_weight(Gx, Gz, "IIIZZZZ")) # 0 — a stabilizer is the identityThe operator may be a Pauli string (I/X/Y/Z, where Y sets both the Z and X bits), a
(z_vec, x_vec) pair, or a length-2n symplectic [z|x] array.
Pass the gauge generators; the stabilizer group (the gauge center) is computed internally and the dressed distance is returned:
Gx, Gz = codes.bacon_shor(3) # gauge generators, n = 9
print(df.subsystem_css_distance(Gx, Gz, method="cc")) # 3For any commuting set of Paulis, pass a symplectic stabilizer matrix of shape (m, 2n) in
[z | x] column order. The distance is the minimum symplectic weight (qubits touched) of a
normalizer element that is not a stabilizer:
import numpy as np
from qubitserf import distfind as df
def paulis(strings): # 'XZZXI' ... -> a [z | x] matrix
n = len(strings[0]); rows = []
for s in strings:
z = np.zeros(n, np.uint8); x = np.zeros(n, np.uint8)
for j, c in enumerate(s):
if c in "XY": x[j] = 1
if c in "ZY": z[j] = 1
rows.append(np.concatenate([z, x]))
return np.array(rows, np.uint8)
S = paulis(["XZZXI", "IXZZX", "XIXZZ", "ZXIXZ"]) # the [[5,1,3]] perfect code
print(df.stabilizer_distance(S)) # 3
print(df.pauli_operator_weight(S, S[0])) # 0 — a stabilizer
print(df.pauli_operator_weight(S, "XXXXX")) # 3 — a logical X, reducedsubsystem_stabilizer_distance(G) is the non-CSS dressed distance from possibly non-commuting
gauge generators. Pure-X/pure-Z input is detected and routed to the fast CSS solvers.
(method="cc" needs a CSS Tanner graph and raises on non-CSS input; bz and mitm both work.)
import numpy as np
from qubitserf import codeaut
# the [7,3,4] simplex code -> GL(3,2)
G = np.array([[0, 0, 0, 1, 1, 1, 1],
[0, 1, 1, 0, 0, 1, 1],
[1, 0, 1, 0, 1, 0, 1]], dtype=np.uint8)
print(codeaut.classical_automorphisms(G).order) # 168
print(codeaut.classical_automorphisms(G, method="bz").order) # 168 (BZ + nauty route)method="auto" (the default) runs Leon's algorithm when the effective dimension
min(dim, n − dim) is small and the Brouwer-Zimmermann + nauty incidence route (best for
LDPC-like codes, usable at any dimension) otherwise.
The qubit permutations preserving both Hx and Hz, returned directly as a permutation group (qubitserf.algebra.Group):
from qubitserf.codeaut import codes
grp = codeaut.css_automorphisms(codes.steane())
print(grp.order()) # 168
print(codeaut.css_automorphisms(codes.gross(), method="bz").order()) # 144It accepts a CSSCode, an (Hx, Hz) pair, two matrices, or any object exposing .Hx/.Hz.
The same method="auto"|"leon"|"bz" choice applies: leon for small effective dimension, bz
(joint Brouwer-Zimmermann + nauty/Traces incidence) for LDPC codes.
codeaut.css_automorphisms(codes.gross(), method="leon")
# RuntimeError: css automorphism group: no engine certified the exact Aut(Hx) ∩ Aut(Hz)
# (method='leon', n=144, eff_dim=66); tried: ... try method='bz' ...The returned Group (classical results expose the same via .group()) supports order,
membership, and enumeration:
grp = codeaut.css_automorphisms(codes.steane())
print(grp.order()) # 168 — a Python int, any size
print(grp.reduced_generators()) # a minimal generating set
print(grp.contains([0, 1, 2, 3, 4, 5, 6])) # True (the identity)
print(sum(1 for _ in grp)) # 168 — a Group is directly iterableA permutation group can be defined directly from its generators — each generator is a
0-indexed image list (perm[i] = image of point i), and the second argument is the
degree:
from qubitserf.algebra import permgroup as pg
# <(0 1 2), (0 1)> acting on 4 points -- S3 on the first three
B = pg.Group([[1, 2, 0, 3], [1, 0, 2, 3]], 4)
print(B.order(), B.contains([2, 0, 1, 3])) # 6 True
print(pg.symmetric_group(5).order()) # 120This is the same class the automorphism results produce, so hand-built groups compose with computed ones everywhere (membership, intersection, iteration).
Intersect two groups, accepting results, (generators, degree) pairs, or Group objects:
A = codeaut.classical_automorphisms(G)
print(codeaut.group_intersection(A, pg.symmetric_group(7)).order()) # 168@software{qubitserf,
title = {Qubitserf : Quantum Coding Theory Library},
author = {Serban Cercelescu},
year = {2026},
url = {https://github.com/Quantinuum/qubitserf}
}