diff --git a/docs/source/_static/1ide-taxi-fly-screen.png b/docs/source/_static/1ide-taxi-fly-screen.png new file mode 100644 index 00000000..41ad6de7 Binary files /dev/null and b/docs/source/_static/1ide-taxi-fly-screen.png differ diff --git a/docs/source/_static/bf1-busy-record.png b/docs/source/_static/bf1-busy-record.png new file mode 100644 index 00000000..7ddb08db Binary files /dev/null and b/docs/source/_static/bf1-busy-record.png differ diff --git a/docs/source/_static/bf1-motor-record.png b/docs/source/_static/bf1-motor-record.png new file mode 100644 index 00000000..79b76ecd Binary files /dev/null and b/docs/source/_static/bf1-motor-record.png differ diff --git a/docs/source/_static/bf1-scaler-record.png b/docs/source/_static/bf1-scaler-record.png new file mode 100644 index 00000000..e34acc6e Binary files /dev/null and b/docs/source/_static/bf1-scaler-record.png differ diff --git a/docs/source/_static/bf1-sscan-record.png b/docs/source/_static/bf1-sscan-record.png new file mode 100644 index 00000000..c78b5191 Binary files /dev/null and b/docs/source/_static/bf1-sscan-record.png differ diff --git a/docs/source/_static/bf1-sseq-record.png b/docs/source/_static/bf1-sseq-record.png new file mode 100644 index 00000000..981fc99b Binary files /dev/null and b/docs/source/_static/bf1-sseq-record.png differ diff --git a/docs/source/_static/bf1-swait-record.png b/docs/source/_static/bf1-swait-record.png new file mode 100644 index 00000000..b286afa8 Binary files /dev/null and b/docs/source/_static/bf1-swait-record.png differ diff --git a/docs/source/_static/userCalc8-as-tc.png b/docs/source/_static/userCalc8-as-tc.png new file mode 100644 index 00000000..11dcfc42 Binary files /dev/null and b/docs/source/_static/userCalc8-as-tc.png differ diff --git a/docs/source/_static/userTran1-as-tc.png b/docs/source/_static/userTran1-as-tc.png new file mode 100644 index 00000000..3f9c2c5a Binary files /dev/null and b/docs/source/_static/userTran1-as-tc.png differ diff --git a/docs/source/example/_busy_fly_scan.ipynb b/docs/source/example/_busy_fly_scan.ipynb new file mode 100644 index 00000000..6bbb651a --- /dev/null +++ b/docs/source/example/_busy_fly_scan.ipynb @@ -0,0 +1,1409 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# APS fly scans with taxi & fly `busy` records\n", + "\n", + "Some EPICS fly scans at APS are triggered by a pair of EPICS\n", + "[*busy*](https://epics-modules.github.io/busy/) records. \n", + "Each *busy* record initates a sequence of EPICS processing\n", + "steps as defined by other EPICS records. The first *busy*\n", + "record is called `taxi` and is responsible for preparing the hardware to fly.\n", + "Once *taxi* is complete, the second *busy* record, called `fly`, performs the\n", + "actual fly scan.\n", + "\n", + "The next figure shows a control screen (from an APS beam line). The screen has\n", + "buttons to initiate taxi & fly sequences. Controls for some other scan\n", + "parameters are also shown.\n", + "\n", + "![taxi/fly control screen](../_static/1ide-taxi-fly-screen.png)\n", + "\n", + "In a third (optional) phase, data is collected from hardware\n", + "and written somewhere (in this example, to the databroker catalog). \n", + "\n", + "This document shows how to operate such a scan with two examples. \n", + "We'll refer to *taxi* and *fly* as phases.\n", + "\n", + "- simplified processing sequence for each phase\n", + " - shows the basic flow of control\n", + " - sequence: delay a short time, then return\n", + " - no data collection\n", + "- step scan of scaler *v*. motor\n", + " - includes data collection\n", + " - additional PVs recorded\n", + " - plot saved data\n", + " - typical use case for APS beamlines\n", + "\n", + "## Overview\n", + "\n", + "Compare the taxi & fly scan algorithm to an airplane flight:\n", + "\n", + "phase | airplane flight | taxi & fly scan\n", + "--- | --- | ---\n", + "preparation | ticketing, boarding, baggage handling | configuration of software\n", + "taxi | move the aircraft to the start of the runway | move the hardware to pre-scan positions\n", + "fly | start moving, liftoff at flight velocity | start moving, begin collecting data at first position\n", + "data | baggage claim | retrieve the fly scan data arrays" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Bluesky (Python) setup\n", + "\n", + "These packages are needed to begin. The first block contains Python standard\n", + "packages, then come the various bluesky packages. Just the parts we plan on\n", + "using here.\n", + "\n", + "* Create a logger instance in case we want to investigate internal details as our code runs.\n", + "* Create an instance of the bluesky `RunEngine`.\n", + "* Create a temporary databroker catalog to save collected data.\n", + "* Subscribe the catalog to receive all data published by the RunEngine." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cat=\n" + ] + } + ], + "source": [ + "import logging\n", + "import time\n", + "\n", + "from apstools.synApps import BusyRecord\n", + "from apstools.plans import run_blocking_function\n", + "import bluesky\n", + "import bluesky.plan_stubs as bps\n", + "import databroker\n", + "from ophyd import Component, Device, Signal\n", + "\n", + "logger = logging.getLogger()\n", + "logger.setLevel(logging.INFO)\n", + "\n", + "RE = bluesky.RunEngine()\n", + "cat = databroker.temp().v2\n", + "RE.subscribe(cat.v1.insert)\n", + "print(f\"{cat=}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## EPICS IOC\n", + "\n", + "We'll start with an EPICS IOC that provides two instances of the\n", + "[*busy*](https://epics-modules.github.io/busy/) record.\n", + "\n", + "In the `gp:` IOC, we can use these general purpose PVs for this example:\n", + "\n", + "PV | record | purpose\n", + "--- | --- | ---\n", + "`gp:mybusy1` | *busy* | taxi (preparation) phase\n", + "`gp:mybusy2` | *busy* | fly (fly scan) phase\n", + "\n", + "Here, an ophyd `Device` subclass coordinates both *busy* records.\n", + "\n", + "The `BusyRecord` class from\n", + "[apstools.devices](https://bcda-aps.github.io/apstools/latest/api/synApps/index.html#records)\n", + "provides a standard interface to the synApps `busy` record. We subclass `BusyRecord`\n", + "here as `MyBusyRecord` and redefine the `.trigger()` method, as advised by this\n", + "[bluesky tutorial](https://blueskyproject.io/tutorials/Ophyd/02%20-%20Complex%20Behaviors%20%28Set%20and%20Multiple%20PVs%29.html).\n", + "A `DeviceStatus` object is returned to monitor the progress of the busy\n", + "record.\n", + "\n", + "Handling of the `taxi` and `fly` phases is identical. A complete taxi/fly scan\n", + "is performed by the `taxi_fly_plan()` method. Note this method is a bluesky\n", + "plan. It should be run by the bluesky RunEngine.\n", + "\n", + "Also note that, as written, the `taxi_fly_plan()` method does not collect any\n", + "data. As such, it should be considered as a part of a bluesky\n", + "[plan](https://blueskyproject.io/bluesky/plans.html#plans) which [opens a\n", + "run](https://blueskyproject.io/bluesky/generated/bluesky.plan_stubs.open_run.html#bluesky.plan_stubs.open_run)\n", + "and ([triggers\n", + "and](https://blueskyproject.io/bluesky/generated/bluesky.plan_stubs.trigger_and_read.html#bluesky.plan_stubs.trigger_and_read))\n", + "[reads](https://blueskyproject.io/bluesky/generated/bluesky.plan_stubs.read.html)\n", + "data from one or more\n", + "[Signals](https://blueskyproject.io/ophyd/user/reference/signals.html) or\n", + "[Devices](https://blueskyproject.io/ophyd/user/tutorials/device.html)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from ophyd import DeviceStatus\n", + "\n", + "class MyBusyRecord(BusyRecord):\n", + " timeout = Component(Signal, value=10, kind=\"config\")\n", + "\n", + " def trigger(self):\n", + " \"\"\"\n", + " Start this busy record and return status to monitor completion.\n", + "\n", + " This method is called from 'bps.trigger(busy, wait=True)'.\n", + " \"\"\"\n", + " status = DeviceStatus(self, timeout=self.timeout.get())\n", + " executing_values = (1, self.state.enum_strs[1])\n", + "\n", + " def watch_state(old_value, value, **kwargs):\n", + " if old_value in executing_values and value not in executing_values:\n", + " # When busy finishes, state changes from 1 to 0.\n", + " status.set_finished()\n", + " self.state.clear_sub(watch_state)\n", + "\n", + " # Push the Busy button...\n", + " self.state.put(1) # Write number in case text is different.\n", + " # Start a CA monitor on self.state, call watch_state() with updates.\n", + " self.state.subscribe(watch_state)\n", + "\n", + " # And return the DeviceStatus object.\n", + " # The caller can use it to tell when the action is complete.\n", + " return status\n", + "\n", + "class TaxiFlyScanDevice(Device):\n", + " taxi = Component(MyBusyRecord, \"mybusy1\", kind=\"config\")\n", + " fly = Component(MyBusyRecord, \"mybusy2\", kind=\"config\")\n", + "\n", + " def taxi_fly_plan(self):\n", + " yield from bps.trigger(self.taxi, wait=True)\n", + " yield from bps.trigger(self.fly, wait=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The `busy` record\n", + "\n", + "Summary: The *busy* record tells the *sseq* record to do all its processing\n", + "steps. The *sseq* record waits its assigned time, then turns the *busy* record\n", + "off.\n", + "\n", + "The *busy* record has a very limited task. It signals the procedure should start\n", + "and reports if the procedure is either `Busy` or `Done`. \n", + "\n", + "*The details of the procedure should be of no concern to the busy record.*\n", + "\n", + "
\n", + "\n", + "The EPICS *busy* record is quite simple. It is a boolean that is used to\n", + "indicate if a procedure is still active (busy). The caller is responsible for\n", + "setting it to `Busy` (value of 1) to start the procedure. The procedure (and\n", + "**not** the caller) is responsible for setting it back to `Done` (value of 0)\n", + "when the procedure is finished.\n", + "\n", + "![example of busy record](../_static/bf1-busy-record.png)\n", + "\n", + "A *userCalc* (the *swait* record) starts the *sseq* record when the *busy* record changes to `Busy`.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Procedure -- Delay a short time\n", + "\n", + "A very simple procedure for the `taxi` phase might be to delay for a programmable time (seconds), then set `busy` to `Done`. The `fly` phase could use the same procedure, with a different programmable time.\n", + "\n", + "A *preparation* function is needed to configure the EPICS subsystem. In addition to the `busy` record, each phase of this example will use these EPICS records. The ophyd Device classes are from [apstools.synApps](https://bcda-aps.github.io/apstools/latest/api/synApps/index.html#records):\n", + "\n", + "EPICS record | ophyd class | purpose\n", + "--- | --- | ---\n", + "sseq | SseqRecord | runs the procedure: delay for _n_ seconds, then set busy to `Done`\n", + "swait | SwaitRecord | Starts sseq when the busy record transitions to `Busy`.\n", + "\n", + "Both phases use the same procedure steps. A separate chain of busy/swait/sseq records is necessary for each phase.\n", + "\n", + "Later, we'll demonstrate an EPICS step scan using the motor, scaler, and sscan\n", + "records.\n", + "\n", + "### SseqRecord\n", + "\n", + "The sseq record runs the procedure, then sets busy to `Done`.\n", + "\n", + "
\n", + "\n", + "![sseq record example](../_static/bf1-sseq-record.png)\n", + "\n", + "Setting `.SCAN=\"Passive\"` allows this record to process on command (from the\n", + "swait record, below). Only the last step, step 10, is needed for this simple\n", + "_delay_ procedure. Other procedures may use steps 1-9 for additional tasks.\n", + "For more than 10 steps, use an additional sseq record(s), called from a step in\n", + "this sseq record.\n", + "\n", + "Write the delay time to `.DLYA`, the busy record value to write\n", + "(`.STRA=\"Done\"`), and the busy record PV to be written (`LNKA`). Note the use\n", + "of the `CA` modifier to the PV name, which is required for the `.WAITA=\"Wait\"`\n", + "setting.\n", + "\n", + "
\n", + "\n", + "### SwaitRecord\n", + "\n", + "The swait record acts like a trigger to start the sseq record. It senses when busy changes value.\n", + "\n", + "
\n", + "\n", + "![swait record example](../_static/bf1-swait-record.png)\n", + "\n", + "For both phases, the swait record watches its busy record (the PV name in channel A). It reacts (via its `.SCAN=\"I/O Intr\"` setting) when the busy record changes value. When busy is 1 (via `.CALC=\"A>0\"` and setting `.OOPT=\"When Non-zero\"`), it tells sseq to process (by sending a 1 to the `.PROC` field of the sseq record configured in the `.OUTN` field).\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Connect with EPICS\n", + "\n", + "Create local (ophyd-style) objects to connect with the EPICS IOC records." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from apstools.synApps import SseqRecord, SwaitRecord\n", + "from ophyd import EpicsSignal\n", + "\n", + "IOC = \"gp:\"\n", + "\n", + "flyscan = TaxiFlyScanDevice(IOC, name=\"flyscan\")\n", + "taxi_sseq = SseqRecord(f\"{IOC}userStringSeq1\", name=\"taxi_sseq\")\n", + "taxi_swait = SwaitRecord(f\"{IOC}userCalc11\", name=\"taxi_swait\")\n", + "fly_sseq = SseqRecord(f\"{IOC}userStringSeq2\", name=\"fly_sseq\")\n", + "fly_swait = SwaitRecord(f\"{IOC}userCalc12\", name=\"fly_swait\")\n", + "\n", + "for obj in (flyscan, taxi_sseq, taxi_swait, fly_sseq, fly_swait):\n", + " obj.wait_for_connection()\n", + "\n", + "# just in case these are not already enabled\n", + "sseq_enable = EpicsSignal(f\"{IOC}userStringSeqEnable\", name=\"sseq_enable\")\n", + "swait_enable = EpicsSignal(f\"{IOC}userCalcEnable\", name=\"swait_enable\")\n", + "for obj in (sseq_enable, swait_enable):\n", + " obj.wait_for_connection()\n", + " obj.put(\"Enable\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Write the plan that prepares EPICS\n", + "\n", + "The *busy*, *swait*, & *sseq* records for the *taxi* & *fly* phases are\n", + "configured by the following bluesky plan.\n", + "\n", + "The plan uses predefined names for the ophyd objects, a pattern typical for\n", + "beamline plans." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### The `reset()` method\n", + "\n", + "The *SseqRecord* support in *apstools* has a `reset()` method to clear any\n", + "previous settings of the EPICS PVs and ophyd object and return them to default\n", + "settings. Note: some of the other record support classes in *apstools.synApps*,\n", + "including *SwaitRecord* and *SscanRecord*, have such `reset()` methods.\n", + "\n", + "The `reset()` method is written as *ophyd* code, intended to be called from a\n", + "command-line session. The commands it contains that may take some time to\n", + "complete and possibly block the normal execution of the RunEngine's callback\n", + "thread. The\n", + "[run_blocking_function()](https://bcda-aps.github.io/apstools/latest/api/_plans.html#module-apstools.plans.run_blocking_function_plan)\n", + "plan from *apstools.plans* allows us to run `reset()` in a thread so that it\n", + "does not block the `RunEngine`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def prep_taxi_fly_simple_delay(taxi_delay_s, fly_delay_s):\n", + " \"\"\"Delay before returning from both taxi & fly phases.\"\"\"\n", + " logger.debug(\"taxi time: %.2f s\", taxi_delay_s)\n", + " logger.debug(\"fly time: %.2f s\", fly_delay_s)\n", + " # stop any action in progress\n", + " yield from bps.mv(\n", + " flyscan.fly.state, \"Done\",\n", + " flyscan.taxi.state, \"Done\",\n", + " )\n", + " # clear the taxi & fly busy records\n", + " yield from bps.mv(\n", + " flyscan.fly.forward_link, \"\",\n", + " flyscan.fly.output_link, \"\",\n", + " flyscan.taxi.forward_link, \"\",\n", + " flyscan.taxi.output_link, \"\",\n", + " )\n", + "\n", + " # clear the records to be used: swait and sseq\n", + " for obj in (fly_sseq, fly_swait, taxi_sseq, taxi_swait):\n", + " yield from run_blocking_function(obj.reset)\n", + " yield from bps.sleep(0.2) # arbitrary wait for EPICS record processing\n", + "\n", + " # busy record (via swait record) triggers sseq record\n", + " yield from bps.mv(\n", + " taxi_swait.scanning_rate, \"I/O Intr\",\n", + " taxi_swait.channels.A.input_pv, flyscan.taxi.prefix,\n", + " taxi_swait.calculation, \"A>0\",\n", + " taxi_swait.output_execute_option, \"When Non-zero\",\n", + " taxi_swait.output_link_pv, taxi_sseq.process_record.pvname,\n", + " )\n", + " yield from bps.mv(\n", + " fly_swait.scanning_rate, \"I/O Intr\",\n", + " fly_swait.channels.A.input_pv, flyscan.fly.prefix,\n", + " fly_swait.calculation, \"A>0\",\n", + " fly_swait.output_execute_option, \"When Non-zero\",\n", + " fly_swait.output_link_pv, fly_sseq.process_record.pvname,\n", + " )\n", + "\n", + " # taxi & fly will each wait the selected time, then return\n", + " yield from bps.mv(\n", + " taxi_sseq.steps.step10.string_value, \"Done\",\n", + " taxi_sseq.steps.step10.wait_completion, \"Wait\",\n", + " taxi_sseq.steps.step10.delay, taxi_delay_s,\n", + " taxi_sseq.steps.step10.output_pv, f\"{flyscan.taxi.prefix} CA NMS\",\n", + " )\n", + " yield from bps.mv(\n", + " fly_sseq.steps.step10.string_value, \"Done\",\n", + " fly_sseq.steps.step10.wait_completion, \"Wait\",\n", + " fly_sseq.steps.step10.delay, fly_delay_s,\n", + " fly_sseq.steps.step10.output_pv, f\"{flyscan.fly.prefix} CA NMS\",\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run the preparation plan\n", + "\n", + "Call the `prep_taxi_fly_simple_delay()` plan (with the bluesky RunEngine, `RE`)\n", + "with delay times for each phase." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "()" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "RE(prep_taxi_fly_simple_delay(2, 4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run taxi & fly scan plan\n", + "\n", + "Call the `taxi_fly_plan()` method with the bluesky RunEngine. Note this plan completes in the ~6s interval, as configured in the preparation step." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "()" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "flyscan.fly.timeout.put(10)\n", + "RE(flyscan.taxi_fly_plan())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Procedure -- step scan scaler & motor\n", + "\n", + "We'll need to connect with the EPICS scaler and motor PVs. Also we want to record\n", + "other PVs in our step scan. And we want to record timestamps at each point to we can post the scan results as bluesky data.\n", + "\n", + "### scaler record\n", + "![scaler](../_static/bf1-scaler-record.png)\n", + "\n", + "### motor record\n", + "![motor](../_static/bf1-motor-record.png)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "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", + "lorentzian = EpicsSignal(f\"{IOC}userCalc1\", name=\"lorentzian\")\n", + "temperature = EpicsSignal(f\"{IOC}userCalc8\", name=\"temperature\")\n", + "\n", + "for obj in (m1, scaler1, lorentzian, temperature):\n", + " obj.wait_for_connection()\n", + "\n", + "# convenience: pick out the individual detector signals from the scaler\n", + "I0 = scaler1.channels.chan02.s\n", + "scint = scaler1.channels.chan03.s\n", + "diode = scaler1.channels.chan04.s\n", + "I000 = scaler1.channels.chan05.s\n", + "I00 = scaler1.channels.chan06.s" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### sscan record\n", + "\n", + "![sscan scaler and motor](../_static/bf1-sscan-record.png)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from apstools.synApps import SscanRecord\n", + "\n", + "scan1 = SscanRecord(f\"{IOC}scan1\", name=\"scan1\")\n", + "scan1.wait_for_connection()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Write a preparation plan for the step scan\n", + "\n", + "The preparation plan configures these actions:\n", + "\n", + "- preparation\n", + " - setup *sseq* records\n", + " - for both *taxi* and *fly* phases\n", + " - set counting time in *scaler* record\n", + " - set scan parameters in *sscan* record\n", + " - start, finish, number of points\n", + " - motor PVs\n", + " - detector PVs\n", + " - detector trigger PVs\n", + " - set *swait* records to start (process) *sseq* records\n", + " - for both *taxi* and *fly* phases\n", + " - only when *busy* record goes to `Busy`\n", + "\n", + "- *taxi* phase\n", + " - move the motor (EPICS) to the start of the *fly* scan\n", + " - wait for the move to finish\n", + " - set its *busy* record to `Done`\n", + "\n", + "- *fly* phase \n", + " - execute the scan (process the (EPICS) *sscan* record)\n", + " - wait for the scan to finish\n", + " - set its *busy* record to `Done`\n", + "\n", + "**Note**: The preparation plan does not actually move the motor or start the scan.\n", + "It configures the *sseq* records to do these actions when commanded by the\n", + "*busy* records.\n", + "\n", + "The *busy* records start the *taxi* and *fly* phases." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def prep_taxi_fly_step_scan(start, finish, npts, ct):\n", + " \"\"\"Setup EPICS for step scan directed by taxi & fly.\"\"\"\n", + " logger.debug(\"start: %g s\", start)\n", + " logger.debug(\"finish: %g s\", finish)\n", + " logger.debug(\"number of points: %g s\", npts)\n", + " logger.debug(\"count time: %g s\", ct)\n", + " # stop any action in progress\n", + " yield from bps.mv(\n", + " flyscan.fly.state, \"Done\",\n", + " flyscan.taxi.state, \"Done\",\n", + " )\n", + " # clear the taxi & fly busy records\n", + " yield from bps.mv(\n", + " flyscan.fly.forward_link, \"\",\n", + " flyscan.fly.output_link, \"\",\n", + " flyscan.taxi.forward_link, \"\",\n", + " flyscan.taxi.output_link, \"\",\n", + " )\n", + "\n", + " # clear the records to be used: swait, sscan, and sseq\n", + " for obj in (fly_sseq, fly_swait, taxi_sseq, taxi_swait, scan1):\n", + " yield from run_blocking_function(obj.reset)\n", + " yield from bps.sleep(0.5) # arbitrary wait for EPICS record processing\n", + "\n", + " yield from bps.mv(\n", + " taxi_sseq.description, \"taxi procedure\",\n", + " fly_sseq.description, \"fly procedure\",\n", + " )\n", + "\n", + " # Move the motor to the start position.\n", + " step = taxi_sseq.steps.step1\n", + " yield from bps.mv(\n", + " step.numeric_value, start,\n", + " step.output_pv, f\"{m1.prefix} CA NMS\",\n", + " step.wait_completion, \"Wait\",\n", + " )\n", + "\n", + " # Start the sscan.\n", + " step = fly_sseq.steps.step1\n", + " yield from bps.mv(\n", + " step.numeric_value, 1,\n", + " step.output_pv, f\"{scan1.execute_scan.pvname} CA NMS\",\n", + " step.wait_completion, \"Wait\",\n", + " )\n", + "\n", + " # Configure scaler count time.\n", + " yield from bps.mv(scaler1.preset_time, ct)\n", + "\n", + " # Configure sscan.\n", + " yield from bps.mv(\n", + " scan1.positioners.p1.start, start,\n", + " scan1.positioners.p1.end, finish,\n", + " scan1.number_points, npts,\n", + " )\n", + " # Remember this mapping in scan1 of positioners and detectors.\n", + " # We'll use that later to get the data arrays.\n", + " # positioners\n", + " yield from bps.mv(\n", + " scan1.positioners.p1.readback_pv, m1.user_readback.pvname,\n", + " scan1.positioners.p1.setpoint_pv, m1.user_setpoint.pvname,\n", + " scan1.positioners.p4.readback_pv, \"time\", # timestamp at each point\n", + " )\n", + " # triggers\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, scint.pvname,\n", + " scan1.detectors.d02.input_pv, diode.pvname,\n", + " scan1.detectors.d03.input_pv, I0.pvname,\n", + " scan1.detectors.d04.input_pv, I00.pvname,\n", + " scan1.detectors.d05.input_pv, I000.pvname,\n", + " scan1.detectors.d06.input_pv, lorentzian.pvname,\n", + " scan1.detectors.d07.input_pv, temperature.pvname,\n", + " )\n", + "\n", + " # Trigger taxi & fly sseq records (via swait record) from their busy records.\n", + " yield from bps.mv(\n", + " taxi_swait.scanning_rate, \"I/O Intr\",\n", + " taxi_swait.channels.A.input_pv, flyscan.taxi.prefix,\n", + " taxi_swait.calculation, \"A>0\",\n", + " taxi_swait.output_execute_option, \"When Non-zero\",\n", + " taxi_swait.output_link_pv, taxi_sseq.process_record.pvname,\n", + " )\n", + " yield from bps.mv(\n", + " fly_swait.scanning_rate, \"I/O Intr\",\n", + " fly_swait.channels.A.input_pv, flyscan.fly.prefix,\n", + " fly_swait.calculation, \"A>0\",\n", + " fly_swait.output_execute_option, \"When Non-zero\",\n", + " fly_swait.output_link_pv, fly_sseq.process_record.pvname,\n", + " )\n", + "\n", + " # taxi & fly: set busy record to `Done`\n", + " step = taxi_sseq.steps.step10\n", + " yield from bps.mv(\n", + " step.string_value, \"Done\",\n", + " step.output_pv, f\"{flyscan.taxi.prefix} CA NMS\",\n", + " step.wait_completion, \"Wait\",\n", + " )\n", + " step = fly_sseq.steps.step10\n", + " yield from bps.mv(\n", + " step.string_value, \"Done\",\n", + " step.output_pv, f\"{flyscan.fly.prefix} CA NMS\",\n", + " step.wait_completion, \"Wait\",\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run the preparation plan" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "()" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "RE(prep_taxi_fly_step_scan(-1.1, 1.2, 11, 0.5))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run the taxi & fly scan" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "()" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "flyscan.fly.timeout.put(60) # might take longer than usual\n", + "RE(flyscan.taxi_fly_plan())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Collect the data\n", + "\n", + "Get the data (arrays) from `scan1`." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "def get_sscan_data(t0=None):\n", + " # t0: timestamp when sscan started\n", + " npts = scan1.current_point.get()\n", + " data = {\n", + " # use the same mapping as configured above\n", + " \"__dt__\": scan1.positioners.p4.array.get()[:npts],\n", + " \"m1\": scan1.positioners.p1.array.get()[:npts],\n", + " \"scint\": scan1.detectors.d01.array.get()[:npts],\n", + " \"diode\": scan1.detectors.d02.array.get()[:npts],\n", + " \"I0\": scan1.detectors.d03.array.get()[:npts],\n", + " \"I00\": scan1.detectors.d04.array.get()[:npts],\n", + " \"I000\": scan1.detectors.d05.array.get()[:npts],\n", + " \"lorentzian\": scan1.detectors.d06.array.get()[:npts],\n", + " \"temperature\": scan1.detectors.d07.array.get()[:npts],\n", + " }\n", + " # get timestamps for each step from sscan p4\n", + " t0 = t0 or time.time() - data[\"__dt__\"][-1]\n", + " data[\"__timestamps__\"] = t0 + data[\"__dt__\"]\n", + " return data\n", + "\n", + "# get_sscan_data()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Write a bluesky plan that puts it all together:\n", + "\n", + "- metadata\n", + "- bluesky run\n", + "- prepare EPICS for the taxi & fly scan\n", + "- taxi\n", + "- fly\n", + "- get the data\n", + "- publish data to primary stream" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "def taxi_fly_sscan_plan(start, finish, npts, ct, md={}):\n", + " md[\"plan_name\"] = \"taxi_fly_sscan_plan\"\n", + " flyscan.fly.timeout.put(60) # might take longer than usual\n", + "\n", + " yield from bps.open_run(md)\n", + " \n", + " yield from prep_taxi_fly_step_scan(start, finish, npts, ct)\n", + "\n", + " timestamps = Signal(name=\"timestamps\", value=[]) # collect by observing 'scan1'\n", + " t0 = time.time()\n", + "\n", + " def callback(**kwargs):\n", + " # print(f\"{len(timestamps.get())=} {kwargs=}\")\n", + " if kwargs[\"value\"] == 0:\n", + " timestamps.put([])\n", + " else:\n", + " timestamps.put(timestamps.get() + [time.time() - t0])\n", + " logger.debug(f\"callback: {kwargs['value']} {time.time()-t0:.4f} {m1.position=}\")\n", + " \n", + " scan1.current_point.subscribe(callback)\n", + "\n", + " yield from bps.trigger(flyscan.taxi, wait=True)\n", + " t0_fly = time.time() # Timestamp start of fly scan.\n", + " yield from bps.trigger(flyscan.fly, wait=True)\n", + "\n", + " t1 = time.time() - t0\n", + " logger.info(\"Fly time: %.3f s\", t1)\n", + " scan1.current_point.clear_sub(callback)\n", + "\n", + " class SscanDataArrays(Device):\n", + " __dt__ = Component(Signal)\n", + " m1 = Component(Signal)\n", + " I0 = Component(Signal)\n", + " I00 = Component(Signal)\n", + " I000 = Component(Signal)\n", + " scint = Component(Signal)\n", + " diode = Component(Signal)\n", + " lorentzian = Component(Signal)\n", + " temperature = Component(Signal)\n", + "\n", + " scan_data_arrays = SscanDataArrays(\"\", name=\"scan1\")\n", + "\n", + " # Get the data arrays from the sscan record.\n", + " data = get_sscan_data(t0_fly)\n", + "\n", + " # Post the data as discrete bluesky events.\n", + " timestamps = data.pop(\"__timestamps__\")\n", + " for i, ts in enumerate(timestamps):\n", + " yield from bps.create(name=\"primary\")\n", + " for k in data.keys():\n", + " obj = getattr(scan_data_arrays, k)\n", + " obj.put(data[k][i]) # to Python memory, will not block RE\n", + " obj._metadata[\"timestamp\"] = ts\n", + " yield from bps.read(scan_data_arrays)\n", + " yield from bps.save()\n", + " yield from bps.close_run()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run the complete taxi/fly scan\n", + "\n", + "Note: Includes data collection. Plotting will follow.\n", + "\n", + "parameter | value | description\n", + "--- | --- | ---\n", + "start | -1.2 | first motor position for the step scan\n", + "finish | 1.2 | last motor position for the step scan\n", + "npts | 21 | number of data points to be collected\n", + "ct | 0.2 | scaler counting time per point\n", + "\n", + "The `m1` motor will be moved in constant size steps between `start` and\n", + "`finish`. At each step of the scan, the scaler will be triggered to accumulate\n", + "counts for `ct` seconds in each of its detector channels." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "BlueskyRun\n", + " uid='ddc2c053-1e25-4d76-ac56-0ebacdc2008f'\n", + " exit_status='success'\n", + " 2024-04-03 13:27:59.117 -- 2024-04-03 13:28:17.454\n", + " Streams:\n", + " * primary\n" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "uids = RE(taxi_fly_sscan_plan(-1.2, 1.2, 21, 0.2))\n", + "run = cat[uids[0]]\n", + "run" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get and show the dataset from the `run`." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "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: 21)\n",
+       "Coordinates:\n",
+       "  * time               (time) float64 1.712e+09 1.712e+09 ... 1.712e+09\n",
+       "Data variables:\n",
+       "    scan1___dt__       (time) float64 0.6779 1.38 2.081 ... 16.61 17.31 18.01\n",
+       "    scan1_m1           (time) float64 -1.2 -1.08 -0.96 -0.84 ... 0.96 1.08 1.2\n",
+       "    scan1_I0           (time) float32 1.0 2.0 1.0 1.0 0.0 ... 1.0 2.0 0.0 1.0\n",
+       "    scan1_I00          (time) float32 1.0 0.0 1.0 0.0 2.0 ... 2.0 1.0 1.0 0.0\n",
+       "    scan1_I000         (time) float32 0.0 1.0 1.0 2.0 1.0 ... 0.0 1.0 2.0 2.0\n",
+       "    scan1_scint        (time) float32 1.0 0.0 1.0 2.0 1.0 ... 2.0 2.0 1.0 1.0\n",
+       "    scan1_diode        (time) float32 1.0 1.0 2.0 1.0 1.0 ... 1.0 2.0 2.0 0.0\n",
+       "    scan1_lorentzian   (time) float32 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n",
+       "    scan1_temperature  (time) float32 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0
" + ], + "text/plain": [ + "\n", + "Dimensions: (time: 21)\n", + "Coordinates:\n", + " * time (time) float64 1.712e+09 1.712e+09 ... 1.712e+09\n", + "Data variables:\n", + " scan1___dt__ (time) float64 0.6779 1.38 2.081 ... 16.61 17.31 18.01\n", + " scan1_m1 (time) float64 -1.2 -1.08 -0.96 -0.84 ... 0.96 1.08 1.2\n", + " scan1_I0 (time) float32 1.0 2.0 1.0 1.0 0.0 ... 1.0 2.0 0.0 1.0\n", + " scan1_I00 (time) float32 1.0 0.0 1.0 0.0 2.0 ... 2.0 1.0 1.0 0.0\n", + " scan1_I000 (time) float32 0.0 1.0 1.0 2.0 1.0 ... 0.0 1.0 2.0 2.0\n", + " scan1_scint (time) float32 1.0 0.0 1.0 2.0 1.0 ... 2.0 2.0 1.0 1.0\n", + " scan1_diode (time) float32 1.0 1.0 2.0 1.0 1.0 ... 1.0 2.0 2.0 0.0\n", + " scan1_lorentzian (time) float32 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n", + " scan1_temperature (time) float32 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset = run.primary.read()\n", + "dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot the data from the last scan\n", + "\n", + "Following the steps from the [plotting howto](https://bcda-aps.github.io/bluesky_training/howto/_plot_x_y_databroker.html#3.-Show-the-(primary)-data)..." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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DRsmfvGLX9itY3vca2trxf/8z/+Ir776ShQVFYmcnByRlpYmevfuzePehg8//FAEBQWJtWvXisLCQvHII4+I8PBwUVxcLIQQ4vHHHxczZ860rv/TTz+JsLAw8eijj4rCwkKxdu1aERQUJDZs2KDXV/BKao/7yy+/LDZt2iR++OEHceDAAfH4448LACIrK0uvr+B1amtrrdduAGLFihWioKBAlJSUCCF4rreHwYhBzJo1SwBoNeXk5FjXASDWrVtnfd3c3Cz+8Ic/iNjYWGE2m8WECRPE/v37PZ94L3bq1Clx5513iq5du4quXbuKO++8s1XTRtvjfu7cOZGZmSl69eolgoKCRJ8+fcSsWbNEaWmp5xPvRd544w3Rt29fERwcLEaNGiXy8vKs782aNUtMnDjRbv3c3FwxcuRIERwcLJKSksSqVas8nGLfoOa4/+lPfxL9+/cXISEhonv37mL8+PHi448/1iHV3ktpHn3pNGvWLCEEz/X2mIT4T20ZIiIiIh2waS8RERHpisEIERER6YrBCBEREemKwQgRERHpisEIERER6YrBCBEREemKwQgRERHpisEIERER6YrBCBHp4uDBg7j55puRlJQEk8mElStXemS/Dz/8MEaPHg2z2YwrrrjCI/skovYxGCEiXZw7dw79+vXD888/bx2F2hOEELj77rvx61//2mP7JKL2MRgh8mMbNmzAsGHDEBoaiqioKFx77bWoq6sDALz11lsYMmQIzGYz4uLiMHfuXOvnVqxYgWHDhiE8PByJiYl44IEHcPbsWev769evR7du3fCvf/0LgwYNQkREBK6//npUVFRY1xkzZgxefPFF3HbbbTCbzarTPmnSJMybNw+PPPIIunfvjpiYGKxZswZ1dXX47W9/i65du6J///749NNP7T736quv4sEHH0S/fv1U75OItMFghMhPVVRU4Pbbb8fdd9+NQ4cOITc3FzNmzIAQAqtWrcKDDz6I3/3ud9i/fz82b96Myy67zPrZLl264NVXX8WBAwfw9ttvY+vWrVi4cKHd9s+dO4fly5fj3XffxbZt21BaWooFCxa49Tu8/fbb6NmzJ7755hvMmzcP999/P2699VaMGzcOe/fuxZQpUzBz5kycO3fOrfslIjfTd5w+ItLLnj17BADrkPK24uPjxZNPPun0tv72t7+JqKgo6+t169YJAOLHH3+0LnvjjTdETEyMw8/37dtXvPzyy84nXggxceJEMX78eOvrxsZGER4ebjdEe0VFhQAg8vPzW33+D3/4gxgxYoSqfRKRNgJ1joWISCcjRoxARkYGhg0bhilTpiAzMxO33HILLl68iGPHjiEjI6PNz+bk5OC5555DYWEhampq0NjYiAsXLqCurg7h4eEAgLCwMPTv39/6mbi4OFRVVbn1OwwfPtw6HxAQgKioKAwbNsy6LCYmBgDcvl8ici8W0xD5qYCAAGRnZ+PTTz/F4MGD8dprryElJQXHjx9v93MlJSWYNm0ahg4diqysLOzZswdvvPEGAODixYvW9YKCguw+ZzKZIIRw63dwtA/bZSaTCQDQ3Nzs1v0SkXsxGCHyYyaTCVdffTWWLFmCgoICBAcHIzs7G0lJSdiyZYvDz+zevRuNjY146aWXMHbsWAwYMADHjh3zcMqJyJewmIbIT+3cuRNbtmxBZmYmoqOjsXPnTpw4cQKDBg3C4sWLMWfOHERHR2Pq1Kmora3Fjh07MG/ePPTv3x+NjY147bXXcOONN2LHjh1YvXq16v03NDSgsLDQOl9eXo59+/YhIiLCrrKsu/344484e/YsKisrcf78eezbtw8AMHjwYAQHB2u2XyJqG4MRIj8VGRmJbdu2YeXKlaipqUHfvn3x0ksvYerUqQCACxcu4OWXX8aCBQvQs2dP3HLLLQCAK664AitWrMCf/vQnLFq0CBMmTMCyZctw1113qdr/sWPHMHLkSOvr5cuXY/ny5Zg4cSJyc3Pd9j0vde+99yIvL8/6WklDUVERkpKSNNsvEbXNJNxdiEtERESkAuuMEBERka4YjBCR4ZSWliIiIqLNqbS0VO8kEpEbsZiGiAynsbERxcXFbb6flJSEwEBWeSPyFQxGiIiISFcspiEiIiJdMRghIiIiXTEYISIiIl0xGCEiIiJdMRghIiIiXTEYISIiIl0xGCEiIiJdMRghIiIiXf3/jdJS2kqTaK0AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "plt.ion()\n", + "\n", + "x = dataset[\"scan1_m1\"]\n", + "y1 = dataset[\"scan1_lorentzian\"]\n", + "y2 = dataset[\"scan1_I0\"]\n", + "\n", + "plt.plot(x.values, y1.values, \"bx-\", label=y1.name)\n", + "plt.plot(x.values, y2.values, \"ro-\", label=y2.name)\n", + "plt.xlabel(x.name)\n", + "plt.title(f\"scan_id={run.metadata['start']['scan_id']}\")\n", + "plt.legend()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "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/howto/_temperature_controller_swait.ipynb b/docs/source/howto/_temperature_controller_swait.ipynb new file mode 100644 index 00000000..c165bf51 --- /dev/null +++ b/docs/source/howto/_temperature_controller_swait.ipynb @@ -0,0 +1,793 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Simulate a temperature controller with an EPICS `swait` record\n", + "\n", + "
\n", + "TODO:\n", + "This HowTo is written as a tutorial (how to setup the swait record then apply it as a positioner).\n", + "Refactor into a HowTo (cut directly to use of the swait record)\n", + "using apstools.devices.SimulatedSwaitControllerPositioner. Then make a new notebook for apstools.devices.SimulatedTransformControllerPositioner.\n", + "
\n", + "\n", + "Learn how to create a simulated [temperature\n", + "controller](https://bcda-aps.github.io/bluesky_training/instrument/describe_instrument.html#temperature)\n", + "with Bluesky and an EPICS\n", + "[swait](https://bcda-aps.github.io/apstools/1.6.17/api/synApps/_swait.html)\n", + "record. We'll show how to simulate the controller in EPICS and use that\n", + "simulation as a *positioner* in Bluesky.\n", + "\n", + "In this simulation, the `swait` record provides the computations for the\n", + "feedback loop that updates the simulated temperature." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Connect with a `swait` record\n", + "\n", + "We'll connect with the `gp:userCalc18` PV, an instance of an EPICS `swait`\n", + "record in our example IOC. We'll create the ophyd controller object using the\n", + "[SwaitRecord](https://bcda-aps.github.io/apstools/latest/api/synApps/_swait.html#apstools.synApps.swait.SwaitRecord)\n", + "structure from the [apstools](https://bcda-aps.github.io/apstools/latest/)\n", + "package." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "controller.read()=OrderedDict([('simulator_calculated_value', {'value': 0.0, 'timestamp': 631152000.0})])\n", + "controller.read_configuration()=OrderedDict([('simulator_description', {'value': 'userCalc 8', 'timestamp': 631152000.0}), ('simulator_scanning_rate', {'value': 0, 'timestamp': 631152000.0}), ('simulator_disable_value', {'value': 0, 'timestamp': 631152000.0}), ('simulator_scan_disable_input_link_value', {'value': 0, 'timestamp': 631152000.0}), ('simulator_scan_disable_value_input_link', {'value': 'gp:userCalcEnable.VAL CA MS', 'timestamp': 631152000.0}), ('simulator_forward_link', {'value': '', 'timestamp': 631152000.0}), ('simulator_device_type', {'value': 0, 'timestamp': 631152000.0}), ('simulator_alarm_status', {'value': 17, 'timestamp': 631152000.0}), ('simulator_alarm_severity', {'value': 3, 'timestamp': 631152000.0}), ('simulator_new_alarm_status', {'value': 0, 'timestamp': 631152000.0}), ('simulator_new_alarm_severity', {'value': 0, 'timestamp': 631152000.0}), ('simulator_disable_alarm_severity', {'value': 0, 'timestamp': 631152000.0}), ('simulator_precision', {'value': 5, 'timestamp': 631152000.0}), ('simulator_high_operating_range', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_low_operating_range', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_calculation', {'value': '0', 'timestamp': 631152000.0}), ('simulator_output_link_pv', {'value': '', 'timestamp': 631152000.0}), ('simulator_output_location_name', {'value': '', 'timestamp': 631152000.0}), ('simulator_output_location_data', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_output_data_option', {'value': 0, 'timestamp': 631152000.0}), ('simulator_output_execute_option', {'value': 0, 'timestamp': 631152000.0}), ('simulator_output_execution_delay', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_event_to_issue', {'value': 0, 'timestamp': 631152000.0}), ('simulator_channels_A_input_value', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_channels_A_input_pv', {'value': '', 'timestamp': 631152000.0}), ('simulator_channels_A_input_trigger', {'value': 1, 'timestamp': 631152000.0}), ('simulator_channels_B_input_value', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_channels_B_input_pv', {'value': '', 'timestamp': 631152000.0}), ('simulator_channels_B_input_trigger', {'value': 1, 'timestamp': 631152000.0}), ('simulator_channels_C_input_value', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_channels_C_input_pv', {'value': '', 'timestamp': 631152000.0}), ('simulator_channels_C_input_trigger', {'value': 1, 'timestamp': 631152000.0}), ('simulator_channels_D_input_value', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_channels_D_input_pv', {'value': '', 'timestamp': 631152000.0}), ('simulator_channels_D_input_trigger', {'value': 1, 'timestamp': 631152000.0}), ('simulator_channels_E_input_value', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_channels_E_input_pv', {'value': '', 'timestamp': 631152000.0}), ('simulator_channels_E_input_trigger', {'value': 1, 'timestamp': 631152000.0}), ('simulator_channels_F_input_value', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_channels_F_input_pv', {'value': '', 'timestamp': 631152000.0}), ('simulator_channels_F_input_trigger', {'value': 1, 'timestamp': 631152000.0}), ('simulator_channels_G_input_value', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_channels_G_input_pv', {'value': '', 'timestamp': 631152000.0}), ('simulator_channels_G_input_trigger', {'value': 1, 'timestamp': 631152000.0}), ('simulator_channels_H_input_value', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_channels_H_input_pv', {'value': '', 'timestamp': 631152000.0}), ('simulator_channels_H_input_trigger', {'value': 1, 'timestamp': 631152000.0}), ('simulator_channels_I_input_value', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_channels_I_input_pv', {'value': '', 'timestamp': 631152000.0}), ('simulator_channels_I_input_trigger', {'value': 1, 'timestamp': 631152000.0}), ('simulator_channels_J_input_value', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_channels_J_input_pv', {'value': '', 'timestamp': 631152000.0}), ('simulator_channels_J_input_trigger', {'value': 1, 'timestamp': 631152000.0}), ('simulator_channels_K_input_value', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_channels_K_input_pv', {'value': '', 'timestamp': 631152000.0}), ('simulator_channels_K_input_trigger', {'value': 1, 'timestamp': 631152000.0}), ('simulator_channels_L_input_value', {'value': 0.0, 'timestamp': 631152000.0}), ('simulator_channels_L_input_pv', {'value': '', 'timestamp': 631152000.0}), ('simulator_channels_L_input_trigger', {'value': 1, 'timestamp': 631152000.0})])\n" + ] + } + ], + "source": [ + "from apstools.synApps import SwaitRecord\n", + "\n", + "controller = SwaitRecord(\"gp:userCalc8\", name=\"simulator\")\n", + "controller.wait_for_connection()\n", + "print(f\"{controller.read()=}\\n{controller.read_configuration()=}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a function to setup the controller\n", + "\n", + "Create a function to configure the `swait` record as a simulated temperature\n", + "controller. The \"controller\" will update the current computed value (the\n", + "*readback*) at `period` based on the setpoint. Note that `period` here is one\n", + "of the preset EPICS `.SCAN` field values. Pick from any of these values (from\n", + "the table at [this\n", + "reference](https://epics-base.github.io/epics-base/menuScan.html)):\n", + "\n", + "- `\"10 second\"`\n", + "- `\"5 second\"`\n", + "- `\"2 second\"`\n", + "- `\"1 second\"`\n", + "- `\".5 second\"`\n", + "- `\".2 second\"`\n", + "- `\".1 second\"`\n", + "\n", + "Be certain to use the exact text string as shown.\n", + "\n", + "The `swait` record will compute the step size based on the difference between\n", + "the previous value and the setpoint, limited to the maximum step size. Random `noise`\n", + "is applied to each new computation. The fields of the `swait` record in\n", + "this simulation are described in the next table:\n", + "\n", + "field | description\n", + "--- | ---\n", + "`.VAL` | readback\n", + "`.B` | setpoint\n", + "`.A` | previous value\n", + "`.C` | noise level\n", + "`.D` | maximum change\n", + "`.CALC` | calculation expression\n", + "`.SCAN` | record scan `period`\n", + "\n", + "The calculation will simulate a feedback loop which reduces the\n", + "value of `abs(readback - setpoint)`." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "def setup_controller(\n", + " swait,\n", + " setpoint=None,\n", + " label=\"controller\",\n", + " noise=2,\n", + " period=\"1 second\",\n", + " max_change=2\n", + "):\n", + " swait.reset() # remove any prior configuration\n", + " swait.description.put(label)\n", + " swait.channels.A.input_pv.put(swait.calculated_value.pvname)\n", + " if setpoint is not None:\n", + " swait.calculated_value.put(setpoint) # preset\n", + " swait.channels.A.input_value.put(setpoint) # readback\n", + " swait.channels.B.input_value.put(setpoint) # setpoint\n", + " swait.channels.C.input_value.put(noise) # 2 * noise amplitude\n", + " swait.channels.D.input_value.put(max_change)\n", + " swait.scanning_rate.put(period)\n", + " swait.calculation.put(\"A+max(-D,min(D,(B-A)))+C*(RNDM-0.5)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup our controller\n", + "\n", + "Setup our controller with a (randomly-selected) setpoint and scan period." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "\n", + "setup_controller(controller, 10 + 30 * random.random(), period=\"1 second\", label=\"temperature\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once configured, the control screen for this `swait` record should look like this screen view:\n", + "\n", + "![swait screen](../_static/userCalc8-as-tc.png)\n", + "\n", + "Watch the controller as it starts for a short time. The readback should already\n", + "be very close (within random noise) to the setpoint value." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.00s: readback=19.20 setpoint=19.81\n", + "2.01s: readback=20.74 setpoint=19.81\n", + "3.01s: readback=18.95 setpoint=19.81\n", + "4.01s: readback=19.22 setpoint=19.81\n", + "5.01s: readback=19.53 setpoint=19.81\n" + ] + } + ], + "source": [ + "import time\n", + "\n", + "t0 = time.time()\n", + "for i in range(5):\n", + " time.sleep(1)\n", + " print(\n", + " f\"{time.time() - t0:.2f}s:\"\n", + " f\" readback={controller.calculated_value.get():.2f}\"\n", + " f\" setpoint={controller.channels.B.input_value.get():.2f}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## temperature as a positioner\n", + "\n", + "A *positioner* is a device that has both a *readback* (the current measured value) and a *setpoint* (the expected, or demanded, value of the device). These are available as EPICS PVs from our `swait` record. We can obtain these directly from our ophyd `controller` object:\n", + "\n", + "signal | swait field | ophyd object\n", + "--- | --- | ---\n", + "readback | `.VAL` | `controller.calculated_value.pvname`\n", + "setpoint | `.B` | `controller.channels.B.input_value.pvname`\n", + "\n", + "We'll create the ophyd `temperature` positioner object using the\n", + "[PVPositionerSoftDoneWithStop](https://bcda-aps.github.io/apstools/latest/api/_devices.html#apstools.devices.positioner_soft_done.PVPositionerSoftDoneWithStop)\n", + "structure from the [apstools](https://bcda-aps.github.io/apstools/latest/)\n", + "package." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "temperature.position=19.527702862815946\n" + ] + } + ], + "source": [ + "from apstools.devices import PVPositionerSoftDoneWithStop\n", + "\n", + "temperature = PVPositionerSoftDoneWithStop(\n", + " \"\",\n", + " name=\"temperature\",\n", + " readback_pv=controller.calculated_value.pvname,\n", + " setpoint_pv=controller.channels.B.input_value.pvname,\n", + " tolerance=1,\n", + ")\n", + "temperature.wait_for_connection()\n", + "print(f\"{temperature.position=}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Change the setpoint\n", + "\n", + "Watch the readback after the setpoint is changed, until the temperature becomes\n", + "`inposition` (`inposition` is a property that reports a `True`/`False` value\n", + "determined by `abs(readback - setpoint) <= tolerance`).\n", + "\n", + "Here, we lower the temperature setpoint by 10 from the current readback value.\n", + "Then, monitor the readback value until `inposition`." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.00s: readback=17.39 setpoint=9.53\n", + "2.00s: readback=16.22 setpoint=9.53\n", + "3.00s: readback=14.24 setpoint=9.53\n", + "4.01s: readback=12.67 setpoint=9.53\n", + "5.01s: readback=10.00 setpoint=9.53\n" + ] + } + ], + "source": [ + "temperature.setpoint.put(temperature.readback.get() - 10)\n", + "\n", + "t0 = time.time()\n", + "while not temperature.inposition:\n", + " time.sleep(1)\n", + " print(\n", + " f\"{time.time() - t0:.2f}s:\"\n", + " f\" readback={temperature.readback.get():.2f}\"\n", + " f\" setpoint={temperature.setpoint.get():.2f}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Move the temperature as a positioner\n", + "\n", + "Here, we treat the `temperature` object as a *positioner*. \n", + "\n", + "Tip:\n", + "In ophyd, a positioner object has a `move()` method and a `position` property. The `position` property is a shortcut for `readback.get()`." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "9.998062212781615" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "temperature.position" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set the `temperature` to `25` and wait for the *move* to complete. A `MoveStatus` object is returned by the `move()` method.\n", + "\n", + "Tip: Python prints the value of the last object shown. In this case,\n", + "Python prints the value of the `MoveStatus` object. It shows\n", + "that that the move is done, how long it took, whether the move was successful,\n", + "and other information." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "MoveStatus(done=True, pos=temperature, elapsed=7.2, success=True, settle_time=0.0)" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "temperature.move(25)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Make a move *relative* to the current (**readback**) position:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "MoveStatus(done=True, pos=temperature, elapsed=3.0, success=True, settle_time=0.0)" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "temperature.move(temperature.position + 5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Make a move *relative* to the current **setpoint**:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "MoveStatus(done=True, pos=temperature, elapsed=2.0, success=True, settle_time=0.0)" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "temperature.move(temperature.setpoint.get() - 5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use the `temperature` positioner with a bluesky plan\n", + "\n", + "The `temperature` positioner may be used as a detector or a positioner in a bluesky plan.\n", + "\n", + "First, setup the bluesky objects needed for scanning and reporting. We won't\n", + "need plots nor will we need to save any data. Also create a convenience function to report the current parameters of the positioner." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "from bluesky.run_engine import RunEngine\n", + "from bluesky import plans as bp\n", + "from bluesky import plan_stubs as bps\n", + "from bluesky.callbacks.best_effort import BestEffortCallback\n", + "\n", + "bec = BestEffortCallback()\n", + "RE = RunEngine()\n", + "RE.subscribe(bec)\n", + "bec.disable_plots()\n", + "\n", + "def print_position(pos):\n", + " print(\n", + " f\"inposition={pos.inposition}\"\n", + " f\" position={pos.position:.3f}\"\n", + " f\" setpoint={pos.setpoint.get():.3f}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set the temperature to `25` using a bluesky plan stub (`bps.mv()`). Here, `bps.mv()` will set the temperature to an *absolute* value.\n", + "\n", + "A plan stub can be used directly with the `RE()` as shown here, or as part of another bluesky plan." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "inposition=True position=23.828 setpoint=24.165\n", + "inposition=True position=24.015 setpoint=25.000\n" + ] + } + ], + "source": [ + "print_position(temperature)\n", + "RE(bps.mv(temperature, 25))\n", + "print_position(temperature)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`bps.mvr()` will make a *relative* move. Decrease the temperature by `5`.\n", + "\n", + "Note that `bps.mvr()` has set the new setpoint to exactly 5 below the\n", + "previous *readback* value (not from the previous *setpoint* value)." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "inposition=True position=24.015 setpoint=25.000\n", + "inposition=True position=18.230 setpoint=19.015\n" + ] + } + ], + "source": [ + "print_position(temperature)\n", + "RE(bps.mvr(temperature, -5))\n", + "print_position(temperature)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can change the setpoint value directly. But notice that the temperature is not inposition immediately. This is because we asked for bluesky to wait *only* until setpoint changed, which happened almost instantly." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "inposition=True position=18.230 setpoint=19.015\n", + "inposition=False position=18.230 setpoint=24.015\n" + ] + } + ], + "source": [ + "print_position(temperature)\n", + "RE(bps.mvr(temperature.setpoint, 5))\n", + "print_position(temperature)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can measure the readback value (over time) by using `temperature` as a detector. Here we use the `bp.count` plan, making 5 readings at 1 second intervals. A data table is printed since this is one of the bluesky plans (`bp`) that create a [run](https://blueskyproject.io/bluesky/multi_run_plans.html#definition-of-a-run) which collects data.\n", + "\n", + "Tip If this cell is executed immediately after the preceding cell, then it will follow the readback as it approaches the new setpoint." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "Transient Scan ID: 1 Time: 2023-12-27 13:12:05\n", + "Persistent Unique Scan ID: '83e470c4-84b3-4669-8828-1ef0bb74a777'\n", + "New stream: 'primary'\n", + "+-----------+------------+-------------+\n", + "| seq_num | time | temperature |\n", + "+-----------+------------+-------------+\n", + "| 1 | 13:12:05.4 | 18.22953 |\n", + "| 2 | 13:12:06.4 | 20.03035 |\n", + "| 3 | 13:12:07.4 | 22.95103 |\n", + "| 4 | 13:12:08.4 | 24.51171 |\n", + "| 5 | 13:12:09.4 | 23.83296 |\n", + "+-----------+------------+-------------+\n", + "generator count ['83e470c4'] (scan num: 1)\n", + "\n", + "\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "('83e470c4-84b3-4669-8828-1ef0bb74a777',)" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "RE(bp.count([temperature], delay=1, num=5))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To demonstrate the use of `temperature` as a positioner in a scan, we'll need another signal to use as a detector. We'll create a simple ophyd Signal with a value that does not change." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "from ophyd import Signal\n", + "\n", + "det = Signal(name=\"det\", value=\"123.45\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To see the temperature setpoint reported in the table, set its `kind` attribute to `\"hinted\"`. Hinted attributes are shown (and plotted) when they are used as detectors." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "temperature.setpoint.kind = \"hinted\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Scan `det` vs. `temperature` in 5 steps from 20..40. See how it is the\n", + "*setpoint* which is advanced in even steps. The `bp.scan()` plan adjusts the\n", + "setpoint at each step, waits for the move to complete, then triggers and reads\n", + "the detectors." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "Transient Scan ID: 2 Time: 2023-12-27 13:12:10\n", + "Persistent Unique Scan ID: 'e4043fdc-debb-4a63-839d-2019230b63fe'\n", + "New stream: 'primary'\n", + "+-----------+------------+-------------+----------------------+------------+\n", + "| seq_num | time | temperature | temperature_setpoint | det |\n", + "+-----------+------------+-------------+----------------------+------------+\n", + "| 1 | 13:12:12.4 | 20.05370 | 20.00000 | 123 |\n", + "| 2 | 13:12:14.4 | 24.35966 | 25.00000 | 123 |\n", + "| 3 | 13:12:14.4 | 24.35966 | 30.00000 | 123 |\n", + "| 4 | 13:12:20.4 | 34.65895 | 35.00000 | 123 |\n", + "| 5 | 13:12:23.4 | 39.39283 | 40.00000 | 123 |\n", + "+-----------+------------+-------------+----------------------+------------+\n", + "generator scan ['e4043fdc'] (scan num: 2)\n", + "\n", + "\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "('e4043fdc-debb-4a63-839d-2019230b63fe',)" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "RE(bp.scan([det], temperature, 20, 40, 5))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`bp.rel_scan()` chooses its limits *relative* to the current position. Here we scan from `-17` to `3`, *relative* to the current position." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "inposition=True position=39.393 setpoint=40.000\n", + "\n", + "\n", + "Transient Scan ID: 3 Time: 2023-12-27 13:12:23\n", + "Persistent Unique Scan ID: 'f1cc2819-cd3d-4efa-b401-3f9dddceb5d1'\n", + "New stream: 'primary'\n", + "+-----------+------------+-------------+----------------------+------------+\n", + "| seq_num | time | temperature | temperature_setpoint | det |\n", + "+-----------+------------+-------------+----------------------+------------+\n", + "| 1 | 13:12:23.5 | 39.39283 | 22.39283 | 123 |\n", + "| 2 | 13:12:29.4 | 28.36643 | 27.39283 | 123 |\n", + "| 3 | 13:12:31.4 | 32.06715 | 32.39283 | 123 |\n", + "| 4 | 13:12:33.4 | 36.75387 | 37.39283 | 123 |\n", + "| 5 | 13:12:37.4 | 41.97115 | 42.39283 | 123 |\n", + "+-----------+------------+-------------+----------------------+------------+\n", + "generator rel_scan ['f1cc2819'] (scan num: 3)\n", + "\n", + "\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "('f1cc2819-cd3d-4efa-b401-3f9dddceb5d1',)" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print_position(temperature)\n", + "RE(bp.rel_scan([det], temperature, -17, 3, 5))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## SimulatedTransformControllerPositioner device\n", + "\n", + "Combine the setup steps into a single ophyd Device to make a simulator. Show the support code first:" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "from apstools.devices import SimulatedTransformControllerPositioner\n", + "\n", + "temperature = SimulatedTransformControllerPositioner(\n", + " \"\", name=\"controller\", loop_pv=\"gp:userTran1\"\n", + ")\n", + "temperature.wait_for_connection()\n", + "temperature.setup(25, label=\"temperature controller\", noise=0.2, max_change=1, tolerance=0.999)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Demonstrate the class by setting up a new temperature controller using a\n", + "different `swait` record." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "t17 = SimulatedTransformControllerPositioner(\n", + " \"\", name=\"t17\", loop_pv=\"gp:userTran17\", tolerance=1,\n", + ")\n", + "t17.wait_for_connection()\n", + "t17.setup(25, label=\"t17 controller\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Screen view of the transform record.\n", + "\n", + "![transform record](../_static/userTran1-as-tc.png)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "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 +}