Hardware Execution Guide

July 7, 2026 · View on GitHub

The hardware package provides the full stack from circuit compilation to QPU execution, noise modelling, classical reference computation, and multi-backend support. 17 modules (April 2026: added qubit_mapper.py for DynQ topology-agnostic placement) covering IBM superconducting, trapped ion, PennyLane, Cirq, GPU acceleration, and circuit cutting.

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:

DeviceFamilyCampaignHighlight
ibm_fezHeron r2, 156 qLegacy March 2026 baseline artifactsArtifact-backed Bell/QKD/VQE/ZNE/UPDE observations; quote only rows named in the hardware ledger.
ibm_kingstonHeron r2, 156 qApril 2026 Phase 1, 342 circuitsPromoted raw-count DLA parity dataset: peak +17.48%+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.

Architecture

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

  1. Account: https://quantum.cloud.ibm.com
  2. Credentials (use ibm_cloud channel, NOT deprecated ibm_quantum):
    export IBM_QUANTUM_TOKEN="your-token-here"
    export IBM_QUANTUM_CRN="your-crn-instance-id"
    
  3. Install IBM runtime:
    pip install -e ".[ibm]"
    

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

transpiled = runner.transpile(circuit, optimization_level=3)

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

FieldTypeDescription
job_idstrIBM job ID or "simulator"
backend_namestrBackend identifier
experiment_namestrUser-specified experiment name
countsdict or NoneMeasurement counts
expectation_valuesndarray or NoneComputed expectations
metadatadictArbitrary metadata
wall_time_sfloatTotal execution time
timestampstrISO 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, February 2026 median):

ParameterValueDescription
T1300 usLongitudinal relaxation
T2200 usTransverse relaxation
CZ error0.5%Two-qubit gate error rate
Readout error0.2%Measurement error rate
Single-gate time0.06 usSX/X/RZ duration
Two-gate time0.66 usCZ/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:

d(theta_i)/dt = omega_i + sum_j K[i,j] * sin(theta_j - theta_i)

Returns {times, theta, R} — phase trajectories and order parameter.

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.

classical_brute_mpc(K, omega, horizon, theta_init)

Brute-force model predictive control: enumerate all 2^horizon action sequences and select the one maximising R(t_final).

Rust acceleration: scpn_quantum_engine.brute_mpc() with rayon parallel enumeration at 5-50x speedup.


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:

theta_i = atan2(<Y_i>, <X_i>)
R = |mean_i exp(i theta_i)|

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 19 experiment functions:

ExperimentQubitsDescription
kuramoto_4osc4Basic Trotter evolution, R(t)
kuramoto_4osc_trotter24Suzuki-Trotter 2nd order
kuramoto_4osc_zne4ZNE-mitigated Kuramoto
kuramoto_8osc88-qubit Kuramoto dynamics
kuramoto_8osc_zne8ZNE-mitigated 8-qubit
vqe_4q4VQE ground state search
vqe_8q8VQE with physics-informed ansatz
vqe_8q_hardware8VQE targeting real QPU
vqe_landscape4Energy landscape scan
qaoa_mpc_44QAOA-based MPC
upde_16_snapshot16Full 16-qubit UPDE state snapshot
upde_16_dd16UPDE with dynamical decoupling
noise_baseline4Noise characterisation baseline
ansatz_comparison_hw4Compare ansatz architectures
sync_threshold4Synchronisation threshold detection
decoherence_scaling4Depth vs fidelity scaling
zne_higher_order4Higher-order ZNE extrapolation
bell_test_4q4CHSH Bell test on hardware
correlator_4q4XY correlator measurement

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).

ExperimentCircuitsShotsQPU Seconds
kuramoto_4osc (1 step)310k~15
vqe_4q (100 COBYLA iter)~10010k~15
qaoa_mpc_4 (p=1)~3010k~100
upde_16 snapshot320k~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:

result = runner.run_vqe(ansatz_depth=1, maxiter=5, seed=42)

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)

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 RangeExpected ErrorRecommendation
< 50< 5%Publishable as-is
50-1505-15%Publishable with error bars
150-25015-25%Apply ZNE mitigation
250-40025-40%Qualitative trends only
> 400> 40%Do not trust individual values

Native Gate Set (Heron r2)

GateDescriptionDuration
CZTwo-qubit entangling (native)0.66 us
RZ(theta)Z rotation (virtual)0 us
SXsqrt(X)0.06 us
XPauli-X0.06 us
IDIdentity (delay)0.06 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 FunctionRust FunctionSpeedupMethod
classical_kuramoto_referencekuramoto_euler, kuramoto_trajectory33xrayon parallel Euler steps
_expectation_pauliexpectation_pauli_fast3-10xBitwise Pauli ops
classical_brute_mpcbrute_mpc5-50xrayon parallel 2^horizon enumeration
_state_order_paramstate_order_param_sparse2-5xSIMD-friendly inner loop
_order_parameter (Floquet)all_xy_expectations5-20xBatch 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:

$ \text{R} = (1/\text{N}) \times |\text{sum\_i} (<\text{X\_i}> + \text{i} \times <\text{Y\_i}>)| $

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

72 tests across 8 test files:

  • test_runner.py — HardwareRunner lifecycle, simulator mode, job serialisation
  • test_noise_model.py — NoiseModel construction, error rates, parameter overrides
  • test_classical.py — Kuramoto reference, exact diag, evolution parity
  • test_experiments.py — All 19 experiment definitions, circuit validity
  • test_pennylane_adapter.py — PennyLane Trotter, VQE, device selection
  • test_cirq_adapter.py — Cirq Trotter, simulator parity
  • test_circuit_cutting.py — Partitioning, recombination, overhead bounds
  • test_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 Zi\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-phaseCircuitsWall timeReps per (depth, sector) point
Pipe cleaner2~0.1 ssanity check
Phase 1 (A/B/C)4244.1 s2
Phase 1.5 (D/E)7256.7 s+4 → 6
Phase 2 exhaust (F/G/H/I)13897.5 s+6 → 12
Phase 2.5 final burn (J)9065.1 s+9 → 21 (at the 5 strongest depths)
Total n=4n=4342~264 s wallup to 21

Headline result: +10.8%+10.8\,\% mean asymmetry for depths 4\ge 4, peak +17.48%+17.48\,\% at depth 6, Welch combined p1016p \ll 10^{-16}.

Pipeline Performance

Measured on ML350 Gen8 (128 GB RAM, Xeon E5-2620v2):

OperationSystemWall Time
HardwareRunner.connect (simulator)50 ms
runner.transpile (opt level 3)4 qubits120 ms
runner.run_circuit (simulator)4 qubits, 10k shots800 ms
classical_kuramoto_reference (Rust)8 oscillators0.3 ms
classical_kuramoto_reference (Python)8 oscillators12 ms
classical_exact_diag8 qubits15 ms
classical_exact_evolution8 qubits120 ms
heron_r2_noise_model5 ms
PennyLaneRunner.run_trotter3 qubits50 ms
`partition_circuit$16 → 2 \times 8 \text{qubits}25 \text{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