Noise modes

Rather than requiring you to construct a maestro.NoiseModel by hand, MaestroBackend inspects the noise already embedded in a Stim or Deltakit circuit, separates gate noise from idle/reset/readout noise when needed, and automatically selects Maestro’s Stabilizer or Matrix Product State simulator.

At a glance

  • Guides: Noise · Simulation types

  • Key APIs: MaestroBackend, noise_params_from_stim(), strip_gate_noise(), config_for(), as_sinter_task()

  • Run locally: python examples/example_noise_modes.py (~2 s)

Setup: two noisy circuits

The script compares a Deltakit-compiled RotatedPlanarCode(3, 3) circuit under SI1000Noise(p=0.01) against a Stim-generated surface_code:rotated_memory_z circuit with explicit gate, idle, reset, and measurement error rates.

def si1000_circuit(p: float = 0.01) -> stim.Circuit:
    """Build a Deltakit distance-3 rotated surface code memory circuit with SI1000 noise.

    SI1000 represents a superconducting architecture noise model with 1-qubit gate
    depolarization, 2-qubit gate depolarization, readout errors, and idle noise.
    """
    code = RotatedPlanarCode(width=3, height=3)
    circuit = css_code_memory_circuit(code, num_rounds=3, logical_basis=PauliBasis.Z)
    noisy_qpu = QPU(circuit.qubits, noise_model=SI1000Noise(p=p))
    compiled_circuit = noisy_qpu.compile_and_add_noise_to_circuit(circuit)
    return to_stim(compiled_circuit)


def stim_circuit() -> stim.Circuit:
    """Build a standard Stim distance-3 rotated surface code circuit.

    Uses distinct, explicit error probabilities for each noise source:
    gate noise, before-round idle noise, reset flip probability, and measurement flip.
    """
    return stim.Circuit.generated(
        "surface_code:rotated_memory_z",
        distance=3,
        rounds=3,
        after_clifford_depolarization=0.01,
        before_round_data_depolarization=0.02,
        after_reset_flip_probability=0.03,
        before_measure_flip_probability=0.04,
    )


Step 1: Separating gate noise from circuit noise

When overriding gate rates or switching to coherent noise, MaestroBackend calls noise_params_from_stim() to read the circuit’s gate and readout rates, and strip_gate_noise() to remove after-gate depolarizing channels so Maestro’s NoiseModel can apply the replacement without double-counting. Idle, reset, and readout channels remain inside the circuit.

def run_section_1_noise_extraction(circuits: dict[str, stim.Circuit]):
    """Demonstrate how MaestroBackend separates gate noise from circuit noise."""
    print(f"\n{'─' * 78}")
    print("  1. Gate noise is split from idle, reset and readout noise")
    print(f"{'─' * 78}")
    print("  Stim circuits embed noise directly inside instructions. When Maestro")
    print("  replaces or modifies gate noise (e.g. for parameter sweeps or coherent")
    print("  errors), it strips the gate noise from the circuit and handles it via")
    print("  Maestro's NoiseModel. Idle, reset, and readout errors stay in the circuit.\n")

    for name, circuit in circuits.items():
        extracted_rates = noise_params_from_stim(circuit)
        original_noise = noise_summary(circuit)
        stripped_circuit = strip_gate_noise(circuit)
        stripped_noise = noise_summary(stripped_circuit)

        print(f"  {name}")
        print(f"    rates:     {extracted_rates}")
        print(f"    circuit:   {original_noise}")
        print(f"    stripped:  {stripped_noise}\n")

Output:

  Deltakit SI1000(p=0.01)
    rates:     {'p_1q': 0.001, 'p_2q': 0.01, 'readout_error': 0.05}
    circuit:   DEPOLARIZE1(0.001)×215  DEPOLARIZE1(0.02)×36  DEPOLARIZE2(0.01)×144  M(0.05)×33  X_ERROR(0.02)×33
    stripped:  DEPOLARIZE1(0.001)×183  DEPOLARIZE1(0.02)×36  M(0.05)×33  X_ERROR(0.02)×33

  Stim-generated
    rates:     {'p_1q': 0.01, 'p_2q': 0.01, 'readout_error': 0.0}
    circuit:   DEPOLARIZE1(0.01)×24  DEPOLARIZE1(0.02)×27  DEPOLARIZE2(0.01)×144  X_ERROR(0.03)×41  X_ERROR(0.04)×33
    stripped:  DEPOLARIZE1(0.02)×27  X_ERROR(0.03)×41  X_ERROR(0.04)×33

