{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "1a2b3c4d",
   "metadata": {},
   "source": [
    "# How to run a custom experiment with Boulder Opal\n",
    "\n",
    "**Define experiment parameters, execute individual quantum characterization tasks, and retrieve results**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1a2bbody",
   "metadata": {},
   "source": [
    "Boulder Opal Scale Up provides high level tools for automating the characterization and calibration of quantum processors. While automated routines handle most recurring calibration work, you may need to run a single experiment with specific parameters to investigate a particular behavior, reproduce a measurement, or validate a new configuration. Running an experiment on its own gives you direct access to the raw data and lets you choose exactly which measurement to perform and how.\n",
    "\n",
    "This user guide walks you through the full workflow of running a single experiment. You learn how to configure an experiment class with the parameters of interest, submit the experiment to the Boulder Opal Scale Up server, and retrieve the results for inspection. The example uses a Power Rabi experiment to calibrate the $\\pi$ pulse amplitude, but the same pattern applies to every experiment class in the library. For the complete list of available experiment classes and their configurable parameters, see the [experiments reference](https://docs.q-ctrl.com/boulder-opal/autocalibration/apply/how-to-run-an-automated-routine-with-boulder-opal).\n",
    "\n",
    "Before starting, you need access to a virtual device that has been created and populated with initial calibration data. If you have not done this yet, complete the [get started tutorial](https://docs.q-ctrl.com/boulder-opal/autocalibration/get-started-with-boulder-opal-autocalibration) first."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2b3c4d5e",
   "metadata": {},
   "source": [
    "## 1. Set the device context\n",
    "\n",
    "Select the target device. All experiments run against the currently active device."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3c4d5e6f",
   "metadata": {},
   "outputs": [],
   "source": [
    "await client.set_current_device(\"<your-device-name>\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4d5e6f7a",
   "metadata": {},
   "source": [
    "## 2. Define experiment parameters\n",
    "\n",
    "Each experiment is represented by a class that encodes what measurement to perform and exposes the parameters you can vary. The pattern shown below applies to every experiment class: import it, instantiate it with the parameters of interest, and pass the instance to the submission step in the next section. In this tutorial you configure a Power Rabi experiment, which sweeps the amplitude of a fixed duration drive pulse and measures the resulting excited state population of the qubit. The population oscillates with amplitude according to the Rabi formula, and fitting the oscillation reveals the amplitude that drives a $\\pi$ rotation.\n",
    "\n",
    "The `PowerRabi` class takes the target transmon, the set of amplitude scales to sweep over, and the number of shots to collect at each sweep point. The `LinspaceIterable` helper generates an evenly spaced sequence of amplitude values between the start and stop values you provide.\n",
    "\n",
    "Import the experiment class from `boulderopalscaleup.experiments` and the sweep iterable from `boulderopalscaleup.common`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5e6f7a8b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from boulderopalscaleup.common import LinspaceIterable\n",
    "from boulderopalscaleup.experiments import PowerRabi\n",
    "\n",
    "experiment = PowerRabi(\n",
    "    transmon=\"q0\",\n",
    "    scales=LinspaceIterable(start=0.0, stop=1.0, count=21),\n",
    "    shot_count=400,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6f7a8b9c",
   "metadata": {},
   "source": [
    "## 3. Submit the experiment\n",
    "\n",
    "The `run_experiment()` method serializes the experiment configuration and submits it to the Boulder Opal Scale Up server, which in turn schedules pulses on the active device. The call returns a job ID that you hold onto for monitoring and data retrieval. The experiment itself runs asynchronously, so you can submit other work or inspect other jobs while this one is in progress. \n",
    "\n",
    "The `client.run()` method accepts any of the three top level workflow objects, `Experiment`, `Routine`, or `Solution`, and is convenient when you want a single code path that dispatches whatever object the caller provides."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7a8b9c0d",
   "metadata": {},
   "outputs": [],
   "source": [
    "job_id = await client.run_experiment(experiment)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8b9c0d1e",
   "metadata": {},
   "source": [
    "## 4. Monitor and retrieve results\n",
    "\n",
    "Once the experiment is submitted, the `get_job_summary()` call returns the job's status, timing, and the configuration you submitted. Passing the summary to `client.display()` renders it as a formatted table that you can scan quickly.\n",
    "\n",
    "When the job finishes, `get_job_data()` returns the full result payload: the swept parameter values, the measured populations, and the fit that is performed automatically. Passing the data to `client.display()` renders the plots and the numeric fit summary, so you can see the Rabi oscillation and read off the amplitude identified as the $\\pi$ pulse."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9c0d1e2f",
   "metadata": {},
   "outputs": [],
   "source": [
    "job_summary = await client.get_job_summary(job_id)\n",
    "client.display(job_summary)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0d1e2f3a",
   "metadata": {},
   "outputs": [],
   "source": [
    "job_data = await client.get_job_data(job_id)\n",
    "client.display(job_data)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10cf9eff",
   "metadata": {},
   "source": [
    "## 5. Persist the calibrated parameters to the device\n",
    "\n",
    "Running experiments generally updates the virtual device with the newly calibrated values. If you want to disable updates, you can explicitly set this in the experiment definition with the `update=\"off\"` flag.\n",
    "\n",
    "For a complete description of the update workflow and how to review or revert changes, see [How to manually update device parameters](https://docs.q-ctrl.com/boulder-opal/autocalibration/apply/how-to-manually-update-device-parameters-with-boulder-opal)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e2f3a4b",
   "metadata": {},
   "source": [
    "## 6. Common experiment attributes\n",
    "\n",
    "Although each experiment has its own required parameters, most share a common set of attributes that control the target element, the sampling behavior, and how the device state is updated after the experiment completes. \n",
    "\n",
    "| Parameter               | Type              | Description                                                       |\n",
    "| ----------------------- | ----------------- | ----------------------------------------------------------------- |\n",
    "| `transmon`              | `str`             | Target transmon element ID (e.g. `\"q0\"`).                         |\n",
    "| `resonator`             | `str`             | Target resonator element ID, when required.                       |\n",
    "| `shot_count`            | `int`             | Number of measurement shots per sweep point.                      |\n",
    "| `recycle_delay_ns`      | `int`             | Delay in nanoseconds between shots for qubit reset.               |\n",
    "| `run_mixer_calibration` | `bool`            | Whether to run mixer calibration before the experiment.           |\n",
    "| `update`                | `str`             | Device update behavior: `\"auto\"`, `\"off\"`, or `\"prompt\"`.         |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2f3a4b5c",
   "metadata": {},
   "source": [
    "## Next steps\n",
    "\n",
    "* To automate multi step calibration workflows that chain several experiments together, see [How to run an automated routine](https://docs.q-ctrl.com/boulder-opal/autocalibration/apply/how-to-run-an-automated-routine-with-boulder-opal). \n",
    "* To measure coherence times with dedicated experiment classes, see [How to run coherence measurements](https://docs.q-ctrl.com/boulder-opal/autocalibration/apply/how-to-run-coherence-measurements-with-boulder-opal). \n",
    "* To retrieve historical results and compare them across runs, see [How to retrieve and display data](https://docs.q-ctrl.com/boulder-opal/autocalibration/apply/how-to-retrieve-and-display-data-with-boulder-opal).\n"
   ]
  }
 ],
 "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.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
