iqm_backend
eigenkit.core.backends.iqm_backend
The IQM backend, which runs circuits on IQM quantum computers through iqm-client.
The iqm-client package, with its qiskit extra, is an optional dependency
(pip install eigenkit[iqm]). It is imported only when it is needed, so the rest of
the library works without it. If it is missing, compile() raises an ImportError
with a clear message.
This backend connects directly to IQM Resonance, or to any IQM Server, instead of
using the same hardware through Amazon Braket. You then pay through IQM's own plans
instead of an Amazon Web Services (AWS) bill. It is also the only way to reach IQM's
fake backends, its calibration sets and its architectures with MOVE gates.
IQM has no estimator primitive: the IQM service returns histograms of measured
bitstrings and nothing else. So this backend works by sampling everywhere. The
observables are sorted into groups of qubit-wise commuting terms, one circuit runs
for each group, and the expectation values are computed locally from the counts
(see the _sampling module). probabilities() and sample() use the same code,
and this is what lets the quantum approximate optimisation algorithm (QAOA) read a
solution from real hardware.
There are two kinds of target:
- Resonance or any IQM Server: pass
urland atoken(or set theIQM_TOKENenvironment variable). You can also passquantum_computerto choose one machine when the server hosts several. - Local fake backends: pass
IQMFakeAdonis(),IQMFakeApollo(),IQMFakeDeneb()or a similar fake backend asbackend_instance. These run on Qiskit Aer with a realistic IQM noise model, with no token and no network access. The tests of this backend run on them.
The default is gradient_method = "none", because hardware is too expensive for
parameter-shift gradients: one backward pass takes at least 2 * n_weights jobs,
each with the full number of shots. Train on a simulator, then use the trained
weights on the device. enable_gradients=True in the configuration turns on
parameter-shift gradients on the device. Compiling for a real device then issues a
CostWarning that gives the number of circuits of one backward pass.
The circuits are transpiled for the device target. The native gates are r and
cz, and also move on the Star architectures, which the Qiskit transpiler routes
through the plugin that iqm-client provides. transpile_mode chooses when this
happens:
"parametric", the default, transpiles once for each measurement group and sets the parameter values at each call. This is cheap, but the transpiler cannot merge rotations whose angles it does not know. For example, an encoding block and a variational block on 2 qubits, measured in one group, gave 6rgates in this mode against 3 with the values set before transpiling."per_call"sets the values first and transpiles at every call. The circuits are shallower, so they run with higher fidelity on hardware. The cost is one transpilation per evaluation, which is small next to the time spent in the queue, but not next to a local simulator.
Example
from eigenkit import Circuit, Encoding, Variational, all_z
from eigenkit.configs import IQMConfig
blocks = [Encoding(in_features=4), Variational(depth=2)]
# Offline, against IQM's noise-modelled fake backend.
from iqm.qiskit_iqm import IQMFakeAdonis
circuit = Circuit(
blocks=blocks,
observables=all_z(4),
provider=IQMConfig(backend_instance=IQMFakeAdonis(), shots=2048),
)
# On real hardware, by name.
circuit = Circuit(
blocks=blocks,
observables=all_z(4),
provider=IQMConfig(
url="https://resonance.iqm.tech/",
quantum_computer="garnet",
shots=2048,
),
)
The backend is registered as "iqm" when the iqm extra is installed, and its
module is imported only when it is first used. A real device uses your IQM
quota: the first compilation for a real device issues a CostWarning, and
dry_run_info() reports the size of the circuit and the number of circuits
that each call submits, before you submit anything.
IQMBackend
Provider for IQM quantum computers, through iqm-client.
It is registered as "iqm" when iqm-client is installed, and its module is
imported only when it is first used. A real device uses your IQM quota: the
first compilation for a real machine issues a
CostWarning, and dry_run_info()
reports the size of a run and the number of circuits that each call submits,
beforehand. Compiling for one of IQM's fake backends is free and gives no
warning.
This class is internal: you select the backend with provider="iqm" or by
passing an IQMConfig, not by
creating this class.
capabilities
property
What this backend supports.
It supports every gate except state preparation, arbitrary unitaries and
CPHASE. It samples with a finite number of shots, which is the only thing
IQM offers. It has no native batching, because a batch takes one job for
each sample. It has no qubit limit of its own, and the number of qubits of
the device applies. See
Capabilities.
gradient_method
property
How this backend computes gradients.
It is "none", unless the configuration sets enable_gradients, and then
it is "parameter_shift". A parameter-shift backward pass costs at least
2 * n_weights jobs, each with the full number of shots, so gradients are
refused unless you turn them on.
__init__(config=None, name='iqm')
Build the backend from a device configuration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
IQMConfig
|
The device settings. |
None
|
name
|
str
|
The registry name, which error messages use. |
'iqm'
|
compile(circuit, observables)
Prepare circuit for repeated evaluation of observables.
It is called once for each Circuit, and the handle is reused for every
call. So the expensive work happens here: planning the measurement groups
and, in "parametric" mode, transpiling one circuit for each group.
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 |
|---|---|
IQMCompiled
|
An |
IQMCompiled
Bases: CompiledCircuit
A circuit prepared for an IQM quantum computer.
This class is internal: you get it from
IQMBackend.compile(),
and you never create it directly. The measurement plan and the parametric
circuit are built once. run() and submit() set the parameter values, and in
"per_call" mode they also transpile.
__init__(circuit, observables, config, *, warn=True)
Prepare the circuit for repeated evaluation.
IQMBackend.compile() builds it, and there is no reason to create 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
|
IQMConfig
|
The device settings. |
required |
warn
|
bool
|
Whether to issue the cost warnings for a real device. It
is |
True
|
compile_shifted(circuit)
Compile a copy of this circuit with one gate angle shifted.
The chain-rule gradient compiles one copy for each shifted gate. Each copy reuses the backend of this handle, without asking the server for it again, and is transpiled with the seed of this handle, so every term of the gradient comes from the same qubits. The compilation of this handle already issued the cost warnings, so the copy issues none.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
circuit
|
CircuitSpec
|
The shifted circuit. |
required |
Returns:
| Type | Description |
|---|---|
'IQMCompiled'
|
An |
counts(weights, inputs)
Measure one sample in the computational basis and return the histogram.
It runs one job of shots shots, measured in the Z basis with no change
of basis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
A 1-D parameter vector. |
required |
inputs
|
ndarray
|
A 1-D vector of input features. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
counts |
dict[str, int]
|
The number of shots for each measured
bitstring. The leftmost bit of each key is qubit |
dry_run_info()
Report the size of a run, before anything is submitted.
The report gives the target backend and whether it is one of IQM's local
fake backends, the number of shots, the transpile mode, the optimisation
level, the seed_transpiler setting, the numbers of logical and physical
qubits, the number of observables, and the number of measurement groups
that the observables need, which is the number of circuits that one call
submits. In "parametric" mode, it also gives the gate counts and the
depth of the transpiled circuit for each measurement basis. "per_call"
mode transpiles at call time, so there is no stored circuit to report.
Returns:
| Name | Type | Description |
|---|---|---|
info |
dict
|
The values above, keyed by name. |
probabilities(weights, inputs)
Return the measurement distribution over all 2**n_qubits basis states.
The distribution is estimated from the shot frequencies, and it is not the
exact one: this is hardware, and the basis states that no shot measured get
a probability of exactly 0. It is limited to _sampling.MAX_DENSE_QUBITS
qubits. For wider circuits, read sample() directly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
A 1-D parameter vector. |
required |
inputs
|
ndarray
|
A 1-D vector of input features. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
probabilities |
ndarray
|
An array of shape |
resources(measured=True)
Report the resources that the transpiled circuit uses on this device.
The two-qubit gate counts include move on Star devices, because every
CZ there goes through the resonator. In "per_call" mode, the report
describes the parametric circuit, and the circuits with the values set can
merge a few more rotations.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
measured
|
bool
|
It is accepted for consistency with the Qiskit Runtime backend, and it has no effect. |
True
|
Returns:
| Type | Description |
|---|---|
dict
|
A dictionary with the same keys as |
retrieve_zero_state_probabilities(job_id)
Reconnect to a job that submit_zero_state_probabilities() submitted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job_id
|
str
|
The identifier of the IQM job. |
required |
Returns:
| Type | Description |
|---|---|
ProbabilityJobHandle
|
A handle whose |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
For a local fake backend, which keeps no history of its jobs, or for a backend that cannot retrieve jobs. |
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. Each
sample costs one job of n_measurement_groups circuits. The values are
estimated from shots, so they carry the shot noise of shots.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
The parameter vector: 1-D for one sample, or 2-D with a leading batch axis. |
required |
inputs
|
ndarray
|
The input features, with the same 1-D or 2-D shape
rule as |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
One expectation value for each observable (for each sample, in a batch). |
sample(weights, inputs)
Return the same histogram as counts().
This is an alias of
counts, under
the name that Circuit.sample() looks for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
A 1-D parameter vector. |
required |
inputs
|
ndarray
|
A 1-D vector of input features. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
counts |
dict[str, int]
|
The number of shots for each measured bitstring. |
submit(weights, inputs)
Submit the circuit without waiting, and return a job handle.
All the measurement groups go into one job, so handle.result() waits once
and returns the whole vector of expectation values.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
A 1-D parameter vector. |
required |
inputs
|
ndarray
|
A 1-D vector of input features. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
handle |
JobHandle
|
A handle for the submitted job. |
submit_zero_state_probabilities(weights, inputs)
Submit the all-zeros probabilities of the rows of inputs, without waiting.
The rows go into one IQM job of B circuits. In "parametric" mode, the
circuit measured in the Z basis is transpiled once, and each row only
sets the values. In "per_call" mode, every row is transpiled.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
A 1-D parameter vector. |
required |
inputs
|
ndarray
|
A batch of inputs, of shape |
required |
Returns:
| Type | Description |
|---|---|
ProbabilityJobHandle
|
A handle whose |
transpiled_circuit(measured=True)
Return the circuit measured in Z and transpiled for this device.
The parameters are not set. Use it to draw or inspect the circuit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
measured
|
bool
|
It is accepted for consistency with the Qiskit Runtime backend, and it has no effect, because IQM always submits measured circuits. |
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
|
A 1-D parameter vector. |
required |
inputs
|
ndarray
|
A batch of inputs, of shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The probabilities estimated from the shots, of shape |
zero_state_probability(weights, inputs)
Return the all-zeros probability of one sample, estimated from the shots.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ndarray
|
A 1-D parameter vector. |
required |
inputs
|
ndarray
|
A 1-D vector of input features. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The probability. |
IQMConfig
dataclass
Bases: SecretFieldsMixin
Device configuration for the IQM backend.
There are two ways to choose a target:
url, and optionallyquantum_computer, opens anIQMProviderfor an IQM Server. This needs a token, fromtokenor from theIQM_TOKENenvironment variable.backend_instanceuses any IQMBackendV2directly: a fake backend for offline work, or a backend that you configured yourself. It takes precedence overurl, and it needs no token and no network access.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str | None
|
The address of the IQM Server, for example
|
None
|
quantum_computer
|
str | None
|
The machine to use when the server hosts more than one,
for example |
None
|
backend_instance
|
Any
|
An IQM backend that is already configured. It takes
precedence over |
None
|
shots
|
int
|
The number of shots for each circuit run. It must be positive, because hardware cannot compute exact values. |
1024
|
optimization_level
|
int
|
The optimisation level passed to |
1
|
seed_transpiler
|
int | None
|
The seed of the random steps of the transpiler, mainly
the |
None
|
transpile_mode
|
TranspileMode
|
|
'parametric'
|
group_commuting
|
bool
|
Whether to put Pauli terms that commute qubit by qubit
into shared measurement circuits. With |
True
|
token
|
str | None
|
The access token for the IQM Server. It is passed on only when it
is set. With |
None
|
calibration_set_id
|
Any
|
The calibration set to use, as a |
None
|
use_metrics
|
bool
|
Whether to ask the server for the error rates of each qubit
and each coupler, and load them into the |
False
|
provider_kwargs
|
dict
|
Extra keyword arguments passed to |
dict()
|
poll_timeout_seconds
|
int
|
How many seconds |
3600
|
enable_gradients
|
bool
|
Whether to allow parameter-shift gradients on the
device, for PyTorch |
False
|
__post_init__()
Reject the settings that can never work, when the configuration is created.
If they were left to the first submission, the error would appear after
the transpilation and the cost warning, which on a paid device is the
wrong time to find out. Only the settings that are wrong on their own are
checked here. Whether a target can be found depends on the environment
at compile time, so _resolve_backend checks that.
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 |
|---|---|
'IQMBackend'
|
The |
IQMJobHandle
A handle to a submitted IQM job.
compiled.submit(weights, inputs) returns one of these handles. Call
result() to wait until the job finishes and get the array of expectation
values, or status() to check the job without waiting.
__init__(job, groups, n_observables, n_qubits, poll_timeout_seconds)
Wrap a submitted IQM job in a job handle.
IQMCompiled.submit() builds it. It is not meant to be created directly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
job
|
JobV1
|
The job returned by the IQM backend. |
required |
groups
|
tuple[MeasurementGroup, ...]
|
The measurement plan that the submitted circuits follow, in the order of submission. |
required |
n_observables
|
int
|
The number of expectation values to compute. |
required |
n_qubits
|
int
|
The number of qubits of the circuit, used to read the keys of the histograms. |
required |
poll_timeout_seconds
|
int
|
How many seconds |
required |
cancel()
Ask IQM to cancel the job. It returns without waiting.
counts()
Wait until the job finishes, then return the raw histograms.
There is one histogram for each measurement group, and qubit 0 is the
leftmost bit of each key. These are the counts that the device returned.
result
computes the expectation values of the observables from them.
job_id()
Return the identifier of the IQM job, to look it up in Resonance or through iqm-client.
result()
Wait until the job finishes, then return one expectation value for each observable, in the order of the observables.
status()
Return the state of the job, as the job object reports it, without waiting.
The state is a string such as "QUEUED", "RUNNING", "DONE",
"CANCELLED" or "ERROR".