Workflows and Examples¶
This page is the routing guide for the main paths through qiskit-qm-provider. Each section points to a deeper guide, API reference, and runnable examples.
1. Running Qiskit circuits on QM hardware or simulators¶
Get a backend from a provider, transpile circuits, then use QMBackend.run() or V2 primitives. Same Qiskit ergonomics; QOP executes the generated QUA underneath.
Large lists passed to backend.run are automatically split into several QUA programs when the batch exceeds max_circuits (default 30); see Backend — multi-circuit batches. Primitives and hybrid embedding are unaffected.
1.1 Local hardware with QMProvider¶
Create
QMProviderwith your QuAM state folder.Optionally pass custom
quam_cls/backend_cls.Transpile and run.
Guide: Providers — QMProvider
API: Providers reference
Examples:
examples/sampler_workflow.py,examples/estimator_workflow.py
1.2 QM SaaS simulator with QmSaasProvider¶
pip install qiskit-qm-provider[qm_saas]Create
QmSaasProvider.Call
get_backend()and run as usual.
Guide: Providers — QmSaasProvider
Examples: adapt
examples/sampler_workflow.py
1.3 IQCC devices with IQCCProvider¶
pip install qiskit-qm-provider[iqcc]Create
IQCCProvider.Obtain a
FluxTunableTransmonBackend:
machine = provider.get_machine(
"arbel",
quam_state_folder_path="/path/to/quam/state", # or QUAM_STATE_PATH
)
backend = provider.get_backend(
"arbel",
quam_state_folder_path="/path/to/quam/state",
)
# or: backend = provider.get_backend(machine)
Guide: Providers — IQCCProvider
Example:
examples/iqcc_t1_experiment.py
2. Calibrations and custom gates¶
2.1 Pulse-level workflows (Qiskit 1.x legacy)¶
When Pulse is available:
Use
FluxTunableTransmonBackendfor QuAM ↔ Pulse channel mapping.Convert gate pulse schedules via
schedule_to_qua_macro.Seed macros with
add_basic_macros(flux-tunable defaults — see Providers guide).
Pulse caveat: supported for gate schedules only. Qiskit Pulse Measure / measurement instructions are not supported. Use circuit-level measure + quantum_circuit_to_qua and read outcomes from comp.outputs in hybrid programs (see Measurement outputs). Legacy get_measurement_outcomes remains available.
Guide: Backend — Pulse scope
Example:
examples/circuit_calibrations_pulse.py
2.2 Custom gates via QMInstructionProperties (Qiskit 2.x)¶
Define a gate at the circuit level.
Write a QUA macro.
Register via
QMInstructionProperties.Call
backend.update_target().
Keeps the Target and qm_qasm compiler in sync for both backend.run() and quantum_circuit_to_qua.
3. Primitives: Sampler and Estimator on QOP¶
QMSamplerV2 and QMEstimatorV2 reuse QuAM Targets, stream parameters via InputType, and map shot budgets to QUA loops. Classified counts only — see Primitives guide.
3.1 Generated QUA programs (and how to inspect them)¶
Every primitive job and backend.run() exposes the compiled QUA programs on job.programs — always a list[Program], regardless of whether chunking occurred. Use get_program() for single-program (non-chunked) access, or iterate job.programs when chunking may have occurred:
from qm import generate_qua_script
# Non-chunked default:
print(generate_qua_script(job.get_program()))
# All chunks:
for i, prog in enumerate(job.programs):
print(generate_qua_script(prog))
See Jobs for the full job interface (get_qm_job(), get_result_handles(), pubs, IQCC run_data, lifecycle).
End-to-end snippet:
from qm import generate_qua_script
from qiskit import QuantumCircuit, transpile
from qiskit.quantum_info import SparsePauliOp
from qiskit_qm_provider import (
QMProvider, QMSamplerV2, QMSamplerOptions,
QMEstimatorV2, QMEstimatorOptions,
)
provider = QMProvider(state_folder_path="/path/to/quam/state")
backend = provider.get_backend()
qc = QuantumCircuit(1, 1)
qc.h(0)
qc.measure(0, 0)
qc = transpile(qc, backend)
sampler = QMSamplerV2(backend=backend, options=QMSamplerOptions(default_shots=256))
sampler_job = sampler.run([qc])
print("=== Sampler ===")
print(generate_qua_script(sampler_job.get_program()))
obs = SparsePauliOp.from_list([("Z", 1.0)])
estimator = QMEstimatorV2(backend=backend, options=QMEstimatorOptions())
estimator_job = estimator.run([(qc.remove_final_measurements(inplace=False), obs, [])])
print("=== Estimator ===")
print(generate_qua_script(estimator_job.get_program()))
backend_job = backend.run(qc, shots=256)
print("=== backend.run() ===")
print(generate_qua_script(backend_job.get_program()))
Guide: Primitives
API: Primitives reference
4. Hybrid QUA/Qiskit programs (embedding circuits in QUA)¶
Treat Qiskit circuits as building blocks inside larger QUA programs:
Transpile a
QuantumCircuit.Inside
with program():, callquantum_circuit_to_quawithParameterTablewhen needed.Use
comp.outputsfor classical results — a local-onlyMeasurementOutcomeTableofMeasurementRegisterFieldhandles (one per classical register, plus_bit0, … for loose clbits).comp.outputs["c"]returns the QUA var; usecomp.outputs.get_parameter("c")for the field handle andcomp.outputs.state_ints["c"]/comp.outputs.streams["c"]for bulk accessors.
from qm.qua import program, save
with program() as prog:
comp = backend.quantum_circuit_to_qua(syndrome_circuit)
save(comp.outputs.state_ints["ancilla"], comp.outputs.streams["ancilla"])
Scope requirement: QUA variable accessors (comp.outputs["c"], .state_ints, .streams, runtime ParameterTable[...]) must be used inside with program():.
Local-only: comp.outputs is tracked in private measurement registries on ParameterPool (weakref iter_measurement_outcome_tables / iter_measurement_register_fields), not in the runtime/OPNIC registry. It does not support OPNIC stream_back() / fetch_from_opx(). Bridge to the host manually via your transport tables (e.g. histogram into RewardParams, then reward.stream_back()).
Name overlap: Runtime input fields and measurement outputs may share the same string (e.g. both "c"). They are different objects — access inputs via your ParameterTable and measurements via comp.outputs. ParameterPool.lookup_runtime_parameter(name) (alias of _lookup_parameter_by_name) returns runtime parameters only. Call ParameterPool.reset() to clear both runtime and measurement registries in a long-lived session.
Re-compile identity: each quantum_circuit_to_qua call returns a new QuaCircuitCompilation with fresh field objects. comp.rewire_outputs(qc, new_result) refreshes wiring on the same wrapper. Measurement fields cannot be attached to runtime tables.
Breaking change: comp.outputs.c.state_int is invalid — use comp.outputs.state_ints["c"] or comp.outputs.get_parameter("c").state_int.
See measurement_outputs.md for the full locality model.
Legacy get_measurement_outcomes remains available and accepts either QuaCircuitCompilation or a raw CompilationResult.
Powerful for error correction, closed-loop calibration, and QUARC/OPNIC hybrid loops.
Guides: Backend, Parameter Table
Example: Error-Correction Workflow
5. Error-correction workflow (overview)¶
Repeated cycles: encode → syndrome measure (Qiskit circuit) → stream syndrome → classical decode → push recovery params → apply recovery (Qiskit circuit). ParameterTable and get_measurement_outcomes keep the classical-quantum boundary explicit.
Guide: Error-Correction Workflow
6. Qiskit Experiments + IQCC (with caveats)¶
examples/iqcc_t1_experiment.py shows T1 characterization with Qiskit Experiments on an IQCC backend. Before adopting this pattern broadly, read the home-page callout:
Batch vs real-time: Experiments emit large batches of near-identical circuits (AWG-style preloading). QUA prefers one program with real-time loops and streaming. For calibration sweeps, consider Qualibrate or qua-libs. Use this provider to compose Qiskit circuits into real-time QUA programs when that is the right model.
When you do run large Experiment batches through backend.run, tune max_circuits on the backend so each QUA program stays within compile/size limits while results are still stitched into one Qiskit Result — see Backend — multi-circuit batches.
Counts only: experiments needing raw I/Q or kerneled data will not work yet. Only classified 0/1 outcomes are supported.
Positive framing: the compiler’s value is frictionless advanced QUA with Qiskit handling circuit synthesis, visualization, transpilation, and portability — not replacing QUA entirely with Qiskit.