{ "cells": [ { "cell_type": "markdown", "id": "cell-00", "metadata": {}, "source": [ "# How to compute custom bounds\n", "\n", "`compute_forward_bounds` and `compute_backward_bounds` are convenience wrappers. Both are built from\n", "the same three pieces, which the public API exposes:\n", "\n", "- A **norm function**, which bounds a single evolved Pauli error term\n", "- A **light cone**, which seeds what the error terms are evolved toward\n", "- `compute_bounds`, which iterates the circuit and distributes the per-term work\n", "\n", "Assembling them yourself means you can bound something the wrappers don't cover. This guide works\n", "through one case where that is worth doing: a **mirror circuit**, where the usual backward bound is\n", "needlessly loose." ] }, { "cell_type": "markdown", "id": "cell-01", "metadata": {}, "source": [ "## What the wrappers actually do\n", "\n", "The two wrappers differ in the three places listed above:\n", "\n", "| | `compute_forward_bounds` | `compute_backward_bounds` |\n", "| --- | --- | --- |\n", "| circuit | as given | `remove_measure(circuit).inverse()` |\n", "| norm function | `time_evolved_norm_forward`, against the observable | `time_evolved_norm_backward`, against the initial state |\n", "| light cone | `LightCone.initialize_from_pauli`, seeded from the observable | `LightCone.initialize_from_measurements`, seeded from every active qubit |\n", "| `backwards` | `False` | `True` |\n", "\n", "The forward bound asks *\"How much can this error change the observable I am about to measure?\"* and\n", "answers it with the spectral norm of a commutator. The backward bound asks *\"How much can this error\n", "change the state I started from?\"* and answers with a nuclear norm, taking that state to be the\n", "all-zero state on every active qubit. They are different questions, and each is the right one for\n", "errors in its own half of the circuit." ] }, { "cell_type": "markdown", "id": "cell-02", "metadata": {}, "source": [ "## The mirror circuit case\n", "\n", "A mirror circuit applies some unitary and then undoes it: $U^\\dagger U$. These appear whenever\n", "the ideal outcome has to be known exactly (generally Loschmidt echoes, randomized benchmarking, and\n", "verification experiments).\n", "\n", "Mirror circuits have a property the generic backward bound cannot exploit. Because the second half\n", "is the inverse of the first, *evolving an error backward to the initial state* traverses the same\n", "gates as *evolving it forward to the observable*. The two questions coincide, so the sharper\n", "forward-style bound is valid for the backward direction too." ] }, { "cell_type": "code", "execution_count": 1, "id": "cell-03", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "mirror circuit: 10 qubits, depth 24\n", "noisy layers: 4\n" ] } ], "source": [ "import numpy as np\n", "from qiskit import QuantumCircuit\n", "from qiskit.quantum_info import Pauli\n", "from qiskit_addon_slc.utils import generate_noise_model_paulis\n", "from samplomatic.transpiler import generate_boxing_pass_manager\n", "from samplomatic.utils import find_unique_box_instructions\n", "\n", "\n", "def trotter_ising_circuit(num_qubits, num_steps, rx_angle, rzz_angle):\n", " \"\"\"Trotterized transverse-field Ising evolution on a 1D chain.\"\"\"\n", " circuit = QuantumCircuit(num_qubits)\n", " for _ in range(num_steps):\n", " circuit.rx(rx_angle, range(num_qubits))\n", " circuit.barrier()\n", " for start in (0, 1): # even then odd bonds\n", " for i in range(start, num_qubits - 1, 2):\n", " circuit.rzz(rzz_angle, i, i + 1)\n", " circuit.barrier()\n", " return circuit\n", "\n", "\n", "num_qubits = 10\n", "# The circuit of the quickstart guide...\n", "forward_half = trotter_ising_circuit(\n", " num_qubits, num_steps=4, rx_angle=np.pi / 16, rzz_angle=-np.pi / 3\n", ")\n", "# ...followed by its inverse. The ideal result is the initial state.\n", "circuit = forward_half.compose(forward_half.inverse())\n", "\n", "observable = Pauli(\"I\" * num_qubits).compose(\"Z\", [num_qubits // 2])\n", "\n", "boxing_pass = generate_boxing_pass_manager(\n", " inject_noise_targets=\"all\",\n", " inject_noise_strategy=\"individual_modification\",\n", " inject_noise_site=\"after\",\n", " twirling_strategy=\"active\",\n", " remove_barriers=\"never\",\n", ")\n", "boxed_circuit = boxing_pass.run(circuit)\n", "noise_model_paulis = generate_noise_model_paulis(find_unique_box_instructions(boxed_circuit))\n", "\n", "print(f\"mirror circuit: {num_qubits} qubits, depth {circuit.depth()}\")\n", "print(f\"noisy layers: {len(noise_model_paulis)}\")" ] }, { "cell_type": "markdown", "id": "cell-04", "metadata": {}, "source": [ "## Assemble the custom bound\n", "\n", "The recipe keeps what `compute_backward_bounds` does with the circuit (strip the measurements,\n", "invert it, and iterate it with `backwards=True`) and substitutes the other two pieces: the forward\n", "norm function, and a light cone seeded from the observable with `LightCone.initialize_from_pauli`,\n", "exactly as `compute_forward_bounds` seeds its own. Each substitution is one line." ] }, { "cell_type": "code", "execution_count": 2, "id": "cell-05", "metadata": {}, "outputs": [], "source": [ "from functools import partial\n", "\n", "from qiskit_addon_slc.bounds import LightCone, compute_bounds, time_evolved_norm_forward\n", "from qiskit_addon_slc.utils import remove_measure\n", "\n", "\n", "def compute_mirror_backward_bounds(\n", " circuit, noise_model_paulis, /, observable, *, evolution_max_terms=1_000_000, **kwargs\n", "):\n", " \"\"\"Bounds the backward direction of a mirror circuit with a forward-style bound.\n", "\n", " Valid only for circuits whose second half inverts the first: the backward evolution then\n", " traverses the same gates as the forward one, and reproduces the forward bounds.\n", " \"\"\"\n", " # As `compute_backward_bounds` does: strip measurements and invert, so that walking the circuit\n", " # backwards from its end becomes walking the inverted circuit forwards.\n", " circuit = remove_measure(circuit).inverse()\n", "\n", " # The substitution: the forward norm function, bounding against the observable rather than\n", " # against the initial state.\n", " norm_fn = partial(\n", " time_evolved_norm_forward,\n", " observable=observable,\n", " evolution_max_terms=evolution_max_terms,\n", " )\n", "\n", " # Seed the light cone from the observable, not from every active qubit.\n", " light_cone = LightCone.initialize_from_pauli(circuit, observable)\n", "\n", " return compute_bounds(\n", " circuit, noise_model_paulis, light_cone, norm_fn, backwards=True, **kwargs\n", " )" ] }, { "cell_type": "markdown", "id": "cell-06", "metadata": {}, "source": [ "## How much it gains\n", "\n", "Comparing against the stock backward bound on the same circuit, summarized the same way as in the\n", "[configuration guide](configuration_options.ipynb): the mean bound and its standard deviation,\n", "followed by the share of terms bounded by 0, with a bound computed in between, and left at the\n", "trivial 2.0." ] }, { "cell_type": "code", "execution_count": 3, "id": "cell-07", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "forward mean 0.4323 ± 0.679 │ at 0: 38% computed: 61% at 2.0: 1%\n", "backward (stock) mean 1.7218 ± 0.592 │ at 0: 0% computed: 95% at 2.0: 5%\n", "backward (custom) mean 0.4323 ± 0.679 │ at 0: 38% computed: 61% at 2.0: 1%\n" ] } ], "source": [ "from qiskit_addon_slc.bounds import compute_backward_bounds, compute_forward_bounds\n", "\n", "forward_bounds = compute_forward_bounds(boxed_circuit, noise_model_paulis, observable)\n", "stock_backward = compute_backward_bounds(boxed_circuit, noise_model_paulis)\n", "custom_backward = compute_mirror_backward_bounds(boxed_circuit, noise_model_paulis, observable)\n", "\n", "\n", "def summarize(label, bounds):\n", " \"\"\"Prints a one-line summary of a set of bounds.\"\"\"\n", " rates = np.concatenate([bound.rates for bound in bounds.values()])\n", " # The bounds are floating-point results, so a term counts as bounded by 0, or as left at the\n", " # trivial 2.0, when it is within rounding error of that value.\n", " exact = np.isclose(rates, 0.0, rtol=0.0, atol=1e-12)\n", " trivial = np.isclose(rates, 2.0, rtol=0.0, atol=1e-12)\n", " computed = ~exact & ~trivial\n", " print(\n", " f\"{label:18} mean {rates.mean():.4f} ± {rates.std():.3f} │ \"\n", " f\"at 0: {exact.mean():4.0%} computed: {computed.mean():4.0%} \"\n", " f\"at 2.0: {trivial.mean():4.0%}\"\n", " )\n", "\n", "\n", "summarize(\"forward\", forward_bounds)\n", "summarize(\"backward (stock)\", stock_backward)\n", "summarize(\"backward (custom)\", custom_backward)" ] }, { "cell_type": "markdown", "id": "cell-08", "metadata": {}, "source": [ "The stock backward bound sits near the trivial value of 2.0, which carries almost no information. On\n", "a mirror circuit, every error is \"close to the initial state\" in the sense that bound measures, so\n", "it cannot discriminate between terms. The custom bound is several times tighter and bounds many of the terms by 0.\n", "\n", "To check the implementation, the two can be compared box by box. For every box, the\n", "custom backward bound comes out **identical** to that same box's forward bound, up to rounding\n", "error. Both bound the same error at the same point in the circuit, and on a mirror circuit the gates\n", "after that point cancel to the inverse of the gates before it, so the two computations evaluate the\n", "same quantity from opposite ends:" ] }, { "cell_type": "code", "execution_count": 4, "id": "cell-09", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "16 of 16 boxes match the forward bounds\n", "largest disagreement across all terms: 6.00e-15\n" ] } ], "source": [ "matches = sum(\n", " np.allclose(forward_bounds[box_id].rates, custom_backward[box_id].rates)\n", " for box_id in forward_bounds\n", ")\n", "print(f\"{matches} of {len(forward_bounds)} boxes match the forward bounds\")\n", "\n", "worst = max(\n", " np.abs(forward_bounds[box_id].rates - custom_backward[box_id].rates).max()\n", " for box_id in forward_bounds\n", ")\n", "print(f\"largest disagreement across all terms: {worst:.2e}\")" ] }, { "cell_type": "markdown", "id": "cell-10", "metadata": {}, "source": [ "