Backends & Plugin Registry

July 7, 2026 · View on GitHub

Two modules for runtime backend management:

  1. Backend dispatch (backend_dispatch.py) — switch between numpy, JAX, and PyTorch for array operations
  2. Plugin registry (hardware/plugin_registry.py) — legacy runner registration for direct adapter construction
  3. Provider-neutral backend registry (hardware/backends.py) — production routing descriptors for IBM Runtime, local Aer, Cirq, Amazon Braket, PennyLane, analogue, and hybrid compiler paths
  4. Hardware abstraction layer (hardware/hal.py) — provider-neutral workload, job, result, approval, and adapter protocol across cloud and simulator routes

Part 1: Backend Dispatch

scpn_quantum_control.backend_dispatch

Runtime array backend selection, inspired by TensorCircuit's tc.set_backend(). All array operations in downstream code use the selected backend.

API Reference

from scpn_quantum_control.backend_dispatch import (
    set_backend,
    get_backend,
    get_array_module,
    to_numpy,
    from_numpy,
    available_backends,
)
FunctionSignatureDescription
set_backend(name)str → NoneSet active backend: "numpy", "jax", "torch"
get_backend()() → strCurrent backend name
get_array_module()() → moduleActive array module (np, jnp, or torch)
to_numpy(arr)Any → ndarrayConvert any backend array to numpy
from_numpy(arr)ndarray → AnyConvert numpy to current backend
available_backends()() → list[str]List installed backends

Example

from scpn_quantum_control.backend_dispatch import (
    set_backend, get_backend, available_backends,
    get_array_module, to_numpy, from_numpy
)
import numpy as np

# Check what's available
print(available_backends())  # ['numpy', 'jax', 'torch'] (if installed)

# Default is numpy
assert get_backend() == "numpy"

# Switch to JAX
set_backend("jax")
xp = get_array_module()  # jax.numpy
arr = from_numpy(np.array([1.0, 2.0, 3.0]))
print(type(arr))  # jaxlib.xla_extension.ArrayImpl

# Convert back
arr_np = to_numpy(arr)
print(type(arr_np))  # numpy.ndarray

# Switch to PyTorch
set_backend("torch")
xp = get_array_module()  # torch
arr_t = from_numpy(np.array([1.0, 2.0]))
print(type(arr_t))  # torch.Tensor

# Reset to numpy
set_backend("numpy")

Part 2: Plugin Registry

scpn_quantum_control.hardware.plugin_registry

Extensible plugin architecture for quantum hardware backends. Register and discover backends at runtime without hard-coding imports.

Inspired by OpenFermion's plugin system (Google Quantum AI).

Built-In Backends

The registry includes lazy loaders for three backends:

BackendPackageProvides
qiskitqiskitTrotter circuits, IBM execution
pennylanepennylaneDifferentiable circuits and VQE value/gradient adapter
cirqcirq-coreGoogle Quantum circuits

These are loaded on first access — no import cost if unused.

API Reference

from scpn_quantum_control.hardware.plugin_registry import registry

PluginRegistry Methods

MethodSignatureDescription
list_backends()() → list[str]All registered + lazy-loadable names
available_backends()() → list[str]Only importable backends
is_available(name)str → boolCheck if backend is installed
get_runner(name, K, omega, **kw)(str, ndarray, ndarray, ...) → RunnerGet instantiated runner
register(name)str → decoratorDecorator for custom backends
register_class(name, cls)(str, type) → NoneProgrammatic registration

Runner Interface

Runners returned by get_runner implement:

class Runner:
    def __init__(self, K, omega, **kwargs): ...
    def run_trotter(self, t: float, reps: int) -> dict: ...
    def run_vqe(self, **kwargs) -> dict: ...  # optional

Example: Using Built-In Backends

from scpn_quantum_control.hardware.plugin_registry import registry
import numpy as np

n = 4
K = 0.45 * np.exp(-0.3 * np.abs(np.subtract.outer(range(n), range(n))))
np.fill_diagonal(K, 0.0)
omega = np.linspace(0.8, 1.2, n)

# List available backends
print(registry.available_backends())

# Use Qiskit backend
if registry.is_available("qiskit"):
    runner = registry.get_runner("qiskit", K, omega)
    result = runner.run_trotter(t=0.1, reps=5)
    print(f"Qiskit circuit depth: {result['depth']}")

Example: Custom Backend

from scpn_quantum_control.hardware.plugin_registry import registry

