Skip to content

stabilizer-qec 2.2.0

Latest

Choose a tag to compare

@Jaspersands Jaspersands released this 30 Oct 05:16
· 383 commits to master since this release

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_weights is False), and the sampler
    now says so with a UserWarning, 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 is 24*(arena+largest) bytes before factors/overhead
    (arena=entries/2, at most 36*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 with max_contraction=0 too. 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, which exact_weights=False reports without a warning: its cross terms
    are overlaps of the exact engine's states, not products of factors, so max_contraction has
    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 from coherent.exact and
    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_blossom line 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's pymatching decoder), decodes
    on it: decode, decode_batch, decode_to_edges_array and decode_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_batch with
    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's compute_wrapped_radius returns int32_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 a replace or smallest-weight merge 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's 1 << id is 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 animate and pymatching.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_in is 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_dir it
    cannot make, a hit index past 64 bits), stabilizer-qec animate prints stabilizer-qec animate: <message> and exits 1, and pymatching.cli raises 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_exact in 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 of tools/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|help is 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 --circuit that 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; predict decodes 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_decoder is Tesseract's tesseract program: 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-out and the --stats-out JSON. 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-out equal, 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-out holds
    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-shots draws this package's shots, not Stim's; an exception that
    ends Tesseract's main prints the same message and exits 134 without a core dump; an --out
    it cannot open stops with Failed to open <path> (Tesseract's program writes through a null
    file); --max-errors stops exactly at the limit (Tesseract's program keeps the shots its
    threads had already decoded, so its num_errors can pass it); and --threads shares one
    decoder (and runs on one thread with --verbose or --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
    SearchDecoder stays at 64, since its Prediction.observables is a u64, 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 raises Unrecognized instruction name: foo). The exception is a model fault that Stim's parser throws std::out_of_range for:
    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
    and compile packages 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 its generate_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_path is taken and never read or
    written); there are no worker processes (the output is the same); k must be a positive
    integer (tqec raises IndexError for 0 and writes a circuit for 1.5); and a
    block_temporal_height that is not a LinearFunction raises 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"]) and cz(["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, requires fixed_boundary;
    fixed_bulk lacks 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 from compile_block_graph(graph, distance) and BlockGraph.compile
    remains available at .circuit; generate_stim_circuit writes 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's GraphS where it imports and on this package's pure-Python
    ZXGraph otherwise (tensor contraction uses NumPy). diagram("pyzx"), "pyzx-dets" and "pyzx-meas" draw them, and
    to_matrix and to_tensor of 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
    random to_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 a stim.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's TPPs on several
    pairs and an undaggered pair rotation on several pairs (the inverse of an SPP_DAG one
    prints R_ZZ(a) 3 2 then R_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 as R_PAULI(0.3) X0*X0 (Tsim's R_XX(0.3) 0 0 does 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 0 then DETECTOR rec[-2]) raises MeasurementRecordError, an
    IndexError as Tsim's is and a ValueError naming the instruction. to_matrix of a circuit
    that does not need the ZX tensor is the unitary engine's, Tsim's up to a global phase
    (SQRT_X_DAG 0 gives 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_matrix refuses 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_chunks and trt_decoder. CUDA-Q QEC 0.8.1's extended detector error
    models (SeamId, ExtendedDem, DemChunksSpec and every other name of py_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_chunks is read from and written to
    CUDA-Q's YAML as CUDA-Q writes it, and expand_dem_chunks closes the chunks into the model the
    decoder gets; create_realtime_decoder expands them. Errors are CUDA-Q's types and messages.
    All 101 tests of CUDA-Q's test_extended_dem.cpp and 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_decoder runs 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
    sparse O inputs 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 EstimatorParams and its
    sections (EstimatorQubitParams, EstimatorQecScheme, DistillationUnitSpecification,
    ErrorBudgetPartition, EstimatorConstraints, ...), the QDK's code for Python 3.9, are taken
    by LogicalCounts.estimate and azure_estimate, and raise the QDK's own errors: a
    LookupError for a custom qubit's or unit's missing field, a name given with a custom unit, or
    both maximum constraints at once, and a bare Exception from EstimatorParams().items without
    num_items. EstimatorResult draws the QDK's report: _repr_html_, summary and slices as
    text with their HTML, diagram, a batch's plot, 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_CODE and FLOQUET_CODE are in. Kept from 2.1: EstimatorError
    and QSharpError are ValueErrors (the QDK's EstimatorError derives from BaseException
    alone), and LogicalCounts.estimate takes any object with as_dict (a batch when it
    has_items, else its one dictionary; the QDK refuses an EstimatorInputParamsItem with a
    TypeError). A batch keeps its summary and single results their summary_data_frame, which
    the QDK lacks, and the tables are drawn when first asked for.
  • Qualtran's Dash app. stabilizer_qec.estimate.qualtran.ui has Qualtran 0.7.0's layout
    builders and callback. ui.make_app() (and ui.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()
    and python -m stabilizer_qec.estimate.qualtran.ui run it, on Dash where dash is installed
    (the new dash extra, 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_app converts 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
    ui imports neither dash nor plotly (Qualtran's ui does not import without dash). Without
    dash, ui.app raises make_app's ImportError, and so does hasattr(ui, "app"), where
    hasattr gives 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, and helper.serve is
    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.js after taking out the one
    change made to it: the page imports a patch_url only 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_visualizer and
    peek_code serve the working directory by default). Raw live parity also has two platform limits:
    GET/HEAD //index.js keeps 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 modules bottle and
    helper (so import bottle gives 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), on Circuit and
    DetectorErrorModel (in Rust, the *_with_budget methods), 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 from decode's order first, and returns the corrections they reach. On the 15
    windows of QUITS's sweep where ldpc's answer varied (recorded in tests/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, and tools/decoder_check.py counts 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). decode is unchanged. A random or relative serial schedule moves on
    at every decode; the search after decode(syndrome) runs that decode's BP again, from the
    schedule's state it started from, so decode'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 raises RuntimeError asking 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, and generate_stim_circuit on a program whose rounds tqec'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's helper.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 fits max_contraction, so its
    weights and their spread can differ from 2.1's truncated ones. max_contraction=0 keeps
    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 error stops
    there. exact_weights is True for a completed contracted kernel, and contraction_width is new.
  • The coherent data. Newly constructed rows record actual version and the explicit
    generation revision above. coherent.exact describes whole-class coverage independently.
    Saving historical rows never upgrades their provenance. Repeated --only selectors 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_model and from_stim_circuit keep each fault's
    detectors in the order written, as PyMatching does, where 2.1 sorted them. A Matching
    loaded with enable_correlations=True is 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), its edges() 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 ^ D1 pieces). ensure_num_fault_ids keeps 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_array or 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.", and decode_to_edges_array with
    enable_correlations=True checks the syndrome first. Rust: UserDecoder's 2.1 decoding
    methods (scratch, decode, decode_to_edges, decode_to_matched_pairs, the four-argument
    decode_packed, and Decoded) are deprecated, in favour of decode_with,
    decode_to_edges_with, decode_to_matched_pairs_with and decode_packed_with, each taking a
    workspace from mwpm().
  • 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 D0 among 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 by ColorMatching.decode_batch with 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 its generate_stim_circuit are 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 a LinearFunction of k): for a number, the one tqec raises for
      that constant height, and for a callable, one saying the height must be a LinearFunction.
      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 (on compile_block_graph(g, distance)) wrote
      the engine's layout at distance 2k + 1 with the program's rounds and merged. 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_positives inherits tqec's single-pass-iterator caveat
      (unlike 2.1): candidate checks can consume it and yield an incorrect maximum; pass a list.
    • database_path (on generate_stim_circuit, generate_stim_circuits_with_detectors,
      generate_sinter_tasks and start_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) and R_XX, R_YY and R_ZZ(±0.25) print as written and are
    Tsim's R_PAULI: its graph's scalar, its matrix through the ZX path and its errors. 2.1
    stored and printed them as TPP. An angle is kept as written: 2.1 printed back the shortest
    decimal that gave the stored radians, so R_Z(1.9967044602602857) came back as
    1.996704460260286. inverse keeps the literals too (a zero as 0.0, as Tsim's). tcount is
    Tsim's, the graph's count, in which a coherent error is not a gate
    (stabilizer_qec.Circuit.tcount still 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] 0 prints T 0). A circuit with Pauli products or
    sweep bits has its ZX graph built before to_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.args is the QDK's (code, reason), where it was (message,),
    and its repr follows (EstimatorError('<code>', '<reason>')); str(e), .message, .code,
    .reason and pickling are unchanged (read e.message where code read e.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].summary has it). A failed result's
    summary raises an error that is both 2.1's EstimatorError, with the estimate's code,
    reason and message, and the QDK's AttributeError: hasattr(failed, "summary") is False and
    getattr(failed, "summary", d) gives d, where 2.1 raised from both, and its str() is the
    QDK's "'EstimatorResult' object has no attribute 'summary'" (read e.message for the
    estimate's). summary and result[i:j] are Reports, a str subclass whose
    _repr_html_ holds the HTML.
  • Fusion Blossom. helper.serve is fusion_blossom's Bottle server, so it answers as
    fusion_blossom's does where 2.1's standard-library server answered otherwise: icon.svg is
    served as text/html; charset=UTF-8, /data/../x is 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 after srv.shutdown(); a later serve call changes the
    data folder of servers already running. Binding happens when the runner starts;
    server_close() releases the socket after shutdown. open_visualizer propagates 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_blossom now loads
    Bottle and appends Bottle's import hook to sys.meta_path, as
    import fusion_blossom does. import stabilizer_qec and import stabilizer_qec.sinter load
    neither, as import sinter does 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_chunks with stim_dem_path is 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 pymatching decodes to the observables when its model has them, as CUDA-Q
    0.8.1's plugin does: given O, 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_strategy says 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, an O or error_rate_vec the caller gives replaces the text's, as CUDA-Q's
    make_pcm_decoder lets the caller's keys win (2.1 refused them), and a wrongly shaped O gets
    CUDA-Q's message ("O must be of shape (num_observables, block_size); ..."). CUDA-Q's two
    PyMatching decoding tests of test_dem.py are ported.
  • Stim's shot writer, and sinter's predictions. write_shot_data_file takes 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, so sinter.predict_observables(dets=) with 0/1
    uint8 detection events, or predict_observables_bit_packed with 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's PanicException ("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_html and the
    command line's --visualizer-filename gzip 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, predict and count_mistakes read, sample, decode and write 1,024 shots at a
    time, as Stim's command line does, so their memory no longer grows with --shots or the
    input's length (2.1 held every shot, about 10 bytes per output bit in 01). The bytes are
    those of one pass: seeded samples are the samplers' own, and decode --decoder relay_bp
    seeds each batch as the whole run's. A ptb64 output whose --shots is not a multiple of 64
    is refused before anything is written. Bad data, or m2d sweep data that ends early, is
    refused when its batch is reached, after the batches before it are written.
  • count_mistakes is PyMatching's. --time writes its two lines to stderr in PyMatching's
    format (Decoding time per shot: infus for no shots); 2.1 wrote them to stdout after the
    count. --obs_in is 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_in shots are
    not read, and too few is PyMatching's Obs 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.
  • diagram draws a detector error model for every match graph type (matchgraph-3d,
    matchgraph-3d-html and the match-graph-* names, as well as matchgraph-svg); 2.1 parsed
    a model as a circuit there and failed. --remove_noise with 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 every stabilizer-qec command, 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 by tools/third_party_crates.py), and THIRD_PARTY_NOTICES.md quotes 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 in estimate/LICENSE-qdk. CITATION.cff gives license: MIT and 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.2 or qldpc==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_matrix gave the wrong shape for noise before a reset (Z_ERROR(0.1) 1 then R 1
      gave a 2×1 matrix) and for II (II 0 1 gave 1×1), and, in a circuit without
      measurements, a factor of 2 too few for each target-less DETECTOR and each of
      OBSERVABLE_INCLUDE(k)'s k + 1 observables (since 2.1.0); OBSERVABLE_INCLUDE(0) X0 gave
      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*Z1 with noise on qubit 1), was dropped from to_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,) and I[T()] gave a wrong detector error
      model silently and broke to_matrix, get_graph, tcount and 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's SPP_DAG[...] text), in fusion with neighbours and in R_PAULI's
      limits and messages. A native II[R_ZZ(theta=pi/8)] prints as TPP Z0*Z1; it printed
      R_ZZ(0.25) 0 1, which reads back as R_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)] ..., and R_XX, R_YY) or of
      II[R_PAULI(...)] is one instruction, printed as Tsim's inverse:
      c.inverse().inverse() == c, and len(c.inverse()) is Tsim's.
    • to_matrix raises Tsim's errors for an R_PAULI naming a qubit twice or more than 64
      qubits, an anti-Hermitian product and sweep bits, and is Tsim's for TPP Z0*Z0 ([[1]]) and
      for products naming a qubit twice. SPP[T] X0 * Y1 no longer raises IndexError.
    • CompiledStateProbs conditions on every record bit (since 2.1.0). Before, the
      stabilizer-rank engine ignored MPAD bits 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 0 gave 1 for both outcomes). An MPAD flip 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's RX, 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_sampler and CompiledStateProbs refuse what Tsim's
      graph builder refuses, with Tsim's messages, as to_matrix and tcount already did: an
      anti-Hermitian product (TPP Y0*X0), an R_PAULI that names a qubit twice or more than 64
      qubits (33 R_ZZ lines in a row), and sweep bits. Before, they sampled such circuits. A
      record entry from before the first measurement raises MeasurementRecordError (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 bare KeyError), 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 with KeyError or "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, and SPP[ R_PAULI(...)] X0 is Tsim's SPP. 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() of II[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 1 came out as I[T] 0 1 twice (S on each qubit) and
    I[R_Z(theta=0.1)] 0 1 2 three times, silently (also I[T] 0 and I[T] 1 on separate lines,
    which the parser fuses, and a tagged channel such as I_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_flows reversed a tagged I/II gate's
    targets but kept its tag, so I[R_PAULI(theta=π/4, pauli=XZ)] 0 1 became ... pauli=XZ)] 1 0
    (the letters on other qubits) and I[T], rotation angles and U3 were not inverted. The tag is
    now inverted as Circuit.inverse() inverts it (I[R_PAULI(theta=-π/4, pauli=ZX)] 1 0,
    I[T_DAG]); a coherent error is negated as inverse() 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's order already did.

  • Copies keep their class. pickle, copy and deepcopy rebuild a Circuit,
    DetectorErrorModel, PauliString, Tableau or Matching as 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
    Matching subclass 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 to RunAllAnalysisEngine'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
    and count_determined_measurements read a tagged gate at a Clifford angle
    (I[R_X(theta=π/2)] 0) as the Clifford gate it is, as reference_sample and 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. DetectorErrorModel reads error[t#g](0.1) D0 as Stim does,
    with the tag whole. Before, len, iteration, indexing, repr, rounded, +,
    approx_equals and get_detector_coordinates raised Failed to parse as a stim.DemTarget
    on such a model, and so pymatching animate refused it.

  • Indexing is linear. A Circuit or DetectorErrorModel parses its instructions once and
    keeps them until it changes, so len(c) and c[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_CX and MQT's collect_circuit_layers loop this way.

  • A stray byte in a file. Circuit.from_file and DetectorErrorModel.from_file (and
    through them Matching.from_detector_error_model_file and from_stim_circuit_file) read a
    byte that is not UTF-8 (caf\xe9 in 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_problem writes Stim's problem for
    a model with a probability-1 error, where it raised math 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 for error(0) L0 beside a distance-2 model), or raise.

  • Heavy graph edges. Matching built from a networkx or rustworkx graph leaves out an edge
    whose weight is past 2^24 − 1, with PyMatching's UserWarning, 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) ...), for add_edge too.

  • Docstring examples. Every example the API reference publishes runs in
    tests/test_docs.py, found from api/'s pages and the modules defining their members. The
    Matching example now prints array([0, 1, 0], dtype=uint8) and the PauliString one
    stabilizer_qec.PauliString("+i_ZZ"), the answers PyMatching and Stim give. The analysis
    examples import what they use.

  • Deltakit's Stim. Deltakit's Circuit.from_stim_circuit (and parse_stim_circuit
    through it) reads this package's own stabilizer_qec.Circuit, which a pickled Deltakit
    circuit comes back as, where it read an empty circuit and an empty decoding graph without
    error. A deltakit_stim circuit's diagrams draw Deltakit's instructions (LEAKAGE(0.1),
    RL, a herald as HERALD_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 lowered I_ERROR and MPAD
    lines, a second qubit's herald on qubit 0; flattened_operations lists ("LEAKAGE", [0], 0.1), not ("I_ERROR", [0], 0); to_tableau and time_reversed_for_flows refuse
    LEAKAGE, RELAX, RL and heralds with deltakit_stim's messages; approx_equals compares
    their probabilities; and explain_detector_error_model_errors explains only Stim's own
    noise, as deltakit_stim does, so Deltakit's parse_explained_dem reads it (it raised
    TypeError: Unsupported gate type: MPAD). A tag on Deltakit's own instructions
    (LEAKAGE[site](0.1) 0, which deltakit_stim and deltakit.circuit's tagged Leakage and
    Relax write) is still refused with ValueError: the lowered line's tag is where the
    instruction is held.

  • Tesseract. An empty answer's cost is 0, as Tesseract's cost_from_errors gives it, not
    −0.

  • ldpc.

    • A pickled or copied BpDecoder, BpOsdDecoder, BpLsdDecoder or BeliefFindDecoder
      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
      LsdDecoder lost its lsd_method and lsd_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% of BpDecoder's
      decodes, 4% of BeliefFindDecoder'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's qsort
      (ldpc's there), when BP's posteriors held NaN: product-sum BP gives them on a model with
      error(0) or error(1) faults (since 2.1.0). The sort is glibc's merge sort (msort.c)
      now, which takes any comparison.
  • relay_bp.

    • A decoder or ObservableDecoderRunner shared 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 as np.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 raises TypeError there.
    • A uint8 CSR or CSC check matrix storing an entry twice was read with that entry set
      (1 + 1 taken as 1, not 0), so success could 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.
  • 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 and steps, 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 --circuit and DecoderClient.spawn(circuit=...), builds the circuit's
    model as stabilizer_qec.sinter.Sampler(name) does: decomposed only for the decoders that
    match on a graph, with approximate_disjoint_errors=True. It decomposed it for
    color_matching and ambiguity_clustering too, so a distance-5 colour code could not be
    served with color_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). A DecoderQueue batch whose decode raised is collected by raising its error once: it
    stayed in the queue, so every later get_corrections() raised the same error, pending()
    never fell, and the decoder server kept each rejected DECODE's syndromes (200 rejected
    1 MiB frames grew it from 62 to 565 MB; it now levels off at 155 MB). A DecoderClient
    request interrupted between sending and reading its reply (Ctrl-C) drops the connection, and
    later calls raise ConnectionError; the next call of the same size returned the interrupted
    batch's predictions. stabilizer-qec serve's default --threads 0 reaches 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 seeded stabilizer-qec qecsim run or run-ftp gave 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's graphtools.mwpm, read
      logging_qecsim.ini and clib/libpypm.so from 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_CFG or ~/.qecsim only, and an
      empty QECSIM_CFG (which qecsim took as the working directory, and which a CI env entry
      for an undefined variable gives) counts as unset. A relative QECSIM_CFG is 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 on Toric3DCode(3) at p = 0.01, against 0 now. It
      now uses decode'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 and pySparseSim) 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 by refactor.
    • Loom's Eka is a frozen dataclass, as Loom's is: Eka.fromdict(e.asdict()) == e, its
      fields cannot be assigned (assigning blocks used to skip its lattice and label checks),
      and it has Loom's repr. AuxCNOT accepts 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.
  • qsample.

    • Each Protocol built without check functions has its own dict. qsample's default was one
      dict shared by them all, so a second NoisyDFTStatePrepSimulator rebound 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's dbinom. comb gives inf where
      the coefficient is beyond a float.
    • save (and DirectSampler.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.
  • MQT QECC. Every circuit_utils function that takes a circuit, each noise model's
    apply, CatStatePreparationExperiment and 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's Circuit and 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_measurements and
      get_transversal_circuit(s) take Stim's own Circuit, Tableau and PauliString; they
      raised, though 2.1.0 said every port took Stim's objects. Adding this package's Circuit
      and Stim's with + still raises TypeError: convert with stabilizer_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's np.reshape(..., copy=False) needs 2.1, its np.vstack(..., dtype=int) 1.24); the
      port's suite passes on NumPy 1.23.5. The qldpc extra now asks for qLDPC's own NumPy floor
      (numpy>=1.24 on Python 3.11 and later).
  • qec-util's tools (stabilizer_qec.analysis).

    • dems.detectors_to_observables and dem_instrs.detectors_to_observables apply a mapping
      keyed by Stim's own DemTargets, 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_file keeps every extra metric. It wrote seconds into 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 after seconds lost its count to seconds.
    • distance.get_upper_bound_circuit_distance decodes 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.fit fits as lmfit does (MINPACK, with lmfit's
      options and standard errors) and takes lmfit's arguments: weights and max_nfev were
      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 under maxTFactories, a patch of 0 physical qubits) and a report that
    cannot format the infinite clock frequency of a 0 ns cycle are each a ValueError, where the
    QDK panics (a batch fails as a whole, as the QDK's call does). 2.1 raised a bare
    ZeroDivisionError or OverflowError on all of these.

  • tqec's engine at a NumPy-integer distance. compile_block_graph(g, np.int64(3), block_temporal_height=5) and BlockGraph.compile at such a distance ran the default rounds,
    dropping the height (and a callable or LinearFunction one), 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 (IfElseCircuit and its BoolOp conditions). 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
    fixed batch_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 dispatch and transport. trt_decoder runs the ONNX
    model on the CPU and refuses engine_load_path and engine_save_path by name; dispatch and
    transport are 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, WindowMatching and stream_memory, LeakageAwareDecoder,
    ArgumentReweighting and PreDecoder (unless reweight=False) with decoder="matching",
    their default, and the Rust crate's Matching decode 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_map order, 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.