真实硬件实验

在真实量子芯片上运行需要面对读出误差、相干性退化等工程挑战。 本节以 OriginQ WK180 芯片为例,演示读出误差缓解(Readout EM)和 交叉熵基准测试(XEB)。

WK180 readout EM example for UnifiedQuantum.

Source: examples/2_advanced/wk180/readout_em.py
Status: not-executed

This example demonstrates calibrating and applying readout error mitigation on the WK180 quantum processor from OriginQ.

Usage: # Dummy mode python examples/wk180/readout_em.py –dummy –qubits 0 1 2

# Real machine
python examples/wk180/readout_em.py --backend originq:wuyuan:wk180 --qubits 0 1 2

Source code

#!/usr/bin/env python
"""WK180 readout EM example for UnifiedQuantum.

This example demonstrates calibrating and applying readout error mitigation
on the WK180 quantum processor from OriginQ.

Usage:
    # Dummy mode
    python examples/wk180/readout_em.py --dummy --qubits 0 1 2

    # Real machine
    python examples/wk180/readout_em.py --backend originq:wuyuan:wk180 --qubits 0 1 2

[doc-require: originq]
[doc-skip-execute]
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent.parent / "UnifiedQuantum"))

from uniqc.backend_adapter.preflight import (  # noqa: E402
    BackendPreflightError,
    MissingDependencyError,
)


def _get_wk180_adapter(dummy: bool, backend_name: str | None):
    """Get adapter and chip characterization for WK180.

    Strict policy (no fallbacks): pyqpanda3 must be installed and the
    chip cache must be present-or-refreshable. ``--dummy`` mode is not
    exempt — chip-noisy simulation depends on real chip data.
    """
    from uniqc.backend_adapter.preflight import ensure_backend_ready
    from uniqc.backend_adapter.task.adapters import DummyAdapter, OriginQAdapter

    backend_name = backend_name or "originq:wuyuan:wk180"
    chip_short = backend_name.split(":")[-1] or "WK_C180"
    backend_id = (
        f"dummy:originq:{chip_short}" if dummy else f"originq:{chip_short}"
    )
    chip_char = ensure_backend_ready(backend_id, max_age_hours=24.0)

    if dummy:
        adapter = DummyAdapter(chip_characterization=chip_char)
    else:
        adapter = OriginQAdapter(backend_name=chip_short)
    return adapter, chip_char, backend_id


def run_wk180_readout_em(
    dummy: bool = True,
    qubits: list[int] | None = None,
    pairs: list[tuple[int, int]] | None = None,
    max_age_hours: float = 24.0,
    shots: int = 1000,
    backend_name: str | None = None,
    output_file: str | None = None,
) -> dict:
    """Run readout EM calibration on WK180.

    Args:
        dummy: Use local noisy simulation if True, real hardware if False.
        qubits: Qubit indices for 1q calibration.
        pairs: Qubit pairs for 2q calibration.
        max_age_hours: Maximum acceptable age of cached calibration data.
        shots: Number of shots per calibration circuit.
        backend_name: OriginQ backend name.
        output_file: If provided, save results to this JSON file.

    Returns:
        Dict with calibration results.
    """
    from uniqc import readout_em_workflow

    if qubits is None:
        qubits = [0, 1, 2, 3]

    adapter, chip_char, backend_label = _get_wk180_adapter(dummy, backend_name)

    print(f"[WK180] Running readout EM calibration on {backend_label}...")
    print(f"  1q qubits: {qubits}")
    print(f"  2q pairs: {pairs}")

    readout_em = readout_em_workflow.run_readout_em_workflow(
        backend=backend_label,
        qubits=qubits,
        pairs=pairs,
        shots=shots,
        max_age_hours=max_age_hours,
        chip_characterization=chip_char,
    )

    # Report calibration quality
    print("[WK180] Calibration complete. Assignment fidelities:")

    from uniqc.calibration.results import find_cached_results

    for q in qubits:
        paths = find_cached_results(
            backend_label, "readout_1q", max_age_hours=max_age_hours
        )
        q_paths = [p for p in paths if f"_q{q}_" in p.name]
        if q_paths:
            import json

            with open(q_paths[-1]) as f:
                d = json.load(f)
            print(f"  Qubit {q}: assignment fidelity = {d['assignment_fidelity']:.5f}")

    if pairs:
        for pu, pv in pairs:
            paths = find_cached_results(
                backend_label, "readout_2q", max_age_hours=max_age_hours
            )
            pair_paths = [p for p in paths if f"pair-{pu}-{pv}" in p.name or f"pair-{pv}-{pu}" in p.name]
            if pair_paths:
                import json

                with open(pair_paths[-1]) as f:
                    d = json.load(f)
                print(f"  Pair ({pu},{pv}): assignment fidelity = {d['assignment_fidelity']:.5f}")

    if output_file:
        from uniqc.calibration.results import find_cached_results

        out_path = Path(output_file)
        out_path.parent.mkdir(parents=True, exist_ok=True)
        all_results = {}
        for q in qubits:
            paths = [p for p in find_cached_results(backend_label, "readout_1q")
                     if f"_q{q}_" in p.name]
            if paths:
                import json

                with open(paths[-1]) as f:
                    all_results[f"1q_q{q}"] = json.load(f)
        for pu, pv in (pairs or []):
            paths = [p for p in find_cached_results(backend_label, "readout_2q")
                     if f"pair-{pu}-{pv}" in p.name or f"pair-{pv}-{pu}" in p.name]
            if paths:
                import json

                with open(paths[-1]) as f:
                    all_results[f"2q_{pu}_{pv}"] = json.load(f)
        with open(out_path, "w") as f:
            json.dump(all_results, f, indent=2)
        print(f"[WK180] Calibration results saved to {out_path}")

    return {"readout_em": readout_em}


def main() -> None:
    parser = argparse.ArgumentParser(description="WK180 readout EM calibration")
    parser.add_argument("--dummy", action="store_true", help="Use dummy mode")
    parser.add_argument("--backend", type=str, default=None, help="OriginQ backend name")
    parser.add_argument("--qubits", type=int, nargs="+", default=None, help="Qubit indices")
    parser.add_argument("--pairs", type=int, nargs="+", default=None,
                        help="Qubit pairs as: u1 v1 u2 v2 ...")
    parser.add_argument("--max-age-hours", type=float, default=24.0, dest="max_age_hours")
    parser.add_argument("--shots", type=int, default=1000)
    parser.add_argument("--output", type=str, default=None)
    args = parser.parse_args()

    pairs = None
    if args.pairs:
        if len(args.pairs) % 2 != 0:
            print("Error: --pairs requires an even number of arguments", file=sys.stderr)
            sys.exit(1)
        pairs = [(args.pairs[i], args.pairs[i + 1]) for i in range(0, len(args.pairs), 2)]

    try:
        run_wk180_readout_em(
            dummy=args.dummy,
            qubits=args.qubits,
            pairs=pairs,
            max_age_hours=args.max_age_hours,
            shots=args.shots,
            backend_name=args.backend,
            output_file=args.output,
        )
    except (MissingDependencyError, BackendPreflightError) as e:
        print(f"\nERROR: {e}\n", file=sys.stderr)
        sys.exit(3)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        raise


if __name__ == "__main__":
    main()

Note

Listed for reference; not executed during the docs build ([doc-skip-execute]).

WK180 chip-wide parallel-CZ XEB example for UnifiedQuantum.

Source: examples/2_advanced/wk180/xeb.py
Status: not-executed

This example partitions every CZ edge of OriginQ’s WK_C180 chip into three disjoint matchings (3-edge-coloring), picks one matching, and runs a chip-wide parallel 2-qubit XEB on that single matching. The full N-qubit circuit factorises as a tensor product over the disjoint pairs of the matching, so per-pair F_XEB(d) decays — and therefore per-pair CZ fidelities — fall out of 2-qubit marginals of the chip-wide bitstring (no 2^N statevector blow-up needed for analysis).

This is the recommended pre-flight characterization step before any larger experiment that depends on accurate per-pair CZ numbers.

Pre-flight policy (no fallbacks)

This example refuses to run unless every prerequisite is satisfied:

  • pyqpanda3 must be importable (the OriginQ SDK).

  • The WK180 chip characterization must be present in the local cache and younger than 24 hours; otherwise it is refreshed via the OriginQ SDK. If the SDK refresh fails (no API key / network / invalid chip name), the example aborts with a precise error.

  • --dummy mode is not exempt from any of the above — chip-noisy simulation is meaningless without real chip data.

  • Real-chip submission additionally requires --confirm-chip.

Usage:

# Inspect the 3-coloring without running anything (still pre-flighted):
python examples/wk180/xeb.py --list-colors

# Dummy mode (use --max-qubits to keep the noisy density-op simulator happy):
python examples/wk180/xeb.py --dummy --max-qubits 10 --color 0

# Real chip:
python examples/wk180/xeb.py --backend originq:wuyuan:wk180 \\
    --color 0 --confirm-chip --shots 5000 --instances 20

# 1q-XEB sanity check on a few qubits:
python examples/wk180/xeb.py --dummy --type 1q --qubits 0 1 2

Source code

#!/usr/bin/env python
"""WK180 chip-wide parallel-CZ XEB example for UnifiedQuantum.

