{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# How to interrupt/stop/abort a running plan & recover to safe settings\n", "\n", "- [Terminate or Interrupt a run](#terminate-or-interrupt)\n", "- [Handle Exceptions](#handle-exceptions)\n", "- [Recover to Safe Settings](#recover-safe-settings)\n", "- [Summary](#summary)\n", "\n", "In this notebook, we'll show how to do each of these with a simple plan that does these steps:\n", "\n", "1. opens a simulated shutter\n", "2. reads an EPICS PV\n", "3. closes the shutter\n", "\n", "Then, we'll also show how to [suspend the RunEngine](#suspending-the-runengine) for a typical case when the shutters close during a run and then resume the plan once the shutters re-open. Finally, we'll show a plan to [abort the RunEngine completely](#abort-the-runengine-from-a-plan) and return it to _idle_ state." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Setup just enough of bluesky and ophyd for this notebook." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from apstools.devices import SimulatedApsPssShutterWithStatus\n", "from bluesky import plans as bp\n", "from bluesky import RunEngine\n", "import databroker\n", "from ophyd import EpicsSignal\n", "\n", "cat = databroker.temp().v2\n", "RE = RunEngine()\n", "RE.subscribe(cat.v1.insert)\n", "\n", "IOC = \"gp:\"\n", "detector = EpicsSignal(f\"{IOC}userCalc8\", name=\"detector\")\n", "detector.wait_for_connection()\n", "shutter = SimulatedApsPssShutterWithStatus(name=\"shutter\")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Terminate or Interrupt\n", "\n", "**Q**: Once a run has been started, how can it be stopped before it finishes its planned sequence of actions?\n", "\n", "### Interactive Interruption\n", "\n", "Bluesky implements [interactive plan interruption](https://blueskyproject.io/bluesky/state-machine.html#pausing-interactively) with the _Control C_ (`^C`) keyboard combination. From the [docs](https://blueskyproject.io/bluesky/state-machine.html#interactive-pause-summary):\n", "\n", "keyboard | outcome\n", "--- | ---\n", "`^C` | pause soon\n", "`^C ^C` | pause now\n", "\n", "These will cause the `RE` to pause (enter the paused state) which allows the user to decide, interactively, what to do next:\n", "\n", "command | outcome\n", "--- | ---\n", "`RE.resume()` | Safely resume plan.\n", "`RE.abort()` | Perform cleanup. Mark as aborted.\n", "`RE.stop()` | Perform cleanup. Mark as success.\n", "`RE.halt()` | Do not perform cleanup — just stop.\n", "`RE.state` | Show the RunEngine state. Check if ‘paused’ or ‘idle’.\n", "\n", "\n", "If the RE is not responding to `^C`, if you hit it 10 times rapidly it will return the prompt, however the `RE` will be\n", "left in an invalid state with no graceful recovery possible. This should be used as a last resort. If the `RE` has become hung\n", "that is likly a bug in one of the ophyd objects methods or the plan.\n", "\n", "### Programmatic Interruption\n", "\n", "There are two ways for a program to interrupt the `RunEngine`: [suspenders](#suspenders) and [exceptions](#exceptions).\n", "\n", "#### Suspenders\n", "\n", "[Suspenders](https://blueskyproject.io/bluesky/state-machine.html#automated-suspension) pause the RunEngine (without asking the user for interaction) while some condition has changed (shutter closed, beam dumped, water flow is low, ...). The RunEngine continues to monitor and will resume automatically when the condition returns to normal.\n", "\n", "#### Exceptions\n", "\n", "An unhandled Python [Exception](https://docs.python.org/3/library/exceptions.html) will terminate a plan run by `bluesky.RunEngine`.\n", "\n", "Next, we create `MyException`, a custom exception, and `my_plan`, a bluesky plan that can raise this custom exception. If `MyException` is raised, then `bp.count` will not be run and `After count` will not be printed.\n", "\n", "**More Reading**\n", "\n", "- https://www.sitepoint.com/python-exception-handling/" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class MyException(Exception):\n", " \"\"\"Our custom exception.\"\"\"" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Make a simple plan that acquires data from a detector. Add diagnostic print statements to show progress through each action of the plan. The detector is an EPICS analog value. We will ignore the value of that detector to focus on the details of how to interrupt a plan." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "from bluesky import plan_stubs as bps\n", "def my_plan(terminate=False):\n", " print(f\"Start my_plan(), with {terminate=}.\")\n", " yield from bps.mv(shutter, \"open\")\n", " if terminate:\n", " print(\"By request, the plan will terminate.\")\n", " raise MyException(f\"Requested {terminate=}\")\n", "\n", " print(f\"Before count(), {shutter.state=}.\")\n", " yield from bp.count([detector])\n", "\n", " yield from bps.mv(shutter, \"close\")\n", " print(f\"After my_plan(), {shutter.state=}.\")" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Start my_plan(), with terminate=False.\n", "Before count(), shutter.state='open'.\n", "After my_plan(), shutter.state='close'.\n" ] }, { "data": { "text/plain": [ "('1dbae8de-1ac2-4fbb-9fcc-e7340d98e0a7',)" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "RE(my_plan())" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "When the plan is run with `terminate=True` (the plan _will_ raise the exception), then execution of `my_plan()` stops before `bp.count`.\n", "\n", "
\n",
    "In: RE(my_plan(True))\n",
    "Out:\n",
    "Start my_plan(), with terminate=True.\n",
    "By request, the plan will terminate.\n",
    "\n",
    "MyException: Requested terminate=True\n",
    "
\n", "\n", "To keep this notebook running, we _wrap_ (see caution below) our call to the `RunEngine` here with a `try..except` clause. The clause intercepts the exception so that it does not stop Python with an error.\n", "\n", "**Caution**: Wrapping the `RunEngine` with `try..except` is not considered best practice since it aborts `RE` completely, subverting many features already built into the `RunEngine` (such as `^C ^C` described above). It is recommended to **_wrap the plan_** with `try..except` rather than wrap the `RE`, as will be shown in the examples below." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Start my_plan(), with terminate=True.\n", "By request, the plan will terminate.\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "Run aborted\n", "Traceback (most recent call last):\n", " File \"/home/prjemian/Documents/projects/Bluesky/bluesky/src/bluesky/run_engine.py\", line 1605, in _run\n", " msg = self._plan_stack[-1].send(resp)\n", " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", " File \"/tmp/ipykernel_194270/1968779660.py\", line 7, in my_plan\n", " raise MyException(f\"Requested {terminate=}\")\n", "MyException: Requested terminate=True\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Found an exception: Requested terminate=True shutter.state='open'\n" ] } ], "source": [ "try: # caution: not recommended\n", " RE(my_plan(True))\n", "except Exception as exinfo:\n", " print(f\"Found an exception: {exinfo} {shutter.state=}\")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Notice that Python kept running because of the `try..except` clause, even after reporting the exception." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Handle Exceptions\n", "\n", "**Q**: How to handle Python Exceptions?\n", "\n", "As shown above, _exceptions_ are Python's way of interrupting program execution when some condition has been detected. Consider this simplification of our bluesky plan:\n", "\n", "```py\n", "def my_plan():\n", " yield from bps.mv(shutter, \"open\")\n", " yield from bp.count([detector])\n", " yield from bps.mv(shutter, \"close\")\n", "```\n", "\n", "If an exception is raised (for whatever reason) when opening the shutter or counting, the call to close the shutter will not happen and the shutter will remain open.\n", "\n", "There are many types of [exceptions](https://docs.python.org/3/library/exceptions.html); it is even possible to create your own. These exception types describe the type of condition that interrupted program flow. Python has statements to handle exceptions, as described in the next section." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### Python's `try..except..else..finally` clause\n", "\n", "Consider this `problematic()` function which will raise an exception:\n", "\n", "```py\n", "def problematic():\n", " raise RuntimeException(\"example of raising an exception\")\n", "```\n", "\n", "We can handle it with a `try..except` clause, so that program flow can continue:\n", "\n", "```py\n", "try:\n", " problematic()\n", "except RuntimeError as exinfo:\n", " print(f\"Found an exception: {exinfo}\")\n", "```\n", "\n", "**More Reading**\n", "\n", "- https://www.freecodecamp.org/news/exception-handling-python/\n", "- https://www.pythontutorial.net/python-basics/python-try-except-else/\n", "- https://www.geeksforgeeks.org/try-except-else-and-finally-in-python/" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### `try..except..else..finally` in bluesky plans\n", "\n", "In `bluesky`, `try..except` is a common pattern, however there is some additional complication due to\n", "plans being generator co-routines so two [_decorator_](https://wiki.python.org/moin/PythonDecorators#What_is_a_Python_Decorator) functions available:\n", "\n", "decorator | synopsis\n", "--- | ---\n", "`finalize_decorator` | Simple. Runs the `final_plan` no matter what happens in the decorated plan.\n", "`contingency_decorator` | Full-featured. Handle each aspect of Python's `try..except..else..finally` clause.\n", "\n", "**Exception handling in generator coroutines**\n", "\n", "Generator coroutines [expose exception throwing](https://tacaswell.github.io/coroutines-i.html) as a user communication channel. This means\n", "there is some [in-band encoding](https://youtu.be/iKzOBWOHGFE?si=XAtQKhk3eHboHcL-&t=1011) of Python's control exceptions with the users exceptions. When the `close()` [method](https://docs.python.org/3/reference/expressions.html#generator.close) is called on a generator coroutine, Python will throw a [GeneratorExit](https://docs.python.org/3/library/exceptions.html#GeneratorExit) exception into the coroutine. If you catch this exception and try to yield another `Msg`, either explicitly in an `except` block or in a `finally` block, Python will raise a `RuntimeError` at the call site.\n", "\n", "If you want to use `try..except..else..finally` directly, you must handle this case. See the source of these decorators for a guide.\n", "\n", "\n", "**More Reading**\n", "\n", "- https://realpython.com/primer-on-python-decorators/\n", "- https://pythonbasics.org/decorators/" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "We'll need the decorators from `bluesky.preprocessors`:" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### The `finalize_decorator()`\n", "\n", "The `finalize_decorator(final_plan)` will always run the `final_plan` after the wrapped plan is run, even if the wrapped plan raises an exception.\n", "\n", "**Hint**: Consider this as a simple means to call `restore_to_safe_settings()` after a plan finishes.\n", "\n", "Let's improve our plan by ensuring the shutter is always closed, even if the plan raises an exception." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "from bluesky import preprocessors as bpp\n", "\n", "def close_the_shutter():\n", " print(\"close_the_shutter()\")\n", " yield from bps.mv(shutter, \"close\")\n", "\n", "@bpp.finalize_decorator(close_the_shutter)\n", "def my_plan(terminate=False):\n", " print(f\"Start my_plan(), with {terminate=}.\")\n", " yield from bps.mv(shutter, \"open\")\n", " if terminate:\n", " print(\"By request, the plan will terminate.\")\n", " raise MyException(f\"Requested {terminate=}\")\n", "\n", " print(f\"Before count(), {shutter.state=}.\")\n", " yield from bp.count([detector])\n", "\n", " print(f\"After my_plan(), {shutter.state=}.\")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "This code is functionally equivalent to:\n", "\n", "```py\n", "try:\n", " yield from bps.mv(shutter, \"open\")\n", " yield from bp.count([detector])\n", "finally:\n", " yield from bps.mv(shutter, \"close\")\n", "```" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "#### Always **_wrap the plan_**, not the `RE`\n", "\n", "As stated above, when using `try..except` clauses in bluesky, we should always **_wrap the plan_** and not the `RE` itself.\n", "\n", "Here, we apply `try..except` to keep the notebook from stopping with an error.\n", "\n", "Our wrapper must accept the same arguments and pass them to the wrapped plan. It's easiest if we use generic terms (`*args, **kwargs`) so we do not need to keep this code synchronized with the wrapped plan.\n", "\n", "We add additional diagnostic print statements." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "def wrap_the_plan(*args, **kwargs):\n", " print(\"Start wrap_the_plan()\")\n", " try:\n", " yield from my_plan(*args, **kwargs)\n", " except Exception as exinfo:\n", " print(f\"Stopped by the error: {exinfo} {shutter.state=}\")\n", " print(f\"Finish wrap_the_plan() {shutter.state=}\")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Show what happens when the plan runs and _no_ exception is raised." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Start wrap_the_plan()\n", "Start my_plan(), with terminate=False.\n", "Before count(), shutter.state='open'.\n", "After my_plan(), shutter.state='open'.\n", "close_the_shutter()\n", "Finish wrap_the_plan() shutter.state='close'\n" ] }, { "data": { "text/plain": [ "('a96675f4-8c04-461a-adeb-e062798c1280',)" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "RE(wrap_the_plan())" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Now, show what happens if `my_plan` raises an exception:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Start wrap_the_plan()\n", "Start my_plan(), with terminate=True.\n", "By request, the plan will terminate.\n", "close_the_shutter()\n", "Stopped by the error: Requested terminate=True shutter.state='close'\n", "Finish wrap_the_plan() shutter.state='close'\n" ] }, { "data": { "text/plain": [ "()" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "RE(wrap_the_plan(True))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### The `contingency_decorator()`\n", "\n", "To learn about specific exceptions than use the `contingency_decorator()` which will handle each aspect of Python's `try..except..else..finally` clause." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "def my_except_plan(ex):\n", " print(f\"my_except_plan(): {ex=}, {shutter.state=}\")\n", " yield from bps.null()\n", "\n", "def my_else_plan():\n", " print(f\"my_else_plan(): plan completed successfully! {shutter.state=}\")\n", " yield from bps.null()\n", " yield from bps.null()\n", "\n", "def close_the_shutter():\n", " print(\"close_the_shutter()\")\n", " yield from bps.mv(shutter, \"close\")\n", "\n", "@bpp.contingency_decorator(\n", " except_plan=my_except_plan,\n", " else_plan=my_else_plan,\n", " final_plan=close_the_shutter,\n", ")\n", "def my_plan(terminate=False):\n", " print(f\"Start my_plan(), with {terminate=}.\")\n", " yield from bps.mv(shutter, \"open\")\n", " if terminate:\n", " print(\"By request, the plan will terminate.\")\n", " raise MyException(f\"Requested {terminate=}\")\n", "\n", " print(f\"Before count(), {shutter.state=}.\")\n", " yield from bp.count([detector])\n", "\n", " print(f\"After my_plan(), {shutter.state=}.\")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "This code is functionally equivalent to:\n", "\n", "```py\n", "try:\n", " yield from bps.mv(shutter, \"open\")\n", " yield from bp.count([detector])\n", "except Exception:\n", " yield from bps.null()\n", "else:\n", " yield from bps.null()\n", " yield from bps.null()\n", "finally:\n", " yield from bps.mv(shutter, \"close\")\n", "```" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Start wrap_the_plan()\n", "Start my_plan(), with terminate=False.\n", "Before count(), shutter.state='open'.\n", "After my_plan(), shutter.state='open'.\n", "my_else_plan(): plan completed successfully! shutter.state='open'\n", "close_the_shutter()\n", "Finish wrap_the_plan() shutter.state='close'\n" ] }, { "data": { "text/plain": [ "('31978648-ac39-425d-b70a-74fdfdb7948a',)" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "RE(wrap_the_plan(False))" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Start wrap_the_plan()\n", "Start my_plan(), with terminate=True.\n", "By request, the plan will terminate.\n", "my_except_plan(): ex=MyException('Requested terminate=True'), shutter.state='open'\n", "close_the_shutter()\n", "Stopped by the error: Requested terminate=True shutter.state='close'\n", "Finish wrap_the_plan() shutter.state='close'\n" ] }, { "data": { "text/plain": [ "()" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "RE(wrap_the_plan(True))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Suspending the RunEngine\n", "\n", "[Suspenders](#suspenders) pause the RunEngine (without asking the user for interaction) while some condition has changed (shutter closed, beam dumped, water flow is low, ...). The RunEngine continues to monitor and will resume automatically when the condition returns to normal.\n", "\n", "To demonstrate a suspender, we must have a plan that will run long enough (longer than our `my_plan()` takes) for the suspender to activate." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "def countdown(t=10):\n", " print(f\"Countdown from {t=}\")\n", " while t > 0:\n", " print(f\"{t=}\")\n", " yield from bps.sleep(1)\n", " t -= 1\n", " print(\"countdown complete.\")" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Countdown from t=5\n", "t=5\n", "t=4\n", "t=3\n", "t=2\n", "t=1\n", "countdown complete.\n" ] }, { "data": { "text/plain": [ "()" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "RE(countdown(5))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Next, we'll need to simulate something that will suspend the RunEngine. Let's suspend if the shutter closes. It's a bit tricky since we must queue the shutter to close and then re-open outside of our plan." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "from apstools.utils import run_in_thread\n", "import time\n", "\n", "@run_in_thread\n", "def blink_shutter_thread():\n", " t0 = time.time()\n", " t = 2.3\n", " print(f\"{time.time()-t0:.2f}s blink_shutter(): waiting for {t} s\")\n", " time.sleep(t)\n", "\n", " t = 3.2\n", " print(f\"{time.time()-t0:.2f}s blink_shutter(): closing the shutter for {t} s\")\n", " shutter.close()\n", " time.sleep(t)\n", "\n", " print(f\"{time.time()-t0:.2f}s blink_shutter(): opening the shutter\")\n", " shutter.open()\n", " print(f\"{time.time()-t0:.2f}s blink_shutter(): ending\") " ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Let's test that the `blink_shutter_thread()` function works as expected. Since this kicks off the action in a thread, the command line returns right away. We need to sleep long enough for it to finish." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.00s blink_shutter(): waiting for 2.3 s\n", "2.30s blink_shutter(): closing the shutter for 3.2 s\n", "5.51s blink_shutter(): opening the shutter\n", "6.37s blink_shutter(): ending\n", "0.00s blink_shutter(): waiting for 2.3 s\n", "2.30s blink_shutter(): closing the shutter for 3.2 s\n", "Suspending....To get prompt hit Ctrl-C twice to pause.\n", "Suspension occurred at 2024-08-17 09:45:07.\n", "Justification for this suspension:\n", "Signal shutter_pss_state is low\n", "5.81s blink_shutter(): opening the shutter\n", "6.08s blink_shutter(): endingSuspender SuspendBoolLow(Signal(name='shutter_pss_state', parent='shutter', value=1, timestamp=1723905911.1114528), sleep=5, pre_plan=None, post_plan=None, tripped_message=) reports a return to nominal conditions. Will sleep for 5 seconds and then release suspension at 2024-08-17 09:45:16.\n", "\n" ] } ], "source": [ "blink_shutter_thread()\n", "time.sleep(7)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "The shutter will be zero when closed, and one when open. Looking at the list of pre-defined [suspenders](https://blueskyproject.io/bluesky/state-machine.html#built-in-suspenders), [bluesky.suspenders.SuspendBoolLow](https://blueskyproject.io/bluesky/generated/bluesky.suspenders.SuspendBoolLow.html#bluesky.suspenders.SuspendBoolLow) fits this pattern. Let's tell it to wait 5 seconds after the shutters open before releasing the suspension on the RunEngine." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "from bluesky.suspenders import SuspendBoolLow\n", "\n", "shutter_closed_suspender = SuspendBoolLow(shutter.pss_state, sleep=5)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Since we start with the shutter closed, we'll need to first open the shutter, then call a plan with the suspender and wait for it to finish, then close the shutter. If we close the shutter with a `finalize_decorator()` at the end of the `countdown()` plan (as before), the `RE` will suspend without an end. The next plan implements these steps:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "def blink_during_countdown(*args, **kwargs):\n", " @bpp.suspend_decorator(shutter_closed_suspender)\n", " def _plan():\n", " yield from countdown(*args, **kwargs)\n", "\n", " t0 = time.time()\n", " print(f\"{shutter.state=}\")\n", " yield from bps.mv(shutter, \"open\")\n", " \n", " blink_shutter_thread() # operate the shutter in the background\n", " yield from _plan() # run the countdown plan with the shutter suspender\n", "\n", " yield from bps.mv(shutter, \"close\")\n", " print(f\"{shutter.state=} total execution time: {time.time()-t0:.2f} s\")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Run the suspender demonstration. The plan will interrupt after ~2 seconds, then resume ~3 seconds later." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "shutter.state='open'\n", "Countdown from t=10\n", "t=10\n", "t=9\n", "t=8\n", "t=7\n", "t=6\n", "t=5\n", "t=4\n", "t=3\n", "t=2\n", "t=1\n", "countdown complete.\n", "shutter.state='close' total execution time: 21.79 s\n" ] }, { "data": { "text/plain": [ "()" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "RE(blink_during_countdown())" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### Abort the RunEngine from a plan\n", "\n", "If you absolutely must **stop** the RunEngine from within a plan, yet do it gracefully, `abort_run_engine_to_idle()` is the plan for you:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "def abort_run_engine_to_idle(reason):\n", " print(f\"Programmatically aborting the RunEngine: {reason=}.\")\n", " print(\"RE returning to idle (after the pause and error message).\")\n", " # clear out any remaining tasks\n", " yield from bps.clear_checkpoint()\n", " # pause that triggers automatic RE.abort()\n", " yield from bps.pause()\n", " # RE.state will be \"idle\"\n", "\n", "@bpp.finalize_decorator(close_the_shutter)\n", "def my_plan(terminate=False):\n", " print(f\"Start my_plan(), with {terminate=}.\")\n", " yield from bps.mv(shutter, \"open\")\n", " if terminate:\n", " print(\"By request, the plan AND the RE will terminate.\")\n", " yield from abort_run_engine_to_idle(\"On request.\")\n", "\n", " print(f\"Before count(), {shutter.state=}.\")\n", " yield from bp.count([detector])\n", "\n", " print(f\"After my_plan(), {shutter.state=}.\")" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Start wrap_the_plan()\n", "Start my_plan(), with terminate=True.\n", "By request, the plan AND the RE will terminate.\n", "Programmatically aborting the RunEngine: reason='On request.'.\n", "RE returning to idle (after the pause and error message).\n", "Pausing...\n", "close_the_shutter()\n", "Stopped by the error: shutter.state='open'\n", "Finish wrap_the_plan() shutter.state='open'\n", "caught exinfo=RunEngineInterrupted(\"\\nYour RunEngine is entering a paused state. These are your options for changing\\nthe state of the RunEngine:\\n\\nRE.resume() Resume the plan.\\nRE.abort() Perform cleanup, then kill plan. Mark exit_stats='aborted'.\\nRE.stop() Perform cleanup, then kill plan. Mark exit_status='success'.\\nRE.halt() Emergency Stop: Do not perform cleanup --- just stop.\\n\")\n", "RE.state='idle'\n" ] } ], "source": [ "try:\n", " RE(wrap_the_plan(True))\n", "except Exception as exinfo:\n", " print(f\"caught {exinfo=}\")\n", " print(f\"{RE.state=}\")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Recover Safe Settings\n", "\n", "**Q**: How to recover to safe settings?\n", "\n", "For some instruments, _safe settings_ may be pre-determined positions and settings for the various parts of the instrument. Other instruments may define _safe settings_ based on some context, such as recent activities.\n", "\n", "Since the variations are plentiful, we describe _schematically_, how to recover to safe settings.\n", "\n", "Keep in mind when restoring settings, that the order in which items are restored may be important. In some cases, it may be necessary to set some settings and wait for them to be set, before proceeding to other settings." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### Restore to Pre-determined Settings\n", "\n", "This has been demonstrated above, with the `close_the_shutter()` plan. That plan could be generalized, such as:\n", "\n", "```py\n", "def safe_settings():\n", " yield from close_the_shutter()\n", " yield from park_the_detector()\n", " yield from park_the_diffractometer()\n", " yield from reset_the_amplifiers()\n", " # ...\n", "```\n", "\n", "where each of these actions are described in their own plans, based on the needs of the instrument." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### Restore to Context-dependent Settings\n", "\n", "In this variation from the pre-determined settings (above), it is necessary to describe the context. Arguments could be added to the `safe_settings()` plan which provide it context to decide what settings to restore and to what values.\n", "\n", "Or, the context may wish to restore to settings before the plan started, or may include results from some post-scan analysis of the collected data (such as move the position to the computed peak center).\n", "\n", "#### Restore to previous values\n", "\n", "To implement this feature, you'll need to collect the values of the items to be restored before the plan is run and then restore those items after the plan finishes. Consider `wrap_the_plan()` above, the two print statements are positioned at exactly the places we need to collect and restore, respectively:\n", "\n", "```py\n", "def wrap_the_plan(terminate=False):\n", " print(\"Start wrap_the_plan()\")\n", " safe_settings = yield from collect_safe_settings()\n", " try:\n", " yield from my_plan(terminate)\n", " except Exception as exinfo:\n", " print(f\"Stopped by the error: {exinfo} {shutter.state=}\")\n", " yield from restore_safe_settings(safe_settings)\n", " print(f\"Finish wrap_the_plan() {shutter.state=}\")\n", "```\n", "\n", "In this case, `safe_settings` is some Python structure (list, dictionary, class instance) with the values defined by the context. Here, it is a dictionary with the ophyd objects used as the dictionary's keys:\n", "\n", "```py\n", "def collect_safe_settings():\n", " settings = dict()\n", " for pos in [\n", " m1, m2, m3, m4, m5, m6,\n", " slit1.top, slit1.bot, slit1.inb, slit1.out,\n", " ]:\n", " settings[pos] = device.position\n", " for signal in [amp1.gain, mono.feedback, heater.setpoint]:\n", " settings[signal] = signal.get()\n", " settings[heater.power] = \"off\" # override\n", " return settings\n", "\n", "def restore_safe_settings(settings):\n", " # Restore one at a time in reverse order (very conservative).\n", " for signal, value in reversed(settings.items()):\n", " signal, value = pair\n", " yield from bps.mv(signal, value)\n", "```" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Summary\n", "\n", "Started with a plan:\n", "\n", "```py\n", "def my_plan():\n", " yield from bps.mv(shutter, \"open\")\n", " yield from bp.count([detector])\n", " yield from bps.mv(shutter, \"close\")\n", "```\n", "\n", "Added `finalize_decorator` (or `contingency_decorator`) to ensure the shutter would always be closed after the plan:\n", "\n", "```py\n", "def close_the_shutter():\n", " yield from bps.mv(shutter, \"close\")\n", "\n", "@bpp.finalize_decorator(close_the_shutter)\n", "def my_plan():\n", " yield from bps.mv(shutter, \"open\")\n", " yield from bp.count([detector])\n", "```\n", "\n", "Added `wrap_the_plan()` to save settings before the plan and restore them afterwards:\n", "\n", "```py\n", "def wrap_the_plan(*args, **kwargs):\n", " safe_settings = yield from collect_safe_settings()\n", " try:\n", " yield from my_plan(*args, **kwargs)\n", " except Exception as exinfo:\n", " print(f\"Report the exception: {exinfo}\")\n", " yield from restore_safe_settings(safe_settings)\n", "```" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "**Remember**: When using `try..except` clauses in bluesky, **_wrap the plan_**, not the `RE`." ] } ], "metadata": { "kernelspec": { "display_name": "bluesky_2023_2", "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.11.0" }, "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 2 }