@registry.register("my_simulator")
class MySimulator:
    def __init__(self, K, omega, **kwargs):
        self.K = K
        self.omega = omega

    def run_trotter(self, t=0.1, reps=5):
        # Custom simulation logic
        return {"energy": -1.23, "method": "my_simulator"}

# Now usable via registry
runner = registry.get_runner("my_simulator", K, omega)
result = runner.run_trotter(t=0.1, reps=5)

Part 3: Provider-Neutral Quantum Backends

scpn_quantum_control.hardware.backends

The production registry exposes a single capability contract across the hardware and simulator surface. Registry lookup is deliberately non-authenticating and non-submitting: it imports at most the local SDK module needed for availability checks and never reads credentials, opens network sessions, or queues QPU jobs.

Built-In Production Descriptors

BackendProviderExecution modeSubmission policy
qiskit_ibmIBM Quantumcloud QPUapproval required
qiskit_aerlocal Qiskit Aerlocal simulatorno submission
cirqGoogle Cirqlocal simulator/exportno submission
braketAmazon Braketcloud QPU or managed simulatorapproval required
pennylanePennyLaneadapter routerprovider plugin decides
analog_kuramotointernal compileranalogue programme compilerno registry-time submission
hybrid_digital_analoginternal compilerhybrid compilerno registry-time submission

Cloud descriptors advertise that a submission interface exists, but they also set submit_requires_approval=True. Production execution code must pass through the explicit hardware approval scheduler before any live IBM or AWS work is attempted.

Descriptor API

from scpn_quantum_control.hardware import (
    describe_backend,
    describe_hal_backend_profile,
    list_hal_backend_descriptors,
    list_quantum_backends,
)

ibm = describe_backend("qiskit_ibm")
assert ibm.provider == "ibm_quantum"
assert ibm.can_submit is True
assert ibm.submit_requires_approval is True

local = describe_backend("qiskit_aer")
assert local.can_simulate is True
assert local.can_submit is False

for descriptor in list_quantum_backends():
    print(descriptor.name, descriptor.execution_mode, descriptor.available)

quera = describe_hal_backend_profile("quera_bloqade")
assert quera.adapter_module == "scpn_quantum_control.hardware.hal_quera_bloqade"
assert quera.submit_requires_approval is True

for route in list_hal_backend_descriptors():
    print(route.name, route.provider, route.workloads)

Every descriptor records:

FieldMeaning
nameregistry key used by routing code
providerprovider namespace, e.g. ibm_quantum, local_qiskit_aer, aws_braket
execution_modelocal simulator, cloud QPU, managed simulator, or adapter router
sdk_packagePython package expected for the route
adapter_modulerepository module that owns execution or export
availableimport-time availability, without credentials or network calls
can_simulate / can_submitwhether the descriptor exposes simulation or live submission semantics
submit_requires_approvalmandatory cloud-job approval flag
supports_*shot, statevector, mid-circuit, and pulse capability flags
capabilities / workloadsstable machine-readable routing tags

list_quantum_backends() describes plugin-registry entries and may probe import-time availability through each backend's is_available() method. list_hal_backend_descriptors() describes every built-in HAL profile using static metadata only. It is the safer selector input when an application needs the complete IBM, Braket, Azure, IonQ, Rigetti, Quantinuum, QuEra, qBraid, simulator, and future-profile route matrix without importing provider SDKs.

Legacy third-party plugins that only implement name and is_available() are still accepted. describe_backend() gives them a conservative descriptor with can_submit=False and submit_requires_approval=True until they implement a real descriptor() method returning QuantumBackendDescriptor.


Part 4: Hardware Abstraction Layer

scpn_quantum_control.hardware.hal

The HAL is the execution contract above provider descriptors. It decouples SCPN workloads from provider SDKs by using immutable metadata profiles and injected adapter objects. Constructing the HAL is offline and metadata-only: it does not import cloud SDKs, inspect credentials, open network sessions, or submit jobs.

Built-In HAL Profiles

Built-in route families include:

