{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "aba0cc79",
   "metadata": {},
   "source": [
    "# How to debug a failed routine with Boulder Opal\n",
    "\n",
    "**Diagnose and resolve failed calibration by re-running experiments with adjusted parameters**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aba0body",
   "metadata": {},
   "source": [
    "A routine failure does not necessarily require a full re-run from scratch. Instead, lower-level experiments can be used to debug an automation error, allowing you to collect any missing data or components, before continuing with automations. This user guide assumes you have a valid Boulder Opal Scale Up client session and a completed (but failed or partially failed) routine execution. It walks through the workflow for isolating the specific experiment that failed, re-running it with adjusted parameters, and feeding the corrected result back into the device model.\n",
    "\n",
    "In this notebook you will:\n",
    "\n",
    "1. Retrieve and inspect results from a failed routine.\n",
    "2. Identify the experiment that caused the failure.\n",
    "3. Re-run that experiment with wider sweep ranges or more shots.\n",
    "4. Update the virtual device with the corrected value and resume calibration.\n",
    "\n",
    "You will need an authenticated client (see documentation specific to [Qblox](https://docs.q-ctrl.com/boulder-opal/autocalibration/discover/setting-up-boulder-opal-with-qblox-controllers) and [Quantum Machines](https://docs.q-ctrl.com/boulder-opal/autocalibration/discover/setting-up-boulder-opal-with-quantummachines-opx-controllers)) and a device configuration YAML file for your quantum processor. If you are starting from scratch, complete the [get started tutorial](https://docs.q-ctrl.com/boulder-opal/autocalibration/get-started-with-boulder-opal-autocalibration) first."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bc48798a",
   "metadata": {},
   "source": [
    "## 1. Inspect the routine result\n",
    "\n",
    "Every routine and experiment returns a job ID. Use `client.get_job_data()` to retrieve results, including error messages and diagnostic plots. This provides more detail than `client.get_job_summary()` and is the recommended starting point when diagnosing a failure."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ae9f9b78",
   "metadata": {},
   "source": [
    "The results show exactly what error occurred, and where it occurred in the routine process. This includes the error message and the affected component with specific experiment and data analysis context."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bd4df38e",
   "metadata": {},
   "outputs": [],
   "source": [
    "routine_job_id = \"<your-failed-job-id>\"\n",
    "\n",
    "job_data = await client.get_job_data(routine_job_id)\n",
    "client.display(job_data)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ca9710c3",
   "metadata": {},
   "source": [
    "## 2. Identify the corresponding experiment\n",
    "\n",
    "Each routine runs a collection of experiments. When an experiment fails, the Routine exits and returns the error message. You can use that information to re-run that specific experiment with customized parameters that the embedded state machine was unable to determine (such as wider sweep ranges or more shots). The table below maps each routine to its constituent experiments and lists common failure modes\n",
    "\n",
    "| Routine | Experiments it runs | Potential failure modes |\n",
    "| --- | --- | --- |\n",
    "| `ResonatorMapping` | `FeedlineDiscovery`, `ResonatorSpectroscopy`, `ResonatorSpectroscopyByBias`, `ResonatorSpectroscopyByPower` | Resonance not found — sweep range too narrow |\n",
    "| `TransmonDiscovery` | `TransmonSpectroscopy`, `PowerRabi`, `ReadoutClassifier` | Qubit frequency not found — sweep range or drive power wrong |\n",
    "| `OneQubitCalibration` | `PowerRabi`, `DragLeakageCalibration`, `FineAmplitudeCalibration`, `Ramsey`, `ReadoutClassifier` | Rabi fit failed — amplitude range too narrow |\n",
    "| `TransmonCoherence` | `T1`, `T2`, `T2Echo` | Exponential fit failed — delay range does not capture the decay |\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d2f1a83c",
   "metadata": {},
   "source": [
    "**Note:** The `JobData` object returned by `client.get_job_data()` includes the component name, failure reason, and any diagnostic data.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c8eecf67",
   "metadata": {},
   "source": [
    "## 3. Read the current device data\n",
    "\n",
    "Before running an experiment manually, check the current state of the device model. A routine may have partially succeeded and updated some parameters before the failing step, so the stored values may already reflect earlier successful calibrations. In this example below, we assume a Routine was run on Qubit 0, so we are checking those related components to verify their latest data. This would be replaced with the affected components for a given failed routine."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1e7893c4",
   "metadata": {},
   "outputs": [],
   "source": [
    "device = await client.get_device_data()\n",
    "await client.display_device_data_sheet()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5a3cf26e",
   "metadata": {},
   "outputs": [],
   "source": [
    "target_transmon = \"q0\"\n",
    "target_resonator = \"r0\"\n",
    "\n",
    "transmon_info = device.qpu.get_component(target_transmon)\n",
    "resonator_info = device.qpu.get_component(target_resonator)\n",
    "\n",
    "transmon_info"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aa58ba39",
   "metadata": {},
   "source": [
    "## 4. Example: re-run resonator spectroscopy\n",
    "\n",
    "If `ResonatorMapping` failed to find a resonance, you can run `ResonatorSpectroscopy` manually with a wider frequency range. Use `CWSIterable` to define a sweep centered on the last known frequency. Increasing the `width` parameter broadens the search window, and reducing the `step` parameter improves resolution.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0a52ba1d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from boulderopalscaleup.common import CWSIterable\n",
    "from boulderopalscaleup.experiments import ResonatorSpectroscopy\n",
    "\n",
    "await client.enable_qubits(target_transmon)\n",
    "\n",
    "frequencies = CWSIterable(\n",
    "    center=resonator_info.frequency_low.value, width=40e6, step=0.05e6\n",
    ")\n",
    "\n",
    "experiment = ResonatorSpectroscopy(\n",
    "    resonator=target_resonator, frequencies=frequencies, shot_count=200\n",
    ")\n",
    "\n",
    "resonator_spectroscopy_job_id = await client.run(experiment)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c7043134",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = await client.get_job_data(resonator_spectroscopy_job_id)\n",
    "client.display(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6878bdf7",
   "metadata": {},
   "source": [
    "## 5. Example: re-run transmon spectroscopy\n",
    "\n",
    "If `TransmonDiscovery` failed to locate the qubit frequency, you can run `TransmonSpectroscopy` with a wider sweep and a higher shot count to improve signal-to-noise.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "749ca5d9",
   "metadata": {},
   "outputs": [],
   "source": [
    "from boulderopalscaleup.experiments import TransmonSpectroscopy\n",
    "\n",
    "frequencies = CWSIterable(center=transmon_info.freq_01.value, width=100e6, step=0.1e6)\n",
    "\n",
    "experiment = TransmonSpectroscopy(\n",
    "    transmon=target_transmon, frequencies=frequencies, shot_count=800\n",
    ")\n",
    "\n",
    "transmon_spectroscopy_job_id = await client.run(experiment)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b66a3bf7",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = await client.get_job_data(transmon_spectroscopy_job_id)\n",
    "client.display(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7d78d536",
   "metadata": {},
   "source": [
    "## 6. Example: re-run power Rabi\n",
    "\n",
    "If `OneQubitCalibration` failed at the Rabi step, you can run `PowerRabi` with a broader amplitude scale range. Passing `scales=None` lets the system choose a default range, or you can specify one explicitly with `LinspaceIterable`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e1448e08",
   "metadata": {},
   "outputs": [],
   "source": [
    "from boulderopalscaleup.common import LinspaceIterable\n",
    "from boulderopalscaleup.experiments import PowerRabi\n",
    "\n",
    "experiment = PowerRabi(\n",
    "    transmon=target_transmon,\n",
    "    scales=LinspaceIterable(start=0.0, stop=2.0, count=101),\n",
    "    shot_count=800,\n",
    ")\n",
    "\n",
    "rabi_job_id = await client.run(experiment)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "da230e4c",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = await client.get_job_data(rabi_job_id)\n",
    "client.display(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4a2c0c68",
   "metadata": {},
   "source": [
    "## 7. Example: re-run T1\n",
    "\n",
    "If `TransmonCoherence` failed to fit T1, the delay range may not capture the full exponential decay. Use `GeomspaceIterable` to span a wider range of delay times, ensuring the measurement window extends well beyond the expected T1 value.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "202960ee",
   "metadata": {},
   "outputs": [],
   "source": [
    "from boulderopalscaleup.common import GeomspaceIterable\n",
    "from boulderopalscaleup.experiments import T1\n",
    "\n",
    "experiment = T1(\n",
    "    transmon=target_transmon,\n",
    "    delays_ns=GeomspaceIterable(start=16, stop=40_000, count=101),\n",
    "    shot_count=800,\n",
    ")\n",
    "\n",
    "t1_job_id = await client.run(experiment)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "06f13148",
   "metadata": {},
   "outputs": [],
   "source": [
    "result = await client.get_job_data(t1_job_id)\n",
    "client.display(result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea77f399",
   "metadata": {},
   "source": [
    "## 8. Update the virtual device and resume\n",
    "\n",
    "Once the individual experiment succeeds, you can manually update the device model with the corrected value and then re-run the full routine. This is useful when a routine fails partway through, as you can fix the blocking step and let the routine continue from an updated baseline. All `update_component` frequencies are in Hz and time constants in nanoseconds.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c1322ab5",
   "metadata": {},
   "outputs": [],
   "source": [
    "device = await client.get_device_data()\n",
    "\n",
    "device.qpu.update_component(component_id=target_transmon, freq_01=5.4e9)\n",
    "await client.update_device(device.qpu)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e4b7c2a1",
   "metadata": {},
   "source": [
    "Verify that the updated value is reflected in the device model before re-running the routine."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f8a3d6c9",
   "metadata": {},
   "outputs": [],
   "source": [
    "await client.display_device_data_sheet(node_name=target_transmon)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4f0b8ba5",
   "metadata": {},
   "outputs": [],
   "source": [
    "from boulderopalscaleup.routines import TransmonDiscovery\n",
    "\n",
    "retry_job_id = await client.run(TransmonDiscovery(transmon=[target_transmon]))\n",
    "\n",
    "retry_result = await client.get_job_data(retry_job_id)\n",
    "client.display(retry_result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1b2c3d4",
   "metadata": {},
   "source": [
    "## 9. Troubleshooting tips\n",
    "\n",
    "- `RuntimeError: No current device has been set` occurs when no device has been selected. Call `await client.set_current_device(\"<your-device-name>\")` before running experiments or routines.\n",
    "\n",
    "- `KeyError` when calling `qpu.get_component(component_id)` indicates that the `component_id` does not match a node in your device configuration (for example, `\"q0\"`, `\"r0\"`)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a47eb2a1",
   "metadata": {},
   "source": [
    "## 10. Next steps\n",
    "\n",
    "- [Experiments and routines reference](https://docs.q-ctrl.com/boulder-opal/autocalibration/apply/how-to-run-an-automated-routine-with-boulder-opal): Full list of experiment parameters.\n",
    "- [How to manually update device parameters](https://docs.q-ctrl.com/boulder-opal/autocalibration/apply/how-to-manually-update-device-parameters-with-boulder-opal): Update component\n",
    "  values or gate calibrations directly.\n",
    "- [Calibration routines: how they work](https://docs.q-ctrl.com/boulder-opal/autocalibration/discover/understanding-virtual-devices-in-boulder-opal): Deeper explanation of the routine\n",
    "  execution model.\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.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
