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 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 SamplingJob
s 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.