{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Parametrized circuits\n",
    "\n",
    "This tutorial shows you can submit parametrized quantum circuits to the `POVMSampler`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "%load_ext autoreload\n",
    "%autoreload 2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Parametrized Circuit\n",
    "\n",
    "Let us look at a 2-qubit quantum circuit with 5 parameters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<pre style=\"word-wrap: normal;white-space: pre;background: #fff0;line-height: 1.1;font-family: &quot;Courier New&quot;,Courier,monospace\">     ┌────────────────────────────────────────────────┐\n",
       "q_0: ┤0                                               ├\n",
       "     │  RealAmplitudes(θ[0],θ[1],θ[2],θ[3],θ[4],θ[5]) │\n",
       "q_1: ┤1                                               ├\n",
       "     └────────────────────────────────────────────────┘</pre>"
      ],
      "text/plain": [
       "     ┌────────────────────────────────────────────────┐\n",
       "q_0: ┤0                                               ├\n",
       "     │  RealAmplitudes(θ[0],θ[1],θ[2],θ[3],θ[4],θ[5]) │\n",
       "q_1: ┤1                                               ├\n",
       "     └────────────────────────────────────────────────┘"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from qiskit.circuit.library import RealAmplitudes\n",
    "\n",
    "# Prepare inputs.\n",
    "num_qubits = 2\n",
    "qc = RealAmplitudes(num_qubits=num_qubits, reps=2)\n",
    "qc.draw()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "# Assume you want to run the circuit with two different sets of parameter values\n",
    "theta = np.array(\n",
    "    [\n",
    "        [0, 1, 1, 2, 3, 5],  # first set of parameter values\n",
    "        [0, 1, 1, 2, 2, 5],  # second set of parameter values\n",
    "    ]\n",
    ")\n",
    "# Shape of the resulting `BindingsArray` is (2,) with `num_param` equal to 6"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Measurement\n",
    "\n",
    "We now look at the implementation of Classical Shadows measurement"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "from povm_toolbox.library import ClassicalShadows\n",
    "\n",
    "# By default, the Classical Shadows (CS) measurement uses X,Y,Z measurements with equal probability.\n",
    "cs_implementation = ClassicalShadows(num_qubits=num_qubits, seed=342)\n",
    "# Define the default shot budget.\n",
    "shots = 4096"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "pub = (qc, theta, shots, cs_implementation)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from povm_toolbox.sampler import POVMSampler\n",
    "from qiskit.primitives import StatevectorSampler as Sampler\n",
    "\n",
    "# Internal `BaseSampler`\n",
    "sampler = Sampler(seed=432)\n",
    "\n",
    "# Actual `POVMSampler` instance\n",
    "povm_sampler = POVMSampler(sampler=sampler)\n",
    "\n",
    "# Submit the job by specifying the list of PUBs to run, here we have a single PUB.\n",
    "job = povm_sampler.run([pub])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Results\n",
    "We submitted a single PUB, hence the `PrimitiveResult` will contain only one `POVMPubResult`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "POVMPubResult(data=DataBin<2>(povm_measurement_creg=BitArray(<shape=(2,), num_shots=4096, num_bits=2>)), metadata=RPMMetadata(povm_implementation=ClassicalShadows(num_qubits=2), composed_circuit=<qiskit.circuit.library.n_local.real_amplitudes.RealAmplitudes object at 0x177cbbd40>, pvm_keys=np.ndarray<2,4096,2>))\n",
      "(2,)\n"
     ]
    }
   ],
   "source": [
    "pub_result = job.result()[0]\n",
    "print(pub_result)\n",
    "\n",
    "# Note that the pub result will contain a `BitArray` that has the same shape\n",
    "# as the submitted `BindingsArray`, which is (2,) in this example.\n",
    "print(pub_result.get_counts(loc=...).shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We now define our POVM post-processor, which will use the result object to estimate expectation values of some observables."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "from povm_toolbox.post_processor.povm_post_processor import POVMPostProcessor\n",
    "\n",
    "post_processor = POVMPostProcessor(pub_result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Since the POVM implementation that we used is informationally complete, we can define our observables of interest after the sampling process."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "from qiskit.quantum_info import SparsePauliOp\n",
    "\n",
    "H1 = SparsePauliOp.from_list([(\"II\", 1), (\"IZ\", 2), (\"XI\", 3)])\n",
    "H2 = SparsePauliOp.from_list([(\"II\", 1), (\"XX\", 1), (\"YY\", -1), (\"ZZ\", 1)])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The post-processor will return two expectation values corresponding to the two different sets of parameter values that were submitted."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[1.98364258 4.71411133]\n"
     ]
    }
   ],
   "source": [
    "exp_value, std = post_processor.get_expectation_value(H1)\n",
    "print(exp_value)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If we are interested to evaluate an observable only for one set of parameter values, this set can be specified through the `loc` argument."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.05297851562500033\n"
     ]
    }
   ],
   "source": [
    "exp_value, std = post_processor.get_expectation_value(H2, loc=1)\n",
    "print(exp_value)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Further example\n",
    "Let us look at a `BindingsArray` instance with a more complex shape."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "bindings_array_shape = (3, 4)\n",
    "num_param = 6\n",
    "\n",
    "theta = np.arange(72).reshape((*bindings_array_shape, num_param))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(3, 4)\n"
     ]
    }
   ],
   "source": [
    "job = povm_sampler.run([(qc, theta, shots, cs_implementation)])\n",
    "pub_result = job.result()[0]\n",
    "print(pub_result.get_counts(loc=...).shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[[-2.65698242 -1.44775391  4.69360352  3.39575195]\n",
      " [-4.09399414 -2.57861328 -1.41918945  5.98339844]\n",
      " [ 4.74121094  5.98266602 -0.06347656 -0.11474609]]\n",
      "[[0.09684276 0.07191009 0.05729555 0.07203544]\n",
      " [0.0796096  0.09563027 0.10687004 0.07980559]\n",
      " [0.0973881  0.07996116 0.08056718 0.07930243]]\n"
     ]
    }
   ],
   "source": [
    "post_processor = POVMPostProcessor(pub_result)\n",
    "exp_values, std = post_processor.get_expectation_value(H1)\n",
    "print(exp_values)\n",
    "print(std)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "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.12.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}