Skip to main contentIBM Quantum Documentation

Migration examples

Follow these examples to design a Qiskit Runtime algorithm.


Use Estimator to design an algorithm

The Estimator primitive is used to design an algorithm that calculates expectation values.

Background

The role of the Estimator primitive is two-fold: it acts as an entry point to quantum devices or simulators, replacing the Backend interface (commonly referred to as backend.run()). Additionally, it is an algorithmic abstraction for expectation value calculations, so you don't have to manually construct the final expectation circuit. This results in a considerable reduction of the code complexity and a more compact algorithm design.

Note

Backend.run() model: In this model, you accessed real systems and remote simulators using the qiskit-ibmq-provider module (now migrated to qiskit-ibm-provider). To run local simulations, you could import a specific simulator from qiskit-aer. All of them followed the backend.run() interface.

Code example for qiskit-ibmq-provider & backend.run()
from qiskit import IBMQ
 
# Select provider
provider = IBMQ.get_provider(hub="ibm-q", group="open", project="main")
 
# Get backend
backend = provider.get_backend("ibmq_qasm_simulator") # cloud simulator
 
# Run
result = backend.run(expectation_circuits)
Code example for qiskit-aer & backend.run()
from qiskit_aer import AerSimulator # former import: from qiskit import Aer
 
# Get local simulator backend
backend = AerSimulator()
 
# Run
result = backend.run(expectation_circuits)

Primitives model: Access real systems and remote simulators through the qiskit-ibm-runtime primitives (Sampler and Estimator). To run local simulations, you can import specific local primitives from qiskit_aer.primitives and qiskit.primitives. All of them follow the BaseSampler and BaseEstimator interfaces, but only the Runtime primitives offer access to the Runtime service, sessions, and built-in error mitigation.

Code example for Runtime Estimator
from qiskit_ibm_runtime import QiskitRuntimeService, Estimator
 
# Define service
service = QiskitRuntimeService()
 
# Get backend
backend = service.backend("ibmq_qasm_simulator") # cloud simulator
 
# Define Estimator  
# (see tutorials for more information about sessions)
estimator = Estimator(session=backend)
 
# Run Expectation value calculation
result = estimator.run(circuits, observables).result()
Code example for Aer Estimator
from qiskit_aer import Estimator
 
# Get local simulator Estimator
estimator = Estimator()
 
# Run expectation value calculation
result = estimator.run(circuits, observables).result()

If your code previously calculated expectation values using backend.run(), you most likely used the qiskit.opflow module to handle operators and state functions. To support this scenario, the following migration example shows how to replace the (qiskit.opflow & backend.run()) workflow with an Estimator-based workflow.

End-to-end example

1. Problem definition

We want to compute the expectation value of a quantum state (circuit) with respect to a certain operator. In this example, we are using the H2 molecule and an arbitrary circuit as the quantum state:

from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
 
# Step 1: Define operator
op = SparsePauliOp.from_list(
    [
        ("II", -1.052373245772859),
        ("IZ", 0.39793742484318045),
        ("ZI", -0.39793742484318045),
        ("ZZ", -0.01128010425623538),
        ("XX", 0.18093119978423156),
    ]
)
 
# Step 2: Define quantum state
state = QuantumCircuit(2)
state.x(0)
state.x(1)
1.a. Legacy: Convert problem to opflow

qiskit.opflow provided its own classes to represent both operators and quantum states, so the problem defined above would be wrapped as:

from qiskit.opflow import CircuitStateFn, PauliSumOp
 
opflow_op = PauliSumOp(op)
opflow_state = CircuitStateFn(state)

This step is no longer necessary when using the primitives.

Note

For instructions to migrate from qiskit.opflow, see the Opflow migration guide.

2. Calculate expectation values on real device or cloud simulator

2.a. Legacy: Use opflow & backend.run()

The legacy workflow required many steps to compute an expectation value:

Note

Replace ibmq_qasm_simulator with your device name to see the complete workflow for a real device.

from qiskit.opflow import StateFn, PauliExpectation, CircuitSampler
from qiskit import IBMQ
 
# Define the state to sample
measurable_expression = StateFn(opflow_op, is_measurement=True).compose(opflow_state)
 
# Convert to expectation value calculation object
expectation = PauliExpectation().convert(measurable_expression)
 
