qiskit_runtime_backend
eigenkit.core.backends.qiskit_runtime_backend
The Qiskit Runtime backend, for IBM Quantum hardware.
Qiskit and qiskit-ibm-runtime are optional dependencies
(pip install eigenkit[qiskit]). They are imported only when they are needed, so
the rest of the library works without them. If Qiskit is missing, compile()
raises a clear ImportError. If qiskit-ibm-runtime is missing, the same kind of
error comes when it is first needed: when compile() looks up a backend by name,
or when the first job is submitted.
The backend runs on real IBM backends (Heron devices such as ibm_fez,
ibm_marrakesh and ibm_torino), on the device that service.least_busy()
chooses (backend_name="least_busy"), and, for local testing, on the fake backends
with noise models from qiskit_ibm_runtime.fake_provider and on
qiskit_aer.AerSimulator.
QiskitRuntimeConfig(primitive=...) chooses one of two primitives:
"estimator", the default, computes the expectation values of Pauli sums withEstimatorV2, which does the basis rotation and the averaging on the server. Each observable is one Primitive Unified Bloc (PUB) of a single job, and only the numbers come back."sampler"computes the same expectation values from raw histograms of bitstrings withSamplerV2. The Pauli terms of the observables are sorted into qubit-wise commuting measurement groups, one circuit is run for each group, and the counts are reduced locally (see the_samplingmodule). There is one PUB per group instead of one per observable, and the shot budget is explicit. This is what you want when the shot cost of a result is itself what you measure.
sample() and probabilities() always use SamplerV2, whatever the configured
primitive, because they are sampling operations and EstimatorV2 returns no
bitstrings. primitive only chooses how the expectation values are computed.
submit() runs run, sample or probabilities without blocking (see
QiskitRuntimeCompiled.submit_methods), and it can tag the IBM job so that you
can find it again. attach() rebuilds a handle from the id of a job submitted
earlier, for example after the process that submitted it restarted. The
result() of either handle returns what the blocking method of the same name
returns.
zero_state_probability() and its batched form zero_state_probabilities() are
used by quantum kernels. They run one SamplerV2 job, measured in the Z basis,
for the whole batch. They are not submit() methods:
submit_zero_state_probabilities() submits the batch without blocking, and
retrieve_zero_state_probabilities() attaches to it again by its job id.
The credentials are looked up in this order, and the most explicit one wins:
QiskitRuntimeConfig(token=..., channel=..., instance=...).- The environment variable
QISKIT_IBM_TOKEN. - The saved account (
~/.qiskit/qiskit-ibm.json).
The channel defaults to "ibm_quantum_platform". The legacy "ibm_quantum"
channel was retired with the old IBM Quantum Platform, and QiskitRuntimeService
no longer accepts it at all.
For testing, pass a FakeBackendV2 instance directly as backend_instance. No
token or service is needed.
By default, gradient_method = "none", because runs on a quantum processing unit
(QPU) are too expensive for parameter-shift gradients: one backward step costs
at least 2 * n_weights jobs, each with the full shot count. The PyTorch wrapper
raises a clear error on .backward(). Train on a simulator first, then use the
trained weights on the QPU. enable_gradients=True in the configuration turns on
parameter-shift gradients on the device, and compiling for a paid device then
gives a CostWarning that states the cost of one backward pass.
compile() runs qiskit.transpile(circuit, backend=target,
optimization_level=N) once and caches the transpiled circuit. For the estimator,
it also caches the SparsePauliOp operators, mapped to the layout of the
transpiled circuit. The sampler transpiles each measured circuit on its first use
and caches it too. Each run() or submit() then only binds the parameter values
in the PUBs that it passes to the primitive.
Example
from eigenkit import Circuit, Encoding, Variational, all_z, register
from eigenkit.configs import QiskitRuntimeConfig
blocks = [Encoding(in_features=4), Variational(depth=2)]
# by a registered name (this backend is opt-in, so register it first)
register("qiskit-runtime", QiskitRuntimeConfig(backend_name="ibm_fez"))
circuit = Circuit(blocks=blocks, observables=all_z(4), provider="qiskit-runtime")
# or with the configuration, when a device setting matters
circuit = Circuit(
blocks=blocks,
observables=all_z(4),
provider=QiskitRuntimeConfig(backend_name="ibm_fez"),
)
It is opt-in, and it costs money. The first compile for a paid device gives a
CostWarning, and dry_run_info() reports the shape of a run, including how
many circuits a call runs, before you submit it.
QiskitRuntimeBackend
The provider for IBM Quantum hardware through Qiskit Runtime.
It is opt-in: the name "qiskit-runtime" exists only after the caller
registers it, for example with
register("qiskit-runtime", QiskitRuntimeConfig(...)).
It costs money: the first compile for a paid device gives a
CostWarning, and dry_run_info()
reports the shape of a run beforehand. It does not estimate a price.
This class is internal. Select the backend with provider="qiskit-runtime" or
by passing a
QiskitRuntimeConfig,
instead of constructing this class.
capabilities
property
Return what this backend supports.
It supports every gate except AmplitudeEncoding (state preparation),
QubitUnitary and ControlledQubitUnitary (arbitrary unitaries) and
CPHASE. It supports finite-shot sampling, which a quantum processing unit
(QPU) always uses. It supports batching, because the primitives evaluate a
batch of parameter values in one job when weights is 1-D and inputs
is 2-D. It has no limit of its own on the number of qubits: the limit of
the device applies. See
Capabilities.
gradient_method
property
Return the gradient method, which depends on enable_gradients.
It is "parameter_shift" when the configuration sets enable_gradients,
and "none" otherwise. A parameter-shift backward pass on a quantum
processing unit (QPU) costs at least 2 * n_weights jobs, each with the
full shot count, so gradients are refused unless the caller opts in.
__init__(config=None, name='qiskit-runtime')
Build the backend from a device configuration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
QiskitRuntimeConfig | None
|
The device settings. |
None
|
name
|
str
|
The registry name, used in error messages. |
'qiskit-runtime'
|
compile(circuit, observables)
Prepare circuit for repeated evaluation of observables.
It is called once per Circuit, and the handle is reused for every call, so
the expensive work is done here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
circuit
|
CircuitSpec
|
The circuit to prepare. |
required |
observables
|
Sequence[Observable]
|
The observables that the handle measures. |
required |
Returns:
| Type | Description |
|---|---|
QiskitRuntimeCompiled
|
A |
QiskitRuntimeCompiled
Bases: CompiledCircuit
A circuit transpiled for an IBM Quantum backend.
This class is internal. It is built by
QiskitRuntimeBackend.compile()
and obtained from it, and it is never constructed directly. The circuit is
transpiled once and cached (each measured circuit of the sampler on its first
use), so run() and submit() only bind the parameter values. submit()
returns a
QiskitRuntimeJobHandle
without blocking, and attach() rebuilds that handle from a job id.
submit_methods = _SUBMIT_METHODS
class-attribute
instance-attribute
The methods submit() and attach() accept: "run", "sample" and
"probabilities". zero_state_probability is not one of them: its batched
async form is submit_zero_state_probabilities(). Any method
added to this adapter's async path must also be added here, or callers that
check this set will refuse it.
__init__(circuit, observables, config, *, warn=True)
Prepare the circuit for repeated evaluation.
QiskitRuntimeBackend.compile() builds it, and there is no reason to
construct one directly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
circuit
|
CircuitSpec
|
The circuit to prepare. |
required |
observables
|
Sequence[Observable]
|
The observables that this handle measures. |
required |
config
|
QiskitRuntimeConfig
|
The device settings. |
required |
warn
|
bool
|
Whether to give the cost warnings for a paid device. It
is |
True
|
attach(job_id, *, method='run')
Rebuild the handle of a job submitted earlier from this circuit.
Use it to pick up a job from a process other than the one that submitted
it, for example after a crash. The handle decodes results exactly as the
handle that submit() returned would have, provided that the job was
submitted from the same circuit, observables, primitive and method.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job_id
|
str
|
The IBM job id, as returned by
|
required |
method
|
str
|
The |
'run'
|
Returns:
| Name | Type | Description |
|---|---|---|
handle |
JobHandle
|
A handle for the existing job. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
compile_shifted(circuit)
Compile a copy of this circuit with one gate angle shifted.
The chain-rule gradient compiles one copy per shifted gate. Each copy runs
on the device of this handle, never on a new "least_busy" choice, and is
transpiled with the seed of this handle, so every term of the gradient
comes from the same qubits. The compile of this handle already gave the
cost warnings, so the copy gives none.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
circuit
|
CircuitSpec
|
The shifted circuit. |
required |
Returns:
| Type | Description |
|---|---|
'QiskitRuntimeCompiled'
|
A |
counts(weights, inputs)
Return the histogram in the computational basis for one sample.
It is always a SamplerV2 job, whatever primitive the configuration
names, because EstimatorV2 has no bitstrings to return. It is one job of
shots shots.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
The parameter vector, which must be 1-D. |
required |
inputs
|
ndarray
|
The input features, which must be 1-D. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
counts |
dict[str, int]
|
The number of shots for each measured bitstring. The first character of each key is qubit 0, which is the most significant bit. |
dry_run_info()
Report the size of a run, before anything is submitted.
It reports the target backend and whether it is a fake one, the primitive in use, the number of shots, the optimisation level, the transpiler seed set in the configuration, the numbers of logical and physical qubits, the gate counts and the depth of the transpiled circuit, the numbers of observables and of measurement groups, and how many circuits one call runs: one per observable with the estimator, and one per measurement group with the sampler.
transpiled_gate_counts and transpiled_depth describe the circuit that
is actually submitted. With the estimator, this is the circuit without
measurements, which carries its observables separately, so both are
single values. With the sampler, there is one circuit per measurement
group, with the basis rotation and the measurements added before
transpiling, so that the layout can place them on the qubits that the
circuit ran on. Both are then dictionaries keyed by basis, as in the IQM
backend. Reporting the circuit without measurements there would
understate a routed circuit, because the transpiler sees a different input
and chooses a different layout.
Returns:
| Name | Type | Description |
|---|---|---|
info |
dict
|
The fields above, keyed by name. |
probabilities(weights, inputs)
Return the measurement distribution over all 2**n_qubits basis states.
It is the estimate from the shot frequencies, not the exact distribution:
on hardware, the basis states that were never sampled come back as exact
zeros. It is limited to circuits of at most _sampling.MAX_DENSE_QUBITS
(24) qubits. For a wider circuit, read sample() directly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
The parameter vector, which must be 1-D. |
required |
inputs
|
ndarray
|
The input features, which must be 1-D. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
probabilities |
ndarray
|
The distribution, of shape
|
resources(measured=True)
Report the resources that the transpiled circuit uses on this device.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
measured
|
bool
|
If |
True
|
Returns:
| Type | Description |
|---|---|
dict
|
A dictionary with |
retrieve_zero_state_probabilities(job_id)
Attach again to a job that submit_zero_state_probabilities() submitted.
The job may have been submitted from another session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job_id
|
str
|
The IBM Quantum job id. |
required |
Returns:
| Type | Description |
|---|---|
ProbabilityJobHandle
|
A handle whose |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
For a backend without a |
run(weights, inputs)
Evaluate the circuit and return one expectation value per observable.
It follows the CompiledCircuit contract: the result has shape (n_obs,)
when weights and inputs are both 1-D, and (B, n_obs) otherwise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
The parameter vector: a 1-D array for one sample, or a 2-D array with a leading batch axis. |
required |
inputs
|
ndarray
|
The input features, with the same 1-D or 2-D
shapes as |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
One expectation value per observable. |
sample(weights, inputs)
Return the same histogram as counts().
This is an alias of
counts,
under the name that the Circuit.sample() dispatch looks for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
The parameter vector, which must be 1-D. |
required |
inputs
|
ndarray
|
The input features, which must be 1-D. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
counts |
dict[str, int]
|
The number of shots for each measured bitstring. |
submit(weights, inputs, *, method='run', tags=None)
Submit the circuit without blocking, and return a job handle.
Nothing blocks: handle.result() waits for the job and decodes it in the
same way as the blocking method of the same name. For "run", the handle
returns expectation values with either primitive: the counts of the
sampler are reduced when the result is collected, so the two paths look
the same to the caller. "sample" and "probabilities" are always one
SamplerV2 job measured in the Z basis, whatever the configured
primitive.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
The parameter vector for one sample (1-D). |
required |
inputs
|
ndarray
|
The input features for one sample (1-D). |
required |
method
|
str
|
What the job computes, one of |
'run'
|
tags
|
Mapping[str, str] | None
|
Tags to put on the IBM job, so that
you can find it again later with
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
handle |
JobHandle
|
A handle for the submitted job. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
submit_zero_state_probabilities(weights, inputs)
Submit the all-zeros probability of every row of inputs as one job.
It submits one SamplerV2 job and returns without waiting. The circuit is
transpiled once and measured in the Z basis, and every row binds one set
of parameter values of the same Primitive Unified Bloc (PUB), so B
kernel entries cost one job of B * shots shots. It works whatever
primitive the configuration names.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
The parameter vector, which must be 1-D. |
required |
inputs
|
ndarray
|
A batch, of shape |
required |
Returns:
| Type | Description |
|---|---|
ProbabilityJobHandle
|
A handle whose |
transpiled_circuit(measured=True)
Return the Qiskit circuit that this backend submits, to draw or inspect it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
measured
|
bool
|
If |
True
|
Returns:
| Type | Description |
|---|---|
'QuantumCircuit'
|
A transpiled |
zero_state_probabilities(weights, inputs)
Return the all-zeros probability of every row of inputs, from one job.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
The parameter vector, which must be 1-D. |
required |
inputs
|
ndarray
|
A batch, of shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The estimates from the shots, of shape |
zero_state_probability(weights, inputs)
Return the all-zeros probability for one sample, estimated from shots.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
The parameter vector, which must be 1-D. |
required |
inputs
|
ndarray
|
The input features, which must be 1-D. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The probability. |
QiskitRuntimeConfig
dataclass
Bases: SecretFieldsMixin
Device configuration for the Qiskit Runtime backend.
There are two ways to choose the target backend:
backend_nameis looked up withQiskitRuntimeService. It needs credentials: a token, an environment variable or a saved account.backend_instanceis aBackendV2that is used directly. It is useful for tests with a fake backend, or to pass any backend that is already configured.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
backend_name
|
str | None
|
The IBM backend name, for example |
None
|
backend_instance
|
Any
|
A backend that is already configured. It takes
precedence over |
None
|
shots
|
int
|
The number of shots per circuit run. Default: |
1024
|
optimization_level
|
int
|
The optimisation level passed to |
1
|
seed_transpiler
|
int | None
|
The seed of the random passes of the transpiler, mainly
the |
None
|
primitive
|
Primitive
|
The Runtime primitive that computes the expectation values.
|
'estimator'
|
group_commuting
|
bool
|
Whether the sampler puts qubit-wise commuting Pauli terms
into shared measurement circuits. |
True
|
token
|
str | None
|
The token passed to |
None
|
channel
|
str
|
The service channel: |
'ibm_quantum_platform'
|
instance
|
str | None
|
The service instance, as a Cloud Resource Name (CRN) or a name,
when one applies. Default: |
None
|
service_kwargs
|
dict
|
Extra keyword arguments passed to |
dict()
|
poll_timeout_seconds
|
int
|
How long |
3600
|
enable_gradients
|
bool
|
Whether to allow parameter-shift gradients on the
device: PyTorch |
False
|
__post_init__()
Reject an unknown primitive when the configuration is built.
If the check waited for the first submission, the error would come as a confusing failure on a paid backend, after the transpilation and the cost warning.
make_backend()
Build the backend that this configuration describes.
The provider layer calls it when a configuration is passed as provider=.
There is no reason to call it directly.
Returns:
| Type | Description |
|---|---|
'QiskitRuntimeBackend'
|
The |
QiskitRuntimeJobHandle
A handle to a Qiskit Runtime job, whose result() blocks and decodes it.
compiled.submit(weights, inputs) and compiled.attach(job_id) return one of
these. What result() returns depends on the method of the handle.
method
property
Return the method of the compiled circuit that this job runs.
It is "run", "sample" or "probabilities".
__init__(job, n_observables, poll_timeout_seconds, reduce=None, counts=None, method='run', *, n_qubits=None)
Wrap a submitted Runtime job as a job handle.
QiskitRuntimeCompiled.submit() and QiskitRuntimeCompiled.attach()
build it, and it is not constructed directly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job
|
RuntimeJob
|
The IBM Runtime job returned by the primitive. |
required |
n_observables
|
int
|
The number of expectation values to expect back, which sets the shape of the result array. |
required |
poll_timeout_seconds
|
int
|
How long |
required |
reduce
|
Callable | None
|
The function that turns the result of the
primitive into the vector of expectation values. |
None
|
counts
|
Callable | None
|
The function that takes the histograms out
of the result of the primitive, one per submitted circuit. Only
sampler jobs have histograms, because the estimator returns
expectation values and never sees a bitstring. With |
None
|
method
|
str
|
The method of the compiled circuit that the job runs,
one of |
'run'
|
n_qubits
|
int | None
|
The number of qubits of the circuit, which sets
the size of the |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
cancel()
Ask IBM Quantum to cancel the job, without blocking.
counts()
Block until the job finishes, then return the raw histograms.
There is one histogram per measurement group. In each key, qubit 0 is the
leftmost character, which is the most significant bit. A "sample" or
"probabilities" job measured one circuit, so it has one histogram.
These are the numbers that the device returned, and
result
is these numbers reduced against the observables. Because both come from a
single job, a run can be scored again offline, against different
observables or with a different metric, without paying for it twice.
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
For a |
job_id()
Return the IBM Quantum job id.
Use it to look the job up in the console or in qiskit-ibm-runtime.
result()
Block until the job finishes, then decode it by the method of the handle.
A handle from attach() decodes a job exactly as the handle that submit()
returned would have.
Returns:
| Type | Description |
|---|---|
ndarray | dict[str, int]
|
For |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the results of the job do not have the shape that the
|
status()
Return the state of the job as IBM Quantum reports it, without blocking.
Examples of states are "QUEUED", "RUNNING", "DONE", "CANCELLED" and
"ERROR".