Notice why DEPOLARIZE1(0.02)×36 stays in the Deltakit circuit and DEPOLARIZE1(0.02)×27 stays in the Stim circuit: Deltakit merges idle noise with gate noise on a per-qubit basis, while Stim’s before_round_data_depolarization follows a CX rather than a single-qubit gate, so both are preserved as non-gate noise.

Step 2: Exploring the four noise_type modes and overrides

MaestroBackend supports four noise_type values ('pauli', 'coherent', 'full', and 'none') plus optional keyword overrides (p_1q, p_2q, readout_error, t1, coherent). Inspecting as_sinter_task() and config_for() reveals the exact circuit, NoiseModel layers, simulator, and decoder DEM produced for each configuration:

for scenario in scenarios:
    task = backend.as_sinter_task(
        si1000_circuit_instance, scenario.noise_type, **scenario.noise_kwargs
    )
    noise_layers = {
        k: v
        for k, v in task.json_metadata["noise"].items()
        if k not in ("noise_type", "stim_noise")
    }
    simulation_type = backend.config_for(
        scenario.noise_type, **scenario.noise_kwargs
    ).simulation_type
    simulator = f"Maestro {simulation_type.name}"
    dem_errors = task.detector_error_model.num_errors if task.detector_error_model else 0

Output across all eight configurations:

  pauli
    circuit simulated:     DEPOLARIZE1(0.001)×215  DEPOLARIZE1(0.02)×36  DEPOLARIZE2(0.01)×144  M(0.05)×33  X_ERROR(0.02)×33
    NoiseModel layers:     (none)
    simulator:             Maestro Stabilizer
    decoder DEM errors:    290

  pauli, {'p_2q': 0.02}
    circuit simulated:     DEPOLARIZE1(0.001)×183  DEPOLARIZE1(0.02)×36  M(0.05)×33  X_ERROR(0.02)×33
    NoiseModel layers:     {'p_1q': 0.001, 'p_2q': 0.02}
    simulator:             Maestro Stabilizer
    decoder DEM errors:    290

  pauli, {'readout_error': 0.1}
    circuit simulated:     DEPOLARIZE1(0.001)×183  DEPOLARIZE1(0.02)×36  X_ERROR(0.02)×33
    NoiseModel layers:     {'p_1q': 0.001, 'p_2q': 0.01, 'readout_error': 0.1}
    simulator:             Maestro Stabilizer
    decoder DEM errors:    290

  pauli, {'t1': 0.002}
    circuit simulated:     DEPOLARIZE1(0.001)×215  DEPOLARIZE1(0.02)×36  DEPOLARIZE2(0.01)×144  M(0.05)×33  X_ERROR(0.02)×33
    NoiseModel layers:     {'t1': 0.002}
    simulator:             Maestro Stabilizer
    decoder DEM errors:    307

  coherent
    circuit simulated:     DEPOLARIZE1(0.001)×183  DEPOLARIZE1(0.02)×36  M(0.05)×33  X_ERROR(0.02)×33
    NoiseModel layers:     {'p_1q': 0.001, 'p_2q': 0.01}
    simulator:             Maestro MatrixProductState
    decoder DEM errors:    159

  coherent, {'coherent': 0.005}
    circuit simulated:     DEPOLARIZE1(0.001)×183  DEPOLARIZE1(0.02)×36  M(0.05)×33  X_ERROR(0.02)×33
    NoiseModel layers:     {'p_1q': 0.001, 'p_2q': 0.01, 'coherent': 0.005}
    simulator:             Maestro MatrixProductState
    decoder DEM errors:    159

  full
    circuit simulated:     DEPOLARIZE1(0.001)×215  DEPOLARIZE1(0.02)×36  DEPOLARIZE2(0.01)×144  M(0.05)×33  X_ERROR(0.02)×33
    NoiseModel layers:     {'coherent': 0.01}
    simulator:             Maestro MatrixProductState
    decoder DEM errors:    294

  none
    circuit simulated:     (none)
    NoiseModel layers:     (none)
    simulator:             Maestro Stabilizer
    decoder DEM errors:    290

