dist-m4ri is a high-performance multithreaded C program and Python library for computing and bracketing the minimum
distance of binary classical linear codes, quantum CSS codes, and Stim Detector Error Models (DEMs).
The program implements three main methods (with method=3 as the default):
-
Method 1 (
method=1) - Random Window (RW) Algorithm: Multithreaded random information set search to find low-weight non-trivial codewords and establish an upper distance bound$d_{\max}$ . -
Method 2 (
method=2) - Connected Cluster (CC) Algorithm: Multithreaded exhaustive depth-first cluster enumeration to compute exact distance or establish a certified lower distance bound$d_{\min}$ . -
Method 3 (
method=3, default) - Bracketing Mode (Artillery Fork / Вилка): Concurrently runs CC and RW on multiple threads, dynamically balancing CPU cores between CC and RW based on current bounds$[d_{\min}, d_{\max}]$ , distance estimate (dexp/dest), remaining RW steps and their measured time, timeout, and the measured scaling characteristics of CC.
For a classical binary linear code, only the parity-check matrix
For a quantum CSS code, matrix finG) or logical operators finL) are needed.
Alternatively, a detector error model file from stim can be specified using fdem=[str]. The repeat blocks and
shift_detectors instructions of a DEM are unrolled, the detector coordinates are ignored, and the components of a
decomposed error (separated by ^) are summed mod 2: a detector or observable listed twice in one error line cancels.
Equal columns of the DEM (error mechanisms with the same detectors and observables, common in DEMs generated by stim
for circuits with REPEAT blocks: 5-16% of the columns in our examples) are merged (merge=1, the default), and when
smax=0 (and classical is not 1), (0,0) columns (error mechanisms with no detectors and no observables, e.g.,
after ^ cancellation) are stripped at the same time. This is exact: dropping two equal columns of a codeword gives a
lighter one, and a (0,0) column cannot belong to any minimum-weight or irreducible codeword, so the distance does not
change. The column indices (outC, finC, start, cbeg, cend, and the printed codewords) are those of the DEM,
the codewords found use the first one of each set of equal columns (RW hit counts are those of the merged codewords),
and in a finC codeword (0,0) columns are dropped and two equal columns cancel; merge=0 keeps all columns (e.g.,
to enumerate the codewords with each of the equal columns; the Python option expand=1 expands the list found instead,
see Codeword Export).
Detectors of the DEM are dropped if this is verified to be exact (drop=auto, the default): the distance is then found
from the smaller DEM without these detectors and the columns touching them, and the codewords found are codewords of
the full DEM. E.g., the full DEMs of the Mitten circuits (mitten_270: 1080 detectors, 70254 columns) reduce to the
size of the simplified DEMs without the minority-basis detectors (648 detectors, 8910 columns), and RW finds codewords
of the minimum weights (9 and 7 for the two circuits) in a few seconds. The binary finds the detectors to drop from the
DEM alone, and drop=[file] gives them in a file instead (drop=0 keeps all detectors); see
Dropping DEM Detectors.
All matrices with entries in debug=16).
With finG, the binary checks that ortho=0 skips the check (with a # WARNING: on
stderr), e.g., to screen many codes which are orthogonal by construction; the result is then valid only if
dist-m4ri strictly separates machine-parseable results from informational progress logs:
stdout: Outputs three space-separated integers:dmin dmax rw_stepsdmin - 1is the maximum cluster size analyzed without success by CC (dmin = dmaxif CC found a minimum-weight codeword).dmaxis the weight of the smallest non-trivial codeword found (by RW or CC, or read withfinC), or the supplied upper bounddmax=[int]if smaller (0if none).rw_stepsis the number of completed RW steps across all threads (0withmethod=2, or if CC found a minimum-weight codeword).- When
dmin = dmax = d, the exact code distance is confirmed. - With a stop target
dstop=U(method=2or3), the run ends once CC has certifieddmin >= U, unless a codeword of weight< Uis found;dmaxis then the weight of the lightest codeword found (0if none), neverU, sincedstopis not an upper bound (see Bracketing Mode).
Note on Compatibility: This 3-number output format (
dmin dmax rw_steps) is specific to the multithreadeddist_m4riand is incompatible with the legacy single-threadeddist_m4ri_old(which returned a single integerdor-w).
stderr: Receives all status banners, thread balancing reports, CC round timings, RW discovery logs, warnings, and confinement profiles, as selected by thedebugbitmap (see Debug Output).
Searches for low-weight non-trivial binary codewords debug&8
reports the choice of thread 0). Only the candidates lighter than the current weight limit are formed (the
weight of each candidate is counted first), and they are checked for outC (weight up to
min_hits, a representative set of
cov_cws lowest-weight codewords, where heavier codewords are gradually replaced as lighter ones are found.
With the RW pairs (pairs, on by default), a RW step also finds the codewords with exactly two non-pivot columns irred=1 (the default), so are the pairs whose
codewords are disjoint: their sum is a reducible codeword (see Codeword Export), at no
extra cost (the weight of the sum is then the sum of the two weights).
Relevant parameters:
-
steps=[int]: Total number of information sets / RW rounds across all threads (default: 100000). -
min_hits=[int](alias:max_hits=[int]): QDistRnd-style automatic convergence stopping criterion (default: 5; 0: off, and then the hits are not counted either: withoutoutCormaxC, RW records only the codewords lighter than the upper bound). Stops RW when the average number of hits per codeword$\langle n\rangle$ reachesmin_hits, both for the codewords of the minimum weight found and for the representative set ofcov_cwslowest-weight codewords (heavier codewords make the estimate more conservative). A single codeword suffices (e.g., a classical code with$k=1$ ). A lighter codeword is then missed with probability about$e^{-\langle n\rangle}$ ; see RW Convergence for this empirical estimate, its test (a warning at the end withdebug&1), and the information-set estimate. All hits count, those of CC andfinCas well as those of the RW steps with windows (kwin > 0, or by default the hybrid windows on every other step for$n \ge 500$ ), which find the codewords local in the column order much more often:$e^{-\langle n\rangle}$ may then be optimistic for the other codewords (debug&1prints the hits of the uniform and of the window steps). -
cov_cws=[int]: Size of the representative set of lowest-weight codewords formin_hits(default: 100; 0 for the minimum-weight codewords only). WithoutoutCandmaxC, at mostcov_cwsminimum-weight codewords are tracked (all withcov_cws=0). For codes with fewer thancov_cwsminimum-weight codewords, a smallercov_cwsmay stop RW earlier. -
kwin=[int](alias:win=[int]): Localized column permutation window size$W$ : the random column order starts with$W$ columns near a random column, which are thus preferred as pivots, in every RW step (default: 0; for$n \ge 500$ , every other RW step then uses an index window ofwin_hybconsecutive columns, as withwin_mode=1, and the other steps use uniform random permutations, also for codewords which are not local in the column order). A negativekwin(e.g.,kwin=-1) turns the windows off: uniform random permutations in every RW step. -
win_mode=[0|1]: Window construction mode whenkwin > 0:0for Tanner graph BFS neighborhood,1for contiguous column index window (default: 0). -
win_hyb=[int]: Window size of the hybrid RW steps (withkwin=0and$n \ge 500$ ; at most$3n/4$ ). Default: 0, i.e., 256 columns for a DEM and 512 for matrices. A RW step finds a codeword if all but one of its columns are pivots; the window columns come first in the elimination order, so that they are pivots unless they depend on earlier window columns. In a DEM, whose errors are in time order (stim), a window of$W$ consecutive columns is a slab of errors close in time, which touch only so many detectors: a window of one round of columns ($L \approx 10D$ columns for$D$ detectors per round) has rank about$2D$ , so that most of its columns cannot be pivots. The minimum-weight codewords of circuit DEMs are compact in this order (e.g., the weight-18 codewords of the double-gross circuit are final data measurement errors within 177 columns), and windows of 192-256 columns find them 2-10 times faster than 512 for the reduced DEMs of the gross, double-gross,bb_360, and mitten circuits (the double-gross DEM: about 10 distinct codewords of weight 18 per$2 \cdot 10^4$ steps with 256 columns, 0-2 with 512 or with one round), while matrices of codes gain nothing from 256. Withdebug&1, the binary prints the rounds of a DEM in the input summary (estimated from the time translation of its errors: the number$D$ of detectors per round is the most frequent distance of the two detectors of an error, as of measurement errors, which maps most errors to other errors when their detectors are shifted by$D$ ;$L = n D / m$ columns per round; none, e.g., for circuits with SWAP routing), and at the end of RW the codewords of the three lowest weights found by the uniform steps and by the window steps (e.g., to see codewords which are not local in the column order). -
pairs=[int]: The RW pairs of non-pivot columns ($p=2$ ): the number$l$ of random pivot rows of their buckets (at most 64; 0: off). Default: -1 (auto),$l = \lceil \log_2 (n-r) \rceil$ (about one non-pivot column per bucket), with fewer rows (at least one) while a codeword of the weight$w$ of the upper bound would be kept with the probability$\binom{r-l}{w-2} / \binom{r}{w-2}$ below 0.8 ($r$ : the rank of$H$ ), but enough rows to keep the about$(n-r)^2/2^{l+1}$ pairs of a step within about the work of its elimination,$rn/16$ pairs (or$2^{20}$ pairs for small matrices). Early in a run, while the upper bound is far above the distance, the keep probability alone gives$l = 1$ , all pairs in two buckets: a step on a large circuit DEM would then check millions or billions of pairs (0.14.4 and 0.14.5: e.g., 0.6 s of RW to the distance 9 of themitten_270circuit instead of 0.12 s, and no stop at the timeout forbb_756). The bound gives$l \ge 6$ for such DEMs, while the codes with$n - r < r$ (e.g.,c1920H) keep$l = 1$ . In a fixed number of RW steps, the pairs found the minimum-weight codewords of circuit DEMs 7-26 times more often per second (bb_72: 7x; gross,surf_d7,mitten_150: 22-26x), the weight 18 of the double-gross circuit in 3000 steps (20 without pairs, with 2% more time per step), 24 forbb_360in 600 steps (26), and lighter codewords of classical LDPC codes with a large distance (c1920H: 210-277 instead of 317-321 in 1000 steps, three seeds). With 128 threads, RW finds the weight 18 of the double-gross circuit in 1.5-2.7 s instead of 10-19 s, and 23 forbb_360in 1-2 min instead of 24 in 4 min (Section 8 of the benchmark). For small codes, a step takes 2-5 times longer, butmin_hitsis reached in fewer steps, mostly in about the same time. Where the hit counts are strongly non-uniform (a warning withdebug&1), whethermin_hitsis reached withinstepsis a matter of chance, with or without pairs: e.g., the unreduced matricesexamples/surf_d7_H.mmxwith_L.mmx(a circuit DEM given as matrices) mostly run to the step limit with the pairs, in twice the time, while the surface-code circuit of stim with$d = 7$ as a DEM (reduced by the binary) converges as soon as without pairs, and the one with$d = 9$ converges only with the pairs (Section 8.4 of the benchmark). Withdebug&1, the binary prints the pairs checked per step and the codewords recorded from the pairs and from the single columns (# RW pairs (p=2, ...)). Not with the experimentalksub. -
ksub=[int]: Experimental, should not be used (a warning is printed wheneverksub>0is given for RW, with or withoutmin_hits). Subspace dimension sampled from$\ker(H)$ for cache-resident RW elimination (default: 0 for full-matrix RW; automatically falls back toksub=0, with a warning ifdebug&1, when$m < \nu = \dim\ker(H)$ , e.g., for DEMs and most quantum codes). Each step reduces the span ofksubdistinct rows of a fixed basis of$\ker(H)$ , so that a few codewords are found much more often than others: then$e^{-\langle n\rangle}$ is optimistic, andmin_hitsmay end RW early. -
refresh=[int]: RW step interval for adaptive$\ker(H)$ basis refresh via low-weight codeword exchange and re-echelonization, only with the experimentalksub>0(default: 5000 whenksub > 0, 0 to disable). -
wmin=[int]: Minimum distance of interest (stop immediately when a codeword of weight$w \le w_{\min}$ is found; not when collecting codewords withoutCormaxC). -
threads=[int]: Maximum number of POSIX worker threads to run (default: all logical CPUs available to the process, i.e., its CPU affinity mask, e.g., withtaskset; subject to automatic throttling unlessnothrottle=1is specified). -
timeout=[sec]: Maximum execution time in seconds (default: 60.0).
Recursively explores connected clusters starting from each column noscan=0 (default), CC scans weights outC is specified, CC exhausts all
columns for weight smax=0 (default), the syndrome of a cluster is kept as a bitset (a column is added by flipping its rows), and
the last column of a cluster is found by a hash lookup: the syndrome left after the next-to-last column must equal a
column of smax=0 and noscan=0, the binary checks at
startup whether the all-ones vector $\mathbf{1}n$ lies in $\mathrm{rowspan}(H)$ over $\text{GF}(2)$ (debug&8 logs
even weights only (1 in rowspan(H))): if so, every codeword in $\ker(H)$ has even Hamming weight, and CC (both
method=2 and method=3) skips all odd weights $w$, advancing $d{\min}$ by 2 after each completed even-weight round
without codewords (and scanning only even weights when collecting codewords with outC and dW>0). With cc_prune=0,
the clusters and the codewords found (with their hit counts) are the same as with smax>0, only faster (about 3-5
times for CSS codes, and 5-20 times for circuit DEMs): a cluster is extended by the columns of the first row of its
syndrome, in the order of the rows of
Branch and bound (cc_prune=1, the default since version 0.15.0), after J. Y. Lee, K. Okada, N. Maskara, K.
Kasai, and H. Zhou, arXiv:2609.30069 (App. B), as used by G. Lee, arXiv:2610.13880 (App. C.4); the proof that it
finds the same codewords is in doc/cc_completeness.tex:
- A column is available if it is larger than the start column, and neither in the cluster nor forbidden. A cluster
is extended by the available columns of the unsatisfied row (a row of its syndrome) with the fewest available
columns, the first such row in the row order; with
sort_rows, the order of the rows only breaks ties. - Sibling exclusion: the column of a branch is forbidden in the later branches of the same cluster, so that every
codeword is found exactly once, with one CC hit (e.g., 1884 hits for the 1884 codewords of weight 12 of the gross
code, against 2084 hits with
cc_prune=0). Only irreducible codewords are found for sure (see Codeword Export). - A branch is pruned by lower bounds on the number of columns still needed: the syndrome weight bound above, an
unsatisfied row without available columns (a dead end), and the packing bound (the number of unsatisfied rows with
pairwise disjoint sets of available columns, each of which needs its own column, found greedily, where it can
prune).
cc_prune=2also uses the coverage and fractional cover bounds (with at least 4 columns left): fewer nodes, but more work per node (since version 0.15.2, about as fast ascc_prune=1on DEMs, up to 1.1 times faster for the largest rounds of theyarn_*codes, and 1.2–1.3 times slower for the BB codes). - The gain grows with the weight. E.g., with
method=2on 128 threads (the rounds up to the distance$d$ ): the$X$ sector ofyarn_mitten_300($d = 14$ ) in 0.23 s instead of 1.7 s (32 times fewer nodes in the round$w = 13$ ),yarn_ablp_560($d = 14$ ) in 0.20 s instead of 3.7 s, and double gross ($d = 18$ ) in 0.33 s instead of 1.4 s; the round$w = 11$ ofyarn_mitten_630with 110 times fewer nodes (22 times less CC work). The full DEMs of the gross circuit ($d = 10$ ) and of the custommitten_270circuit ($d = 9$ ) are certified in 79 s and 141 s instead of 189 s and 197 s (method=3). For DEMs at small weights ($w \le 7$ ), the fewer nodes cost about as much time. Version 0.15.2 lowered the cost per node, with the same nodes: up to 1.06 times less CC work for codes, and 1.04–1.26 times for DEMs (more on many threads).
With sort_rows=1, the default for matrices (finH or fin), the rows of cc_prune=0, this reduces the CC work for
matrices with very different row weights, e.g., 1.7-4.2 times for the matrices with hook columns of an extended CSS
distance (rows of weight 5 to 103), while codes with rows of equal weight are not changed. For a DEM (fdem), the
default is sort_rows=0: its detectors are in time order, and with cc_prune=0, the first unsatisfied detector of a
cluster then has most of its columns before the start column (these are not used), which is better than a light row;
sorting the rows made CC 1.3-1.6 times slower for the matrices surf_d5_H.mmx and surf_d7_H.mmx exported from DEMs
in examples/ (use sort_rows=0 for such matrices), and up to about 4 times (estimated) for the reduced
DEMs of the Mitten circuits. The distance and RW do not depend on the order of the rows (with the same seed, RW finds
the same codewords); CC enumerates the same codewords (e.g., with outC), possibly in another order (with
cc_prune=0, also with other hit counts). With debug=1, the input summary notes that the rows were sorted.
The scan ends with the round outC and dW>0, after the extra rounds up to
dmax=[int] or from codewords in finC) ends the scan as
soon as outC or maxC). A stop target dstop=U ends the scan
once outC, after the rounds 0 if none).
With a timeout, each round is started even if it is predicted not to finish in time (its CC work, the total CC
thread time, is extrapolated from the last two rounds with their growth factor, clamped to
Relevant parameters:
-
wmax=[int]: Maximum cluster weight to search (optional withtimeout>0, the default, withdmax>0, where the rounds end once$d_{\min} = d_{\max}$ , or withdstop>0, where they end once$d_{\min} \ge$ dstop; otherwise required formethod=2). -
smax=[int]: Maximum syndrome weight to track for confinement profile (default: 0, disabled for faster CC pruning and the bitset syndromes with hash lookups above; set e.g.smax=5to compute confinement; not withnoscan=1ordmin>1, which skip the small weights: thensmax=0is used, with a# WARNING:). -
sort_rows=[0|1]: Sort the rows of$H$ by increasing weight for CC (default: 1 for matrices, 0 for a DEM; see above). -
cc_prune=[0|1|2]: The CC search withsmax=0(default: 1, the branch and bound; 0: the walk of the first unsatisfied row of the versions before 0.15.0; 2: also the coverage and fractional cover bounds; see above). All values find the same codewords and distances; withdebug&8, the binary prints the nodes (calls of the recursion) of each CC round.
Expert parameters (each prints a # WARNING: to stderr; see
Restricting the CC Search below):
-
noscan=[0|1]: If set to 1 (method=2only), run CC only at weight$w = w_{\max}$ , without scanning smaller weights. The result is not a lower bound unlessdmin=wmaxis also supplied. -
start=[list]: Comma-separated list of CC start columns (0-based), e.g.start=0,48,96. Clusters grown from a listed column are not limited to larger column indices (for quasi-cyclic or otherwise symmetric codes). -
start_scan=[0|1](withstart, since version 0.15.1): 1: a cluster grown from a listed column$c$ only uses the columns$j > c$ , as in the scan of all columns (e.g.,start=0,l start_scan=1for the two blocks of a two-block group-algebra code over an abelian group). -
cbeg=[int],cend=[int]: Range$[c_{\text{beg}}, c_{\text{end}}]$ of CC start columns, for splitting one CC calculation into several runs which together cover all columns$0, \dots, n-1$ .
By default, CC grows clusters from every column start and
cbeg/cend (with method=2 or method=3) and noscan (method=2 only) restrict this search. Each of them
prints a # WARNING: to stderr, since the reported
| Option | CC start columns | Cluster growth | Reported |
|---|---|---|---|
| (default) | all, |
columns |
always |
start=a,b,c |
listed columns only | unlimited | under a code symmetry only |
start=a,b,c start_scan=1 |
listed columns only | columns |
under a code symmetry only |
cbeg, cend
|
columns |
after combining split runs | |
noscan=1 |
all, |
columns |
only if dmin=wmax is given |
-
start=a,b,c(e.g., quasi-cyclic codes): CC clusters are grown only from the listed (0-based) columns, but they are not limited to larger column indices, so that every codeword whose support contains a listed column is found.start=Nis a one-element list, andstart=-1(default) means all columns; the list is sorted, and duplicates are removed. The reported$d_{\min}$ (and an exact result$d_{\min} = d_{\max}$ ) is valid only if a code symmetry maps every minimum-weight codeword to a codeword containing a listed column. E.g., for a quasi-cyclic code with circulant blocks of size$\ell$ (columns ordered block by block), simultaneous cyclic shifts of all blocks are a symmetry, and it is sufficient to list one column per block, e.g.,start=0,48,96for three blocks with$\ell = 48$ . Cannot be combined withcbeg/cend. -
start=a,b,c start_scan=1(since version 0.15.1; codes only, not withfdem): the clusters grown from a listed column$c$ use only the columns$j > c$ , as in the scan of all columns. The reported$d_{\min}$ is valid if a code symmetry maps every minimum-weight codeword to one whose smallest column is listed. This holds if the listed columns are the smallest columns of the orbits of a group of code automorphisms, one column per orbit: a codeword meets some orbits, and an automorphism maps one of its columns in the orbit with the smallest listed column onto that column, which is then the smallest column of the image (the columns below it belong to the earlier orbits, which the image does not meet). The later start columns then need much less work than withstartalone (all columns below them are excluded), and the least if each orbit is a range of consecutive columns, e.g.,start=0,l start_scan=1for the two blocks (the orbits of the translations) of a two-block group-algebra code of length$2\ell$ over an abelian group, such as the BB codes: from column$\ell$ , only the block$\ell, \dots, 2\ell-1$ (2x less CC work thanstart=0,l, e.g., 2.1 against 4.4 thread-s for double gross up to$w = 17$ ). For other groups, renumber the columns so that each orbit is a range of consecutive columns, with the largest orbits first, and list the first column of each range. -
cbeg=[int],cend=[int](split runs): CC starts only from columns$c_{\text{beg}} \le i \le c_{\text{end}}$ (default-1: column$0$ , respectively, column$n-1$ ), still growing clusters using only columns$j > i$ . Thus the$d_{\min}$ of a single run only covers codewords whose smallest column is in$[c_{\text{beg}}, c_{\text{end}}]$ . To split a calculation, e.g., among several machines, runcbeg=0 cend=99,cbeg=100 cend=199, ..., so that every column$0, \dots, n-1$ is eventually listed in one of the runs, and take the minimum of$d_{\min}$ (and of the positive$d_{\max}$ ) over all runs. -
noscan=1(method=2only): a single CC round at$w = w_{\max}$ , skipping weights$w < w_{\max}$ . A codeword found only gives an upper bound$d_{\max}$ , and$d_{\min}$ is not raised above the supplieddmin. The result is certified only ifdmin=wmaxis supplied, i.e., if all smaller weights are already known to be absent. -
Before version 0.10.0,
start=Nwas equivalent tocbeg=N cend=N(clusters started from column$N$ using only larger columns; usecbeg=N cend=Nto reproduce it), andnoscan=1results were reported as certified even though smaller weights were not checked. -
Python interface (
dist_m4ri.py):starttakes a list of columns (start=[0, 48],start="0,48", orstart=0,48in the CLI), whilenoscan,cbeg, andcendare disabled: they are accepted for backward compatibility, but ignored with a warning tostderr(run thedist_m4ribinary directly to use them). With the persistent cache, start-list results are stored under the separate key<key>:start=a,b,c; such a run is seeded with the bounds of the main record, and only the always valid$d_{\max}$ and codewords are merged into the main record. The expert optiontrust_start(CLI:--trust-startortrust_start=1) accepts the start-list results as valid:$d_{\min}$ andrw_stepsare merged as well, and an existing exact start-list record is copied to the main record (and the cache file is saved) even if no calculation is needed. For DEM input, the start columns index the error mechanisms in their order in the flattened DEM (after thepmincutoff). For CSS codes, the same list is used in both sectors.start_scan=True(CLI:start_scan=1or--start-scan) passesstart_scan=1, with the separate key<key>:start=a,b,c:scan; it is ignored with a warning without a start list and for a DEM. -
Clusters from a listed column skip the columns listed before it (since version 0.15.0, with
cc_prune=1or2, andstart_scan=0): all codewords containing those columns have already been enumerated from them, so that each codeword is found once, from the first listed column it contains (seedoc/cc_completeness.tex).
Dynamically partitions the available thread pool between CC (pushing
-
Target Search Depth:
- Once a codeword has been found, CC rounds continue up to
$w = d_{\max} - 1$ : the round$w = d_{\max} - 1$ certifies$d_{\min} = d_{\max}$ (withoutC, CC continues with the rounds$w = d_{\max}, \dots, d_{\max} + \text{dW}$ , which enumerate all codewords to export). Before that, CC is limited only bywmaxand thetimeout. -
dexp=D(alias:dest=D) is a hint: as long as RW has not found any codeword, CC rounds$w > D$ run only on the threads which cannot run RW (see Multithreading, Throttling & Batch Sizing), i.e., CC pauses if RW can use all threads. CC resumes as soon as RW finds a codeword, or when RW ends. - A stop target
dstop=U(not an upper bound) takes the place of$d_{\max}$ as long as no codeword of weight$< U$ is known: the run ends once the round$w = U - 1$ certifies$d_{\min} \ge U$ (withoutC, after the rounds$w = U, \dots, U + \text{dW}$ ), without waiting for RW, and$d_{\max}$ is the weight of the lightest codeword found (0if none). E.g.,dstop=U wmin=U-1decides whether$d \ge U$ , and for a CSS code with$d = \min(d_X, d_Z) \le U$ known (e.g., from the other sector),dstop=Uis all that is needed.
- Once a codeword has been found, CC rounds continue up to
-
Predictive Workload Modeling:
- The RW threads measure the RW step time
$t_{\text{RW}}$ (thread time per step) continuously. The CC work of each round is measured as the total CC thread time, and the work of the next round is extrapolated with the growth factor of the last two rounds (clamped to$[2, 10]$ ). - For each round, the coordinator compares the remaining work in thread-seconds:
$T_{\text{CC}}$ of the rounds$w, w+1, w+2$ (up to$d_{\max} - 1$ , or up to$d_{\exp}$ before a codeword is found), and$T_{\text{RW}} = t_{\text{RW}} \times$ (remaining RW steps, at most 2000 once$d_{\max}$ is known), but no more than the RW threads can do before thetimeout. It splits the threads as$$\text{ratio} = \frac{T_{\text{CC}}}{T_{\text{CC}} + T_{\text{RW}}},$$ $$N_{\text{CC}} = \text{round}(N_{\text{threads}} \times \text{ratio}),$$ $$N_{\text{RW}} = N_{\text{threads}} - N_{\text{CC}}$$ with$1 \le N_{\text{CC}} \le N_{\text{threads}} - 1$ , where$N_{\text{RW}}$ cannot exceed the number of threads allowed to run RW (see Multithreading, Throttling & Batch Sizing). - Small rounds (estimated CC work below 5 ms): only 1–2 threads run CC while the majority maximize RW sampling speed.
- Heavier rounds: as the CC work grows, additional threads are shifted to CC, so that both algorithms converge on the exact distance simultaneously.
-
The certifying round
$w = d_{\max} - 1$ (and any later codeword collection round) runs on all threads, since RW can no longer improve the result. -
The first round at a supplied
dmin(no measured round yet) runs on half of the threads. -
During a round, CC threads are added when RW finds a new
$d_{\max}$ (e.g., if the round now certifies$d_{\min} = d_{\max}$ ), and all threads join a round which takes more than 1.5 times its predicted time. A busy RW thread joins a CC round when its current RW batch is done.
- The RW threads measure the RW step time
-
Timeout, Early Cutoff & RW Convergence:
- With a
timeout, a CC round gets enough CC threads to finish within 3/4 of the remaining time. A round predicted not to finish in time on all threads is not started while RW runs; once RW has ended, the run ends instead. Withsteps=0(no RW), as inmethod=2, such a round is started anyway, since it may still find a codeword of weight$w = d_{\min}$ . - The coordinator never ends CC while RW runs. Whenever no CC round can be started (CC done up to
wmax, a round predicted to exceed thetimeout, or CC paused bydexp), it waits for RW and re-plans when RW finds a new$d_{\max}$ or ends. The run ends once RW has ended and no CC round can be started. - When RW satisfies the
min_hitsconvergence criterion, RW workers stop early and yield 100% of threads to CC to finish certifying$d_{\min}$ . - Likewise, once all RW
stepshave been claimed, RW threads join the current CC round instead of waiting for it to finish.
- With a
Relevant parameters:
-
dexp=[int](alias:dest=[int]): Expected code distance, a hint for the thread allocation before RW finds a codeword (see above). -
dstop=[int]: Stop target for the lower bound (default: 0, off; see above, also formethod=2). Unlikedmax, it is not an upper bound, and it is never reported asdmax. A supplieddmin >= dstopends the run without a search (unless codewords are collected withoutCormaxC); otherwise,dstophas no effect withmethod=1. -
threads=[int]: Maximum number of worker threads (default: all logical CPUs available to the process; subject to throttling unlessnothrottle=1is specified). -
nothrottle=[int]: Disable automatic thread throttling (default: 0; CLI flag:--no-throttle). -
chunk_size=[int](alias:batch=[int]): Maximum RW step batch claimed at once by a thread (default: 0 for automatic adaptive sizing). Whenchunk_size=0(default in Python and binary), chunk size is chosen adaptively based on$n$ and step budget:- Small codes (
$n < 500$ ): 50 steps/chunk (250 for$\ge 10^3$ steps; 500 for$\ge 5 \times 10^4$ steps). - Medium codes (
$500 \le n < 5000$ ): 50 steps/chunk (100 for$\ge 10^4$ steps). - Large matrices (
$n \ge 5000$ ): 25 steps/chunk (50 for$\ge 10^4$ steps).
In either case, a batch takes at most
$\lceil r / (4 N_{\text{RW}}) \rceil$ of the$r$ remaining steps, where$N_{\text{RW}}$ is the number of RW threads (guided scheduling, see below). - Small codes (
-
timeout=[sec]: Maximum execution time in seconds (default: 60.0; set to0for infinite / no timeout). RW threads check the timeout (and the other stop conditions) between RW steps, and also within a step (every 16 columns of the Gaussian elimination), so that even on huge matrices, where a single RW step may take many seconds, the run ends shortly after the timeout. -
steps=[int]: Maximum total RW steps (default: 100000; set to0to run pure CC via bracketing coordinator). -
min_hits=[int]: Average number of hits per lowest-weight codeword for early RW termination (default: 5; 0 to disable, which also turns off the hit counting; see method=1). -
dW=[int]: Extra weight window above the minimum weight found,$w_{\min}$ , to continue collecting codewords ($w \le w_{\min} + \text{dW}$ ).
The parameter threads=[int] specifies the maximum number of worker threads to allocate (default: all logical CPUs
available to the process, i.e., its CPU affinity mask, so that taskset -c 0-15 dist_m4ri ... runs 16 threads; with
SMT, the sibling hyperthreads typically add 10-50% to the throughput of a fully loaded CPU, more for RW than for CC).
By default (nothrottle=0), automatic heuristics clamp thread usage to avoid overhead and resource thrashing:
-
Small-Code Throttling (
method=1andmethod=3, all threads), to avoid the thread creation and lock contention overhead on tiny workloads; with$m$ the number of rows of$H$ and$W_{\text{RW}} = m \cdot n \cdot \text{steps}$ the RW workload: at most 4 threads for$n < 60$ , or for$m \cdot n < 2 \cdot 10^4$ with$W_{\text{RW}} < 5 \cdot 10^7$ ; otherwise at most 16 threads for$n < 150$ or$m \cdot n < 10^5$ , if$W_{\text{RW}} < 2 \cdot 10^8$ . -
Lazy Thread Start (
method=2): the CC work is not known in advance (it grows rapidly with the weight), somethod=2starts only 4 threads, and the others for the first CC round predicted to take more than 2 ms on the threads started (or once a round has run for 5 ms): tiny runs avoid the start-up of many threads, longer runs use all of them. -
Large-Matrix Memory Throttling (RW threads only): For massive matrices (e.g. circuit DEMs with
$n > 20,000, m > 5,000$ ), RW is limited by the memory bandwidth: above 15 MB of dense working memory per RW thread (copies of$H$ and$H^T$ , or theksub$\times n$ subspace matrix withksub>0; about$m \cdot n > 6 \cdot 10^7$ for full-matrix RW), the RW throughput peaks at 32-64 threads whatever the matrix size, so that at most 32 threads run RW, with their total dense working memory within a quarter of the physical memory (at least 2 threads; 1.5 GB if the physical memory is not known). E.g., the reduced DEM of the$[[756,16,34]]$ code ($m = 13230$ ,$n = 129276$ , 0.43 GB per thread) runs 32 RW threads, 1.5 RW steps/s against 0.23 with the 3 threads of the former 1.5 GB limit. CC needs no dense matrices:method=2is not limited, and inmethod=3only the RW share is limited, while CC rounds can use all threads. -
Small Step Counts (RW threads only): At most
$\lceil \text{steps} / 10 \rceil$ threads run RW (all threads inmethod=1, the RW share inmethod=3, where CC rounds can still use all threads;steps=0inmethod=3runs pure CC without RW threads). -
Adaptive RW Chunk Sizing: Setting
chunk_size=0(or omitting it) enables adaptive batching, reducing atomic CAS contention by up to 10x during extended searches ($10^4$ or$10^5$ steps). -
Guided RW Scheduling: A thread claims at most
$\lceil r / (4 N_{\text{RW}}) \rceil$ of the$r$ remaining RW steps at once, where$N_{\text{RW}}$ is the number of RW threads (also with a givenchunk_size): no thread can monopolize the steps, and the batches shrink towards the end, so that the RW threads finish together. -
CC Work Units: A CC round at weight
$w$ hands out its start columns$i$ one at a time. The clusters from the first start columns, which can grow to all larger columns, are the largest share of a round (about$w/n$ for the first one), which limits the speedup to about$n/w$ threads: e.g., 16 threads for the$[[288,12,18]]$ code at$w=15$ . Rounds predicted to take more than 1 ms on all threads (with an expertstartlist of fewer columns than threads: 1 ms on each listed column, since 0.15.1), with fewer than about 4 such shares per thread, are therefore split into$B^L$ work units per start column ($L = 1, 2$ , or$3$ levels,$L \le w-3$ ,$B$ the maximum row weight of$H$ ): a unit fixes the column added at each of the first$L$ cluster extensions (empty units, where a row has fewer columns, are skipped). The number of units is limited by the predicted work of the round (at most$10^4$ per thread-second) to keep the claims cheap. Withdebug&8, the binary reports the work units of each CC round. -
Manual Override: Pass
nothrottle=1(or--no-throttlein Python) to forcedist_m4rito use the exact number of requested threads without throttling (all threads are started at once, also inmethod=2), and specifychunk_size=Nto override batch sizing.
With debug&8, the binary reports the throttling and the thread allocation of every CC round, and with debug&4 a
periodic status line shows the progress of the current CC round. A CC round in method=3 may have to wait until the RW
threads assigned to it finish their current RW batch.
The RW upper bound
Empirical estimate and test, as in the
QDistRnd manual, Sec. 3.3. The probability
that a RW step finds a given codeword is expected to depend only on its weight, and to decrease with the weight. If,
after
RW stops once min_hits: min_hits=5, and
min_hits=10. Here cov_cws lowest-weight codewords (heavier codewords only
make the estimate more conservative). The assumption of equal probabilities for the codewords of the same weight can
be tested: QDistRnd uses Pearson's statistic dist_m4ri applies it to each weight
class of the representative set, using the zero-truncated Poisson distribution (codewords never found are not known),
and with debug&1 warns at the end if the hit counts are strongly non-uniform: a p-value below min_hits is advisable; this happens, e.g., for the surface-code circuit examples/surf_d5_H.mmx. The hit
counts include those of CC and finC and of all RW steps, also of the steps with windows (kwin > 0, or by default
the hybrid windows on every other step for
Probability to find a given codeword (information sets; compare the QDistRnd manual, Sec. 3.2). A RW step reduces
per step (pairs,
Among the weights ksub>0, the estimate is not available), which makes
the estimate conservative (for circuit DEMs, often very much so); on the other hand, as the QDistRnd manual points out,
the information sets of a sparse matrix need not be equally likely.
With debug&1, the binary prints # codewords accumulated: ...), the data for the
information-set estimate (# RW information sets: n=..., rank(H)=..., steps=...), and the estimate itself together
with the non-uniform hit count warning. The Python wrapper (--verbose) prints both estimates for a RW upper bound
which is not certified, with the RW steps and time needed for a 1% miss probability, and a model check which compares
the observed average hits of the codewords of weight
Example (examples/surf_d5_H.mmx, a circuit-level matrix of the distance-5 surface code; one thread for a reproducible
result; the single columns only, pairs=0):
$ ./src/dist_m4ri method=1 finH=examples/surf_d5_H.mmx finL=examples/surf_d5_L.mmx steps=10000 seed=1 threads=1 \
pairs=0 debug=1
...
# codewords accumulated: total=100, min_w=5: cws=100, total_hits=208, hits min=1, max=33, avg=2.08, ...
# RW information sets: n=1958, rank(H)=120, steps=10000 (uniform permutations: 5000), ksub=0, pairs=0, ...
# Warning: non-uniform RW hit counts of the 100 codewords of weight 5: hits min=1, max=33, avg=2.08, ...
# some codewords are found much more often than others of the same weight (relative spread of hit rates 1.89):
# a lighter codeword may be missed with probability ~0.58 rather than exp(-1.70)=0.18; consider a larger min_hits
# information-set estimate (uniform random information sets, n=1958, rank(H)=120): a codeword of weight 4
# is found with probability 0.000845 per RW step, missed in 5000 uniform steps with probability 0.015 (1% ...
1 5 10000
Here, 10000 steps are not enough for min_hits=5 (method=2 with wmax=4 finds no codeword in 0.1 s. With the RW pairs (the
default, here min_hits=5 after 3331 steps (0.73 s), without the warning: a codeword of
weight 4 would be found with the probability 0.018 per step instead of 0.000845, and the pairs gave 67400 hits of
codewords of weight 5 (18043 distinct ones exist), the single columns 1825.
By default, smax=0 (confinement tracking disabled for maximum CC speed; for matrices, with method=2 or 3 and
debug&1, a notice is logged to stderr). When smax > 0 (e.g. smax=5), the CC algorithm tracks the minimum
non-zero syndrome weight observed for each cluster weight
$ ./src/dist_m4ri method=2 finH=./examples/surf_d5_H.mmx finL=./examples/surf_d5_L.mmx wmax=4 smax=5 debug=0 threads=4
# confinement: 1,1,1,1
5 0 0With debug=1, detailed per-weight lines are printed to stderr:
# w=1 min non-zero syndrome weight 1
# w=2 min non-zero syndrome weight 1
# w=3 min non-zero syndrome weight 1
# w=4 min non-zero syndrome weight 1
-
outC=[file.nz]: Saves the discovered codewords of weight up to$w_{\min} + \text{dW}$ ($w_{\min}$ : the minimum weight found) in standard NZLIST format (heavier codewords kept only for themin_hitsstatistic are not exported). Withmethod=2or3, once the distance$d$ is known, the CC rounds$w = d, \dots, d + \text{dW}$ enumerate all such codewords (unless thetimeoutis hit; with a stop targetdstop=Ubelow$d$ , only the rounds up to$w = U + \text{dW}$ run); withmethod=1, only the codewords found by RW are exported:(Indices are 1-based; for a DEM, those of its columns: with%% NZLIST % generated by dist_m4ri <weight> <col_1> <col_2> ... <col_weight>merge=1, the default, the codewords found use the first one of each set of equal columns, see the DEM input in the Overview.) -
Keep the DEM with its codewords: for a DEM, the indices refer to its
errorlines (in the order of the flattened DEM, after thepmincutoff: with anotherpmin, the columns and the codewords change). For a Stim circuit, save the DEM generated bydist_m4ri.pyin the same run that writes the codewords (--out-demtogether withoutC; otherwise a warning is printed). A DEM generated again from the circuit (also from one saved with--out-stim) can have differenterrorlines, or the same ones in another order, with another version ofstimor other options (e.g.,decompose_errors), and then the codewords do not fit it. (The distance cache is not affected: its keys are the DEM contents andpmin.) -
Expanded list (
expand=1, Python only): withmerge=1,dist_m4ri.pycan list each DEM codeword found once for every choice among equal columns (the binary and the cache keep the merged list). WithdWat most 1, this is the list ofmerge=0(withdWof 2 or more,merge=0also lists lighter codewords with two equal columns added). The expanded list grows quickly with the weight; withmaxC > 0, at mostmaxCcodewords are written, with a warning if some are dropped. -
Dropped detectors (
drop): with detectors dropped from a DEM (drop=auto, the default), the codewords found use only the columns which touch no dropped detector (see Dropping DEM Detectors); usedrop=0to enumerate all codewords of the DEM. (The Python wrapper returns the codewords of a cached record if it has any: use--no-cacheor another cache file to enumerate them again with other options.) -
finC=[file.nz]: Reads initial codewords from a file to initialize$d_{\max}$ and the codeword hash table (codewords heavier thanwmax > 0are skipped; for a DEM withmerge=1, two equal columns of a codeword cancel; withirred=1, a reducible codeword is replaced by an irreducible part, see below). -
dW=[int]: When set (e.g.dW=1), preserves and exports codewords of weight up to$w_{\min} + \text{dW}$ . -
maxC=[int]: Limits collection to at mostmaxCunique codewords (of weight up to$w_{\min} + \text{dW}$ ). -
Irreducible codewords only (
irred=1, the default; since version 0.14.8): a codeword$c$ is reducible if it is the union of a codeword and a disjoint nonzero vector$x$ with zero syndrome,$Hx = 0$ (e.g., a trivial weight-3 vector of a circuit DEM, or a second codeword); equivalently, the columns of$H$ on the support of$c$ have a rank below$|c| - 1$ . The nontrivial one of the two parts (at least one is) is a lighter codeword, so that the minimum-weight codewords are irreducible, and reducible codewords can only appear in the windowdW: e.g., for a circuit DEM, from the weight$d + 3$ on. They are not recorded (hence not exported or counted formin_hits): CC tests the rank of the columns of each codeword found (the CC search finds every irreducible codeword, but only some reducible ones, seedoc/cc_completeness.tex), RW skips the pairs of disjoint codewords (see method=1; the codewords of single columns are irreducible), and a reduciblefinCcodeword is replaced by a nontrivial part (the lighter one if both are), repeatedly, until it is irreducible (debug&1gives their number with the codewords read; this can lower$d_{\max}$ ). E.g., 102 of the 1300 codewords of weight 6 found by CC for the surface-code DEMexamples/surf_d3.dem(dW=3) are reducible, and 18% of the codewords of weight 9 found by RW with the pairs for thebb_72circuit DEM (dW=3). Withdebug&1, the binary prints how many times reducible codewords were skipped (# reducible codewords skipped ...). The expert optionirred=0(binary only; the Python CLI ignores it with a warning) records all codewords, as before version 0.14.8.
Notation: column
Dropping finC
codewords are mapped to the twins. The binary checks this for every set debug=1 prints a
summary line, e.g., dropped 8 of 24 detectors (drop=auto), exact: ...).
-
drop=auto(the default; alsodrop=1): the binary searches for the largest set$M$ (with$A = 0$ ) from the DEM alone (it finds it in all our examples), see below. -
drop=[file]: the detectors listed in the file (a text file with the 0-based detector indices,kforDk, separated by white space or commas, where lines starting with#or%are comments); if dropping them is not exact, a note is printed, and the full DEM is used. The values0,1, andautotake precedence over files with these names (write./1for a file named1). -
drop=0: keep all detectors. -
outD=[file]: writes the detectors dropped, in the format of a drop file, e.g.,# 8 of 24 detectors dropped (drop=auto), 0-basedfollowed by5 7 8 10 13 15 16 18(only the comment line if none is dropped).
The Python wrapper also offers drop=css (the minority-basis detectors of a Stim circuit classified as CSS, checked by
the binary in the same way; otherwise, e.g., for a DEM, drop=auto is used, with a warning), and writes the reduced
DEM with its maps to the full DEM (--out-reduced, see Python Wrapper). In every case,
the bounds found are those of the full DEM.
The binary only sees the DEM, which does not say which detectors belong to which basis. The search rests on four
facts (with
-
The sets which can be dropped are closed under union. If
$M_1$ and$M_2$ can be dropped and$j$ touches$M_1$ , then its twin$t = (S_j \setminus M_1, O_j)$ is a column; if$t$ touches$M_2$ , then its twin$(S_j \setminus (M_1 \cup M_2), O_j)$ is a column too (the other cases are similar). So there is a unique largest set$M^*$ , which gives the smallest reduced DEM. -
Sub-columns. Let
$\mathrm{Sub}(j)$ be the columns$t$ with$S_t \subsetneq S_j$ and$O_t = O_j$ . If$j$ touches$M$ , then$S_j \setminus M$ is the support of a column in$\mathrm{Sub}(j)$ , or it is empty (only if$O_j = \emptyset$ ). So a column without sub-columns (O-minimal) has all or none of its detectors dropped, and none if it flips an observable. The O-minimal columns join the detectors into groups (union-find); the groups with an O-minimal column which flips an observable are kept. -
Propagation. Once some detectors of a column
$j$ are dropped, its candidates are the members of$\mathrm{Sub}(j)$ (and the empty set, if$O_j = \emptyset$ ) which agree with the detectors of$j$ already dropped or kept: a detector of$j$ in no candidate must be dropped, and one in every candidate must be kept. No candidate at all is a contradiction: then no set which can be dropped contains the seed (with the detectors dropped before), so the seed group is kept. -
Seeds. The groups which are not decided yet are tried as seeds, largest first, each followed by the
propagation. The detectors dropped are kept if the propagation succeeds and all detectors dropped so far pass the
check (the kept part of every column touching them is the support of one of its sub-columns, or empty with
$O_j = \emptyset$ ); otherwise, the propagation is undone. The final set is checked once more as above.
The propagation is needed: dropping every group without an O-minimal column which flips an observable would drop 66 of the 120 detectors of a distance-5 surface code circuit (5 rounds), which is not exact; the propagation gives the 48 minority-basis detectors.
Why it fits CSS circuits. In a memory experiment in the bb_756 with 1.04M columns, with
si1000 or uniform noise), drop=auto finds exactly the minority-basis detectors of the circuit classification of
dist_m4ri.py; it finds nothing for a non-CSS color code circuit or for a repetition code.
Limitations. A set which needs drop=[file]. With missing twins (e.g., with pmin > 0, or biased noise without the
Performance. The search takes 0.08 s for gross_144 (1728 detectors, 67752 columns after merging), 0.36 s for
bb_360 (8640 detectors, 346680 columns), and 1.3 s for bb_756 (25704 detectors, 1037610 columns; 5.5 s to read,
merge, and reduce the DEM in all), and it drops about half of the detectors and 7/8 of the columns of these DEMs
(debug=8 prints the time).
dist_m4ri provides a four-tier help system:
- Short help on error: Printed to
stderrwhen invoked without arguments or with an unrecognized parameter, listing all allowed parameters concisely. - Standard
--help(fits 80 rows): Displays the most commonly used parameters with clear descriptions, bunches extra available parameters at the bottom, and points to--morehelp. - Full
--morehelp: Displays complete, detailed descriptions of all command-line parameters, debug bitmask flags, and output formats. - Help on some parameters,
--morehelp TOPIC(also--morehelp=TOPIC,--help TOPIC, or--help=TOPIC): Displays only the entries of the full help for the parametersTOPIC, under their section headers, e.g.,dist_m4ri --help maxC,dW(several names separated by commas; the names are case-insensitive,-and_are ignored, and so is=VALUEin--help=TOPICand--morehelp=TOPIC(a separateTOPIChas no=), the aliases given in the help entries (e.g.,destorbatch) work, anddebuggives the debug bitmap). An unknown name gives the closest names onstderr, and the exit code1. The section "Option groups" of the full help lists groups of related parameters:windows(kwin,win_mode,win_hyb),stopping,bounds,dem,matrices,codewords,threading,expert(the CC options whose results rely on assumptions:noscan,start,start_scan,cbeg,cend), andexperimental(ksub,refresh). A group name, also as singular or plural (e.g.,dist_m4ri -h window), gives all entries of the group, and a parameter of a group also the other entries of its group, as "see also" (e.g.,--help kwingives the three window entries).
--help, --morehelp, and --version (also -help, -?, -morehelp, --more-help, -more-help, and -version)
print to stdout. A file name may also be given as the next argument (e.g., finH= file). Conflicting inputs: fdem
together with matrix files (finH, finG, finL, fin), pmin, outD, or drop (other than drop=0) without
fdem, finG together with finL, and drop, finG, finL, or fin with classical=1, are errors; fin
followed by finH, finG, or finL is ignored entirely (with a warning on stderr: e.g., fin=A finG=B still
needs finH), while fin after them is an error. The Python CLI (python3 dist_m4ri.py) has the same help options
(see Python Wrapper).
$ ./src/dist_m4ri --help
./src/dist_m4ri (version 0.16.1): calculate distance of a classical or quantum CSS code
Usage: ./src/dist_m4ri [method=1|2|3] [parameter=value ...]
Calculation method:
method=[int] 1: Random Window (RW) algorithm (upper bound)
2: Connected Cluster (CC) algorithm (lower bound / exact)
3: Bracketing mode (concurrent RW and CC) (default: 3)
Input matrices (Matrix Market .mmx/.mtx format or Stim DEM):
finH=[file] Parity check matrix H (classical) or Hx (CSS quantum)
finG=[file] Hz check matrix (quantum CSS code only)
finL=[file] Lx logical operator matrix (quantum CSS code only)
Note: Either L=Lx or G=Hz is required for quantum CSS codes
fin=[str] Base name for CSS matrices (loads ${fin}X.mtx, ${fin}Z.mtx)
fdem=[file] Stim detector error model (DEM) file
pmin=[float] Minimum error probability threshold to keep for DEM (0.0)
merge=[0|1] Merge equal DEM columns (exact; codewords use the first one) (1)
drop=[auto|file] DEM detectors to drop if exact: auto=1 (default), 0 (none), or a file
outD=[file] Write the DEM detectors dropped (0-based) to file
classical=[0|1] 1: classical code (Hx only), 0: quantum CSS (auto-detected)
Distance bounds and guidance:
dmin=[int] Certified lower bound on distance (CC starts from dmin) (1)
dmax=[int] Known upper bound on distance (CC up to w=dmax-1 only) (0)
dstop=[int] Stop once CC certifies dmin>=dstop (not an upper bound) (0)
dexp=[int] Expected distance for method=3 thread allocation (alias: dest) (0)
Search limits and stopping criteria:
steps=[int] Maximum RW decoding steps / information sets (100000)
wmax=[int] Maximum cluster weight to search in CC (0=until bound/timeout)
wmin=[int] Stop immediately if cw with weight <= wmin is found (1)
min_hits=[int] Stop RW when lowest-wt cws are hit min_hits times on average (5)
timeout=[sec] Execution timeout in seconds, 0 for infinite (60.0)
Multithreading and RW optimization:
threads=[int] Max worker threads to use (0: all logical CPUs) (0)
ksub=[int] EXPERIMENTAL, do not use: subspace dimension sampled from ker(H) for RW (0)
kwin=[int] Localized column permutation window size W (0: auto/hybrid) (0)
Codeword collection:
outC=[file] Export found min-weight codewords (up to +dW) to file (.nz format)
finC=[file] Import initial codewords from file (.nz format)
maxC=[int] Maximum number of codewords to collect (0 for unlimited) (0)
dW=[int] Collect codewords up to the minimum weight found + dW (0)
Extra parameters (see --morehelp for details):
smax=[int] (0) Max syndrome weight for confinement profile (0 to disable)
noscan=[0|1] (0) Expert, method 2: CC at w=wmax only (no lower bound!)
start=[list] (-1) Expert: CC from listed columns only, e.g. start=0,48
start_scan=[0|1] (0) Expert, with start: clusters from column c use only columns > c
cbeg/cend=[int] (-1) Expert: CC start column range, for split runs
irred=[0|1] (1) Expert: 0 records also reducible codewords (cw + disjoint 0-syndrome vector)
ortho=[0|1] (1) Expert: 0 skips the check H G^T = 0 of finG (valid only if it holds)
sort_rows=[0|1] (1) Sort the rows of H by weight for CC (default 0 with fdem)
cc_prune=[0|1|2] (1) CC search: 0 first-row walk, 1 branch and bound, 2 also cover bounds
nothrottle=[0|1] (0) Disable thread throttling (also --no-throttle)
chunk_size=[int] (0) RW batch chunk size (0: auto, alias: batch)
win_mode=[0|1] (0) Window mode: 0=Tanner BFS, 1=index proximity
win_hyb=[int] (0) Hybrid RW window size (0: 256 for a DEM, 512 otherwise)
pairs=[int] (-1) RW pairs of non-pivot columns (p=2; -1: auto, 0: off)
cov_cws=[int] (100) Number of lowest-wt cws used for the min_hits statistic
refresh=[int] (0) RW steps between ker(H) basis refreshes (experimental ksub only; 5000 if ksub>0)
seed=[int] (0) RNG seed [0 for time(NULL)]
debug=[int] (3) Debug bitmask (0: silent, 1: summary, 2: progress, ...)
Help options:
-h, --help Display this help message (--help TOPIC: the same as --morehelp TOPIC)
--morehelp Display full help for all parameters (--morehelp TOPIC: only TOPIC, e.g., maxC or windows)
--version Display program version
Use ./src/dist_m4ri --morehelp to view exhaustive parameter explanations, including all debug bitmask flags (see
Debug Output below).
All diagnostic output goes to stderr; stdout only has the result line dmin dmax rw_steps. The debug bitmap
selects the diagnostic output, where lower bits are more informative. The value is a decimal or hexadecimal (0x...)
integer. Multiple debug arguments are OR-combined, where the first one replaces the default debug=3: e.g.,
debug=0 alone is silent, while debug=4 and debug=0 debug=4 both give only the periodic status.
-
0: silent, except for errors, warnings on the validity of the result (expert options, the experimentalksub, invalidfinCcodewords) and on ignored or overridden parameters (e.g.,stepswithmethod=2,smaxwithdmin>1), and the confinement profile withsmax>0. -
1(default) summary: input matrices (sizes, numbers of nonzeros, maximum row and column weights; for a DEM also its rounds,# rounds of the DEM: D=... detectors and L=... columns per round, and for the hybrid RW the window size), warnings, the reason why the run ended (# stopped after ...s: ..., e.g.,bounds coincide: dmin = dmax = 5,timeout=60s reached during CC round w=9 (37.5% of start columns claimed), orRW convergence reached), codeword and hit statistics, the reducible codewords skipped (irred=1, see Codeword Export), RW information sets (# RW information sets: ..., pairs=l, ...), the pairs checked per step and the codewords found by the pairs and by the single columns (# RW pairs (p=2, ...)), the codewords of the three lowest weights found by the uniform and by the window steps (# RW step types, w=...), codeword export. -
2(default) progress: the run plan (method, threads, RW steps, CC weights, timeout, rng seed), finished CC rounds, new upper bounds, RW convergence. -
4periodic status after 1, 2, 4, ... seconds, then every 60 s: the bounds, the RW steps and their rate,$\langle n\rangle$ , and the progress of the current CC round (or why no CC round runs). -
8thread allocation and timing: throttling, the planning and re-planning of CC rounds, waits for RW, and the predicted vs measured CC work of each round, with its number of nodes (calls of the CC recursion; seecc_prune), which does not depend on the number of threads. -
16code parameters:$\mathrm{rank},H$ ,$\mathrm{rank},L$ (or$\mathrm{rank},G$ ), and$k$ , by dense elimination at startup (slow for large matrices). -
32codewords: the support (1-based columns) of each new lightest codeword (RW, CC,finC). -
64arguments: the command-line arguments, the rng seed, and the files read. -
128matrices$H$ ,$G$ ,$L$ (for$n < 150$ , unless bit2048is set). -
256codeword dump: all codewords in the hash table with their hit counts (at the end), also those of the RW steps with uniform permutations (u) and with windows (w). -
2048: no size cutoff for matrices, codeword supports, and per-weight codeword statistics. -
4096:dist_m4ri_oldonly, CC recursion and confinement hash traces.
Bits 512, 1024, and 8192 to 32768 are reserved; bits 65536 and above are reserved for the Python wrapper
(ignored by the binary). E.g., debug=7 adds the periodic status to the default output, and debug=11 the thread
allocation and timing.
Before version 0.11.0, a different numbering was used: the old default bits 1 and 2 are now split into 1,
2, and 8 (debug=11 gives similar output); the old 4 (arguments) is now 64; the old 8 (dist_m4ri_old: a
line every 1000 RW steps) is now 4; the old 16 (new codewords) is now 32 (the upper bound messages: 2); the old
32 (matrices and codeword list) is now 128 and 256; the old 64 and 128 (hash traces) are now 4096.
Repeated debug arguments with different values were rejected.
# 1. Classical linear code using 8 threads in bracketing mode (method=3 is default); the number of RW steps varies
$ ./src/dist_m4ri finH=./examples/c204H.mmx dest=10 steps=100000 threads=8 debug=0
8 8 6154
# 2. Stim Detector Error Model (DEM) with timeout and codeword export (rw_steps=0: CC found a codeword of weight dmin)
$ ./src/dist_m4ri fdem=./examples/surf_d3.dem dexp=3 timeout=10 outC=cws.nz threads=4 debug=0
3 3 0
# 3. Quantum CSS code (Hx and Lx) using pure CC search up to wmax=5 with confinement
$ ./src/dist_m4ri method=2 finH=./examples/surf_d5_H.mmx finL=./examples/surf_d5_L.mmx wmax=5 smax=5 debug=0 threads=4
# confinement: 1,1,1,1,1
5 5 0A Python module dist_m4ri.py is included for high-level scripting, NumPy/SciPy integration, and Stim
interoperability without manual threading overhead.
-
The binary: the
compute_*()functions take its path asdist_m4ri=PATH(dist_m4ri_path=ofrun_dist_m4ri()), the positional parameter after the matrices or aftercircuit(give the other arguments by keyword). Otherwise,find_dist_m4ri_binary()triessrc/dist_m4ri,dist_m4ri, andbin/dist_m4riin the directory ofdist_m4ri.py, then../dist-m4ri/src/dist_m4rifrom there, the same three paths in the current directory, and finally thePATH(FileNotFoundErrorif none is found; no environment variable is read, and the CLI has no option for the path).--help,--morehelp, and--versionof the CLI warn onstderrif the binary is missing or older than the wrapper. -
compute_classical_distance(H, ...): Minimum distance of a classical linear code (from NumPy 2D array, SciPy sparse matrix, or.mtxfile). -
compute_quantum_distance(H, G=None, L=None, ...): Distance of one sector of a quantum CSS code, i.e., the minimum weight of a codeword$c$ with$Hc = 0$ and$Lc \neq 0$ , given eitherG(then$L$ is constructed from$H$ and$G$ ) orL, not both (ValueErrorif both or neither are given). -
compute_css_distance(Hx, Hz, Lx=None, Lz=None, ...): Distance$d = \min(d_X, d_Z)$ of a CSS quantum code (two runs of the binary, one for each sector); bothHxandHzare required (otherwiseValueError; usecompute_quantum_distance()for one sector). WithoutC="cws.nz", the$X$ - and$Z$ -codewords are saved tocws_X.nzandcws_Z.nz; withfinC="cws.nz", each sector reads its own filecws_X.nzorcws_Z.nzif at least one of them exists and is not empty (a sector without its file gets no input codewords), and otherwisecws.nzis given to both runs (codewords which are not valid in a sector are skipped). A known upper bounddmaxon$d$ is not an upper bound of either sector: it is passed to both sector runs as the stop targetdstop(and it is not stored in the cache), and the returned$d$ is at mostdmax. The two sectors are cached as the quantum codes(H=Hx, G=Hz)($d_Z$ ) and(H=Hz, G=Hx)($d_X$ ), withL=Lx/L=Lzif given, i.e., with the same cache records ascompute_quantum_distance(). -
compute_dem_distance(dem=None, circuit=None, basis=None, rounds=None, out_dir=None, out_dem=None, out_stim=None,p_noise=0.001, force_noise=False, drop=None, outD=None, out_reduced=None, ...): Minimum distance directly from astim.DetectorErrorModel,stim.Circuit,.demfile, or.stimcircuit file (the DEM has all detectors; an error channel which stim cannot write as independent error mechanisms, e.g., aPAULI_CHANNEL_1without$Y$ errors, is converted with stim'sapproximate_disjoint_errors=True, which only changes the probabilities of the errors):-
p_noise(CLI:--p-noise Porp_noise=P; default:0.001): the strength of the uniform circuit-level noise added to a noiseless circuit (seeadd_noise()below;0: none is added, and then a noiseless circuit gives no distance, with a note). A circuit with noise (a noise channel, or a noisy measurement such asM(0.001), seehas_noise()) is used as is, unlessforce_noise=True(CLI:--force-noise, also--force): then its noise is replaced by the uniform noise of strengthp_noise(withp_noise=0, all noise is removed, e.g., to save the noiseless circuit with--out-stim), e.g., to compare circuits with the same noise model. -
drop(CLI:drop=auto|0|css|FILE; default:None, i.e.,drop=auto): the detectors which the binary drops if this is verified to be exact (see Dropping DEM Detectors), with the distance of the full DEM (and the full DEM as the cache key), found from the reduced DEM; the codewords found are codewords of the full DEM which use only errors touching no dropped detector.None,True,1, or"auto": the binary finds the detectors from the DEM (drop=auto)."css": for a Stim circuit classified as CSS, its minority-basis detectors (seestrip_minority_detectors(); the basis other than the memory basisbasis), passed to the binary in a file (otherwise, e.g., for a DEM,drop=autois used, with a warning).Falseor0: keep all detectors. A file name: the detectors listed in the file, for any DEM. With"css"or a file, if dropping the detectors is not exact, a note is printed tostderr, and the full DEM is used. -
Ancilla and data basis tracking (
classify_qubits_thorough, withdrop="css"or--verbose) across virtual SWAP permutations and Clifford gates identifies data qubits,$X$ -sector and$Z$ -sector ancillas, routing qubits, and local data basis rotations (automatically recognizing both standard CSS codes and locally basis-rotated CSS codes such as XZZX). -
outD(CLI:outD=FILE): the binary writes the detectors dropped toFILE(seeread_drop_file()); with a cached result, the binary runs only for this (withdmin = dmax: no search). -
out_reduced(CLI:--out-reduced [PREFIX]orout_reduced=PREFIX): writes the reduced DEM (the errors which touch no dropped detector, after thepmincutoff, in the order of the flattened DEM, with the other detectors numbered consecutively) toPREFIX_red.dem, and its maps to the full DEM to one JSON filePREFIX_red.json:"detectors"(the original index of each detector of the reduced DEM, i.e., the complement of the list ofoutD),"errors"(the original index of each error of the reduced DEM, as the column indices ofoutC),"dropped","num_detectors","num_errors","pmin","source","dem"(the DEM file, if any),"drop", and"__version__"(defaultPREFIX: the basename of the input file without_full, in--out-diror the directory of the input file; seewrite_reduced_dem()). The reduced DEM has the same distance, and its codewords are mapped to codewords of the full DEM by"errors". -
--simpleand--full(simple=,full=): disabled since version 0.13.0 (accepted for backward compatibility, but ignored with a warning tostderr). The simplified DEM (without the minority-basis detectors) is replaced bydrop, which is as fast and exact: the distance of the simplified DEM is only a lower bound on that of the full DEM when dropping the detectors is not exact. To compute it anyway, pass the circuit stripped withstrip_minority_detectors(). -
--rounds [N]: Constructs the DEM withNrepetitions in the circuit'sREPEATblock (rounds=2by default when--roundsis passed without a number). If the.stimcircuit does not contain aREPEATblock, issues a warning tostderrand continues with the original circuit. -
--out-dir DIR,--out-dem [FILE],--out-stim [FILE]: Optionally saves the constructed DEM and/or the processed (noisy, round-adjusted).stimcircuit. When--out-demor--out-stimis passed without a filename (orout_dem=True/out_stim=True), filenames default to<basename>_full.demand<basename>_full.stim. By default,--out-dirdefaults to the directory of the input file. To use codewords (outC) later, save the DEM in the same run: a DEM generated again from the saved circuit may differ with another version ofstim(see Codeword Export). -
merge(CLI:merge=0|1; default:True): the binary merges the equal columns of the DEM (see the DEM input in the Overview). This is exact, and the cache key does not depend on it;merge=Falsekeeps all columns (e.g., to enumerate the codewords with each of the equal columns). -
expand(CLI:expand=0|1; default:False): the codewords returned and written tooutCare expanded over the choices among equal columns (see Codeword Export);maxC > 0limits the expanded list (with a warning if some codewords are dropped).
-
-
Return values (
cws: the codewords as lists of 0-based column indices):compute_classical_distance()andcompute_quantum_distance()returndist, or(dist, cws)withdo_cws=True, or(dist, d_info)/(dist, d_info, cws)withreturn_info=True;compute_css_distance()returns(dist, dX_info, dZ_info)(withdo_cws=True:(dist, dX_info, dZ_info, cws_X, cws_Z)), andcompute_dem_distance()always returns(dist, d_info)(withdo_cws=True:(dist, d_info, cws)). WithoutCalone, the codewords are written to the file but not returned. The boundsd_info = [dmin, dmax, rw_steps](seeformat_bounds_list()) differ from the output of the binary:dminis reported as0if it is at most 1 (no lower bound; e.g.,0 6 50where the binary prints1 6 50),rw_stepsas0if the distance is exact or there is no upper bound, and with the cache,rw_stepsadds up the RW steps of all runs with the same cache key. The CLI prints these numbers (and(exact)ifdmin = dmax), and for a CSS code (Hx=,Hz=) the linedX: dmin dmax rw_steps dZ: dmin dmax rw_steps (d = N). -
compute_model_lower_bound(circuit=None, ..., minority=False, out_model=None, return_info=False, dem=None)(CLI:fdem=FILE.stim|FILE.dem --model-lb [--model-minority] [--out-model PREFIX]): a certified lower bound on the distance of a Stim memory circuit (or standalone DEM) from its code-sized model, a row reduction of its DEM$(H, L)$ , for circuits whose DEM is too large for CC:- Two detectors are linked if they share a measurement record other than a final data measurement (identified after
both the last non-final detector and the last unitary gate, so circuits that place
DETECTORdeclarations at the end after the final measurements are handled properly); in a memory circuit, each group of linked detectors is the chain of the detectors of one check over the rounds. For a standalone DEM (or if the circuit groups are trivial), the detector chains across rounds are built from the detector coordinates(x, y, ..., t)when present, or else by linking consecutive-round detectors via dominant weight-2 observable-free (ancilla measurement) error mechanisms. Each group gives one row of the model, the sum of the rows of its detectors: the chain of a check$h$ of the measured basis telescopes to the parity of the final data measurements on$h$ , so that the column of an error is the syndrome of its net data error (a data or hook error); withminority=True(CLI:--model-minority), the chains of the minority-basis checks are rows as well. Equal columns are merged, and the errors with no effect (e.g., ancilla measurement errors) are dropped: a few columns per data qubit, e.g.,$144 \times 720$ for the double-gross circuit without the minority-basis rows. - The minority-basis rows are off by default: with depolarizing-type noise, every net data error which flips the
observable of the measured basis also occurs without a component of the other basis, so that a lightest model
codeword has a version which these rows do not detect. They never raised the bound of the benchmark circuits or
of the surface-code circuits of stim, and cost up to 9 s (
mitten_270, where CC on the larger model was needed); they may raise it for other noise models (e.g., biased noise). - Every row is a sum of detector rows, so every codeword of the DEM gives a model codeword of no larger weight: the
distance of the model certified by CC is a lower bound on the circuit distance. It ignores the timing within the
rounds: it is tight for the surface-code circuits of stim, the
hefMitten circuits, and then120torus circuit, but 1-2 below the distance of the BB circuits (bb_72: 5, distance 6; gross: 8, distance 10; double gross: 12, distance at most 18), of the custom Mitten circuits (mitten_270: 8, distance 9), and of then60torus circuit (4, distance 6). - The binary runs on the model with the measured-basis rows first, then (with
minority=True) with all rows starting from that bound: RW first, which ends as soon as it finds a model codeword of that weight, and only otherwise CC fromdmin(CC on all rows alone would be much slower: gross 75 s for 7, instead of 1.8 s for 8). The bound is stored asdminin the cache record of the DEM (with"dmin_model"), so thatcompute_dem_distance()starts CC from it, and the CLI prints the bounds of the circuit. Withreturn_info=True, the info has the model ("H","L", and"col_map", the model column of each error of the DEM), its sizes, and the bounds of both runs. The optionsmethod,threads,timeout,num_steps,dstop,wmax,seed,nothrottle,pairs,sort_rows, andcc_pruneapply to each run of the binary (also in the CLI).
- Two detectors are linked if they share a measurement record other than a final data measurement (identified after
both the last non-final detector and the last unitary gate, so circuits that place
-
classify_qubits_thorough(circuit, ...)/strip_minority_detectors(circuit, basis, ..., mask_out=None)/set_circuit_rounds(circuit, rounds): Helpers for Stim circuit Pauli basis tracking, minority-detector stripping (a listmask_outreceives one entry per detector:Trueif it is stripped;compute_dem_distance()uses it for the detectors to drop withdrop="css"), andREPEATblock round adjustment. -
read_drop_file(path)/write_reduced_dem(dem, dropped, prefix, pmin=0.0, num_detectors=None, meta=None): Reads a drop file (e.g., written by the binary withoutD): the sorted 0-based detectors and the number of detectors of the DEM from the comment line# N of M detectors dropped ...(Noneif none); writes the DEM without the detectorsdroppedtoPREFIX_red.demand its maps toPREFIX_red.json(seeout_reducedabove). -
has_noise(circuit)/strip_noise(circuit)/add_noise(circuit, p=0.001, force=False): Checks astim.Circuitfor noise (a noise channel, or a noisy measurement such asM(0.001)); removes all noise (stim'sCircuit.without_noise(): the noise channels are removed, the measurements lose their flip probabilities, and heralded noise such asHERALDED_ERASEis replaced byMPAD, which keeps the measurement record); and adds uniform circuit-level noise of strengthp(to the noise of a noisy circuit, which is kept; withforce=True, after removing it;p=0: none), with the gates classified bystim.gate_data():-
DEPOLARIZE2(p)after the two-qubit gates (CX,CY,CZ,XCX,XCY,XCZ,YCX,YCY,YCZ,SWAP,ISWAP,SQRT_XX,SQRT_YY,SQRT_ZZ, their_DAGversions,CXSWAP,SWAPCX,CZSWAP, andII); a classically controlled gate (e.g.,CX rec[-1] 5orCZ sweep[0] 5) getsDEPOLARIZE1(p/10)on its qubit; -
DEPOLARIZE1(p/10)after the one-qubit gates (I,X,Y,Z,H,H_XY,H_YZ,S,SQRT_X,SQRT_Y, their_DAGversions,C_XYZ,C_ZYX, and the other variants), and on the qubits idle since the previousTICK; - bit (phase) flips with probability
pafter$Z$ -basis or$Y$ -basis ($X$ -basis) resets and before measurements, both before and after the measurements with reset (MR,MRX,MRY); - none for the multi-qubit measurements (
MPP,MXX,MYY,MZZ) and the Pauli product gates (SPP,SPP_DAG).
compute_dem_distance()adds this noise to a noiseless circuit (seep_noiseandforce_noiseabove); for the distance, only which errors are possible matters, as long aspmin=0. -
-
read_sparse_vectors(filepath): Parses NZLIST files into lists of 0-based integer support indices. -
Distance caching (on by default, in memory for the Python process):
enable_distance_cache(),disable_distance_cache(),clear_distance_cache(cache_file=None, clear_file=False)(clear_file=Truealso deletes the file),get_cached_distance(..., pmin=0.0, start=None), and a persistent JSON file with the version"__version__", given as thecache_fileargument of thecompute_*_distance()functions or for all calls withset_distance_cache_file(path), which also loads it (see alsoload_distance_cache(),save_distance_cache(), andget_distance_cache()); the CLI usestmp_dist_cache.jsonin the working directory unless--no-cache(alsocache=0) orcache=FILEis given. For a Stim circuit,get_cached_distance(circuit=...)processes it ascompute_dem_distance()does (with the samerounds,p_noise, andforce_noise), so that the same DEM gives the cache key. A cache file written by a newer version is ignored (with a warning) and is not overwritten. The CSS records written before version 0.12.0 (keyscss:...) are converted into the two sector records: the lower bounds are kept, but an upper bound only if a stored codeword confirms it, or if it is smaller than that of the other sector (earlier versions could store admaxgiven tocompute_css_distance()as the upper bound of both sectors). Earlier versions ignore a cache file written by a later version (with a warning), and they may overwrite it with their own results: use separate cache files for different versions. -
Codewords (
do_cws=TrueoroutC=FILE): the codewords of a run are merged with the cached ones (also in the cache record), and they are written tooutC(for CSS codes, to the sector files); withfinCidentical tooutC, the codewords already in the file are kept. These can include reducible codewords recorded before version 0.14.8 (see Codeword Export): use a new cache file (or--no-cache) and a newoutCfile to get only irreducible codewords. -
Stop target
dstopof allcompute_*_distance()functions (CLI:dstop=U): the search ends once CC has certifieddmin >= dstop, unless a lighter codeword is found (see Bracketing Mode); the bounds are then[dmin, w, rw_steps]withdmin >= dstopand the weightwof the lightest codeword found (0if none). A cached lower bounddmin >= dstopanswers the request without a calculation. -
Row order
sort_rowsof allcompute_*_distance()functions (CLI:sort_rows=0|1; default:None, the binary's default, i.e., sorted rows for matrices and the detectors of a DEM in time order): passed to the binary (see CC Algorithm); the result and the cache key do not depend on it. -
CC search
cc_pruneofrun_dist_m4ri()and of allcompute_*_distance()functions (CLI:cc_prune=0|1|2or--cc-prune N; default:None, the binary's default 1, the branch and bound): passed to the binary (see CC Algorithm); the result and the cache key do not depend on it. Also forcompute_model_lower_bound()(withsort_rows). Withdebug=8(bit 8 of the binary),_last_run_stats["cc_rounds"]lists the completed CC rounds of the last run, each with its weight"w", CC thread time"work"and predicted work"predicted"(in thread-seconds;Noneif not known), wall time"time", number of CC threads"threads", and number of nodes"nodes"(the calls of the CC recursion), and"cc_nodes"is their total; the node counts do not depend on the number of threads (e.g., for benchmarks ofcc_prune). -
RW window size
win_hybof allcompute_*_distance()functions (CLI:win_hyb=N; default: 0, i.e., 256 columns for a DEM and 512 for matrices): passed to the binary if nonzero (see RW Algorithm); the cache key does not depend on it. Withverbose=True(CLI:--verbose), the binary's summary shows the rounds of a DEM, and the explanation of the bounds the codewords found by the uniform and by the window steps;_last_run_statsholds them as"rounds"(D,L,score) and"rw_step_types"(per weight: the distinct codewords and hits of each kind of step), and the model check of the information-set estimate uses the hits of the uniform steps only. -
RW pairs
pairsof allcompute_*_distance()functions (CLI:pairs=N; default: -1, auto; 0: off): passed to the binary if not -1 (see RW Algorithm); the cache key does not depend on it. Withdebug&1(orverbose=True),_last_run_statsholds the number of rows"rw_pairs_l"(0: no pairs), the pairs checked per step"rw_pairs_per_step", and the codewords recorded from the pairs and from the single columns ("rw_hits_pairs","rw_hits_single"); the information-set estimate (rw_infoset_find_prob(),rw_infoset_estimate(): argumentpairs_l) includes the pairs. -
Expert CC options of all
compute_*_distance()functions:start(list of CC start columns, e.g.start=[0, 48]; separate cache record<key>:start=a,b,c),start_scan=True(CLI:start_scan=1or--start-scan; withstart, clusters from a listed column$c$ use only the columns$j > c$ ; cache record<key>:start=a,b,c:scan; not for a DEM), andtrust_start=True(CLI:--trust-start; accept the start-list results as valid and copy them to the main cache record). The binary optionsnoscan,cbeg, andcendare disabled in Python (ignored with a warning tostderr); see Restricting the CC Search. The expert optionirred=0of the binary is not available either (the CLI ignores it with a warning): the codewords recorded and cached are irreducible (see Codeword Export). -
Expert option
orthoofrun_dist_m4ri(),compute_quantum_distance(), andcompute_css_distance()(CLI:ortho=0|1; default:True, since version 0.15.2):ortho=Falsepassesortho=0to the binary, which then does not check that the rows of$H$ and$G$ are orthogonal (in each sector of a CSS code with$G$ , i.e., withoutLxorLz), and the same warning as from the binary is written tostderr. The result is valid only if$H G^T = 0$ ; the cache key does not depend on it. Without$G$ (or withsolver="codedistance"), it has no effect (a warning). -
Matrices given as NumPy arrays (or array-likes such as lists) or SciPy sparse matrices are written to temporary MatrixMarket files for the binary (in
tmp/of the current directory, created if needed, or else in the system temporary directory, as are the temporary codeword, DEM, and drop files; kept withPY_DBG_KEEP_FILES), in the coordinate pattern format: their nonzero entries over GF(2), i.e., the entries with an odd value (floats are truncated, and the duplicate entries of a sparse matrix are summed). Since version 0.15.2, the wrapper formats these files itself (up to 65536 entries, larger matrices withscipy.io.mmwrite): e.g., 0.3 ms instead of 10 ms for a$36 \times 72$ matrix, and 1.1 ms instead of 13 ms for$499 \times 998$ (on a 128-core machine, wheremmwritehas a fixed cost of about 10 ms from the thread pool of its parallel writer). -
The experimental RW option
ksubof thecompute_*_distance()functions and the CLI should not be used: withksub>0(andmethod=1or3), the same warning as from the binary is written tostderr. -
The parameters of the
compute_*()functions have the names of the CLI options (--out-dirasout_dir, etc.), exceptnum_steps(steps),d_exp(dexp,dest),H/G/L(finH/finG/finL),demorcircuit(fdem),cache_file(cache),do_cws(--cws), andminority(--model-minority);d_min/d_maxare aliases ofdmin/dmax(run_dist_m4ri()takes the names of the binary). -
An unknown keyword argument of the
compute_*_distance()functions (e.g., a misspelled option, orsteps=fornum_steps=) raisesTypeError, with the closest parameter name as a hint (win=is accepted forkwin=). Other errors:FileNotFoundErrorif the binary is not found,RuntimeErrorif it exits with an error (e.g., for a missing input file, or rows of$H$ and$G$ which are not orthogonal),ValueErrorfor invalid option values (e.g.,cc_prune,drop,p_noise, ormethod=2without a limitwmax,dmax,dstop, ortimeout), andImportErrorifstim,qec_cert(withsolver="cert"/engine="cert"), orcodedistanceis needed but not installed. -
Inputs which are ignored give a warning on
stderr(# Warning: ...), unlessset_silent()is on (in the CLI:--silent, also-q): e.g., withsolver="codedistance", the options of the binary which are not at their defaults. The warnings on the validity of a result (e.g., forksub,ortho=0, orstart) are always written. -
CLI (
python3 dist_m4ri.py):--versionalso shows the version of thedist_m4ribinary; inputs which are ignored give a warning onstderr(the input is selected in the orderfdem,Hx/Hz,finH/finG/finL/fin; e.g., matrices withfdem,pmin,merge,expand,drop,outD, or--out-reducedwithoutfdem,basis,--p-noise, or--force-noisewithout a Stim circuit,basiswithoutdrop=css,classical=1withfinG,ortho=0without$G$ ), where the binary rejects some of these combinations with an error. WithfinH,finG, orfinL, the CLI takes only the missingfinHandfinG(the latter not withfinL) fromfin, in any order, with a warning, while the binary ignores afingiven before them and rejects one after them. -
CLI help:
--help(commonly used parameters, fits 80 rows) and--morehelp(all parameters);--morehelp TOPIC(also--morehelp=TOPIC, and--help TOPICor--help=TOPIC) prints only the entries of the parametersTOPIC(several separated by commas, e.g.,--morehelp maxC,dW; the names are case-insensitive,-and_are ignored, and so is=VALUEin--morehelp=TOPICand--help=TOPIC(a separateTOPIChas no=), and the aliases given in the help entries, e.g.,destorbatch, work). An unknown name gives the closest names onstderr, and the exit code1. As in the binary, the option groups (alsostimandcachein the Python CLI) give all their entries (e.g.,-h window), and a parameter of a group also the other entries of its group. The binary has the same help options. -
Solver / engine selection (
solver=orengine=in thecompute_*_distance()functions and the CLI, whereenginetakes precedence if both are given):-
'dist_m4ri'(default; alsoengine='native'or'off'): uses the native multithreadeddist_m4ribinary. -
'cert'(also'qec_cert'or'qec-cert') and'auto': hand off distance-only requests toqec_cert(qec_cert.pipeline.certify,certify_dem, orcertify_circuit). To makeqec_certimportable, install theqec-certpackage in the active Python environment (e.g.,pip install -e /path/to/qec-certafter building its native library withmake) or add its repository root toPYTHONPATH. - Hand-off scope: the hand-off covers distance-only requests. Codeword output (
do_cws=TrueoroutC),finC,smax > 0,dW >= 0,maxC > 0,start,outD,out_reduced,pmin > 0, andmethod=1or2stay on the nativedist_m4ribinary with'auto'(which also falls back to the native binary ifqec_certis not importable). With'cert',qec_certmust be importable (ImportErrorotherwise), codeword output (do_cws=TrueoroutC) raisesValueError, andoutD/out_reducedare ignored with a warning (method=1runsqec_certin upper-bound modemode="ub").--model-lbalways runs the native binary (solver=andengine=are ignored with a warning). - Cache rules for
qec_certhand-off: only unconditional lower bounds (statusin('exact', 'bounds', 'timeout')withoutlb_conditional) and verified upper-bound witnesses (witness_verified=True) are written to the distance cache. When the cache already holds a certified lower bounddmin > 1, that bound is passed toqec_certaswmin=dmin; if the certificate's conditionality is caused solely by that cached floor (lb_assumed_wmin <= dmin, with no trust flags, precondition violations, or unverified symmetries), the cached certifieddminis discharged and no longer makes theqec_certlower bound conditional. -
'codedistance': uses thecodedistancelibrary if installed (ImportErrorif missing,ValueErrorwith codeword outputdo_cws=TrueoroutC, and a warning onstderrfor non-defaultdist_m4rioptions).
-
-
Debug output:
debug=N(default: 0) is a bitmap, where the bits1to32768are passed to the binary (see Debug Output; itsstderris then printed), and the higher bits are used by the wrapper:PY_DBG_COMMANDS(65536: thedist_m4ricommand lines and run times),PY_DBG_CACHE(131072: cache messages),PY_DBG_CIRCUITS(262144: Stim circuit to DEM conversion details), andPY_DBG_KEEP_FILES(524288: keep and list the temporary files). In the CLI, the value may be hexadecimal (e.g.,debug=0x10003), multipledebugarguments are OR-combined, and--verboseimplies the bits1,2,65536, and131072.
Run from the repository root (the example reads examples/QX150.mtx and examples/QZ150.mtx, and writes to tmp/):
import numpy as np
import stim
import dist_m4ri
# 1. Classical Code Distance
H = np.array([
[1, 0, 0, 1, 1, 0, 1],
[0, 1, 0, 1, 0, 1, 1],
[0, 0, 1, 0, 1, 1, 1]
], dtype=np.int8)
d = dist_m4ri.compute_classical_distance(H, threads=4)
print(f"Hamming code distance: {d}") # 3
# 2. Stim DEM / Circuit Distance with Codewords
circuit = stim.Circuit.generated(
"surface_code:rotated_memory_z",
rounds=3,
distance=3,
after_clifford_depolarization=0.001
)
dist, dist_list, cws = dist_m4ri.compute_dem_distance(circuit=circuit, do_cws=True, threads=4)
print(f"Surface code distance: {dist}, found {len(cws)} minimum-weight codewords")
# The binary drops 8 of the 24 detectors (drop=auto, the default): save the reduced DEM and its maps to the DEM
dist_m4ri.compute_dem_distance(circuit=circuit, out_reduced="tmp/surf_d3", outD="tmp/surf_d3_drop.txt")
print(dist_m4ri.read_drop_file("tmp/surf_d3_drop.txt")) # ([4, 6, 9, 11, 12, 14, 17, 19], 24): the X detectors
dist_red, _ = dist_m4ri.compute_dem_distance(dem="tmp/surf_d3_red.dem", drop=0) # the same distance, 3
# 3. CSS Quantum Code ([[150, 32, 6]]): d = min(dX, dZ), with the bounds [dmin, dmax, rw_steps] of each sector
dist, d_x, d_z = dist_m4ri.compute_css_distance(
Hx="examples/QX150.mtx",
Hz="examples/QZ150.mtx",
threads=4
)
print(f"CSS distance: {dist}, dX: {d_x}, dZ: {d_z}") # 6, [6, 6, 0], [6, 6, 0]- Recent
gccwith POSIX threads support (-pthread). libm4ri-devlinear algebra library:sudo apt-get update -y sudo apt-get install -y libm4ri-dev
cd src
# Compile both multithreaded dist_m4ri and single-threaded dist_m4ri_old
make all
# Run full C test suite (115 tests)
make testpytest tests/test_dist_m4ri.py -vThe benchmark/ directory provides quantum CSS codes (dist_m4ri, and the results of the benchmarks of the program on a
128-thread server: the scaling with the number of threads (0.15.2 and earlier versions), and a comparison of the
versions 0.14.6, 0.14.5, 0.14.3, 0.14.0, 0.12.2, and 0.9.0. For example, on the full DEMs of the circuits, 0.14.0 finds
a codeword of the minimum weight in at most 1.4 s, while 0.12.2 needs up to 52 s or finds none in 300 s, and it
certifies the circuit-level distance 9 of the mitten_270 circuit (custom schedule) on its full DEM in 3 min (0.12.1:
2.1 h for the DEM with the minority-basis detectors stripped, whose distance is only a lower bound). Its Section 7
measures the RW window size and the thread throttle of 0.14.3 on the large circuit DEMs (see win_hyb), and its
Section 8 the RW pairs of 0.14.4 with the work bound of 0.14.6 (see pairs) and the code-sized model bound of 0.14.5.
With the pairs, RW needs 6-30 times fewer steps to a minimum-weight codeword of a full DEM where many steps are needed
(the time of these short runs is mostly reading and reducing the DEM), it finds the weight 18 of the double-gross
circuit in 1.5-2.7 s instead of 10-19 s and the weight 23 of the bb_360 circuit in 1-2 min (0.14.3: 24 in 4 min), and
it finds lighter codewords of codes with a large distance. Its Sections 9 and 10 measure the branch-and-bound CC
search of 0.15.0 (4-35 times faster for the codes with yarn_* codes and of
bb_360 certified, up to
If you use this program, please cite:
- I. Dumer, A. A. Kovalev, and L. P. Pryadko, "Distance verification for classical and quantum LDPC codes," IEEE Transactions on Information Theory, vol. 63, no. 7, pp. 4675-4690, 2017. doi:10.1109/TIT.2017.2690381.
Other related papers and software:
-
Branch and bound for the CC search (
cc_prune=1, since version 0.15.0): J. Y. Lee, K. Okada, N. Maskara, K. Kasai, and H. Zhou, "Design principles for ultra-high-rate quantum codes," arXiv:2609.30069 [quant-ph], 2026, Appendix B; G. Lee, "Bicycle Race: small two-block codes on the Pareto frontier," arXiv:2610.13880 [quant-ph], 2026, Appendix C.4. -
vecdec Repository (Random Information Set (RW) algorithm with error weights/probabilities): QEC-pages/vecdec.
-
QDistRnd GAP Package (Random Information Set algorithm for quantum codes over arbitrary finite fields): L. P. Pryadko, V. A. Shabashov, and V. K. Kozin, "QDistRnd: A GAP package for computing the distance of quantum error-correcting codes," Journal of Open Source Software, vol. 7, no. 71, p. 4120, 2022. doi:10.21105/joss.04120.
-
Performance Comparison: M. Webster, A. Jacob, and O. Higgott, "Distance-Finding Algorithms for Quantum Codes and Circuits," arXiv:2603.22532 [quant-ph], 2026. arXiv:2603.22532.

