Watch a temperature : scan temperature v time#

From APS Python Training for Bluesky Data Acquisition.

Objective

Measure the temperature for a short period and compute basics measures of the collected readings.

Start the instrument package#

Our instrument package is in the bluesky subdirectory here so we add that to the search path before importing it.

[1]:
import pathlib, sys
sys.path.append(str(pathlib.Path.home() / "bluesky"))
from instrument.collection import *
/home/prjemian/bluesky/instrument/_iconfig.py
Activating auto-logging. Current session state plus future input saved.
Filename       : /home/prjemian/Documents/projects/BCDA-APS/bluesky_training/docs/source/howto/.logs/ipython_console.log
Mode           : rotate
Output logging : True
Raw input log  : False
Timestamping   : True
State          : active
I Thu-17:54:44 - ############################################################ startup
I Thu-17:54:44 - logging started
I Thu-17:54:44 - logging level = 10
I Thu-17:54:44 - /home/prjemian/bluesky/instrument/session_logs.py
I Thu-17:54:44 - /home/prjemian/bluesky/instrument/collection.py
I Thu-17:54:44 - CONDA_PREFIX = /home/prjemian/.conda/envs/bluesky_2023_2
Exception reporting mode: Minimal
I Thu-17:54:44 - xmode exception level: 'Minimal'
I Thu-17:54:44 - /home/prjemian/bluesky/instrument/mpl/notebook.py
I Thu-17:54:44 - #### Bluesky Framework ####
I Thu-17:54:44 - /home/prjemian/bluesky/instrument/framework/check_python.py
I Thu-17:54:44 - /home/prjemian/bluesky/instrument/framework/check_bluesky.py
I Thu-17:54:44 - /home/prjemian/bluesky/instrument/framework/initialize.py
I Thu-17:54:44 - RunEngine metadata saved in directory: /home/prjemian/Bluesky_RunEngine_md
I Thu-17:54:44 - using databroker catalog 'training'
I Thu-17:54:44 - using ophyd control layer: pyepics
I Thu-17:54:44 - /home/prjemian/bluesky/instrument/framework/metadata.py
I Thu-17:54:44 - /home/prjemian/bluesky/instrument/epics_signal_config.py
I Thu-17:54:44 - Using RunEngine metadata for scan_id
I Thu-17:54:44 - #### Devices ####
I Thu-17:54:44 - /home/prjemian/bluesky/instrument/devices/area_detector.py
I Thu-17:54:44 - /home/prjemian/bluesky/instrument/devices/calculation_records.py
I Thu-17:54:47 - /home/prjemian/bluesky/instrument/devices/fourc_diffractometer.py
I Thu-17:54:47 - /home/prjemian/bluesky/instrument/devices/ioc_stats.py
I Thu-17:54:47 - /home/prjemian/bluesky/instrument/devices/kohzu_monochromator.py
I Thu-17:54:47 - /home/prjemian/bluesky/instrument/devices/motors.py
I Thu-17:54:47 - /home/prjemian/bluesky/instrument/devices/noisy_detector.py
I Thu-17:54:47 - /home/prjemian/bluesky/instrument/devices/scaler.py
I Thu-17:54:48 - /home/prjemian/bluesky/instrument/devices/shutter_simulator.py
I Thu-17:54:48 - /home/prjemian/bluesky/instrument/devices/simulated_fourc.py
I Thu-17:54:48 - /home/prjemian/bluesky/instrument/devices/simulated_kappa.py
I Thu-17:54:48 - /home/prjemian/bluesky/instrument/devices/slits.py
I Thu-17:54:48 - /home/prjemian/bluesky/instrument/devices/sixc_diffractometer.py
I Thu-17:54:49 - /home/prjemian/bluesky/instrument/devices/temperature_signal.py
I Thu-17:54:49 - #### Callbacks ####
I Thu-17:54:49 - /home/prjemian/bluesky/instrument/callbacks/spec_data_file_writer.py
I Thu-17:54:49 - #### Plans ####
I Thu-17:54:49 - /home/prjemian/bluesky/instrument/plans/lup_plan.py
I Thu-17:54:49 - /home/prjemian/bluesky/instrument/plans/peak_finder_example.py
I Thu-17:54:49 - /home/prjemian/bluesky/instrument/utils/image_analysis.py
I Thu-17:54:49 - #### Utilities ####
I Thu-17:54:49 - writing to SPEC file: /home/prjemian/Documents/projects/BCDA-APS/bluesky_training/docs/source/howto/20230413-175449.dat
I Thu-17:54:49 -    >>>>   Using default SPEC file name   <<<<
I Thu-17:54:49 -    file will be created when bluesky ends its next scan
I Thu-17:54:49 -    to change SPEC file, use command:   newSpecFile('title')
I Thu-17:54:49 - #### Startup is complete. ####