Key routing behavior visible in the output:

  • pauli (default): runs the circuit as-is on Maestro Stabilizer (Pauli frames). Overriding p_2q or readout_error strips the matching instructions from the circuit and supplies them via NoiseModel. Adding t1 keeps everything on Maestro Stabilizer, simulated shot by shot.

  • coherent / full: automatically routes to Maestro MatrixProductState. coherent replaces gate depolarizing with unitary over-rotations; full keeps the circuit’s gate depolarizing and adds the coherent rotation on top.

  • none: strips all noise from the circuit for noiseless validation.

Step 3: Round-trip fidelity verification

To verify that stripping gate noise from a Stim circuit and re-applying the same rates through MaestroBackend’s NoiseModel produces identical physics, the script compares mean detection-event rates across three paths: Stim’s native sampler, Maestro running the unmodified circuit, and Maestro running with extracted p_1q/p_2q overrides.

def run_section_3_fidelity_verification(circuits: dict[str, stim.Circuit]):
    """Verify that stripping and re-injecting noise reproduces original circuit results."""
    print(f"\n{'─' * 78}")
    print("  3. Overriding with a circuit's own gate rates reproduces the circuit")
    print(f"{'─' * 78}")
    print("  This comparison verifies round-trip fidelity across three execution paths:")
    print("    1. Stim reference: direct sampling from the native Stim circuit.")
    print("    2. Maestro as-is: Maestro running the circuit without modifying gate noise.")
    print("    3. Maestro own rates: Maestro strips gate noise from the circuit and delegates")
    print("       it to Maestro's NoiseModel. Close agreement confirms exact translation.")
    print("  Stim's own sampler is the independent reference.\n")

    print(f"  {'circuit':<26} │ {'Stim':>7} │ {'Maestro as-is':>13} │ {'Maestro, own rates':>18}")
    print(f"  {'─' * 72}")

    for name, circuit in circuits.items():
        rates = noise_params_from_stim(circuit)
        # 1. Native Stim reference rate
        stim_rate = circuit.compile_detector_sampler(seed=1).sample(50_000).mean()
        # 2. Maestro without stripping gate noise
        as_is_rate = mean_detection_rate(circuit, shots=3000, seed=1)
        # 3. Maestro stripping gate noise and re-applying via NoiseModel
        own_rates = mean_detection_rate(
            circuit,
            shots=3000,
            seed=1,
            p_1q=rates["p_1q"],
            p_2q=rates["p_2q"],
        )
        print(f"  {name:<26} │ {stim_rate:>7.4f} │ {as_is_rate:>13.4f} │ {own_rates:>18.4f}")

    print("\n  Mean detection-event rate. In the last column the gate noise runs through a")
    print("  NoiseModel while idle, reset and readout noise stay in the circuit.")


Output:

  circuit                    │    Stim │ Maestro as-is │ Maestro, own rates
  ────────────────────────────────────────────────────────────────────────
  Deltakit SI1000(p=0.01)    │  0.2112 │        0.2136 │             0.2115
  Stim-generated             │  0.1990 │        0.1988 │             0.1994

All three columns match within 3,000-shot sampling error.

Step 4: Fast-fail validation of invalid arguments

Misspelled keywords or incompatible mode/parameter combinations (such as passing coherent= under noise_type="pauli", or t1= under noise_type="none") raise TypeError or ValueError immediately at task-construction time rather than failing inside worker processes.

invalid_configurations = [
    ("pauli", {"p2q": 0.01}, "Typo in keyword argument ('p2q' instead of 'p_2q')"),
    (
        "pauli",
        {"coherent": 0.01},
        "Incompatible noise parameter (coherent noise cannot run under Pauli)",
    ),
    (
        "none",
        {"t1": 0.01},
        "Contradictory options (requested noiseless 'none' but provided t1 noise)",
    ),
    ("depolarizing", {}, "Unrecognized noise_type name"),
]

for noise_type, noise_kwargs, reason in invalid_configurations:
    try:
        backend.as_sinter_task(si1000_circuit_instance, noise_type, **noise_kwargs)
    except (TypeError, ValueError) as error:
        print(f"  {noise_type!r}, {noise_kwargs}  # {reason}")
        print(f"    {type(error).__name__}: {error}")


