English
Languages
English
Japanese
Spanish

Circuit sampling in 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 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.

Code example with qiskit-ibmq-provider & backend.run()
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)

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(circuits)

Primitives model: Access real backends and remote simulators through the qiskit-ibm-runtime primitives (Sampler and Estimator). 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.

Code example for Runtime Sampler
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
sampler = Sampler(backend=backend)

# Run Quasi-Probability calculation
result = sampler.run(circuits).result()

Code example for Aer Sampler
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. [New] 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(backend=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. [New] 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 information, see Noisy simulators in Qiskit Runtime.