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 Accessing Results, or the
Examples below, for what that looks like). Job submission, polling, and result
retrieval otherwise follow the same Job patterns as for QPU tasks.
Note
EradoConfig requires oqc-qcaas-sdk version 0.1.22 or later; see
oqc-qcaas-sdk on PyPI for the latest release.
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 and its supporting types are re-exported from the SDK package:
>>> from oqc_qcaas_sdk import EradoConfig, ErasureModel, IdlingErrorConfig
They can equally be imported from compiler_config, where they are defined:
>>> from compiler_config.experimental.erado.config import EradoConfig, ErasureModel
EradoConfig is a dataclass, so a configuration can be built and checked without a live
connection:
>>> config = EradoConfig(
... repeats=1000,
... erasure_rate=0.01,
... erasure_model=ErasureModel.TRANSPILER_PASS,
... )
>>> config.erasure_model
<ErasureModel.TRANSPILER_PASS: 'transpiler_pass'>
>>> config.validate() # raises ValueError for out-of-range parameters
The full set of parameters is:
Parameter |
Description |
Notes |
|---|---|---|
|
Number of shots. |
Type:
int (>= 1)Default:
1000 |
|
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. |
|
When |
Type:
boolDefault:
False |
|
Simulation method. |
Type:
ErasureModelDefault:
CIRCUIT_SAMPLERCIRCUIT_SAMPLER is more memory-hungry at scale; prefer TRANSPILER_PASS for
larger circuits. |
|
When |
Type:
boolDefault:
False |
|
Return per-shot circuit fidelity alongside results. Not currently supported; |
Type:
boolDefault:
False |
|
Rate at which erasure checks incorrectly signal an erasure. |
Type:
float (0-1)Default:
0.0 |
|
Rate at which erasure checks miss a true erasure. This is what limits how effective
|
Type:
float (0-1)Default:
0.0 |
|
An optional |
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 job, for example to make erasures more
visible, will cause the job 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.
Parameter |
Description |
Notes |
|---|---|---|
|
Maximum number of idle gates inserted into an idle period. |
Type:
intDefault:
14Values below 2 insert no idle gates.
|
|
Qiskit gate inserted during idle periods. |
Type:
str, one of "id", "x", "y", "z"Default:
"id" |
|
Unit duration assigned to each existing circuit gate. |
Type:
float (> 0)Arbitrary relative units, not physical time.
Default:
1.0 |
|
Unit duration assigned to each inserted idle gate. |
Type:
float (> 0)Arbitrary relative units, not physical time.
Default:
0.8 |
|
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.
All examples share these imports and this circuit:
import asyncio, os
from oqc_qcaas_sdk import OqcSdk, EradoConfig, ErasureModel, IdlingErrorConfig
ERADO_QPU_ID = "qpu:uk:4:a63d17f4a0"
URL = os.environ["OQC_URL"]
TOKEN = os.environ["OQC_AUTHENTICATION_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;
"""
Noiseless baseline¶
>>> async def run():
... async with OqcSdk(url=URL, authentication_token=TOKEN) as sdk:
... job = sdk.create_job(
... program=GHZ,
... qpu_id=ERADO_QPU_ID,
... config=EradoConfig(repeats=2000, erasure_rate=0.0),
... )
... proxy = await job.execute(timeout_s=300)
... if job.error:
... raise RuntimeError(f"[{job.error.error_code}] {job.error.error_message}")
... return proxy.results()[0].data["c"]
>>> counts = run_async(run())
>>> set(counts) == {"00000", "11111"}
True
proxy.results()[0].data holds one entry per classical register, each mapping bitstring to
count. For this run, counts was:
{'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
error check shown above for brevity; see Exception-based Error Handling in the Jobs documentation for
the full pattern.
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. Pass
fetch_diagnostics=True to create_job() to populate job.metrics after execution:
>>> async def run():
... async with OqcSdk(url=URL, authentication_token=TOKEN) as sdk:
... job = sdk.create_job(
... program=GHZ,
... qpu_id=ERADO_QPU_ID,
... config=EradoConfig(
... repeats=2000,
... erasure_rate=0.01,
... erasure_model=ErasureModel.CIRCUIT_SAMPLER,
... ),
... fetch_diagnostics=True,
... )
... proxy = await job.execute(timeout_s=300)
... return proxy.results()[0].data["c"], job.metrics
>>> counts, metrics = run_async(run())
>>> metrics["n_accepted"] == 2000 and metrics["n_rejected"] > 0
True
For this run, counts and the relevant entries of metrics were:
{'00000': 1005, '10000': 14, '11000': 12, '11100': 11, '11110': 14, '11111': 944}
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 the returned data 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.
Erasure with post-selection¶
Setting post_selection=True discards those corrupted shots and re-runs until repeats
accepted shots are collected:
>>> async def run():
... async with OqcSdk(url=URL, authentication_token=TOKEN) as sdk:
... job = sdk.create_job(
... program=GHZ,
... qpu_id=ERADO_QPU_ID,
... config=EradoConfig(repeats=2000, erasure_rate=0.01, post_selection=True),
... fetch_diagnostics=True,
... )
... proxy = await job.execute(timeout_s=300)
... return proxy.results()[0].data["c"], job.metrics
>>> counts, metrics = run_async(run())
>>> set(counts) == {"00000", "11111"} and metrics["shots"] > 2000
True
For this run, counts and the relevant entries of metrics were:
{'00000': 1011, '11111': 989}
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, job.submit() followed by
await job.wait() is preferable to the blocking execute() used in these examples, the
same recommendation as for other potentially long-running QPU jobs; see
Re-running a Job in the Jobs documentation.
Post-selection cost¶
Because post_selection=True re-runs every rejected shot, its overhead grows with
erasure_rate:
>>> async def run():
... async with OqcSdk(url=URL, authentication_token=TOKEN) as sdk:
... for rate in (0.002, 0.005, 0.01):
... job = sdk.create_job(
... program=GHZ,
... qpu_id=ERADO_QPU_ID,
... config=EradoConfig(repeats=2000, erasure_rate=rate, post_selection=True),
... fetch_diagnostics=True,
... )
... await job.execute(timeout_s=300)
... print(f"{rate}: {job.metrics['shots']}")
>>> run_async(run())
0.002: 2016
0.005: 2046
0.01: 2115
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),
... }
>>> async def run():
... async with OqcSdk(url=URL, authentication_token=TOKEN) as sdk:
... for name, rates in scenarios.items():
... job = sdk.create_job(
... program=GHZ,
... qpu_id=ERADO_QPU_ID,
... config=EradoConfig(
... repeats=2000, erasure_rate=0.01, post_selection=True, **rates
... ),
... fetch_diagnostics=True,
... )
... proxy = await job.execute(timeout_s=300)
... counts = proxy.results()[0].data["c"]
... leaked = sum(n for o, n in counts.items() if o not in ("00000", "11111"))
... print(name, "- shots:", job.metrics["shots"], "leaked outcomes:", leaked)
>>> run_async(run())
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.
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:
>>> async def run():
... async with OqcSdk(url=URL, authentication_token=TOKEN) as sdk:
... job = sdk.create_job(
... program=GHZ,
... qpu_id=ERADO_QPU_ID,
... config=EradoConfig(
... repeats=2000,
... erasure_rate=0.01,
... idling_error=IdlingErrorConfig(),
... ),
... fetch_diagnostics=True,
... )
... await job.execute(timeout_s=300)
... return job.metrics
>>> metrics = run_async(run())
>>> metrics["n_erasable_gates"] > 5 and metrics["circuit_depth"] == 6
True
For this run the relevant entries of metrics were:
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.
Most of the corrupted outcomes are visibly taller with idling_error enabled, tracking the
roughly 3x jump in n_erasable_gates shown above.