Output:

  'pauli', {'p2q': 0.01}  # Typo in keyword argument ('p2q' instead of 'p_2q')
    TypeError: Unknown noise keywords ['p2q']; expected ('p_1q', 'p_2q', 'readout_error', 't1', 'coherent')
  'pauli', {'coherent': 0.01}  # Incompatible noise parameter (coherent noise cannot run under Pauli)
    ValueError: ['coherent'] have no effect with noise_type='pauli'
  'none', {'t1': 0.01}  # Contradictory options (requested noiseless 'none' but provided t1 noise)
    ValueError: ['t1'] have no effect with noise_type='none'
  'depolarizing', {}  # Unrecognized noise_type name
    ValueError: noise_type must be one of ('pauli', 'coherent', 'full', 'none'), got 'depolarizing'

Key takeaways

  • You rarely need to build a maestro.NoiseModel manually: MaestroBackend extracts gate rates and preserves non-gate noise automatically.

  • Switching between Pauli frame sampling, T1 decay, and coherent MPS simulation only requires changing noise_type or keyword arguments.

  • Next: Custom Maestro noise model shows how to configure per-qubit calibration rates, asymmetric readout, and crosstalk on a custom maestro.NoiseModel.


Full script and complete output

View full script (examples/example_noise_modes.py)
"""
Noise Modes — Understanding How MaestroBackend Handles Circuit Noise
====================================================================

In quantum error correction (QEC) workflows, noise is typically embedded
directly inside a Stim circuit (e.g. as DEPOLARIZE1, DEPOLARIZE2, or X_ERROR).

With Deltakit-Maestro, you do not need to manually build a `maestro.NoiseModel`.
Instead, `MaestroBackend` inspects the circuit, identifies gate and readout error
rates, and picks Maestro's simulation type: Stabilizer for runs without coherent
noise (Pauli noise is sampled on Pauli frames, T1 decay shot by shot), and Matrix
Product State for coherent noise.

This script demonstrates:
    1. Noise Extraction & Stripping:
       How MaestroBackend identifies gate noise and separates it from idle,
       reset, and readout noise remaining in the circuit.
    2. Available Noise Modes & Simulation Types:
       What each `noise_type` ('pauli', 'coherent', 'full', 'none') and keyword
       override does, and which Maestro simulation type runs it.
    3. Round-Trip Verification:
       Demonstrating that stripping gate noise and replacing it with Maestro's
       NoiseModel reproduces the exact detection event rates of the original circuit,
       checked against Stim's own sampler.
    4. Fast-Fail Validation:
       Confirming that invalid noise parameters fail immediately before launching
       expensive sampling tasks.

Usage:
    pip install -e ".[examples]"    # from project root (deltakit-maestro/)
    python examples/example_noise_modes.py
"""

# Deltakit must be imported before pymatching: with pymatching 2.4 loaded
# first, Deltakit's circuit builders fail with "Caught an unknown exception!".
from collections import Counter
from dataclasses import dataclass
from typing import Any

import stim
from deltakit.circuit.gates import PauliBasis
from deltakit.explorer.codes import RotatedPlanarCode, css_code_memory_circuit
from deltakit.explorer.qpu import QPU, SI1000Noise

from deltakit_maestro import MaestroBackend, noise_params_from_stim, strip_gate_noise, to_stim

# =============================================================================
# CIRCUITS UNDER TEST
# =============================================================================


def si1000_circuit(p: float = 0.01) -> stim.Circuit:
    """Build a Deltakit distance-3 rotated surface code memory circuit with SI1000 noise.

    SI1000 represents a superconducting architecture noise model with 1-qubit gate
    depolarization, 2-qubit gate depolarization, readout errors, and idle noise.
    """
    code = RotatedPlanarCode(width=3, height=3)
    circuit = css_code_memory_circuit(code, num_rounds=3, logical_basis=PauliBasis.Z)
    noisy_qpu = QPU(circuit.qubits, noise_model=SI1000Noise(p=p))
    compiled_circuit = noisy_qpu.compile_and_add_noise_to_circuit(circuit)
    return to_stim(compiled_circuit)


def stim_circuit() -> stim.Circuit:
    """Build a standard Stim distance-3 rotated surface code circuit.

    Uses distinct, explicit error probabilities for each noise source:
    gate noise, before-round idle noise, reset flip probability, and measurement flip.
    """
    return stim.Circuit.generated(
        "surface_code:rotated_memory_z",
        distance=3,
        rounds=3,
        after_clifford_depolarization=0.01,
        before_round_data_depolarization=0.02,
        after_reset_flip_probability=0.03,
        before_measure_flip_probability=0.04,
    )