# Define provider and backend
provider = IBMQ.get_provider(hub="ibm-q", group="open", project="main")
backend = provider.get_backend("ibmq_qasm_simulator")
 
# Inject backend into circuit sampler
sampler = CircuitSampler(backend).convert(expectation)
 
# Evaluate
expectation_value = sampler.eval().real
>>> print("expectation: ", expectation_value)
expectation:  -1.065734058826613
2.b. Updated: Use the Estimator Runtime primitive

The Estimator simplifies the user-side syntax, making it a more convenient tool for algorithm design.

Note

Replace ibmq_qasm_simulator with your device name to see the complete workflow for a real device.

from qiskit_ibm_runtime import QiskitRuntimeService, Estimator
 
service = QiskitRuntimeService()
backend = service.backend("ibmq_qasm_simulator")
 
estimator = Estimator(session=backend)
 
expectation_value = estimator.run(state, op).result().values

Note that the Estimator returns a list of values, as it can perform batched evaluations.

>>> print("expectation: ", expectation_value)
expectation:  [-1.06329149]

The Estimator Runtime primitive offers a series of features and tuning options that do not have a legacy alternative to migrate from, but can help improve your performance and results. For more information, refer to the following:

3. Other execution alternatives (non-Runtime)

This section describes how to use non-Runtime primitives to test an algorithm using local simulation. Let's assume that we want to solve the problem defined above with a local state vector simulation.

3.a. Legacy: Using the Qiskit Aer simulator
from qiskit.opflow import StateFn, PauliExpectation, CircuitSampler
from qiskit_aer import AerSimulator
 
# Define the state to sample
measurable_expression = StateFn(opflow_op, is_measurement=True).compose(opflow_state)
 
# Convert to expectation value calculation object
expectation = PauliExpectation().convert(measurable_expression)
 
# Define statevector simulator
simulator = AerSimulator(method="statevector", shots=100)
 
# Inject backend into circuit sampler
circuit_sampler = CircuitSampler(simulator).convert(expectation)
 
# Evaluate
expectation_value = circuit_sampler.eval().real
>>> print("expectation: ", expectation_value)
expectation:  -1.0636533500290943
3.b. Updated: Use the Reference Estimator or Aer Estimator primitive

The reference Estimator lets you perform either an exact or a shot-based noisy simulation based on the Statevector class in the qiskit.quantum_info module.

from qiskit.primitives import Estimator
 
estimator = Estimator()
 
expectation_value = estimator.run(state, op).result().values
 
# for shot-based simulation:
expectation_value = estimator.run(state, op, shots=100).result().values
>>> print("expectation: ", expectation_value)
expectation:  [-1.03134297]

You can still access the Aer Simulator through its dedicated Estimator. This can be handy for performing simulations with noise models. In this example, the simulation method has been updated to match the result from 3.a.

from qiskit_aer.primitives import Estimator # import change
 
estimator = Estimator(run_options= {"method": "statevector"})
 
expectation_value = estimator.run(state, op, shots=100).result().values
>>> print("expectation: ", expectation_value)
expectation:  [-1.06365335]

For more information on using the Aer primitives, see the VQE tutorial(opens in a new tab).

For more information about running noisy simulations with the Runtime primitives, see the Noisy simulators in Qiskit Runtime topic.


Use Sampler to design an algorithm

The Sampler primitive is used to design an algorithm that samples circuits and extracts probability distributions.

Background

The role of the Sampler primitive is two-fold: it acts as an entry point to quantum devices or simulators, replacing backend.run(). Additionally, it is an algorithmic abstraction to extract probability distributions from measurement counts.

Both Sampler and backend.run() take in circuits as inputs. The main difference is the format of the output: backend.run() outputs counts, while Sampler processes those counts and outputs the quasi-probability distribution associated with them.

Note

Backend.run() model: In this model, you used the qiskit-ibmq-provider (now migrated to qiskit-ibm-provider) module to access real systems and remote simulators. To run local simulations, you could import a specific simulator from qiskit-aer. All of them followed the backend.run() interface.

from qiskit import IBMQ
 
# Select provider
provider = IBMQ.load_account()
 
# Get backend
backend = provider.get_backend("ibmq_qasm_simulator") # Use the cloud simulator
 
# Run
result = backend.run(circuits)
from qiskit_aer import AerSimulator # former import: from qiskit import Aer
 
# Get local simulator backend
backend = AerSimulator()
 
