{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "frontmatter",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Classically simulating circuits with OBP\"\n",
        "description: \"Classically simulating circuits with OBP for the latest version of Operator backpropagation (OBP)\"\n",
        "---\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "26d926db-2e2f-41ff-947e-9757a9f426d8",
      "metadata": {},
      "source": [
        "# Classically simulating circuits with OBP\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7642d6db-fe6d-4506-8e49-6c840f166e2b",
      "metadata": {},
      "source": [
        "In this guide, you will learn how to classically simulate `QuantumCircuit` instances to estimate expectation values entirely through the means of OBP.\n",
        "\n",
        "Since OBP will take an observable and backpropagate it through a given circuit, the \"simulation\" of a circuit amounts to computing the expectation value of the target observable with respect to this circuit.\n",
        "As you will see later, the `qiskit-addon-obp` package is even capable of handling simple noise models, allowing you to compute noisy expectation values, too!\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2e996d79-36fd-4a71-bd6b-737fc8e44987",
      "metadata": {},
      "source": [
        "## Constructing an example circuit\n",
        "\n",
        "For the purposes of this guide, we will use the same example circuit as in the [Pauli term truncation guide](/docs/addons/qiskit-addon-obp/guides/truncate-operator-terms):\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "b7946767-904b-422e-b076-7f956f3fdb70",
      "metadata": {},
      "outputs": [],
      "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",
        "# we generate a linear chain of 10 qubits\n",
        "num_qubits = 10\n",
        "linear_chain = rustworkx.generators.path_graph(num_qubits)\n",
        "\n",
        "# we 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",
        "# we 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)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "63bcf4d0-7abe-4467-bc2b-f95d8d66dee5",
      "metadata": {},
      "source": [
        "However, the above is purely the circuit describing the time evolution under a chosen Hamiltonian.\n",
        "We also need an initial state to start from, with respect to which we compute the expectation values of our observable.\n",
        "\n",
        "Of course, we could choose the all-zero (or vacuum) state as our initial state, but to show how one would insert their own initial state, we choose a different one below.\n",
        "\n",
        "One possibility, would be to prepend the initial state to our time-evolution circuit above: `circuit.compose(initial_state, front=True)`.\n",
        "But since we have already sliced our `circuit`, it is easier to simply insert the initial state as the first slice, which we do below.\n",
        "\n",
        "In this way, we can simply replace the first slice with another initial state, if we want to exchange that in the future, without having to recompute our slices.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "ffaa07e2-af75-4424-9e7b-61c81b3bb9ff",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit.circuit import QuantumCircuit\n",
        "\n",
        "initial_state = QuantumCircuit(num_qubits)\n",
        "for i in range(0, num_qubits, 2):\n",
        "    initial_state.x(i)\n",
        "\n",
        "slices.insert(0, initial_state)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "8758f393-8691-4116-94ca-1185c6188bb4",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/addons/qiskit-addon-obp/guides/simulating-circuits-with-obp/extracted-outputs/8758f393-8691-4116-94ca-1185c6188bb4-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 3,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# for visualization purposes only, we 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": "bc9d51da-0a4e-46e2-8a5a-3501456aeb81",
      "metadata": {},
      "source": [
        "## Simulating a noiseless expectation value\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1125cb39-4eda-4928-86b2-24e1872ed87e",
      "metadata": {},
      "source": [
        "As our target observable, we choose the `ZZ` observable on the central qubits:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "id": "ee438fc3-8196-43d9-bcdb-956fd9ec4cf4",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit.quantum_info import SparsePauliOp\n",
        "\n",
        "obs = SparsePauliOp(\"IIIIZZIIII\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d06f10b0-683e-476e-9ac3-5c110364a0c8",
      "metadata": {},
      "source": [
        "At this point, we are already set to classically simulate the expectation value using OBP.\n",
        "To do so, we simply provide the *all* the slices to the `backpropagate` method, like so:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "6402f5f5-08e0-4d05-ab2b-0c8863e9f966",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit_addon_obp import backpropagate\n",
        "\n",
        "vacuum_state_obs, _, metadata = backpropagate(obs, slices)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7b2d555e-7fab-49f1-82d4-74a770f1be75",
      "metadata": {},
      "source": [
        "We have now backpropagated our target observable `obs` through the *entire* circuit (**including** the `initial_state` which we placed on `slices[0]`) resulting in a new `SparsePauliOp` whose expectation value we obtain by projecting it on the *vacuum state* (`|00...00>`).\n",
        "\n",
        "This can be achieved in a straight forward manner by summing up the coefficients of all Pauli terms defined in the computational basis:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "63ae0edc-e8f6-467b-896a-ddd451d40a9a",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "np.complex128(-0.8285688012239535+4.9487770271457865e-20j)"
            ]
          },
          "execution_count": 6,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "vacuum_state_obs.coeffs[~vacuum_state_obs.paulis.x.any(axis=1)].sum()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "107a0d89-323c-469b-8442-5e3f48169cd4",
      "metadata": {},
      "source": [
        "As a sanity check (and to prove that this works) we can compare our result against Qiskit's `Statevector`:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "f3cb999d-688a-4e82-bf12-d958ef0e2ec0",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "np.complex128(-0.8285687255430366+0j)"
            ]
          },
          "execution_count": 7,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "from qiskit.quantum_info import Statevector\n",
        "\n",
        "Statevector(combine_slices(slices)).expectation_value(obs)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "02207cde-e89e-4cf6-8b3e-727a750d245c",
      "metadata": {},
      "source": [
        "### Some notes on performance\n",
        "\n",
        "The computational efficiency of the `backpropagate` call above will heavily depend on many things, including:\n",
        "\n",
        "* the structure of the `circuit`\n",
        "* the method of slicing the circuit\n",
        "* the target observable\n",
        "* the truncation parameters\n",
        "\n",
        "Since the `backpropagate` method simplifies the observable after every *slice* has been applied, the number of gates in a slice can dramatically influence the computational burden.\n",
        "The most aggressive strategy in terms of operator simplification can be achieved by slicing your circuit into slices of individual gates.\n",
        "\n",
        "Additionally, you can leverage all of the truncation mechanism built into the `backpropagate` method.\n",
        "We did not do so above, effectively resulting in an exact expectation value, but you can learn how to in the [Pauli term truncation guide](/docs/addons/qiskit-addon-obp/guides/truncate-operator-terms).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1ba37abf-0f78-4053-8995-2ab8340237ab",
      "metadata": {},
      "source": [
        "## Simulating a noisy expectation value\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3a5e753b-c479-42d9-84d0-5ce4860ec091",
      "metadata": {},
      "source": [
        "The `qiskit-addon-obp` package also supports handling of noise models in the form of `PauliLindbladError`s.\n",
        "This is especially useful when you have characterized the noise model of the 2-qubit layers in your circuit, for example using the [`NoiseLearner`](/docs/api/qiskit-ibm-runtime/noise-learner-noise-learner).\n",
        "\n",
        "In this section, you will see how you can use the `LayerError` objects returned by the `NoiseLearner` to compute noisy expectation values using OBP.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2d50dbf1-3abd-4945-bed9-1ed49758e9cc",
      "metadata": {},
      "source": [
        "### Obtaining a noise model\n",
        "\n",
        "Normally, you would execute the `NoiseLearner` to obtain a noise model of your specific circuit.\n",
        "To avoid complexity (and randomness) in this tutorial, we will refrain from doing so, and instead hard-code some noise model for our circuit below.\n",
        "\n",
        "However, we make sure that the structure of our data matches that of the [`NoiseLearnerResult`](/docs/api/qiskit-ibm-runtime/results-noise-learner-result).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "54a7aba1-a7ee-4d0f-b95b-095158511ce0",
      "metadata": {},
      "source": [
        "In its current (non-transpiled) form, our circuit contains 4 unique layers of 2-qubit gates:\n",
        "\n",
        "* `slices[1]`: which has `Rxx` gates acting on all odd pairs of qubits\n",
        "* `slices[2]`: which has `Rxx` gates acting on all even pairs of qubits\n",
        "* `slices[3]`: which has `Ryy` gates acting on all odd pairs of qubits\n",
        "* `slices[4]`: which has `Ryy` gates acting on all even pairs of qubits\n",
        "\n",
        "In the cell below, we manually construct 4 `LayerError` instances for each one of these layers with some randomized error rates.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "id": "e7a3b1f9-b60c-4735-8d6b-5151a7c5b92b",
      "metadata": {},
      "outputs": [],
      "source": [
        "import numpy as np\n",
        "from qiskit.quantum_info import PauliList\n",
        "from qiskit_ibm_runtime.utils.noise_learner_result import (\n",
        "    LayerError,\n",
        "    PauliLindbladError,\n",
        ")\n",
        "\n",
        "# fmt: off\n",
        "pauli_errors_even = ['IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIXX', 'IIIIIIIIXY', 'IIIIIIIIXZ', 'IIIIIIIIYI', 'IIIIIIIIYX', 'IIIIIIIIYY', 'IIIIIIIIYZ', 'IIIIIIIIZI', 'IIIIIIIIZX', 'IIIIIIIIZY', 'IIIIIIIIZZ', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII', 'XIIIIIIIII', 'XXIIIIIIII', 'XYIIIIIIII', 'XZIIIIIIII', 'YIIIIIIIII', 'YXIIIIIIII', 'YYIIIIIIII', 'YZIIIIIIII', 'ZIIIIIIIII', 'ZXIIIIIIII', 'ZYIIIIIIII', 'ZZIIIIIIII']\n",
        "pauli_errors_odd = ['IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII']\n",
        "# fmt: on\n",
        "\n",
        "np.random.seed(42)\n",
        "\n",
        "layer_error_odd_xx = LayerError(\n",
        "    circuit=slices[1],\n",
        "    qubits=list(range(num_qubits)),\n",
        "    error=PauliLindbladError(\n",
        "        PauliList(pauli_errors_odd),\n",
        "        0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),\n",
        "    ),\n",
        ")\n",
        "\n",
        "layer_error_even_xx = LayerError(\n",
        "    circuit=slices[2],\n",
        "    qubits=list(range(num_qubits)),\n",
        "    error=PauliLindbladError(\n",
        "        PauliList(pauli_errors_even),\n",
        "        0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),\n",
        "    ),\n",
        ")\n",
        "\n",
        "layer_error_odd_yy = LayerError(\n",
        "    circuit=slices[3],\n",
        "    qubits=list(range(num_qubits)),\n",
        "    error=PauliLindbladError(\n",
        "        PauliList(pauli_errors_odd),\n",
        "        0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),\n",
        "    ),\n",
        ")\n",
        "\n",
        "layer_error_even_yy = LayerError(\n",
        "    circuit=slices[4],\n",
        "    qubits=list(range(num_qubits)),\n",
        "    error=PauliLindbladError(\n",
        "        PauliList(pauli_errors_even),\n",
        "        0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),\n",
        "    ),\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a9dff7ad-2c7a-4fc2-b7b5-5aa1a0e9b9db",
      "metadata": {},
      "source": [
        "If you would have used the `NoiseLearner` to identify the unique 2-qubit gate layers of your circuit and characterize their noise, you would obtain a `NoiseLearnerResult` object.\n",
        "This result would contain a list of `LayerError` objects, just like the ones we have manually constructed above.\n",
        "\n",
        "For each unique 2-qubit layer, the `LayerError` contains:\n",
        "\n",
        "* the `QuantumCircuit` representing that 2-qubit gate layer\n",
        "* the qubit indices which this circuit is acting upon\n",
        "* the `PauliLindbladError` which represents the characterized noise model of this layer\n",
        "\n",
        "The `PauliLindbladError` will contain two objects:\n",
        "\n",
        "* the list of Pauli errors that have been characterized\n",
        "* the error rates corresponding to each one of those Pauli errors\n",
        "\n",
        "Normally, the list of Pauli errors will be sparse. More specifically, it will contain the single-qubit Pauli errors on all qubits that have gates acting upon them as well as the two-qubit Pauli errors on all those qubits that are connected.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9c3a1a9d-6fcb-4345-b9ba-e86e2e362a7e",
      "metadata": {},
      "source": [
        "### Inserting the noisy layers into our circuit\n",
        "\n",
        "In the previous section, we have specifically constructed one `LayerError` for each of our known unique 2-qubit gate layers.\n",
        "This means, we know which `LayerError` matches a specific one of our slices exactly.\n",
        "\n",
        "Normally, when using the `LayerError`, you will need to figure out what the unique 2-qubit gate layer is, and where it occurs inside of your circuit.\n",
        "You will then need to adjust your `circuit` and/or `slices` to insert the `LayerError` accordingly.\n",
        "How to do this in the general case, is beyond the scope of this how-to guide.\n",
        "**TODO: link to external documentation, once it exists!**\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "dcb5937e-83d8-41ab-a526-d6677201c8f4",
      "metadata": {},
      "source": [
        "Here, our life is simpler because we know which slice a `LayerError` corresponds to.\n",
        "Therefore, it is now just a matter of inserting new slices to represent the noise.\n",
        "\n",
        "Note, that we must wrap each `PauliLindbladError` from `LayerError.error` in a `PauliLindbladErrorInstruction` for it to be a valid `QuantumCircuit` instruction.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "id": "1f5c4655-f6af-439f-8475-0ecf0c673de5",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit_addon_obp.utils.noise import PauliLindbladErrorInstruction\n",
        "\n",
        "noisy_slices = []\n",
        "for slice_ in slices:\n",
        "    if slice_ == layer_error_even_xx.circuit:\n",
        "        noisy_slices.append(\n",
        "            QuantumCircuit.from_instructions(\n",
        "                [\n",
        "                    (\n",
        "                        PauliLindbladErrorInstruction(\n",
        "                            layer_error_even_xx.error\n",
        "                        ),\n",
        "                        slice_.qubits,\n",
        "                    )\n",
        "                ]\n",
        "            )\n",
        "        )\n",
        "    elif slice_ == layer_error_odd_xx.circuit:\n",
        "        noisy_slices.append(\n",
        "            QuantumCircuit.from_instructions(\n",
        "                [\n",
        "                    (\n",
        "                        PauliLindbladErrorInstruction(\n",
        "                            layer_error_odd_xx.error\n",
        "                        ),\n",
        "                        slice_.qubits,\n",
        "                    )\n",
        "                ]\n",
        "            )\n",
        "        )\n",
        "    elif slice_ == layer_error_even_yy.circuit:\n",
        "        noisy_slices.append(\n",
        "            QuantumCircuit.from_instructions(\n",
        "                [\n",
        "                    (\n",
        "                        PauliLindbladErrorInstruction(\n",
        "                            layer_error_even_yy.error\n",
        "                        ),\n",
        "                        slice_.qubits,\n",
        "                    )\n",
        "                ]\n",
        "            )\n",
        "        )\n",
        "    elif slice_ == layer_error_odd_yy.circuit:\n",
        "        noisy_slices.append(\n",
        "            QuantumCircuit.from_instructions(\n",
        "                [\n",
        "                    (\n",
        "                        PauliLindbladErrorInstruction(\n",
        "                            layer_error_odd_yy.error\n",
        "                        ),\n",
        "                        slice_.qubits,\n",
        "                    )\n",
        "                ]\n",
        "            )\n",
        "        )\n",
        "    noisy_slices.append(slice_)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b3778d94-4d04-4094-990d-a354f2062932",
      "metadata": {},
      "source": [
        "We can check our work and draw the circuit below:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "id": "dadc314d-452f-465c-a382-d54685ce3f2a",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/addons/qiskit-addon-obp/guides/simulating-circuits-with-obp/extracted-outputs/dadc314d-452f-465c-a382-d54685ce3f2a-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 10,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "combine_slices(noisy_slices, include_barriers=True).draw(\n",
        "    \"mpl\", fold=100, scale=0.8\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "10b88555-0263-4cdb-a4c1-6895d7e55a5b",
      "metadata": {},
      "source": [
        "### Simulating a noisy expectation value\n",
        "\n",
        "At this point, classically simulating the expectation value works exactly the same as before, just\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "0e3ecb85-08c4-4fda-85e2-1000983c8842",
      "metadata": {},
      "outputs": [],
      "source": [
        "vacuum_state_noisy_obs, _, metadata = backpropagate(obs, noisy_slices)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "id": "a74e9e88-2f1c-4d40-873d-bf4524e2b7fe",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "np.complex128(-0.7230801696448901+7.082755280463563e-19j)"
            ]
          },
          "execution_count": 12,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "vacuum_state_noisy_obs.coeffs[\n",
        "    ~vacuum_state_noisy_obs.paulis.x.any(axis=1)\n",
        "].sum()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7a4d2ff5-7c49-463e-8515-6fc9daa951ad",
      "metadata": {},
      "source": [
        "We point out again, that multiple performance concerns should be considered.\n",
        "Please go back to the [corresponding section above](#some-notes-on-performance).\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
}