{ "cells": [ { "cell_type": "markdown", "id": "cell-00", "metadata": {}, "source": [ "# How to tune the configuration options\n", "\n", "The bounds computation is exact in principle but approximate in practice: evolving a Pauli error\n", "term through a circuit produces an operator with exponentially many terms, and finding the spectral\n", "norm of the resulting commutator is itself expensive. The options below control that cost. They fall\n", "into three groups:\n", "\n", "- **Accuracy** options change the numerical value of the bounds, trading tightness for cost.\n", "- **Scope** determines *which* bounds are computed.\n", "- **Execution** options change *how* the computation runs (not what it computes).\n", "\n", "| Option | Default | Controls | Effect on the result |\n", "| --- | --- | --- | --- |\n", "| **Accuracy** | | | |\n", "| `evolution_max_terms` | `1_000_000` | How many operator terms survive the evolution | Lower: cheaper, looser bounds |\n", "| `atol_simplify` | `1e-8` | Which commutator terms are discarded as negligible | Higher: cheaper, looser bounds |\n", "| `eigval_max_qubits` | `14` | How wide a commutator can be for its spectral norm to be computed | Lower: cheaper, looser bounds |\n", "| `atol_eigenvalue` | `1e-10` | When the iterative eigensolver counts as converged | Higher: cheaper, less accurate bounds |\n", "| **Scope** | | | |\n", "| `max_num_boxes` | `None` | How many boxes have bounds computed | Boxes beyond the limit keep the trivial bound of 2.0 |\n", "| **Execution** | | | |\n", "| `timeout` | `None` | A wall-clock budget for the whole computation | Any remaining work keeps the trivial bound of 2.0 |\n", "| `num_processes` | `1` | How many worker processes compute bounds in parallel | None on its own |\n", "\n", "A bound is only useful if it is *valid*: it must never understate an error term's effect on the\n", "observable. Loosening any of these options other than `atol_eigenvalue` can only make the bounds\n", "larger (more conservative), never smaller. Thus, the cost of a loose setting is a larger sampling\n", "overhead, not a wrong expectation value - [`atol_eigenvalue`](#atol_eigenvalue) is the exception." ] }, { "cell_type": "markdown", "id": "cell-01", "metadata": {}, "source": [ "## Circuit definition\n", "\n", "The following experiments use the same Trotterized Ising circuit and observable as the [quickstart guide](quickstart.ipynb). At\n", "this size, every option has a visible effect while each computation still takes less than a second." ] }, { "cell_type": "code", "execution_count": 1, "id": "cell-02", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "noisy layers: 2\n", "Pauli error terms: 198\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", "circuit = trotter_ising_circuit(num_qubits, num_steps=4, rx_angle=np.pi / 16, rzz_angle=-np.pi / 3)\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\"noisy layers: {len(noise_model_paulis)}\")\n", "print(f\"Pauli error terms: {sum(len(p) for p in noise_model_paulis.values())}\")" ] }, { "cell_type": "markdown", "id": "cell-03", "metadata": {}, "source": [ "A small helper summarizes each set of bounds in one line, so the effect of an option shows up as a\n", "few numbers rather than a wall of arrays.\n", "\n", "Every bound lies between 0 and the trivial bound of 2.0. The summary reports the mean bound and its\n", "standard deviation, followed by how the terms fall into these groups: \n", " - Terms bounded by exactly 0 because they commute with the evolved observable\n", " - Terms with a bound computed between 0 and 2.0\n", " - Terms left at the trivial 2.0\n", "\n", "Note that **a lower mean is tighter, and tighter means a smaller sampling overhead later.**\n", "\n", "A sweep over one option marks the option's default with `*`:" ] }, { "cell_type": "code", "execution_count": 2, "id": "cell-04", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " defaults mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.29s\n" ] } ], "source": [ "import inspect\n", "import time\n", "\n", "from qiskit_addon_slc.bounds import compute_forward_bounds\n", "\n", "\n", "def summarize(rates, label, elapsed):\n", " \"\"\"Prints a one-line summary of a set of bounds.\"\"\"\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:28} 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", " f\"{elapsed:4.2f}s\"\n", " )\n", "\n", "\n", "def forward(label, **kwargs):\n", " \"\"\"Computes the forward bounds, summarizes them and returns all their rates.\"\"\"\n", " start = time.perf_counter()\n", " bounds = compute_forward_bounds(boxed_circuit, noise_model_paulis, observable, **kwargs)\n", " elapsed = time.perf_counter() - start\n", " rates = np.concatenate([bound.rates for bound in bounds.values()])\n", " summarize(rates, label, elapsed)\n", " return rates\n", "\n", "\n", "def sweep(option, values):\n", " \"\"\"Computes the forward bounds for each value of one option, marking the default with `*`.\"\"\"\n", " default = inspect.signature(compute_forward_bounds).parameters[option].default\n", " for value in values:\n", " marker = \"*\" if value == default else \" \"\n", " forward(f\"{marker} {option}={value:g}\", **{option: value})\n", "\n", "\n", "baseline = forward(\" defaults\")" ] }, { "cell_type": "markdown", "id": "cell-05", "metadata": {}, "source": [ "## Accuracy\n", "\n", "### `evolution_max_terms`\n", "\n", "Evolving a Pauli error term through non-Clifford gates turns it into a sum of Paulis, and that sum\n", "grows with circuit depth. `evolution_max_terms` caps how many of those terms are kept. The 1-norm of\n", "whatever is discarded is added to the bound, so nothing is silently lost.\n", "\n", "The sweep runs from tighter than the default down to an extreme cut. Lowering the cap makes the\n", "computation cheaper and the bounds looser." ] }, { "cell_type": "code", "execution_count": 3, "id": "cell-06", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " evolution_max_terms=1e+07 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.38s\n", "* evolution_max_terms=1e+06 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s\n", " evolution_max_terms=1000 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.30s\n", " evolution_max_terms=100 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.29s\n", " evolution_max_terms=10 mean 0.3927 ± 0.711 │ at 0: 59% computed: 31% at 2.0: 9% │ 0.28s\n", " evolution_max_terms=2 mean 0.5245 ± 0.792 │ at 0: 59% computed: 23% at 2.0: 18% │ 0.21s\n" ] } ], "source": [ "sweep(\"evolution_max_terms\", (10_000_000, 1_000_000, 1000, 100, 10, 2))" ] }, { "cell_type": "markdown", "id": "cell-07", "metadata": {}, "source": [ "Raising the cap past its default changes nothing here. For a circuit of this size, the default\n", "already keeps every term, so the bounds are as tight as this option can make them. That is the\n", "typical situation. Tightening an accuracy option only pays off once its default is actually limiting,\n", "and the way to find out is the experiment above. Rerun it with a tighter value and see whether the\n", "bounds move.\n", "\n", "