Describe#

Note the initial settings of the temperature (controller). It’s actually an EPICS swait record that simulates a temperature sensor and controller. It has a setpoint, a readback, and a few other support attributes as shown. A Random noise level is part of the simulation. We’ll compute that noise level from the standard deviation of the measurements.

[2]:
listdevice(temperature)
[2]:
=============================== =================================== ==========================
data name                       value                               timestamp
=============================== =================================== ==========================
temperature_setpoint            25.0                                2023-04-13 17:54:49.157724
temperature                     0.0                                 2023-04-13 17:54:49.159466
temperature_done                False                               2023-04-13 17:54:49.178146
temperature_calculation         A+max(-D,min(D,(B-A)))+C*(RNDM-0.5) 2023-04-13 17:54:49.160429
temperature_description         temperature                         2023-04-13 17:54:49.154181
temperature_max_change          2.0                                 2023-04-13 17:54:49.160349
temperature_noise               1.0                                 2023-04-13 17:54:49.159466
temperature_previous_value_pv   gp:userCalc8.VAL                    2023-04-13 17:54:49.157188
temperature_scanning_rate       5                                   2023-04-13 17:54:49.160429
temperature_tolerance           1.0                                 2023-04-13 17:54:49.160429
temperature_report_dmov_changes False                               2023-04-13 17:54:49.156380
=============================== =================================== ==========================

Count#

Use the bluesky RunEngine, RE, and a standard plan, bp.count(), to count the temperature. The bp.count() plan triggers (typically a scaler) and then reads from the detector. In this case, the temperature device’s .trigger() method does not do anything, yet it is still countable.

[3]:
RE(bp.count([temperature]))


Transient Scan ID: 958     Time: 2023-04-13 17:54:49
Persistent Unique Scan ID: 'cd24d583-6fd9-43ce-9ea3-583b104c2a6d'
New stream: 'label_start_motor'
New stream: 'primary'
+-----------+------------+-------------+
|   seq_num |       time | temperature |
+-----------+------------+-------------+
|         1 | 17:54:49.4 |     0.00000 |
+-----------+------------+-------------+
generator count ['cd24d583'] (scan num: 958)
[3]:
('cd24d583-6fd9-43ce-9ea3-583b104c2a6d',)

Take ten readings by adding the num=10 keyword argument (a.k.a. kwarg).

[4]:
RE(bp.count([temperature], num=10))


Transient Scan ID: 959     Time: 2023-04-13 17:54:49
Persistent Unique Scan ID: '8b99ba9a-1361-4b5d-8910-3a7b1e0566e2'
New stream: 'label_start_motor'
New stream: 'primary'
+-----------+------------+-------------+
|   seq_num |       time | temperature |
+-----------+------------+-------------+
|         1 | 17:54:49.8 |     0.00000 |
|         2 | 17:54:49.8 |     0.00000 |
|         3 | 17:54:49.8 |     0.00000 |
|         4 | 17:54:49.9 |     2.24478 |
|         5 | 17:54:49.9 |     2.24478 |
|         6 | 17:54:49.9 |     2.24478 |
|         7 | 17:54:50.0 |     2.24478 |
|         8 | 17:54:50.0 |     2.24478 |
|         9 | 17:54:50.0 |     2.24478 |
|        10 | 17:54:50.0 |     2.24478 |
+-----------+------------+-------------+
generator count ['8b99ba9a'] (scan num: 959)
[4]:
('8b99ba9a-1361-4b5d-8910-3a7b1e0566e2',)
../_images/example__watch_temperature_7_2.png

