Skip to content

Commit

Permalink
DOC #940 refactor to polling loop. Show the recommended style in a de…
Browse files Browse the repository at this point in the history
…tails section.
  • Loading branch information
prjemian committed Mar 20, 2024
1 parent ea97c33 commit 48f4a14
Showing 1 changed file with 97 additions and 47 deletions.
144 changes: 97 additions & 47 deletions docs/source/examples/de_sscan.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -116,63 +116,113 @@
"the button's value until it reports the scan ended. As written above, we know\n",
"exactly when the scan has ended when the button value changes from `1` to `0`.\n",
"\n",
"<details>\n",
"\n",
"1. The `ophyd.status.Status` class (runs in the background) is used here to\n",
" report when the scan has finished, aborted, or has not finished in the\n",
" expected time. Here, `st = Status(timeout=20)` creates a status object with a\n",
" default timeout of 20s.\n",
"\n",
"1. The inner `watch_the_button()` function detects when the button goes from `1`\n",
" to `0` (the scan has finished). Once the scan has finished,\n",
" `st.set_finished()` sets `st.done=True` and `st.success=True`.\n",
"\n",
" The `if` statement compares both `old_value` and `value`. We catch the exact\n",
" event when the scan finishes. This is cautious programming to avoid the odd\n",
" case when `old_value=0` and `value=0` (occurs when IOC just booted and scan\n",
" not started yet).\n",
"\n",
"1. A subscription is started (*after* we start the move) to monitor the button\n",
" value and respond promptly to any updates of the PV from EPICS. A\n",
" subscription starts a CA monitor on the signal and calls `watch_the_button()`\n",
" whenever a new value is received. The ophyd `EpicsSignal` class is\n",
" responsible for supplying the keyword arguments each time it calls a\n",
" subscription function (such as `watch_the_button()`) in response to an EPICS\n",
" CA monitor event.\n",
"\n",
"1. The plan waits for the scan to finish by waiting for `st.done=True`. We must\n",
" use `apstools.plans.run_blocking_function` with `st.wait()` since it is a\n",
" blocking function. The statement `run_blocking_function(st.wait)` runs\n",
" `st.wait()` in a background thread so it does not block the `RE`.\n",
"\n",
"Finally, we remove the subscription of the button.\n",
"</details>"
"We use `bps.sleep()` here to allow the `RE` to attend to its other responsibilities while waiting, rather than `time.sleep()` which would suspend all python activities (i.e. block the `RE`)."
]
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"from apstools.plans import run_blocking_function\n",
"from ophyd.status import Status\n",
"\n",
"def run_sscan():\n",
" # Create an instance of the Status class.\n",
" # Set a default timeout to 20s.\n",
" # If not finished by this time, it raises a timeout exception.\n",
" st = Status(timeout=20)\n",
" yield from bps.mv(scan_button, 1)\n",
"\n",
" def watch_the_button(old_value, value, **kwargs):\n",
" if old_value == 1 and value == 0:\n",
" # Once the scan finishes, scan1.EXSC changes from 1 to 0.\n",
" st.set_finished()\n",
" scan_button.clear_sub(watch_the_button)\n",
" # Wait for the scan to end with a polling loop.\n",
" while scan_button.get() != 0:\n",
" yield from bps.sleep(0.1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"NOTE: The polling loop, used by this simple example, is not recommended for bluesky plans.\n",
"\n",
" yield from bps.mv(scan_button, 1)\n",
" scan_button.subscribe(watch_the_button)\n",
" yield from run_blocking_function(st.wait)"
"<details>\n",
"<summary>Use a Status object instead of a polling loop.</summary>\n",
"\n",
"Polling loops are discouraged because:\n",
"\n",
"- they are not efficient (involving waiting periods of arbitrary duration)\n",
"- they do not handle timeouts or settling times\n",
"- they do not handle other Python exceptions\n",
"- they introduce additional loops into the existing `RE` main loop\n",
"\n",
"Instead of polling the value of an EpicsSignal, it is more efficient to set up\n",
"an EPICS CA monitor on the EpicsSignal.\n",
"When new values of the signal are reported by EPICS, a designated function is\n",
"called to respond.\n",
"\n",
"Ophyd\n",
"[Status](https://blueskyproject.io/ophyd/ser/generated/ophyd.status.Status.html#ophyd.status.Status)\n",
"objects track actions, like moving and triggering, that could take some time to\n",
"complete. These ophyd status objects have additional features such as timeout\n",
"and settling time.\n",
"\n",
"See the ophyd\n",
"[tutorial](https://blueskyproject.io/tutorials/Ophyd/02%20-%20Complex%20Behaviors%20%28Set%20and%20Multiple%20PVs%29.html#adding-a-set-method-to-device)\n",
"for use of a status object with a `.set()` method (which is the method called by\n",
"`bps.mv()`). It is not intuitive to use something like `bps.mv(scan_button, 1)`\n",
"here. That would only trigger the scan to *start* but would not wait for the\n",
"scan button value to return to `0`. We also want to wait until the scan is\n",
"complete.\n",
"\n",
"There is a different bluesky plan stub to use in this case: `bps.trigger()`.\n",
"This triggers an ophyd object (by calling the object's `.trigger()` method) and\n",
"(optionally) waits for that trigger to report it is done. It waits using the\n",
"ophyd Status object returned by the object's `.trigger()` method.\n",
"\n",
"We can add such a `.trigger()` method if we create a subclass of `EpicsSignal`.\n",
"The `.trigger()` method is called from a bluesky plan using\n",
"`bps.trigger(scan_button)`.\n",
"\n",
"We use the\n",
"[SubscriptionStatus](https://blueskyproject.io/ophyd/user/generated/ophyd.status.SubscriptionStatus.html#ophyd-status-subscriptionstatus)\n",
"class which manages the subscription to CA monitor events. The designated\n",
"function receives `old_value` and `value` from a CA monitor event and returns a\n",
"boolean value. Once the scan ends, the status object is set to `done=True` and\n",
"`success=True` and the CA monitor subscription is removed.\n",
"\n",
"Here is the code for the scan button, written with a status object:\n",
"\n",
"```py\n",
"from bluesky import plan_stubs as bps\n",
"from ophyd import EpicsSignal\n",
"from ophyd.status import SubscriptionStatus\n",
"\n",
"class MySscanScanButton(EpicsSignal):\n",
" timeout = 60\n",
"\n",
" def trigger(self):\n",
" \"\"\"\n",
" Start the scan and return status to monitor completion.\n",
"\n",
" This method is called from 'bps.trigger(scan_button, wait=True)'.\n",
" \"\"\"\n",
"\n",
" def just_ended(old_value, value, **kwargs):\n",
" \"\"\"Returns True when scan ends (signal changes from 1 to 0).\"\"\"\n",
" return old_value == 1 and value == 0\n",
"\n",
" # Starts an EPICS CA monitor on this signal and calls 'just_ended()' with updates.\n",
" # Once the status object is set to done, the CA subscription will be ended.\n",
" status = SubscriptionStatus(self, just_ended, timeout=self.timeout)\n",
"\n",
" # Push the scan button...\n",
" self.put(1)\n",
"\n",
" # And return the status object.\n",
" # The caller can use it to tell when the scan is complete.\n",
" return status\n",
"\n",
"scan_button = MySscanScanButton(\"gp:scan1.EXSC\", name=\"scan_button\")\n",
"\n",
"def run_sscan():\n",
" yield from bps.trigger(scan_button, wait=True)\n",
"```\n",
"\n",
"</details>"
]
},
{
Expand Down

0 comments on commit 48f4a14

Please sign in to comment.