6. Noise-accurate simulation of the Bernstein–Vazirani algorithm¶
The Bernstein–Vazirani (BV) algorithm [Bernstein, Vazirani, 1993] finds a secret bitstring \(s\in\{0, 1\}^n\), knowing the function \(f(x)=x\cdot s\), where \(\cdot\) denotes the bit-wise dot-product and \(x\in\{0, 1\}^n\).
Assuming we have a black-box oracle computing \(f\), the algorithm finds \(s\) in one call to the oracle, against \(n\) calls for a classical algorithm.
In the following, we will estimate the success probability of the BV algorithms in the presence of noise.
6.1. Transpilation of the algorithm¶
We start by generating a qiskit circuit implementing the BV algorithm. We use the hidden string to implement the oracle and neglect the consecutive Hadamard gates (\(H^2=\mathbb{1}\)).
This is clearly a toy example, as we use the hidden string to build the circuit. However, this allows us to measure the impact of noise on the result.
%load_ext autoreload
%autoreload 2
import numpy as np
from qiskit.circuit import QuantumCircuit
from spin_pulse import ExperimentalEnvironment, HardwareSpecs, PulseCircuit, Shape
from spin_pulse.environment.noise import NoiseType
def gen_bv_circ(hidden_string: str | None = None) -> QuantumCircuit:
"""
Generate the BV circuit using the hidden bitstring to create the oracle.
We omit the Hadamard gates when there are two consecutive ones on the same qubit.
"""
num_qubits = len(hidden_string) + 1
circuit = QuantumCircuit(num_qubits, 0)
# Initialize the ancilla in |1>
circuit.x(-1)
for i in range(num_qubits - 2, -1, -1):
if hidden_string[num_qubits - 2 - i] == "1":
circuit.h(i)
circuit.cz(-1, i)
circuit.h(i)
return circuit
# Pick your string
hidden_string = "011"
num_qubits = len(hidden_string) + 1
# Ideal circuit
qc = gen_bv_circ(hidden_string)
qc.draw("mpl")
We first define our hardware specifications and use them to transpile the qiskit circuit according to our needs. We can apply Dynamical Decoupling (DD) on idle qubits during single-qubit unitaries by specifying the DD scheme in HardwareSpecs.
We then define our PulseCircuits to which we attach the transpiled circuit and the hardware specifications. For the noisy case, we also attach an experimental environment that specifies the noise. Note that we deliberately chose a high level of noise to exhibit the effect of DD even on small circuits.
from spin_pulse import DynamicalDecoupling
def get_hdw_specs(DD):
return HardwareSpecs(
num_qubits=num_qubits,
B_field=0.1,
delta=0.1,
J_coupling=0.01,
rotation_shape=Shape.GAUSSIAN,
ramp_duration=5,
coeff_duration=5,
dynamical_decoupling=DD,
optim=1,
)
hardware_specs = get_hdw_specs(None)
hardware_specs_DD = get_hdw_specs(DynamicalDecoupling.FULL_DRIVE)
# ISA circuit
isa_qc_bv = hardware_specs.gate_transpile(qc)
# Pulse circuit without noise
pulse_qc_bv_noiseless = PulseCircuit.from_circuit(isa_qc_bv, hardware_specs)
6.2. Noise simulations¶
Let’s define the duration of our ExperimentalEnvironment such that it is \(n_\text{samples}\) times longer than the circuit duration. This will provide us \(n_\text{samples}\) noisy circuit instances to work with.
n_samples = 500
duration = pulse_qc_bv_noiseless.duration * n_samples
exp_env = ExperimentalEnvironment(
hardware_specs=hardware_specs,
noise_type=NoiseType.PINK,
T2S=500,
duration=duration,
segment_duration=duration,
only_idle=False,
)
# Pulse circuit with noise
pulse_qc_bv_noisy = PulseCircuit.from_circuit(
isa_qc_bv, hardware_specs, exp_env=exp_env
)
# ISA circuit with DD
isa_qc_bv_DD = hardware_specs_DD.gate_transpile(qc)
# Pulse circuit with noise and DD
pulse_qc_bv_noisy_DD = PulseCircuit.from_circuit(
isa_qc_bv_DD, hardware_specs_DD, exp_env=exp_env
)
There exist two equivalent solutions (in the limit of an infinite number of samples) to compute our success probability.
We can simulate an experiment that performs a bitstring measurement on each realization of the noisy circuit. Averaging over multiple realizations gives the probability distribution \(P(s)=\bra{s}\rho_f\ket{s}\), where \(\rho_f\) is the density matrix of the system after running the algorithm. In particular, for \(s=s_0\) where \(s_0\) denotes the hidden string, we obtain the success probability of the algorithm.
Alternatively, noting that
where \(u\) are the matrices representing the noisy realizations of the quantum circuit, we can recast the success probability as
and estimate it numerically by calculating the matrices \(u\) explicitely, and averaging the overlap \(\left|\bra{s_0}u\ket{0^{\otimes N}}\right|^2\) over many noisy circuit realizations.
6.2.1. Estimating the success probability by simulating an experiment¶
The first option is implemented by the method run_experiment of the PulseCircuit class. It executes a single-shot measurement on each noisy instance of the circuit that can be created using the given ExperimentalEnvironment.
Simulations are performed using Matrix Product State (MPS) implementation by qiskit_aer.
print("Hidden string ", hidden_string)
print("Expected output in the counts ", "1" + hidden_string)
counts_pulse_qc_bv_noisy = pulse_qc_bv_noisy.run_experiment(exp_env)
print(
"P_sucess_noise=",
counts_pulse_qc_bv_noisy["1" + hidden_string]
/ sum(counts_pulse_qc_bv_noisy.values()),
)
counts_pulse_qc_bv_noisy_DD = pulse_qc_bv_noisy_DD.run_experiment(exp_env)
print(
"P_sucess_noise_DD=",
counts_pulse_qc_bv_noisy_DD["1" + hidden_string]
/ sum(counts_pulse_qc_bv_noisy_DD.values()),
)
Hidden string 011
Expected output in the counts 1011
P_sucess_noise= 0.836
P_sucess_noise_DD= 0.884
We can visualize the results with plot_histogram provided by qiskit. We observe that DD brings a significant improvement.
from qiskit.visualization import plot_histogram
def normalize_counts(counts):
tot_sum = sum(counts.values())
return {k: v / tot_sum for k, v in counts.items()}
normalized_counts_pulse_qc_bv_noisy = normalize_counts(counts_pulse_qc_bv_noisy)
normalized_counts_pulse_qc_bv_noisy_DD = normalize_counts(counts_pulse_qc_bv_noisy_DD)
plot_histogram(
[normalized_counts_pulse_qc_bv_noisy, normalized_counts_pulse_qc_bv_noisy_DD],
legend=["without DD", "with DD"],
title=f"Expected string: 1{hidden_string}",
figsize=(10, 4),
)