From d412151f2f5317f9ff18f93e043bdffe511fd7bb Mon Sep 17 00:00:00 2001 From: Pete R Jemian Date: Mon, 18 Mar 2024 14:39:52 -0500 Subject: [PATCH] DOC #940 per review --- docs/source/examples/de_sscan.ipynb | 154 ++++++++++++++-------------- 1 file changed, 75 insertions(+), 79 deletions(-) diff --git a/docs/source/examples/de_sscan.ipynb b/docs/source/examples/de_sscan.ipynb index 7f3a0e1ed..293b1b3cc 100644 --- a/docs/source/examples/de_sscan.ipynb +++ b/docs/source/examples/de_sscan.ipynb @@ -7,13 +7,6 @@ "# The synApps `sscan`\n", "\n", "The synApps `sscan` record is used to measure scans of detector(s) *v*. positioner(s). \n", - "The `sscan` record has three different scan modes:\n", - "\n", - "`sscan` scan mode | bluesky plan | description\n", - "--- | --- | ---\n", - "linear | `scan()` | step scan with constant-size steps\n", - "table | `list_scan()` | step scan with list of positions\n", - "fly | `fly()` | continuous motion scan\n", "\n", "**Goals**: Demonstrate use of the `sscan` record with Bluesky.\n", "\n", @@ -22,11 +15,37 @@ " 1. Use a polling loop to wait for scan to end.\n", " 2. Use a Status object to wait for scan to end.\n", "\n", - "2. Same, but connect with most `sscan` record fields and get data after 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." + "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 with for a step scan of `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": [ + "Here, `gp:scan1` is preconfigured and will begin running when the *SCAN* button\n", + "is pressed. (Try it.) When the *SCAN* button is pressed, a `1` is sent to EPICS\n", + "PV `gp:scan1.EXSC` and the scan starts. When the scan finishes (or aborts), the\n", + "value of the button returns to `0`." ] }, { @@ -35,8 +54,8 @@ "source": [ "## Python setup\n", "\n", - "The first example will need only the bluesky RunEngine. It will not need any\n", - "databroker catalog. The EPICS IOC has a prefix of `gp:`." + "All these examples will need this minimum setup. The first example will not need\n", + "any databroker catalog. The EPICS IOC has a prefix of `gp:`." ] }, { @@ -57,30 +76,13 @@ "metadata": {}, "source": [ "## 1. Press SCAN button of a preconfigured scan\n", - "\n", - "Consider the case where the `sscan` record is already configured with all the\n", - "parameters necessary to perform a complete step scan.\n", - "\n", - "In the next figure, (a) shows `gp:scan1` is configured to step scan motor `m1`\n", - "from -1.2 to 1.2 in 21 points, collecting counts from scaler `gp:scaler1`\n", - "channels 2 & 4 (`I0` & `diode`, respectively). (b) shows\n", - "`gp:scaler1` is configured with a counting time of 0.2 seconds per point and\n", - "several detector channels.\n", - "\n", - "(a) scaler | (b) scan\n", - "--- | ---\n", - "![scaler1 setup](./scaler16.png) | ![scan1 setup](./sscan-scaler-v-motor.png)" + "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The `sscan` is preconfigured and will begin running when the *SCAN* button is\n", - "pressed. When the *SCAN* button is pressed, a `1` is sent to EPICS PV\n", - "`gp:scan1.EXSC` and the `sscan` starts. When the `sscan` finishes (or aborts),\n", - "the value of the button returns to `0`.\n", - "\n", "First, connect with the EPICS PV of the button using `ophyd.EpicsSignal`. Once connected, show the current value." ] }, @@ -113,11 +115,7 @@ "\n", "Write a bluesky plan that pushes the button, waits a brief moment for the scan\n", "to start, then waits for the scan to end. This simple example waits by\n", - "periodically polling the value of the button.\n", - "\n", - "Here, `scan_button.get(use_monitor=False)` forces ophyd to get a new value\n", - "from EPICS (and not to rely on the monitor value being updated promptly).\n", - "Probably not necessary to use `use_monitor=False`." + "periodically polling the value of the button." ] }, { @@ -130,7 +128,7 @@ " yield from bps.mv(scan_button, 1) # i.e. caput(\"gp:scan1.EXSC\", 1)\n", " print(\"Started scan ...\")\n", " yield from bps.sleep(0.1) # wait for scan to start\n", - " while scan_button.get(use_monitor=False) != 0:\n", + " while scan_button.get() != 0:\n", " yield from bps.sleep(0.1)\n", " print(\"Scan done.\")" ] @@ -195,17 +193,13 @@ "\n", "
\n", "\n", - "By comparing both `old_value` and `value`, we catch the exact event when the\n", - "scan finishes. This is cautious programming to avoid the odd case when\n", - "`old_value=0` and `value=0` (occurs when IOC just booted and scan not started\n", - "yet).\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 not\n", + "started yet).\n", "\n", - "The `RE` handles the job of supplying arguments to subscription functions such\n", - "as `watch_the_button`. The call to `scan_button.subscription(watch_the_button)`\n", - "(remember, `scan_button` is in instance of `EpicsSignal`) does not generate any\n", - "bluesky `Msg` objects, so `yield from ...` would fail. `.subscribe()` is a very\n", - "fast call. So fast that it will never block execution of the `RE`. It does not\n", - "need to be called as a plan.\n", + "The `ophyd.EpicsSignal` class handles the job of supplying arguments to\n", + "subscription functions such as `watch_the_button`.\n", "\n", "
\n", "\n", @@ -222,6 +216,8 @@ "from ophyd.status import Status\n", "\n", "def run_sscan():\n", + " # Create an instance of the Status object.\n", + " # If it is not marked as finished within 20s, it will raise a timeout exception.\n", " st = Status(timeout=20)\n", "\n", " def watch_the_button(old_value, value, **kwargs):\n", @@ -278,7 +274,7 @@ "scan1.wait_for_connection()\n", "\n", "def run_sscan():\n", - " # create an instance of the Status object.\n", + " # Create an instance of the Status object.\n", " # If it is not marked as finished within 20s, it will raise a timeout exception.\n", " st = Status(timeout=20)\n", "\n", @@ -338,10 +334,10 @@ "{'m1': array([-1.2 , -1.08, -0.96, -0.84, -0.72, -0.6 , -0.48, -0.36, -0.24,\n", " -0.12, 0. , 0.12, 0.24, 0.36, 0.48, 0.6 , 0.72, 0.84,\n", " 0.96, 1.08, 1.2 ]),\n", - " 'I0': array([1., 1., 1., 1., 1., 0., 1., 1., 2., 3., 4., 3., 2., 2., 1., 1., 1.,\n", - " 1., 1., 0., 2.], dtype=float32),\n", - " 'diode': array([2., 2., 0., 1., 2., 1., 1., 1., 1., 2., 4., 3., 3., 2., 1., 1., 0.,\n", - " 0., 2., 0., 0.], dtype=float32)}" + " 'I0': array([1., 1., 1., 1., 2., 1., 2., 1., 2., 3., 5., 3., 3., 2., 1., 1., 2.,\n", + " 2., 0., 1., 1.], dtype=float32),\n", + " 'diode': array([1., 1., 1., 1., 1., 0., 1., 1., 3., 3., 4., 4., 3., 2., 2., 1., 1.,\n", + " 1., 1., 1., 0.], dtype=float32)}" ] }, "execution_count": 9, @@ -369,8 +365,8 @@ "## 3. Setup and run same scan from bluesky\n", "\n", "Repeat the scan from the previous examples, but use bluesky to configure\n", - "`scan1`. It will be useful to connect the motor, the scaler, and two the scaler\n", - "channels." + "`scan1`. It will be useful to connect the motor, the scaler, and two of the\n", + "scaler channels." ] }, { @@ -399,10 +395,8 @@ "We can supply the count time per point and scan range parameters as arguments to\n", "the setup. After setting the counting time for the scaler, the next step in the\n", "setup is to clear any existing configuration of `scan1` using its `reset()`\n", - "method.\n", - "\n", - "In a bluesky plan, we'll need to use `apstools.plans.run_blocking_function` with\n", - "that `reset()` method.\n", + "method. Because `scan1.reset()` was written as an ophyd function, we'll call it\n", + "with `yield from run_blocking_function(scan1.reset)`.\n", "\n", "Finally, we'll setup `scan1` with the EPICS PV names to be used." ] @@ -509,8 +503,8 @@ "text/plain": [ "{'m1': array([-1.1 , -0.88, -0.66, -0.44, -0.22, 0. , 0.22, 0.44, 0.66,\n", " 0.88, 1.1 ]),\n", - " 'I0': array([0., 2., 1., 1., 1., 4., 3., 1., 0., 2., 1.], dtype=float32),\n", - " 'diode': array([1., 1., 0., 2., 1., 3., 3., 1., 1., 2., 0.], dtype=float32)}" + " 'I0': array([0., 1., 2., 1., 1., 3., 3., 2., 1., 1., 0.], dtype=float32),\n", + " 'diode': array([1., 2., 2., 0., 1., 3., 4., 2., 2., 1., 1.], dtype=float32)}" ] }, "execution_count": 14, @@ -535,12 +529,14 @@ "\n", "While we might just use `bps.read(scan1)` to get the data, the data would be\n", "named according to the `scan1` structure. We want more meaningful names so we\n", - "re-assign the names of the original objects (motor and scaler channel names).\n", - "To post *bluesky data*, it must come from an ophyd `Device` (subclass). We'll\n", - "create a custom device just for the arrays of our `scan1`. We'll read the\n", - "arrays from `scan1`, then write them to our custom device. Since we do not have\n", - "timestamps for each of the data points, we'll post the entire array as a single\n", - "event. The event will have the timestamp from the `bps.mv()` plan stub.\n", + "re-assign the names of the original objects (motor and scaler channel names). To\n", + "post *bluesky data*, it must come from an ophyd `Device` (subclass). (See\n", + "[here](https://nsls-ii.github.io/ophyd/device-overview.html#device-and-component)\n", + "for help with `Device` and `Component`.) We'll create a custom device just for\n", + "the arrays of our `scan1`. We'll read the arrays from `scan1`, then write them\n", + "to our custom device. Since we do not have timestamps for each of the data\n", + "points, we'll post the entire array as a single event. The event will have the\n", + "timestamp from the `bps.mv()` plan stub.\n", "\n", "The last five steps are all descriptive. The run is opened, the `primary`\n", "stream is written with the `scan_data`, then all is buttoned up and bluesky\n", @@ -624,7 +620,7 @@ { "data": { "text/plain": [ - "('edcc4ca7-6af6-415a-ae48-59d9d7c7a9d3',)" + "('eaa4598f-2e97-4e19-9e7c-4818ff62e042',)" ] }, "execution_count": 17, @@ -653,9 +649,9 @@ "data": { "text/plain": [ "BlueskyRun\n", - " uid='edcc4ca7-6af6-415a-ae48-59d9d7c7a9d3'\n", + " uid='eaa4598f-2e97-4e19-9e7c-4818ff62e042'\n", " exit_status='success'\n", - " 2024-03-18 10:53:29.903 -- 2024-03-18 10:53:29.910\n", + " 2024-03-18 14:34:24.557 -- 2024-03-18 14:34:24.563\n", " Streams:\n", " * primary\n" ] @@ -1056,9 +1052,9 @@ "Dimensions without coordinates: dim_0, dim_1, dim_2\n", "Data variables:\n", " scan1_m1 (time, dim_0) float64 -1.1 -0.88 -0.66 -0.44 ... 0.66 0.88 1.1\n", - " scan1_I0 (time, dim_1) float32 0.0 2.0 1.0 1.0 1.0 ... 1.0 0.0 2.0 1.0\n", - " scan1_diode (time, dim_2) float32 1.0 1.0 0.0 2.0 1.0 ... 1.0 1.0 2.0 0.0" + " scan1_I0 (time, dim_1) float32 0.0 1.0 2.0 1.0 1.0 ... 2.0 1.0 1.0 0.0\n", + " scan1_diode (time, dim_2) float32 1.0 2.0 2.0 0.0 1.0 ... 2.0 2.0 1.0 1.0" ], "text/plain": [ "\n", @@ -1068,8 +1064,8 @@ "Dimensions without coordinates: dim_0, dim_1, dim_2\n", "Data variables:\n", " scan1_m1 (time, dim_0) float64 -1.1 -0.88 -0.66 -0.44 ... 0.66 0.88 1.1\n", - " scan1_I0 (time, dim_1) float32 0.0 2.0 1.0 1.0 1.0 ... 1.0 0.0 2.0 1.0\n", - " scan1_diode (time, dim_2) float32 1.0 1.0 0.0 2.0 1.0 ... 1.0 1.0 2.0 0.0" + " scan1_I0 (time, dim_1) float32 0.0 1.0 2.0 1.0 1.0 ... 2.0 1.0 1.0 0.0\n", + " scan1_diode (time, dim_2) float32 1.0 2.0 2.0 0.0 1.0 ... 2.0 2.0 1.0 1.0" ] }, "execution_count": 19, @@ -1488,12 +1484,12 @@ "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 2.0 1.0 1.0 ... 1.0 1.0 2.0 2.0\n", - " scan1_diode (time, dim_2) float32 1.0 1.0 1.0 2.0 1.0 ... 1.0 1.0 1.0 1.0
    • time
      PandasIndex
      PandasIndex(Index([1710790482.3142707], dtype='float64', name='time'))
  • " ], "text/plain": [ "\n", @@ -1503,8 +1499,8 @@ "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 2.0 1.0 1.0 ... 1.0 1.0 2.0 2.0\n", - " scan1_diode (time, dim_2) float32 1.0 1.0 1.0 2.0 1.0 ... 1.0 1.0 1.0 1.0" + " scan1_I0 (time, dim_1) float32 2.0 0.0 1.0 1.0 0.0 ... 2.0 2.0 1.0 1.0\n", + " scan1_diode (time, dim_2) float32 1.0 1.0 1.0 2.0 1.0 ... 0.0 1.0 2.0 0.0" ] }, "execution_count": 21,