This example partitions every CZ edge of OriginQ's WK_C180 chip into
three disjoint matchings (3-edge-coloring), picks one matching, and
runs a chip-wide *parallel* 2-qubit XEB on that single matching. The
full N-qubit circuit factorises as a tensor product over the disjoint
pairs of the matching, so per-pair F_XEB(d) decays — and therefore
per-pair CZ fidelities — fall out of 2-qubit marginals of the
chip-wide bitstring (no 2^N statevector blow-up needed for analysis).

This is the recommended *pre-flight* characterization step before any
larger experiment that depends on accurate per-pair CZ numbers.

Pre-flight policy (no fallbacks)
--------------------------------
This example refuses to run unless every prerequisite is satisfied:

* ``pyqpanda3`` must be importable (the OriginQ SDK).
* The WK180 chip characterization must be present in the local cache
  *and* younger than 24 hours; otherwise it is refreshed via the
  OriginQ SDK. If the SDK refresh fails (no API key / network /
  invalid chip name), the example aborts with a precise error.
* ``--dummy`` mode is **not** exempt from any of the above — chip-noisy
  simulation is meaningless without real chip data.
* Real-chip submission additionally requires ``--confirm-chip``.

Usage:

    # Inspect the 3-coloring without running anything (still pre-flighted):
    python examples/wk180/xeb.py --list-colors

    # Dummy mode (use --max-qubits to keep the noisy density-op simulator happy):
    python examples/wk180/xeb.py --dummy --max-qubits 10 --color 0

    # Real chip:
    python examples/wk180/xeb.py --backend originq:wuyuan:wk180 \\
        --color 0 --confirm-chip --shots 5000 --instances 20

    # 1q-XEB sanity check on a few qubits:
    python examples/wk180/xeb.py --dummy --type 1q --qubits 0 1 2

