Erado Basic Simulator

Erado Basic is a simulator for erasure noise: it does not model other noise types such as depolarising or dephasing noise. Erasure noise converts a qubit into a distinct, checkable “lost” state rather than flipping or randomising it, which changes how error mitigation needs to be approached. Erado Basic lets you validate programs and mitigation strategies against this noise model ahead of running on erasure-prone hardware. It runs on a Qiskit AerSimulator backend using the statevector method: an exact, full-state simulation rather than an approximation. For the underlying theory, see Background in the Erado documentation.

 

Task submission

QCaaS provides a 16-qubit Erado Basic simulator available via QPU ID qpu:uk:4:a63d17f4a0. Only OpenQASM 2.0 programs are currently supported. Results are always returned in binary count format (see Task results, or the Examples below, for what that looks like). Task submission, monitoring, and result retrieval otherwise follow the same patterns as for QPU tasks; refer to Submitting and managing tasks.

Note

EradoConfig requires qcaas-client version 3.23.0 or later; see oqc-qcaas-client on PyPI for the latest release, and Installation for how to upgrade.

 

Task configuration

Erado Basic tasks use EradoConfig in place of CompilerConfig. Erasure-noise simulation needs its own controls, such as which noise model to run and how erasures are detected and handled, that don’t map onto standard QPU compiler settings.

Note

Submitting a CompilerConfig instead of an EradoConfig still works: only repeats (shot count) carries over, and the task runs as a noiseless simulation with erasure_rate=0.0 (the current default; may vary by deployment). Tket optimisations, error mitigation, and other CompilerConfig settings do not apply to Erado Basic and are ignored.

EradoConfig is a dataclass, so a configuration can be built and checked without a live connection:

from qcaas_client.client import EradoConfig, ErasureModel

config = EradoConfig(
    repeats=1000,
    erasure_rate=0.01,
    erasure_model=ErasureModel.TRANSPILER_PASS,
)
config.validate()  # raises ValueError for out-of-range parameters

The full set of parameters is:

Table 1: EradoConfig parameters

Parameter

Description

Notes

repeats

Number of shots.

Type: int (>= 1)
Default: 1000

erasure_rate

Per-gate probability that a gate causes an erasure; if it fires, every qubit that gate acts on becomes erased. Total erasure incidence therefore compounds with the number of erasable gates in your circuit, not just circuit depth.

Type: float (0-1)
Default: 0.5. This is high (50%); most workloads will want a lower value.
When post_selection=True, values above 0.01 are currently rejected to avoid excessive shot-rejection overhead.

erasure_before_gates

When True, the gate whose trial triggers an erasure is itself deleted along with subsequent gates on the affected qubit(s). When False (default), only gates after the triggering gate are deleted.

Type: bool
Default: False

erasure_model

Simulation method. CIRCUIT_SAMPLER uses erado.models.ErasureCircuitSampler; TRANSPILER_PASS uses erado.models.ErasurePassJob.

Type: ErasureModel
Default: CIRCUIT_SAMPLER
CIRCUIT_SAMPLER is more memory-hungry at scale; prefer TRANSPILER_PASS for larger circuits.

post_selection

When True, shots containing erasures are discarded and re-run until repeats accepted shots are collected. Retries are uncapped and bounded only by the task’s execution timeout, so cost scales with erasure_rate. When False (the default), shots with a detected erasure are not discarded: they’re included in the returned counts with no indication that a measurement was affected, since Erado does not currently return per-shot erasure flags.

Type: bool
Default: False

get_fidelities

Return per-shot circuit fidelity alongside results. Not currently supported; True is rejected.

Type: bool
Default: False

false_positive_rate

Rate at which erasure checks incorrectly signal an erasure.

Type: float (0-1)
Default: 0.0

false_negative_rate

Rate at which erasure checks miss a true erasure. This is what limits how effective post_selection can be: an undetected erasure is accepted, and its corrupted measurement is kept.

Type: float (0-1)
Default: 0.0

idling_error

An optional IdlingErrorConfig instance describing idle-period padding. Padding does not carry its own error rate: inserted idle gates simply become additional erasable gate positions subject to the same erasure_rate.

Type: IdlingErrorConfig
Default: None (disabled)

Warning

With post_selection=True, erasure_rate values above 0.01 are currently rejected to avoid excessive shot-rejection overhead. This is easy to hit by accident: raising erasure_rate on a working post_selection=True task, for example to make erasures more visible, will cause the task to fail.

Idling error models the noise a qubit accumulates while it sits idle between gates: idle periods are padded with extra gates, which become additional erasable positions. When set, idling_error takes an IdlingErrorConfig instance with the following fields; see Idling error in the Erado documentation for the underlying mechanism and modelling rationale.

Table 2: IdlingErrorConfig parameters

Parameter

Description

Notes

max_idle_length

Maximum number of idle gates inserted into an idle period.

Type: int
Default: 14
Values below 2 insert no idle gates.

