Skip to content

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 with EstimatorV2, 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 with SamplerV2. 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 _sampling module). 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:

  1. QiskitRuntimeConfig(token=..., channel=..., instance=...).
  2. The environment variable QISKIT_IBM_TOKEN.
  3. 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 uses QiskitRuntimeConfig().

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

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 False only for the shifted copies that compile_shifted builds. Default: True.

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 QiskitRuntimeJobHandle.job_id().

required
method str

The method that the job was submitted with, one of submit_methods.

'run'

Returns:

Name Type Description
handle JobHandle

A handle for the existing job.

Raises:

Type Description
ValueError

If job_id is not made of 1 to 64 letters, digits, _ or -, if method is not in submit_methods, or if the backend is not an IBM Runtime backend with a service to look the job up in (a fake backend has none). In all these cases, the service is not contacted.

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 QiskitRuntimeCompiled handle for circuit.

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 (2**n_qubits,), which sums to 1.

resources(measured=True)

Report the resources that the transpiled circuit uses on this device.

Parameters:

Name Type Description Default
measured bool

If True, the default, report the circuit measured in the Z basis, which is what kernels and sampling submit. If False, report the circuit without measurements that the estimator runs.

True

Returns:

Type Description
dict

A dictionary with backend_name, is_fake, n_logical_qubits, optimization_level, seed_transpiler, gate_counts, n_2q_gates, two_qubit_gate_counts, depth, depth_2q, n_2q_gates_unrouted (the same circuit on the native gates with all-to-all connectivity), routing_overhead_2q, physical_qubits, n_physical_qubits_device, and estimated_success_probability, computed from the error rates of the target.

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 result() is the probability array.

Raises:

Type Description
ValueError

If job_id is not made of 1 to 64 letters, digits, _ or -.

NotImplementedError

For a backend without a service to look the job up in, such as a fake backend or Aer, which keep no job history.

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

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 submit_methods. "run", the default, measures the observables, like run(). "sample" returns the measurement histogram, like sample(). "probabilities" returns the distribution over every basis state, like probabilities().

'run'
tags Mapping[str, str] | None

Tags to put on the IBM job, so that you can find it again later with QiskitRuntimeService.jobs(job_tags=[...]). Each one becomes the job tag "key=value", sorted by key. IBM allows at most 8 tags of at most 86 characters each. Keys and values must not be empty, and a key must not contain =.

None

Returns:

Name Type Description
handle JobHandle

A handle for the submitted job.

Raises:

Type Description
ValueError

If method is not in submit_methods, if weights or inputs is not 1-D, or if tags breaks the IBM job tag limits. In all these cases, no primitive is built and nothing is submitted.

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 (B, n_inputs).

required

Returns:

Type Description
ProbabilityJobHandle

A handle whose result() is the probability array, of shape (B,).

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, the default, return the circuit measured in the Z basis that sample(), probabilities() and the kernel path run. If False, return the transpiled circuit without measurements that the estimator runs.

True

Returns:

Type Description
'QuantumCircuit'

A transpiled qiskit.QuantumCircuit.

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 (B, n_inputs).

required

Returns:

Type Description
ndarray

The estimates from the shots, of shape (B,).

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_name is looked up with QiskitRuntimeService. It needs credentials: a token, an environment variable or a saved account.
  • backend_instance is a BackendV2 that 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 "ibm_fez", or "least_busy" for the least busy operational device that the account can use, chosen once per compiled circuit. It is required unless backend_instance is set, and it is ignored when it is. Default: None.

None
backend_instance Any

A backend that is already configured. It takes precedence over backend_name. Default: None.

None
shots int

The number of shots per circuit run. Default: 1024.

1024
optimization_level int

The optimisation level passed to qiskit.transpile, from 0 to 3. Default: 1.

1
seed_transpiler int | None