Backend idProviderBrokerModality
ibm_quantumIBMdirectsuperconducting gate model
ionq_cloudIonQdirecttrapped-ion gate model
aws_braket_ionqIonQAWS Brakettrapped-ion gate model
aws_braket_iqmIQMAWS Braketsuperconducting gate model
aws_braket_queraQuEraAWS Braketneutral-atom analogue
aws_braket_rigettiRigettiAWS Braketsuperconducting gate model
aws_braket_aqtAQTAWS Brakettrapped-ion gate model
aws_braket_sv1AWSAWS Braketmanaged statevector simulator
aws_braket_dm1AWSAWS Braketmanaged density-matrix simulator
aws_braket_tn1AWSAWS Braketmanaged tensor-network simulator
azure_quantum_quantinuumQuantinuumAzure Quantumtrapped-ion gate model
azure_quantum_quantinuum_emulatorQuantinuumAzure Quantumtrapped-ion emulator
azure_quantum_ionqIonQAzure Quantumtrapped-ion gate model
azure_quantum_ionq_simulatorIonQAzure Quantummanaged gate-model simulator
azure_quantum_rigettiRigettiAzure Quantumsuperconducting gate model
azure_quantum_rigetti_qvmRigettiAzure Quantummanaged QVM simulator
azure_quantum_pasqalPasqalAzure Quantumneutral-atom analogue
azure_quantum_pasqal_emulatorPasqalAzure Quantumneutral-atom emulator
azure_quantum_qci_previewQuantum CircuitsAzure Quantumprivate-preview superconducting route
quantinuum_cloudQuantinuumdirecttrapped-ion gate model
rigetti_qcsRigettidirectsuperconducting gate model
quera_bloqadeQuEradirectneutral-atom analogue
iqm_cloudIQMdirectsuperconducting gate model
pasqal_cloudPasqaldirectneutral-atom analogue
oqc_cloudOQCdirectsuperconducting gate model
qbraid_ionqIonQqBraidtrapped-ion gate model
qbraid_runtimedynamic catalogqBraidprovider-agnostic runtime
quandela_cloudQuandeladirectphotonic gate model
dwave_leapD-Wavedirectquantum annealing
strangeworks_computedynamic catalogStrangeworksprovider-agnostic compute
local_statevectorSCPNlocaldeterministic simulator
local_braket_svAWS Braket SDKlocalstatevector simulator
local_braket_dmAWS Braket SDKlocaldensity-matrix simulator
local_braket_ahsAWS Braket SDKlocalanalogue Hamiltonian simulator
local_qiskit_aerQiskit Aerlocalsimulator
local_cirqCirqlocalsimulator
local_pennylanePennyLanelocalsimulator

All cloud profiles set submit_requires_approval=True. A cloud workload fails closed unless the application has registered a concrete adapter and supplied an approval token for that submission. Provider credentials, queue selection, region policy, pricing, and detailed target selection belong inside the provider adapter, not the HAL registry.

Aggregator/provider matrix

built_in_aggregator_provider_routes() provides the broker-facing coverage view used by conformance tests and documentation. Each row resolves to a real HAL profile and adapter module, so broad aggregator catalogues are first-class without multiplying identical runtime adapters.

AggregatorProvider familyExecutable HAL route
DirectIBM Quantum / Qiskit Runtimeibm_quantum
AWS BraketAQT, IonQ, IQM, QuEra, Rigetti, Amazon simulatorsaws_braket_*
Azure QuantumIonQ, Quantinuum, Rigetti, Pasqal, QCI previewazure_quantum_*
qBraidAWS Braket, Azure Quantum, Equal1, IBM Quantum, IonQ, IQM, NEC vector annealer, OQC, Pasqal, Quantinuum, QuEra, Rigetti, QIR simulatorqbraid_ionq or qbraid_runtime
StrangeworksAQT, IonQ, IQM, Quantinuum, QuEra, Rigetti, IBM Quantum/Qiskit Runtime, AWS Braket, Azure Quantum, classical/HPC compute targetsstrangeworks_compute

Dynamic rows carry dynamic_catalog_target notes and still require the runtime caller to inject the authenticated provider backend or workspace. The matrix is metadata-only: it imports no provider SDKs, reads no credentials, authenticates nowhere, and submits no jobs.

resolve_aggregator_provider_route() is the fail-closed selector above this matrix. It returns the selected row, executable HAL profile, and backend descriptor for a requested aggregator/provider/IR tuple, and raises LookupError when no row exists or the requested IR format is unsupported.

HAL API

