Skip to main content

Sampling backends

In the previous section (Sampling estimation), we described how to estimate expectation value of operators using sampling measurements on a quantum circuit simulator. Since QURI Parts is designed to be platform independent, you can execute almost the same code on a real quantum computer.

In QURI Parts, we use SamplingBackend objects to submit jobs to the real devices. This tutorial is for explaining some common features shared between devices from different providers, e.g. Qiskit and Braket. For provider specific features, please refer to the corresponding tutorial pages.

Prerequisite

This section requires topics described in the previous section (Sampling estimation), so you need to read it before this section. QURI Parts is capable of supporting backends provided by all providers. You may install any one depending on your preference. In this tutorial, we will be using backends provided by Amazon Braket as well as IBM Quantum as examples. Then, we will explain how to install and use both backends in their corresponding tutorials.

Sampling Backend and Sampler

In order to use a real device, you need to create a SamplingBackend object and then a Sampler using the backend. The SamplingBackend provides a unified interface for handling various backend devices, computation jobs for the devices and results of the jobs.

You can create a sampler with a sampling backend. First, you can create sampling backends with the backend provider you prefer. For example:

from quri_parts.qiskit.backend import QiskitSamplingBackend
from quri_parts.braket.backend import BraketSamplingBackend
from qiskit_aer import AerSimulator
from braket.devices import LocalSimulator


# sampling_backend = QiskitSamplingBackend(backend=AerSimulator())
sampling_backend = BraketSamplingBackend(device=LocalSimulator())

Using the sampling backend

It is possible to use these backends directly, though it is usually unnecessary as we will see below. The SamplingBackend has sample() method, which returns a SamplingJob object, and you can extract a result of the sampling job:

from math import pi
from quri_parts.circuit import QuantumCircuit

circuit = QuantumCircuit(4)
circuit.add_X_gate(0)
circuit.add_H_gate(1)
circuit.add_Y_gate(2)
circuit.add_CNOT_gate(1, 2)
circuit.add_RX_gate(3, pi/4)

sampling_job = sampling_backend.sample(circuit, n_shots=1000)
sampling_result = sampling_job.result()

print(sampling_result.counts)
#output
Counter({3: 421, 5: 416, 13: 88, 11: 75})

Create samplers with backend

Instead of using the backends directly, you can create a Sampler from it with the create_sampler_from_sampling_backend function:

from quri_parts.core.sampling import create_sampler_from_sampling_backend

sampler = create_sampler_from_sampling_backend(
sampling_backend # you may replace it with other sampling backends you prefer.
)

The sampler can then be used as usual:

sampling_count = sampler(circuit, 1000)
print(sampling_count)
#output
{11: 77, 13: 83, 3: 409, 5: 431}

Sampling Estimate

Here we describe how to perform sampling estimate with the same code used in the previous Sampling estimation tutorials. To create a SamplingEstimator, one needs to specify the concurrent sampler.

from quri_parts.core.sampling import create_concurrent_sampler_from_sampling_backend
from quri_parts.qiskit.backend import QiskitSamplingBackend
from quri_parts.braket.backend import BraketSamplingBackend
from qiskit_aer import AerSimulator
from braket.devices import LocalSimulator


# sampling_backend = QiskitSamplingBackend(backend=AerSimulator())
# concurrent_sampler = create_concurrent_sampler_from_sampling_backend(qiskit_sampling_backend)

sampling_backend = BraketSamplingBackend(device=LocalSimulator())
concurrent_sampler = create_concurrent_sampler_from_sampling_backend(sampling_backend)

Then you can use either concurrent sampler to perform sampling estimation.

from quri_parts.core.estimator.sampling import create_sampling_estimator
from quri_parts.core.state import quantum_state
from quri_parts.core.operator import Operator, pauli_label, PAULI_IDENTITY
from quri_parts.core.measurement import bitwise_commuting_pauli_measurement
from quri_parts.core.sampling.shots_allocator import create_weighted_random_shots_allocator

estimator = create_sampling_estimator(
5000,
concurrent_sampler,
bitwise_commuting_pauli_measurement,
create_weighted_random_shots_allocator(seed=777),
)

initial_state = quantum_state(4, bits=0b0101)
op = Operator({
pauli_label("Z0"): 0.25,
pauli_label("Z1 Z2"): 2.0,
pauli_label("X1 X2"): 0.5 + 0.25j,
pauli_label("Z1 Y3"): 1.0j,
pauli_label("Z2 Y3"): 1.5 + 0.5j,
pauli_label("X1 Y3"): 2.0j,
PAULI_IDENTITY: 3.0,
})

estimate = estimator(op, initial_state)

print(f'Estimated value: {estimate.value}')
print(f'Estimated error: {estimate.error}')
#output
Estimated value: (0.6730848581603831+0.042719018177966035j)
Estimated error: 0.07069176439271635

Common Options and Features of Sampling Backends

Shot Distribution

Usually the real device does not allow arbitrary large number of shots to be executed. However, QURI Parts' SamplingBackend.sample allows submitting shot count greater than the max shot count supported by the device. This is because SamplingBackend performs shot distribution that group n_shots into batches of SamplingJobs where the shot count of each batch is equal to or smaller than the max shot supported by the device.

On the other hand, the device may restrict the minimal number of shots to be greater than some minimal shot number. In this case, if a shot count in a batch is smaller than the min shot supported by the device, you may use the enable_shots_roundup argument in the backend to decide what to do with the remaining batch. If it is set to True, the backend will round the shot count of the remaining batch to the specified min shot. Otherwise, the backend will ignore the batch.

Qubit Mapping

When you use a real quantum device, you may want to use specific device qubits selected by inspecting calibration data of the device. A SamplingBackend supports such usage with qubit_mapping argument. With qubit_mapping you can specify an arbitrary one-to-one mapping between qubit indices in the input circuit and device qubits. For example, if you want to map qubits in the circuit into device qubits as 0 → 3, 1 → 2, 2 → 0 and 3 → 1, you can specify the mapping as follows:

qubit_mapping = {0: 3, 1: 2, 2: 0, 3: 1}

and pass it into the SamplingBackend. The result would look similar to one with no qubit mapping, since the measurement result from the device is mapped backward so that it is interpreted in terms of the original qubit indices.

Circuit transpilation before execution

When the SamplingBackend receives an input circuit, it performs circuit transpilation before sending the circuit to its backend since each device can have a different supported gate set. The transpilation performed by default depends on the backend.