diff --git a/docs/source/examples/pl_sscan.ipynb b/docs/source/examples/pl_sscan.ipynb new file mode 100644 index 000000000..1ed927799 --- /dev/null +++ b/docs/source/examples/pl_sscan.ipynb @@ -0,0 +1,1494 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The synApps `sscan`\n", + "\n", + "The synApps `sscan` record is used to measure scans of detector(s) *v*. positioner(s). It can be configured for step (linear) scans, list (table) scans, or fly scans.\n", + "\n", + "**Goals**: Demonstrate use the `sscan` record with Bluesky.\n", + "\n", + "1. Press SCAN button of a preconfigured scan.\n", + "\n", + " 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", + "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." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "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:`." + ] + }, + { + "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": [ + "## Example 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", + "The next screen (left) 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). The next screen (right) shows\n", + "`gp:scaler1` is configured with a counting time of 0.2 seconds per point and\n", + "several detector channels.\n", + "\n", + "scaler | scan\n", + "--- | ---\n", + "![scaler1 setup](./scaler16.png) | ![scan1 setup](./sscan-scaler-v-motor.png)" + ] + }, + { + "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, the value of\n", + "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." + ] + }, + { + "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 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`." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def run_sscan():\n", + " yield from bps.mv(scan_button, 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", + " yield from bps.sleep(0.1)\n", + " print(\"Scan done.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Execute the `run_sscan()` plan using the bluesky RunEngine `RE`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Started scan ...\n", + "Scan done.\n" + ] + }, + { + "data": { + "text/plain": [ + "()" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "RE(run_sscan())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To get the data from the `sscan` record, we use `ophyd.EpicsSignalRO` to connect with the arrays. We'll do that in a later section of this notebook.\n", + "\n", + "Let's eliminate that polling loop. This will make the plan respond more quickly\n", + "when the button changes value. The `ophyd.status.Status` class (runs in the\n", + "background) reports when the scan has finished or times out. We define an inner\n", + "`watch_the_button()` function to detect when the button goes from `1` to `0`\n", + "(the scan stops). We setup a subscription (*after* we start the move) to\n", + "monitor the button value and respond immediately. We wait for the status object\n", + "to finish with `st.wait()`. Finally, we cancel the monitor of the button." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from ophyd.status import Status\n", + "\n", + "def run_sscan():\n", + " st = Status(timeout=20)\n", + "\n", + " def watch_the_button(old_value, value, **kwargs):\n", + " if old_value == 1 and value == 0:\n", + " st.set_finished()\n", + "\n", + " yield from bps.mv(scan_button, 1)\n", + " scan_button.subscribe(watch_the_button)\n", + " st.wait()\n", + " scan_button.unsubscribe_all()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "()" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "RE(run_sscan())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 2 -- Run preconfigured `sscan` record\n", + "\n", + "The `apstools.synApps.SscanRecord` class provides access to most of the fields\n", + "provided by a `sscan` record. Use `SscanRecord` to connect and repeat the above\n", + "example. With the `SscanRecord` class, the button is called `execute_scan`." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "from apstools.synApps import SscanRecord\n", + "\n", + "scan1 = SscanRecord(f\"{IOC}scan1\", name=\"scan1\")\n", + "scan1.wait_for_connection()\n", + "\n", + "def run_sscan():\n", + " st = Status(timeout=20)\n", + "\n", + " def watch_the_button(old_value, value, **kwargs):\n", + " if old_value == 1 and value == 0:\n", + " st.set_finished()\n", + "\n", + " yield from bps.mv(scan1.execute_scan, 1)\n", + " scan1.execute_scan.subscribe(watch_the_button)\n", + " st.wait()\n", + " scan1.execute_scan.unsubscribe_all()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "()" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "RE(run_sscan())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Retrieve the data collected by `scan1` as a dictionary of numpy arrays. Include\n", + "both detectors and the motor. The `sscan` record has buffers capable of\n", + "collecting as many as 1,000 points per array. First get the number of points\n", + "collected, then limit each array length to that number.\n", + "\n", + "If we write this as a function, we can call it again later. Since it executes quickly, it does not need to be written as a bluesky plan." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'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., 0., 1., 1., 1., 0., 2., 1., 3., 4., 4., 3., 2., 1., 1., 1.,\n", + " 0., 2., 1., 0.], dtype=float32),\n", + " 'diode': array([0., 1., 1., 1., 1., 1., 1., 1., 2., 2., 4., 5., 3., 1., 2., 1., 1.,\n", + " 1., 1., 2., 1.], dtype=float32)}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def get_sscan_data():\n", + " npts = scan1.current_point.get()\n", + " data = {\n", + " \"m1\": scan1.positioners.p1.array.get()[:npts],\n", + " \"I0\": scan1.detectors.d01.array.get()[:npts],\n", + " \"diode\": scan1.detectors.d02.array.get()[:npts],\n", + " }\n", + " return data\n", + "\n", + "get_sscan_data()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 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." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "from ophyd import EpicsMotor\n", + "from ophyd.scaler import ScalerCH\n", + "\n", + "m1 = EpicsMotor(f\"{IOC}m1\", name=\"m1\")\n", + "scaler1 = ScalerCH(f\"{IOC}scaler1\", name=\"scaler1\")\n", + "m1.wait_for_connection()\n", + "scaler1.wait_for_connection()\n", + "\n", + "# for convenience\n", + "I0 = scaler1.channels.chan02.s\n", + "diode = scaler1.channels.chan04.s" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "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. In a bluesky plan, we'll need to use\n", + "`apstools.plans.run_blocking_function` with that `reset()` method. Finally,\n", + "we'll setup `scan1` with the EPICS PV names to be used." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "from apstools.plans import run_blocking_function\n", + "\n", + "def setup_scan1(start, finish, npts, ct=1):\n", + " yield from bps.mv(scaler1.preset_time, ct) # counting time/point\n", + " yield from run_blocking_function(scan1.reset)\n", + " yield from bps.sleep(0.2) # arbitrary wait for EPICS record processing\n", + "\n", + " # positioners\n", + " yield from bps.mv(\n", + " scan1.number_points, npts,\n", + " scan1.positioners.p1.start, start,\n", + " scan1.positioners.p1.end, finish,\n", + " scan1.positioners.p1.readback_pv, m1.user_readback.pvname,\n", + " scan1.positioners.p1.setpoint_pv, m1.user_setpoint.pvname,\n", + " )\n", + " # triggers (push scaler count button at each point)\n", + " yield from bps.mv(\n", + " scan1.triggers.t1.trigger_pv, scaler1.count.pvname,\n", + " )\n", + " # detectors\n", + " yield from bps.mv(\n", + " scan1.detectors.d01.input_pv, I0.pvname,\n", + " scan1.detectors.d02.input_pv, diode.pvname,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Setup the scan." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "()" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "RE(setup_scan1(-1.1, 1.1, 11, 0.2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run the scan. We should not have to reprogram this plan." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "()" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "RE(run_sscan())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get the scan data. Same function as before. It's not a bluesky plan, so `RE()`\n", + "is not needed." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "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([1., 0., 2., 2., 1., 2., 2., 2., 1., 1., 2.], dtype=float32),\n", + " 'diode': array([1., 2., 1., 1., 2., 3., 2., 1., 2., 1., 1.], dtype=float32)}" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "get_sscan_data()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Post data to bluesky as a run\n", + "\n", + "So far, we have just run `scan1` and shown the data collected. We'll need to do\n", + "a bit more work to get this data into bluesky as a run. While we might just use\n", + "`bps.read(scan1)` to get the data, the data would be named according to the\n", + "`scan1` structure. We want more meaningful names so we re-assign the names of\n", + "the original objects (motor and scaler channel names). To post *bluesky data*,\n", + "it must come from an ophyd `Device` (subclass). We'll create a custom one just\n", + "for our setup of `scan1`, then `bps.mv()` each of the arrays. Since we do not\n", + "have timestamps for each of the data points, we'll post the entire array as a\n", + "single event. The event will have the 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", + "finished the run. We define `name=\"scan1\"` so the names of the data in the databroker will match their usage here. The `scan_data` object is only a tool to get the data from the `sscan` record into the databroker." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "from ophyd import Component, Device, Signal\n", + "\n", + "class ThisSscanData(Device):\n", + " m1 = Component(Signal)\n", + " I0 = Component(Signal)\n", + " diode = Component(Signal)\n", + "\n", + "scan_data = ThisSscanData(\"\", name=\"scan1\")\n", + "\n", + "def post_sscan_data(md={}):\n", + " data = get_sscan_data()\n", + " yield from bps.mv(\n", + " scan_data.m1, data[\"m1\"],\n", + " scan_data.I0, data[\"I0\"],\n", + " scan_data.diode, data[\"diode\"],\n", + " )\n", + " yield from bps.open_run(md)\n", + " yield from bps.create(name=\"primary\")\n", + " yield from bps.read(scan_data)\n", + " yield from bps.save()\n", + " yield from bps.close_run()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we need a databroker catalog. Make a temporary one for this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import databroker\n", + "\n", + "cat = databroker.temp().v2\n", + "RE.subscribe(cat.v1.insert)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Post the data from the recent scan as a new bluesky run.\n", + "\n", + "Note: A bluesky plan can generate zero or more runs, each indexed by a `uid`.\n", + "Print the tuple of run uids returned from `RE()`." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('3e1451b1-255e-49ee-9a76-48f6f9708615',)" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "uids = RE(post_sscan_data())\n", + "uids" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get the run entry (by uid) from the catalog." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "BlueskyRun\n", + " uid='3e1451b1-255e-49ee-9a76-48f6f9708615'\n", + " exit_status='success'\n", + " 2024-03-17 09:59:56.147 -- 2024-03-17 09:59:56.151\n", + " Streams:\n", + " * primary\n" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "run = cat[uids[0]]\n", + "run" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Show the data arrays from the run's primary stream. The data are returned as a\n", + "single xarray [Dataset](https://docs.xarray.dev/en/stable/generated/xarray.Dataset.html)." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset>\n",
+       "Dimensions:      (time: 1, dim_0: 11, dim_1: 11, dim_2: 11)\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.1 -0.88 -0.66 -0.44 ... 0.66 0.88 1.1\n",
+       "    scan1_I0     (time, dim_1) float32 1.0 0.0 2.0 2.0 1.0 ... 2.0 1.0 1.0 2.0\n",
+       "    scan1_diode  (time, dim_2) float32 1.0 2.0 1.0 1.0 2.0 ... 1.0 2.0 1.0 1.0
" + ], + "text/plain": [ + "\n", + "Dimensions: (time: 1, dim_0: 11, dim_1: 11, dim_2: 11)\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.1 -0.88 -0.66 -0.44 ... 0.66 0.88 1.1\n", + " scan1_I0 (time, dim_1) float32 1.0 0.0 2.0 2.0 1.0 ... 2.0 1.0 1.0 2.0\n", + " scan1_diode (time, dim_2) float32 1.0 2.0 1.0 1.0 2.0 ... 1.0 2.0 1.0 1.0" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "run.primary.read()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Combined setup, scan, post data\n", + "\n", + "Instead of calling the `RE()` separately with each of these plans, combine them\n", + "into an outer plan." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "def combined(start, finish, npts, ct=1, md={}):\n", + " yield from setup_scan1(start, finish, npts, ct=ct)\n", + " yield from run_sscan()\n", + " yield from post_sscan_data(md)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run the combined plan and retrieve the data from the databroker catalog." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\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 0.0 2.0 2.0 0.0 2.0 ... 1.0 0.0 1.0 1.0\n",
+       "    scan1_diode  (time, dim_2) float32 1.0 0.0 2.0 2.0 1.0 ... 0.0 0.0 0.0 1.0
" + ], + "text/plain": [ + "\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 0.0 2.0 2.0 0.0 2.0 ... 1.0 0.0 1.0 1.0\n", + " scan1_diode (time, dim_2) float32 1.0 0.0 2.0 2.0 1.0 ... 0.0 0.0 0.0 1.0" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "uids = RE(combined(-1.2, 1.2, 21, ct=.2))\n", + "cat[uids[0]].primary.read()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "bluesky_2024_1", + "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.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/source/examples/scaler16.png b/docs/source/examples/scaler16.png new file mode 100644 index 000000000..ce3bf9194 Binary files /dev/null and b/docs/source/examples/scaler16.png differ diff --git a/docs/source/examples/sscan-scaler-v-motor.png b/docs/source/examples/sscan-scaler-v-motor.png new file mode 100644 index 000000000..fb9b5723c Binary files /dev/null and b/docs/source/examples/sscan-scaler-v-motor.png differ