RustQEC 0.3 · Development

1. Create a standalone project

This example uses published rstim and rmatching crates. Their interfaces are pinned to 0.3.0. Download the project with Cargo.lock, or create these files:

mkdir rustqec-experiment
cd rustqec-experiment
mkdir src

Save as Cargo.toml:

[workspace]

[package]
name = "rustqec-doc-experiment"
version = "0.1.0"
edition = "2024"
publish = false

[dependencies]
rand = "0.8"
rmatching = "=0.3.0"
rstim = { version = "=0.3.0", default-features = false }

Library defaults are disabled for rstim because this example needs neither CLI parsing, viewer, CSS generation nor a native solver. See the API index for the complete crate and feature boundaries.

2. Simulate, decode and verify

Save as src/main.rs. The first experiment checks Bell parity. The second introduces one possible X error that flips both D0 and L0, so its matching prediction can be checked against the held-out observable.

use rand::{SeedableRng, rngs::StdRng};
use rmatching::Matching;
use rstim::{error_analyzer::ErrorAnalyzer, parser::parse_lines, sampler::sample_batch};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut rng = StdRng::seed_from_u64(0x5eed);

    // Parsing and seeded sampling work from an ordinary downstream crate.
    let bell = parse_lines("H 0\nCNOT 0 1\nM 0 1")?;
    let bell_samples = sample_batch(&bell, 32, &mut rng)?;
    for shot in 0..32 {
        assert_eq!(
            bell_samples.measurements.get(0, shot),
            bell_samples.measurements.get(1, shot),
            "Bell measurements must have even parity in shot {shot}"
        );
    }

    // This error has the same detector and observable symptom. A fixed seed
    // makes the example reproducible while still exercising both outcomes.
    let circuit = parse_lines(
        "R 0\n\
         X_ERROR(0.25) 0\n\
         M 0\n\
         DETECTOR rec[-1]\n\
         OBSERVABLE_INCLUDE(0) rec[-1]",
    )?;
    let dem = ErrorAnalyzer::circuit_to_dem_decomposed(&circuit)?;
    assert_eq!(dem.num_detectors(), 1);
    assert_eq!(dem.num_observables(), 1);

    let mut matching = Matching::from_dem(&dem.to_string())?;
    let samples = sample_batch(&circuit, 128, &mut rng)?;
    let mut observed_zero = false;
    let mut observed_one = false;
    for shot in 0..128 {
        let syndrome: Vec<u8> = (0..samples.detections.num_major())
            .map(|detector| u8::from(samples.detections.get(detector, shot)))
            .collect();
        let prediction = matching.decode(&syndrome);
        let actual = u8::from(samples.observable_flips.get(0, shot));

        assert_eq!(syndrome, vec![actual], "D0 and L0 differ in shot {shot}");
        assert_eq!(prediction, vec![actual], "decoder mismatch in shot {shot}");
        observed_zero |= actual == 0;
        observed_one |= actual == 1;
    }
    assert!(
        observed_zero && observed_one,
        "seeded run must exercise both outcomes"
    );

    println!("validated Bell parity and 128/128 observable predictions");
    Ok(())
}

sample_batch stores matrices with detector/measurement index first and shot index second. Pass only detections to the decoder; observable_flips is the scoring truth. Extract the DEM from the same circuit that produced the shots.

3. Run and interpret

cargo run --quiet
validated Bell parity and 128/128 observable predictions

The seed exercises both error and no-error outcomes. Perfect predictions are expected for this deliberately simple one-mechanism model; they do not imply a zero logical error rate for a surface code. Use cargo run --quiet --locked with the downloaded project to reproduce its dependency resolution.

For your own experiment, replace the circuit, regenerate its DEM, check model dimensions and retain the observable labels for scoring. Matching rejects non-graphlike components; use another decoder when its assumptions do not hold. Under loss, use the loss-visible workflow instead of interpreting placeholder parities as ordinary detectors.