{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "b7675b9f",
   "metadata": {},
   "source": [
    "# Correlated Noise Simulation\n",
    "\n",
    "This notebook demonstrates how to simulate **correlated (multi-qubit) noise** using the QDK simulators.\n",
    "While the [noise.ipynb](noise.ipynb) notebook covers independent Pauli noise applied uniformly to all gates,\n",
    "this notebook shows how to:\n",
    "\n",
    "1. Configure per-gate noise using `NoiseConfig`.\n",
    "2. Define custom noise intrinsics to model multi-qubit noise for custom gates.\n",
    "\n",
    "First, make sure prerequisites are available. The package and extra `qdk[jupyter]` must be already installed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3f51ef59",
   "metadata": {},
   "outputs": [],
   "source": [
    "import qdk\n",
    "from qdk import qsharp, openqasm\n",
    "from qdk.simulation import NoiseConfig, run_qir\n",
    "from qdk.widgets import Histogram"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3944616a",
   "metadata": {},
   "source": [
    "## 1. Per-Gate Noise with `NoiseConfig`\n",
    "\n",
    "The convenience functions `DepolarizingNoise(p)` and `BitFlipNoise(p)` apply the same noise\n",
    "to every gate. For more realistic modeling, you often need different error rates for different\n",
    "gates, e.g., two-qubit gates are typically noisier than single-qubit gates, and they might apply\n",
    "different noise to each qubit.\n",
    "\n",
    "`NoiseConfig` gives you a noise table for each gate (H, X, CNOT, etc.), and each table\n",
    "supports arbitrary Pauli error strings."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fbbeb079",
   "metadata": {},
   "source": [
    "### Bell pair with per-gate noise\n",
    "\n",
    "Define a Bell pair circuit in Q# and run it with different noise rates on single-qubit vs. two-qubit gates."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "65ae7014",
   "metadata": {
    "vscode": {
     "languageId": "qsharp"
    }
   },
   "outputs": [],
   "source": [
    "%%qsharp\n",
    "\n",
    "operation BellPair() : Result[] {\n",
    "    use qs = Qubit[2];\n",
    "    H(qs[0]);\n",
    "    CNOT(qs[0], qs[1]);\n",
    "    MResetEachZ(qs)\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e9d78333",
   "metadata": {},
   "source": [
    "First, run without noise to establish a baseline. We expect only `[Zero, Zero]` and `[One, One]`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aa9be062",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = qsharp.run(\"BellPair()\", 1000, seed=42)\n",
    "Histogram(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "91fd10e8",
   "metadata": {},
   "source": [
    "Now configure `NoiseConfig` with different noise rates per gate:\n",
    "- H gate: 0.5% depolarizing (single-qubit gates are relatively clean)\n",
    "- CNOT (cx): 5% depolarizing (two-qubit gates are typically 10× noisier)\n",
    "- Measurement (mresetz): 1% bit-flip (measurement readout error)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e62c90e2",
   "metadata": {},
   "outputs": [],
   "source": [
    "noise = NoiseConfig()\n",
    "noise.h.set_depolarizing(0.005)\n",
    "noise.cx.set_depolarizing(0.05)\n",
    "noise.mresetz.set_bitflip(0.01)\n",
    "\n",
    "result = qsharp.run(\"BellPair()\", 1000, noise=noise, seed=42)\n",
    "Histogram(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f231a569",
   "metadata": {},
   "source": [
    "The histogram now shows some `[Zero, One]` and `[One, Zero]` outcomes from errors.\n",
    "Since the CNOT has 10× more noise than the H gate, most errors come from the two-qubit gate."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "443c3f27",
   "metadata": {},
   "source": [
    "### Arbitrary Pauli errors on a gate\n",
    "\n",
    "Each `NoiseTable` supports arbitrary Pauli strings. For example, on a CNOT gate we can\n",
    "set specific correlated errors using multi-character Pauli strings. Each character in the\n",
    "string corresponds to a qubit argument of the gate:\n",
    "\n",
    "| Pauli string | Meaning |\n",
    "|---|---|\n",
    "| `xi` | X error on control, identity on target |\n",
    "| `ix` | Identity on control, X error on target |\n",
    "| `xx` | Correlated X error on both qubits simultaneously |\n",
    "| `zz` | Correlated Z (dephasing) on both qubits |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6bbb9c77",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Model a CNOT gate with 5% correlated IX and 2% correlated ZI on both qubits.\n",
    "noise = NoiseConfig()\n",
    "noise.cx.ix = 0.05  # 5% probability of correlated IX error\n",
    "noise.cx.zi = 0.02  # 5% probability of correlated ZI error\n",
    "\n",
    "result = qsharp.run(\"BellPair()\", 1000, noise=noise, seed=42)\n",
    "Histogram(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aa361125",
   "metadata": {},
   "source": [
    "You can also set multiple Pauli error channels at once using `set_pauli_noise`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3bbbdbf0",
   "metadata": {},
   "outputs": [],
   "source": [
    "noise = NoiseConfig()\n",
    "noise.cx.set_pauli_noise([\n",
    "    (\"XI\", 0.01),   # 1% X error on control only\n",
    "    (\"IX\", 0.01),   # 1% X error on target only\n",
    "    (\"XX\", 0.005),  # 0.5% correlated XX\n",
    "    (\"ZZ\", 0.02),   # 2% correlated ZZ crosstalk\n",
    "])\n",
    "\n",
    "result = qsharp.run(\"BellPair()\", 1000, noise=noise, seed=42)\n",
    "Histogram(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "130889ea",
   "metadata": {},
   "source": [
    "## 2. Custom Noise Intrinsics\n",
    "\n",
    "For more complex noise models, you can define custom noise intrinsics. These\n",
    "are no-op gates in the ideal circuit that act as insertion points for correlated noise during\n",
    "simulation. This is useful for modeling correlated noise on custom gates (those not included in `NoiseConfig`).\n",
    "\n",
    "A noise intrinsic is declared with `@NoiseIntrinsic()` in Q# (or `@qdk.qir.noise_intrinsic`\n",
    "in OpenQASM). The noise model is then configured in Python via `NoiseConfig.intrinsic()`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adc6a923",
   "metadata": {},
   "source": [
    "### Q# example: modeling crosstalk between qubits\n",
    "\n",
    "Imagine a 3-qubit register where applying a gate on qubit 0 causes crosstalk\n",
    "(correlated ZZ dephasing) on qubits 1 and 2. We model this by inserting a noise\n",
    "intrinsic after the gate."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e0df5c17",
   "metadata": {},
   "outputs": [],
   "source": [
    "qdk.init(target_profile=qdk.TargetProfile.Adaptive_RIF)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "51340462",
   "metadata": {
    "vscode": {
     "languageId": "qsharp"
    }
   },
   "outputs": [],
   "source": [
    "%%qsharp\n",
    "\n",
    "// A noise intrinsic representing crosstalk on 3 qubits.\n",
    "// In the ideal circuit this is a no-op; the simulator injects\n",
    "// Pauli errors according to the NoiseConfig.\n",
    "@NoiseIntrinsic()\n",
    "operation Crosstalk3Q(q0: Qubit, q1: Qubit, q2: Qubit) : Unit {\n",
    "    body intrinsic;\n",
    "}\n",
    "\n",
    "// Prepare a GHZ state on 3 qubits, with crosstalk after each CNOT.\n",
    "operation GHZ() : Result[] {\n",
    "    use qs = Qubit[3];\n",
    "    H(qs[0]);\n",
    "    CNOT(qs[0], qs[1]);\n",
    "    Crosstalk3Q(qs[0], qs[1], qs[2]);  // crosstalk hits all 3 qubits\n",
    "    CNOT(qs[1], qs[2]);\n",
    "    Crosstalk3Q(qs[0], qs[1], qs[2]);  // crosstalk again\n",
    "    MResetEachZ(qs)\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce61b334",
   "metadata": {},
   "source": [
    "Compile to QIR so we can run with a custom `NoiseConfig`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1b211a19",
   "metadata": {},
   "outputs": [],
   "source": [
    "qir = qsharp.compile(\"GHZ()\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4e431ff4",
   "metadata": {},
   "source": [
    "First, run without noise, we should see a clean GHZ state (`000` and `111` only):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6b7ed62a",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = run_qir(qir, shots=1000, seed=42)\n",
    "Histogram(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3155ed57",
   "metadata": {},
   "source": [
    "Now configure the crosstalk noise intrinsic. We'll model two types of correlated errors:\n",
    "- `ixx`: correlated XX on qubits 1 and 2 (10% probability)\n",
    "- `xxi`: correlated XX on qubits 0 and 1 (5% probability)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "958335ff",
   "metadata": {},
   "outputs": [],
   "source": [
    "noise = NoiseConfig()\n",
    "table = noise.intrinsic(\"Crosstalk3Q\", num_qubits=3)\n",
    "table.ixx = 0.10  # 10% XX on qubits 1-2\n",
    "table.xxi = 0.05  # 5% XX on qubits 0-1\n",
    "\n",
    "result = run_qir(qir, shots=1000, noise=noise, seed=42)\n",
    "Histogram(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f18d9f40",
   "metadata": {},
   "source": [
    "The crosstalk breaks the perfect GHZ correlations, we now see outcomes like `001`, `010`,\n",
    "etc. that would be impossible in the ideal circuit."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d9cd8528",
   "metadata": {},
   "source": [
    "### OpenQASM example\n",
    "\n",
    "The same noise intrinsic pattern works in OpenQASM using the `@qdk.qir.noise_intrinsic`\n",
    "annotation on a custom gate definition."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8ec4beaa",
   "metadata": {},
   "outputs": [],
   "source": [
    "qasm_source = \"\"\"\n",
    "OPENQASM 3.0;\n",
    "include \"stdgates.inc\";\n",
    "\n",
    "// A noise intrinsic representing crosstalk on 3 qubits.\n",
    "// In the ideal circuit this is a no-op; the simulator injects\n",
    "// Pauli errors according to the NoiseConfig.\n",
    "@qdk.qir.noise_intrinsic\n",
    "gate crosstalk_3q q0, q1, q2 {}\n",
    "\n",
    "qubit[3] qs;\n",
    "\n",
    "// Prepare a GHZ state on 3 qubits, with crosstalk after each CNOT.\n",
    "h qs[0];\n",
    "cx qs[0], qs[1];\n",
    "crosstalk_3q qs[0], qs[1], qs[2];  // crosstalk hits all 3 qubits\n",
    "cx qs[1], qs[2];\n",
    "crosstalk_3q qs[0], qs[1], qs[2];  // crosstalk again\n",
    "\n",
    "bit[3] res = measure qs;\n",
    "\"\"\"\n",
    "\n",
    "qir_qasm = openqasm.compile(\n",
    "    qasm_source,\n",
    "    output_semantics=openqasm.OutputSemantics.OpenQasm,\n",
    "    target_profile=qdk.TargetProfile.Base,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a3d230ea",
   "metadata": {},
   "outputs": [],
   "source": [
    "noise = NoiseConfig()\n",
    "table = noise.intrinsic(\"crosstalk_3q\", num_qubits=3)\n",
    "table.ixx = 0.10  # 10% XX on qubits 1-2\n",
    "table.xxi = 0.05  # 5% XX on qubits 0-1\n",
    "\n",
    "result = run_qir(qir_qasm, shots=1000, noise=noise, seed=42)\n",
    "Histogram(result)"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
