Sampling backends#

In the previous section (Sampling simulation), 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 simulation), 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:

[3]:
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:

[7]:
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)
{11: 74, 13: 68, 5: 431, 3: 427}
Counter({3: 442, 5: 426, 11: 82, 13: 50})

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:

[8]:
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:

[4]:
sampling_count = sampler(circuit, 1000)
print(sampling_count)
{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 Simulation tutorials. To create a SamplingEstimator, one needs to specify

[10]:
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 put either concurrent sampler into the code below to perform sampling estimation.

[13]:
from quri_parts.core.operator import Operator, pauli_label, PAULI_IDENTITY
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,
})

from quri_parts.core.state import ComputationalBasisState
initial_state = ComputationalBasisState(4, bits=0b0101)

from quri_parts.core.measurement import bitwise_commuting_pauli_measurement
from quri_parts.core.sampling.shots_allocator import create_weighted_random_shots_allocator
allocator = create_weighted_random_shots_allocator(seed=777)

from quri_parts.core.estimator.sampling import sampling_estimate
estimate = sampling_estimate(
    op,            # Operator to estimate
    initial_state, # Initial (circuit) state
    5000,          # Total sampling shots
    concurrent_sampler, # ConcurrentSampler should be created by a sampling backend
    bitwise_commuting_pauli_measurement, # Factory function for CommutablePauliSetMeasurement
    allocator,     # PauliSamplingShotsAllocator
)
print(f"Estimated expectation value: {estimate.value}")
print(f"Standard error of estimation: {estimate.error}")
Estimated expectation value: (0.7157030578410907+0.07082474359115679j)
Standard error of estimation: 0.07071047108767443

You can also create a QuantumEstimator that performs sampling estimation:

[12]:
from quri_parts.core.estimator.sampling import create_sampling_estimator
from quri_parts.core.state import ComputationalBasisState

estimator = create_sampling_estimator(
    5000,               # Total sampling shots
    concurrent_sampler, # ConcurrentSampler should be created by a sampling backend
    bitwise_commuting_pauli_measurement,  # Factory function for CommutablePauliSetMeasurement
    allocator,          # PauliSamplingShotsAllocator
)

initial_state = ComputationalBasisState(4, bits=0b0101)

estimate = estimator(op, initial_state)

print(f'Estimated value: {estimate.value}')
print(f'Estimated error: {estimate.error}')
Estimated value: (0.7012656804438002+0.011764716247822493j)
Estimated error: 0.07033330217651333

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

[16]:
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.