Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Repeat until success notebook #1192

Merged
merged 11 commits into from
Apr 22, 2024
173 changes: 126 additions & 47 deletions tutorials/repeat-until-success/notebook.ipynb
Eric-Arellano marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
"\n",
"## Background\n",
"\n",
"This tutorial demonstrates the current capabilities of IBM dynamic-circuit backends to use mid-circuit measurements to produce a circuit that repeatedly attempts its setup until a syndrome measurement reveals that it has been successful.\n",
"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.\n",
"\n",
"We will build an abstract circuit that uses the non-parametrized gate set $\\{H,\\,X,\\,S,\\,\\text{Toffoli}\\}$ to construct a heralded $R_X(\\theta)$ gate on a target qubit, where $\\theta$ satisfies $\\cos\\theta = \\frac35$. Each iteration of the circuit only has a finite chance of success, but successes are heralded, so we will use our dynamic-circuit capabilities to repeat the setup until it succeeds."
"You will build an abstract circuit that uses the non-parametrized gate set $\\{H,\\,X,\\,S,\\,\\text{Toffoli}\\}$ to construct a heralded $R_X(\\theta)$ gate on a target qubit, where $\\theta$ satisfies $\\cos\\theta = \\frac35$. 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."
]
},
{
Expand All @@ -32,15 +32,16 @@
"metadata": {},
"outputs": [],
"source": [
"import warnings\n",
"warnings.filterwarnings('ignore')\n",
"\n",
"# Qiskit imports\n",
"from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister\n",
"from qiskit import transpile\n",
"\n",
"# Qiskit Aer (Simulator)\n",
"from qiskit_aer import AerSimulator\n",
"\n",
"# Qiskit IBM Provider\n",
"from qiskit_ibm_provider import IBMProvider, least_busy"
"# Qiskit Runtime\n",
"from qiskit_ibm_runtime import SamplerV2 as Sampler\n",
"from qiskit_ibm_runtime import QiskitRuntimeService"
]
},
{
Expand Down Expand Up @@ -99,6 +100,17 @@
"circuit = QuantumCircuit(controls, target, mid_measure, final_measure)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2c4064b5",
"metadata": {},
"outputs": [],
"source": [
"# Install pylatexenc if you don't already have it.\n",
"# pip install pylatexenc"
]
},
{
"cell_type": "code",
"execution_count": 4,
Expand Down Expand Up @@ -140,7 +152,7 @@
"id": "87e96cf7-68b9-4822-a559-85cfa01eef90",
"metadata": {},
"source": [
"If _both_ measurements of the control bits return $0$, the applied gate is $R_X(\\theta)$ with $\\cos\\theta = \\frac35$. If any of the measurements are $1$, then the applied gate is simply $X$, which is a failure. This is our heralding; we can tell from the measurement whether we applied the correct gate, without disturbing the coherence of the target qubit. Without fully reproducing the mathematics, the success probability of this gate is:\n",
"If _both_ control bit measurements return $0$, the gate $R_X(\\theta)$ with $\\cos\\theta = \\frac35$ is applied. If any of the measurements are $1$, gate $X$ 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:\n",
"$$\n",
"P_{\\text{success}} = \\frac{\n",
"{|3 + i|}^2\n",
Expand All @@ -151,7 +163,7 @@
"= \\frac58\n",
"$$\n",
"\n",
"If there is a failure, we should reset the dirty state, and start again. Since we know what is applied in the case of a failure, we can use this knowledge to perform the reset efficiently, without using a general hardware reset. For the two auxiliary qubits, this is just an $X$ gate conditioned on its respective measurement being $1$. 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 flow."
"If there is a failure, you should 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 $X$ gate conditioned on its respective measurement being $1$. 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."
]
},
{
Expand All @@ -175,7 +187,7 @@
"id": "4f6d46fe-5eb7-4e57-83c0-a8c13ab4d67a",
"metadata": {},
"source": [
"Qiskit cannot directly represent an _inequality_ condition, which is what we require. We only need to do the repeat if the mid-circuit measurement result was not the bitstring `\"00\"`. Instead, we can create an `if` statement with the condition `mid_measure == \"00\"`, pass an empty block, and then use the `else` branch to perform the logic we want."
"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 create an `if` statement with the condition `mid_measure == \"00\"`, pass an empty block, and then use the `else` branch to perform the necessary logic."
]
},
{
Expand All @@ -199,8 +211,8 @@
"source": [
"max_trials = 2\n",
"\n",
"# Manually add the rest of the trials. In the future, we will be\n",
"# able to use a dynamic `while` loop to do this, but for now, we\n",
"# Manually add the rest of the trials. In the future, you can\n",
"# use a dynamic `while` loop to do this, but for now,\n",
"# statically add each loop iteration with a manual condition check\n",
"# on each one. This involves more classical synchronizations than\n",
"# the while loop, but will suffice for now.\n",
Expand All @@ -217,12 +229,12 @@
" # Then repeat the trial.\n",
" trial(circuit, target, controls, mid_measure)\n",
"\n",
"# We need to measure the control qubits again to ensure we\n",
"# Measure the control qubits again to ensure you\n",
"# get their final results; this is a hardware limitation.\n",
"circuit.measure(controls, mid_measure)\n",
"\n",
"# Finally, let's measure our target, to check that we're\n",
"# getting the rotation we desired.\n",
"# Finally, measure the target, to check that you're\n",
"# getting the desired rotation.\n",
"circuit.measure(target, final_measure)\n",
"\n",
"circuit.draw(output=\"mpl\", style='iqp', cregbundle=False)"
Expand All @@ -234,16 +246,7 @@
"id": "e563959a-1543-48b2-ab90-d5dfe8a6ddbc",
"metadata": {},
"source": [
"## Optimize the circuits"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "31be5d78-8115-4a20-9a22-7d99e07ab4dc",
"metadata": {},
"source": [
"To prepare the circuit to run on real hardware, we use Qiskit's `transpile` function. By not specifying an `optimization_level` argument to `transpile`, we implicitly use level 1, which has light quantum optimization. For now, levels 2 and 3 do not support the dynamic circuits path as some of the optimizations used at those levels are not fully control-flow-aware."
"## Run in local testing mode"
]
},
{
Expand All @@ -265,16 +268,18 @@
}
],
"source": [
"built = transpile(circuit, backend)\n",
"built.draw(output=\"mpl\", style='iqp', idle_wires=False, cregbundle=False)"
"from qiskit_ibm_runtime.fake_provider import FakeSherbrooke\n",
"\n",
"# Run the sampler job locally using FakeSherbrooke\n",
"backend = FakeSherbrooke()"
]
},
{
"cell_type": "markdown",
"id": "0029abf2",
"metadata": {},
"source": [
"## Run on a simulator"
"### Convert to ISA input"
]
},
{
Expand Down Expand Up @@ -302,10 +307,28 @@
}
],
"source": [
"sim = AerSimulator().from_backend(backend)\n",
"simulator_result = sim.run(built).result()\n",
"simulator_counts = simulator_result.get_counts()\n",
"simulator_counts"
"# Circuits must obey the Instruction Set Architecture (ISA) of a particular backend.\n",
"from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
"\n",
"pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n",
"isa_circuit = pm.run(circuit)\n",
"\n",
"built.draw(output=\"mpl\", style='iqp', idle_wires=False, cregbundle=False)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7287136b",
"metadata": {},
"outputs": [],
"source": [
"sampler = Sampler(backend)\n",
"job = sampler.run([isa_circuit])\n",
"\n",
"simulator_counts = job.result()[0]\n",
"print(f\">>> Simulator counts for mid: {simulator_counts.data.mid.get_counts()}\")\n",
"print(f\">>> Simulator counts for final: {simulator_counts.data.final.get_counts()}\")"
]
},
{
Expand Down Expand Up @@ -334,7 +357,44 @@
}
],
"source": [
"job = backend.run(built, dynamic=True)\n",
"service = QiskitRuntimeService()\n",
"backend = service.least_busy(operational=True, simulator=False)\n",
"print(f\">>> Connected to {backend.name} backend.\")"
]
},
{
"cell_type": "markdown",
"id": "47d3c339",
"metadata": {},
"source": [
"### Convert to ISA input"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "15956b9b",
"metadata": {},
"outputs": [],
"source": [
"# Circuits must obey the ISA of a particular backend.\n",
"from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
"\n",
"pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n",
"isa_circuit = pm.run(circuit)\n",
"\n",
"built.draw(output=\"mpl\", style='iqp', idle_wires=False, cregbundle=False)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5217697f",
"metadata": {},
"outputs": [],
"source": [
"sampler = Sampler(backend)\n",
"job = sampler.run([isa_circuit])\n",
"job.job_id()"
]
},
Expand Down Expand Up @@ -365,8 +425,29 @@
}
],
"source": [
"hardware_counts = job.result().get_counts()\n",
"hardware_counts"
"hardware_counts = job.result()[0]\n",
"\n",
"data_mid = job.result()[0].data.mid\n",
"data_final = job.result()[0].data.final\n",
"\n",
"print(f\">>> Hardware counts for mid: {data_mid.get_counts()}\")\n",
"print(f\">>> Hardware counts for final: {data_final.get_counts()}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0529dc0b",
"metadata": {},
"outputs": [],
"source": [
"from collections import defaultdict\n",
"\n",
"merged_data = defaultdict(int)\n",
"for bs1, bs2 in zip(data_mid.get_bitstrings(), data_final.get_bitstrings()):\n",
" merged_data[f\"{bs1}-{bs2}\"] += 1\n",
"\n",
"print(f\">>> Hardware merged data: {dict(merged_data)}\")"
]
},
{
Expand All @@ -383,7 +464,7 @@
"id": "f63d48c1-6f79-4491-bb45-36e57595b967",
"metadata": {},
"source": [
"A successful result is one in which the measurements on the two controls end in the `00` state. Let's filter those out, and see how many successes will result. This is a type of post-selection. In the complete repeat-until-success circuit, with a dynamic `while` loop, we would not need this, as we would be guaranteed a success. However, in the interim, we can use the probabilities to examine the quality of the output, and verify that the probabilities are what we expect."
"A successful result is one in which the measurements on the two controls end in the `00` state. You can filter those out and see how many successes will result. 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."
]
},
{
Expand All @@ -393,21 +474,19 @@
"metadata": {},
"outputs": [],
"source": [
"import collections\n",
"\n",
"def marginalize_successes(counts):\n",
" \"\"\"Split the full output `counts` dictionary\n",
" into two separate dictionaries, marginalizing\n",
" the results to leave only the target qubit's\n",
" state.\"\"\"\n",
" successes = collections.defaultdict(int)\n",
" failures = collections.defaultdict(int)\n",
" successes = defaultdict(int)\n",
" failures = defaultdict(int)\n",
"\n",
" for key, value in counts.items():\n",
" if key.endswith(\"00\"):\n",
" successes[key[0]] += value\n",
" if key.startswith(\"00\"):\n",
" successes[key[-1]] += value\n",
" else:\n",
" failures[key[0]] += value\n",
" failures[key[-1]] += value\n",
"\n",
" return successes, failures"
]
Expand All @@ -427,10 +506,10 @@
}
],
"source": [
"hw_successes, hw_failures = marginalize_successes(hardware_counts)\n",
"hw_successes, hw_failures = marginalize_successes(merged_data)\n",
"\n",
"expected_successes = 1 - (1 - 5/8)**max_trials\n",
"actual_successes = sum(hw_successes.values()) / sum(hardware_counts.values())\n",
"actual_successes = sum(hw_successes.values()) / sum(merged_data.values())\n",
"print(f\"Expected success rate {expected_successes:5.3f}. Actual rate {actual_successes:5.3f}.\")"
]
},
Expand Down Expand Up @@ -480,9 +559,9 @@
}
],
"source": [
"import qiskit_ibm_provider\n",
"import qiskit_ibm_runtime\n",
"\n",
"qiskit_ibm_provider.version.get_version_info()"
"qiskit_ibm_runtime.version.get_version_info()"
]
},
{
Expand Down
Loading