from scpn_quantum_control.hardware import (
    AzureQuantumHALAdapter,
    BraketLocalHALAdapter,
    CirqLocalHALAdapter,
    DWaveLeapHALAdapter,
    HardwareAbstractionLayer,
    IonQCloudHALAdapter,
    IQMHALAdapter,
    LocalDeterministicSimulator,
    OQCHALAdapter,
    PasqalPulserHALAdapter,
    PennyLaneDeviceHALAdapter,
    QbraidRuntimeHALAdapter,
    QuandelaPercevalHALAdapter,
    QuEraBloqadeHALAdapter,
    QuantinuumCloudHALAdapter,
    RigettiQCSHALAdapter,
    QiskitAerHALAdapter,
    QuantumWorkload,
    azure_openqasm3_to_workload,
    braket_circuit_to_workload,
    bloqade_ahs_workload,
    cirq_circuit_workload,
    dwave_bqm_workload,
    ionq_qis_workload,
    iqm_qiskit_workload,
    oqc_openqasm3_workload,
    pulser_sequence_workload,
    pennylane_gate_workload,
    qbraid_program_to_workload,
    quandela_perceval_workload,
    quantinuum_tket_workload,
    rigetti_quil_workload,
    qiskit_circuit_to_workload,
)

hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(LocalDeterministicSimulator(hal.profile("local_statevector")))

job = hal.submit(
    "local_statevector",
    QuantumWorkload(
        workload_id="demo",
        ir_format="mlir",
        program="module {}",
        n_qubits=4,
        shots=1024,
    ),
)
result = hal.result(job)

Injected adapters implement QuantumBackend.submit, status, result, and cancel. The HAL validates workload id, IR format, qubit limits, shot count, backend registration, and approval before delegating.

The Qiskit adapter layer provides:

  • QiskitAerHALAdapter for local qiskit-aer execution through HAL.
  • QiskitRuntimeHALAdapter for IBM Runtime Sampler execution through HAL.
  • qiskit_circuit_to_workload() for base64 QPY payloads. This is the preferred high-fidelity Qiskit transport because it preserves circuit structure without requiring lossy text conversion.
  • qiskit_circuit_to_qasm3_workload() for OpenQASM 3 payloads when the qiskit-qasm3-import optional importer is installed.
from qiskit import QuantumCircuit

qc = QuantumCircuit(1, 1)
qc.h(0)
qc.measure(0, 0)

hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(QiskitAerHALAdapter(hal.profile("local_qiskit_aer")))
result = hal.result(
    hal.submit(
        "local_qiskit_aer",
        qiskit_circuit_to_workload(qc, workload_id="h_sample", shots=256),
    )
)

The Braket adapter layer provides:

  • BraketLocalHALAdapter for local Braket SV/DM simulator execution through HAL.
  • BraketAwsHALAdapter for AWS Braket QPU or managed-simulator task submission through HAL with explicit approval tokens.
  • braket_circuit_to_workload() for OpenQASM 3 payloads generated from braket.circuits.Circuit.
  • snapshot_from_braket_device() for no-submit capability snapshots from injected Braket device metadata, including OpenQASM and AHS action support, shot limits, queue depth, online state, and calibration/update timestamp.
from braket.circuits import Circuit

circuit = Circuit().h(0).cnot(0, 1)
hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(BraketLocalHALAdapter(hal.profile("local_braket_sv")))
result = hal.result(
    hal.submit(
        "local_braket_sv",
        braket_circuit_to_workload(circuit, workload_id="bell", shots=256),
    )
)

The Azure Quantum adapter layer provides AzureQuantumHALAdapter and azure_openqasm3_to_workload(). The adapter calls Azure Target.submit(...) only after HAL approval has been supplied and only when a target object or explicit workspace/target factory was injected.

hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(
    AzureQuantumHALAdapter(
        hal.profile("azure_quantum_ionq_simulator"),
        target=target,
    )
)
job = hal.submit(
    "azure_quantum_ionq_simulator",
    azure_openqasm3_to_workload(
        "OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nx q[0];",
        workload_id="azure_x",
        n_qubits=1,
        shots=128,
    ),
    approval_id="approved-run",
)

The direct IonQ adapter layer provides IonQCloudHALAdapter and ionq_qis_workload(). It uses IonQ API v0.4 directly, targets a named IonQ backend such as simulator or qpu.forte-1, submits IonQ QIS JSON circuits, fetches sparse probability results, converts them to fixed-shot bitstring counts, and cancels via the v0.4 job status endpoint. API keys are supplied by constructor argument or IONQ_API_KEY; they are never part of QuantumWorkload. snapshot_from_ionq_backend() provides the matching no-submit readiness path for injected IonQ backend metadata or REST JSON. It records backend name, qubit count, declared IonQ JSON/OpenQASM/QIR support, basis gates, shot and queue limits, online state, simulator flag, gateset, and calibration timestamp without creating an IonQ job.

hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(
    IonQCloudHALAdapter(
        hal.profile("ionq_cloud"),
        api_key=ionq_api_key,
        backend="simulator",
    )
)
job = hal.submit(
    "ionq_cloud",
    ionq_qis_workload(
        [{"gate": "h", "target": 0}, {"gate": "cnot", "control": 0, "target": 1}],
        workload_id="ionq_bell",
        n_qubits=2,
        shots=256,
    ),
    approval_id="approved-run",
)

The local Cirq adapter layer provides CirqLocalHALAdapter and cirq_circuit_workload(). It runs a caller-supplied Cirq circuit representation through an injected Cirq simulator or simulator factory, normalises measurement histograms into HAL counts, and uses the same submit/status/result/cancel lifecycle shape as cloud adapters without requiring a cloud approval token. Automatic circuit reconstruction remains explicit through circuit_factory.

hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(
    CirqLocalHALAdapter(
        hal.profile("local_cirq"),
        circuit_factory=cirq_circuit_factory,
        simulator_factory=cirq_simulator_factory,
    )
)
job = hal.submit(
    "local_cirq",
    cirq_circuit_workload(cirq_payload, workload_id="cirq_bell", n_qubits=2, shots=256),
)

The direct OQC adapter layer provides OQCHALAdapter and oqc_openqasm3_workload(). It consumes OpenQASM 3 programs, validates the program header before submission, calls an injected QCAAS-style client, normalises provider counts into HAL counts, and keeps OQC cloud execution approval-gated. Automatic client construction is calibration-gated.

snapshot_from_oqc_target() provides the matching no-submit readiness path for injected OQC target metadata or metadata JSON. It records target name, qubit count, declared OpenQASM/QIR support, native gate set, shot and circuit limits, queue depth, online state, simulator flag, topology, and calibration timestamp without calling QCAAS submission APIs.

hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(
    OQCHALAdapter(
        hal.profile("oqc_cloud"),
        client_factory=calibrated_oqc_client_factory,
        target="Lucy",
    )
)
job = hal.submit(
    "oqc_cloud",
    oqc_openqasm3_workload(openqasm3_program, workload_id="oqc_bell", n_qubits=2, shots=256),
    approval_id="approved-run",
)

provider_optional_dependency_matrix() gives an offline smoke matrix for every built-in HAL route. It uses import-spec discovery only; it does not import provider SDKs, read credentials, authenticate, create clients, or touch provider networks.

aggregator_provider_optional_dependency_matrix() joins the aggregator/provider route matrix to the same dependency evidence. This is the preflight surface for broker/provider combinations: it reports the route id, resolved HAL backend, SDK package, import names, supported IR formats, approval gate, and whether the row is a dynamic catalogue target.

probe_aggregator_provider_capability() is the next no-submit layer. It accepts an injected provider metadata probe, resolves the selected aggregator/provider route, rejects route mismatches and non-no-submit snapshots, and returns a ready/blocked/unknown decision based on online status, qubit count, and required IR support. The function does not create clients or submit jobs; provider SDK authentication remains inside the injected read-only probe. snapshot_from_azure_target(), snapshot_from_braket_device(), snapshot_from_dwave_solver(), snapshot_from_iqm_backend(), snapshot_from_ionq_backend(), snapshot_from_oqc_target(), snapshot_from_pasqal_target(), snapshot_from_qiskit_runtime_backend(), snapshot_from_qbraid_device(), snapshot_from_quandela_processor(), snapshot_from_quantinuum_backend(), snapshot_from_quera_bloqade(), snapshot_from_rigetti_qcs(), and snapshot_from_strangeworks_backend() are concrete metadata adapters for this contract. They consume injected SDK backend/device objects and record target name, qubit count, route-supported or declared IR formats, gate basis, queue depth, shot/circuit limits, online state, simulator state, and calibration timestamp when the provider object exposes those fields.

The same check is exposed as scpn-provider-smoke. In CI or operator preflight lanes, install scpn-quantum-control[providers] and run:

scpn-provider-smoke --format table
scpn-provider-smoke --format json --sdk-package qiskit-ibm-runtime --require-all
scpn-provider-smoke --aggregator-routes --aggregator qbraid --provider rigetti --ir-format quil --format json

The portable providers extra intentionally excludes the current D-Wave, IQM, and QuEra direct SDK extras because their dependency trees are not compatible with the shared development/application environment as one aggregate install. Use [dwave], [iqm], or [quera] in isolated runner environments when those direct routes are being exercised.

isolated_provider_smoke_lanes() and scpn-provider-smoke --plan-isolated emit deterministic runner commands for those conflict-prone SDK families. Each lane creates a provider-specific virtual environment, installs only the matching extra, and runs a filtered no-network smoke gate:

