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.
Backend.run() model: In this model, you accessed real backends and remote simulators using the qiskit-ibmq-provider
module (now migrated to qiskit-ibm-provider
). To run local simulations, you could import a specific backend from qiskit-aer
. All of them followed the backend.run()
interface.
Code example for qiskit-ibmq-provider
& backend.run()
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()
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 backends 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.
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:
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.
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:
- Setting execution options topic
- Primitive execution options API reference
- How to run a session topic
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.
Backend.run() model: In this model, you used the qiskit-ibmq-provider
(now migrated to qiskit-ibm-provider
) module to access real backends and remote simulators. To run local simulations, you could import a specific backend 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 backends 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:
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:
- Run circuits
- Get counts from the result object
- Use the counts and shots to calculate the probability distribution
First, we run the circuit in a cloud simulator and output the result object:
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.
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}]
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:
- Error mitigation tutorial (opens in a new tab)
- Setting execution options topic
- How to run a session topic
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.