[doc-require: originq]
[doc-skip-execute]
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import Any

sys.path.insert(0, str(Path(__file__).parent.parent.parent / "UnifiedQuantum"))

from uniqc.backend_adapter.preflight import (  # noqa: E402
    BackendPreflightError,
    MissingDependencyError,
)


# ---------------------------------------------------------------------------
# Adapter / chip plumbing
# ---------------------------------------------------------------------------


def _get_wk180_backend(
    dummy: bool, backend_name: str | None
) -> tuple[Any, Any, str]:
    """Return ``(adapter, chip_characterization, backend_label)``.

    Strict policy (no fallbacks): if ``pyqpanda3`` (the OriginQ SDK) is
    not importable, or the WK180 chip characterization cannot be
    fetched / refreshed, this raises immediately with a clear install /
    setup hint. ``--dummy`` mode is *not* exempt — noise-aware
    simulation depends on real chip data, so the real-provider checks
    apply.
    """
    from uniqc.backend_adapter.preflight import ensure_backend_ready
    from uniqc.backend_adapter.task.adapters import DummyAdapter, OriginQAdapter

    backend_name = backend_name or "originq:wuyuan:wk180"
    chip_short = backend_name.split(":")[-1] or "WK_C180"

    backend_id = (
        f"dummy:originq:{chip_short}" if dummy else f"originq:{chip_short}"
    )
    chip_char = ensure_backend_ready(backend_id, max_age_hours=24.0)
    if chip_char is None:
        raise RuntimeError(
            f"Preflight returned no chip characterization for {backend_id!r}; "
            "this is a bug — please report it."
        )

    if dummy:
        adapter = DummyAdapter(chip_characterization=chip_char)
        return adapter, chip_char, f"dummy:originq:{chip_short}"
    adapter = OriginQAdapter(backend_name=chip_short)
    return adapter, chip_char, backend_name


# ---------------------------------------------------------------------------
# Three-coloring + region restriction
# ---------------------------------------------------------------------------


