{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "frontmatter",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Use different Lp-norms for Pauli term truncation\"\n",
        "description: \"Use different Lp-norms for Pauli term truncation for the latest version of Operator backpropagation (OBP)\"\n",
        "---\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8a7c9df5-d836-4ecb-8aec-8a1f2faa017a",
      "metadata": {},
      "source": [
        "# Use different Lp-norms for Pauli term truncation\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5958e635-64c1-4599-82ca-225b83312906",
      "metadata": {},
      "source": [
        "**Note:** Before reading this guide, you should read the [Truncate Pauli terms](/docs/addons/qiskit-addon-obp/guides/truncate-operator-terms) guide, which describes the truncation of low-weight Pauli terms that is built into the [backpropagate](/docs/api/qiskit-addon-obp/qiskit-addon-obp#backpropagate) method based on a specified [TruncationErrorBudget](/docs/api/qiskit-addon-obp/utils-truncating#truncationerrorbudget).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2780c587-cd2a-4398-8faa-de5e0c661a10",
      "metadata": {},
      "source": [
        "In this guide, you will learn about the [backpropagate](/docs/api/qiskit-addon-obp/qiskit-addon-obp#backpropagate) keyword argument, `p_norm`, which can be used to change the Lp-norm used to estimate the error incurred by the truncated Pauli terms.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "872aeae7-0312-41db-8ebb-f7b02838b54d",
      "metadata": {},
      "source": [
        "## Construct an example circuit\n",
        "\n",
        "This guide uses the same example circuit as in the [Truncate Pauli terms](/docs/addons/qiskit-addon-obp/guides/truncate-operator-terms) guide:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "d6f281d4-706e-41ad-b7b5-bc7ebe106996",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/addons/qiskit-addon-obp/guides/bound-error-using-p-norm/extracted-outputs/d6f281d4-706e-41ad-b7b5-bc7ebe106996-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 1,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "import rustworkx.generators\n",
        "from qiskit.synthesis import LieTrotter\n",
        "from qiskit_addon_utils.problem_generators import (\n",
        "    PauliOrderStrategy,\n",
        "    generate_time_evolution_circuit,\n",
        "    generate_xyz_hamiltonian,\n",
        ")\n",
        "from qiskit_addon_utils.slicing import combine_slices, slice_by_gate_types\n",
        "\n",
        "# Generate a linear chain of 10 qubits\n",
        "num_qubits = 10\n",
        "linear_chain = rustworkx.generators.path_graph(num_qubits)\n",
        "\n",
        "# Use an arbitrary XY model\n",
        "hamiltonian = generate_xyz_hamiltonian(\n",
        "    linear_chain,\n",
        "    coupling_constants=(0.05, 0.02, 0.0),\n",
        "    ext_magnetic_field=(0.02, 0.08, 0.0),\n",
        "    pauli_order_strategy=PauliOrderStrategy.InteractionThenColor,\n",
        ")\n",
        "# Evolve for some time\n",
        "circuit = generate_time_evolution_circuit(\n",
        "    hamiltonian, synthesis=LieTrotter(reps=3), time=2.0\n",
        ")\n",
        "# slice the circuit by gate type\n",
        "slices = slice_by_gate_types(circuit)\n",
        "\n",
        "# For visualization purposes only, recombine the slices with barriers between them and draw the resulting circuit\n",
        "combine_slices(slices, include_barriers=True).draw(\"mpl\", fold=50, scale=0.6)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7a7c7299-0c82-4279-b3dc-4f39e8dddd7e",
      "metadata": {},
      "source": [
        "Define an observable for the total magnetization:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "ea9e7adc-b003-4762-a889-10e8d84a63d5",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit.quantum_info import SparsePauliOp\n",
        "\n",
        "obs = SparsePauliOp.from_sparse_list(\n",
        "    [(\"Z\", [i], 1.0) for i in range(num_qubits)], num_qubits=num_qubits\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "423150d8-80d2-4b80-9665-033808d30272",
      "metadata": {},
      "source": [
        "For reference, compute the exact expectation value:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "4dc72426-2eb5-4f8d-8bba-79650da06b54",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "9.318197859862146\n"
          ]
        }
      ],
      "source": [
        "from qiskit.primitives import StatevectorEstimator\n",
        "\n",
        "estimator = StatevectorEstimator()\n",
        "job = estimator.run([(circuit, obs)])\n",
        "res = job.result()\n",
        "exact_exp = res[0].data.evs\n",
        "print(exact_exp)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d21490c0-e86c-4fe0-af90-c9625813e0c0",
      "metadata": {},
      "source": [
        "## Use the L1 norm\n",
        "\n",
        "By default, and as you have already seen in the [Truncate Pauli terms](/docs/addons/qiskit-addon-obp/guides/truncate-operator-terms) guide, `p_norm=1`, which means that the error is estimated as:\n",
        "\n",
        "$$\n",
        "|\\langle\\psi|\\Delta|\\psi\\rangle| \\leq \\sum_{P\\in\\mathcal{T}} |c_P|\n",
        "$$\n",
        "\n",
        "where $\\psi$ is the quantum state, $\\Delta$ is the real difference between the exact and truncated observable (which is unknown), $\\mathcal{T}$ is the set of Pauli terms that were truncated, and $c_P$ is the Pauli terms coefficient.\n",
        "This inequality is a rigorous but very loose upper bound for most scenarios.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "425293ce-49fb-4bd0-8d2f-44a3a43db967",
      "metadata": {},
      "source": [
        "This guide, backpropagates six slices of the example circuit, using a constant error per slice of `0.001`. This value is to be understood as the budget within the given `p_norm`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "id": "b46531b3-055b-4112-903a-11f2dd52a9ac",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "TruncationErrorBudget(per_slice_budget=[0.001], max_error_total=inf, p_norm=1)\n"
          ]
        }
      ],
      "source": [
        "from qiskit_addon_obp.utils.truncating import setup_budget\n",
        "\n",
        "l1_truncation_error_budget = setup_budget(max_error_per_slice=0.001, p_norm=1)\n",
        "print(l1_truncation_error_budget)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "bc252678-58a0-407e-91fe-b7c545d57ae8",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Backpropagated 6 circuit slices.\n",
            "New observable contains 116 terms and 10 commuting groups.\n"
          ]
        }
      ],
      "source": [
        "from qiskit_addon_obp import backpropagate\n",
        "\n",
        "max_slices = 6\n",
        "l1_bp_obs, l1_remaining_slices, l1_metadata = backpropagate(\n",
        "    obs,\n",
        "    slices[-max_slices:],\n",
        "    truncation_error_budget=l1_truncation_error_budget,\n",
        ")\n",
        "l1_reduced_circuit = combine_slices(\n",
        "    slices[:-max_slices] + l1_remaining_slices\n",
        ")\n",
        "print(\n",
        "    f\"Backpropagated {max_slices - len(l1_remaining_slices)} circuit slices.\"\n",
        ")\n",
        "print(\n",
        "    f\"New observable contains {len(l1_bp_obs)} terms and {len(l1_bp_obs.group_commuting(qubit_wise=True))} commuting groups.\"\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9f2782f8-1e93-4319-af26-dc467d7ed459",
      "metadata": {},
      "source": [
        "We can now compute the expectation value of the backpropagated observable, as well as the error with respect to the exact reference:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "5be8c457-6569-452d-8502-0fc5e39bf918",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "9.317869899338842 0.00032796052330397174\n"
          ]
        }
      ],
      "source": [
        "estimator = StatevectorEstimator()\n",
        "job = estimator.run([(l1_reduced_circuit, l1_bp_obs)])\n",
        "res = job.result()\n",
        "l1_exp = res[0].data.evs\n",
        "l1_error = exact_exp - l1_exp\n",
        "print(l1_exp, l1_error)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c0eccc5b-dc71-45b3-ad79-3d3dcb491f2d",
      "metadata": {},
      "source": [
        "Finally, we can plot the error incurred during the backpropagation of each slice, as well as the accumulated error.\n",
        "The accumulated error is the sum of the slice errors. We can see that the accumulated error is a very loose upper bound to the actual error.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "ff72e77f-e2c0-4cce-8575-c5aa587f78fb",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/addons/qiskit-addon-obp/guides/bound-error-using-p-norm/extracted-outputs/ff72e77f-e2c0-4cce-8575-c5aa587f78fb-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "from matplotlib import pyplot as plt\n",
        "from qiskit_addon_obp.utils.visualization import (\n",
        "    plot_accumulated_error,\n",
        "    plot_slice_errors,\n",
        ")\n",
        "\n",
        "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
        "axes[1].plot([6], [l1_error], \"x\", color=\"red\", label=\"actual error\")\n",
        "plot_slice_errors(l1_metadata, axes[0])\n",
        "plot_accumulated_error(l1_metadata, axes[1])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "10dd1139-c597-43c2-9f58-0a0988de768f",
      "metadata": {},
      "source": [
        "## Use the L2 norm\n",
        "\n",
        "One can argue that the L2 norm is a better approximation of the incurred error than the L1 norm.\n",
        "That is because we can assume the quantum state $|\\psi\\rangle$ to be drawn from a Haar random ensemble, in which case the incurred error follows a distribution with a vanishing mean and a variance that can be approximately bound by the L2 norm:\n",
        "\n",
        "$$\n",
        "|\\langle\\psi|\\Delta|\\psi\\rangle| \\lesssim \\left( \\sum_{P\\in\\mathcal{T}} |c_P|^2 \\right)^{1/2}\n",
        "$$\n",
        "\n",
        "While the bound is not rigorous, it will only be violated in pathological cases.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f302d808-b341-49cb-8bbd-b03f2bfc6dd6",
      "metadata": {},
      "source": [
        "We again backpropagate six slices of the example circuit, using a maximum error per slice of `0.001` (this time, interpreted on the L2 norm).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "id": "c7b7e21f-22e4-479d-954c-39400974f8c1",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "TruncationErrorBudget(per_slice_budget=[0.001], max_error_total=inf, p_norm=2)\n"
          ]
        }
      ],
      "source": [
        "l2_truncation_error_budget = setup_budget(max_error_per_slice=0.001, p_norm=2)\n",
        "print(l2_truncation_error_budget)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "id": "e92680c5-497e-47e0-ae0f-69465028dd66",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Backpropagated 6 circuit slices.\n",
            "New observable contains 84 terms and 6 commuting groups.\n"
          ]
        }
      ],
      "source": [
        "max_slices = 6\n",
        "l2_bp_obs, l2_remaining_slices, l2_metadata = backpropagate(\n",
        "    obs,\n",
        "    slices[-max_slices:],\n",
        "    truncation_error_budget=l2_truncation_error_budget,\n",
        ")\n",
        "l2_reduced_circuit = combine_slices(\n",
        "    slices[:-max_slices] + l2_remaining_slices\n",
        ")\n",
        "print(\n",
        "    f\"Backpropagated {max_slices - len(l2_remaining_slices)} circuit slices.\"\n",
        ")\n",
        "print(\n",
        "    f\"New observable contains {len(l2_bp_obs)} terms and {len(l2_bp_obs.group_commuting(qubit_wise=True))} commuting groups.\"\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a05f992a-3a38-4912-9e1b-bcd40463c35e",
      "metadata": {},
      "source": [
        "Again, we compute the expectation value of the backpropagated observable and the error with respect to the exact reference:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "id": "73fe737a-c3b0-4639-83dc-d614521dd77a",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "9.317829770853422 0.00036808900872387085\n"
          ]
        }
      ],
      "source": [
        "estimator = StatevectorEstimator()\n",
        "job = estimator.run([(l2_reduced_circuit, l2_bp_obs)])\n",
        "res = job.result()\n",
        "l2_exp = res[0].data.evs\n",
        "l2_error = exact_exp - l2_exp\n",
        "print(l2_exp, l2_error)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "81fd099c-f144-4c59-9b76-72fd8b06d8ad",
      "metadata": {},
      "source": [
        "Plotting the incurred errors per slice and the accumulated error yields a similar picture as before.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "fe7f454c-7d79-4c61-b1dd-6a98564fb5f1",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/addons/qiskit-addon-obp/guides/bound-error-using-p-norm/extracted-outputs/fe7f454c-7d79-4c61-b1dd-6a98564fb5f1-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
        "axes[1].plot([6], [l2_error], \"x\", color=\"red\", label=\"actual error\")\n",
        "plot_slice_errors(l2_metadata, axes[0])\n",
        "plot_accumulated_error(l2_metadata, axes[1])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9ef23a93-5c3b-48be-a02a-e666179c8c65",
      "metadata": {},
      "source": [
        "Note that the accumulated error is again the sum of the single slice errors. This is another loose bound due to the Minkowski inequality, since we have to compute this bound recursively:\n",
        "\n",
        "$$\n",
        "|\\langle\\psi|\\Delta_{i}|\\psi\\rangle| \\leq |\\langle\\psi|\\tilde{\\Delta}_{i-1}|\\psi\\rangle| + \\left( \\sum_{P\\in\\mathcal{T_i}} |c_P|^2 \\right)^{1/2} = |\\langle\\psi|\\tilde{\\Delta}_{i}|\\psi\\rangle|\n",
        "$$\n",
        "\n",
        "where the new subscript $i$ indicates the current slice iteration, making $\\Delta_i$ the actual error at backpropagation iteration $i$, $\\tilde{\\Delta}_{i-1}$ the approximated truncation error from iteration $i-1$, and $\\mathcal{T}_i$ the set of Pauli terms truncated at iteration $i$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}