{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# Fly Scans with EPICS motor and scaler\n", "\n", "The `ScalerMotorFlyer()` device from [apstools](https://bcda-aps.github.io/apstools/latest/api/_devices.html#apstools.devices.flyer_motor_scaler.ScalerMotorFlyer) makes it possible to run fly scans with just the EPICS motor and scaler records.\n", "\n", "This combination of positioner and detector represent common EPICS support available to most APS beam lines. An external fly scan controller is not necessary, nor is any dedicated data collection hardware. Keep in mind that the capabilities of the motor and scaler will provide certain limits on how fast the scan completes and how many data points may be collected.\n", "\n", "**Contents**\n", "\n", "- [What is a bluesky/ophyd 'Flyer'?](#Overview)\n", "- [Step-by-step outline of 'ScalerMotorFlyer()'](#Outline)\n", "- [Prepare the session](#Setup)\n", "- [Run the fly scan](#Scan)\n", "- [Get the data and plot it](#Visualize)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Overview\n", "\n", "Q: *What is a bluesky/ophyd `Flyer`?*\n", "\n", "A bluesky/ophyd\n", "[Flyer](https://github.com/bluesky/ophyd/blob/0e8acb3df3d17e0ab7aa2cc924831f4a0c580449/ophyd/flyers.py#L17) is an ophyd [Device](https://github.com/bluesky/ophyd/blob/0e8acb3df3d17e0ab7aa2cc924831f4a0c580449/ophyd/device.py#L778) which describes a data collection process that is not managed by the bluesky RunEngine.\n", "Examples of such data collection processes include:\n", "\n", "- A system using dedicated hardware to control the measurement sequence and collect data.\n", "- Some software that can be called from Python.\n", "- A Python function that runs in a background thread.\n", "\n", "The `Flyer` interfaces with the RunEngine with these three distinct steps:\n", "\n", "1. `kickoff()` : Start the fly scan.\n", "2. `complete()` : Wait for the fly scan to complete.\n", "3. `collect()` : Get (and report) the fly scan data.\n", "\n", "Note: There is an additional step, `describe_collect()`, which informs bluesky\n", "about the type(s) of data reported by `collect()`.\n", "\n", "In `ScalerMotorFlyer()`, the *fly scan* protocol is managed by the\n", "`actions_thread()` method. This method is run in its own thread so it does not\n", "interfere with the bluesky `RunEngine`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Outline\n", "\n", "Here is a step-by-step outline of the fly scan protocol:\n", "\n", "1. Setup\n", " 1. Scaler update rate is set from the requested sampling `period`.\n", "2. Taxi\n", " 1. Motor is sent to the `start` position (using original velocity).\n", " 2. Wait for motor to reach `start` position.\n", "3. Fly\n", " 1. Motor velocity is set based on the requested `start` & `finish` positions and `fly_time` for the scan.\n", " 2. Scaler count time set to `fly_time` plus a smidgen (`scaler_time_pad`).\n", " 3. Start periodic data collection\n", " 1. Scaler provides (EPICS Channel Access) updates as new data is available.\n", " 2. Record motor position & scaler channel counts.\n", " 3. Record time stamps for motor and scaler (probably different).\n", " 4. Data accumulated to internal memory.\n", " 4. Scaler is started to count.\n", " 5. Motor is sent to `finish` (using fly scan velocity).\n", " 6. Wait for motor to stop moving.\n", " 7. Scaler is stopped.\n", " 8. Stop periodic data collection\n", "4. Finish\n", " 1. Reset to previous values\n", " 1. motor velocity\n", " 2. scaler update rate\n", " 2. Report any acquired data to Bluesky RunEngine.\n", " 1. Data for each counter is reported as the difference of successive readings.\n", "\n", "If any exceptions are raised by steps 1-3 (such as cannot set a value, timeout, wrong type of parameter given, ...), skip directly to step 4." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Setup\n", "\n", "Without [more explanation here](https://bcda-aps.github.io/apstools/latest/examples/de_0_adsim_hdf5_basic.html#bluesky), we set up our bluesky session for data acquisition, importing needed libraries and constructing the RunEngine and databroker for a temporary catalog. We will use an [EPICS IOC](https://hub.docker.com/r/prjemian/custom-synapps-6.2/tags) with the prefix `gp:`. This IOC provides a simulated motor and a simulated scaler, among other [features](https://github.com/prjemian/epics-docker/tree/main/v1.1/n5_custom_synApps#readmemd). The scaler is preconfigured with detector inputs. When the scaler is counting, its counters (detector channels) are updated by random integers." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%matplotlib inline\n", "from apstools.devices import make_dict_device\n", "from apstools.devices import ScalerMotorFlyer\n", "from bluesky import plan_stubs as bps\n", "from bluesky import plans as bp\n", "from bluesky import preprocessors as bpp\n", "from bluesky import RunEngine\n", "from bluesky.callbacks.best_effort import BestEffortCallback\n", "from matplotlib import pyplot as plt\n", "from ophyd import EpicsMotor\n", "from ophyd.scaler import ScalerCH\n", "import databroker\n", "\n", "IOC = \"gp:\"\n", "\n", "# ophyd-level\n", "m1 = EpicsMotor(f\"{IOC}m10\", name=\"motor\")\n", "scaler = ScalerCH(f\"{IOC}scaler1\", name=\"scaler\")\n", "m1.wait_for_connection()\n", "scaler.wait_for_connection()\n", "scaler.select_channels()\n", "\n", "# bluesky-level\n", "cat = databroker.temp().v2\n", "plt.ion() # enables matplotlib graphics\n", "RE = RunEngine({})\n", "RE.subscribe(cat.v1.insert)\n", "best_effort_callback = BestEffortCallback()\n", "RE.subscribe(best_effort_callback) # LivePlot & LiveTable" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Create the `flyer` object that will be used by the bluesky [fly()](https://blueskyproject.io/bluesky/generated/bluesky.plans.fly.html#bluesky.plans.fly) plan. The `ScalerMotorFlyer()` deivce supports the ophyd flyer interface, as described above.\n", "\n", "In this example, we describe a fly scan from motor position 1 to 5 that should take 4 seconds, collecting data at 0.1 second intervals. Other keyword parameters are accepted. See the documentation for full details." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "flyer = ScalerMotorFlyer(scaler, m1, 1, 5, fly_time=4, period=.1, name=\"flyer\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Augment the *standard* (a.k.a. bluesky pre-assembled) [`bp.fly()`](https://blueskyproject.io/bluesky/generated/bluesky.plans.fly.html#bluesky.plans.fly) plan to save the peak statistics to a separate stream in the run after the fly scan is complete." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "def fly_with_stats(flyers, *, md=None):\n", " \"\"\"Replaces bp.fly(), adding stream for channel stats.\"\"\"\n", "\n", " @bpp.stage_decorator(flyers)\n", " @bpp.run_decorator(md=md)\n", " @bpp.stub_decorator()\n", " def _inner_fly():\n", " yield from bp.fly(flyers)\n", " for flyer in flyers:\n", " if hasattr(flyer, \"stats\") and isinstance(flyer.stats, dict):\n", " yield from _flyer_stats_stream(flyer, f\"{flyer.name}_stats\")\n", "\n", " def _flyer_stats_stream(flyer, stream=None):\n", " \"\"\"Output stats from this flyer into separate stream.\"\"\"\n", " yield from bps.create(name=stream or f\"{flyer.name}_stats\")\n", " for ch in list(flyer.stats.keys()):\n", " yield from bps.read(\n", " make_dict_device(\n", " {\n", " # fmt: off\n", " stat: v\n", " for stat, v in flyer.stats[ch].to_dict().items()\n", " if v is not None\n", " # fmt: on\n", " },\n", " name=ch\n", " )\n", " )\n", " yield from bps.save()\n", "\n", " yield from _inner_fly()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Scan\n", "\n", "Using our `flyer` device with the bluesky [fly()](https://blueskyproject.io/bluesky/generated/bluesky.plans.fly.html#bluesky.plans.fly) plan, run the fly scan. Supply additional metadata so we can label our plot. The call to `RE()` returns a list of the uids for each run that was executed. Collect this list (we expect only one uid in the list) for later use when accessing the run from the databroker." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "Transient Scan ID: 1 Time: 2022-12-14 12:42:21\n", "Persistent Unique Scan ID: '50b5225e-f698-46c2-9b54-c2038e635826'\n", "New stream: 'primary'\n", "+-----------+------------+\n", "| seq_num | time |\n", "+-----------+------------+\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/prjemian/micromamba/envs/bluesky_2022_3/lib/python3.9/site-packages/event_model/__init__.py:208: UserWarning: The document type 'bulk_events' has been deprecated in favor of 'event_page', whose structure is a transpose of 'bulk_events'.\n", " warnings.warn(\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "New stream: 'flyer_stats'\n", "+-----------+------------+\n", "generator fly_with_stats ['50b5225e'] (scan num: 1)\n", "\n", "\n", "\n" ] } ], "source": [ "uids = RE(fly_with_stats([flyer], md=dict(title=\"Demonstrate a scaler v. motor fly scan.\")))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that the LiveTable and LivePlot from the BestEffortCallback do not yet know how to show this data, so they report minimal information about the fly scan." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Visualize\n", "\n", "To make a plot of the data, first get the run from the databroker, identified here by its uid." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "BlueskyRun\n", " uid='50b5225e-f698-46c2-9b54-c2038e635826'\n", " exit_status='success'\n", " 2022-12-14 12:42:21.253 -- 2022-12-14 12:42:29.889\n", " Streams:\n", " * flyer_stats\n", " * primary\n" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "run = cat.v2[uids[0]]\n", "run" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Show some of the run's metadata, verifying that we are looking at the run we just acquired." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "run.metadata['start']['scan_id']=1\n", "run.metadata['start']['title']='Demonstrate a scaler v. motor fly scan.'\n" ] } ], "source": [ "print(f\"{run.metadata['start']['scan_id']=}\")\n", "print(f\"{run.metadata['start']['title']=}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `run` has a single stream of data, named `primary`. Get the data from that stream:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
<xarray.Dataset>\n", "Dimensions: (time: 41)\n", "Coordinates:\n", " * time (time) float64 1.671e+09 1.671e+09 ... 1.671e+09\n", "Data variables:\n", " motor (time) float64 1.09 1.19 1.19 1.29 ... 4.8 4.9 4.97 5.0\n", " motor_user_setpoint (time) float64 5.0 5.0 5.0 5.0 5.0 ... 5.0 5.0 5.0 5.0\n", " clock (time) float64 1e+06 1e+06 1e+06 ... 1e+06 1e+06 1e+06\n", " I0 (time) float64 0.0 1.0 0.0 0.0 1.0 ... 1.0 1.0 0.0 1.0\n", " I00 (time) float64 0.0 0.0 0.0 1.0 0.0 ... 0.0 1.0 0.0 1.0\n", " scint (time) float64 0.0 1.0 1.0 0.0 1.0 ... 1.0 0.0 1.0 0.0\n", " diode (time) float64 0.0 0.0 0.0 1.0 1.0 ... 1.0 1.0 0.0 0.0\n", " scaler_time (time) float64 0.1 0.1 0.1 0.1 0.1 ... 0.1 0.1 0.1 0.1
<xarray.Dataset>\n", "Dimensions: (time: 1)\n", "Coordinates:\n", " * time (time) float64 1.671e+09\n", "Data variables: (12/81)\n", " I0_mean_x (time) float64 3.046\n", " I0_mean_y (time) float64 0.4878\n", " I0_stddev_x (time) float64 1.213\n", " I0_stddev_y (time) float64 0.5061\n", " I0_slope (time) float64 0.04756\n", " I0_intercept (time) float64 0.343\n", " ... ...\n", " diode_sigma (time) float64 1.154\n", " diode_min_x (time) float64 1.09\n", " diode_max_x (time) float64 5.0\n", " diode_min_y (time) float64 0.0\n", " diode_max_y (time) float64 1.0\n", " diode_x_at_max_y (time) float64 4.9