def _three_color(chip_char: Any) -> tuple[Any, list[list[tuple[int, int]]]]:
    """Build a chip view + 3-color partition of every CZ edge.

    Returns ``(view, colors)`` where ``colors`` is a list of matchings
    ordered from largest to smallest edge count.
    """
    from uniqc.calibration.xeb import ChipTopologyView, three_color_chip

    view = ChipTopologyView.from_chip_characterization(chip_char)
    colors = [list(c) for c in three_color_chip(view, max_K=4)]
    colors.sort(key=lambda c: -len(c))
    return view, colors


def _restrict_color_to_region(
    view: Any, color: list[tuple[int, int]],
    max_touched_qubits: int, *, seed: int = 0,
) -> tuple[list[int], list[tuple[int, int]]]:
    """Pick a high-fidelity region and intersect ``color`` with its
    induced edges, capping the number of *touched* qubits at
    ``max_touched_qubits`` (so the resulting circuit fits in the
    DummyAdapter's noisy simulator, which is limited to ~10 qubits).

    Strategy: pick the highest-fidelity region of ``2 * (max_touched_qubits)``
    qubits, intersect with the matching, then greedily keep pairs in
    descending pair-fidelity order until the qubit cap is reached.
    """
    from uniqc.calibration.xeb import pick_region

    region_size = max(min(len(view.enabled_qubits), 4 * max_touched_qubits),
                      max_touched_qubits)
    region = pick_region(view, n=int(region_size), seed=seed)
    region_set = set(region.qubits)
    candidates = [
        (a, b) for (a, b) in color if a in region_set and b in region_set
    ]
    # Sort by ascending 2q error (best pairs first).
    candidates.sort(key=lambda e: view.two_qubit_error(*e))

    chosen: list[tuple[int, int]] = []
    touched: set[int] = set()
    for a, b in candidates:
        if len(touched | {a, b}) > max_touched_qubits:
            continue
        chosen.append((a, b))
        touched.update((a, b))
        if len(touched) >= max_touched_qubits:
            break
    return sorted(touched), chosen


# ---------------------------------------------------------------------------
# Entry points
# ---------------------------------------------------------------------------