# Run
result = backend.run(circuits)

Primitives model: Access real systems and remote simulators through the qiskit-ibm-runtime Sampler and Estimator primitives. To run local simulations, import specific local primitives from qiskit_aer.primitives and qiskit.primitives. All of them follow the BaseSampler and BaseEstimator interfaces, but only the Runtime primitives offer access to the Runtime service, sessions, and built-in error mitigation.

from qiskit_ibm_runtime import QiskitRuntimeService, Sampler
 
# Define service
service = QiskitRuntimeService()
 
# Get backend
backend = service.backend("ibmq_qasm_simulator") # Use a cloud simulator
 
# Define Sampler
# (see tutorials more more info on sessions)
sampler = Sampler(session=backend)
 
# Run Quasi-Probability calculation
result = sampler.run(circuits).result()
from qiskit_aer import Sampler
 
# Get local simulator Sampler
sampler = Sampler()
 
# Run Quasi-Probability calculation
result = sampler.run(circuits).result()

Next, we will show an end-to-end example of sampling a circuit: first, with backend.run(), then by using the Sampler.


End-to-end example

1. Problem definition

We want to find the probability (or quasi-probability) distribution associated with a quantum state:

Attention

Important: If you want to use the Sampler primitive, the circuit must contain measurements.

from qiskit import QuantumCircuit
 
circuit = QuantumCircuit(4)
circuit.h(range(2))
circuit.cx(0,1)
circuit.measure_all() # measurement!

2. Calculate probability distribution on a real device or cloud simulator

2.a. Legacy: Use backend.run()

The required steps to reach our goal with backend.run() are:

  1. Run circuits
  2. Get counts from the result object
  3. Use the counts and shots to calculate the probability distribution

First, we run the circuit in a cloud simulator and output the result object:

Note

Replace ibmq_qasm_simulator with your device name to see the complete workflow for a real device.

from qiskit import IBMQ
 
# Define provider and backend
provider = IBMQ.load_account()
backend = provider.get_backend("ibmq_qasm_simulator")
 
# Run
result = backend.run(circuit, shots=1024).result()
>>> print("result: ", result)
result:  Result(backend_name='ibmq_qasm_simulator', backend_version='0.11.0',
qobj_id='65bb8a73-cced-40c1-995a-8961cc2badc4', job_id='63fc95612751d57b6639f777',
success=True, results=[ExperimentResult(shots=1024, success=True, meas_level=2,
data=ExperimentResultData(counts={'0x0': 255, '0x1': 258, '0x2': 243, '0x3': 268}),
header=QobjExperimentHeader(clbit_labels=[['meas', 0], ['meas', 1], ['meas', 2], ['meas', 3]],
creg_sizes=[['meas', 4]], global_phase=0.0, memory_slots=4, metadata={}, n_qubits=4,
name='circuit-930', qreg_sizes=[['q', 4]], qubit_labels=[['q', 0], ['q', 1], ['q', 2], ['q', 3]]),
status=DONE, metadata={'active_input_qubits': [0, 1, 2, 3], 'batched_shots_optimization': False,
'device': 'CPU', 'fusion': {'enabled': False}, 'input_qubit_map': [[3, 3], [2, 2], [1, 1], [0, 0]],
'measure_sampling': True, 'method': 'stabilizer', 'noise': 'ideal', 'num_clbits': 4, 'num_qubits': 4,
'parallel_shots': 1, 'parallel_state_update': 16, 'remapped_qubits': False,
'sample_measure_time': 0.001001096}, seed_simulator=2191402198, time_taken=0.002996865)],
date=2023-02-27 12:35:00.203255+01:00, status=COMPLETED, header=QobjHeader(backend_name='ibmq_qasm_simulator',
backend_version='0.1.547'), metadata={'max_gpu_memory_mb': 0, 'max_memory_mb': 386782, 'mpi_rank': 0,
'num_mpi_processes': 1, 'num_processes_per_experiments': 1, 'omp_enabled': True, 'parallel_experiments': 1,
'time_taken': 0.003215252, 'time_taken_execute': 0.00303248, 'time_taken_load_qobj': 0.000169435},
time_taken=0.003215252, client_version={'qiskit': '0.39.5'})

Now we get the probability distribution from the output:

counts = result.get_counts(circuit)
quasi_dists = {}
for key,count in counts.items():
    quasi_dists[key] = count/1024