def noise_summary(circuit: stim.Circuit) -> str:
    """Summarize noise channels present in a Stim circuit along with target counts."""
    counts: Counter = Counter()
    for inst in circuit.flattened():
        args = inst.gate_args_copy()
        is_noise_instruction = (
            inst.name.startswith(("DEPOLARIZE", "PAULI_CHANNEL"))
            or inst.name.endswith("_ERROR")
            or (inst.name in ("M", "MR") and args)
        )
        if is_noise_instruction:
            formatted_args = ",".join(f"{a:g}" for a in args)
            channel_name = f"{inst.name}({formatted_args})"
            counts[channel_name] += len(inst.targets_copy())

    summary = "  ".join(f"{name}×{n}" for name, n in sorted(counts.items()))
    return summary or "(none)"


def mean_detection_rate(circuit: stim.Circuit, shots: int, seed: int, **noise: Any) -> float:
    """Simulate a circuit on Maestro and return the average fraction of triggered detectors."""
    result = MaestroBackend().simulate(circuit, shots=shots, seed=seed, **noise)
    return float(result.detection_events.mean())


# =============================================================================
# DEMONSTRATION HELPERS
# =============================================================================


@dataclass
class NoiseScenario:
    """A test configuration representing a simulation mode and parameter set."""

    noise_type: str
    noise_kwargs: dict[str, Any]
    purpose: str


def run_section_1_noise_extraction(circuits: dict[str, stim.Circuit]):
    """Demonstrate how MaestroBackend separates gate noise from circuit noise."""
    print(f"\n{'─' * 78}")
    print("  1. Gate noise is split from idle, reset and readout noise")
    print(f"{'─' * 78}")
    print("  Stim circuits embed noise directly inside instructions. When Maestro")
    print("  replaces or modifies gate noise (e.g. for parameter sweeps or coherent")
    print("  errors), it strips the gate noise from the circuit and handles it via")
    print("  Maestro's NoiseModel. Idle, reset, and readout errors stay in the circuit.\n")

    for name, circuit in circuits.items():
        extracted_rates = noise_params_from_stim(circuit)
        original_noise = noise_summary(circuit)
        stripped_circuit = strip_gate_noise(circuit)
        stripped_noise = noise_summary(stripped_circuit)

        print(f"  {name}")
        print(f"    rates:     {extracted_rates}")
        print(f"    circuit:   {original_noise}")
        print(f"    stripped:  {stripped_noise}\n")

    print("  Stripped keeps everything the NoiseModel does not replace. Gate noise is the")
    print("  depolarizing after a gate; Deltakit merges it with idle noise, so those are")
    print("  split per qubit. The Stim circuit's 'before round' noise follows a CX, so it stays.")


def run_section_2_noise_modes(si1000_circuit_instance: stim.Circuit):
    """Demonstrate each noise_type, parameter overrides, and the simulation type used."""
    backend = MaestroBackend()

    print(f"\n{'─' * 78}")
    print("  2. What each mode runs (SI1000 circuit)")
    print(f"{'─' * 78}")
    print("  MaestroBackend supports four primary noise modes:")
    print("    - 'pauli': Standard Pauli errors, sampled exactly on Maestro's Pauli frames.")
    print("    - 'coherent': Unitary rotation errors, requiring Maestro's Matrix Product States.")
    print("    - 'full': Combined Pauli and coherent errors, simulated via MPS.")
    print("    - 'none': Strips all noise for noiseless baseline verification.\n")

    scenarios = [
        NoiseScenario(
            noise_type="pauli",
            noise_kwargs={},
            purpose="Default baseline: Maestro runs the circuit as-is with its original noise.",
        ),
        NoiseScenario(
            noise_type="pauli",
            noise_kwargs={"p_2q": 0.02},
            purpose="Override 2-qubit gate error to 0.02: existing gate noise stripped, a NoiseModel supplies the new rate.",
        ),
        NoiseScenario(
            noise_type="pauli",
            noise_kwargs={"readout_error": 0.1},
            purpose="Override measurement error to 0.1: the circuit's readout flips replaced by the new rate.",
        ),
        NoiseScenario(
            noise_type="pauli",
            noise_kwargs={"t1": 0.002},
            purpose="Add T1 decay, a sampled reset: not a Pauli channel, so the Stabilizer runs it shot by shot.",
        ),
        NoiseScenario(
            noise_type="coherent",
            noise_kwargs={},
            purpose="Unitary over-rotations: automatically switches to Maestro's MatrixProductState.",
        ),
        NoiseScenario(
            noise_type="coherent",
            noise_kwargs={"coherent": 0.005},
            purpose="Coherent noise with custom rotation angle override.",
        ),
        NoiseScenario(
            noise_type="full",
            noise_kwargs={},
            purpose="Combined Pauli and coherent noise channels simulated under MatrixProductState.",
        ),
        NoiseScenario(
            noise_type="none",
            noise_kwargs={},
            purpose="Noiseless execution: all noise stripped, used for baseline checks and syndrome validation.",
        ),
    ]

    for scenario in scenarios:
        task = backend.as_sinter_task(
            si1000_circuit_instance, scenario.noise_type, **scenario.noise_kwargs
        )
        noise_layers = {
            k: v
            for k, v in task.json_metadata["noise"].items()
            if k not in ("noise_type", "stim_noise")
        }
        simulation_type = backend.config_for(
            scenario.noise_type, **scenario.noise_kwargs
        ).simulation_type
        simulator = f"Maestro {simulation_type.name}"
        dem_errors = task.detector_error_model.num_errors if task.detector_error_model else 0

        label = (
            f"{scenario.noise_type}, {scenario.noise_kwargs}"
            if scenario.noise_kwargs
            else scenario.noise_type
        )

        print(f"\n  {label}")
        print(f"    circuit simulated:     {noise_summary(task.circuit)}")
        print(f"    NoiseModel layers:     {noise_layers or '(none)'}")
        print(f"    simulator:             {simulator}")
        print(f"    decoder DEM errors:    {dem_errors}")


