Hardware Execution Guide¶
The hardware package provides the full stack from circuit compilation
to QPU execution, noise modelling, classical reference computation, and
multi-backend support, including gate-model, photonic, analog and annealing
adapters. The generated module catalog records the
current source inventory.
Why this page exists¶
This page gives operators and integrators one entry point for how experiments flow from circuit definition to reference comparisons. It is the practical route for teams that need to control submission policy, evidence classes, and reproducibility boundaries before running local or provider workloads.
Hardware evidence status:
| Device | Family | Campaign | Highlight |
|---|---|---|---|
ibm_fez |
Heron r2, 156 q | Legacy March 2026 baseline artifacts | Artifact-backed Bell/QKD/VQE/ZNE/UPDE observations; quote only rows named in the hardware ledger. |
ibm_kingston |
Heron r2, 156 q | April 2026 Phase 1, 342 circuits | Promoted raw-count DLA parity dataset: peak \(+17.48\,\%\) at depth 6, reproduced by scripts/run_dla_parity_suite.py. |
V2, frontier, queued-job, placeholder, and aggregate-only IBM outputs are not promoted unless the hardware ledger names raw counts, private retrieval map, analysis code, and review status.
Recoverable asynchronous batches¶
AsyncHardwareRunner.submit_one_async accepts an optional
ProviderJobJournal and canonical UUID attempt_id. Supply both together.
The journal records the exact compiled native QPY batch, backend name, requested
shots and experiment before dispatch. A batch is bounded to256 circuits and
sixteen mebibytes of encoded QPY. Existing calls without these arguments retain
the original memory-only behaviour.
An attempted dispatch changes the durable state to submission_unknown before
crossing the provider boundary. A lost response or process exit cannot turn that
state into a new submission. recover_job retrieves an existing stored handle;
when no handle was returned, an operator must supply the original provider job
ID. Recovery compares the provider's native inputs, target and shots with the
stored request. A mismatch leaves the original attempt unchanged. This is
read-only reconciliation, not another submission or renewed spending authority.
wait_for_job_async retains the original SDK result before decoding counts,
then records a completed batch only after every publication and sample total
matches. Repeated reads of a completed attempt use detached journal results.
observe_job_async reads status without submitting. cancel_job_async first
records cancellation intent; acknowledgement alone remains
cancellation_requested until the provider confirms cancellation. A concurrent
completed result remains available. Partial results and previous observations
stay in ordered history. Billing stays unknown: completion, cancellation and
an empty response do not establish the provider's actual debit.
The journal uses local SQLite transactions with full synchronisation and an explicit schema version. Keep its directory under the operator's own custody. Unknown journal versions refuse rather than migrate automatically. Local SDK sampling and transport fault tests qualify this lifecycle contract; they do not prove a physical provider session, execution or billing record.
Native workload and result semantics¶
HAL workload builders accept capture_semantics=True to attach a source-bound
request. The resulting QuantumJobRef.submission records the original encoded
program, requested/effective sample count and selected target.
QuantumJobResult.provider_observation carries the matching native output.
The existing raw payload and legacy counts view keep their established format;
the new companion uses schema provider_semantics.v1.
Gate-model requests use WorkloadSemantics from
hardware.provider_semantics. Qiskit QPY preserves native parameter UUIDs,
shared instruction uses and global-phase parameters. Supply one complete finite
parameter_bindings mapping to the QPY builder for execution; binding leaves
the stored original source unchanged. The QASM 3 builder accepts an already
bound circuit. Braket preserves shared native symbols and their supplied
bindings in its OpenQASM source. The native SDK owns binding and compilation.
The supported measurement contract is static final readout with explicit qubit-to-classical-bit assignments. Partial, permuted and multiple-register Qiskit measurements retain their declared order and marginals. Dynamic control, reset, operations after measurement and unsupported instructions are refused before backend execution. A changed or incompatible selected target cannot reuse an admitted compiled payload or trigger an untargeted compilation retry.
For example, this local Aer run prepares q2=1, q0=0 and measures them into
c0, c1. Qiskit places c1 on the left of its count string:
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
from scpn_quantum_control.hardware.hal import HardwareAbstractionLayer
from scpn_quantum_control.hardware.hal_qiskit import (
QiskitAerHALAdapter, qiskit_circuit_to_workload,
)
circuit = QuantumCircuit(3, 2)
circuit.x(2)
circuit.measure(2, 0)
circuit.measure(0, 1)
hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(QiskitAerHALAdapter(
hal.profile("local_qiskit_aer"),
backend=AerSimulator(max_parallel_threads=1, seed_simulator=7),
))
workload = qiskit_circuit_to_workload(
circuit, workload_id="partial_readout", shots=32, capture_semantics=True,
)
job = hal.submit("local_qiskit_aer", workload)
result = hal.result(job)
assert result.counts == {"01": 32}
assert job.submission.request.measurement_map == ((2, 0), (0, 1))
assert result.provider_observation.measurement_map == ((2, 0), (0, 1))
Braket native readout for the same ordered wires uses "10": its first
classical output is on the left. GateModelObservation.raw_counts retains the
provider labels separately from the validated count view. A Qiskit Runtime
result must contain exactly one PUB for the submitted circuit. Native register
BitArray buffers are retained as immutable bytes with their bit width, shape
and register name; padding, sample count and registration must agree. Multiple
PUBs or broadcast parameter axes cannot masquerade as a single observation.
Other modalities use ModalitySemantics and output types from
hardware.provider_modalities:
| Modality | Workload builder | Native observation |
|---|---|---|
| Photonic | quandela_perceval_workload |
PhotonicObservation retains ordered mode occupations, including occupations above one, original native labels and exact occurrences. |
| Analog | pasqal_pulser_workload, quera_bloqade_workload |
AnalogObservation retains native site order and the original count or per-shot readout channel, including repeated samples. Readout polarity and atom-loss interpretation remain unknown. |
| Annealing | dwave_bqm_workload |
AnnealingObservation retains native variable order, BINARY/SPIN samples, occurrences and available energies. Structured records also retain their exact dtype, shape and bytes. Missing energies remain unknown. |
These native axes represent modes, sites or variables. The historical workload
width field does not turn them into gate-model qubits. In particular, the legacy
binary count projection of a SPIN sample does not replace its native -1/+1
values. Incompatible output channels, widths, types or occurrence totals refuse
without replacing earlier retained results.
Use requested_target with semantics capture to require an exact selected
target. An explicitly empty selector is invalid. Submission provenance
distinguishes a native SDK target from an adapter selector, and target-compiled
data from native-provider or caller-precompiled input. A selector or opaque
prepared-sequence digest does not establish physical placement or calibrated
device compilation. Cloud routes retain their existing explicit approval gate.
Adapters that do not declare support refuse a semantics companion before
submission; omitting capture keeps the established legacy route.
Local Aer, Runtime on a local Aer backend and Braket Local runs exercise installed gate-model software. IQM-compatible Aer execution does not qualify the IQM SDK or an IQM device. Controlled photonic, analog and annealing I/O fixtures verify the adapter boundary; installed vendor SDK and physical-device qualification require their own evidence. No hardware result or performance claim follows from native semantics capture alone.
Operator admission before transport¶
Configure HardwareAbstractionLayer(profiles, operator_policy=policy) with an
immutable OperatorPolicy from hardware.operator_policy_contracts. Every
submission through that configured HAL then requires an OperatorRequest and
a known, request-bound PricingEstimate. The request binds the original
workload source and complete native semantics, exact backend, native target,
region, shots, declared concurrency, declared time limit and unattended flag.
hal.assess_operator_policy(backend_id, workload, request, estimate=estimate)
returns an immutable offline verdict with ordered refusal reasons and rejected
substitutions. Optional now is an explicit UTC-second clock for offline
assessment. hal.submit(..., operator_request=request, pricing_estimate=estimate)
reassesses using current UTC before adapter transport and accepts no caller
clock or stored decision. Existing cloud approval and native capability checks
remain required.
Equality at a ceiling is admitted; excess refuses without changing shots or other values. Target and region must agree with the original native workload and governing profile. Missing cloud target/region bindings, a future or expired policy, and unknown, stale, future, wrong-currency or request-mismatched pricing refuse before transport. Amounts are nonnegative decimal strings with at most nine fractional digits; no floating-point price, exchange rate or tariff is inferred. Expiry is exclusive, including at the exact second.
This admission checks the declared plan. It does not authenticate the supplied estimate, predict elapsed time, charge an account or enforce cross-process concurrency. Configure this boundary for automated execution; the original unconfigured HAL keeps its established local and explicitly approved cloud routes. Passing policy admission does not prove device availability, account credit, calibration or a hardware observation.
For the complete original settings/provenance export and browser inspection, see Operator policy decisions.
Architecture¶
HAL job identity custody¶
QuantumWorkload.metadata contains application annotations, not execution
settings. It rejects these adapter-owned keys before any provider operation:
approval_id, provider_job_id, execution_mode, backend_name,
quantum_computer, ir_format, n_qubits, shots, target, workload_id
and broker. Put requested resources in the typed workload fields and route,
target and approval choices in the relevant adapter/router arguments. For
descriptive annotations, use application-specific names such as
campaign or analysis_target. The restriction applies even when the duplicate
value agrees with the typed setting; no value is silently discarded or replaced.
Job and result metadata remain allowed to carry adapter-owned provenance.
The Qiskit Runtime, AWS Braket, Azure Quantum, qBraid and Strangeworks adapters
resolve supplied handles against their retained submission record before result,
status or cancellation access, including cached results. Backend/workload
mismatches raise ValueError; an unknown job ID raises KeyError. Supplied
status or metadata annotations do not replace the stored expected shots,
qubit width or approval/provenance fields. Cancellation retains those fields.
This supports reconstructed handles only while the adapter still retains the
original record; it does not provide persistence or recovery after process loss.
For IBM Runtime and AWS Braket, cancel checks the provider state before and
after a cancellation request and reports the observed state. A job already
completed is not cancelled, and a request still reported as running is not
labelled cancelled. A locally decoded completed result remains retrievable
even when cancellation arrives later. This is an offline-tested lifecycle
contract, not proof of an actual provider race or successful hardware job.
IBM Runtime and AWS Braket adapters also accept a paired capability_probe
and max_calibration_age_seconds at construction. When configured, submit
calls the no-submit probe immediately before the provider run and rechecks its
timestamp against the current UTC time. It refuses an offline or uncertain
target, stale/missing/future calibration, wrong provider/backend/target/IR,
insufficient qubits, or an unknown/exceeded shot limit before provider run().
The returned job records the calibration timestamp, check time and age limit.
This opt-in gate relies on the caller's probe; it does not authenticate provider
metadata. Existing approval-only submissions without the pair remain possible
and make no fresh-calibration claim.
HardwareAbstractionLayer checks adapter-returned handles before exposing them:
submission must preserve the selected backend_id and requested workload_id;
result retrieval and cancellation must also preserve job_id. A mismatch raises
ValueError. Recovered handles are supported without a router-local submission
cache. Status and metadata may change as the job progresses; accepted results
retain the adapter's original counts, shots and metadata without rewriting.
The local deterministic, Qiskit Aer, Braket Local and Cirq adapters complete
synchronously. Their status, result and cancel methods require the
retained job, backend and workload identity. A cancellation request after
completion returns the existing completed handle and leaves its raw counts
intact; it does not relabel the job as cancelled. This local lifecycle rule
does not predict a provider's cancellation race or confer a hardware result.
The standard Cirq simulator result has no provider job ID; its HAL handle is
generated locally and labelled job_id_origin=hal_local_generated, without a
provider_job_id claim. Injected non-Cirq results still require a real
source-provided identifier.
QuantumJobResult rejects a positive observed shot total unless the returned
counts sum to it, including an empty count map. Partial status may remain in a
typed local result, but the result-pack bridge refuses to mint provenance
until the result status is completed.
These checks bind identities, not scientific fidelity, shot-setting agreement or provider authenticity. They neither submit a replacement job nor promote a cancellation request to confirmed cancellation. A mismatch detected after submit or cancel does not roll back that provider operation. Reconcile its outcome before any resubmission; do not automatically retry a rejected submit response.
Experiment Definition (experiments.py)
│
├── HardwareRunner (runner.py)
│ ├── connect() → IBM Runtime / AerSimulator
│ ├── transpile() → native gate set
│ ├── run_circuit() → JobResult
│ └── run_with_zne() → ZNE-mitigated result
│
├── Noise Model (noise_model.py)
│ └── heron_r2_noise_model() → NoiseModel (thermal + depolarizing)
│
├── Classical Reference (classical.py)
│ ├── classical_kuramoto_reference() → Euler integration
│ ├── classical_exact_diag() → full eigendecomposition
│ ├── classical_exact_evolution() → matrix expm
│ └── classical_brute_mpc() → brute-force MPC
│
├── Multi-Backend
│ ├── PennyLane (pennylane_adapter.py)
│ ├── Cirq (cirq_adapter.py)
│ ├── Trapped Ion (trapped_ion.py)
│ ├── GPU (gpu_accel.py, jax_accel.py)
│ └── Plugin Registry (plugin_registry.py)
│
└── Circuit Tools
├── Circuit Cutting (circuit_cutting.py, cutting_runner.py)
├── QASM Export (qasm_export.py)
├── Circuit Export (circuit_export.py)
└── QCVV (qcvv.py)
Prerequisites¶
IBM Quantum¶
- Account: https://quantum.cloud.ibm.com
- Credentials (use
ibm_cloudchannel, NOT deprecatedibm_quantum): - Install IBM runtime:
HardwareRunner (runner.py)¶
The primary execution interface. Handles authentication, backend selection, transpilation, job submission, and result collection.
Connection¶
from scpn_quantum_control.hardware import HardwareRunner
# Real hardware
runner = HardwareRunner(use_simulator=False)
runner.connect() # Authenticates with IBM_QUANTUM_TOKEN env var
# Local simulator (default)
runner = HardwareRunner(use_simulator=True, results_dir="results/")
runner.connect()
When use_simulator=True, uses AerSimulator with the Heron r2 noise
model for realistic local testing without QPU budget consumption.
Transpilation¶
Uses Qiskit's preset pass manager with Heron r2 target. Optimization level 3 performs heavy gate cancellation and routing.
Execution¶
result = runner.run_circuit(
circuit,
experiment_name="kuramoto_4osc",
shots=10000,
)
# result: JobResult with counts, wall_time_s, metadata
ZNE Execution¶
result = runner.run_with_zne(
circuit,
experiment_name="kuramoto_zne",
noise_scales=[1, 3, 5],
shots=10000,
)
Internally calls gate_fold_circuit from the mitigation package.
JobResult¶
| Field | Type | Description |
|---|---|---|
job_id |
str | IBM job ID or "simulator" |
backend_name |
str | Backend identifier |
experiment_name |
str | User-specified experiment name |
counts |
dict or None | Measurement counts |
expectation_values |
ndarray or None | Computed expectations |
metadata |
dict | Arbitrary metadata |
wall_time_s |
float | Total execution time |
timestamp |
str | ISO timestamp |
Results are serialised to JSON via to_dict() and saved to results_dir.
Noise Model (noise_model.py)¶
IBM Heron r2 calibration (ibm_fez, 2026-03-29 median snapshot; source:
backend.properties(datetime=2026-03-29), retrieved read-only 2026-07-18):
| Parameter | Value | Description |
|---|---|---|
| T1 | 146.7 us | Longitudinal relaxation |
| T2 | 109.3 us | Transverse relaxation |
| CZ error | 0.262% | Two-qubit gate error rate |
| Readout error | 1.5% | Measurement error rate |
| Single-gate time | 0.024 us | SX/X/RZ duration |
| Two-gate time | 0.068 us | CZ/ECR duration |
heron_r2_noise_model(t1_us, t2_us, cz_error, readout_error)¶
Constructs a Qiskit-Aer NoiseModel:
- Single-qubit gates: thermal relaxation only
- Two-qubit gates (ECR/CZ): thermal relaxation + depolarizing
- Readout: symmetric bit-flip error
Qiskit-Aer is imported lazily when the noise-model function is called.
Importing scpn_quantum_control.hardware does not require a working
local Aer installation unless a simulator noise model is actually built.
from scpn_quantum_control.hardware import heron_r2_noise_model
model = heron_r2_noise_model()
# Use with AerSimulator:
from qiskit_aer import AerSimulator
backend = AerSimulator(noise_model=model)
Classical Reference (classical.py)¶
Exact classical computations for hardware experiment comparison. Every quantum result should be compared against these references.
classical_kuramoto_reference(n_osc, t_max, dt, K=None, omega=None)¶
Euler integration of the classical Kuramoto model:
Returns {times, theta, R} — phase trajectories and order parameter.
Integration grid. The trajectory takes floor(t_max / dt) steps and reports
sample s at s · dt, which is where the integrator actually put the state. A
duration that is not a multiple of dt therefore ends short of t_max rather
than mislabelling its last sample: for t_max = 1, dt = 0.3 and omega = 1,
an uncoupled oscillator reaches phase 0.9 and the last reported time is 0.9.
t_max = 0 returns the initial condition alone. A quotient within
INTEGRATION_GRID_RELATIVE_TOLERANCE of an integer is snapped to it, because
0.3 / 0.1 is 2.9999999999999996 in binary and a bare floor would drop the
caller's last step.
The Rust kernel and the Julia symplectic and delayed kernels already report
s · dt, so all tiers return identical times for identical inputs. The shared
rule is exposed as integration_step_count(t_max, dt) and
integration_times(n_steps, dt).
Rust acceleration: scpn_quantum_engine.kuramoto_euler() at 33x
speedup for n >= 8.
classical_exact_diag(n, K=None, omega=None)¶
Full eigendecomposition of the XY Hamiltonian. Returns eigenvalues, eigenvectors, ground state, and ground energy.
For n <= 14: direct dense diagonalisation via numpy.linalg.eigh.
For n > 14: sparse ARPACK via scipy.sparse.linalg.eigsh.
classical_exact_evolution(n, t_max, dt, K=None, omega=None)¶
Matrix exponential evolution: psi(t+dt) = exp(-iHdt) psi(t).
Returns time series of R(t) and energy E(t) for direct comparison with Trotter evolution on quantum hardware.
Uses the same integration grid as classical_kuramoto_reference: the
propagator is applied floor(t_max / dt) times and sample s is reported at
s · dt. t_max = 0 returns the initial state alone rather than applying the
propagator once.
classical_brute_mpc(B_matrix, target, horizon)¶
Brute-force binary model predictive control: enumerate all 2^horizon on/off action sequences and select the one minimising the tracking cost
where u_t in {0, 1} switches the whole actuation vector on or off at
timestep t. The residual stays a vector, so the target's sign and its
direction relative to B_matrix both change the optimum; a norm-only cost
would treat target and -target as identical.
B_matrix is (dim, dim), target has length dim, and bit t of an index
into the returned all_costs carries u_t.
Rust acceleration: scpn_quantum_engine.brute_mpc() with rayon
parallel enumeration. See the performance table for the current measurement
status.
PennyLane Adapter (pennylane_adapter.py)¶
PennyLaneRunner exposes the same Kuramoto-XY Hamiltonian through any
PennyLane-compatible device. run_trotter() evaluates the energy after
Trotterized time evolution and reconstructs the Kuramoto order parameter
from local transverse expectations:
run_vqe() uses the optimized hardware-efficient ansatz for both the
energy objective and the post-optimization observable pass. The returned
order_parameter is therefore measured from the final ansatz via the
same X/Y phase reconstruction; it is not a sentinel, simulator-only
statevector value, or unmeasured placeholder.
Provider and plugin routing is intentionally delegated to PennyLane: unknown
device strings are forwarded to qml.device(...) so installed plugins can own
their validation. The adapter still fails closed before plugin dispatch for
empty device names, control-character payloads, invalid finite-shot counts,
non-finite physics inputs, non-square Kuramoto coupling matrices, and omega
vectors whose width does not match K; shots=None remains the
analytic/simulator route and finite-shot runs require a positive integer.
Experiments (experiments.py)¶
Pre-defined experiment configurations for systematic QPU characterisation.
ALL_EXPERIMENTS¶
Registry of all 20 experiment functions:
| Experiment | Qubits | Description |
|---|---|---|
kuramoto_4osc |
4 | Basic Trotter evolution, R(t) |
kuramoto_4osc_trotter2 |
4 | Suzuki-Trotter 2nd order |
kuramoto_4osc_zne |
4 | ZNE-mitigated Kuramoto |
kuramoto_8osc |
8 | 8-qubit Kuramoto dynamics |
kuramoto_8osc_zne |
8 | ZNE-mitigated 8-qubit |
vqe_4q |
4 | VQE ground state search |
vqe_8q |
8 | VQE with physics-informed ansatz |
vqe_8q_hardware |
8 | VQE targeting real QPU |
vqe_landscape |
4 | Energy landscape scan |
qaoa_mpc_4 |
4 | QAOA-based MPC |
upde_16_snapshot |
16 | Full 16-qubit UPDE state snapshot |
upde_16_dd |
16 | UPDE with dynamical decoupling |
noise_baseline |
4 | Noise characterisation baseline |
ansatz_comparison_hw |
4 | Compare ansatz architectures |
sync_threshold |
4 | Synchronisation threshold detection |
decoherence_scaling |
4 | Depth vs fidelity scaling |
zne_higher_order |
4 | Higher-order ZNE extrapolation |
bell_test_4q |
4 | CHSH Bell test on hardware |
correlator_4q |
4 | XY correlator measurement |
qkd_qber_4q |
4 | QBER measurement for BB84-family validation |
Each experiment function returns a dict with circuit, shots,
n_qubits, and experiment-specific metadata.
QPU Budget¶
Free tier: 10 minutes/month on ibm_fez (Heron r2, 156 qubits).
| Experiment | Circuits | Shots | QPU Seconds |
|---|---|---|---|
| kuramoto_4osc (1 step) | 3 | 10k | ~15 |
| vqe_4q (100 COBYLA iter) | ~100 | 10k | ~15 |
| qaoa_mpc_4 (p=1) | ~30 | 10k | ~100 |
| upde_16 snapshot | 3 | 20k | ~60 |
Multi-Backend Support¶
PennyLane Adapter (pennylane_adapter.py)¶
from scpn_quantum_control.hardware.pennylane_adapter import PennyLaneRunner
runner = PennyLaneRunner(K, omega, device="default.qubit")
result = runner.run_trotter(t=0.5, reps=2)
# result: PennyLaneResult(energy, order_parameter, n_qubits, device_name, statevector)
Device strings are trimmed before dispatch. Malformed native-gate payloads are
rejected before qml.device(...) is called, including unsupported gates, wrong
gate arities, non-integer or duplicate wires, wrong rotation-parameter counts,
boolean parameters, and non-finite rotation parameters. Vendor-specific keyword
arguments are forwarded verbatim; no allow-list is maintained in the adapter,
and mocked provider-breadth tests do not touch live hardware.
VQE via PennyLane optimisers:
Differentiable VQE surface:
result = runner.vqe_value_and_grad(params, ansatz_depth=1)
result.value # VQE energy
result.gradient # PennyLane autodiff gradient over ansatz parameters
result.method # "pennylane_autodiff"
For framework-native gradients that do not require PennyLane, use
scpn_quantum_control.differentiable.parameter_shift_gradient.
Requires: pip install pennylane
Cirq Adapter (cirq_adapter.py)¶
from scpn_quantum_control.hardware.cirq_adapter import CirqRunner
runner = CirqRunner(K, omega)
result = runner.run_trotter(t=0.5, reps=2)
Enables targeting Google Sycamore/Weber QPUs via Cirq.
Requires: pip install cirq-core
Trapped Ion (trapped_ion.py)¶
from scpn_quantum_control.hardware import transpile_for_trapped_ion, trapped_ion_noise_model
ion_circuit = transpile_for_trapped_ion(circuit, allow_proxy_basis=True)
model = trapped_ion_noise_model()
Representative target: all-to-all QCCD-style trapped-ion devices. The helper emits a CX-basis proxy for MS/RXX-style entangling operations, records that proxy in circuit metadata, and is not a vendor-native IonQ or Quantinuum compiler path.
GPU Acceleration (gpu_accel.py)¶
cuQuantum integration for large-scale statevector simulation. Falls back to CPU when CUDA is not available.
JAX Acceleration (jax_accel.py)¶
JAX-based compilation for VQE parameter optimisation. Enables automatic differentiation of quantum circuits.
Plugin Registry (plugin_registry.py)¶
Dynamic backend registration. Third-party backends register via:
from scpn_quantum_control.hardware.plugin_registry import register_backend
register_backend("my_backend", MyBackendClass)
Provider Route Inventory¶
build_provider_route_catalogue inventories every declared aggregator/provider
route without contacting a provider. It reads the declared route table and
recorded evidence only: no credential is read, no workload is submitted, and no
network call is made.
Each row is keyed by provider, broker, device, modality and observation date.
A route reached without a broker records broker as None, so the same
provider and device offered through a broker is a separate row rather than a
merged one.
The five-field inventory_key groups shared backend/profile identity, while
route_key prefixes it with route_id for unambiguous row identity. Declared
aliases such as strangeworks/ibm_quantum and strangeworks/qiskit_runtime
share the former but not the latter. Store or index rows by route_key (or
the exported route_id plus the five fields); never merge alias observations.
The inventory retains both rows without inventing different physical devices.
The export contract is provider_route_catalogue.v2. modality now comes
from the authoritative HAL profile (for example, superconducting_gate_model,
quantum_annealing or photonic_gate_model); target_family retains the
route's original family label. Dynamic broker profiles retain their declared
provider-agnostic modality rather than guessing one from the provider name.
Version 1 incorrectly used the target-family label as modality. Regenerate
inventories from route/profile sources when migrating; do not relabel a saved
v1 row as v2. Consumers must check the exported contract before interpreting
the inventory key. Raw provider observations are not rewritten.
Rows also expose sdk_package, adapter_module and
credential_configuration_refs. SDK names are declared dependencies, not
installation or availability checks. Credential configuration references point
to real adapter constructor inputs in module:Class.__init__.parameter form:
configured clients, factories, or explicitly named credential parameters.
They contain no values and do not inspect SDK stores or environment variables.
For example, direct IQM references IQMHALAdapter.__init__.backend; brokered
IQM references the broker's client/configuration inputs, not IQM's direct ones.
Authentication remains with the supplied client or SDK. Unknown/custom adapter
references are None, not an assertion that no credentials are needed.
Custom backend routes require explicit profiles= when no built-in profile
exists. Duplicate backend profiles and route/profile SDK disagreement are
refused. Profile metadata does not promote any verb to observed or ready.
observed_at, declared_on and observed_on must be real Gregorian calendar
dates in zero-padded ASCII YYYY-MM-DD format, with years 0001–9999. Impossible
dates, non-ASCII digits, whitespace and timestamps raise ValueError naming
the affected field. Optional evidence dates may remain None; when provided,
they are validated even for unknown or negative support. Validation does not
compare against the workstation clock or certify that an observation occurred.
from scpn_quantum_control.hardware.provider_capability_discovery import (
build_provider_route_catalogue,
)
catalogue = build_provider_route_catalogue(observed_at="2026-09-05")
direct = next(row for row in catalogue if row.route_id == "direct/iqm")
brokered = next(row for row in catalogue if row.route_id == "qbraid/iqm")
direct.is_direct # True; broker is None
brokered.broker # "qbraid"
direct.inventory_key # ("iqm", None, "iqm_cloud", "superconducting_gate_model", "2026-09-05")
direct.unverified # True until evidence is supplied
The original discovery facade re-exports the exact catalogue classes, constants
and builder from provider_capability_core; it does not keep a second registry.
Support is recorded twice, and unknown stays unknown¶
Every operation — metadata, compile, submit, retrieve, cancel and
result_formats — carries a RouteVerbSupport record holding source-declared
support and dated observed support in separate fields. None means unknown
and is preserved as unknown; it is never narrowed to True or False to make a
row look complete.
Provenance is mandatory for any positive claim. Declaring support requires the
source it was read from, the date it was read and a conformance owner. Recording
an observation requires the date and a canonical repository-relative
tests/test_*.py conformance owner,
and an observation that contradicts an explicit non-declaration is refused
outright rather than silently accepted.
Whitespace-only source labels are rejected. Catalogue rows require immutable
tuples of verb records and an actual boolean approval flag; strings such as
"false" are not interpreted as policy decisions.
from scpn_quantum_control.hardware.provider_capability_core import RouteVerbSupport
RouteVerbSupport(
verb="metadata",
declared=True,
declared_source="hardware/aggregators.py route table",
declared_on="2026-09-05",
observed=True,
observed_on="2026-09-05",
conformance_owner="tests/test_hardware_hal_iqm_adapters.py",
)
Positive records require explicit conformance_root (an absolute source checkout
directory) and conformance_owners when constructing the catalogue or an entry
directly. The latter maps (route_id, verb) to its independently reviewed test
owner. Do not construct this registry from the incoming claims: doing so would
merely echo their assertions. A positive record must match the exact route and
operation binding and resolve to a regular, non-symlink Python test file under
the given root. Missing owners, unregistered bindings, traversal paths and
relative roots raise ValueError. The current working directory is not used
as a default source root, and private absolute paths are not exported.
For example, a reviewed metadata-owner binding can be supplied as
conformance_owners={("direct/iqm", "metadata"):
"tests/test_hardware_hal_iqm_adapters.py"} together with the absolute checkout
path as conformance_root=Path(...). The caller must retain the independent
review and dated observation evidence. This lookup does not execute a test or
certify a real provider run. observed_verbs reflects supplied observations with
resolved owners; unverified=False is not a hardware-readiness or approval gate.
A route with no evidence reports unverified and every operation unknown;
unknown-only inventory needs no source checkout. A source declaration with a
resolved owner still does not imply observed support. Existing positive-evidence
callers must now supply the explicit context; a RouteVerbSupport alone remains
a provenance-shaped assertion, not a qualified catalogue row. Reconstructing a
positive row (including dataclass replacement) requires the context again.
Circuit Tools¶
Circuit Cutting (circuit_cutting.py, cutting_runner.py)¶
Decomposes large circuits (> available qubits) into subcircuits connected by classical communication. Enables running n-qubit circuits on n/2-qubit hardware with polynomial overhead.
from scpn_quantum_control.hardware.circuit_cutting import partition_circuit
from scpn_quantum_control.hardware.cutting_runner import CuttingRunner
subcircuits = partition_circuit(circuit, max_partition_size=8)
runner = CuttingRunner(backend)
result = runner.run_partitioned(subcircuits)
QASM Export (qasm_export.py)¶
Export circuits to OpenQASM 2.0/3.0 for platform-independent storage and submission to third-party systems.
Circuit Export (circuit_export.py)¶
Export circuits to JSON, LaTeX (Qiskit drawer), and SVG formats for documentation and publication.
QCVV (qcvv.py)¶
Quantum Characterisation, Verification, and Validation protocols. Randomised benchmarking and gate set tomography for hardware qualification.
Decoherence Reference¶
| Depth Range | Expected Error | Recommendation |
|---|---|---|
| < 50 | < 5% | Publishable as-is |
| 50-150 | 5-15% | Publishable with error bars |
| 150-250 | 15-25% | Apply ZNE mitigation |
| 250-400 | 25-40% | Qualitative trends only |
| > 400 | > 40% | Do not trust individual values |
On deep campaigns above this table (e.g. WIDTH-1 at transpiled depth 538–2103). This reference bounds the trust in individual per-circuit values. The maximum-width Kuramoto-XY sweep deliberately runs far past depth 400, and is consistent with — not a violation of — this rule: it reports a collective order parameter R(n) against an exact MPS baseline (a noise characterisation), never individual per-circuit values, and it carries an explicit no-advantage boundary (shallow 1-D circuits are classically simulable by construction). Read that campaign as "how the collective observable degrades with depth", exactly the regime where this table says individual values are untrustworthy. See
docs/campaigns/max_width_kuramoto_xy_prereg_2026-07-16.md.
Native Gate Set (Heron r2)¶
| Gate | Description | Duration |
|---|---|---|
| CZ | Two-qubit entangling (native) | 0.068 us |
| RZ(theta) | Z rotation (virtual) | 0 us |
| SX | sqrt(X) | 0.024 us |
| X | Pauli-X | 0.024 us |
| ID | Identity (delay) | 0.024 us |
Transpilation from Qiskit standard gates increases depth. Typical expansion: 1 CNOT → 2 SX + 1 CZ + RZ gates.
Rust Acceleration¶
The classical.py module transparently uses Rust via scpn_quantum_engine
when available:
| Python Function | Rust Function | Speedup | Method |
|---|---|---|---|
classical_kuramoto_reference |
kuramoto_euler, kuramoto_trajectory |
33x | rayon parallel Euler steps |
_expectation_pauli |
expectation_pauli_fast |
3-10x | Bitwise Pauli ops |
classical_brute_mpc |
brute_mpc |
5-50x | rayon parallel 2^horizon enumeration |
_state_order_param |
state_order_param_sparse |
2-5x | SIMD-friendly inner loop |
_order_parameter (Floquet) |
all_xy_expectations |
5-20x | Batch bitwise, single FFI call |
All Rust functions accept split real/imaginary arrays (no complex64 across FFI). Python fallback always available when the Rust crate is not installed.
Interpreting Results¶
Order parameter R from qubit expectation values:
where <X_i> = 2×P(|0>)_x - 1 from X-basis measurement. Requires 3
measurement bases (X, Y, Z) for full reconstruction.
Compare hw_R against exact_R (from classical_kuramoto_reference or
classical_exact_evolution) to quantify hardware error.
Testing¶
The hardware surface is covered by these test owners:
test_runner.py— HardwareRunner lifecycle, simulator mode, job serialisationtest_noise_model.py— NoiseModel construction, error rates, parameter overridestest_classical.py— Kuramoto reference, exact diag, evolution paritytest_experiments.py— Registered experiment definitions and circuit validitytest_pennylane_adapter.py— PennyLane Trotter, VQE, device selectiontest_cirq_adapter.py— Cirq Trotter, simulator paritytest_circuit_cutting.py— Partitioning, recombination, overhead boundstest_qcvv.py— RB, gate set tomography, fidelity extraction
DynQ Topology-Agnostic Qubit Placement¶
scpn_quantum_control.hardware.qubit_mapper (added April 2026) implements
the DynQ method (Liu et al., arXiv:2601.19635) for selecting an
execution region on a heavy-hex device based on the live calibration
data. The QPU is modelled as a graph weighted by inverse two-qubit gate
errors, and Louvain community detection partitions it into high-fidelity
sub-regions; the sub-region with the best composite quality score is
chosen for the circuit. Quality scoring is Rust-accelerated.
See dynq_qubit_mapping.md for the full
theory, the dynq_initial_layout() API, and a Qiskit transpiler
integration recipe.
GUESS Symmetry-Decay Error Mitigation¶
scpn_quantum_control.mitigation.symmetry_decay (added April 2026)
provides physics-informed zero-noise extrapolation specifically for the
XY Hamiltonian, using its conserved \(\sum Z_i\) as the guide observable.
GUESS is the recommended default for any SCPN Kuramoto-XY hardware run
because the symmetry observable is measured for free in the standard
Z-basis read-out, so the mitigation adds zero shot overhead.
See symmetry_decay_guess.md for the full
theory, API, and a worked example calibrating \(\alpha\) from the Phase 1
ibm_kingston dataset.
Phase 1 Campaign Protocol (April 2026)¶
The Phase 1 campaign is recorded in
<private-local-record> and IBM_EXECUTION_LOG.md, and
the analysis is reproduced by scripts/analyse_phase1_dla_parity.py.
The four sub-phases progressively increased the per-point repetition
count from 2 to 21 to drive the per-depth uncertainty below the 5 %
asymmetry signal:
| Sub-phase | Circuits | Wall time | Reps per (depth, sector) point |
|---|---|---|---|
| Pipe cleaner | 2 | ~0.1 s | sanity check |
| Phase 1 (A/B/C) | 42 | 44.1 s | 2 |
| Phase 1.5 (D/E) | 72 | 56.7 s | +4 → 6 |
| Phase 2 exhaust (F/G/H/I) | 138 | 97.5 s | +6 → 12 |
| Phase 2.5 final burn (J) | 90 | 65.1 s | +9 → 21 (at the 5 strongest depths) |
| Total \(n=4\) | 342 | ~264 s wall | up to 21 |
Headline result: \(+10.8\,\%\) mean asymmetry for depths \(\ge 4\), peak \(+17.48\,\%\) at depth 6, Welch combined \(p \ll 10^{-16}\).
Pipeline Performance¶
Measured on ML350 Gen8 (128 GB RAM, Xeon E5-2620v2):
| Operation | System | Wall Time |
|---|---|---|
HardwareRunner.connect (simulator) |
— | 50 ms |
runner.transpile (opt level 3) |
4 qubits | 120 ms |
runner.run_circuit (simulator) |
4 qubits, 10k shots | 800 ms |
classical_kuramoto_reference (Rust) |
8 oscillators | 0.3 ms |
classical_kuramoto_reference (Python) |
8 oscillators | 12 ms |
classical_exact_diag |
8 qubits | 15 ms |
classical_exact_evolution |
8 qubits | 120 ms |
heron_r2_noise_model |
— | 5 ms |
PennyLaneRunner.run_trotter |
3 qubits | 50 ms |
partition_circuit |
16 → 2×8 qubits | 25 ms |
dynq_initial_layout (156-qubit graph, 5-qubit circuit) |
— | < 100 ms |
learn_symmetry_decay (5 noise scales, Rust) |
— | < 1 µs |
guess_extrapolate_batch (1,000 observables, Rust) |
— | < 50 µs |
hypergeometric_envelope (10,000 points, Rust) |
— | 2.6 ms |
ici_three_level_evolution (2,000 points, Rust) |
— | 0.04 ms |