>>> print("counts: ", counts)
>>> print("quasi_dists: ", quasi_dists)
counts:  {'0000': 255, '0001': 258, '0010': 243, '0011': 268}
quasi_dists:  {'0000': 0.2490234375, '0001': 0.251953125, '0010': 0.2373046875, '0011': 0.26171875}

2.b. Updated: Use the Sampler runtime primitive

While the user-side syntax of the Sampler is very similar to backend.run(), notice that the workflow is now simplified, as the quasi-probability distribution is returned directly (no need to perform post-processing), together with some key metadata.

Note

Replace ibmq_qasm_simulator with your device name to see the complete workflow for a real device.

from qiskit_ibm_runtime import QiskitRuntimeService, Sampler
 
service = QiskitRuntimeService(channel="ibm_quantum")
backend = service.backend("ibmq_qasm_simulator")
 
sampler = Sampler(session=backend)
 
result = sampler.run(circuit, shots=1024).result()
quasi_dists = result.quasi_dists
>>> print("result: ", result)
>>> print("quasi_dists: ", quasi_dists)
result:  SamplerResult(quasi_dists=[{0: 0.2802734375, 1: 0.2509765625, 2: 0.232421875, 3: 0.236328125}],
metadata=[{'header_metadata': {}, 'shots': 1024, 'readout_mitigation_overhead': 1.0,
'readout_mitigation_time': 0.03801989182829857}])
quasi_dists:  [{0: 0.2802734375, 1: 0.2509765625, 2: 0.232421875, 3: 0.236328125}]
Attention

Be careful with the output format. With Sampler, the states are no longer represented by bit strings, for example, "11", but by integers, for example, 3. To convert the Sampler output to bit strings, you can use the QuasiDistribution.binary_probabilities() method, as shown below.

>>> # convert the output to bit strings
>>> binary_quasi_dist = quasi_dists[0].binary_probabilities()
>>> print("binary_quasi_dist: ", binary_quasi_dist)
binary_quasi_dist:  {'0000': 0.2802734375, '0001': 0.2509765625, '0010': 0.232421875, '0011': 0.236328125}

The Sampler Runtime primitive offers several features and tuning options that do not have a legacy alternative to migrate from, but can help improve your performance and results. For more information, refer to the following:

3. Other execution alternatives (non-Runtime)

The following migration paths use non-Runtime primitives to use local simulation to test an algorithm. Let's assume that we want to use a local state vector simulation to solve the problem defined above.

3.a. Legacy: Use the Qiskit Aer simulator

from qiskit_aer import AerSimulator
 
# Define the statevector simulator
simulator = AerSimulator(method="statevector")
 
# Run and get counts
result = simulator.run(circuit, shots=1024).result()
>>> print("result: ", result)
result:  Result(backend_name='aer_simulator_statevector', backend_version='0.11.2',
qobj_id='e51e51bc-96d8-4e10-aa4e-15ee6264f4a0', job_id='c603daa7-2c03-488c-8c75-8c6ea0381bbc',
success=True, results=[ExperimentResult(shots=1024, success=True, meas_level=2,
data=ExperimentResultData(counts={'0x2': 236, '0x0': 276, '0x3': 262, '0x1': 250}),
header=QobjExperimentHeader(clbit_labels=[['meas', 0], ['meas', 1], ['meas', 2], ['meas', 3]],
creg_sizes=[['meas', 4]], global_phase=0.0, memory_slots=4, metadata={}, n_qubits=4, name='circuit-930',
qreg_sizes=[['q', 4]], qubit_labels=[['q', 0], ['q', 1], ['q', 2], ['q', 3]]), status=DONE,
seed_simulator=3531074553, metadata={'parallel_state_update': 16, 'parallel_shots': 1,
'sample_measure_time': 0.000405246, 'noise': 'ideal', 'batched_shots_optimization': False,
'remapped_qubits': False, 'device': 'CPU', 'active_input_qubits': [0, 1, 2, 3], 'measure_sampling': True,
'num_clbits': 4, 'input_qubit_map': [[3, 3], [2, 2], [1, 1], [0, 0]], 'num_qubits': 4, 'method': 'statevector',
'fusion': {'applied': False, 'max_fused_qubits': 5, 'threshold': 14, 'enabled': True}}, time_taken=0.001981756)],
date=2023-02-27T12:38:18.580995, status=COMPLETED, header=QobjHeader(backend_name='aer_simulator_statevector',
backend_version='0.11.2'), metadata={'mpi_rank': 0, 'num_mpi_processes': 1, 'num_processes_per_experiments': 1,
'time_taken': 0.002216379, 'max_gpu_memory_mb': 0, 'time_taken_execute': 0.002005713, 'max_memory_mb': 65536,
'time_taken_load_qobj': 0.000200642, 'parallel_experiments': 1, 'omp_enabled': True},
time_taken=0.0025920867919921875)

