{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Noisy Simulation \n",
    "\n",
    "Quantum noise can be characterized into coherent and incoherent sources of errors that arise during a computation. Coherent noise is commonly due to systematic errors originating from device miscalibrations, for example, gates implementing a rotation $\\theta + \\epsilon$ instead of $\\theta$.\n",
    "\n",
    "Incoherent noise has its origins in quantum states being entangled with the environment due to decoherence. This leads to mixed states which are probability distributions over pure states and are described by employing the density matrix formalism. \n",
    "\n",
    "We can model incoherent noise via quantum channels which are linear, completely positive, and trace preserving maps. These maps are called Kraus operators, $\\{ K_i \\}$, which satisfy the condition $\\sum_{i} K_i^\\dagger K_i = \\mathbb{I}$. \n",
    "\n",
    "The bit-flip channel flips the qubit with probability $p$ and leaves it unchanged with probability $1-p$. This can be represented by employing Kraus operators: \n",
    "\n",
    "$$K_0 = \\sqrt{1-p} \\begin{pmatrix}\n",
    "    1 & 0 \\\\\n",
    "    0 & 1\n",
    "\\end{pmatrix}$$\n",
    "\n",
    "$$K_1 = \\sqrt{p} \\begin{pmatrix}\n",
    "  0 & 1 \\\\\n",
    "  1 & 0\n",
    "\\end{pmatrix}$$\n",
    "\n",
    "Let's implement the bit-flip channel using CUDA-Q:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import cudaq\n",
    "from cudaq import spin\n",
    "\n",
    "import numpy as np\n",
    "\n",
    "# To model quantum noise, we need to utilize the density matrix simulator target.\n",
    "cudaq.set_target(\"density-matrix-cpu\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "     ╭───╮\n",
      "q0 : ┤ x ├\n",
      "     ├───┤\n",
      "q1 : ┤ x ├\n",
      "     ╰───╯\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Let's define a simple kernel that we will add noise to.\n",
    "qubit_count = 2\n",
    "\n",
    "\n",
    "@cudaq.kernel\n",
    "def kernel(qubit_count: int):\n",
    "    qvector = cudaq.qvector(qubit_count)\n",
    "    x(qvector)\n",
    "\n",
    "\n",
    "print(cudaq.draw(kernel, qubit_count))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{ 11:1000 }\n"
     ]
    }
   ],
   "source": [
    "# In the ideal noiseless case, we get |11> 100% of the time.\n",
    "\n",
    "ideal_counts = cudaq.sample(kernel, qubit_count, shots_count=1000)\n",
    "ideal_counts.dump()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{ 00:8 01:54 10:95 11:843 }\n"
     ]
    }
   ],
   "source": [
    "# First, we will define an out of the box noise channel. In this case,\n",
    "# we choose depolarization noise. This depolarization will result in\n",
    "# the qubit state decaying into a mix of the basis states, |0> and |1>,\n",
    "# with our provided probability.\n",
    "error_probability = 0.1\n",
    "depolarization_channel = cudaq.DepolarizationChannel(error_probability)\n",
    "\n",
    "# Other built in noise models \n",
    "bit_flip = cudaq.BitFlipChannel(error_probability) \n",
    "phase_flip = cudaq.PhaseFlipChannel(error_probability)\n",
    "amplitude_damping = cudaq.AmplitudeDampingChannel(error_probability)\n",
    "\n",
    "# We can also define our own, custom noise channels through\n",
    "# Kraus operators. Here we will define two operators representing\n",
    "# bit flip errors.\n",
    "\n",
    "# Define the Kraus Error Operator as a complex ndarray.\n",
    "kraus_0 = np.sqrt(1 - error_probability) * np.array([[1.0, 0.0], \n",
    "                                                     [0.0, 1.0]],\n",
    "                                                    dtype=np.complex128)\n",
    "\n",
    "kraus_1 = np.sqrt(error_probability) * np.array([[0.0, 1.0], \n",
    "                                                 [1.0, 0.0]],\n",
    "                                                dtype=np.complex128)\n",
    "\n",
    "# Add the Kraus Operator to create a quantum channel.\n",
    "bitflip_channel = cudaq.KrausChannel([kraus_0, kraus_1])\n",
    "\n",
    "# Add noise channels to our noise model.\n",
    "noise_model = cudaq.NoiseModel()\n",
    "\n",
    "# Apply the depolarization channel to any X-gate on the 0th qubit.\n",
    "noise_model.add_channel(\"x\", [0], depolarization_channel)\n",
    "# Apply the bitflip channel to any X-gate on the 1st qubit.\n",
    "noise_model.add_channel(\"x\", [1], bitflip_channel)\n",
    "\n",
    "# Due to the impact of noise, our measurements will no longer be uniformly\n",
    "# in the |11> state.\n",
    "noisy_counts = cudaq.sample(kernel,\n",
    "                            qubit_count,\n",
    "                            noise_model=noise_model,\n",
    "                            shots_count=1000)\n",
    "noisy_counts.dump()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "-0.8666666666666666"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# We can also use noise models with the observe function\n",
    "\n",
    "hamiltonian = spin.z(0)\n",
    "\n",
    "noisy_result = cudaq.observe(kernel,\n",
    "                             hamiltonian,\n",
    "                             qubit_count,\n",
    "                             noise_model=noise_model)\n",
    "\n",
    "noisy_result.expectation()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In addition to gate-based noise injection, CUDA-Q also supports fine-grained, explicit noise injection via the `cudaq.apply_noise` function. You can place this method in your kernels, and inject specific noise on specific qubits wherever you need."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{ 000:351 001:117 010:115 011:345 100:29 101:7 110:5 111:31 }\n",
      "\n"
     ]
    }
   ],
   "source": [
    "@cudaq.kernel\n",
    "def inject_noise_example():\n",
    "    q = cudaq.qvector(3)\n",
    "\n",
    "    # Apply depolarization noise to the first qubit\n",
    "    cudaq.apply_noise(cudaq.DepolarizationChannel, 0.1, q[0])\n",
    "\n",
    "    # Perform gate operations\n",
    "    h(q[1])\n",
    "    x.ctrl(q[1], q[2])\n",
    "\n",
    "    # Inject a Y error into the second qubit\n",
    "    cudaq.apply_noise(cudaq.YError, 0.1, q[1])\n",
    "\n",
    "    # Apply a general Pauli noise channel to the third qubit, where the 3 values indicate the probability of X, Y, and Z errors.\n",
    "    cudaq.apply_noise(cudaq.Pauli1, 0.1, 0.1, 0.1, q[2])\n",
    "\n",
    "# Define and apply a noise model\n",
    "noise = cudaq.NoiseModel()\n",
    "counts = cudaq.sample(inject_noise_example, noise_model=noise)\n",
    "print(counts)"
   ]
  }
 ],
 "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.10.12"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
