Repeat until success
Usage estimate: under 1 minute on IBM Canberra (NOTE: This is an estimate only. Your runtime may vary.)
Background
This tutorial demonstrates how certain IBM Quantum™ systems (those that support dynamic circuits) use mid-circuit measurements to produce a circuit that repeatedly attempts its setup until a syndrome measurement reveals that it has been successful.
Build an abstract circuit that uses the non-parameterized gate set to construct a heralded gate on a target qubit, where satisfies . Each iteration of the circuit has a finite chance of success, but because successes are heralded, dynamic circuit capabilities are used to repeat the setup until it succeeds.
Requirements
Before starting this tutorial, ensure that you have the following installed:
- Qiskit SDK v1.0 or later, with visualization support (
pip install 'qiskit[visualization]'
) - Qiskit Runtime 0.22 or later (
pip install qiskit-ibm-runtime
)
Setup
from collections import defaultdict
# Qiskit imports
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
# Qiskit Runtime
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_ibm_runtime import QiskitRuntimeService
Step 1: Map classical inputs to a quantum problem
A single trial of the probabilistic gate has the following form:
controls = QuantumRegister(2, name="control")
target = QuantumRegister(1, name="target")
mid_measure = ClassicalRegister(2, name="mid")
final_measure = ClassicalRegister(1, name="final")
circuit = QuantumCircuit(controls, target, mid_measure, final_measure)
def trial(circuit, target, controls, measures):
"""Probabilistically perform an Rx gate for an
angle that is an irrational multiple of pi."""
circuit.h(target)
circuit.h(controls)
circuit.ccx(*controls, target)
circuit.s(target)
circuit.ccx(*controls, target)
circuit.h(controls)
circuit.h(target)
circuit.measure(controls, measures)
trial(circuit, target, controls, mid_measure)
circuit.draw(output="mpl", style="iqp", cregbundle=False)
Output:
If both control bit measurements return , the gate with is applied. If any of the measurements are , gate is applied, which is a failure. This is the heralding; you can tell from the measurement whether the correct gate was applied, without disturbing the coherence of the target qubit. Without fully reproducing the mathematics, the success probability of this gate is:
If there is a failure, reset the "dirty" state and start again. Because you know what is applied in the case of a failure, you can use this knowledge to perform the reset efficiently without using a general hardware reset. For the two auxiliary qubits, this is an gate conditioned on its respective measurement being . IBM® hardware has a special fast path for when qubits are conditioned on the result of their own measurement, so this is more efficient than most control flows.
def reset_controls(circuit, controls, measures):
"""Reset the controlling qubits if they are in |1>."""
with circuit.if_test((measures[0], True)):
circuit.x(controls[0])
with circuit.if_test((measures[1], True)):
circuit.x(controls[1])
Qiskit cannot directly represent an inequality condition, which is required for this exercise. You only need to repeat if the mid-circuit measurement result was not the bitstring "00"
. Instead, you can use an if
statement with the condition mid_measure == "00"
, pass an empty block, and then use the else
branch to perform the necessary logic.
max_trials = 2
# Manually add the rest of the trials. In the future, you can
# use a dynamic `while` loop to do this, but for now,
# statically add each loop iteration with a manual condition check
# on each one. This involves more classical synchronizations than
# the while loop, but will suffice for now.
for _ in range(max_trials - 1):
reset_controls(circuit, controls, mid_measure)
with circuit.if_test((mid_measure, 0b00)) as else_:
# This is the success path, but Qiskit can't directly
# represent a negative condition yet, so we have an
# empty `true` block in order to use the `else` branch.
pass
with else_:
# First reset the target.
circuit.x(target)
# Then repeat the trial.
trial(circuit, target, controls, mid_measure)
# Measure the control qubits again to ensure you
# get their final results; this is a hardware limitation.
circuit.measure(controls, mid_measure)
# Finally, measure the target, to check that you're
# getting the desired rotation.
circuit.measure(target, final_measure)
circuit.draw(output="mpl", style="iqp", cregbundle=False)
Output:
Step 2: Optimize problem for quantum hardware execution
To reduce the total job execution time, V2 primitives only accept circuits and observables that conform to the instructions and connectivity supported by the target system (referred to as instruction set architecture (ISA) circuits and observables).
Convert to ISA input
# To run on hardware, select the backend with the fewest number of jobs in the queue
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
print(f">>> Connected to {backend.name} backend.")
Output:
>>> Connected to ibm_cairo backend.
# Circuits must obey the ISA of a particular backend.
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_circuit = pm.run(circuit)
isa_circuit.draw(
output="mpl", style="iqp", idle_wires=False, cregbundle=False
)
Output:
Step 3: Execute using Qiskit primitives
sampler = Sampler(backend)
job = sampler.run([isa_circuit])
job.job_id()
Output:
'cs29tes965y000852sb0'
hardware_counts = job.result()[0]
data_mid = job.result()[0].data.mid
data_final = job.result()[0].data.final
print(f">>> Hardware counts for mid: {data_mid.get_counts()}")
print(f">>> Hardware counts for final: {data_final.get_counts()}")
Output:
>>> Hardware counts for mid: {'10': 480, '00': 2852, '11': 313, '01': 451}
>>> Hardware counts for final: {'1': 1590, '0': 2506}
merged_data = defaultdict(int)
for bs1, bs2 in zip(data_mid.get_bitstrings(), data_final.get_bitstrings()):
merged_data[f"{bs1}-{bs2}"] += 1
print(f">>> Hardware merged data: {dict(merged_data)}")
Output:
>>> Hardware merged data: {'10-1': 224, '00-1': 971, '10-0': 256, '11-1': 198, '00-0': 1881, '11-0': 115, '01-0': 254, '01-1': 197}
Step 4: Post-process and return result in desired classical format
A successful result is one in which the measurements on the two controls end in the 00
state. You can filter those and count how many successes occurred. This is a type of post-selection. In the complete repeat-until-success circuit with a dynamic while
loop, this would not be necessary, as you would be guaranteed a success. However, in the interim, you can use the probabilities to examine the quality of the output and verify that the probabilities are as expected.
def marginalize_successes(counts):
"""Split the full output `counts` dictionary
into two separate dictionaries, marginalizing
the results to leave only the target qubit's
state."""
successes = defaultdict(int)
failures = defaultdict(int)
for key, value in counts.items():
if key.startswith("00"):
successes[key[-1]] += value
else:
failures[key[-1]] += value
return successes, failures
hw_successes, hw_failures = marginalize_successes(merged_data)
expected_successes = 1 - (1 - 5 / 8) ** max_trials
actual_successes = sum(hw_successes.values()) / sum(merged_data.values())
print(
f"Expected success rate {expected_successes:5.3f}. Actual rate {actual_successes:5.3f}."
)
Output:
Expected success rate 0.859. Actual rate 0.696.
We can also test the post-selected output state:
actual_probability = hw_successes["1"] / sum(hw_successes.values())
print(
f"Expected |1> probability: 0.2. Actual probability {actual_probability:5.3f}."
)
Output:
Expected |1> probability: 0.2. Actual probability 0.340.
Tutorial survey
Please take one minute to provide feedback on this tutorial. Your insights will help us improve our content offerings and user experience.
© IBM Corp. 2024