Primitives¶
QMSamplerV2 and QMEstimatorV2 are QOP-aware adapters of Qiskit’s V2 Sampler and Estimator — same pub interface, but execution leverages input streaming, real-time parameter updates, and (for Estimator) on-device control flow.
For signatures and options fields, see the Primitives API reference.
Purpose¶
Generic cloud primitives assume parameters are bound at submission time. QOP workloads often stream parameters cycle-by-cycle via InputType (INPUT_STREAM, IO1, IO2, OPNIC). These primitives expose that capability while reusing QuAM-derived Targets for transpilation.
The traditional QMBackend.run() interface mimics Sampler-like behavior for users who prefer the classic backend API. Like backend.run, primitives respect the max_circuits backend option and will split large batches into multiple QUA programs when needed. See Backend — multi-circuit batches.
Classified measurement outcomes only¶
Only classified results (0/1 bitstrings / counts) are reliably supported today. Experiments requiring raw I & Q, kerneled shots, or other non-discriminated outputs are not yet supported. Contributors welcome.
Although QMSamplerOptions.meas_level accepts "kerneled" and "avg_kerneled" in the API, those paths are not production-ready. Use "classified" (the default) for all current workflows, including Qiskit Experiments.
QMSamplerV2¶
Shot-based measurement counts. Maps to backend run() under the hood. The returned QMSamplerJob exposes the compiled QUA programs via get_program():
from qm import generate_qua_script
sampler = QMSamplerV2(backend=backend, options=QMSamplerOptions(default_shots=256))
sampler_job = sampler.run([qc])
print(generate_qua_script(sampler_job.get_program()))
QMEstimatorV2¶
Expectation values and standard errors with optional abelian grouping of observables. Estimator observables assume classified measurement outcomes. Returns a QMEstimatorJob.
from qiskit_qm_provider import QMEstimatorV2, QMEstimatorOptions, InputType
options = QMEstimatorOptions(input_type=InputType.INPUT_STREAM)
estimator = QMEstimatorV2(backend=backend, options=options)
job = estimator.run([(circuit, observables, parameter_values)])
result = job.result()
Options at a glance¶
Option |
Sampler |
Estimator |
Meaning |
|---|---|---|---|
|
✓ |
✓ |
How parameters reach the OPX: stream, IO, QUARC-backed OPNIC, or |
|
✓ |
— |
Default shots when not specified in |
|
— |
✓ |
Default precision (e.g. 1/√4096) when not specified |
|
— |
✓ |
Group commuting observables (default |
|
✓ |
✓ |
Extra dict passed through to |
|
✓ |
— |
Use |
Configure options via QMSamplerOptions or QMEstimatorOptions. Set input_type=None to bind all parameter values at compile time (suitable only when the number of distinct parameter sets is small).
Large PUB batches and max_circuits¶
When the number of PUBs passed to sampler.run() or estimator.run() exceeds backend.options.max_circuits (default 30), the provider automatically splits them into consecutive chunks. Each chunk is compiled into its own QUA program and queued sequentially on QOP. The returned PrimitiveResult always has one entry per input PUB in the original order — the splitting is fully transparent to calling code.
# Reduce the limit so that even small batches trigger splitting:
backend.set_options(max_circuits=5)
sampler = QMSamplerV2(backend=backend)
# 12 PUBs -> 3 programs of 5, 5, 2 PUBs queued sequentially
job = sampler.run([pub_0, pub_1, ..., pub_11])
result = job.result() # 12 SamplerPubResult entries, in original order
Set max_circuits=None to disable splitting and always build a single program regardless of batch size.
Running on IQCC Cloud¶
When the backend is an IQCC cloud backend, QMSamplerV2.run() / QMEstimatorV2.run() return an IQCCSamplerJob / IQCCEstimatorJob. For streamed input_types, the job submits the QUA program together with an auto-generated sync hook — a small Python script that runs on the cloud side and pushes the per-pub parameter values (and, for the Estimator, the observable indices) into the running program cycle-by-cycle.
The sync hook is fully self-contained. It imports only:
from iqcc_cloud_client.runtime import get_qm_job
and drives the QM job directly. It does not require qiskit_qm_provider, numpy, or qualang_tools to be installed in the cloud runtime, so any IQCC user can run streamed primitives without extra packages. Parameter tables are serialised to plain Python data before submission and the push logic is rendered from a Jinja template; values are coerced to each parameter’s QUA type (int / fixed / bool).
Supported input_type on IQCC¶
|
IQCC support |
Cloud mechanism |
|---|---|---|
|
✓ |
|
|
✓ |
|
|
✓ |
Values bound at compile time (no sync hook); only for small parameter sets |
|
✗ |
Raises |
from qiskit_qm_provider import (
IQCCProvider,
QMSamplerV2,
QMSamplerOptions,
InputType,
add_basic_macros,
)
# 1. Grab the cloud backend by name ("arbel", or whichever quantum computer you have access to).
backend = IQCCProvider().get_backend("arbel")
# 2. IQCC does not yet ship the standard single-qubit gate macros, so add them once
# after fetching the backend. This populates x, sx, rz, sy, sydg, measure, reset,
# delay, id, and cz macros and syncs the backend target.
add_basic_macros(backend)
# 3. Streamed primitives run on any IQCC user's cloud runtime — no provider install needed cloud-side.
sampler = QMSamplerV2(backend=backend, options=QMSamplerOptions(input_type=InputType.INPUT_STREAM))
job = sampler.run([(circuit, parameter_values)])
# OPNIC over IQCC is not supported yet and raises at run():
QMSamplerV2(backend=backend, options=QMSamplerOptions(input_type=InputType.OPNIC)).run([circuit])
# NotImplementedError: OPNIC input_type is not yet supported for IQCC cloud jobs; use INPUT_STREAM or IO1/IO2.
IQCC cloud failures and run_data¶
Cloud-side failures (config validation, open_qm errors, etc.) often surface locally as a misleading KeyError on a measurement stream (for example KeyError: '__c_0') because the QUA program never reached the streaming stage.
All IQCC wrapper jobs — IQCCJob, IQCCSamplerJob, and IQCCEstimatorJob — expose the raw IQCC execution record on job.run_data (backed by job.get_qm_job()._run_data). Typical keys:
Key |
Content |
|---|---|
|
QM / QOP log lines from the cloud runtime |
|
Python traceback when the remote script failed |
|
Timing and fridge metadata when execution completed |
When job.result() is called, the wrapper inspects run_data["stderr"] and, if it contains a Python traceback, raises IQCCCloudExecutionError with the exact cloud stderr as the exception message — instead of a local stream KeyError.
from qiskit_qm_provider.job import IQCCCloudExecutionError
try:
result = job.result()
except IQCCCloudExecutionError as exc:
print(exc) # full cloud traceback
print(job.run_data["stdout"]) # e.g. PHYSICAL CONFIG ERROR lines
You can also inspect job.run_data after submission without calling result() when debugging a failed cloud run.
Debugging generated QUA¶
Every primitive job and backend.run() exposes the compiled QUA programs via get_program() (single/default) or by iterating job.programs (all chunks). See the Jobs guide for the full accessor table (qm_jobs, get_qm_job(), get_program(), get_result_handles(), pubs, IQCC run_data, …).
For QMEstimatorJob, the compiled execution plans are also available on job.runtime_pubs — one _ExecutionPlan per input PUB, showing how observables were grouped and what will be streamed to the OPX.
from qm import generate_qua_script
# Non-chunked (default): use the getter
print(generate_qua_script(job.get_program()))
# Chunked: iterate all programs
for chunk_idx, prog in enumerate(job.programs):
print(f"=== QUA program {chunk_idx} ===")
print(generate_qua_script(prog))