Quantum Instance Migration Guide
The QuantumInstance
is a utility class that allows the joint
configuration of the circuit transpilation and execution steps, and provides functions
at a higher level of abstraction for a more convenient integration with algorithms.
These include measurement error mitigation, splitting/combining execution to
conform to job limits,
and ensuring reliable execution of circuits with additional job management tools.
The QuantumInstance
is being deprecated for several reasons:
On one hand, the functionality of execute()
has
now been delegated to the different implementations of the primitives
base classes.
On the other hand, with the direct implementation of transpilation at the primitives level,
the algorithms no longer
need to manage that aspect of execution, and thus transpile()
is no longer
required by the workflow. If desired, custom transpilation routines can still be performed at the
user level through the transpiler
module (see table below).
The following table summarizes the migration alternatives for the QuantumInstance
class:
The remainder of this guide will focus on the QuantumInstance.execute()
to
primitives
migration path.
Contents
Achtung
Background on the Qiskit Primitives
The Qiskit Primitives are algorithmic abstractions that encapsulate the access to backends or simulators
for an easy integration into algorithm workflows.
The current pool of primitives includes two different types of primitives: Sampler and
Estimator.
Qiskit provides reference implementations in qiskit.primitives.Sampler
and qiskit.primitives.Estimator
. Additionally,
qiskit.primitives.BackendSampler
and a qiskit.primitives.BackendEstimator
are
wrappers for backend.run()
that follow the primitives interface.
Providers can implement these primitives as subclasses of BaseSampler
and BaseEstimator
respectively.
IBM’s Qiskit Runtime (qiskit_ibm_runtime
) and Aer (qiskit_aer.primitives
) are examples of native implementations of primitives.
This guide uses the following naming convention:
For guidelines on which primitives to choose for your task, please continue reading.
Choosing the right primitive for your task
The QuantumInstance
was designed to be an abstraction over transpile/run.
It took inspiration from execute()
, but retained config information that could be set
at the algorithm level, to save the user from defining the same parameters for every transpile/execute call.
The qiskit.primitives
share some of these features, but unlike the QuantumInstance
,
there are multiple primitive classes, and each is optimized for a specific
purpose. Selecting the right primitive (Sampler
or Estimator
) requires some knowledge about
what it is expected to do and where/how it is expected to run.
Bemerkung
The role of the primitives is two-fold. On one hand, they act as access points to backends and simulators.
On the other hand, they are algorithmic abstractions with defined tasks:
The Estimator
takes in circuits and observables and returns expectation values.
The Sampler
takes in circuits, measures them, and returns their quasi-probability distributions.
In order to know which primitive to use instead of QuantumInstance
, you should ask
yourself two questions:
- What is the minimal unit of information used by your algorithm?
Expectation value - you will need an Estimator
Probability distribution (from sampling the device) - you will need a Sampler
How do you want to execute your circuits?
This question is not new. In the legacy algorithm workflow, you would have to decide to set up a
QuantumInstance
with either a real backend from a provider, or a simulator.
Now, this „backend selection“ process is translated to where do you import your primitives
from:
Using local statevector simulators for quick prototyping: Reference Primitives
Using local noisy simulations for finer algorithm tuning: Aer Primitives
Accessing runtime-enabled backends (or cloud simulators): Qiskit Runtime Primitives
Accessing non runtime-enabled backends : Backend Primitives
Arguably, the Sampler
is the closest primitive to QuantumInstance
, as they
both execute circuits and provide a result back. However, with the QuantumInstance
,
the result data was backend dependent (it could be a counts dict
, a numpy.array
for
statevector simulations, etc), while the Sampler
normalizes its SamplerResult
to
return a QuasiDistribution
object with the resulting quasi-probability distribution.
The Estimator
provides a specific abstraction for the expectation value calculation that can replace
the use of QuantumInstance
as well as the associated pre- and post-processing steps, usually performed
with an additional library such as qiskit.opflow
.
Choosing the right primitive for your settings
Certain QuantumInstance
features are only available in certain primitive implementations.
The following table summarizes the most common QuantumInstance
settings and which
primitives expose a similar setting through their interface:
Achtung
In some cases, a setting might not be exposed through the interface, but there might an alternative path to make
it work. This is the case for custom transpiler passes, which cannot be set through the primitives interface,
but pre-transpiled circuits can be sent if setting the option skip_transpilation=True
. For more information,
please refer to the API reference or source code of the desired primitive implementation.
QuantumInstance |
Reference Primitives |
Aer Primitives |
Qiskit Runtime Primitives |
Backend Primitives |
Select backend |
No |
No |
Yes |
Yes |
Set shots |
Yes |
Yes |
Yes |
Yes |
Simulator settings: basis_gates , coupling_map , initial_layout , noise_model , backend_options |
No |
Yes |
Yes |
No (inferred from internal backend ) |
Transpiler settings: seed_transpiler , optimization_level |
No |
No |
Yes (via options ) (*) |
Yes (via .set_transpile_options() ) |
Set unbound pass_manager |
No |
No |
No (but can skip_transpilation ) |
No (but can skip_transpilation ) |
Set bound_pass_manager |
No |
No |
No |
Yes |
Set backend_options : common ones were memory and meas_level |
No |
No |
No (only qubit_layout ) |
No |
Measurement error mitigation: measurement_error_mitigation_cls , cals_matrix_refresh_period ,
measurement_error_mitigation_shots , mit_pattern |
No |
No |
Sampler default -> M3 (*) |
No |
Job management: job_callback , max_job_retries , timeout , wait |
Does not apply |
Does not apply |
Sessions, callback (**) |
No |
(*) For more information on error mitigation and setting options on Qiskit Runtime Primitives, visit
this link.
(**) For more information on Runtime sessions, visit this how-to.
Code examples
Using Quantum Instance
The only alternative for local simulations using the quantum instance was using an Aer simulator backend.
If no simulation method is specified, the Aer simulator will default to an exact simulation
(statevector/stabilizer), if shots are specified, it will add shot noise.
Please note that QuantumInstance.execute()
returned the counts in hexadecimal format.
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
from qiskit.utils import QuantumInstance
circuit = QuantumCircuit(2)
circuit.x(0)
circuit.x(1)
circuit.measure_all()
simulator = AerSimulator()
qi = QuantumInstance(backend=simulator, shots=200)
result = qi.execute(circuit).results[0]
data = result.data
counts = data.counts
print("Counts: ", counts)
print("Data: ", data)
print("Result: ", result)
Counts: {'0x3': 200}
Data: ExperimentResultData(counts={'0x3': 200})
Result: ExperimentResult(shots=200, success=True, meas_level=2, data=ExperimentResultData(counts={'0x3': 200}), header=QobjExperimentHeader(clbit_labels=[['meas', 0], ['meas', 1]], creg_sizes=[['meas', 2]], global_phase=0.0, memory_slots=2, metadata={}, n_qubits=2, name='circuit-99', qreg_sizes=[['q', 2]], qubit_labels=[['q', 0], ['q', 1]]), status=DONE, seed_simulator=2846213898, metadata={'parallel_state_update': 16, 'parallel_shots': 1, 'sample_measure_time': 0.00025145, 'noise': 'ideal', 'batched_shots_optimization': False, 'remapped_qubits': False, 'device': 'CPU', 'active_input_qubits': [0, 1], 'measure_sampling': True, 'num_clbits': 2, 'input_qubit_map': [[1, 1], [0, 0]], 'num_qubits': 2, 'method': 'stabilizer', 'fusion': {'enabled': False}}, time_taken=0.000672166)
Using Primitives
The primitives offer two alternatives for local simulation, one with the Reference primitives
and one with the Aer primitives. As mentioned above the closest alternative to QuantumInstance.execute()
for sampling is the Sampler
primitive.
a. Using the Reference Primitives
Basic simulation implemented using the qiskit.quantum_info
module. If shots are
specified, the results will include shot noise. Please note that
the resulting quasi-probability distribution does not use bitstrings but integers to identify the states.
from qiskit import QuantumCircuit
from qiskit.primitives import Sampler
circuit = QuantumCircuit(2)
circuit.x(0)
circuit.x(1)
circuit.measure_all()
sampler = Sampler()
result = sampler.run(circuit, shots=200).result()
quasi_dists = result.quasi_dists
print("Quasi-dists: ", quasi_dists)
print("Result: ", result)
Quasi-dists: [{3: 1.0}]
Result: SamplerResult(quasi_dists=[{3: 1.0}], metadata=[{'shots': 200}])
b. Using the Aer Primitives
Aer simulation following the statevector method. This would be the closer replacement of the
QuantumInstance
example, as they are both accessing the same simulator. For this reason, the output metadata is
closer to the Quantum Instance’s output. Please note that
the resulting quasi-probability distribution does not use bitstrings but integers to identify the states.
from qiskit import QuantumCircuit
from qiskit_aer.primitives import Sampler
circuit = QuantumCircuit(2)
circuit.x(0)
circuit.x(1)
circuit.measure_all()
# if no Noise Model provided, the aer primitives
# perform an exact (statevector) simulation
sampler = Sampler()
result = sampler.run(circuit, shots=200).result()
quasi_dists = result.quasi_dists
# convert keys to binary bitstrings
binary_dist = quasi_dists[0].binary_probabilities()
print("Quasi-dists: ", quasi_dists)
print("Result: ", result)
print("Binary quasi-dist: ", binary_dist)
Quasi-dists: [{3: 1.0}]
Result: SamplerResult(quasi_dists=[{3: 1.0}], metadata=[{'shots': 200, 'simulator_metadata': {'parallel_state_update': 16, 'parallel_shots': 1, 'sample_measure_time': 9.016e-05, 'noise': 'ideal', 'batched_shots_optimization': False, 'remapped_qubits': False, 'device': 'CPU', 'active_input_qubits': [0, 1], 'measure_sampling': True, 'num_clbits': 2, 'input_qubit_map': [[1, 1], [0, 0]], 'num_qubits': 2, 'method': 'statevector', 'fusion': {'applied': False, 'max_fused_qubits': 5, 'threshold': 14, 'enabled': True}}}])
Binary quasi-dist: {'11': 1.0}
While this example does not include a direct call to QuantumInstance.execute()
, it shows
how to migrate from a QuantumInstance
-based to a primitives
-based
workflow.
Using Quantum Instance
The most common use case for computing expectation values with the Quantum Instance was as in combination with the
opflow
library. You can see more information in the opflow migration guide.
from qiskit import QuantumCircuit
from qiskit.opflow import StateFn, PauliSumOp, PauliExpectation, CircuitSampler
from qiskit.utils import QuantumInstance
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel
from qiskit_ibm_provider import IBMProvider
# Define problem using opflow
op = PauliSumOp.from_list([("XY",1)])
qc = QuantumCircuit(2)
qc.x(0)
qc.x(1)
state = StateFn(qc)
measurable_expression = StateFn(op, is_measurement=True).compose(state)
expectation = PauliExpectation().convert(measurable_expression)
# Define Quantum Instance with noisy simulator
provider = IBMProvider()
device = provider.get_backend("ibmq_manila")
noise_model = NoiseModel.from_backend(device)
coupling_map = device.configuration().coupling_map
backend = AerSimulator()
qi = QuantumInstance(backend=backend, shots=1024,
seed_simulator=42, seed_transpiler=42,
coupling_map=coupling_map, noise_model=noise_model)
# Run
sampler = CircuitSampler(qi).convert(expectation)
expectation_value = sampler.eval().real
print(expectation_value)
Using Primitives
The primitives now allow the combination of the opflow and quantum instance functionality in a single Estimator
.
In this case, for local noisy simulation, this will be the Aer Estimator.
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit_aer.noise import NoiseModel
from qiskit_aer.primitives import Estimator
from qiskit_ibm_provider import IBMProvider
# Define problem
op = SparsePauliOp("XY")
qc = QuantumCircuit(2)
qc.x(0)
qc.x(1)
# Define Aer Estimator with noisy simulator
device = provider.get_backend("ibmq_manila")
noise_model = NoiseModel.from_backend(device)
coupling_map = device.configuration().coupling_map
# if Noise Model provided, the aer primitives
# perform a "qasm" simulation
estimator = Estimator(
backend_options={ # method chosen automatically to match options
"coupling_map": coupling_map,
"noise_model": noise_model,
},
run_options={"seed": 42, "shots": 1024},
transpile_options={"seed_transpiler": 42},
)
# Run
expectation_value = estimator.run(qc, op).result().values
print(expectation_value)
Using Quantum Instance
The QuantumInstance
interface allowed the configuration of measurement error mitigation settings such as the method, the
matrix refresh period or the mitigation pattern. This configuration is no longer available in the primitives
interface.
from qiskit import QuantumCircuit
from qiskit.utils import QuantumInstance
from qiskit.utils.mitigation import CompleteMeasFitter
from qiskit_ibm_provider import IBMProvider
circuit = QuantumCircuit(2)
circuit.x(0)
circuit.x(1)
circuit.measure_all()
provider = IBMProvider()
backend = provider.get_backend("ibmq_montreal")
qi = QuantumInstance(
backend=backend,
shots=4000,
measurement_error_mitigation_cls=CompleteMeasFitter,
cals_matrix_refresh_period=0,
)
result = qi.execute(circuit).results[0].data
print(result)
ExperimentResultData(counts={'11': 4000})
Using Primitives
The Qiskit Runtime Primitives offer a suite of error mitigation methods that can be easily turned on with the
resilience_level
option. These are, however, not configurable. The sampler’s resilience_level=1
is the closest alternative to the Quantum Instance’s measurement error mitigation implementation, but this
is not a 1-1 replacement.
For more information on the error mitigation options in the Qiskit Runtime Primitives, you can check out the following
link.
from qiskit import QuantumCircuit
from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, Options
circuit = QuantumCircuit(2)
circuit.x(0)
circuit.x(1)
circuit.measure_all()
service = QiskitRuntimeService(channel="ibm_quantum")
backend = service.backend("ibmq_montreal")
options = Options(resilience_level = 1) # 1 = measurement error mitigation
sampler = Sampler(session=backend, options=options)
# Run
result = sampler.run(circuit, shots=4000).result()
quasi_dists = result.quasi_dists
print("Quasi dists: ", quasi_dists)
Quasi dists: [{2: 0.0008492371522941081, 3: 0.9968874384378738, 0: -0.0003921227905920063,
1: 0.002655447200424097}]
The management of transpilation is different between the QuantumInstance
and the Primitives.
The Quantum Instance allowed you to:
However:
On the other hand, when using the primitives:
You cannot explicitly access their transpilation routine.
The mechanism to apply custom transpilation passes to the Aer, Runtime and Backend primitives is to pre-transpile
locally and set skip_transpilation=True
in the corresponding primitive.
The only primitives that currently accept a custom bound transpiler pass manager are instances of BackendSampler
or BackendEstimator
.
If a bound_pass_manager
is defined, the skip_transpilation=True
option will not skip this bound pass.
Achtung
Care is needed when setting skip_transpilation=True
with the Estimator
primitive.
Since operator and circuit size need to match for the Estimator, should the custom transpilation change
the circuit size, then the operator must be adapted before sending it
to the Estimator, as there is no currently no mechanism to identify the active qubits it should consider.
Note that the primitives do handle parameter bindings, meaning that even if a bound_pass_manager
is defined in a
BackendSampler
or BackendEstimator
, you do not have to manually assign parameters as expected in the Quantum Instance workflow.
The use-case that motivated the addition of the two-stage transpilation to the QuantumInstance
was to allow
running pulse-efficient transpilation passes with the CircuitSampler
class. The following
example shows to migrate this particular use-case, where the QuantumInstance.execute()
method is called
under the hood by the CircuitSampler
.
Using Quantum Instance
from qiskit.circuit.library.standard_gates.equivalence_library import StandardEquivalenceLibrary as std_eqlib
from qiskit.circuit.library import RealAmplitudes
from qiskit.opflow import CircuitSampler, StateFn
from qiskit.providers.fake_provider import FakeBelem
from qiskit.transpiler import PassManager, PassManagerConfig, CouplingMap
from qiskit.transpiler.preset_passmanagers import level_1_pass_manager
from qiskit.transpiler.passes import (
Collect2qBlocks, ConsolidateBlocks, Optimize1qGatesDecomposition,
RZXCalibrationBuilderNoEcho, UnrollCustomDefinitions, BasisTranslator
)
from qiskit.transpiler.passes.optimization.echo_rzx_weyl_decomposition import EchoRZXWeylDecomposition
from qiskit.utils import QuantumInstance
# Define backend
backend = FakeBelem()
# Build the pass manager for the parameterized circuit
rzx_basis = ['rzx', 'rz', 'x', 'sx']
coupling_map = CouplingMap(backend.configuration().coupling_map)
config = PassManagerConfig(basis_gates=rzx_basis, coupling_map=coupling_map)
pre = level_1_pass_manager(config)
inst_map = backend.defaults().instruction_schedule_map
# Build a pass manager for the CX decomposition (works only on bound circuits)
post = PassManager([
# Consolidate consecutive two-qubit operations.
Collect2qBlocks(),
ConsolidateBlocks(basis_gates=['rz', 'sx', 'x', 'rxx']),
# Rewrite circuit in terms of Weyl-decomposed echoed RZX gates.
EchoRZXWeylDecomposition(inst_map),
# Attach scaled CR pulse schedules to the RZX gates.
RZXCalibrationBuilderNoEcho(inst_map),
# Simplify single-qubit gates.
UnrollCustomDefinitions(std_eqlib, rzx_basis),
BasisTranslator(std_eqlib, rzx_basis),
Optimize1qGatesDecomposition(rzx_basis),
])
# Instantiate qi
quantum_instance = QuantumInstance(backend, pass_manager=pre, bound_pass_manager=post)
# Define parametrized circuit and parameter values
qc = RealAmplitudes(2)
print(qc.decompose())
param_dict = {p: 0.5 for p in qc.parameters}
# Instantiate CircuitSampler
sampler = CircuitSampler(quantum_instance)
# Run
quasi_dists = sampler.convert(StateFn(qc), params=param_dict).sample()
print("Quasi-dists: ", quasi_dists)
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
q_0: ┤ Ry(θ[0]) ├──■──┤ Ry(θ[2]) ├──■──┤ Ry(θ[4]) ├──■──┤ Ry(θ[6]) ├
├──────────┤┌─┴─┐├──────────┤┌─┴─┐├──────────┤┌─┴─┐├──────────┤
q_1: ┤ Ry(θ[1]) ├┤ X ├┤ Ry(θ[3]) ├┤ X ├┤ Ry(θ[5]) ├┤ X ├┤ Ry(θ[7]) ├
└──────────┘└───┘└──────────┘└───┘└──────────┘└───┘└──────────┘
Quasi-dists: {'11': 0.443359375, '10': 0.21875, '01': 0.189453125, '00': 0.1484375}
Using Primitives
Let’s see how the workflow changes with the Backend Sampler:
from qiskit.circuit.library.standard_gates.equivalence_library import StandardEquivalenceLibrary as std_eqlib
from qiskit.circuit.library import RealAmplitudes
from qiskit.primitives import BackendSampler
from qiskit.providers.fake_provider import FakeBelem
from qiskit.transpiler import PassManager, PassManagerConfig, CouplingMap
from qiskit.transpiler.preset_passmanagers import level_1_pass_manager
from qiskit.transpiler.passes import (
Collect2qBlocks, ConsolidateBlocks, Optimize1qGatesDecomposition,
RZXCalibrationBuilderNoEcho, UnrollCustomDefinitions, BasisTranslator
)
from qiskit.transpiler.passes.optimization.echo_rzx_weyl_decomposition import EchoRZXWeylDecomposition
# Define backend
backend = FakeBelem()
# Build the pass manager for the parameterized circuit
rzx_basis = ['rzx', 'rz', 'x', 'sx']
coupling_map = CouplingMap(backend.configuration().coupling_map)
config = PassManagerConfig(basis_gates=rzx_basis, coupling_map=coupling_map)
pre = level_1_pass_manager(config)
# Build a pass manager for the CX decomposition (works only on bound circuits)
inst_map = backend.defaults().instruction_schedule_map
post = PassManager([
# Consolidate consecutive two-qubit operations.
Collect2qBlocks(),
ConsolidateBlocks(basis_gates=['rz', 'sx', 'x', 'rxx']),
# Rewrite circuit in terms of Weyl-decomposed echoed RZX gates.
EchoRZXWeylDecomposition(inst_map),
# Attach scaled CR pulse schedules to the RZX gates.
RZXCalibrationBuilderNoEcho(inst_map),
# Simplify single-qubit gates.
UnrollCustomDefinitions(std_eqlib, rzx_basis),
BasisTranslator(std_eqlib, rzx_basis),
Optimize1qGatesDecomposition(rzx_basis),
])
# Define parametrized circuit and parameter values
qc = RealAmplitudes(2)
qc.measure_all() # add measurements!
print(qc.decompose())
# Instantiate backend sampler with skip_transpilation
sampler = BackendSampler(backend=backend, skip_transpilation=True, bound_pass_manager=post)
# Run unbound transpiler pass
transpiled_circuit = pre.run(qc)
# Run sampler
quasi_dists = sampler.run(transpiled_circuit, [[0.5] * len(qc.parameters)]).result().quasi_dists
print("Quasi-dists: ", quasi_dists)
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ░ ┌─┐
q_0: ┤ Ry(θ[0]) ├──■──┤ Ry(θ[2]) ├──■──┤ Ry(θ[4]) ├──■──┤ Ry(θ[6]) ├─░─┤M├───
├──────────┤┌─┴─┐├──────────┤┌─┴─┐├──────────┤┌─┴─┐├──────────┤ ░ └╥┘┌─┐
q_1: ┤ Ry(θ[1]) ├┤ X ├┤ Ry(θ[3]) ├┤ X ├┤ Ry(θ[5]) ├┤ X ├┤ Ry(θ[7]) ├─░──╫─┤M├
└──────────┘└───┘└──────────┘└───┘└──────────┘└───┘└──────────┘ ░ ║ └╥┘
meas: 2/═══════════════════════════════════════════════════════════════════╩══╩═
0 1
Quasi-dists: [{1: 0.18359375, 2: 0.2333984375, 0: 0.1748046875, 3: 0.408203125}]