def run_section_3_fidelity_verification(circuits: dict[str, stim.Circuit]):
    """Verify that stripping and re-injecting noise reproduces original circuit results."""
    print(f"\n{'─' * 78}")
    print("  3. Overriding with a circuit's own gate rates reproduces the circuit")
    print(f"{'─' * 78}")
    print("  This comparison verifies round-trip fidelity across three execution paths:")
    print("    1. Stim reference: direct sampling from the native Stim circuit.")
    print("    2. Maestro as-is: Maestro running the circuit without modifying gate noise.")
    print("    3. Maestro own rates: Maestro strips gate noise from the circuit and delegates")
    print("       it to Maestro's NoiseModel. Close agreement confirms exact translation.")
    print("  Stim's own sampler is the independent reference.\n")

    print(f"  {'circuit':<26} │ {'Stim':>7} │ {'Maestro as-is':>13} │ {'Maestro, own rates':>18}")
    print(f"  {'─' * 72}")

    for name, circuit in circuits.items():
        rates = noise_params_from_stim(circuit)
        # 1. Native Stim reference rate
        stim_rate = circuit.compile_detector_sampler(seed=1).sample(50_000).mean()
        # 2. Maestro without stripping gate noise
        as_is_rate = mean_detection_rate(circuit, shots=3000, seed=1)
        # 3. Maestro stripping gate noise and re-applying via NoiseModel
        own_rates = mean_detection_rate(
            circuit,
            shots=3000,
            seed=1,
            p_1q=rates["p_1q"],
            p_2q=rates["p_2q"],
        )
        print(f"  {name:<26} │ {stim_rate:>7.4f} │ {as_is_rate:>13.4f} │ {own_rates:>18.4f}")

    print("\n  Mean detection-event rate. In the last column the gate noise runs through a")
    print("  NoiseModel while idle, reset and readout noise stay in the circuit.")


def run_section_4_bad_arguments(si1000_circuit_instance: stim.Circuit):
    """Demonstrate immediate upfront validation of invalid noise options."""
    backend = MaestroBackend()

    print(f"\n{'─' * 78}")
    print("  4. Bad noise arguments fail before sampling")
    print(f"{'─' * 78}")
    print("  In large QEC experiments with Sinter, invalid configurations must fail")
    print("  immediately before allocating worker processes or running shots.\n")

    invalid_configurations = [
        ("pauli", {"p2q": 0.01}, "Typo in keyword argument ('p2q' instead of 'p_2q')"),
        (
            "pauli",
            {"coherent": 0.01},
            "Incompatible noise parameter (coherent noise cannot run under Pauli)",
        ),
        (
            "none",
            {"t1": 0.01},
            "Contradictory options (requested noiseless 'none' but provided t1 noise)",
        ),
        ("depolarizing", {}, "Unrecognized noise_type name"),
    ]

    for noise_type, noise_kwargs, reason in invalid_configurations:
        try:
            backend.as_sinter_task(si1000_circuit_instance, noise_type, **noise_kwargs)
        except (TypeError, ValueError) as error:
            print(f"  {noise_type!r}, {noise_kwargs}  # {reason}")
            print(f"    {type(error).__name__}: {error}")


