{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a1000001",
   "metadata": {},
   "source": [
    "# Wigner Tomography"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000002",
   "metadata": {},
   "source": [
    "## Prerequisites\n",
    "\n",
    "This guide assumes you have configured a `DeviceSetup` and created `BosonicQubit` objects with assigned parameters. Please see [our tutorials](https://docs.zhinst.com/labone_q_user_manual/applications_library/tutorials/index.html) if you need to create your setup and qubits for the first time.\n",
    "\n",
    "You can run this notebook on real hardware in the lab. If you don't have the hardware at your disposal, you can also run the notebook \"as is\" using an emulated session (see below).\n",
    "\n",
    "Before running the Wigner tomography experiment, the following calibrations must have been performed:\n",
    "\n",
    "- **Transmon spectroscopy**: the steady-state ancilla transmon resonance frequency `transmon_resonance_frequency_at_n0` must be known.\n",
    "- **Dispersive shift**: the dispersive shift `chi` between the memory cavity and the ancilla transmon must be measured.\n",
    "- **Displacement calibration**: the conversion factor `displacement_amp_per_unit_beta` must be known (see the [Displacement Calibration how-to guide](02_displacement_calibration.ipynb)).\n",
    "- **Selective transmon Rx calibration**: the parity mapping uses $R_y(\\pm\\pi/2)$ pulses on the $n=0$ transition, so the π/2-pulse amplitude `selective_transmon_X90_amplitudes[0]` must be calibrated (see the [Selective Transmon Rx Calibration how-to guide](03_selective_transmon_rx_calibration.ipynb)).\n",
    "- **Readout discrimination**: the readout must be calibrated for single-shot state discrimination, since the experiment uses `AcquisitionType.DISCRIMINATION` and extracts $P(|g\\rangle)$ from the averaged result.\n",
    "\n",
    "If you are just getting started with the LabOne Q Applications Library, please don't hesitate to reach out to us at info@zhinst.com."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000003",
   "metadata": {},
   "source": [
    "## Background\n",
    "\n",
    "In this how-to guide, you will perform **Wigner tomography** on the memory cavity of a bosonic qubit using the `wigner_tomography_demonstrator` experiment workflow in the LabOne Q Applications Library.\n",
    "\n",
    "### The Wigner function\n",
    "\n",
    "The Wigner function $W(\\beta)$ is a quasi-probability distribution in the phase space of a quantum harmonic oscillator. It provides a complete description of the oscillator's quantum state and is defined as:\n",
    "\n",
    "$$W(\\beta) = \\frac{2}{\\pi} \\operatorname{Tr}\\!\\left[\\hat{\\rho}\\,\\hat{D}(\\beta)\\,\\hat{\\Pi}\\,\\hat{D}^\\dagger(\\beta)\\right]$$\n",
    "\n",
    "where $\\hat{\\rho}$ is the cavity density matrix, $\\hat{D}(\\beta)$ is the displacement operator, and $\\hat{\\Pi} = (-1)^{\\hat{n}}$ is the photon-number parity operator. For a pure state, the Wigner function can take negative values — a signature of non-classical behaviour. The vacuum state has a Gaussian Wigner function centred at the origin, while Fock states and cat states exhibit characteristic interference fringes with negative regions.\n",
    "\n",
    "### Parity measurement via the ancilla transmon\n",
    "\n",
    "The photon-number parity is measured indirectly through the dispersively coupled ancilla transmon. The full protocol — implemented by the `wigner_point` quantum operation — is:\n",
    "\n",
    "1. **Displacement** $\\hat{D}(-\\beta)$ — the memory cavity is displaced by $-\\beta$, mapping the phase-space point $\\beta$ to the origin.\n",
    "2. **Parity mapping** — a Ramsey interferometry sequence maps the cavity photon-number parity onto the transmon population.\n",
    "3. **Readout** — the transmon is read out; the result encodes $\\langle \\hat{\\Pi} \\rangle$.\n",
    "\n",
    "#### How the parity mapping works\n",
    "\n",
    "The ancilla transmon and memory cavity are coupled dispersively. In the dispersive regime the interaction Hamiltonian is\n",
    "\n",
    "$$H_\\mathrm{disp} = \\chi\\, \\hat{a}^\\dagger \\hat{a}\\, |e\\rangle\\langle e|$$\n",
    "\n",
    "where $\\chi$ is the dispersive shift, $\\hat{a}^\\dagger \\hat{a} = \\hat{n}$ is the cavity photon-number operator, and $|e\\rangle\\langle e|$ projects onto the transmon excited state. The key consequence is that each cavity Fock state $|n\\rangle$ shifts the transmon transition frequency by $\\chi n$, causing the transmon to accumulate a photon-number-dependent phase during free evolution.\n",
    "\n",
    "The parity mapping sequence exploits this with a three-step Ramsey protocol:\n",
    "\n",
    "$$R_y(+\\pi/2) \\;\\longrightarrow\\; \\text{wait}\\left(\\frac{1}{2|\\chi|}\\right) \\;\\longrightarrow\\; R_y(-\\pi/2)$$\n",
    "\n",
    "Starting with the transmon in $|g\\rangle$ and the cavity in Fock state $|n\\rangle$:\n",
    "\n",
    "1. $R_y(+\\pi/2)$ rotates the transmon into the superposition $\\tfrac{1}{\\sqrt{2}}(|g\\rangle + |e\\rangle)$.\n",
    "2. During the wait time $t = 1/(2|\\chi|)$, the dispersive interaction causes the $|e\\rangle$ component to accumulate a phase $\\phi(n) = 2\\pi \\chi n\\, t = \\pi n$ relative to $|g\\rangle$. The joint state after the wait is\n",
    "$$\\frac{1}{\\sqrt{2}}\\bigl(|g\\rangle + e^{i\\pi n}|e\\rangle\\bigr)|n\\rangle.$$\n",
    "3. $R_y(-\\pi/2)$ converts the phase back into a population difference:\n",
    "   - **Even $n$**: $e^{i\\pi n} = +1$ — the transmon is returned to $|g\\rangle$.\n",
    "   - **Odd $n$**: $e^{i\\pi n} = -1$ — the transmon is mapped to $|e\\rangle$.\n",
    "\n",
    "For a general cavity state $\\hat{\\rho}$, each Fock component contributes with weight $\\langle n|\\hat{\\rho}|n\\rangle$, so the ground-state probability after the sequence is\n",
    "\n",
    "$$P(|g\\rangle) = \\sum_n \\langle n|\\hat{\\rho}|n\\rangle \\cdot \\frac{1 + (-1)^n}{2} = \\frac{1 + \\langle\\hat{\\Pi}\\rangle}{2}$$\n",
    "\n",
    "which gives the parity expectation value directly:\n",
    "\n",
    "$$\\langle\\hat{\\Pi}\\rangle = P(|g\\rangle) - P(|e\\rangle) = 2P(|g\\rangle) - 1$$\n",
    "\n",
    "The Wigner function is then recovered as:\n",
    "\n",
    "$$ W(\\beta) = \\frac{2}{\\pi} \\langle \\hat{\\Pi}\\rangle = \\frac{2}{\\pi} \\left(2 P(|g \\rangle) - 1\\right) $$\n",
    "\n",
    "where $\\langle \\hat{\\Pi} \\rangle = \\operatorname{Tr}\\!\\left[\\hat{\\rho}\\,\\hat{D}(\\beta)\\,\\hat{\\Pi}\\,\\hat{D}^\\dagger(\\beta)\\right]$ is the parity expectation value of the displaced cavity state, and $P(|g\\rangle)$ is the probability of finding the transmon in the ground state.\n",
    "\n",
    "<div class=\"admonition note\">\n",
    "<p class=\"admonition-title\"><b>Important</b></p>\n",
    "<div>\n",
    "\n",
    "**$R_y$ pulse bandwidth requirement.** The $R_y(\\pm\\pi/2)$ pulses must rotate the transmon equally for *every* photon-number component of the cavity state simultaneously. This requires the pulse to be spectrally broad compared with all occupied photon-number-shifted transitions. A drive at $f_\\mathrm{transmon}(0)$ has detuning $n|\\chi|$ from the $n$-photon transition, so the condition is:\n",
    "\n",
    "$$T_{R_y} \\ll \\frac{1}{N_\\mathrm{max}\\,|\\chi|}$$\n",
    "\n",
    "In practice this means the $R_y$ pulse must be **shorter** than the number-selective $R_x$ pulses used elsewhere in the tune-up sequence. Using the selective $R_x$ length directly for the parity mapping is acceptable only when this condition holds. For example, $|\\chi| = 1\\,\\mathrm{MHz}$ and $T_{R_y} = 80\\,\\mathrm{ns}$ give $T_{R_y} \\cdot |\\chi| = 0.08$, so the low-$n$ components are addressed well, but at $N_\\mathrm{max} = 5$ the product $T_{R_y} \\cdot N_\\mathrm{max} |\\chi| = 0.4$ is only marginally small, and the highest-$n$ components lose some parity contrast. On devices with larger $|\\chi|$ (typically $3$–$5\\,\\mathrm{MHz}$) the degradation is stronger. If you observe a systematic reduction of the Wigner function magnitude at large $|\\beta|$ that cannot be explained by decoherence, shortening `selective_transmon_Rx_length` (and increasing `selective_transmon_X90_amplitudes[0]` proportionally to maintain the $\\pi/2$ rotation angle) is the first corrective step to try.\n",
    "\n",
    "</div>\n",
    "</div>\n",
    "\n",
    "### Scan modes\n",
    "\n",
    "The workflow supports two scan modes:\n",
    "\n",
    "1. **1D line cut** (`beta_im_values=None`) — scans along the real axis with $\\mathrm{Im}(\\beta) = 0$. This is the fastest scan and is useful for quick diagnostics and verification of state preparation.\n",
    "\n",
    "2. **2D phase-space scan** (both `beta_re_values` and `beta_im_values` provided) — scans a full $\\mathrm{Re}(\\beta) \\times \\mathrm{Im}(\\beta)$ grid to produce a complete Wigner tomogram.\n",
    "\n",
    "### Pulse sequence\n",
    "\n",
    "The pulse sequence per sweep point is:\n",
    "\n",
    "$$\\text{qubit} \\;---\\; [D(-\\beta)] \\;---\\; [\\text{parity mapping}] \\;---\\; [\\text{measure}] \\;---\\; [\\text{passive reset}]$$"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000004",
   "metadata": {},
   "source": [
    "## Imports\n",
    "\n",
    "You'll start by importing `laboneq.simple`, as well as the `BosonicQubit` and `BosonicQubitOperations` classes from the bosonic qubits contribution."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000005",
   "metadata": {},
   "source": [
    "import copy\n",
    "\n",
    "import numpy as np\n",
    "from laboneq.simple import *\n",
    "\n",
    "from laboneq_applications.contrib.qpu_types.bosonic_qubits import (\n",
    "    BosonicQubit,\n",
    "    BosonicQubitOperations,\n",
    ")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000006",
   "metadata": {},
   "source": [
    "## Define your experimental setup\n",
    "\n",
    "Let's define our experimental setup. We will need:\n",
    "\n",
    "- a [DeviceSetup](https://docs.zhinst.com/labone_q_user_manual/core/functionality_and_concepts/00_device_setup/concepts/index.html)\n",
    "- one or more [BosonicQubits](https://docs.zhinst.com/labone_q_user_manual/applications_library/reference/contrib/index.html)\n",
    "- a set of `BosonicQubitOperations`\n",
    "- a [QPU](https://docs.zhinst.com/labone_q_user_manual/core/reference/dsl/quantum.html#laboneq.dsl.quantum.qpu.QPU)\n",
    "\n",
    "Here we will be brief and mainly provide the code to obtain these objects. For full details, see the tutorials on [defining your experimental setup](../../../tutorials/sources/getting_started.ipynb) and [logbooks and data saving](../../../tutorials/sources/logbooks.ipynb).\n",
    "\n",
    "We will use **2 bosonic qubits** in this guide. Change this number to match your setup."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000007",
   "metadata": {},
   "source": [
    "### Instrument serial numbers"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000008",
   "metadata": {},
   "source": [
    "# Replace with the serial number of your SHFQC instrument\n",
    "SHFQC_SERIAL = \"DEV12073\"\n",
    "\n",
    "DATASERVER_HOST = \"localhost\"\n",
    "DATASERVER_PORT = \"8004\"\n",
    "\n",
    "number_of_qubits = 2"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000009",
   "metadata": {},
   "source": [
    "### DeviceSetup\n",
    "\n",
    "The Wigner tomography experiment requires signal lines for the memory cavity drive, the number-selective transmon drives, and qubit readout. Here we use a single **SHFQC** instrument.\n",
    "\n",
    "Each bosonic qubit uses:\n",
    "- One **QA channel** pair (OUTPUT + INPUT) for readout\n",
    "- One **SG channel** for the memory cavity drive (`drive_memory`) — used to apply the displacement $D(-\\beta)$ at each phase-space point\n",
    "- One **SG channel** for all number-selective transmon drives (`drive_transmon_at_n0` … `drive_transmon_at_n{max_photon_number}`) — used during the parity mapping sequence\n",
    "- Optionally, one **SG channel** in LF mode for the SWAP drive (`swap_drive`) — not used in this experiment, but included so the same `DeviceSetup` can be reused across all bosonic qubit experiments\n",
    "\n",
    "If you already have a `DeviceSetup`, you can reuse it and skip this cell. Just make sure the signal line names match those expected by `BosonicQubit`."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000010",
   "metadata": {},
   "source": [
    "# Maximum photon number — determines how many selective transmon signal lines are created.\n",
    "MAX_PHOTON_NUMBER = 5\n",
    "\n",
    "device_setup = DeviceSetup(uid=\"my_bosonic_setup\")\n",
    "device_setup.add_dataserver(\n",
    "    uid=\"my_bosonic_setup\",\n",
    "    host=DATASERVER_HOST,\n",
    "    port=DATASERVER_PORT,\n",
    ")\n",
    "\n",
    "device_setup.add_instruments(\n",
    "    SHFQC(\n",
    "        uid=\"device_shfqc\",\n",
    "        address=SHFQC_SERIAL,\n",
    "        device_options=\"SHFQC/LRT/PLUS/QC6CH/RTR\",\n",
    "    ),\n",
    ")\n",
    "\n",
    "# ── Signal lines for qubit q0 ─────────────────────────────────────────\n",
    "device_setup.add_connections(\n",
    "    \"device_shfqc\",\n",
    "    # Readout (QA channel 0)\n",
    "    create_connection(to_signal=\"q0/measure\", ports=\"QACHANNELS/0/OUTPUT\"),\n",
    "    create_connection(to_signal=\"q0/acquire\", ports=\"QACHANNELS/0/INPUT\"),\n",
    "    # Memory cavity drive (SG channel 0) — displacement for Wigner tomography\n",
    "    create_connection(to_signal=\"q0/drive_memory\", ports=\"SGCHANNELS/0/OUTPUT\"),\n",
    "    # SWAP drive — not used here, included for setup reuse\n",
    "    create_connection(to_signal=\"q0/swap_drive\", ports=\"SGCHANNELS/4/OUTPUT\"),\n",
    ")\n",
    "# Number-selective transmon drive lines for q0 (all share SG channel 2)\n",
    "for n in range(MAX_PHOTON_NUMBER + 1):\n",
    "    device_setup.add_connections(\n",
    "        \"device_shfqc\",\n",
    "        create_connection(\n",
    "            to_signal=f\"q0/drive_transmon_at_n{n}\",\n",
    "            ports=\"SGCHANNELS/2/OUTPUT\",\n",
    "        ),\n",
    "    )\n",
    "\n",
    "# ── Signal lines for qubit q1 (optional) ─────────────────────────────\n",
    "if number_of_qubits == 2:\n",
    "    device_setup.add_connections(\n",
    "        \"device_shfqc\",\n",
    "        create_connection(to_signal=\"q1/measure\", ports=\"QACHANNELS/0/OUTPUT\"),\n",
    "        create_connection(to_signal=\"q1/acquire\", ports=\"QACHANNELS/0/INPUT\"),\n",
    "        create_connection(to_signal=\"q1/drive_memory\", ports=\"SGCHANNELS/1/OUTPUT\"),\n",
    "        create_connection(to_signal=\"q1/swap_drive\", ports=\"SGCHANNELS/5/OUTPUT\"),\n",
    "    )\n",
    "    for n in range(MAX_PHOTON_NUMBER + 1):\n",
    "        device_setup.add_connections(\n",
    "            \"device_shfqc\",\n",
    "            create_connection(\n",
    "                to_signal=f\"q1/drive_transmon_at_n{n}\",\n",
    "                ports=\"SGCHANNELS/3/OUTPUT\",\n",
    "            ),\n",
    "        )"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000011",
   "metadata": {},
   "source": [
    "### BosonicQubits\n",
    "\n",
    "We will create `BosonicQubit` objects from the logical signal groups in our `DeviceSetup` and assign qubit parameters. The qubit UID will match the name of the logical signal group (`q0`, `q1`, …).\n",
    "\n",
    "Adjust the parameter values to reflect the properties of your device. The parameters relevant to Wigner tomography are highlighted in the comments."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000012",
   "metadata": {},
   "source": [
    "# Qubit parameters — adjust these to match your device.\n",
    "# Parameters marked (*) are directly relevant to the Wigner tomography experiment.\n",
    "QUBIT_PARAMS = {\n",
    "    # ── Memory cavity ────────────────────────────────────────────────\n",
    "    \"memory_lo_frequency\": 5.8e9,\n",
    "    \"memory_resonance_frequency\": 6.0e9,\n",
    "    \"memory_drive_amplitude\": 0.5,\n",
    "    \"memory_drive_length\": 0.1e-6,  # (*) Displacement pulse duration\n",
    "    \"memory_drive_pulse\": {\n",
    "        \"function\": \"gaussian\",\n",
    "        \"sigma\": 0.25,\n",
    "    },  # (*) Displacement pulse shape\n",
    "    \"memory_spectroscopy_amplitude\": 0.5,\n",
    "    \"memory_spectroscopy_length\": 4e-6,\n",
    "    \"memory_drive_range\": 10,\n",
    "    # ── Ancilla transmon ─────────────────────────────────────────────\n",
    "    \"transmon_lo_frequency\": 4.8e9,\n",
    "    \"transmon_resonance_frequency_at_n0\": 5.0e9,  # (*) From transmon spectroscopy\n",
    "    \"transmon_drive_range\": 10,\n",
    "    # ── Selective transmon Rx drives (*) ─────────────────────────────\n",
    "    # These must be calibrated before running Wigner tomography.\n",
    "    # The Ry(±π/2) pulses in the parity mapping reuse selective_transmon_Rx_length.\n",
    "    # IMPORTANT: this length must satisfy T_Ry * N_max * |chi| << 1 so the Ry pulses\n",
    "    # are spectrally broad enough to address all occupied photon-number transitions\n",
    "    # simultaneously. For |chi| = 1 MHz and T_Ry = 80 ns: T_Ry * |chi| = 0.08, which\n",
    "    # gives 0.4 at N_max = 5 (marginal for the highest photon numbers).\n",
    "    # For larger |chi| (3-5 MHz typical), reduce this value and increase\n",
    "    # selective_transmon_X90_amplitudes[0] proportionally.\n",
    "    \"max_photon_number\": MAX_PHOTON_NUMBER,\n",
    "    \"selective_transmon_Rx_length\": 0.08e-6,  # (*) Must satisfy T * N_max * |chi| << 1 for parity mapping\n",
    "    \"selective_transmon_Rx_pulse\": {\"function\": \"gaussian\", \"sigma\": 0.25},\n",
    "    \"selective_transmon_X180_amplitudes\": [0.8]\n",
    "    * (MAX_PHOTON_NUMBER + 1),  # (*) From selective Rx calibration\n",
    "    \"selective_transmon_X90_amplitudes\": [0.4]\n",
    "    * (MAX_PHOTON_NUMBER + 1),  # (*) Used for Ry(π/2) in parity mapping\n",
    "    # ── SWAP drive ────────────────────────────────────────────────────\n",
    "    # This setup drives the SWAP line from an SG channel, which is put into\n",
    "    # LF mode with its local oscillator at 0 Hz. Leave this out for a SWAP\n",
    "    # line on a baseband channel, such as an HDAWG output.\n",
    "    \"swap_drive_on_sg_channel\": True,\n",
    "    # ── SNAP gate parameters ──────────────────────────────────────────\n",
    "    \"snap_drive_amp_per_n\": {},\n",
    "    \"snap_drive_phase_per_n\": {},\n",
    "    \"snap_drive_phase_slope_sign_per_n\": {},\n",
    "    # ── SWAP gate ─────────────────────────────────────────────────────\n",
    "    \"swap_length\": 500e-9,\n",
    "    \"swap_amplitude\": 0.1,\n",
    "    \"swap_pulse\": {\"function\": \"const\"},\n",
    "    \"swap_drive_range\": 5,\n",
    "    # ── Readout ───────────────────────────────────────────────────────\n",
    "    \"readout_lo_frequency\": 6.8e9,\n",
    "    \"readout_resonator_frequency\": 7.0e9,\n",
    "    \"readout_amplitude\": 0.5,\n",
    "    \"readout_length\": 1e-6,\n",
    "    \"readout_pulse\": {\"function\": \"const\"},\n",
    "    \"readout_integration_length\": 1e-6,\n",
    "    \"readout_integration_delay\": 20e-9,\n",
    "    # ── Reset ─────────────────────────────────────────────────────────\n",
    "    \"reset_delay_length\": 1e-6,\n",
    "    # ── Memory-transmon coupling (*) ──────────────────────────────────\n",
    "    \"chi\": -1e6,  # (*) Dispersive shift in Hz\n",
    "    # ── Displacement calibration (*) ─────────────────────────────────\n",
    "    \"displacement_amp_per_unit_beta\": 0.2,  # (*) Must be calibrated before this experiment\n",
    "}"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "code",
   "id": "a1000013",
   "metadata": {},
   "source": [
    "q0 = BosonicQubit.from_logical_signal_group(\n",
    "    uid=\"q0\",\n",
    "    lsg=device_setup.logical_signal_groups[\"q0\"],\n",
    "    parameters=copy.deepcopy(QUBIT_PARAMS),\n",
    ")\n",
    "qubits = [q0]\n",
    "\n",
    "if number_of_qubits == 2:\n",
    "    q1 = BosonicQubit.from_logical_signal_group(\n",
    "        uid=\"q1\",\n",
    "        lsg=device_setup.logical_signal_groups[\"q1\"],\n",
    "        parameters=copy.deepcopy(QUBIT_PARAMS),\n",
    "    )\n",
    "    qubits.append(q1)\n",
    "\n",
    "for q in qubits:\n",
    "    print(\"-------------\")\n",
    "    print(\"Qubit UID:\", q.uid)\n",
    "    print(\"Qubit logical signals:\")\n",
    "    for sig, lsg in q.signals.items():\n",
    "        print(f\"  {sig:<30} ('{lsg}')\")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000014",
   "metadata": {},
   "source": [
    "### Quantum Operations\n",
    "\n",
    "Create the set of `BosonicQubitOperations`:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000015",
   "metadata": {},
   "source": [
    "qops = BosonicQubitOperations()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000016",
   "metadata": {},
   "source": [
    "### QPU\n",
    "\n",
    "Create the `QPU` object from the qubits and the quantum operations:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000017",
   "metadata": {},
   "source": [
    "from laboneq.dsl.quantum import QPU\n",
    "\n",
    "qpu = QPU(qubits, quantum_operations=qops)"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000018",
   "metadata": {},
   "source": [
    "## Connect to Session"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000019",
   "metadata": {},
   "source": [
    "session = Session(device_setup)\n",
    "session.connect(do_emulation=True)  # set do_emulation=False when at a real setup"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000020",
   "metadata": {},
   "source": [
    "## Create a `FolderStore` for Saving Data\n",
    "\n",
    "The experiment `Workflows` can automatically save the inputs and outputs of all their tasks to the folder path we specify when instantiating the `FolderStore`. Here, we choose the current working directory."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000021",
   "metadata": {},
   "source": [
    "from pathlib import Path\n",
    "\n",
    "folder_store = workflow.logbook.FolderStore(Path.cwd() / \"data\")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "7a4e00c1",
   "metadata": {},
   "source": [
    "We disable saving in this guide. To enable it, simply run `folder_store.activate()`."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000022",
   "metadata": {},
   "source": [
    "folder_store.deactivate()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000023",
   "metadata": {},
   "source": [
    "## Optional: Configure the LoggingStore\n",
    "\n",
    "You can also activate/deactivate the `LoggingStore`, which controls the display of `Workflow` logging information in the notebook. See the [tutorial on Recording Experiment Workflow Results](../../../tutorials/sources/logbooks.ipynb) for details.\n",
    "\n",
    "Displaying the `Workflow` logging information is activated by default, but here we deactivate it to shorten the outputs, which are not very meaningful in emulation mode.\n",
    "\n",
    "**We recommend that you do not deactivate the Workflow logging in practice.**"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000024",
   "metadata": {},
   "source": [
    "from laboneq.workflow.logbook import LoggingStore\n",
    "\n",
    "logging_store = LoggingStore()\n",
    "logging_store.deactivate()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000025",
   "metadata": {},
   "source": [
    "## Running the Experiment Workflow\n",
    "\n",
    "You'll now instantiate the experiment workflow and run it. For more details on what experiment workflows are and what tasks they execute, see the [Experiment Workflows tutorial](../../../tutorials/sources/experiment_workflows.ipynb).\n",
    "\n",
    "Start by importing the `wigner_tomography_demonstrator` experiment workflow from the bosonic qubits contribution, as well as `plot_simulation` for inspecting the pulse sequence."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000026",
   "metadata": {},
   "source": [
    "from laboneq.contrib.example_helpers.plotting.plot_helpers import plot_simulation\n",
    "\n",
    "from laboneq_applications.contrib.experiments.bosonic_qubits import (\n",
    "    wigner_tomography_demonstrator,\n",
    ")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000027",
   "metadata": {},
   "source": [
    "### Configure the experiment options\n",
    "\n",
    "Let's create the options object for the Wigner tomography workflow and inspect it using `workflow.show_fields`:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000028",
   "metadata": {},
   "source": [
    "options = wigner_tomography_demonstrator.experiment_workflow.options()\n",
    "workflow.show_fields(options)"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000029",
   "metadata": {},
   "source": [
    "Notice that, unless we change it:\n",
    "\n",
    "- The experiment is run in `AcquisitionType.DISCRIMINATION` and `AveragingMode.CYCLIC`, using 1024 averages (`count`). The discrimination acquisition type is required because the Wigner function is extracted from averaged single-shot readout results $P(|g\\rangle)$.\n",
    "- The analysis workflow runs automatically after the experiment (`do_analysis=True`). It computes the parity $\\langle \\hat{\\Pi} \\rangle$ and Wigner function $W(\\beta)$ from the averaged readout signal, and generates the appropriate plots for 1D or 2D scans.\n",
    "- The figures produced by the analysis are automatically closed (`close_figures=True`)\n",
    "\n",
    "The `create_experiment` sub-options expose additional settings for advanced use:\n",
    "- `chunk_count`: number of chunks the real-time sweep is split into (default: 1). Increase it if a large 2D scan does not fit into the instrument memory in a single chunk.\n",
    "\n",
    "Here, let's reduce the number of averages for a quicker run in emulation mode, and disable closing the figures produced by the analysis so we see them in the cell output."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000030",
   "metadata": {},
   "source": [
    "options.count(1)\n",
    "options.close_figures(False)"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000031",
   "metadata": {},
   "source": [
    "## Mode 1: 1D Line Cut\n",
    "\n",
    "The 1D mode scans the Wigner function along the real axis ($\\mathrm{Im}(\\beta) = 0$). This is the fastest way to characterise the cavity state and is useful for quick diagnostics — for example, verifying that a displacement has prepared a coherent state, or that the vacuum state is present before an experiment.\n",
    "\n",
    "Pass `beta_re_values` and leave `beta_im_values` as `None` (the default) to select the 1D mode.\n",
    "\n",
    "### Define the phase-space scan range\n",
    "\n",
    "Choose a range that covers the expected extent of the cavity state in phase space. For a vacuum state, the Wigner function is a Gaussian of width $\\sim 0.5$ in phase-space units. For a coherent state $|\\beta_0\\rangle$, the peak is centred at $\\mathrm{Re}(\\beta_0)$. A range of $[-4, 4]$ is appropriate for states with mean photon numbers up to $\\bar{n} \\approx 10$."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000032",
   "metadata": {},
   "source": [
    "qubit = qpu.quantum_elements[0]\n",
    "\n",
    "# 1D phase-space scan along the real axis.\n",
    "beta_re_q0 = np.linspace(-4, 4, 41)\n",
    "\n",
    "print(\n",
    "    f\"1D scan: Re(beta) from {beta_re_q0[0]:.1f} to {beta_re_q0[-1]:.1f} \"\n",
    "    f\"({len(beta_re_q0)} points)\"\n",
    ")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "code",
   "id": "a1000033",
   "metadata": {},
   "source": [
    "# Run the 1D line cut on a single qubit.\n",
    "# beta_im_values defaults to None, selecting the 1D mode.\n",
    "exp_workflow_1d = wigner_tomography_demonstrator.experiment_workflow(\n",
    "    session=session,\n",
    "    qpu=qpu,\n",
    "    qubits=[qubit.uid],\n",
    "    beta_re_values=[beta_re_q0],\n",
    "    options=options,\n",
    ")\n",
    "\n",
    "workflow_results_1d = exp_workflow_1d.run()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000034",
   "metadata": {},
   "source": [
    "### Inspect the Tasks That Were Run\n",
    "\n",
    "The Wigner tomography workflow executes five steps in sequence: `compute_sweep_parameters`, `create_experiment`, `compile_experiment`, `run_experiment`, and (when `do_analysis=True`) the `analysis_workflow`, which runs the `calculate_wigner_function` and plotting tasks and returns the Wigner function data."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000035",
   "metadata": {},
   "source": [
    "for t in workflow_results_1d.tasks:\n",
    "    print(t)"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000036",
   "metadata": {},
   "source": [
    "### Inspect the Output Simulation\n",
    "\n",
    "You can inspect the compiled experiment and plot the simulated output to verify that the pulse sequence looks as expected. The Wigner tomography sequence consists of a displacement pulse on the `drive_memory` line (applying $D(-\\beta)$), followed by the parity mapping sequence on the number-selective transmon drive lines, and a readout pulse.\n",
    "\n",
    "The parity mapping contains three steps: $R_y(+\\pi/2)$, a free-evolution wait of $1/(2|\\chi|)$, and $R_y(-\\pi/2)$, so\n",
    "\n",
    "$$t_\\mathrm{parity\\,mapping} = 2\\,t_{R_y} + \\frac{1}{2|\\chi|}$$\n",
    "\n",
    "The total duration of one real-time repetition is approximately:\n",
    "\n",
    "$$T \\approx t_\\mathrm{displacement} + 2\\,t_{R_y} + \\frac{1}{2|\\chi|} + t_\\mathrm{readout} + t_\\mathrm{reset}$$"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000037",
   "metadata": {},
   "source": [
    "q = qpu.quantum_elements[0]\n",
    "total_time = (\n",
    "    q.parameters.memory_drive_length\n",
    "    + 2 * q.parameters.selective_transmon_Rx_length  # two Ry(±π/2) pulses\n",
    "    + 1 / (2 * abs(q.parameters.chi))  # Ramsey wait time\n",
    "    + q.parameters.readout_length\n",
    "    + q.parameters.reset_delay_length\n",
    ")\n",
    "\n",
    "compiled_experiment_1d = workflow_results_1d.tasks[\"compile_experiment\"].output\n",
    "plot_simulation(\n",
    "    compiled_experiment_1d,\n",
    "    length=total_time * 1.05,\n",
    ")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000038",
   "metadata": {},
   "source": [
    "### Inspect the Source Code of the Pulse-Sequence Creation Task\n",
    "\n",
    "You can inspect the source code of the `create_experiment` task to see exactly how the pulse sequence is constructed using quantum operations. To learn more about quantum operations, see the [Quantum Operations tutorial](../../../tutorials/sources/quantum_operations.ipynb)."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000039",
   "metadata": {},
   "source": [
    "wigner_tomography_demonstrator.create_experiment.src"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000040",
   "metadata": {},
   "source": [
    "### Analyse the Results (1D)\n",
    "\n",
    "Because `do_analysis=True` by default, the analysis workflow ran automatically after the experiment. For the 1D scan it performed the following steps:\n",
    "\n",
    "1. Retrieved the averaged discrimination result from the handle `{qubit_uid}/result`.\n",
    "2. Interpreted the averaged discrimination result as $P(|e\\rangle)$ and computed $P(|g\\rangle) = 1 - P(|e\\rangle)$.\n",
    "3. Computed the parity expectation value $\\langle \\hat{\\Pi} \\rangle = 2 P(|g\\rangle) - 1$.\n",
    "4. Computed the Wigner function $W(\\beta) = (2/\\pi) \\langle \\hat{\\Pi} \\rangle$.\n",
    "\n",
    "One plot is generated automatically for the 1D scan:\n",
    "- **Left panel** — the Wigner function $W(\\beta)$ versus $\\mathrm{Re}(\\beta)$, with a filled-area trace.\n",
    "- **Right panel** — the intermediate quantities $P(|g\\rangle)$ and $\\langle \\hat{\\Pi} \\rangle$ versus $\\mathrm{Re}(\\beta)$, for diagnostic purposes."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000041",
   "metadata": {},
   "source": [
    "### Access the Analysis Outputs"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000042",
   "metadata": {},
   "source": [
    "# Access the Wigner function data returned by the analysis workflow.\n",
    "analysis_result_1d = workflow_results_1d.tasks[\"analysis_workflow\"].output\n",
    "\n",
    "q_uid = qpu.quantum_elements[0].uid\n",
    "q_result_1d = analysis_result_1d[q_uid]\n",
    "\n",
    "# Keys available in the per-qubit result dict:\n",
    "print(\"Available result keys:\", list(q_result_1d.keys()))"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000043",
   "metadata": {},
   "source": [
    "The per-qubit result dictionary contains:\n",
    "\n",
    "| Key | Description |\n",
    "|-----|-------------|\n",
    "| `\"w\"` | 1D array of Wigner function values $W(\\beta)$, shape `(n_re,)` |\n",
    "| `\"parity\"` | 1D array of parity expectation values $\\langle \\hat{\\Pi} \\rangle$, shape `(n_re,)` |\n",
    "| `\"pg\"` | 1D array of ground-state probabilities $P(|g\\rangle)$, shape `(n_re,)` |\n",
    "| `\"beta_re\"` | 1D array of $\\mathrm{Re}(\\beta)$ scan values |\n",
    "| `\"beta_im\"` | `None` for a 1D scan |\n",
    "| `\"is_2d\"` | `False` for a 1D scan |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000044",
   "metadata": {},
   "source": [
    "### Inspect the Wigner Function Values"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000045",
   "metadata": {},
   "source": [
    "w_1d = q_result_1d[\"w\"]\n",
    "beta_re = q_result_1d[\"beta_re\"]\n",
    "\n",
    "w_max_theory = 2.0 / np.pi\n",
    "\n",
    "print(f\"Qubit {q_uid} (1D scan):\")\n",
    "print(f\"  Number of scan points : {len(w_1d)}\")\n",
    "print(f\"  W(beta) range         : [{w_1d.min():.4f}, {w_1d.max():.4f}]\")\n",
    "print(\n",
    "    f\"  Theoretical maximum   : {w_max_theory:.4f}  (2/pi, achieved at origin for vacuum)\"\n",
    ")\n",
    "print(f\"  W at Re(beta) = 0     : {w_1d[np.argmin(np.abs(beta_re))]:.4f}\")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000048",
   "metadata": {},
   "source": [
    "## Mode 2: 2D Phase-Space Scan\n",
    "\n",
    "The 2D mode scans the full $\\mathrm{Re}(\\beta) \\times \\mathrm{Im}(\\beta)$ plane to produce a complete Wigner tomogram. Pass both `beta_re_values` and `beta_im_values` to select this mode.\n",
    "\n",
    "The sweep is constructed as a Cartesian grid: for each qubit, `compute_sweep_parameters` builds a meshgrid from the two 1D arrays, converts each grid point to polar coordinates $(|\\beta|, \\arg\\beta)$, and flattens the result into a single sweep array. The hardware then sweeps all $(|\\beta|, \\arg\\beta)$ pairs in a single real-time loop.\n",
    "\n",
    "### Define the phase-space grid\n",
    "\n",
    "For a symmetric scan of the vacuum or a low-amplitude coherent state, a grid of $[-3, 3] \\times [-3, 3]$ with 31 points per axis is a reasonable starting point. For higher-photon-number states, extend the range accordingly."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000049",
   "metadata": {},
   "source": [
    "qubit = qpu.quantum_elements[0]\n",
    "\n",
    "# 2D phase-space grid.\n",
    "beta_re_2d_q0 = np.linspace(-3, 3, 31)\n",
    "beta_im_2d_q0 = np.linspace(-3, 3, 31)\n",
    "\n",
    "print(\n",
    "    f\"2D scan grid: {len(beta_re_2d_q0)} x {len(beta_im_2d_q0)} = \"\n",
    "    f\"{len(beta_re_2d_q0) * len(beta_im_2d_q0)} phase-space points\"\n",
    ")\n",
    "print(f\"Re(beta): {beta_re_2d_q0[0]:.1f} to {beta_re_2d_q0[-1]:.1f}\")\n",
    "print(f\"Im(beta): {beta_im_2d_q0[0]:.1f} to {beta_im_2d_q0[-1]:.1f}\")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "code",
   "id": "a1000050",
   "metadata": {},
   "source": [
    "# activate auto-chunking\n",
    "options.chunk_count(0)\n",
    "\n",
    "# Run the 2D scan on a single qubit.\n",
    "exp_workflow_2d = wigner_tomography_demonstrator.experiment_workflow(\n",
    "    session=session,\n",
    "    qpu=qpu,\n",
    "    qubits=[qubit.uid],\n",
    "    beta_re_values=[beta_re_2d_q0],\n",
    "    beta_im_values=[beta_im_2d_q0],\n",
    "    options=options,\n",
    ")\n",
    "\n",
    "workflow_results_2d = exp_workflow_2d.run()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000051",
   "metadata": {},
   "source": [
    "### Analyse the Results (2D)\n",
    "\n",
    "For the 2D scan, the analysis reshapes the flattened sweep result back into the $\\mathrm{Re}(\\beta) \\times \\mathrm{Im}(\\beta)$ grid before computing the Wigner function. Two plots are generated automatically:\n",
    "\n",
    "- **Wigner function color map** — the full phase-space tomogram $W(\\beta)$ displayed as a `pcolormesh` with a symmetric red–blue colour scale, clipped at the theoretical maximum $2/\\pi$.\n",
    "- **Line cuts** — two panels showing cuts through the origin: $W(\\mathrm{Re}(\\beta))$ at $\\mathrm{Im}(\\beta) \\approx 0$ (left) and $W(\\mathrm{Im}(\\beta))$ at $\\mathrm{Re}(\\beta) \\approx 0$ (right)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000052",
   "metadata": {},
   "source": [
    "### Access the Analysis Outputs"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000053",
   "metadata": {},
   "source": [
    "analysis_result_2d = workflow_results_2d.tasks[\"analysis_workflow\"].output\n",
    "\n",
    "q_result_2d = analysis_result_2d[q_uid]\n",
    "print(\"Available result keys:\", list(q_result_2d.keys()))"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000054",
   "metadata": {},
   "source": [
    "The per-qubit result dictionary for a 2D scan contains:\n",
    "\n",
    "| Key | Description |\n",
    "|-----|-------------|\n",
    "| `\"w\"` | 2D array of Wigner function values, shape `(n_re, n_im)` |\n",
    "| `\"parity\"` | 2D array of parity expectation values, shape `(n_re, n_im)` |\n",
    "| `\"pg\"` | 2D array of ground-state probabilities, shape `(n_re, n_im)` |\n",
    "| `\"beta_re\"` | 1D array of $\\mathrm{Re}(\\beta)$ grid values |\n",
    "| `\"beta_im\"` | 1D array of $\\mathrm{Im}(\\beta)$ grid values |\n",
    "| `\"is_2d\"` | `True` for a 2D scan |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000055",
   "metadata": {},
   "source": [
    "### Inspect the 2D Wigner Function"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000056",
   "metadata": {},
   "source": [
    "w_2d = q_result_2d[\"w\"]\n",
    "\n",
    "print(f\"Qubit {q_uid} (2D scan):\")\n",
    "print(f\"  Wigner function array shape : {w_2d.shape}  (n_re x n_im)\")\n",
    "print(f\"  W(beta) range               : [{w_2d.min():.4f}, {w_2d.max():.4f}]\")\n",
    "print(f\"  Theoretical maximum         : {2.0 / np.pi:.4f}  (2/pi)\")\n",
    "print(\n",
    "    f\"  W at origin (0, 0)          : {w_2d[len(beta_re_2d_q0) // 2, len(beta_im_2d_q0) // 2]:.4f}\"\n",
    ")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000060",
   "metadata": {},
   "source": [
    "## Parallel Wigner Tomography\n",
    "\n",
    "The `wigner_tomography_demonstrator` workflow natively supports running multiple qubits in parallel. When you pass a list of qubit UIDs together with corresponding lists of phase-space arrays, the pulse sequences for all qubits are compiled into a single experiment and executed simultaneously on the hardware.\n",
    "\n",
    "All qubits must produce the same total number of flattened sweep points, because the hardware sweeps them in lock-step. The simplest way to ensure this is to use the same grid shape for every qubit; the actual Re(β) and Im(β) values may differ between qubits.\n",
    "\n",
    "Here we demonstrate a parallel 2D scan on both qubits, using the same grid:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000061",
   "metadata": {},
   "source": [
    "q0 = qpu.quantum_elements[0]\n",
    "q1 = qpu.quantum_elements[1]\n",
    "\n",
    "# Each qubit can have its own scan range, but both grids must have the same shape.\n",
    "beta_re_q0 = np.linspace(-3, 3, 31)\n",
    "beta_im_q0 = np.linspace(-3, 3, 31)\n",
    "\n",
    "beta_re_q1 = np.linspace(-3, 3, 31)\n",
    "beta_im_q1 = np.linspace(-3, 3, 31)\n",
    "\n",
    "print(\n",
    "    f\"Grid shape: {len(beta_re_q0)} x {len(beta_im_q0)} = \"\n",
    "    f\"{len(beta_re_q0) * len(beta_im_q0)} sweep points per qubit\"\n",
    ")\n",
    "\n",
    "exp_workflow_2q = wigner_tomography_demonstrator.experiment_workflow(\n",
    "    session=session,\n",
    "    qpu=qpu,\n",
    "    qubits=[q0.uid, q1.uid],\n",
    "    beta_re_values=[beta_re_q0, beta_re_q1],\n",
    "    beta_im_values=[beta_im_q0, beta_im_q1],\n",
    "    options=options,\n",
    ")\n",
    "\n",
    "workflow_results_2q = exp_workflow_2q.run()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000062",
   "metadata": {},
   "source": [
    "### Access the Analysis Results for Both Qubits"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000063",
   "metadata": {},
   "source": [
    "analysis_result_2q = workflow_results_2q.tasks[\"analysis_workflow\"].output\n",
    "\n",
    "for q in [q0, q1]:\n",
    "    res = analysis_result_2q[q.uid]\n",
    "    w = res[\"w\"]\n",
    "    print(\n",
    "        f\"{q.uid}: W(beta) shape = {w.shape}, \"\n",
    "        f\"range = [{w.min():.4f}, {w.max():.4f}], \"\n",
    "        f\"W at origin = {w[len(beta_re_q0) // 2, len(beta_im_q0) // 2]:.4f}\"\n",
    "    )"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000064",
   "metadata": {},
   "source": [
    "## Advanced: Overriding the Color Scale\n",
    "\n",
    "In practice, decoherence and readout infidelity reduce the contrast of the measured Wigner function. The theoretical maximum $2/\\pi \\approx 0.6366$ is rarely achieved experimentally. To improve the visual contrast of the plot, you can override the color scale using the `w_max_override` analysis option.\n",
    "\n",
    "This is an option of the Wigner function plotting tasks (`PlotWignerFunctionOptions`), which you can set from the workflow options object:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000065",
   "metadata": {},
   "source": [
    "# Override the colour scale to match the actual experimental contrast.\n",
    "# Here we assume the measured Wigner function peaks at ~0.1 due to decoherence.\n",
    "options_scaled = wigner_tomography_demonstrator.experiment_workflow.options()\n",
    "options_scaled.count(1)\n",
    "options_scaled.close_figures(False)\n",
    "options_scaled.w_max_override(0.1)\n",
    "options_scaled.chunk_count(0)\n",
    "\n",
    "exp_workflow_scaled = wigner_tomography_demonstrator.experiment_workflow(\n",
    "    session=session,\n",
    "    qpu=qpu,\n",
    "    qubits=[qubit.uid],\n",
    "    beta_re_values=[beta_re_2d_q0],\n",
    "    beta_im_values=[beta_im_2d_q0],\n",
    "    options=options_scaled,\n",
    ")\n",
    "\n",
    "workflow_results_scaled = exp_workflow_scaled.run()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000070",
   "metadata": {},
   "source": [
    "Great! You have now run Wigner tomography on your bosonic qubit's memory cavity.\n",
    "\n",
    "The Wigner function encodes the complete quantum state of the cavity in phase space. Key signatures to look for:\n",
    "\n",
    "- **Vacuum state** $|0\\rangle$: a Gaussian peak centred at the origin with $W(0) = 2/\\pi$.\n",
    "- **Coherent state** $|\\beta_0\\rangle$: a Gaussian peak displaced from the origin to position $\\beta_0$ in phase space.\n",
    "- **Fock state** $|n\\rangle$: a circularly symmetric pattern with $n$ rings of oscillating sign. $W(0) = (2/\\pi)(-1)^n$, which is negative for odd-photon Fock states — a direct signature of non-classicality.\n",
    "- **Cat states**: interference fringes between two displaced Gaussian peaks, with alternating positive and negative regions at the midpoint.\n",
    "\n",
    "For a comparison with theoretical predictions and further analysis of specific cavity states, see also the [SNAP Gate Calibration how-to guide](06_snap_calibration.ipynb), which calibrates the gates used to prepare non-classical cavity states."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
