Skip to main contentIBM Quantum Documentation Preview

QuantumInstance migration guide

Caution

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 methodAlternative
qiskit.utils.QuantumInstance.executeqiskit.primitives.Sampler.run or qiskit.primitives.Estimator.run
qiskit.utils.QuantumInstance.transpileqiskit.compiler.transpile or qiskit.transpiler.pass_manager.PassManager.run
qiskit.utils.QuantumInstance.assembleqiskit.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.

Caution

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:


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.

Note

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:

  1. 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
  2. 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:

Caution

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.

QuantumInstanceReference PrimitivesAer PrimitivesQiskit Runtime PrimitivesBackend Primitives
Select backendNoNoYesYes
Set shotsYesYesYesYes
Simulator settings: basis_gates, coupling_map, initial_layout, noise_model, backend_optionsNoYesYesNo (inferred from internal backend)
Transpiler settings: seed_transpiler, optimization_levelNoNoYes (via options) (*)Yes (via .set_transpile_options())
Set unbound pass_managerNoNoNo (but can skip_transpilation)No (but can skip_transpilation)
Set bound_pass_managerNoNoNoYes
Set backend_options: common ones were memory and meas_levelNoNoNo (only qubit_layout)No
Measurement error mitigation: measurement_error_mitigation_cls, cals_matrix_refresh_period,NoNoSampler default > M3 (*)No
Job management: job_callback, max_job_retries, timeout, waitDoes not applyDoes not applySessions, 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.

Note

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 set skip_transpilation=True in the corresponding primitive.

  • The only primitives that accept a custom bound transpiler pass manager are instances of qiskit.primitives.BackendSampler or qiskit.primitives.BackendEstimator. If a bound_pass_manager is defined, the skip_transpilation=True option does not skip this bound pass.

    Caution

    Care 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}]