# =============================================================================
# MAIN ENTRYPOINT
# =============================================================================


def main():
    circuits = {
        "Deltakit SI1000(p=0.01)": si1000_circuit(),
        "Stim-generated": stim_circuit(),
    }
    si1000 = circuits["Deltakit SI1000(p=0.01)"]

    print("=" * 78)
    print("  Noise modes — MaestroBackend reads the noise from the Stim circuit")
    print("=" * 78)

    # 1. Noise extraction and gate noise separation
    run_section_1_noise_extraction(circuits)

    # 2. Noise modes and the simulation type each uses
    run_section_2_noise_modes(si1000)

    # 3. Round-trip fidelity validation
    run_section_3_fidelity_verification(circuits)

    # 4. Input validation and error handling
    run_section_4_bad_arguments(si1000)

    print(f"\n{'=' * 78}")
    print("  A custom maestro.NoiseModel is not accepted by MaestroBackend; use")
    print("  maestro.sinter.MaestroCompiledSampler directly for that.")
    print("=" * 78)


if __name__ == "__main__":
    main()
View complete output
==============================================================================
  Noise modes — MaestroBackend reads the noise from the Stim circuit
==============================================================================

──────────────────────────────────────────────────────────────────────────────
  1. Gate noise is split from idle, reset and readout noise
──────────────────────────────────────────────────────────────────────────────
  Stim circuits embed noise directly inside instructions. When Maestro
  replaces or modifies gate noise (e.g. for parameter sweeps or coherent
  errors), it strips the gate noise from the circuit and handles it via
  Maestro's NoiseModel. Idle, reset, and readout errors stay in the circuit.

  Deltakit SI1000(p=0.01)
    rates:     {'p_1q': 0.001, 'p_2q': 0.01, 'readout_error': 0.05}
    circuit:   DEPOLARIZE1(0.001)×215  DEPOLARIZE1(0.02)×36  DEPOLARIZE2(0.01)×144  M(0.05)×33  X_ERROR(0.02)×33
    stripped:  DEPOLARIZE1(0.001)×183  DEPOLARIZE1(0.02)×36  M(0.05)×33  X_ERROR(0.02)×33

  Stim-generated
    rates:     {'p_1q': 0.01, 'p_2q': 0.01, 'readout_error': 0.0}
    circuit:   DEPOLARIZE1(0.01)×24  DEPOLARIZE1(0.02)×27  DEPOLARIZE2(0.01)×144  X_ERROR(0.03)×41  X_ERROR(0.04)×33
    stripped:  DEPOLARIZE1(0.02)×27  X_ERROR(0.03)×41  X_ERROR(0.04)×33

  Stripped keeps everything the NoiseModel does not replace. Gate noise is the
  depolarizing after a gate; Deltakit merges it with idle noise, so those are
  split per qubit. The Stim circuit's 'before round' noise follows a CX, so it stays.

──────────────────────────────────────────────────────────────────────────────
  2. What each mode runs (SI1000 circuit)
