QuantumInstance migration guide
Deprecation Notice
This guide precedes the introduction of the V2 primitives interface. It was created to guide users from QuantumInstance
to
the V1 primitives interface. Following the introduction of the V2 primitives, some providers have deprecated V1 primitive
implementations in favor of the V2 alternatives. If you are interested in following this guide, we recommend combining it with the
Migrate to V2 primitives guide to bring your code to the most updated state.
The qiskit.utils.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 and combining execution to
conform to job limits,
and ensuring reliable circuit execution with additional job management tools.
The qiskit.utils.QuantumInstance
class is being deprecated because
the functionality of qiskit.utils.QuantumInstance.execute
has been delegated to
the different implementations of the qiskit.primitives
base classes.
The following table summarizes the migration alternatives for the qiskit.utils.QuantumInstance
class:
QuantumInstance method | Alternative |
---|---|
qiskit.utils.QuantumInstance.execute | qiskit.primitives.Sampler.run or qiskit.primitives.Estimator.run |
qiskit.utils.QuantumInstance.transpile | qiskit.compiler.transpile or qiskit.transpiler.pass_manager.PassManager.run |
qiskit.utils.QuantumInstance.assemble | qiskit.compiler.assemble [Deprecated: no longer part of the workflow] |
The remainder of this guide focused on the qiskit.utils.QuantumInstance.execute
to
qiskit.primitives
migration path.
Background on the Qiskit primitives
The Qiskit primitives are algorithmic abstractions that encapsulate system or simulator access for easy integration into algorithm workflows.
There are two types of primitives: Sampler and Estimator.
Qiskit provides reference implementations in qiskit.primitives.Sampler
and qiskit.primitives.Estimator
. Additionally,
qiskit.primitives.BackendSampler
and qiskit.primitives.BackendEstimator
are
wrappers for backend.run()
that follow the primitives interface.
Providers can implement these primitives as subclasses of qiskit.primitives.BaseSampler
and qiskit.primitives.BaseEstimator
, respectively.
Qiskit Runtime (qiskit_ibm_runtime
) and Aer (qiskit_aer.primitives
) are examples of native implementations of primitives.
This guide uses the following naming convention:
- Primitives - Any Sampler or Estimator implementation using base classes
qiskit.primitives.BackendSampler
and aqiskit.primitives.BackendEstimator
. - Reference primitives -
qiskit.primitives.Sampler
andqiskit.primitives.Estimator
are reference implementations that come with Qiskit. - Aer primitives - The Aer primitive implementations
qiskit_aer.primitives.Sampler
andqiskit_aer.primitives.Estimator
. - Qiskit Runtime primitives - The Qiskit Runtime primitive implementations
qiskit_ibm_runtime.Sampler
andqiskit_ibm_runtime.Estimator
. Backend
primitives - Instances ofqiskit.primitives.BackendSampler
andqiskit.primitives.BackendEstimator
. These allow any processor to implement primitive interfaces.
Choose the right primitive for your task
The qiskit.utils.QuantumInstance
was designed to be an abstraction of transpile and run.
It took inspiration from qiskit.execute_function.execute
but retained configuration information that could be set
at the algorithm level to save the user from defining the same parameters for every transpile or execute call.
The qiskit.primitives
classes share some of these features, but unlike the qiskit.utils.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 or how it is expected to run.
Primitives are also 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.
To determine which primitive to use instead of qiskit.utils.QuantumInstance
, you should ask
yourself two questions:
-
What is the minimal unit of information used by your algorithm?
- If it uses an expectation value, you need an Estimator.
- If it uses a probability distribution (from sampling the device), you need a Sampler
-
How do you want to run your circuits?
This question is not new. In the legacy algorithm workflow, you would set up a
qiskit.utils.QuantumInstance
with either a real system from a provider, or a simulator. For this migration, this "system 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 systems (or cloud simulators): Qiskit Runtime primitives
- Accessing non runtime-enabled systems :
Backend
primitives
Arguably, the Sampler is the closest primitive to qiskit.utils.QuantumInstance
, as they
both execute circuits and provide a result. However, with the qiskit.utils.QuantumInstance
,
the result data was system-dependent (it could be a counts dict
, a numpy.array
for
statevector simulations, and so on), while Sampler normalizes its SamplerResult
to
return a qiskit.result.QuasiDistribution
object with the resulting quasi-probability distribution.
The Estimator provides a specific abstraction for the expectation value calculation that can replace
qiskit.utils.QuantumInstance
as well as the associated pre- and post-processing steps, usually performed
with an additional library such as qiskit.opflow
.
Choose the right primitive for your settings
Certain qiskit.utils.QuantumInstance
features are only available in certain primitive implementations.
The following table summarizes the most common qiskit.utils.QuantumInstance
settings and which
primitives expose a similar setting through their interface:
In some cases, a setting might not be exposed through the interface, but there might be 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 you specify the option skip_transpilation=True
. For more information,
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 , | 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, see Advanced Runtime Options.
(**) For more information on Runtime sessions, see About Sessions.
Code examples
Example 1: Circuit sampling with local simulation
QuantumInstance
The only option for local simulations using the quantum instance was using an Aer simulator.
If no simulation method is specified, the Aer simulator defaults to an exact simulation
(statevector/stabilizer), if shots are specified, it adds shot noise.
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)
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. Reference primitives
Basic simulation implemented using the qiskit.quantum_info
module. If shots are
specified, the results include shot noise. 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. Aer primitives
This method uses Aer simulation following the statevector method. This is a closer replacement of the
qiskit.utils.QuantumInstance
example, as they are access the same simulator. Note that
the resulting quasi-probability distribution does not use bitstrings but integers to identify the states.
The qiskit.result.QuasiDistribution
class that is returned as part of the qiskit.primitives.SamplerResult
exposes two methods to convert the result keys from integer to binary strings / hexadecimal:
from qiskit import QuantumCircuit
from qiskit_aer.primitives import Sampler
circuit = QuantumCircuit(2)
circuit.x(0)
circuit.x(1)
circuit.measure_all()
# If a noise model is 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}
Example 2: Expectation value calculation with local noisy simulation
While this example does not include a direct call to QuantumInstance.execute()
, it shows
how to migrate from a qiskit.utils.QuantumInstance
-based to a qiskit.primitives
-based
workflow.
QuantumInstance
The most common use case for computing expectation values with the Quantum Instance was as in combination with the
qiskit.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 QuantumInstance with a 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)
-0.04687500000000008
Primitives
The primitives allow the combination of the opflow and QuantumInstance 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 a noise model is 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)
[-0.04101562]
Example 3: Circuit sampling on IBM system with error mitigation
QuantumInstance
The QuantumInstance
interface allowed configuring 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})
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 QuantumInstance measurement error mitigation implementation, but this
is not a one-to-one replacement.
For more information about the error mitigation options in the Qiskit Runtime primitives, see Configure Error Mitigation.
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}]
Example 4: Circuit sampling with custom bound and unbound pass managers
Transpilation management is different between QuantumInstance
and the primitives.
QuantumInstance allowed you to:
- Define bound and unbound pass managers that were called during
.execute()
. - Explicitly call its
.transpile()
method with a specific pass manager.
QuantumInstance did not manage parameter bindings on parametrized quantum circuits. Therefore, if a bound_pass_manager
was set, the circuit sent to QuantumInstance.execute()
could
not have any free parameters.
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 setskip_transpilation=True
in the corresponding primitive. -
The only primitives that accept a custom bound transpiler pass manager are instances of
qiskit.primitives.BackendSampler
orqiskit.primitives.BackendEstimator
. If abound_pass_manager
is defined, theskip_transpilation=True
option does not skip this bound pass.CautionCare is needed when setting
skip_transpilation=True
with the Estimator primitive. Since operator and circuit size need to match for the Estimator, if the custom transpilation changes the circuit size, the operator must be adapted before sending it to the Estimator, as there is no mechanism to identify the active qubits it should consider.
Note that the primitives do handle parameter bindings, so that even if a bound_pass_manager
is defined in a
qiskit.primitives.BackendSampler
or qiskit.primitives.BackendEstimator
, you do not have to manually assign parameters as expected in the QuantumInstance workflow.
The two-stage transpilation was added to the QuantumInstance
to allow running pulse-efficient transpilation passes with the qiskit.opflow.converters.CircuitSampler
class. The following
example shows how to migrate this use case, where the QuantumInstance.execute()
method is called by the qiskit.opflow.converters.CircuitSampler
code.
QuantumInstance
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}
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}]