{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# The synApps `sscan` and `SscanRecord`\n",
"\n",
"The synApps `sscan` record is used to measure scans of detector(s) *v*. positioner(s). \n",
"\n",
"**Goals**: Demonstrate use of the `sscan` record with Bluesky.\n",
"\n",
"1. Press SCAN button of a preconfigured scan.\n",
"2. Same example as section 1, but uses\n",
" [SscanRecord](https://bcda-aps.github.io/apstools/latest/api/synApps/_sscan.html)\n",
" instead of\n",
" [EpicsSignal](https://blueskyproject.io/ophyd/user/tutorials/single-PV.html).\n",
"3. Setup the same scan from bluesky.\n",
"4. Add the scan data as a bluesky run.\n",
"\n",
"This notebook is intended for those who are familiar with EPICS and its motor, scaler, and sscan records but are new to Bluesky.\n",
"\n",
"## sscan record configuration\n",
"\n",
"Consider this `sscan` record (`gp:scan1`) which is configured for a step scan of\n",
"`scaler` (`gp:scaler1`) *vs.* `motor` (`gp:m1`).\n",
"\n",
"Figure (1a) shows `gp:scan1` configured to step scan motor `m1` from -1.2 to\n",
"1.2 in 21 points, collecting counts from scaler `gp:scaler1` channels 2 & 4\n",
"(`I0` & `diode`, respectively). Figure (1b) shows `gp:scaler1` configured\n",
"with a counting time of 0.2 seconds per point and several detector channels.\n",
"\n",
"Figure (1a) scan | Figure (1b) scaler\n",
"--- | ---\n",
"![scan1 setup](./sscan-scaler-v-motor.png) | ![scaler1 setup](./scaler16.png)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The *SCAN* button is connected to EPICS PV `gp:scan1.EXSC`. The scan starts\n",
"when SCAN is pressed. (Try it.) When the *SCAN* button is pressed, the GUI\n",
"sends `1` to the EPICS PV and the scan starts. When the scan finishes (or\n",
"aborts), the value of the PV changes to `0` which is then sent back to the GUI."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Python setup\n",
"\n",
"All these examples need this minimum setup. The first example will not need\n",
"any databroker catalog. The EPICS IOC has a prefix of `gp:`."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import bluesky\n",
"import bluesky.plan_stubs as bps\n",
"\n",
"RE = bluesky.RunEngine()\n",
"IOC = \"gp:\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. Press SCAN button of a preconfigured scan\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Above, we said the scan can be run by pressing the *SCAN* button in the GUI. Let's do the same thing with bluesky. That is, we'll have bluesky press the *SCAN* button and then wait for the scan to end.\n",
"\n",
"First, connect with the EPICS PV of the *SCAN* button (`gp:scan1.EXSC`) using\n",
"`ophyd.EpicsSignal`. Once the object is connected with the EPICS PV, show the\n",
"current value of the PV."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0\n"
]
}
],
"source": [
"from ophyd import EpicsSignal\n",
"\n",
"scan_button = EpicsSignal(f\"{IOC}scan1.EXSC\", name=\"scan_button\")\n",
"scan_button.wait_for_connection()\n",
"print(f\"{scan_button.get()}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Write a bluesky plan that starts the scan (by pushing the button) and watches\n",
"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",
"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": 6,
"metadata": {},
"outputs": [],
"source": [
"def run_sscan():\n",
" yield from bps.mv(scan_button, 1)\n",
"\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",
"Use a Status object instead of a polling loop.
\n",
"\n",
"Polling loops are discouraged because:\n",
"\n",
"- they are not efficient (involving waiting periods of empirical duration)\n",
"- they do not handle timeouts, settling times, Python exceptions\n",
"- the `RE` already has a main event loop\n",
"- we often want to watch *multiple* \n",
" [signals](https://blueskyproject.io/ophyd/user/tutorials/single-PV.html#set)\n",
" with different update rates or complex logic\n",
"\n",
"Instead of polling the value of an EpicsSignal, it is more efficient to start an\n",
"EPICS CA monitor on the EpicsSignal. When new values of the signal are reported\n",
"by EPICS as CA monitor events, a designated *callback* function is 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 `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",
"Instead, `bps.trigger(ophyd_object)` tells the ophyd object (such as a scaler or\n",
"area detector) to acquire its data. This triggers the ophyd object (by calling\n",
"the object's `.trigger()` method which returns an ophyd Status object) and\n",
"(optionally) waits for that status object to report it is done.\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",
"In our `.trigger()` method, our status object is built from 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",
"
<xarray.Dataset>\n", "Dimensions: (time: 1, dim_0: 21, dim_1: 21, dim_2: 21)\n", "Coordinates:\n", " * time (time) float64 1.711e+09\n", "Dimensions without coordinates: dim_0, dim_1, dim_2\n", "Data variables:\n", " scan1_m1 (time, dim_0) float64 -1.2 -1.08 -0.96 -0.84 ... 0.96 1.08 1.2\n", " scan1_I0 (time, dim_1) float32 1.0 1.0 1.0 1.0 1.0 ... 1.0 1.0 1.0 1.0\n", " scan1_diode (time, dim_2) float32 1.0 1.0 2.0 1.0 2.0 ... 1.0 1.0 1.0 1.0
<xarray.Dataset>\n", "Dimensions: (time: 1, dim_0: 21, dim_1: 21, dim_2: 21)\n", "Coordinates:\n", " * time (time) float64 1.711e+09\n", "Dimensions without coordinates: dim_0, dim_1, dim_2\n", "Data variables:\n", " scan1_m1 (time, dim_0) float64 -1.2 -1.08 -0.96 -0.84 ... 0.96 1.08 1.2\n", " scan1_I0 (time, dim_1) float32 2.0 0.0 0.0 0.0 0.0 ... 1.0 1.0 1.0 1.0\n", " scan1_diode (time, dim_2) float32 2.0 0.0 0.0 1.0 1.0 ... 1.0 1.0 1.0 1.0