Do all the readings look the same? Perhaps there are only two different values (first one, then the other)? That’s because our measurements were only reading the temperature without waiting for it to update. (If there are two different values, we caught the moment when EPICS updated the calculation.) The table above showed temperature_scanning_rate 5 which means EPICS is only updating every 2 seconds.

[5]:
temperature.scanning_rate.get(as_string=True)
[5]:
'2 second'

Adjust the measurement interval#

We need to slow down the measurement to this interval by adding the delay=2 kwarg, sampling every 2 seconds.

[6]:
RE(bp.count([temperature], num=10, delay=2))


Transient Scan ID: 960     Time: 2023-04-13 17:54:50
Persistent Unique Scan ID: 'b55776f1-0a02-464c-9762-fe807c90c014'
New stream: 'label_start_motor'
New stream: 'primary'
+-----------+------------+-------------+
|   seq_num |       time | temperature |
+-----------+------------+-------------+
|         1 | 17:54:50.6 |     2.24478 |
|         2 | 17:54:52.6 |     4.66694 |
|         3 | 17:54:54.6 |     6.66461 |
|         4 | 17:54:56.6 |     8.64790 |
|         5 | 17:54:58.6 |    10.99491 |
|         6 | 17:55:00.6 |    13.23049 |
|         7 | 17:55:02.6 |    15.09944 |
|         8 | 17:55:04.6 |    17.26870 |
|         9 | 17:55:06.6 |    19.20524 |
|        10 | 17:55:08.6 |    21.33919 |
+-----------+------------+-------------+
generator count ['b55776f1'] (scan num: 960)
[6]:
('b55776f1-0a02-464c-9762-fe807c90c014',)
../_images/example__watch_temperature_11_2.png

Let’s reduce the update interval that EPICS uses, to the shortest interval EPICS provides, so our measurements do not take so long.

[7]:
# print the list of possible values
print(temperature.scanning_rate.enum_strs)

# set the shortest interval, by name
temperature.scanning_rate.put(".1 second")
temperature.scanning_rate.get(as_string=True)
('Passive', 'Event', 'I/O Intr', '10 second', '5 second', '2 second', '1 second', '.5 second', '.2 second', '.1 second')
[7]:
'2 second'

Collect our measurements#

Repeat the measurement, this time with more observations.

Also, we’ll add a metadata key:value pair so that later we can find this data more easily in the database. Most plans take a md={} kwarg where the value is a Python dictionary with the key:value pair(s) to add to the run’s metadata.

[8]:
RE(bp.count([temperature], num=50, delay=0.1, md={"motive": "tscan"}))