def run_wk180_parallel_cz_xeb(
    *,
    dummy: bool = True,
    backend_name: str | None = None,
    color_idx: int = 0,
    max_qubits: int | None = None,
    region_seed: int = 0,
    depths: list[int] | None = None,
    instances: int = 20,
    shots: int = 5000,
    seed: int = 2026,
    output_file: str | None = None,
    cache_dir: str | None = None,
) -> dict[str, Any]:
    """Run chip-wide parallel-CZ XEB on one 3-coloring matching of WK180.

    Args:
        dummy: If True, run on local DummyAdapter (chip-noisy). For
            dummy mode you almost always want ``max_qubits`` set,
            since simulating ~140 qubits is intractable.
        backend_name: Override OriginQ backend identifier.
        color_idx: Which matching to use (0 = largest by default).
        max_qubits: If set, restrict to a high-fidelity region of this
            many qubits and intersect the matching with its induced edges.
        region_seed: Seed for region picking when ``max_qubits`` is set.
        depths: Depth grid. Defaults to ``[5, 10, 15, 20, 25, 30]``.
        instances: Random circuit instances per depth.
        shots: Shots per circuit.
        seed: Master corpus RNG seed.
        output_file: If supplied, write the full result as JSON.
        cache_dir: Forwarded to the benchmarker for per-pair XEBResult JSON.

    Returns:
        Dict with keys ``region_qubits``, ``patterns``, ``pairs``,
        ``per_pair_decays``, ``per_pair_results``, ``corpus_size``,
        ``color_summary`` (sizes of the three colors).
    """
    from uniqc import xeb_workflow

    if depths is None:
        depths = [5, 10, 15, 20, 25, 30]

    adapter, chip_char, backend_label = _get_wk180_backend(dummy, backend_name)

    view, colors = _three_color(chip_char)
    color_summary = [len(c) for c in colors]
    print(
        f"[WK180] 3-coloring of {len(view.coupling_map)} CZ edges → "
        f"3 matchings of sizes {color_summary} "
        f"({sum(color_summary)} edges total)"
    )
    if not (0 <= color_idx < len(colors)):
        raise SystemExit(
            f"--color={color_idx} out of range; "
            f"only {len(colors)} colors available"
        )
    chosen_color = colors[color_idx]
    chosen_qubits = sorted({q for e in chosen_color for q in e})
    print(
        f"[WK180] selected color {color_idx}: "
        f"{len(chosen_color)} pairs touching {len(chosen_qubits)} qubits"
    )

    if max_qubits is not None and max_qubits < len(chosen_qubits):
        region_qubits, restricted = _restrict_color_to_region(
            view, chosen_color, max_qubits, seed=region_seed,
        )
        print(
            f"[WK180] restricted to a {max_qubits}-qubit high-fidelity "
            f"region; {len(restricted)} pairs of color {color_idx} fall "
            f"inside it (touching {len(region_qubits)} qubits)"
        )
        if not restricted:
            raise SystemExit(
                f"After restricting to {max_qubits}-qubit region, color "
                f"{color_idx} has no edges. Try a different --color or "
                "increase --max-qubits."
            )
        patterns = [restricted]
    else:
        region_qubits = chosen_qubits
        patterns = [chosen_color]

    # The workflow routes through ParallelCZBenchmarker which uses the
    # adapter's simulate_pmeasure fast path on the dummy backend, or
    # submit_batch on real hardware.
    result = xeb_workflow.run_parallel_cz_xeb_workflow(
        backend=backend_label,
        chip_characterization=chip_char,
        region_qubits=region_qubits,
        patterns=patterns,
        depths=depths,
        instances=instances,
        shots=shots,
        seed=seed,
        cache_dir=cache_dir,
    )

    print(
        f"[WK180] ran {result['corpus_size']} circuits "
        f"(1 pattern × {len(depths)} depths × {instances} instances), "
        f"{shots} shots each"
    )
    print(f"[WK180] per-pair CZ fidelity (alpha = per-cycle survival):")
    rows = sorted(result["per_pair_decays"].items(), key=lambda kv: -kv[1].alpha)
    for pair, dec in rows:
        print(
            f"  {pair[0]:>3d}{pair[1]:<3d}: "
            f"alpha={dec.alpha:.4f} ± {dec.alpha * dec.sigma_log_alpha:.4f}  "
            f"(beta={dec.beta:.3f}, n_pts={dec.n_points})"
        )

    payload = {
        "backend": backend_label,
        "color_summary": color_summary,
        "selected_color_idx": color_idx,
        "selected_color_n_pairs": len(patterns[0]),
        "region_qubits": list(result["region_qubits"]),
        "patterns": [[list(e) for e in p] for p in result["patterns"]],
        "depths": list(depths),
        "instances": instances,
        "shots": shots,
        "corpus_size": result["corpus_size"],
        "per_pair_alpha": {
            f"{p[0]}-{p[1]}": {
                "alpha": dec.alpha,
                "alpha_sigma": dec.alpha * dec.sigma_log_alpha,
                "beta": dec.beta,
                "log_alpha": dec.log_alpha,
                "log_beta": dec.log_beta,
                "n_points": dec.n_points,
                "log_residual_std": dec.log_residual_std,
            }
            for p, dec in result["per_pair_decays"].items()
        },
    }

    if output_file:
        out_path = Path(output_file)
        out_path.parent.mkdir(parents=True, exist_ok=True)
        out_path.write_text(json.dumps(payload, indent=2))
        print(f"[WK180] results saved to {out_path}")

    return payload


def run_wk180_1q_xeb(
    *,
    dummy: bool = True,
    backend_name: str | None = None,
    qubits: list[int] | None = None,
    depths: list[int] | None = None,
    n_circuits: int = 50,
    shots: int = 1000,
    use_readout_em: bool = True,
) -> dict:
    """Convenience wrapper for 1-qubit XEB on a few WK180 qubits."""
    from uniqc import xeb_workflow

    if depths is None:
        depths = [5, 10, 20, 50]
    if qubits is None:
        qubits = [0, 1, 2]

    adapter, chip_char, backend_label = _get_wk180_backend(dummy, backend_name)
    print(f"[WK180] 1q XEB on qubits {qubits}")
    results = xeb_workflow.run_1q_xeb_workflow(
        backend=backend_label,
        qubits=qubits,
        depths=depths,
        n_circuits=n_circuits,
        shots=shots,
        use_readout_em=use_readout_em,
        chip_characterization=chip_char,
    )
    for q, r in results.items():
        print(
            f"  q{q}: r={r.fidelity_per_layer:.5f} ± "
            f"{r.fidelity_std_error:.5f}"
        )
    return results


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------