Now let's get the probability distribution from the output:

counts = result.get_counts(circuit)
quasi_dists = {}
for key,count in counts.items():
    quasi_dists[key] = count/1024
>>> print("counts: ", counts)
>>> print("quasi_dists: ", quasi_dists)
counts:  {'0010': 236, '0000': 276, '0011': 262, '0001': 250}
quasi_dists:  {'0010': 0.23046875, '0000': 0.26953125, '0011': 0.255859375, '0001': 0.244140625}

3.b. Updated: Use the Reference Sampler or Aer Sampler primitive

The reference Sampler lets you perform an exact or a shot-based noisy simulation based on the Statevector class in the qiskit.quantum_info module.

from qiskit.primitives import Sampler
 
sampler = Sampler()
 
result = sampler.run(circuit).result()
quasi_dists = result.quasi_dists
>>> print("result: ", result)
>>> print("quasi_dists: ", quasi_dists)
result:  SamplerResult(quasi_dists=[{0: 0.249999999999, 1: 0.249999999999,
2: 0.249999999999, 3: 0.249999999999}], metadata=[{}])
quasi_dists:  [{0: 0.249999999999, 1: 0.249999999999, 2: 0.249999999999,
3: 0.249999999999}]

If shots are specified, this primitive outputs a shot-based simulation (no longer exact):

from qiskit.primitives import Sampler
 
sampler = Sampler()
 
result = sampler.run(circuit, shots=1024).result()
quasi_dists = result.quasi_dists
>>> print("result: ", result)
>>> print("quasi_dists: ", quasi_dists)
result:  SamplerResult(quasi_dists=[{0: 0.2490234375, 1: 0.2578125,
2: 0.2431640625, 3: 0.25}], metadata=[{'shots': 1024}])
quasi_dists:  [{0: 0.2490234375, 1: 0.2578125, 2: 0.2431640625, 3: 0.25}]

You can still access the Aer simulator through its dedicated Sampler. This can be handy for performing simulations with noise models. In this example, the simulation method has been updated to match the result from 3.a.

from qiskit_aer.primitives import Sampler as AerSampler # import change!
 
sampler = AerSampler(run_options= {"method": "statevector"})
 
result = sampler.run(circuit, shots=1024).result()
quasi_dists = result.quasi_dists
>>> print("result: ", result)
>>> print("quasi_dists: ", quasi_dists)
result:  SamplerResult(quasi_dists=[{1: 0.2802734375, 2: 0.2412109375, 0: 0.2392578125,
3: 0.2392578125}], metadata=[{'shots': 1024, 'simulator_metadata':
{'parallel_state_update': 16, 'parallel_shots': 1, 'sample_measure_time': 0.000409608,
'noise': 'ideal', 'batched_shots_optimization': False, 'remapped_qubits': False,
'device': 'CPU', 'active_input_qubits': [0, 1, 2, 3], 'measure_sampling': True,
'num_clbits': 4, 'input_qubit_map': [[3, 3], [2, 2], [1, 1], [0, 0]], 'num_qubits': 4,
'method': 'statevector', 'fusion': {'applied': False, 'max_fused_qubits': 5,
'threshold': 14, 'enabled': True}}}])
quasi_dists:  [{1: 0.2802734375, 2: 0.2412109375, 0: 0.2392578125, 3: 0.2392578125}]
>>> # Convert the output to bit strings
>>> binary_quasi_dist = quasi_dists[0].binary_probabilities()
>>> print("binary_quasi_dist: ", binary_quasi_dist)
binary_quasi_dist:  {'0001': 0.2802734375, '0010': 0.2412109375, '0000': 0.2392578125, '0011': 0.2392578125}

For more information, see Noisy simulators in Qiskit Runtime.

Was this page helpful?