scpn-provider-smoke --plan-isolated --format table

python -m venv .venv-provider-iqm
.venv-provider-iqm/bin/python -m pip install -U pip
.venv-provider-iqm/bin/python -m pip install -e ".[iqm]"
.venv-provider-iqm/bin/scpn-provider-smoke --backend iqm_cloud --require-all

The same isolated lanes are available as the manual GitHub Actions workflow Provider Isolated Smoke. It is deliberately separate from the blocking CI gate and performs offline import checks only; it does not read credentials, authenticate, create provider clients, or submit jobs.

The direct Quandela adapter layer provides QuandelaPercevalHALAdapter and quandela_perceval_workload(). It consumes scpn.quandela.perceval.v1 photonic circuit plans, validates mode count, input occupations, beam-splitter and phase-shifter components, and postselection bounds, then executes through an injected Perceval processor or processor/sampler factory. Remote Quandela execution remains approval-gated and automatic processor construction is calibration-gated.

hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(
    QuandelaPercevalHALAdapter(
        hal.profile("quandela_cloud"),
        processor_factory=calibrated_quandela_processor_factory,
        sampler_factory=calibrated_perceval_sampler_factory,
        target="ascella",
    )
)
job = hal.submit(
    "quandela_cloud",
    quandela_perceval_workload(
        photonic_plan,
        workload_id="quandela_pair",
        n_modes=2,
        shots=256,
    ),
    approval_id="approved-run",
)

snapshot_from_quandela_processor() provides the matching no-submit readiness path for injected Quandela processor metadata or metadata JSON. It records processor name, mode count, declared Perceval/OpenQASM/MLIR support, optical component set, photonic feature flags, shot and circuit limits, queue depth, online state, simulator flag, topology, and calibration timestamp without calling processor or sampler APIs.

The direct D-Wave Leap adapter layer provides DWaveLeapHALAdapter and dwave_bqm_workload(). It consumes a scpn.dwave.bqm.v1 binary quadratic model payload, validates variable order, linear and quadratic biases, vartype, and read count, submits through an injected sampler or the D-Wave system SDK, normalises sample-set occurrences into HAL counts, and keeps Leap execution approval-gated.

hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(
    DWaveLeapHALAdapter(
        hal.profile("dwave_leap"),
        sampler_factory=calibrated_dwave_sampler_factory,
        solver="Advantage_system",
    )
)
job = hal.submit(
    "dwave_leap",
    dwave_bqm_workload(
        linear={"0": -1.0, "1": 0.5},
        quadratic={("0", "1"): -0.25},
        workload_id="dwave_pair",
        n_variables=2,
        reads=256,
    ),
    approval_id="approved-run",
)

snapshot_from_dwave_solver() provides the matching no-submit readiness path for injected D-Wave solver metadata or metadata JSON. It records solver name, qubit count, declared BQM/Ising/QUBO/MLIR support, annealing topology, read limits, queue/load estimate, online state, simulator flag, category, and last update timestamp without calling sampler APIs.

The direct IQM adapter layer provides IQMHALAdapter and iqm_qiskit_workload(). It follows the IQM Qiskit provider path lazily, accepts injected backend objects for tests or calibrated execution routes, encodes circuits as QPY-backed qiskit_qpy workloads, normalises job status and counts into HAL payloads, and keeps remote execution approval-gated. snapshot_from_iqm_backend() provides the matching no-submit readiness path for injected IQM backend metadata or metadata JSON. It records target name, qubit count, declared QPY/Qiskit/OpenQASM support, native gate set, shot and circuit limits, queue depth, online state, simulator flag, architecture name, and calibration timestamp without running a circuit.

hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(
    IQMHALAdapter(
        hal.profile("iqm_cloud"),
        server_url=iqm_server_url,
        quantum_computer="garnet",
    )
)
job = hal.submit(
    "iqm_cloud",
    iqm_qiskit_workload(qiskit_circuit, workload_id="iqm_bell", shots=256),
    approval_id="approved-run",
)

The direct Pasqal adapter layer provides PasqalPulserHALAdapter and pulser_sequence_workload(). It consumes pulser_sequence_plan_v1, validates register coordinates, Rabi envelope points, local detunings, interaction terms, and FIM feedback terms, submits through an injected Pasqal client or client factory, normalises result counters into HAL counts, and keeps remote execution approval-gated.

hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(
    PasqalPulserHALAdapter(
        hal.profile("pasqal_cloud"),
        client_factory=calibrated_pasqal_client_factory,
        target="FRESNEL",
    )
)
job = hal.submit(
    "pasqal_cloud",
    pulser_sequence_workload(pulser_plan, workload_id="pasqal_pair", n_qubits=2, shots=256),
    approval_id="approved-run",
)

snapshot_from_pasqal_target() provides the matching no-submit readiness path for injected Pasqal target metadata or metadata JSON. It records target name, atom count, declared Pulser/Pasqal IR/OpenQASM/MLIR support, supported bases, channel declarations, shot and sequence limits, queue depth, online state, simulator flag, lattice geometry, and calibration timestamp without calling Pasqal submission APIs.

The direct QuEra/Bloqade adapter layer provides QuEraBloqadeHALAdapter and bloqade_ahs_workload(). It consumes the repository's bloqade_ahs_plan_v1 neutral-atom export schema, validates atom geometry and piecewise schedules, runs an injected Bloqade local or remote routine with run(shots=..., name=...), normalises fetch()/report() bitstrings or count mappings into HAL counts, and cancels batches that expose cancel(). Automatic provider-object construction remains calibration-gated; production callers inject the calibrated Bloqade routine or a routine factory. snapshot_from_quera_bloqade() provides the matching no-submit readiness path for injected Bloqade routine metadata or metadata JSON. It records target name, atom count, declared Bloqade/Braket AHS/MLIR support, native analogue operations, shot and circuit limits, queue depth, online state, simulator flag, lattice geometry, and calibration timestamp without running a routine.

hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(
    QuEraBloqadeHALAdapter(
        hal.profile("quera_bloqade"),
        routine=calibrated_bloqade_routine,
        routine_name="aquila-approved-route",
    )
)
job = hal.submit(
    "quera_bloqade",
    bloqade_ahs_workload(
        bloqade_ahs_plan,
        workload_id="quera_rydberg_pair",
        n_qubits=2,
        shots=128,
    ),
    approval_id="approved-run",
)

The direct Quantinuum adapter layer provides QuantinuumCloudHALAdapter and quantinuum_tket_workload(). It keeps pytket and pytket-quantinuum loading lazy until a concrete adapter is registered, then follows the documented Quantinuum route: get_compiled_circuit(...), process_circuit(..., n_shots=...), circuit_status(...), get_result(...).get_counts(), and QuantinuumBackend.cancel(...). Direct Quantinuum execution is tket-native; OpenQASM 3, QIR, and MLIR route entries are registry-level translation targets and must be converted before submission. snapshot_from_quantinuum_backend() provides the matching no-submit readiness path for injected Quantinuum backend metadata. It records machine name, qubit count, declared tket/OpenQASM/QIR support, native gate set, mid-circuit measurement support, shot and batch limits, queue depth, online state, simulator flag, and calibration timestamp without processing a circuit.

quantinuum_machine = "H1-1E"
hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(
    QuantinuumCloudHALAdapter(
        hal.profile("quantinuum_cloud"),
        machine=quantinuum_machine,
    )
)
job = hal.submit(
    "quantinuum_cloud",
    quantinuum_tket_workload(
        {"name": "h_sample", "qubits": 1},
        workload_id="quantinuum_h",
        n_qubits=1,
        shots=128,
    ),
    approval_id="approved-run",
)

The direct Rigetti adapter layer provides RigettiQCSHALAdapter and rigetti_quil_workload(). It keeps pyQuil and QCS loading lazy until a concrete adapter is registered, then follows the documented pyQuil route: Program, wrap_in_numshots_loop(...), QuantumComputer.compile(...), QuantumComputer.run(...), and ro register readout extraction. Direct Rigetti execution is Quil-native; OpenQASM 3 and MLIR route entries are registry-level translation targets and must be converted before submission. snapshot_from_rigetti_qcs() provides the matching no-submit readiness path for injected Rigetti QCS QuantumComputer metadata or metadata JSON. It records target name, qubit count, declared Quil/OpenQASM support, native gate set, shot and queue limits, online state, simulator flag, compiler versions, and calibration timestamp without compiling or running a program.

rigetti_qc_name = "9q-square-qvm"
hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(
    RigettiQCSHALAdapter(
        hal.profile("rigetti_qcs"),
        quantum_computer_name=rigetti_qc_name,
    )
)
job = hal.submit(
    "rigetti_qcs",
    rigetti_quil_workload(
        "DECLARE ro BIT[1]\nH 0\nMEASURE 0 ro[0]",
        workload_id="rigetti_h",
        n_qubits=1,
        shots=128,
    ),
    approval_id="approved-run",
)