def main() -> None:
    parser = argparse.ArgumentParser(
        description="WK180 chip-wide parallel-CZ XEB benchmark"
    )
    parser.add_argument("--dummy", action="store_true",
                        help="Run on DummyAdapter (local noisy simulation)")
    parser.add_argument("--backend", type=str, default=None,
                        help="OriginQ backend (default: originq:wuyuan:wk180)")
    parser.add_argument("--type", choices=["parallel_cz", "1q"],
                        default="parallel_cz",
                        help="parallel_cz: chip-wide parallel CZ XEB on one "
                             "3-coloring matching (default). "
                             "1q: separate 1-qubit XEB workflow on --qubits.")
    # parallel-CZ XEB args
    parser.add_argument("--color", type=int, default=0, dest="color_idx",
                        help="Which 3-coloring matching to run (0..2). "
                             "Sorted descending by edge count, so 0 is the largest.")
    parser.add_argument("--list-colors", action="store_true",
                        help="Print the 3-coloring summary and exit.")
    parser.add_argument("--max-qubits", type=int, default=None,
                        help="Cap on actually-touched qubits in the matching "
                             "(recommended for --dummy because the noisy "
                             "DummyAdapter simulator is limited to ~10 qubits). "
                             "Defaults to running every qubit touched by the matching.")
    parser.add_argument("--region-seed", type=int, default=0,
                        help="Seed for region picking when --max-qubits is set.")
    parser.add_argument("--depths", type=int, nargs="+", default=None,
                        help="Depth grid. Default: 5 10 15 20 25 30.")
    parser.add_argument("--instances", type=int, default=20,
                        help="Random circuit instances per depth (default 20).")
    parser.add_argument("--shots", type=int, default=5000,
                        help="Shots per circuit (default 5000).")
    parser.add_argument("--seed", type=int, default=2026,
                        help="Master corpus RNG seed.")
    parser.add_argument("--output", type=str, default=None,
                        help="Write per-pair fidelities to this JSON file.")
    parser.add_argument("--cache-dir", type=str, default=None,
                        help="Per-pair XEBResult JSON output directory.")
    parser.add_argument("--confirm-chip", action="store_true",
                        help="Required to submit to real hardware.")
    # 1q XEB args
    parser.add_argument("--qubits", type=int, nargs="+", default=None,
                        help="Qubits for --type=1q (default 0 1 2).")
    parser.add_argument("--n-circuits", type=int, default=50,
                        dest="n_circuits",
                        help="Random circuits per depth for --type=1q.")
    parser.add_argument("--no-readout-em", action="store_true",
                        dest="no_readout_em",
                        help="Disable readout EM for --type=1q.")
    args = parser.parse_args()

    try:
        if args.list_colors:
            adapter, chip_char, _ = _get_wk180_backend(
                dummy=True, backend_name=args.backend,
            )
            _, colors = _three_color(chip_char)
            for i, c in enumerate(colors):
                qs = sorted({q for e in c for q in e})
                print(f"color {i}: {len(c)} pairs, {len(qs)} qubits")
            return

        if not args.dummy and not args.confirm_chip:
            print(
                "ERROR: real-chip submission requires --confirm-chip "
                "(safety check to prevent accidental chip jobs).",
                file=sys.stderr,
            )
            sys.exit(2)

        if args.type == "1q":
            run_wk180_1q_xeb(
                dummy=args.dummy,
                backend_name=args.backend,
                qubits=args.qubits,
                depths=args.depths,
                n_circuits=args.n_circuits,
                shots=args.shots,
                use_readout_em=not args.no_readout_em,
            )
        else:
            run_wk180_parallel_cz_xeb(
                dummy=args.dummy,
                backend_name=args.backend,
                color_idx=args.color_idx,
                max_qubits=args.max_qubits,
                region_seed=args.region_seed,
                depths=args.depths,
                instances=args.instances,
                shots=args.shots,
                seed=args.seed,
                output_file=args.output,
                cache_dir=args.cache_dir,
            )
    except (MissingDependencyError, BackendPreflightError) as e:
        # Pre-flight rejection: surface the message cleanly, no traceback.
        print(f"\nERROR: {e}\n", file=sys.stderr)
        sys.exit(3)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        raise


if __name__ == "__main__":
    main()

Note

Listed for reference; not executed during the docs build ([doc-skip-execute]).