The seed of the random passes of the transpiler, mainly the SabreLayout and SabreSwap passes, which choose the layout and the routing. None, the default, draws a new seed for every compile, so a circuit that needs routing compiles to a different number of gates on every run. Within one compile, all the transpilations share the seed, including the shifted copies that a gradient on hardware compiles. Set it whenever two compilations must be comparable. Since compiling submits nothing, trying a few seeds and keeping the cheapest circuit is a cheap and fair way to choose a layout.

None
primitive Primitive

The Runtime primitive that computes the expectation values. "estimator", the default, uses EstimatorV2, with one Primitive Unified Bloc (PUB) per observable in each job. "sampler" uses SamplerV2, with one PUB per qubit-wise commuting measurement group, and reduces the raw counts locally. sample() and probabilities() use SamplerV2 with either value.

'estimator'
group_commuting bool

Whether the sampler puts qubit-wise commuting Pauli terms into shared measurement circuits. False runs one circuit per term, which runs more circuits but keeps the shot budget of each term independent. The estimator ignores it. Default: True.

True
token str | None

The token passed to QiskitRuntimeService when backend_instance is None. With None, the backend uses QISKIT_IBM_TOKEN, then the saved account file. It is secret: it is never shown by repr(), and it is never saved in .eigenkit bundles, pickles or joblib checkpoints. After a load it is None, so set QISKIT_IBM_TOKEN.

None
channel str

The service channel: "ibm_quantum_platform" (the default), "ibm_cloud" or "local". The legacy "ibm_quantum" channel was retired, and QiskitRuntimeService no longer accepts it.

'ibm_quantum_platform'
instance str | None

The service instance, as a Cloud Resource Name (CRN) or a name, when one applies. Default: None.

None
service_kwargs dict

Extra keyword arguments passed to QiskitRuntimeService, such as region, url or proxies.

dict()
poll_timeout_seconds int

How long result() waits before it gives up, in seconds. Default: 3600.

3600
enable_gradients bool

Whether to allow parameter-shift gradients on the device: PyTorch .backward(), jacobian() and gradient-based optimisers. It is off by default, because one backward pass costs at least 2 * n_weights times the jobs of a forward call. When it is on, compiling for a paid device gives a CostWarning that states this cost.

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 QiskitRuntimeBackend for this configuration.

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 result() waits before it gives up, in seconds.

required
reduce Callable | None

The function that turns the result of the primitive into the vector of expectation values. None reads the EstimatorV2 result of each Primitive Unified Bloc (PUB) directly, which is what the estimator submits. The sampler passes its reduction of the counts instead.

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, counts() raises NotImplementedError.

None
method str

The method of the compiled circuit that the job runs, one of QiskitRuntimeCompiled.submit_methods. It decides how result() decodes the job.

'run'
n_qubits int | None

The number of qubits of the circuit, which sets the size of the "probabilities" distribution.

None

Raises:

Type Description
ValueError

If method is not in QiskitRuntimeCompiled.submit_methods, if method is "sample" or "probabilities" without counts, or if method is "probabilities" without n_qubits.

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 "run" job on the estimator, which returns expectation values and never sees a bitstring. Set primitive="sampler" to get counts.

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 method="run", one float64 expectation value per observable, of shape (n_obs,), in the order of submission, as QiskitRuntimeCompiled.run() returns. For method="sample", the histogram that QiskitRuntimeCompiled.sample() returns, which maps each bitstring to its number of shots, with qubit 0 as the leftmost character. For method="probabilities", the float64 distribution of shape (2**n_qubits,) that QiskitRuntimeCompiled.probabilities() returns.

Raises:

Type Description
ValueError

If the results of the job do not have the shape that the method and the circuit of this handle produce, for example because the handle was attached with the wrong method. Also, for "probabilities", if the circuit has more than _sampling.MAX_DENSE_QUBITS (24) qubits.

status()

Return the state of the job as IBM Quantum reports it, without blocking.

Examples of states are "QUEUED", "RUNNING", "DONE", "CANCELLED" and "ERROR".