The PennyLane adapter layer provides PennyLaneDeviceHALAdapter and pennylane_gate_workload(). It executes a strict SCPN native-gate payload on a local PennyLane device such as default.qubit; unsupported gate names, invalid wire references, wrong arity, and malformed JSON are rejected before execution. HAL device names are trimmed and rejected when empty or when they contain control characters before qml.device(...) is called. Device keyword arguments are forwarded unchanged, so installed PennyLane plugins remain selectable while this route stays a local-device execution path rather than a provider hardware route. For differentiable-programming parity checks, the phase namespace also exposes check_pennylane_parameter_shift_agreement() to compare SCPN parameter-shift gradients with caller-supplied PennyLane/QNode gradient callables.

hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(PennyLaneDeviceHALAdapter(hal.profile("local_pennylane")))
result = hal.result(
    hal.submit(
        "local_pennylane",
        pennylane_gate_workload(
            [{"gate": "h", "wires": [0]}, {"gate": "cnot", "wires": [0, 1]}],
            workload_id="pl_bell",
            n_qubits=2,
            shots=256,
        ),
    )
)

The qBraid adapter layer provides QbraidRuntimeHALAdapter and qbraid_program_to_workload(). It accepts injected qBraid runtime devices or providers, supports provider lookup by qBraid device id, forwards the exact HAL program payload to device.run(...), and converts qBraid measurement counts back into QuantumJobResult. Use qbraid_ionq for the named IonQ broker route or qbraid_runtime for qBraid's dynamic provider catalog. Cloud submission remains approval-gated by HAL. For pre-submit capability checks, use snapshot_from_qbraid_device() with an authenticated qBraid device object; the snapshot path reads metadata only and fails closed when the target does not declare supported IR formats. qBraid catalogue program_specs are normalised into HAL IR tokens such as openqasm3, qiskit, cirq, quil, braket_ir, pennylane, pyqubo, tket, qir, and mlir, and snapshots record the resolved broker route.

hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(QbraidRuntimeHALAdapter(hal.profile("qbraid_ionq"), device=qbraid_device))
job = hal.submit(
    "qbraid_ionq",
    qbraid_program_to_workload(
        "OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nx q[0];",
        workload_id="qbraid_x",
        ir_format="openqasm3",
        n_qubits=1,
        shots=128,
    ),
    approval_id="approved-run",
)

The Strangeworks adapter layer provides StrangeworksComputeHALAdapter and strangeworks_program_to_workload(). It mirrors the qBraid dynamic-catalog pattern: production callers inject an authenticated Strangeworks backend, workspace, or factory; the adapter forwards the HAL program payload to the selected backend, records the resolved backend id, and normalises measurement counts into QuantumJobResult. For pre-submit capability checks, use snapshot_from_strangeworks_backend() with an injected Strangeworks backend; it reads the backend metadata surface and feeds the route-bound no-submit readiness decision before any workload path is eligible. Strangeworks catalogue program declarations such as available_programs, program_specs, and supported_programs are normalised into HAL IR tokens, snapshots record the resolved broker route, and backend state/availability metadata is consumed as readiness evidence without submission.

hal = HardwareAbstractionLayer.with_builtin_profiles()
hal.register_backend(
    StrangeworksComputeHALAdapter(
        hal.profile("strangeworks_compute"),
        workspace=strangeworks_workspace,
        backend_id="rigetti.qvm",
    )
)
job = hal.submit(
    "strangeworks_compute",
    strangeworks_program_to_workload(
        "DECLARE ro BIT[2]",
        workload_id="sw_rigetti",
        ir_format="quil",
        n_qubits=2,
        shots=128,
    ),
    approval_id="approved-run",
)

Comparison

FeatureBackend DispatchPlugin RegistryHALTensorCircuit
Array backend switchingYesNoNoYes
Hardware backend registryNoYesYesNo
Provider-neutral execution contractNoNoYesNo
Cloud approval gateNoPartialYesNo
Custom backendsNoYes (decorator)Yes (protocol adapter)No
Lazy loadingN/AYesMetadata-onlyNo
JAX supportYesVia backendsVia adapterYes
PyTorch supportYesVia backendsVia adapterYes

References

  1. Zhang, S.-X. et al. "TensorCircuit: An open-source cloud-oriented quantum computing platform." arXiv:2205.10091 (2022).
  2. McClean, J. R. et al. "OpenFermion: The electronic structure package for quantum computers." Quantum Sci. Technol. 5, 034014 (2020).

See Also