Transient Scan ID: 961     Time: 2023-04-13 17:55:11
Persistent Unique Scan ID: '51779bd1-f506-4622-8a89-f6002f281de4'
New stream: 'label_start_motor'
New stream: 'primary'
+-----------+------------+-------------+
|   seq_num |       time | temperature |
+-----------+------------+-------------+
|         1 | 17:55:11.1 |    24.67653 |
|         2 | 17:55:11.2 |    25.31126 |
|         3 | 17:55:11.3 |    25.33679 |
|         4 | 17:55:11.4 |    25.47115 |
|         5 | 17:55:11.5 |    25.43527 |
|         6 | 17:55:11.6 |    25.44133 |
|         7 | 17:55:11.7 |    24.72783 |
|         8 | 17:55:11.8 |    24.95423 |
|         9 | 17:55:11.9 |    25.01724 |
|        10 | 17:55:12.0 |    24.70978 |
|        11 | 17:55:12.1 |    25.28183 |
|        12 | 17:55:12.2 |    25.21415 |
|        13 | 17:55:12.3 |    25.47186 |
|        14 | 17:55:12.4 |    24.53467 |
|        15 | 17:55:12.5 |    24.83840 |
|        16 | 17:55:12.6 |    25.45067 |
|        17 | 17:55:12.7 |    25.04357 |
|        18 | 17:55:12.8 |    24.97578 |
|        19 | 17:55:12.9 |    25.04635 |
|        20 | 17:55:13.0 |    25.32997 |
|        21 | 17:55:13.1 |    25.01504 |
|        22 | 17:55:13.2 |    25.23326 |
|        23 | 17:55:13.3 |    24.75829 |
|        24 | 17:55:13.4 |    24.64411 |
|        25 | 17:55:13.5 |    24.60384 |
|        26 | 17:55:13.6 |    24.87308 |
|        27 | 17:55:13.7 |    24.62004 |
|        28 | 17:55:13.8 |    24.71509 |
|        29 | 17:55:13.9 |    25.42215 |
|        30 | 17:55:14.0 |    25.08979 |
|        31 | 17:55:14.1 |    24.82930 |
|        32 | 17:55:14.2 |    24.50919 |
|        33 | 17:55:14.3 |    25.06507 |
|        34 | 17:55:14.4 |    24.93471 |
|        35 | 17:55:14.5 |    25.09922 |
|        36 | 17:55:14.6 |    25.28540 |
|        37 | 17:55:14.7 |    24.68781 |
|        38 | 17:55:14.8 |    24.59773 |
|        39 | 17:55:14.9 |    24.75489 |
|        40 | 17:55:15.0 |    25.42776 |
|        41 | 17:55:15.1 |    24.93259 |
|        42 | 17:55:15.2 |    24.84777 |
|        43 | 17:55:15.3 |    24.81318 |
|        44 | 17:55:15.4 |    24.78414 |
|        45 | 17:55:15.5 |    25.26970 |
|        46 | 17:55:15.6 |    24.61772 |
|        47 | 17:55:15.7 |    25.15956 |
|        48 | 17:55:15.8 |    24.77626 |
|        49 | 17:55:15.9 |    24.84089 |
+-----------+------------+-------------+
|   seq_num |       time | temperature |
+-----------+------------+-------------+
|        50 | 17:55:16.0 |    25.26352 |
+-----------+------------+-------------+
generator count ['51779bd1'] (scan num: 961)
[8]:
('51779bd1-f506-4622-8a89-f6002f281de4',)
../_images/example__watch_temperature_15_2.png

Get the data (from the database)#

Let’s get the measured data from the database now. It’s the most recent bluesky run, so an index of -1 will identify it.

[9]:
dataset = cat[-1].primary.read()

Let’s get the values as a 1-D numpy array.

[10]:
T = dataset["temperature"].values

Compute the statistics#

Use the methods of numpy arrays to compute maximum, mean, standard deviation, and minimum. Looks nicer in a table.

[11]:
import pyRestTable

table = pyRestTable.Table()
table.addLabel("measure")
table.addLabel("value")
table.addRow(("max", T.max()))
table.addRow(("mean", T.mean()))
table.addRow(("dev", T.std()))
table.addRow(("min", T.min()))
table.addRow(("#", len(T)))
print(table)
======= ==================
measure value
======= ==================
max     25.471862363622492
mean    24.994794842450602
dev     0.2892339831470962
min     24.50918593118181
#       50
======= ==================

Let’s replot that data for comparison.

[12]:
dataset["temperature"].plot(marker="o", color="red")
[12]:
[<matplotlib.lines.Line2D at 0x7f6b49565bd0>]
../_images/example__watch_temperature_23_1.png