Skip to content

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 url and a token (or set the IQM_TOKEN environment variable). You can also pass quantum_computer to choose one machine when the server hosts several.
  • Local fake backends: pass IQMFakeAdonis(), IQMFakeApollo(), IQMFakeDeneb() or a similar fake backend as backend_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 6 r gates 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 builds the default configuration, which reads the server address from IQM_SERVER_URL.

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

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

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

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

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 (2**n_qubits,) whose entries add up to 1.

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 resources() of the Qiskit Runtime backend.

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

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

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

required

Returns:

Type Description
ProbabilityJobHandle

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

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

A 1-D parameter vector.

required
inputs ndarray

A batch of inputs, of shape (B, n_inputs).

required

Returns:

Type Description
ndarray

The probabilities estimated from the shots, of shape (B,).

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 optionally quantum_computer, opens an IQMProvider for an IQM Server. This needs a token, from token or from the IQM_TOKEN environment variable.
  • backend_instance uses any IQM BackendV2 directly: a fake backend for offline work, or a backend that you configured yourself. It takes precedence over url, 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 "https://resonance.iqm.tech/". It is ignored when backend_instance is set. With None, the backend reads the address from the IQM_SERVER_URL environment variable when the circuit is compiled.

None
quantum_computer str | None

The machine to use when the server hosts more than one, for example "garnet". With None, the server's default is used.

None
backend_instance Any

An IQM backend that is already configured. It takes precedence over url.

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 qiskit.transpile, from 0 to 3.

1
seed_transpiler int | None

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

None
transpile_mode TranspileMode

"parametric" transpiles once and sets the parameter values at each call. "per_call" sets the values first and transpiles at every call, which gives shallower circuits but costs one transpilation per evaluation.

'parametric'
group_commuting bool

Whether to put Pauli terms that commute qubit by qubit into shared measurement circuits. With False, one circuit runs for each term: there are more jobs, but each term gets all the shots.

True
token str | None

The access token for the IQM Server. It is passed on only when it is set. With None, iqm-client handles authentication on its own, from IQM_TOKEN and from its own tokens file. Do not set both token and IQM_TOKEN, because iqm-client rejects a token given in two ways at once. This field is secret: repr() never shows it, and it is never saved in .eigenkit bundles, pickles or joblib checkpoints. After a load it is None, so set IQM_TOKEN.

None
calibration_set_id Any

The calibration set to use, as a UUID. With None, the server's default is used.

None
use_metrics bool

Whether to ask the server for the error rates of each qubit and each coupler, and load them into the InstructionProperties of the target. With False (the default of iqm-client itself), the target has no error rates, so the Qiskit layout pass does not see the noise. It then cannot prefer the better qubits, and a comparison with a backend that does have calibration data is not like for like. It costs one extra request at compile time, and it is ignored when backend_instance is set.

False
provider_kwargs dict

Extra keyword arguments passed to IQMProvider, for example tokens_file.

dict()
poll_timeout_seconds int

How many seconds result() waits before giving up.

3600
enable_gradients bool

Whether to allow parameter-shift gradients on the device, for PyTorch .backward(), jacobian() and gradient-based optimisers. It is off by default, because one backward pass runs the circuits of a forward call at least 2 * n_weights times. When it is set, compiling for a real device issues a CostWarning that gives this count.

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

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 result() waits before giving up.

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