Portuguese
Idiomas
English
Bengali
French
German
Japanese
Korean
Portuguese
Spanish
Tamil

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:

QuantumInstance method

Alternative

QuantumInstance.execute()

qiskit.primitives.Sampler.run() or qiskit.primitives.Estimator.run()

QuantumInstance.transpile()

qiskit.compiler.transpile()

QuantumInstance.assemble()

qiskit.compiler.assemble()

The remainder of this guide will focus on the QuantumInstance.execute() to primitives migration path.

Contents

Atenção

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.

Nota

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:

  1. What is the minimal unit of information used by your algorithm?
    1. Expectation value - you will need an Estimator

    2. Probability distribution (from sampling the device) - you will need a Sampler

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

    1. Using local statevector simulators for quick prototyping: Reference Primitives

    2. Using local noisy simulations for finer algorithm tuning: Aer Primitives

    3. Accessing runtime-enabled backends (or cloud simulators): Qiskit Runtime Primitives

    4. 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:

Atenção

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

Example 1: Circuit Sampling with Local Simulation

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.

Nota

The qiskit.result.QuasiDistribution class 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 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}
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 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)
-0.04687500000000008

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)
[-0.04101562]
Example 3: Circuit Sampling on IBM Backend with Error Mitigation

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}]
Example 4: Circuit Sampling with Custom Bound and Unbound Pass Managers

The management of transpilation is different between the QuantumInstance and the Primitives.

The Quantum Instance allowed you to:

  • Define bound and unbound pass managers that will be called during .execute().

  • Explicitly call its .transpile() method with a specific pass manager.

However:

  • The Quantum Instance did not manage parameter bindings on parametrized quantum circuits. This would mean that if a bound_pass_manager was set, the circuit sent to QuantumInstance.execute() could not have any free parameters.

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.

Atenção

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