──────────────────────────────────────────────────────────────────────────────
  MaestroBackend supports four primary noise modes:
    - 'pauli': Standard Pauli errors, sampled exactly on Maestro's Pauli frames.
    - 'coherent': Unitary rotation errors, requiring Maestro's Matrix Product States.
    - 'full': Combined Pauli and coherent errors, simulated via MPS.
    - 'none': Strips all noise for noiseless baseline verification.


  pauli
    circuit simulated:     DEPOLARIZE1(0.001)×215  DEPOLARIZE1(0.02)×36  DEPOLARIZE2(0.01)×144  M(0.05)×33  X_ERROR(0.02)×33
    NoiseModel layers:     (none)
    simulator:             Maestro Stabilizer
    decoder DEM errors:    290

  pauli, {'p_2q': 0.02}
    circuit simulated:     DEPOLARIZE1(0.001)×183  DEPOLARIZE1(0.02)×36  M(0.05)×33  X_ERROR(0.02)×33
    NoiseModel layers:     {'p_1q': 0.001, 'p_2q': 0.02}
    simulator:             Maestro Stabilizer
    decoder DEM errors:    290

  pauli, {'readout_error': 0.1}
    circuit simulated:     DEPOLARIZE1(0.001)×183  DEPOLARIZE1(0.02)×36  X_ERROR(0.02)×33
    NoiseModel layers:     {'p_1q': 0.001, 'p_2q': 0.01, 'readout_error': 0.1}
    simulator:             Maestro Stabilizer
    decoder DEM errors:    290

  pauli, {'t1': 0.002}
    circuit simulated:     DEPOLARIZE1(0.001)×215  DEPOLARIZE1(0.02)×36  DEPOLARIZE2(0.01)×144  M(0.05)×33  X_ERROR(0.02)×33
    NoiseModel layers:     {'t1': 0.002}
    simulator:             Maestro Stabilizer
    decoder DEM errors:    307

  coherent
    circuit simulated:     DEPOLARIZE1(0.001)×183  DEPOLARIZE1(0.02)×36  M(0.05)×33  X_ERROR(0.02)×33
    NoiseModel layers:     {'p_1q': 0.001, 'p_2q': 0.01}
    simulator:             Maestro MatrixProductState
    decoder DEM errors:    159

  coherent, {'coherent': 0.005}
    circuit simulated:     DEPOLARIZE1(0.001)×183  DEPOLARIZE1(0.02)×36  M(0.05)×33  X_ERROR(0.02)×33
    NoiseModel layers:     {'p_1q': 0.001, 'p_2q': 0.01, 'coherent': 0.005}
    simulator:             Maestro MatrixProductState
    decoder DEM errors:    159

  full
    circuit simulated:     DEPOLARIZE1(0.001)×215  DEPOLARIZE1(0.02)×36  DEPOLARIZE2(0.01)×144  M(0.05)×33  X_ERROR(0.02)×33
    NoiseModel layers:     {'coherent': 0.01}
    simulator:             Maestro MatrixProductState
    decoder DEM errors:    294

  none
    circuit simulated:     (none)
    NoiseModel layers:     (none)
    simulator:             Maestro Stabilizer
    decoder DEM errors:    290

──────────────────────────────────────────────────────────────────────────────
  3. Overriding with a circuit's own gate rates reproduces the circuit
──────────────────────────────────────────────────────────────────────────────
  This comparison verifies round-trip fidelity across three execution paths:
    1. Stim reference: direct sampling from the native Stim circuit.
    2. Maestro as-is: Maestro running the circuit without modifying gate noise.
    3. Maestro own rates: Maestro strips gate noise from the circuit and delegates
       it to Maestro's NoiseModel. Close agreement confirms exact translation.
  Stim's own sampler is the independent reference.

  circuit                    │    Stim │ Maestro as-is │ Maestro, own rates
  ────────────────────────────────────────────────────────────────────────
  Deltakit SI1000(p=0.01)    │  0.2112 │        0.2136 │             0.2115
  Stim-generated             │  0.1990 │        0.1988 │             0.1994

  Mean detection-event rate. In the last column the gate noise runs through a
  NoiseModel while idle, reset and readout noise stay in the circuit.

──────────────────────────────────────────────────────────────────────────────
  4. Bad noise arguments fail before sampling
──────────────────────────────────────────────────────────────────────────────
  In large QEC experiments with Sinter, invalid configurations must fail
  immediately before allocating worker processes or running shots.

  'pauli', {'p2q': 0.01}  # Typo in keyword argument ('p2q' instead of 'p_2q')
    TypeError: Unknown noise keywords ['p2q']; expected ('p_1q', 'p_2q', 'readout_error', 't1', 'coherent')
  'pauli', {'coherent': 0.01}  # Incompatible noise parameter (coherent noise cannot run under Pauli)
    ValueError: ['coherent'] have no effect with noise_type='pauli'
  'none', {'t1': 0.01}  # Contradictory options (requested noiseless 'none' but provided t1 noise)
    ValueError: ['t1'] have no effect with noise_type='none'
  'depolarizing', {}  # Unrecognized noise_type name
    ValueError: noise_type must be one of ('pauli', 'coherent', 'full', 'none'), got 'depolarizing'

==============================================================================
  A custom maestro.NoiseModel is not accepted by MaestroBackend; use
  maestro.sinter.MaestroCompiledSampler directly for that.
==============================================================================