idle_gate

Qiskit gate inserted during idle periods.

Type: str, one of "id", "x", "y", "z"
Default: "id"

circuit_gate_time

Unit duration assigned to each existing circuit gate.

Type: float (> 0)
Arbitrary relative units, not physical time.
Default: 1.0

idle_gate_time

Unit duration assigned to each inserted idle gate.

Type: float (> 0)
Arbitrary relative units, not physical time.
Default: 0.8

sequence_min_length_ratio

Minimum ratio between an idle period and the inserted gate sequence length.

Type: float (>= 0)
Default: 1.0

 

Examples

The following examples build up from a noiseless baseline to erasure with and without post-selection, then to configuring idling error, all using the same 5-qubit GHZ circuit. The counts and metrics shown are taken from real runs against the simulator.

 

Noiseless baseline

from qcaas_client.client import (
    OQCClient, QPUTask, EradoConfig, ErasureModel, IdlingErrorConfig,
)

client = OQCClient(
    url=<oqc_cloud_url>,
    authentication_token=<access_token>
)

ghz = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[5];
creg c[5];
h q[0];
cx q[0], q[1];
cx q[1], q[2];
cx q[2], q[3];
cx q[3], q[4];
measure q -> c;
"""

config = EradoConfig(repeats=2000, erasure_rate=0.0)
task = QPUTask(program=ghz, config=config, qpu_id="qpu:uk:4:a63d17f4a0")
result = client.execute_tasks(task)[0]
if result.has_errored():
    raise RuntimeError(result.error_details.error_message)
print(result.result)
# {'c': {'00000': 1002, '11111': 998}}

With erasure_rate=0.0, Erado Basic behaves like any other noiseless Qiskit simulator: results split close to evenly between the GHZ state’s two ideal outcomes, with the usual shot noise you’d see from any simulator or real device. The remaining examples on this page omit the has_errored() check shown above for brevity; see Task error for more information if a task fails.

 

Erasure without post-selection

Enabling erasure with post_selection left at its default (False) includes corrupted measurements in the results with no indication that anything went wrong:

config = EradoConfig(
    repeats=2000,
    erasure_rate=0.01,
    erasure_model=ErasureModel.CIRCUIT_SAMPLER,
)
task = QPUTask(program=ghz, config=config, qpu_id="qpu:uk:4:a63d17f4a0")
result = client.execute_tasks(task, include_metrics=True)[0]
print(result.result)
# {'c': {'00000': 1005, '10000': 14, '11000': 12, '11100': 11, '11110': 14, '11111': 944}}
metrics = result.metrics
for key in ("n_accepted", "n_rejected", "rejection_rate"):
    print(f"{key}: {metrics[key]}")
#     n_accepted: 2000
#     n_rejected: 111
# rejection_rate: 0.0555

Note

erasure_model is set explicitly here to show the syntax, even though CIRCUIT_SAMPLER is also the default.

Outcomes other than 00000 and 11111 are shots where an erasure cut the run of cx gates short: entanglement never reached the remaining qubits, so those qubits measured 0. Nothing in result.result marks these corrupted outcomes as such. Note that n_rejected and rejection_rate are non-zero even though no shots were discarded: with post_selection=False they report erasure incidence, not actual rejections, and n_accepted still equals the full repeats.

Log-scale bar chart comparing GHZ circuit outcome counts with no erasure against erasure_rate=0.01 without post-selection, showing small counts appearing at the corrupted outcomes 10000, 11000, 11100 and 11110. Log-scale bar chart comparing GHZ circuit outcome counts with no erasure against erasure_rate=0.01 without post-selection, showing small counts appearing at the corrupted outcomes 10000, 11000, 11100 and 11110.

 

Erasure with post-selection

Setting post_selection=True discards those corrupted shots and re-runs until repeats accepted shots are collected:

config = EradoConfig(repeats=2000, erasure_rate=0.01, post_selection=True)
task = QPUTask(program=ghz, config=config, qpu_id="qpu:uk:4:a63d17f4a0")
result = client.execute_tasks(task, include_metrics=True)[0]
print(result.result)
# {'c': {'00000': 1011, '11111': 989}}
metrics = result.metrics
for key in ("shots", "n_accepted", "n_rejected"):
    print(f"{key}: {metrics[key]}")
#      shots: 2115
# n_accepted: 2000
# n_rejected: 115

Results are clean again: only 00000 and 11111 remain. shots (2115) now exceeds repeats (2000): the extra shots were simulated to replace the rejected ones, and retries are uncapped, bounded only by the task’s execution timeout. For circuits or erasure rates where that retry cost could be significant, prefer schedule_tasks with polling over the blocking execute_tasks used in these examples, the same recommendation as for other potentially long-running QPU tasks; see Task status.

 

Post-selection cost

Because post_selection=True re-runs every rejected shot, its overhead grows with erasure_rate:

for rate in (0.002, 0.005, 0.01):
    config = EradoConfig(repeats=2000, erasure_rate=rate, post_selection=True)
    task = QPUTask(program=ghz, config=config, qpu_id="qpu:uk:4:a63d17f4a0")
    result = client.execute_tasks(task, include_metrics=True)[0]
    print(f"{rate}: {result.metrics['shots']}")
# 0.002: 2016
# 0.005: 2046
#  0.01: 2115
Bar chart showing post-selection retry overhead as a percentage of requested repeats, increasing from under 1% at erasure_rate=0.002 to over 5% at erasure_rate=0.01. Bar chart showing post-selection retry overhead as a percentage of requested repeats, increasing from under 1% at erasure_rate=0.002 to over 5% at erasure_rate=0.01.

At erasure_rate=0.01 the overhead here is modest (under 6%), but this assumes accurate erasure detection: see Detection accuracy: false_positive_rate and false_negative_rate below for what happens when it isn’t.

 

Detection accuracy: false_positive_rate and false_negative_rate

false_positive_rate and false_negative_rate model imperfect erasure detection, and affect post_selection=True in two different ways. This compares the same erasure_rate=0.01 run above against a high false_negative_rate and a high false_positive_rate (values chosen to make the effect visible; real detector error rates are typically much lower):

scenarios = {
    "clean": dict(false_positive_rate=0.0, false_negative_rate=0.0),
    "high false_negative_rate": dict(false_negative_rate=0.5),
    "high false_positive_rate": dict(false_positive_rate=0.1),
}
for name, rates in scenarios.items():
    config = EradoConfig(
        repeats=2000, erasure_rate=0.01, post_selection=True, **rates
    )
    task = QPUTask(program=ghz, config=config, qpu_id="qpu:uk:4:a63d17f4a0")
    result = client.execute_tasks(task, include_metrics=True)[0]
    counts = result.result["c"]
    leaked = sum(n for o, n in counts.items() if o not in ("00000", "11111"))
    print(name, "- shots:", result.metrics["shots"], "leaked outcomes:", leaked)
#                    clean - shots: 2115 leaked outcomes: 0
# high false_negative_rate - shots: 2079 leaked outcomes: 21
# high false_positive_rate - shots: 3628 leaked outcomes: 0

With false_negative_rate=0.5, a small number of shots (21 out of 2000 here) leak through post_selection=True with corrupted outcomes: the erasure genuinely happened but wasn’t detected, so it was never rejected. This is exactly the limitation described in false_negative_rate’s row above: post-selection can only discard what it detects. With false_positive_rate=0.1 instead, the results stay perfectly clean (only 00000 and 11111), but shots jumps to 3628 (an 81% overhead, versus under 6% with no false positives): clean shots are being incorrectly flagged and discarded too, at real cost but no accuracy loss.

Log-scale bar chart comparing GHZ circuit outcome counts for clean post-selection, false_negative_rate=0.5, and false_positive_rate=0.1. Only the false_negative_rate series shows counts at the corrupted outcomes; the other two are clean. Log-scale bar chart comparing GHZ circuit outcome counts for clean post-selection, false_negative_rate=0.5, and false_positive_rate=0.1. Only the false_negative_rate series shows counts at the corrupted outcomes; the other two are clean.

Only the false_negative_rate series has any counts at the corrupted outcomes; clean and false_positive_rate sit exactly on top of each other at 00000 and 11111, which is the visual signature of “costs more, but doesn’t corrupt results.”

 

Idling error

Configuring idling_error adds extra erasable gate positions during idle periods:

config = EradoConfig(
    repeats=2000,
    erasure_rate=0.01,
    idling_error=IdlingErrorConfig(),
)
task = QPUTask(program=ghz, config=config, qpu_id="qpu:uk:4:a63d17f4a0")
result = client.execute_tasks(task, include_metrics=True)[0]
metrics = result.metrics
for key in ("n_erasable_gates", "circuit_depth"):
    print(f"{key}: {metrics[key]}")
# n_erasable_gates: 17
#    circuit_depth: 6

Compare this to n_erasable_gates=5 (and the same circuit_depth=6) for the identical circuit without idling_error (the earlier examples above): padding more than triples the number of erasable gate positions here without adding depth, since the scheduler fills idle time already present within existing layers rather than adding new ones. More erasable gates at the same erasure_rate means more chances for an erasure. idling_error can be combined with post_selection=True in the same way as the plain-erasure example above.

Log-scale bar chart comparing GHZ circuit outcome counts with and without idling_error at the same erasure_rate=0.01, showing visibly larger counts at most of the corrupted outcomes with idling_error enabled. Log-scale bar chart comparing GHZ circuit outcome counts with and without idling_error at the same erasure_rate=0.01, showing visibly larger counts at most of the corrupted outcomes with idling_error enabled.

Most of the corrupted outcomes are visibly taller with idling_error enabled, tracking the roughly 3x jump in n_erasable_gates shown above.