Repository navigation
The coherent sampler sums the selected completed kernel in floating-point arithmetic when
its contraction plan fits the entry budget, where 2.1 summed each shot's whole class only
up to twelve. PyMatching's own sparse blossom, transliterated line for line, is now the matcher
behind PyMatching's API and behind Chromobius. tqec's compile pipeline, PyMatching's animate,
Tsim's ZX graphs, CUDA-Q QEC's dem_chunks and trt_decoder, the QDK's parameter classes and
report, Qualtran's Dash app, Fusion Blossom's Bottle server, and Tesseract's and Chromobius's
command lines are ported, each checked against what its original records or its own tests. What is
still not ported, and why, is at the end. The Python package and the Rust crate both at 2.2.0.
Behaviour changes are under Changed.
Exact class sums, by contraction. A coherent-sampler shot's weight sums the amplitudes of
every fault set no record tells apart from the shot's: its class, a coset of the circuit's
spacetime kernel, 2^k sets for k generators. 2.1 summed the whole class up to twelve generators
and truncated it past that. 2.2 writes the sum over c ∈ GF(2)^k as a product of small factors
(one for each group of generators that share locations, a phase and a sign for each generator,
and (−1)^(c_i c_j) for each pair whose operators anticommute: a quadratic form) and contracts it
one generator at a time along a min-fill elimination order, in O(k·2^w) a shot, w the
contraction width (CoherentSampler.contraction_width).
- When it is exact. Whenever the contraction's tables fit
compile_coherent_sampler(max_contraction=65536), table entries in all. 0 turns the
contraction off, a budget past 2^31 is 2^31, and no table spans 32 or more generators at any
budget. Past the budget, a kernel of at most twelve generators is still listed whole, as in
2.1; a larger one is truncated as 2.1 truncated it (exact_weightsis False), and the sampler
now says so with aUserWarning, where 2.1 was silent. Its entry lower bound is not a
guaranteed sufficient budget. The warning describes the chosen basis/elimination heuristic: "the circuit's K interference generators contract past max_contraction=B table
entries (their tables would hold N or more): ...; a max_contraction below N cannot sum the
whole class, and N or more may"; or, when no budget would, "... interact too widely to
contract at any max_contraction: their tables would hold more than 2^31 = 2147483648 entries
in all, the most a budget allows; ..."; or "the contraction is off (max_contraction=0), ...".
Unexpected internal refusal warns and asks for the circuit. Width is not optimized over
equivalent circuit representations. Successful planning can dominate compilation: audited
20/40-round crosstalk memories took 0.3060–0.3078 / 2.1947–2.2014 CPU seconds, versus 2.1.0's
0.00648–0.00812 / 0.02049–0.02076 (three runs, shared load; different exact/truncated work).
Sampling scratch per active worker is24*(arena+largest)bytes before factors/overhead
(arena=entries/2, at most36*entries), so the entry budget is not a byte limit. - Checked against the whole kernel listed (a random circuit for each k from 13 to 20, every
one with couplings between its generators, to 1e-10), against a sum over every one of the 2^k
products (eight random circuits for each k from 13 to 16, to 1e-11), and against the state
vector.
These are finite controls, not a proof over all numerical ranges. Contraction can change
last bits even at ≤12 generators; corrected proposal/group arithmetic can change seeded
outcomes withmax_contraction=0too. Historical sample-time ratios require remeasurement. - The joint sampler keeps 2.1's rule, the whole class up to twelve generators and the
truncation past that, whichexact_weights=Falsereports without a warning: its cross terms
are overlaps of the exact engine's states, not products of factors, somax_contractionhas
no effect on it. - Measurement provenance. All 8 validation and 28 capacity rows in
data/coherent/, and
the 6 d = 3 sweep and 4 d = 3 crosstalk rows, were remeasured with the 2.2.0 wheel and carry
generation_revision="coherent-merged-proposal-v1", which the current table and chart
generators require. This marker records provenance, separately fromcoherent.exactand
validation; it is never inferred from a version or added to an old row. Only the 12
larger-distance sweep and 5 crosstalk rows, measured by 1.8.0 before the inherited
merged-proposal correction, remain, as archive data excluded from the current tables; their
ratios, ESS/error-bar claims and distance-scaling conclusions are not current validation.
Earlier development-2.2 measurements remain in Git history and the preserved raw evidence.
Added
- PyMatching's sparse blossom, transliterated.
src/pm_exact/is PyMatching 2.4.0's
sparse_blossomline for line: its radix heap and the order it breaks ties in, its arenas'
reuse of freed slots, and its graph's neighbour order.Matching, PyMatching's class here
(sq.Matching,stabilizer_qec.pymatching.Matching, sinter'spymatchingdecoder), decodes
on it:decode,decode_batch,decode_to_edges_arrayanddecode_to_matched_dets_array
agree in the recorded plain-decoding corpus, ties and orders included. Checked shot by shot on
950,000 shots of 19 models (surface codes d = 3 to 7 at p = 0.001 to 0.02, colour-code
Möbius graphs, repetition codes, unit and negative weights) and 600,000 of 3,000 hand-built
graphs: 0 differences. It takes 1.00 to 1.04 times PyMatching's time (decode_batchwith
weights, d = 5 memory over 5 rounds, 100,000 shots, one thread). The tie tolerances in the
PyMatching, sinter, PanQEC, beliefmatching (min-sum BP) and folding comparisons are gone:
those finite comparisons are exact. Correlated facade decoding uses the separate matcher and
requires an unedited DEM/circuit loaded with correlations, with actual observable IDs 0–63;
ensured wider output is zero-padded. Edited or hand-built graphs refuse correlated decoding,
even where upstream accepts them. Two further divergences are deliberate. When a bit-packed
batch fails on a padding bit, PyMatching's later decodes raise "!mwpm.flooder.queue.empty()"
if the failing shot had earlier detection events at non-boundary nodes; this decoder refuses
the shot before it makes any region, and keeps decoding. A failed batch still names its shot
("shot k: " and PyMatching's message). And a region's radius offset is 64-bit: PyMatching
2.4.0'scompute_wrapped_radiusreturnsint32_t, so where a region's scaled radius passes
what that holds (a uniform chain of 131 or more nodes at weight 1.5 or 16,777,214.5) its decoder never
finishes, and this one decodes the path (tests/test_matching_radius.py). A fault id at or
past the decoder's count, which only areplaceorsmallest-weightmerge can put on an
edge (merges do not extend the count), is ignored when the count is 64 or fewer, as it is
above 64: PyMatching's1 << idis undefined there, and its arm64 wheel drops such ids eight
at a time and wraps the rest modulo 64, so no rule matches it, and this one never reports a
counted fault id for an uncounted one. - PyMatching's
animate:stabilizer-qec animate,python -m stabilizer_qec.pymatching animateandpymatching.cli(command_line_args=["animate", ...]), with PyMatching's flags,
messages and exit codes. Its frames are those of PyMatching 2.4.0's macOS arm64 wheel: all
1,080 frames of 11 recorded cases equal as sorted lines, the arithmetic fused as that wheel
fuses it on aarch64 (nothing is fused on x86-64), and all 44 recorded misuses give the
wheel's messages. Of 600 random command lines, 590 behave identically; the other 10 name a
model that does not parse (see the Tesseract entry). These recordings do not establish
general byte parity for NaN/overflow geometry or locale-dependent high-byte parsing. All
coordinate axes contribute to projection. Write/close failures produce controlled exit 1,
deliberately differing from observed native silent truncated success.--dets_inis read only to the first
shot's end, as C's stdio reads it, and a model's bytes past ASCII in comments and tags are
passed over, as Stim passes over them. Where PyMatching's program aborts (an--out_dirit
cannot make, a hit index past 64 bits),stabilizer-qec animateprintsstabilizer-qec animate: <message>and exits 1, andpymatching.cliraises the RuntimeError PyMatching's
does. Out-directory messages follow local host/library policies; exact probes below
cover macOS arm64, not universal native wording. MSVC's wording is not reproduced. Fresh exact ENAMETOOLONG/EACCES probes were macOS
arm64 only; a trailing diagnostic punctuation difference is retained. The six-digit halfway
formatter correction was checked on 20,374 finite f32 values on Apple libc, not all runtimes. - Chromobius on PyMatching 2.2.2's matcher, and its command line. The Möbius matching is now
pm_exactin the neighbour order of PyMatching 2.2.2, the PyMatching Chromobius 1.1.1 links.
The lifting gets PyMatching's edges in PyMatching's order. Recorded macOS predictions agree
on every shot of a 65,000-shot sweep (colour memories d = 3, 5 and 7
over 3 to 7 rounds, p = 0.001 to 0.01, X and Z; 240,000 shots in an extended run) and on all
340,000 shots oftools/decoder_check.py's colour cases (0 ties, at 0.72 times Chromobius's
time), with the weights equal as float32. This finite evidence does not ensure equal
observable choices on other inputs: tied lifting can differ at equal matching weight on
any platform, including macOS. Facade cases permit 95% agreement; the sweep requires 99%.python -m stabilizer_qec.chromobius predict|benchmark|describe_decoder|helpis Chromobius's program, with Stim's flag parsing
and record formats, and its outputs and messages, checked against 184 cases recorded from
Chromobius 1.1.1's own binary. Where it differs: a--circuitthat Stim's error analysis
refuses gets this package's analysis message; a fault of probability 1 is refused
(Chromobius describes and decodes it, with NaN weights); a model that only declares an
observable past L63 gets this package's 64-observable message (Chromobius decodes it, its
predictions past L63 undefined); records are read whole before decoding, and sparse hits/dets
additionally expand to a shots-by-detectors bool array;predictdecodes on every core; an index whose digits overflow Stim's 64-bit read
prints pinned macOS libc++abi text on every host and exits 134 without raising SIGABRT; a bit past the
last detector, where Chromobius's result is undefined, is refused with PyMatching's message
every time; and the matcher's weight scale counts a self-loop given more than once at its
merged weight alone, where PyMatching 2.2.2's also counts its first weight (no Möbius graph
has a self-loop). Invalid UTF-8 model bytes use replacement; native byte preservation
was not freshly verified. Precision policy is 16 significant digits on macOS arm64/Windows,
19 on non-Windows x86-64, 34 on other ARM64; exact text fixtures are macOS arm64 only.
Lifting failures use the port's shot-indexed message and preserve later valid calls rather
than imitating native state damage. Finite-coordinate overflow now yields controlled exit 1. - Tesseract's command line, and any number of observables.
python -m stabilizer_qec.tesseract_decoderis Tesseract'stesseractprogram: its flags (--circuit,
--dem,--sample-num-shots,--in,--out, the detector-order flags,--beam,
--pqlimit,--multipass,--stats-out, ...), its argparse library's errors and help, its
output lines,--dem-outand the--stats-outJSON. Checked against Tesseract's Python API
run as its program runs it (7 flag sets on 120 and 60 recorded shots) and its source:
predictions, counts and--dem-outequal, and the cost lines equal on the same model. With
no shot to count from (an empty--in, or every shot a logical error),--dem-outholds
error(nan)lines, as Tesseract's program writes them, and the stats and final line follow.
Model and circuit bytes that are not UTF-8 are replaced, as in the Chromobius program, so a
comment holding one reads as Stim reads it. Where it differs:--sample-num-shotsdraws this package's shots, not Stim's; an exception that
ends Tesseract'smainprints the same message and exits 134 without a core dump; an--out
it cannot open stops withFailed to open <path>(Tesseract's program writes through a null
file);--max-errorsstops exactly at the limit (Tesseract's program keeps the shots its
threads had already decoded, so itsnum_errorscan pass it); and--threadsshares one
decoder (and runs on one thread with--verboseor--multipass). The search decoder now
takes any number of observables, as Tesseract's does (SearchDecoder, Tesseract's
decoders,build_det_orders,build_detector_graph): a 100-observable model recorded from
Tesseract's wheel decodes as it does there, the verbose log included. The Rust API's
SearchDecoderstays at 64, since itsPrediction.observablesis au64, and refuses more
with its own message. Prediction write/close errors now yield controlled exit 1 and preserve
the primary error. Native Tesseract I/O-failure policy was not independently executed.
In all three programs, as everywhere in this package, a model or
circuit that does not parse is refused with this package's parser's message, not Stim's
(line 2: unsupported instruction 'FOO', where Stim raisesUnrecognized instruction name: foo). The exception is a model fault that Stim's parser throwsstd::out_of_rangefor:
an unrecognized instruction name, a number of 2^60 or more, a{never closed or a}never
opened. Chromobius's program does not catch it, so the Chromobius program aborts as
Chromobius's does, with Stim's message in the pinned libc++abi text and status 134. - tqec's compile pipeline, byte for byte. tqec 0.2.0's
circuit,plaquette,templates
andcompilepackages are ported, with tqecd 0.2.1's detector search
(stabilizer_qec/tqec/_compile/, on numpy alone, Python 3.9 up).
compile_block_graph(graph, convention=..., observables=..., block_temporal_height=...)
builds tqec's graph, and itsgenerate_stim_circuit(k, noise_model, manhattan_radius, ...)
writes tqec's circuit from tqec's own plaquettes, templates and detectors. Hadamard pipes,
spatial junction cubes and the whole gallery compile (stability, move/rotation, three CNOTs,
Steane encoding, CZ). All 51 circuits recorded from tqec 0.2.0, one fresh process each, are
byte-identical, and the 6 recorded refusals raise tqec's type and message. It is faster than
tqec in wall time on the cases timed (compile_block_graph(graph, convention=...).generate_stim_circuit(1), three runs a side in separate processes at a load
average of 5 to 11: 1.5 to 1.6 times on the Steane encoding, 2.8 to 3.1 on the CNOT, where
tqec takes 9 to 10 s, 5.9 to 6.6 on the memory; repeated at load averages of 38 to 184 the
ratios were 1.9 to 5.0 on the CNOT and 5.0 to 8.8 on the memory, higher in some runs and lower
in others, with no trend with load). Where it differs, on purpose: each compilation starts from fresh caches, so its
first generated circuit is a fresh tqec process's (tqec's caches hand out shared plaquettes whose schedules a
compilation changes, so tqec's circuit for a graph can depend on what the process compiled
before); the detector database is kept in memory (database_pathis taken and never read or
written); there are no worker processes (the output is the same);kmust be a positive
integer (tqec raises IndexError for 0 and writes a circuit for 1.5); and a
block_temporal_heightthat is not aLinearFunctionraises a TQECError (for a number, the
one tqec raises for the constant height the number stands for), where tqec 0.2.0 fails with
an AttributeError (a TypeError for an unhashable value). tqec 0.2.0's own bugs come with its
circuits:cz(["XI -> XZ", "IZ -> IZ"])andcz(["ZI -> ZI", "IX -> ZX"])
have non-deterministic observables at k = 1 (tqec issue #1034);cz("ZZ -> ZZ")in the
fixed-boundary convention at k = 2 has a non-deterministic detector; and so do two L-shaped
spatial-junction memories stacked with no pipe between them (#1062, fixed after 0.2.0). The
fixtures record examples, not an exhaustive list: spatial Hadamards at spatial junctions
can also produce non-deterministic detectors/observables. These inherited circuits are
reproduced for parity, not certified as valid error-correction experiments. A spatial
Hadamard pipe between two non-spatial cubes, along x or y, requiresfixed_boundary;
fixed_bulklacks the template (one touching a spatial cube, as in the CZ, compiles in both).
Y cubes and unfilled ports are unsupported. The lattice-surgery
engine's layout fromcompile_block_graph(graph, distance)andBlockGraph.compile
remains available at.circuit;generate_stim_circuitwrites tqec's layout (see Changed). - Tsim's ZX graphs.
tsim.Circuit.get_graph(backend=)and
get_sampling_graph(sample_detectors, backend=)build Tsim 0.1.5's ZX graphs, vertex for
vertex and edge for edge (stabilizer_qec.tsim.zx: Tsim's instruction builders and parser,
pyzx_param's simple graph with its vertex numbering and rewrite rules, and the sampling
graph's transforms), on pyzx_param'sGraphSwhere it imports and on this package's pure-Python
ZXGraphotherwise (tensor contraction uses NumPy).diagram("pyzx"),"pyzx-dets"and"pyzx-meas"draw them, and
to_matrixandto_tensorof a circuit with measurements, detectors, observables, feedback,
a reset after a qubit's first operation or noise before a reset are Tsim's ZX tensor, where
2.1 raised (measurements, feedback, later resets) or gave the wrong matrix (see Fixed).
Checked against 40 recorded circuits exercising selected gates, channels and annotations
(I_ERROR/II_ERROR are absent from this corpus) on
both backends, and against live Tsim on 4,200 random circuits (five graphs each), 8,000
randomto_matrix(902 of them equal up to a global phase, the unitary engine's, below) and
60 circuits' diagrams: 0 differences. CI runs a bounded sample of these live comparisons on
every push (one Linux job with Tsim installed). Where it differs: the shots stay this package's (see
Not ported); an angle appended as an int prints as a float, the spacing inside a tag is not
kept, and astim.Circuit's angle literals are read as floats; two product rotations with
the same angle in a row print as two lines, and so do a pair rotation'sTPPs on several
pairs and an undaggered pair rotation on several pairs (the inverse of anSPP_DAGone
printsR_ZZ(a) 3 2thenR_ZZ(a) 1 0), where Tsim prints each as one line
(R_ZZ(0.3) 0 1 Z2*Z3,TPP X3*X2 X1*X0,R_ZZ(a) 3 2 Z1*Z0); and a product naming a qubit
twice prints asR_PAULI(0.3) X0*X0(Tsim'sR_XX(0.3) 0 0does not read back). Tsim reads
each of these prints as its own circuit. An angle whose radians are past a float's range
(from about 5.7e307 half-turns) is refused, where Tsim takes it. A record entry from before
the first measurement (M 0thenDETECTOR rec[-2]) raisesMeasurementRecordError, an
IndexErroras Tsim's is and aValueErrornaming the instruction.to_matrixof a circuit
that does not need the ZX tensor is the unitary engine's, Tsim's up to a global phase
(SQRT_X_DAG 0gives Tsim's times e^(iπ/4)), and so is that of such a circuit with an
instruction Tsim's graph builders cannot read (a tag the core leaves inert,I[foo(x)] 0),
where Tsim's raises its parse error (as it is raised here too when the circuit needs the ZX
tensor or a product rotation names a qubit twice).to_matrixrefuses more than 2^22
entries, a spider of more than 24 legs and an intermediate tensor of more than 2^26 entries,
where Tsim allocates. - CUDA-Q QEC:
dem_chunksandtrt_decoder. CUDA-Q QEC 0.8.1's extended detector error
models (SeamId,ExtendedDem,DemChunksSpecand every other name ofpy_extended_dem.cpp;
dem_stitch,dem_close,dem_chunks_to_pcm, ...) and its CSS model construction
(dem_from_css_matrices) are ported.decoder_config.dem_chunksis read from and written to
CUDA-Q's YAML as CUDA-Q writes it, andexpand_dem_chunkscloses the chunks into the model the
decoder gets;create_realtime_decoderexpands them. Errors are CUDA-Q's types and messages.
All 101 tests of CUDA-Q'stest_extended_dem.cppand 70 of its DEM and YAML tests (Python and
C++) are ported, and the closed chunks equal, mechanism for mechanism, the model this package
derives from the same circuit.trt_decoderruns its ONNX model on the CPU: a zero-dependency
ONNX reader and a numpy evaluator of the operators the decoder needs (Gemm, MatMul, Add, Relu,
Sigmoid, Identity, Cast, Reshape, Flatten, and QuantizeLinear and DequantizeLinear in int8,
uint8 and float8), using deterministic typed operations and selected MLAS arithmetic: each
float32 multiply-add rounded once, as MLAS's FMA rounds it, and a MatMul followed by an Add
fused into one Gemm where onnxruntime's default optimizer fuses them. Its outputs match
onnxruntime 1.31.0's bit for bit on all 180 cases recorded on 23 models (macOS arm64; CUDA-Q
0.8.1's three models among them, equal to its git-lfs pointers' sha256). The selected
numerical contract also checks CUDA-Q's 30 PyTorch surface-model outputs at absolute
tolerance 1e-4. Other ORT optimizer/prepacking paths, computed MatMul weights, weight-only DQ
fusion and reduced-precision paths can round differently on the same CPU;
near 0.5 this can change a binary correction. This is not a native TensorRT/GPU comparison.
ONNX_CONTRACT.md specifies the targeted
IR 7–10/opset 13–21 eleven-operator subset, types, tolerances and limits; it is not a full
schema validator or resource bound. Numeric/strided/Fortran arrays, rectangular lists and
sparseOinputs are intentionally broader than CUDA-Q's row-major uint8 tensor binding. - Azure: the QDK's parameter classes and its report. qdk 1.33.1's
EstimatorParamsand its
sections (EstimatorQubitParams,EstimatorQecScheme,DistillationUnitSpecification,
ErrorBudgetPartition,EstimatorConstraints, ...), the QDK's code for Python 3.9, are taken
byLogicalCounts.estimateandazure_estimate, and raise the QDK's own errors: a
LookupErrorfor a custom qubit's or unit's missing field, a name given with a custom unit, or
both maximum constraints at once, and a bareExceptionfromEstimatorParams().itemswithout
num_items.EstimatorResultdraws the QDK's report:_repr_html_,summaryand slices as
text with their HTML,diagram, a batch'splot, and qsharp_widgets 1.33.1's space and scatter
charts as SVG (space_chart(),scatter_chart()). They are checked against 40 recorded
results, plain and with markdown, and the charts against the widgets' own functions run in node,
to 4 ulps.QECScheme.SURFACE_CODEandFLOQUET_CODEare in. Kept from 2.1:EstimatorError
andQSharpErrorareValueErrors (the QDK'sEstimatorErrorderives from BaseException
alone), andLogicalCounts.estimatetakes any object withas_dict(a batch when it
has_items, else its one dictionary; the QDK refuses anEstimatorInputParamsItemwith a
TypeError). A batch keeps itssummaryand single results theirsummary_data_frame, which
the QDK lacks, and the tables are drawn when first asked for. - Qualtran's Dash app.
stabilizer_qec.estimate.qualtran.uihas Qualtran 0.7.0's layout
builders and callback.ui.make_app()(andui.app) is Qualtran's Dash app: its layout and
callback match the recorded Qualtran cases through Flask's test client. Very large gate
counts can differ in last bits of 15-to-1 factory cycles/durations.ui.serve()
andpython -m stabilizer_qec.estimate.qualtran.uirun it, on Dash where dash is installed
(the newdashextra,pip install "stabilizer-qec[dash]"; CI installs dash 4.4.1) and
otherwise on the standard library's HTTP server, with a page of this package's own that draws
the component layout and runs the same callback, with template-free figures whose colors,
backgrounds and fonts can differ from Dash's installed-Plotly template. Public builders are
lightweight component/dependency mirrors, not Dash's full mutation/pickling/callback API;
make_appconverts them. Only the fallback guarantees ephemeral binding at port 0; Dash
receives 0 unchanged (Dash 4.4.1 uses PORT or 8050). The fallback accepts JSON media types
and bounds body reads at 1 MiB, with explicit length/method errors; this is not a bound on
callback work, connection count or client duration, nor a Host/CORS security guarantee. That page loads the installed plotly's
plotly.js, or without plotly cdn.plot.ly's plotly.js 3.7.0, pinned by its hash. Importing
uiimports neither dash nor plotly (Qualtran'suidoes not import without dash). Without
dash,ui.appraisesmake_app's ImportError, and so doeshasattr(ui, "app"), where
hasattrgives False for a missing attribute. - Fusion Blossom's
helper.bottle. fusion_blossom 0.2.13's vendored Bottle (0.14-dev) is
stabilizer_qec.fusion_blossom.helper.bottle, byte for byte, andhelper.serveis
fusion_blossom's server on it: 55 recorded requests (18 paths by GET, HEAD and POST, and a
range request) retain exact recorded headers and bodies,index.jsafter taking out the one
change made to it: the page imports apatch_urlonly from its own origin, where
fusion_blossom 0.2.13's imports any URL and so runs any web page's module in the data
server's origin, where it can read the data folder (serve,open_visualizerand
peek_codeserve the working directory by default). Raw live parity also has two platform limits:
GET/HEAD//index.jskeeps the recorded 200 on older HTTP handlers whose upstream returns
404; that exact 404 is checked separately, and the port must match the same live upstream's
canonical/index.js. Windows upstream website CRLF is compared as LF after raw body/length
checks, while installed resources must contain LF. It is imported with
stabilizer_qec.fusion_blossom, as fusion_blossom imports its own.
LF bytes and the raw upstream SHA are enforced for the vendored path; a Windows wheel
byte comparison remains unexecuted. Data serving now validates UTF-8 and stable-filesystem
containment of Bottle's strip+abspath candidate while preserving original stripped spelling
for both static-file calls (including Range preflight). Concurrent filesystem replacement
is outside this guarantee. JSON uses application/json; extensionless files default to HTML.
Quiet suppresses request logs, not all syntax/error logging.
fusion_blossom also installs its Bottle and its helper as the top-level modulesbottleand
helper(soimport bottlegives fusion_blossom's); this package installs neither. - Stim: a memory budget for the undetectable-error search.
search_for_undetectable_logical_errors(..., max_memory_bytes=None), onCircuitand
DetectorErrorModel(in Rust, the*_with_budgetmethods), keeps the sets it visits in
columns, within the budget even while they grow: half the machine's physical memory by
default, or on Linux half the cgroup's memory limit if that is less (Stim has no limit). A set
of k detectors takes 24 + 4k bytes, plus 5 to 11 bytes of the table. When the sets fill the
budget they hold between half and all of it, so a set takes one to two times its bytes in
budget: about 40 to 90 bytes for the usual sets of two to four detectors. A larger budget
never keeps fewer sets. Out of budget, the search raises Stim's message for a search that
finds nothing, then "(memory budget of N bytes reached after M states)". Limits of (2, 2)
take Stim's graph-like search, which the budget does not cover. The answers are unchanged:
6,000 recorded searches give Stim 1.16.0's. On QUITS's [[72, 8, 8]] (PYTHONHASHSEED 0, the
whole process, three runs each) the search takes 0.64 to 0.66 s of user time and a 144 to
148 MB peak footprint, against 2.1.0's 0.79 to 0.81 s and 228 to 241 MB, and Stim's 4.25 to
4.54 s and 530 MB. A gigabyte holds about twice the sets it did: on the gross code's memory
at limits (6, 6), 2.1 gave up at its 20-million-set cap with a 2.45 GB footprint, and 2.2 with
a 1 GB budget keeps 12.7 million sets in 763 MB. - ldpc: BP+LSD's merge orders. When LSD merges three or more clusters at once, ldpc takes
the order from a hash map keyed by heap addresses, so its answer to a syndrome can change
from one decode to the next in the same process.BpLsdDecoder._merge_order_outcomes(syndrome, max_leaves=4096, target=None)walks the merge orders this decoder can take, fewest
departures fromdecode's order first, and returns the corrections they reach. On the 15
windows of QUITS's sweep where ldpc's answer varied (recorded intests/ldpc_lsd_merge/), the
corrections reached are exactly two, this decoder's and ldpc's other. The QUITS differential
test now replays ldpc's window answers and requires equal predictions, where it let a shot or
two differ, andtools/decoder_check.pycounts a BP+LSD shot as tied only when ldpc's answer
is one of the reachable ones: in every run, none is not (how many differ varies from run to
run with ldpc's heap).decodeis unchanged. A random or relative serial schedule moves on
at every decode; the search afterdecode(syndrome)runs that decode's BP again, from the
schedule's state it started from, sodecode's answer is always among the corrections. The
search of any other syndrome is the next decode's, and no search moves the schedule on.
Changed
- The compatibility promise. 2.1's README promised that the public API "keeps working
through every 2.x release", that a seed gives the same shots "on any machine and any number
of threads", and that an engine bug raisesRuntimeErrorasking for a report, "never a
crash". 2.2 states the promise as the
compatibility policy, and narrows it. Names and
signatures keep working and are deprecated before removal, as before, but what a call returns
or raises can change in a minor release to correct it: to make a port do what its upstream
does, or to fix a defect, each change named here. 2.2's tqec, Chromobius, Tsim, Fusion
Blossom and coherent-sampler changes below are such corrections, and some 2.1 calls now raise:
compile_block_graph(g, block_temporal_height=5)(a number or a callable without a
distance) raises a TQECError, andgenerate_stim_circuiton a program whoseroundstqec's
layout cannot honour (compile_block_graph(g, 3, rounds=5).generate_stim_circuit()) a
ValueError, each saying what to call instead (see tqec below). Seeds reproduce within a version, on any number of threads, not across
machines or versions: a sampler correction can change seeded shots, as 2.2's coherent
corrections do. Crashes and hangs are bugs to report, not a supported response. Names a
module merely imports (fusion_blossom'shelper.ThreadingHTTPServer, say) are not part of
the API. - The coherent sampler. With more than twelve generators, a Clifford circuit's coherent
sampler now sums each shot's whole class when the contraction fitsmax_contraction, so its
weights and their spread can differ from 2.1's truncated ones.max_contraction=0keeps
truncation, but does not undo corrected proposals or normalized group arithmetic. Past the budget it warns where 2.1 was silent, so code run under-W errorstops
there.exact_weightsis True for a completed contracted kernel, andcontraction_widthis new. - The coherent data. Newly constructed rows record actual
versionand the explicit
generation revision above.coherent.exactdescribes whole-class coverage independently.
Saving historical rows never upgrades their provenance. Repeated--onlyselectors for
one field now union their values; selectors on different fields intersect. - PyMatching. Among equally light matchings,
Matching's predictions, weights, edges and
matched pairs are now PyMatching's (2.1 could pick another, and listed edges in its own
order).Matching.from_detector_error_modelandfrom_stim_circuitkeep each fault's
detectors in the order written, as PyMatching does, where 2.1 sorted them. AMatching
loaded withenable_correlations=Trueis loaded as PyMatching loads it: it decodes plainly as
PyMatching's does (2.1 used the plain load's weight scale, so ties and, rarely, predictions
differed), itsedges()list each edge low node first, and it refuses what PyMatching
refuses, with PyMatching's messages (2.1 gave its correlated matcher's messages and took
L0 ^ D1pieces).ensure_num_fault_idskeeps a decoder already built, as PyMatching's
does: predictions are as long as the new count, and until the graph changes (or
decode_to_edges_arrayor a correlated call rebuilds it) the decoder fills the fault ids it
was built with. A correlated decode on a load whose implied weights name a self-loop refuses
with PyMatching's "Failed to find neighbor.", anddecode_to_edges_arraywith
enable_correlations=Truechecks the syndrome first. Rust:UserDecoder's 2.1 decoding
methods (scratch,decode,decode_to_edges,decode_to_matched_pairs, the four-argument
decode_packed, andDecoded) are deprecated, in favour ofdecode_with,
decode_to_edges_with,decode_to_matched_pairs_withanddecode_packed_with, each taking a
workspace frommwpm(). - Chromobius. A colour-code model is refused as Chromobius refuses it, with its messages: a
detector declared without a fourth coordinate that is a whole number from −1 to 5
(detector D0among them), which 2.1 decoded as uncoloured; an error naming a detector no
declaration colours ("Expected all detectors to have at least 4 coordinates, ... but got
error(0.1) D1 L0", where 2.1 said "detector D1 has no colour annotation ...") or an
observable past L63 ("Max logical observable is L63 but a larger one appeared in ...", where
2.1 gave its 64-observable message); and a fault it cannot decompose (Chromobius's message,
with the instruction, its pieces and the detectors' data). The model is read as written, as
Chromobius reads it: a detector declared twice takes its last declaration. A shot the
Möbius matching cannot match is refused byColorMatching.decode_batchwith PyMatching's
"No perfect matching could be found ...", where 2.1 said "no lifting of the matching explains
the detection events". - tqec.
compile_block_graph(graph)and itsgenerate_stim_circuitare tqec's pipeline now,
so their circuits are tqec's: the CNOT at k = 1 has 81 qubits, where 2.1's engine layout had
113.compile_block_graph(g, block_temporal_height=5)without a distance, or with a callable
of k, ran the engine's rounds in 2.1 (height + 2 in every time slice). It now raises a
TQECError (tqec's height is aLinearFunctionof k): for a number, the one tqec raises for
that constant height, and for a callable, one saying the height must be aLinearFunction.
Each is followed by a sentence pointing at the engine, which still takes numbers and
callables at a distance:compile_block_graph(g, distance, block_temporal_height=...)and
BlockGraph.compile. The engine now also takes NumPy integers, which 2.1 refused, as
heights and as the distance (see Fixed); a float or bool distance is a TQECError (2.1
raised a TypeError for a float).CompiledBlockGraph.generate_stim_circuit(oncompile_block_graph(g, distance)) wrote
the engine's layout at distance 2k + 1 with the program'sroundsandmerged. It now
writes tqec's layout and takes tqec's keyword parameters (an unknown keyword is a
TypeError, where 2.1 ignored it), and raises a ValueError pointing at.circuit, the
engine's layout, for a program whose rounds tqec's layout cannot honour.- Separate compilations isolate the cache initialization/build transaction across threads.
First generation matches a fresh reference process; repeated generation of one compilation
retains upstream mutable rescheduling. Concurrent mutation/generation of one object is not
guaranteed. Built-in compilation pickle restores schedules and the next generated circuit;
observable iterables are materialized once. Direct graph rounds/merged controls are checked
against tqec's 2k + 1 layout on the graph's time slices, on every graph tqec compiles
(spatial cubes and Hadamard pipes too): matching values pass through to tqec's circuit,
others receive the same layout ValueError as the distance wrapper. LinearFunction.unambiguous_max_on_positivesinherits tqec's single-pass-iterator caveat
(unlike 2.1): candidate checks can consume it and yield an incorrect maximum; pass a list.database_path(ongenerate_stim_circuit,generate_stim_circuits_with_detectors,
generate_sinter_tasksandstart_simulation_using_sinter) defaults to tqec's
DEFAULT_DETECTOR_DATABASE_PATH, where it was None. The path is never read or written.
- Tsim.
R_PAULI(±0.25)andR_XX,R_YYandR_ZZ(±0.25)print as written and are
Tsim'sR_PAULI: its graph's scalar, its matrix through the ZX path and its errors. 2.1
stored and printed them asTPP. An angle is kept as written: 2.1 printed back the shortest
decimal that gave the stored radians, soR_Z(1.9967044602602857)came back as
1.996704460260286.inversekeeps the literals too (a zero as0.0, as Tsim's).tcountis
Tsim's, the graph's count, in which a coherent error is not a gate
(stabilizer_qec.Circuit.tcountstill counts its phase). The unrolled Python graph incurs
memory/time and can refuse sweep targets, malformed tags, unsupported products and invalid
record lookbacks where 2.1 returned its fast count. The two identified repeated scans were
removed without a general linear-cost promise. Aliases/case are resolved before tags, and
measurement-only/feedback/measured-I[RL] matrices use the ZX tensor. Lazy-chain integer
indexing works beyond sys.maxsize outcomes; len still overflows and full materialization
remains refused. T/T_DAG/TPP suffix metadata is not retained (S[T:x] 0printsT 0). A circuit with Pauli products or
sweep bits has its ZX graph built beforeto_matrix, so its errors are Tsim's. Choosing the
default ZX backend imports pyzx_param where it is installed, which calls
numpy.set_printoptions(suppress=True)for the whole process, as importing Tsim does. - Azure.
EstimatorError.argsis the QDK's(code, reason), where it was(message,),
and its repr follows (EstimatorError('<code>', '<reason>'));str(e),.message,.code,
.reasonand pickling are unchanged (reade.messagewhere code reade.args[0]). A batch's
side-by-side table shows "N/A" for a frontier item's counts, as the QDK's does, where 2.1
read the item's first frontier entry (result[i].summaryhas it). A failed result's
summaryraises an error that is both 2.1'sEstimatorError, with the estimate's code,
reason and message, and the QDK'sAttributeError:hasattr(failed, "summary")is False and
getattr(failed, "summary", d)givesd, where 2.1 raised from both, and itsstr()is the
QDK's "'EstimatorResult' object has no attribute 'summary'" (reade.messagefor the
estimate's).summaryandresult[i:j]areReports, astrsubclass whose
_repr_html_holds the HTML. - Fusion Blossom.
helper.serveis fusion_blossom's Bottle server, so it answers as
fusion_blossom's does where 2.1's standard-library server answered otherwise:icon.svgis
served astext/html; charset=UTF-8,/data/../xis a 403 "Access denied.", errors are
Bottle's pages, data files carry ETag, Last-Modified and Accept-Ranges and answer range and
conditional requests, HEAD works, and POST is a 405. The server is wsgiref's, one thread, and
leaves its listening socket open aftersrv.shutdown(); a laterservecall changes the
data folder of servers already running. Binding happens when the runner starts;
server_close()releases the socket after shutdown.open_visualizerpropagates bind
failure before browser/input work and cleans up its owned worker/socket on input errors;
synchronous startup and in-flight requests are not universally time-bounded.import stabilizer_qec.fusion_blossomnow loads
Bottle and appends Bottle's import hook tosys.meta_path, as
import fusion_blossomdoes.import stabilizer_qecandimport stabilizer_qec.sinterload
neither, asimport sinterdoes not: sinter's Fusion Blossom decoder imports Fusion Blossom
when it decodes, as sinter's does. - Stim's undetectable-error search. The 20-million-set cap and its message ("the search
visited 20000000 sets of detectors without finding an undetectable logical error; lower its
limits") are gone: the search runs to its memory budget. - Models past 64 observables. The refusal now names what takes at most 64 (the matching,
BP, clustering and window decoders, the distance searches, matching-graph diagrams and the
model sampler) and what takes any number (the Python search decoder, the text, counts,
circuit sampling, the leakage samplers among it (compile_leakage_sampler, and
deltakit_stim's samplers of a circuit with leakage, which 2.1 refused past 64), and
measurement conversion). - CUDA-Q QEC. A configuration with
dem_chunks, which 2.1 refused, is expanded, and
dem_chunkswithstim_dem_pathis refused, as in CUDA-Q.trt_decoder, which 2.1 lacked
(as a CUDA-Q built without TensorRT lacks it), is a registered decoder with CUDA-Q's
parameter schema. - CUDA-Q QEC's
pymatchingdecodes to the observables when its model has them, as CUDA-Q
0.8.1's plugin does: givenO, or a detector error model's text with observables, a decode
returns the observable flips (2.1 returned an error frame unless asked with
output="observables"), and parallel edges merge as independent errors unless
merge_strategysays otherwise (2.1 refused model text whose mechanisms shared a detector
pair).output="errors", this package's keyword, still asks for the error frame. With model
text, anOorerror_rate_vecthe caller gives replaces the text's, as CUDA-Q's
make_pcm_decoderlets the caller's keys win (2.1 refused them), and a wrongly shapedOgets
CUDA-Q's message ("O must be of shape (num_observables, block_size); ..."). CUDA-Q's two
PyMatching decoding tests oftest_dem.pyare ported. - Stim's shot writer, and sinter's predictions.
write_shot_data_filetakes what Stim's
takes: a 2-D numpy array, bool with one column per bit or uint8 bit-packed with ⌈n/8⌉
columns, and refuses anything else with Stim's ValueError ("Expected 24 bits per shot. Got
bit packed data (dtype=np.uint8) but data.shape[1]=24 != math.ceil(24 / 8)=3", "data must be
a 2-dimensional numpy array with dtype=np.uint8 or dtype=np.bool_"). 2.1 read any uint8 array
as packed and cut or padded it to the width, sosinter.predict_observables(dets=)with 0/1
uint8 detection events, orpredict_observables_bit_packedwith rows of the wrong width,
decoded other shots without an error; both raise now, as sinter does. - Fusion Blossom on a syndrome with no perfect matching (an odd number of defects in a part
of the graph with no virtual vertex, say, or a sinter model that does not match its data):
perfect_matching(),subgraph()and the Stim and sinter decoders on every solver raise
fusion_blossom'sPanicException("cannot compute final matching with unmatched outer node
0") at once. 2.1 spun forever there, holding the GIL and deaf to Ctrl-C, as formatting the
unmatched node for that message waited on a lock its own thread held. - MWPF's HTML export.
HTMLExport.compress_content,Visualizer.generate_htmland the
command line's--visualizer-filenamegzip the visualizer data as MWPF does, with
miniz_oxide 0.8.0's deflate at flate2's default level, byte for byte: a d=15 planar code's
page is MWPF's 493,740 bytes, where 2.1 wrote stored (uncompressed) deflate blocks and
1,141,236 bytes. - The command line streams shots.
sample,detect,m2d,convert,sample_dem,
decode,predictandcount_mistakesread, sample, decode and write 1,024 shots at a
time, as Stim's command line does, so their memory no longer grows with--shotsor the
input's length (2.1 held every shot, about 10 bytes per output bit in01). The bytes are
those of one pass: seeded samples are the samplers' own, anddecode --decoder relay_bp
seeds each batch as the whole run's. A ptb64 output whose--shotsis not a multiple of 64
is refused before anything is written. Bad data, orm2dsweep data that ends early, is
refused when its batch is reached, after the batches before it are written. count_mistakesis PyMatching's.--timewrites its two lines to stderr in PyMatching's
format (Decoding time per shot: infusfor no shots); 2.1 wrote them to stdout after the
count.--obs_inis used whenever it is given, even with
--in_includes_appended_observables. Without either, the predictions are compared with zero
observables, as PyMatching's program compares them (2.1 refused). Extra--obs_inshots are
not read, and too few is PyMatching'sObs data ended before shot data ended.; 2.1 broadcast
mismatched files, printing impossible counts when either had one shot. Malformed shot data
is still refused with this package's reader's message, not Stim's.diagramdraws a detector error model for every match graph type (matchgraph-3d,
matchgraph-3d-htmland thematch-graph-*names, as well asmatchgraph-svg); 2.1 parsed
a model as a circuit there and failed.--remove_noisewith a match graph type is refused
with Stim's message, where 2.1 drew a noiseless circuit's empty graph.- Model and circuit files are read as bytes. A byte that is not UTF-8 (in a comment, say)
becomes U+FFFD rather than refusing the file, in everystabilizer-qeccommand, whatever the
locale (stdin too). - Licence notices. The wheels and the sdist carry
THIRD_PARTY_CRATES.md, the licence texts
and notices of the Rust crates the compiled module links (PyO3, rand, wgpu, naga, objc2,
libloading, libm and the rest, about 90, listed for each wheel's target; written from
Cargo.lock bytools/third_party_crates.py), andTHIRD_PARTY_NOTICES.mdquotes HiGHS's,
filereaderlp's and pdqsort's licences, where it cited files the wheels do not carry. It also
reproduces the attribution of CUDA-Q QEC's NOTICE file, and names fasteval 0.2.4 (Christopher
Sebastian, MIT), whose parser and evaluator the Azure estimator's formulas run on, with its
notice inestimate/LICENSE-qdk.CITATION.cffgiveslicense: MITand says in its abstract
that the package is MIT AND Apache-2.0, as CFF reads a list of licences as a choice.
Fixed
-
Coherent arithmetic. Corrected the inherited merged-proposal odd probability (previously
odd/even odds) and normalized long-group parity probabilities/weights. Demonstrated valid
long groups now remain finite in deterministic and state-vector controls. The recurrence is
still f64 with limiting regularization; no proof over every numerical range is claimed.
Seeds and cost can change: three shared-load CPU runs on 100,000 shots of twenty merged
rotations took 0.261004–0.262286 s versus 0.138538–0.142096 before; the measured surface
case's overlapping spread is not evidence of a speedup. Early provably-wide refusal is
faster; it does not remove successful-plan compilation costs. -
Matching state. Stable shared-object decode/search calls lease independent mutable
scratch; failed calls drop it. No-op merges retain configuration; edited graphs invalidate.
Numeric batch coercion casts to uint8 before nonzero; NaN readback/noise validity follows
the reference predicates. Correlated packed output/copies preserve ensured width. Allocation
checks do not promise recoverable true OOM, and arbitrary concurrent mutation/load is not
atomic. Arm64 FMA can still change probability readback in last bits; no universal immunity
of weights or tie choices is claimed. -
QDK rendering. Untrusted table/report fields are escaped, optional Markdown retains a
fixed balanced formatting subset and HTTP(S) links, and diagrams embed quoted escaped JSON
preserving caller data. This deliberately differs from unsafe QDK 1.33.1 output. Explicit
developer HTML wrappers remain HTML. Finite output/semantic checks are not browser/CDN safety
certification. -
CI and helpers. PyMatching 2.4.0/Stim 1.16.0 references are pinned. One bounded extended
Ubuntu/Python 3.12 job covers three designated slow corpus files, and a second runs what the
matrix cannot install, none of it skipped: the Tsim tests against live Tsim, the sinter 1.16.0
and relay-bp 0.2.2 differentials (both built from their sources), and PanQEC's slow tests
(PANQEC_RUNSLOW=1); other matrices use reduced cases. The matrix no longer names
relay-bp==0.2.2orqldpc==0.4.1, which it could never install. This source change does not establish remote CI success. Valid scalar qsample comb
results again retain numpy.float64 rather than Python float. -
Tsim.
to_matrixgave the wrong shape for noise before a reset (Z_ERROR(0.1) 1thenR 1
gave a 2×1 matrix) and forII(II 0 1gave 1×1), and, in a circuit without
measurements, a factor of 2 too few for each target-lessDETECTORand each of
OBSERVABLE_INCLUDE(k)'s k + 1 observables (since 2.1.0);OBSERVABLE_INCLUDE(0) X0gave
the identity, where Tsim raises.- A qubit named only by noise and a product's identity letter, or by noise and a product that
cancels on it (SPP X0*Z1*Z1with noise on qubit 1), was dropped fromto_matrix(2×2
where Tsim's is 4×4). - Hand-written held forms are read as the core reads them:
pauli=X_Z(the core's_
identity), a tag's empty arguments (theta=0.1,) andI[T()]gave a wrong detector error
model silently and broketo_matrix,get_graph,tcountand printing, and
II[R_PAULI(theta=t, pauli=...)]was dropped, a wrong model and matrix, silently. SPP_DAG[R_PAULI(theta=a*pi)]and!factors are kept as Tsim keeps them: in printing, in
inverse(Tsim'sSPP_DAG[...]text), in fusion with neighbours and inR_PAULI's
limits and messages. A nativeII[R_ZZ(theta=pi/8)]prints asTPP Z0*Z1; it printed
R_ZZ(0.25) 0 1, which reads back asR_PAULI.- An all-identity product rotation is left out of
str(); Tsim read its raw line as a plain
II, another matrix, or refused it. - The inverse of a pair rotation (
II[R_ZZ(theta=t)] ..., andR_XX,R_YY) or of
II[R_PAULI(...)]is one instruction, printed as Tsim's inverse:
c.inverse().inverse() == c, andlen(c.inverse())is Tsim's. to_matrixraises Tsim's errors for anR_PAULInaming a qubit twice or more than 64
qubits, an anti-Hermitian product and sweep bits, and is Tsim's forTPP Z0*Z0([[1]]) and
for products naming a qubit twice.SPP[T] X0 * Y1no longer raisesIndexError.CompiledStateProbsconditions on every record bit (since 2.1.0). Before, the
stabilizer-rank engine ignoredMPADbits and herald bits and matched each later
measurement against the wrong bit, so probabilities over all outcomes summed to 2 and
impossible records got probability 1 (MPAD 0gave 1 for both outcomes). AnMPADflip and
a herald are now noise drawn with the rest, as Tsim draws them, and a draw's probabilities
sum to 1. A reset's unread outcome is summed over, as in Tsim, where the qubit is entangled
with the rest. A reset of a qubit on its own, such as a fresh qubit'sRX, leaves the same
state either way, so it costs one run and is never drawn. Before, the outcome was drawn
once, so a noiseless circuit that resets an entangled qubit (H 0,CX 0 1,R 0,M 1)
gave 0 or 1 depending on the seed, where the truth and Tsim's answer are 0.5.compile_sampler,compile_detector_samplerandCompiledStateProbsrefuse what Tsim's
graph builder refuses, with Tsim's messages, asto_matrixandtcountalready did: an
anti-Hermitian product (TPP Y0*X0), anR_PAULIthat names a qubit twice or more than 64
qubits (33R_ZZlines in a row), and sweep bits. Before, they sampled such circuits. A
record entry from before the first measurement raisesMeasurementRecordError(an
IndexError, as in Tsim) from every sampler. Before, the samplers raised a plain
ValueError.- Parametric tags are read with Tsim's grammar. A tag of the form
name(...)that Tsim
refuses raises Tsim's message, prefixed with the line number. Examples: an extra or unknown
key (I[R_Z(theta=0.3*pi, phi=0.1*pi)],U3(..., mu=0.1*pi)), a missing key
(I[U3(theta=0.1*pi)], which gave a bareKeyError), and a literal that is not a decimal
number times pi (theta=0.3*pi*pi,theta=1/3*pi,theta=abc*pi,theta = 0.3*pi).
Before, these were read as rotations, or crashed withKeyErroror "could not convert
string to float". A half-turn tag that Tsim does not read as a rotation (I[R_Z (theta=0.5*pi)] 0,
I[ R_Z(...)] 0,I[R_Z(...) ] 0,I[R_Z(theta=0.3*pi)#x] 0) is the identity, as in
Tsim, andSPP[ R_PAULI(...)] X0is Tsim'sSPP. Before, both were rotations. A#inside
a tag's brackets is part of the tag, as Stim reads it, not the start of a comment.
-
Stim.
Circuit.inverse()ofII[R_PAULI(theta=t, pauli=...)]keeps each letter on its
qubit. It reversed the letters whole while reversing II's targets pair by pair, so
c + c.inverse()was not the identity. -
Coherent twirl.
Circuit.twirled()(merge=False) repeated a tagged gate of several
targets once per target:I[T] 0 1came out asI[T] 0 1twice (S on each qubit) and
I[R_Z(theta=0.1)] 0 1 2three times, silently (alsoI[T] 0andI[T] 1on separate lines,
which the parser fuses, and a tagged channel such asI_ERROR[LEAK(p=0.1)] 0 1). Each gate is
now kept once, beside its errors twirled target by target, so a circuit with no coherent error
twirls to itself (since 2.0.0). -
Merged twirl.
twirled(merge=True)read a tagged gate at a Clifford angle
(I[U3(theta=π/2, phi=0, lambda=π)],I[R_Z(theta=π/4)]) as a non-Clifford location rather
than the Clifford gate it is, so the rotations either side of it that are one fault were not
merged and the "coherence-aware" model was the plain twirl. It now reads them as the coherent
samplers do, and gives the plain-gate circuit's merged errors. A non-Clifford gate between
errors of its own axis was counted twice:R_X(0.1),I[R_X(0.3)],R_X(0.2)merged to one
error of sin²(0.6) while the gate also ran. The errors now merge to sin²(0.3) beside the gate.
Errors either side of a gate they anticommute with (Z either side of R_X, X either side of T)
stay apart, and a gate on other qubits (a T layer between a fault's places) does not stop the
merge. -
Time reversal.
Circuit.time_reversed_for_flowsreversed a taggedI/IIgate's
targets but kept its tag, soI[R_PAULI(theta=π/4, pauli=XZ)] 0 1became... pauli=XZ)] 1 0
(the letters on other qubits) andI[T], rotation angles andU3were not inverted. The tag is
now inverted asCircuit.inverse()inverts it (I[R_PAULI(theta=-π/4, pauli=ZX)] 1 0,
I[T_DAG]); a coherent error is negated asinverse()negates it
(I_ERROR[R_X(theta=-0.1)], the same twirl), and a channel with no inverse (damping,
leakage) and a tag that is not ours are kept. This text differs from Stim's, which treats
every tag as inert. -
Coherent sampler order.
compile_coherent_sampler(order=0)(and the joint sampler's)
enumerated every connected product of each cluster, exponential in its size, and hung on a
truncated kernel (d = 3, 40 rounds,max_contraction=0). The core now takes 0 as 1, as the
Rust builder'sorderalready did. -
Copies keep their class.
pickle,copyanddeepcopyrebuild aCircuit,
DetectorErrorModel,PauliString,TableauorMatchingas its own class, as Stim's
and PyMatching's do. A subclass stays one, rebuilt without calling its constructor (as
Stim's), so a subclass whose constructor takes other arguments than the text copies too. A
Matchingsubclass keeps its overrides and its attributes; the other classes keep none of
the instance's own attributes, as in Stim. Deltakit's circuit
(stabilizer_qec.deltakit_stim) keeps its leakage, so its copies sample and model it.
Before, every copy was the base class. A Deltakit circuit sent toRunAllAnalysisEngine's
process pool (its default of 16 processes) therefore lost its leakage there, and the engine
counted 0 failures where a serial run counts about 3%. Pickles written by 2.1 still load. -
Reference signs of tagged Clifford-angle gates.
reference_detector_and_observable_signs
andcount_determined_measurementsread a tagged gate at a Clifford angle
(I[R_X(theta=π/2)] 0) as the Clifford gate it is, asreference_sampleand the samplers
do.compile_m2d_converter(skip_reference_sample=True)on such a circuit now gives raw
parities, where it gave the reference-relative events. -
A
#in a model's tag.DetectorErrorModelreadserror[t#g](0.1) D0as Stim does,
with the tag whole. Before,len, iteration, indexing,repr,rounded,+,
approx_equalsandget_detector_coordinatesraisedFailed to parse as a stim.DemTarget
on such a model, and sopymatching animaterefused it. -
Indexing is linear. A
CircuitorDetectorErrorModelparses its instructions once and
keeps them until it changes, solen(c)andc[i]in a loop are O(1) each, as Stim's.
Before, each call printed and re-parsed the whole object. Indexing all 1,600 items took
2.1 s for a circuit and 9.5 s for a model; it now takes under 0.01 s. QUITS's
check_overlapping_CXand MQT'scollect_circuit_layersloop this way. -
A stray byte in a file.
Circuit.from_fileandDetectorErrorModel.from_file(and
through themMatching.from_detector_error_model_fileandfrom_stim_circuit_file) read a
byte that is not UTF-8 (caf\xe9in a comment) as U+FFFD, not refusing the file. Stim skips
such bytes in comments. They also take a file opened in binary mode. -
WDIMACS with a certain error.
likeliest_error_sat_problemwrites Stim's problem for
a model with a probability-1 error, where it raisedmath domain error. Stim's weight for
that error is a NaN cast to an integer, so it is the processor's: none on ARM64 (the clause
is left out), 2^63 on x86-64; this package writes this machine's. -
The exact distance.
DetectorErrorModel.distance()(the integer program) leaves out
faults of probability 0, as the graph-like and search distances and Stim do. It reads the
model's own faults, so a tag holding)no longer breaks it. Before, it could prove a
distance below the true one (1 forerror(0) L0beside a distance-2 model), or raise. -
Heavy graph edges.
Matchingbuilt from a networkx or rustworkx graph leaves out an edge
whose weight is past 2^24 − 1, with PyMatching'sUserWarning, and decodes the rest. Before,
it kept the edge, and then every decode raised. The warning's weight is printed as
PyMatching prints it (Weight 20000000.000000 of edge (1, 2) ...), foradd_edgetoo. -
Docstring examples. Every example the API reference publishes runs in
tests/test_docs.py, found fromapi/'s pages and the modules defining their members. The
Matchingexample now printsarray([0, 1, 0], dtype=uint8)and thePauliStringone
stabilizer_qec.PauliString("+i_ZZ"), the answers PyMatching and Stim give. Theanalysis
examples import what they use. -
Deltakit's Stim. Deltakit's
Circuit.from_stim_circuit(andparse_stim_circuit
through it) reads this package's ownstabilizer_qec.Circuit, which a pickled Deltakit
circuit comes back as, where it read an empty circuit and an empty decoding graph without
error. Adeltakit_stimcircuit's diagrams draw Deltakit's instructions (LEAKAGE(0.1),
RL, a herald asHERALD_LEAKAGE_EVENT(0.01):rec[k]on the qubit it reads, a transport
gate's four arguments on both its ends), where they drew the loweredI_ERRORandMPAD
lines, a second qubit's herald on qubit 0;flattened_operationslists("LEAKAGE", [0], 0.1), not("I_ERROR", [0], 0);to_tableauandtime_reversed_for_flowsrefuse
LEAKAGE,RELAX,RLand heralds with deltakit_stim's messages;approx_equalscompares
their probabilities; andexplain_detector_error_model_errorsexplains only Stim's own
noise, as deltakit_stim does, so Deltakit'sparse_explained_demreads it (it raised
TypeError: Unsupported gate type: MPAD). A tag on Deltakit's own instructions
(LEAKAGE[site](0.1) 0, which deltakit_stim anddeltakit.circuit's taggedLeakageand
Relaxwrite) is still refused withValueError: the lowered line's tag is where the
instruction is held. -
Tesseract. An empty answer's cost is 0, as Tesseract's
cost_from_errorsgives it, not
−0. -
ldpc.
- A pickled or copied
BpDecoder,BpOsdDecoder,BpLsdDecoderorBeliefFindDecoder
decoded with its constructor's settings where one had been changed
since (lsd_method,lsd_order,osd_order,serial_schedule_order,
random_schedule_seed,set_do_stats), while it reported the new ones; a copied
LsdDecoderlost itslsd_methodandlsd_order(since 2.1.0). A copy now has them, and
the serial schedule's state as the decodes so far left it (a random schedule's
generator, a relative schedule's order), so it decodes on as the original does. - One decoder decoding from several threads could answer a decode with another thread's
correction, silently (8 threads at a 1 µs switch interval: about 13% ofBpDecoder's
decodes, 4% ofBeliefFindDecoder's; since 2.1.0). Each decode keeps its results on the
decoder and answered from them. Decodes on one decoder of the ldpc family now take turns,
as ldpc's do under the GIL. - On Linux, OSD's column sort could panic ("user-provided comparison function does not
correctly implement a total order"), or order the columns otherwise than glibc'sqsort
(ldpc's there), when BP's posteriors held NaN: product-sum BP gives them on a model with
error(0)orerror(1)faults (since 2.1.0). The sort is glibc's merge sort (msort.c)
now, which takes any comparison.
- A pickled or copied
-
relay_bp.
- A decoder or
ObservableDecoderRunnershared between threads raised "Already borrowed"
when two calls overlapped (since 2.1.0). Calls now wait their turn, as relay_bp's do. - The sinter decoders (
relay-bp,mem-bp,msl-bp) on a model with a fault of
probability 1 predicted wrongly on nearly every shot, silently (logical error rate 0.9985
where 0.0015 is right; since 2.1.0). The default pruning built the biases asnp.matrix
values, and the observables' from the check matrix's rows. Both are flat arrays now, the
observables' from the observables' matrix. relay_bp 0.2.2 raisesTypeErrorthere. - A
uint8CSR or CSC check matrix storing an entry twice was read with that entry set
(1 + 1 taken as 1, not 0), sosuccesscould be True for a correction whose real checks
are not the syndrome (since 2.1.0). The entry is now the matrix's sum, 0 mod 2, as with
any stored values; relay_bp 0.2.2 panics on such a matrix.
- A decoder or
-
PreDecoder. A pickled or copied pre-decoder had its constructor's
threshold(0.5 by
default) where one had been set since, so it applied other faults (about 4 times as many
on the distance-5 memory at threshold 0.95) and decoded some shots otherwise. It also lost
the optimizer's state andsteps, so trained on, it took other steps (since 2.1.0). A
copy now has the threshold and either engine's optimizer state. -
Real-time decoding (since 2.0.0).
realtime.make_decoder(name, circuit=...), and so
stabilizer-qec serve --circuitandDecoderClient.spawn(circuit=...), builds the circuit's
model asstabilizer_qec.sinter.Sampler(name)does: decomposed only for the decoders that
match on a graph, withapproximate_disjoint_errors=True. It decomposed it for
color_matchingandambiguity_clusteringtoo, so a distance-5 colour code could not be
served withcolor_matching(its faults do not split into graph-like pieces) and ambiguity
clustering decoded another model than sinter's (31 of 3,000 shots differed on a d=3 surface
code). ADecoderQueuebatch whose decode raised is collected by raising its error once: it
stayed in the queue, so every laterget_corrections()raised the same error,pending()
never fell, and the decoder server kept each rejectedDECODE's syndromes (200 rejected
1 MiB frames grew it from 62 to 565 MB; it now levels off at 155 MB). ADecoderClient
request interrupted between sending and reading its reply (Ctrl-C) drops the connection, and
later calls raiseConnectionError; the next call of the same size returned the interrupted
batch's predictions.stabilizer-qec serve's default--threads 0reaches the decoder as 0,
every core, as its help says; it was left out, so each batch decoded on one thread. -
qecsim, PanQEC, PECOS and Loom ports.
- qecsim's Blossom V interface numbered its nodes in a set's order. The planar CMWPM and the
rotated planar and rotated toric SMWPM decoders hash their nodes by identity, so the choice
among equal-weight matchings followed memory addresses, and a seededstabilizer-qec qecsim runorrun-ftpgave a different result in each process (rotated_planar(4,4),
rotated_planar.smwpm,-s5: n_fail 113 to 126). The nodes are now numbered in order of
first appearance, so the same seed gives the same result every time. Ties can still be
broken differently from qecsim's NetworkX matching. stabilizer-qec qecsim, and any decode through qecsim'sgraphtools.mwpm, read
logging_qecsim.iniandclib/libpypm.sofrom the working directory first, as qecsim
1.0b9 does. Python evaluates the first file's handler arguments, and the second is loaded as
a library in place of the matcher, so running in a directory someone else supplied ran that
directory's code. Both files are now read from$QECSIM_CFGor~/.qecsimonly, and an
emptyQECSIM_CFG(which qecsim took as the working directory, and which a CIenventry
for an undefined variable gives) counts as unset. A relativeQECSIM_CFGis still read
relative to the working directory, as in qecsim (SECURITY.md).- PanQEC's BP-OSD decoder took each sector's correction from ldpc's
osdw_decoding, which
ldpc 2 leaves holding the previous shot's correction whenever a sector's syndrome is zero. A
decoder used for many shots (DirectSimulation,generate-input's default decoder, and the
X-cube decoder's Z part) therefore reapplied a stale correction that did not explain the
syndrome: 216 of 300 logical failures onToric3DCode(3)at p = 0.01, against 0 now. It
now usesdecode's return value. PanQEC 0.1.7 has the same bug on ldpc 2. Its own
single-error test for this decoder, which was marked as an expected failure, now passes. - PECOS's
SparseSim.refactor(both the default simulator andpySparseSim) ignored the
-1s that come from reordering the generators it multiplies together. In about one call in
five this flipped the new generator's sign, and later measurements then gave outcomes the
state cannot produce. The signed stabilizer group is now unchanged byrefactor. - Loom's
Ekais a frozen dataclass, as Loom's is:Eka.fromdict(e.asdict()) == e, its
fields cannot be assigned (assigningblocksused to skip its lattice and label checks),
and it has Loom's repr.AuxCNOTaccepts exactly two block names, as Loom does. With
three names it used to drop the third without warning, and with one it failed later with
IndexError.
- qecsim's Blossom V interface numbered its nodes in a set's order. The planar CMWPM and the
-
qsample.
- Each
Protocolbuilt without check functions has its own dict. qsample's default was one
dict shared by them all, so a secondNoisyDFTStatePrepSimulatorrebound the first one's
decoder and checks: a |+> simulator's p_L at p = 0.02 went from 0.022 to 0.078 once a |0>
one was built. - Subset probabilities stay finite for any number of fault locations. A group of 1030 or more
raised OverflowError (qsample gives NaN from 171); terms whose coefficient is beyond a
float, or whose power falls below the normal floats (binom(200, 1029, 0.02)was 0, not
3.3e-129), come from Loader's saddle-point method, as R'sdbinom.combgives inf where
the coefficient is beyond a float. save(andDirectSampler.save,SubsetSampler.save) uses dill when it is installed, as
qsample does, so samplers with lambda or nested check functions save; without dill it uses
pickle, which takes only top-level functions. It writes a new file beside the target and
replaces the target only once written whole: a failed save no longer leaves an existing
file at 0 bytes.
- Each
-
MQT QECC. Every
circuit_utilsfunction that takes a circuit, each noise model's
apply,CatStatePreparationExperimentand the NDFT simulators take Stim's own circuits;
they raised AssertionError or TypeError, though 2.1.0 said every port took them. Adding this
package'sCircuitand Stim's with+still raises TypeError: convert with
stabilizer_qec.Circuit(str(c))first. -
qLDPC.
as_noiseless_circuit,get_state_stabilizers,get_logical_state_stabilizers,
get_state_prep_diagnostic_circuit/_tasks,QuditCode.deformed/CSSCode.deformed,
get_logical_tableau,get_encoder_and_decoder,get_pauli_product_measurementsand
get_transversal_circuit(s)take Stim's ownCircuit,TableauandPauliString; they
raised, though 2.1.0 said every port took Stim's objects. Adding this package'sCircuit
and Stim's with+still raises TypeError: convert withstabilizer_qec.Circuit(str(c))
first.CSSCode.reduce_logical_op(s)and the SL, PSL, GL and PGL lifts run on NumPy before 2.1
(qLDPC'snp.reshape(..., copy=False)needs 2.1, itsnp.vstack(..., dtype=int)1.24); the
port's suite passes on NumPy 1.23.5. Theqldpcextra now asks for qLDPC's own NumPy floor
(numpy>=1.24on Python 3.11 and later).
-
qec-util's tools (
stabilizer_qec.analysis).dems.detectors_to_observablesanddem_instrs.detectors_to_observablesapply a mapping
keyed by Stim's ownDemTargets, as qec-util does. Stim's targets neither equal nor hash
like this package's, so such a mapping matched nothing and the model came back unchanged.samplers.merge_batches_in_filekeeps every extra metric. It wrotesecondsinto the
header a second time (as qec-util 0.5.0 does), so the merged row was one value short and,
read back, a metric named aftersecondslost its count toseconds.distance.get_upper_bound_circuit_distancedecodes over dem_decoders' check matrix (errors
flipping the same detectors merged, their probabilities combined), so its bound is
qec-util's (3 against qec-util's 2 on a decomposed d = 3 color code before), and returns
the decoded errors, which flip observables and no detector. It (like qec-util) took the
fault indices as indices of every instruction of the model, so it could return detector
lines or errors that flip detectors.- Without scipy, threshold fits are MINPACK's Levenberg-Marquardt, as scipy's
curve_fit:
they stop where scipy's do (poly-4's threshold was 0.042 of a standard error off, and
tanh took 400,000 evaluations against scipy's 7,000), and report scipy's infinite error,
with its warning, where the fit cannot fix the threshold (poly-9 gave a finite interval,
all-zero failures NaN). - Without lmfit,
LogicalErrorProbDecayModel.fitfits as lmfit does (MINPACK, with lmfit's
options and standard errors) and takes lmfit's arguments:weightsandmax_nfevwere
ignored, as were unknown keywords (lmfit warns), and too few points or NaN data gave a
fit or a LinAlgError where lmfit raises TypeError or ValueError.
-
Azure estimates on degenerate parameters. A crossing prefactor of 0 (or one whose logical
error rate underflows) gives the QDK's estimate: its f64 quotient and saturating cast allow
every cycle. A division by zero (a logical cycle time that rounds to 0 ns, a T factory that
cannot finish a run undermaxTFactories, a patch of 0 physical qubits) and a report that
cannot format the infinite clock frequency of a 0 ns cycle are each aValueError, where the
QDK panics (a batch fails as a whole, as the QDK's call does). 2.1 raised a bare
ZeroDivisionErrororOverflowErroron all of these. -
tqec's engine at a NumPy-integer distance.
compile_block_graph(g, np.int64(3), block_temporal_height=5)andBlockGraph.compileat such a distance ran the default rounds,
dropping the height (and a callable orLinearFunctionone), silently; the distance is now
read as an integer, and the height honoured. -
2.1.0's "Not ported" list said QUITS's distance search used more memory and time than
Stim's: 456 MB and 8.7 s against 139 MB and 1.6 s on [[72, 8, 8]]. That was the search before
it took Stim's steps (f3dc7ac, which is in 2.1.0). 2.1.0's own search used a 228 to 241 MB
peak footprint and 0.79 to 0.81 s of user time on that circuit, against Stim's 530 MB and
4.25 to 4.54 s (PYTHONHASHSEED 0, the whole process, three runs each).
Not ported
These remain, each in the README and the inventory:
- Loom's if/else circuits (
IfElseCircuitand itsBoolOpconditions). The file that
defines them in el-loom 0.4.0,src/loom/eka/ifelse_circuit.py, alone among Loom's files
carries a notice that prohibits its use, distribution and reproduction without Entropica Labs'
express written consent, in place of the Apache-2.0 header. A port, and fixtures recorded by
running it, need that consent. - tqec's Y cubes. tqec 0.2.0 has none: it raises
NotImplementedError("Y cube is not implemented."), and so does this package. - Tsim's shots, bit for bit. Tsim draws its noise with numpy's PCG64, channel by simplified
channel, and its outcomes with JAX's threefry, from probabilities whose last bits follow
pyzx_param's rewrite order and XLA's complex64 sums; Tsim itself is reproducible only at a
fixedbatch_size. The shots here are this package's, equal in distribution. - The QDK's live widgets. The space and scatter charts are drawn as static SVG from
qsharp_widgets' own functions, not by the widget bundle: no watermark, the overview table
static (its first five columns, no sorting or selection), and a batch's failed items left out
of the scatter. - CUDA-Q QEC's TensorRT engines, and
dispatchandtransport.trt_decoderruns the ONNX
model on the CPU and refusesengine_load_pathandengine_save_pathby name;dispatchand
transportare kept and written, and ignored on the host, as CUDA-Q's host library ignores
them. - PyMatching's ties outside its API. Correlated matching (
enable_correlations=True),
BeliefMatching,WindowMatchingandstream_memory,LeakageAwareDecoder,
ArgumentReweightingandPreDecoder(unlessreweight=False) withdecoder="matching",
their default, and the Rust crate'sMatchingdecode on this package's own sparse blossom
(src/sparse), whose answer among equally light matchings can differ from PyMatching's (the
README lists what decodes on which). - Chromobius tied lifting. Chromobius walks a table in
std::unordered_maporder, which is the
C++ library's own: with libstdc++ it may drag charge another way on some shots. Off macOS
the sweep requires 99% agreement and some facade cases require 95%; equal-weight lifting can
differ on macOS too. - The coherent sampler past its budget, and the joint sampler past twelve generators, which
truncate the class sums as 2.1 did. The sweep's d ≥ 5 rows and the crosstalk's d = 5 and 7
rows keep 1.8.0's truncated measurements as pre-correction archive data, not current validation.