genie-python 15.1.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- genie_python/.pylintrc +539 -0
- genie_python/__init__.py +1 -0
- genie_python/block_names.py +123 -0
- genie_python/channel_access_exceptions.py +45 -0
- genie_python/genie.py +2462 -0
- genie_python/genie_advanced.py +418 -0
- genie_python/genie_alerts.py +195 -0
- genie_python/genie_api_setup.py +451 -0
- genie_python/genie_blockserver.py +64 -0
- genie_python/genie_cachannel_wrapper.py +545 -0
- genie_python/genie_change_cache.py +151 -0
- genie_python/genie_dae.py +2218 -0
- genie_python/genie_epics_api.py +906 -0
- genie_python/genie_experimental_data.py +186 -0
- genie_python/genie_logging.py +200 -0
- genie_python/genie_p4p_wrapper.py +203 -0
- genie_python/genie_plot.py +77 -0
- genie_python/genie_pre_post_cmd_manager.py +21 -0
- genie_python/genie_pv_connection_protocol.py +36 -0
- genie_python/genie_script_checker.py +507 -0
- genie_python/genie_script_generator.py +212 -0
- genie_python/genie_simulate.py +69 -0
- genie_python/genie_simulate_impl.py +1265 -0
- genie_python/genie_startup.py +29 -0
- genie_python/genie_toggle_settings.py +58 -0
- genie_python/genie_wait_for_move.py +154 -0
- genie_python/genie_waitfor.py +576 -0
- genie_python/matplotlib_backend/__init__.py +0 -0
- genie_python/matplotlib_backend/ibex_websocket_backend.py +366 -0
- genie_python/mysql_abstraction_layer.py +272 -0
- genie_python/run_tests.py +56 -0
- genie_python/scanning_instrument_pylint_plugin.py +31 -0
- genie_python/typings/CaChannel/CaChannel.pyi +893 -0
- genie_python/typings/CaChannel/__init__.pyi +9 -0
- genie_python/typings/CaChannel/_version.pyi +6 -0
- genie_python/typings/CaChannel/ca.pyi +31 -0
- genie_python/utilities.py +406 -0
- genie_python/version.py +1 -0
- genie_python-15.1.0rc1.dist-info/LICENSE +28 -0
- genie_python-15.1.0rc1.dist-info/METADATA +95 -0
- genie_python-15.1.0rc1.dist-info/RECORD +43 -0
- genie_python-15.1.0rc1.dist-info/WHEEL +5 -0
- genie_python-15.1.0rc1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2218 @@
|
|
|
1
|
+
from __future__ import absolute_import, print_function
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import xml.etree.ElementTree as ET
|
|
7
|
+
import zlib
|
|
8
|
+
from binascii import hexlify
|
|
9
|
+
from builtins import str
|
|
10
|
+
from collections import namedtuple
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
from io import open
|
|
14
|
+
from stat import S_IREAD, S_IWUSR
|
|
15
|
+
from time import sleep, strftime
|
|
16
|
+
from typing import TYPE_CHECKING, cast
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import numpy.typing as npt
|
|
20
|
+
import psutil
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from CaChannel._ca import AlarmCondition, AlarmSeverity
|
|
24
|
+
except ImportError:
|
|
25
|
+
from caffi.ca import AlarmCondition, AlarmSeverity
|
|
26
|
+
|
|
27
|
+
from genie_python.genie_cachannel_wrapper import CaChannelWrapper
|
|
28
|
+
from genie_python.genie_change_cache import ChangeCache
|
|
29
|
+
from genie_python.utilities import (
|
|
30
|
+
compress_and_hex,
|
|
31
|
+
dehex_and_decompress,
|
|
32
|
+
get_correct_path,
|
|
33
|
+
require_runstate,
|
|
34
|
+
waveform_to_string,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from genie_python.genie import PVValue
|
|
39
|
+
from genie_python.genie_epics_api import API
|
|
40
|
+
|
|
41
|
+
## for beginrun etc. there exists both the PV specified here and also a PV with
|
|
42
|
+
## an '_' appended that skips the additional pre/post commands defined in
|
|
43
|
+
## the IOC write and is used for when prepost=False is specified for command
|
|
44
|
+
DAE_PVS_LOOKUP = {
|
|
45
|
+
"runstate": "DAE:RUNSTATE",
|
|
46
|
+
"runstate_str": "DAE:RUNSTATE_STR",
|
|
47
|
+
"beginrun": "DAE:BEGINRUNEX",
|
|
48
|
+
"abortrun": "DAE:ABORTRUN",
|
|
49
|
+
"pauserun": "DAE:PAUSERUN",
|
|
50
|
+
"resumerun": "DAE:RESUMERUN",
|
|
51
|
+
"endrun": "DAE:ENDRUN",
|
|
52
|
+
"recoverrun": "DAE:RECOVERRUN",
|
|
53
|
+
"saverun": "DAE:SAVERUN",
|
|
54
|
+
"updaterun": "DAE:UPDATERUN",
|
|
55
|
+
"storerun": "DAE:STORERUN",
|
|
56
|
+
"snapshot": "DAE:SNAPSHOTCRPT",
|
|
57
|
+
"period_rbv": "DAE:PERIOD:RBV",
|
|
58
|
+
"period": "DAE:PERIOD",
|
|
59
|
+
"runnumber": "DAE:RUNNUMBER",
|
|
60
|
+
"numperiods": "DAE:NUMPERIODS",
|
|
61
|
+
"events": "DAE:EVENTS",
|
|
62
|
+
"mevents": "DAE:MEVENTS",
|
|
63
|
+
"totalcounts": "DAE:TOTALCOUNTS",
|
|
64
|
+
"goodframes": "DAE:GOODFRAMES",
|
|
65
|
+
"goodframesperiod": "DAE:GOODFRAMES_PD",
|
|
66
|
+
"rawframes": "DAE:RAWFRAMES",
|
|
67
|
+
"uamps": "DAE:GOODUAH",
|
|
68
|
+
"histmemory": "DAE:HISTMEMORY",
|
|
69
|
+
"spectrasum": "DAE:SPECTRASUM",
|
|
70
|
+
"uampsperiod": "DAE:GOODUAH_PD",
|
|
71
|
+
"title": "DAE:TITLE",
|
|
72
|
+
"title_sp": "DAE:TITLE:SP",
|
|
73
|
+
"display_title": "DAE:TITLE:DISPLAY",
|
|
74
|
+
"rbnum": "ED:RBNUMBER",
|
|
75
|
+
"rbnum_sp": "ED:RBNUMBER:SP",
|
|
76
|
+
"period_sp": "DAE:PERIOD:SP",
|
|
77
|
+
"users": "ED:SURNAME",
|
|
78
|
+
"users_table_sp": "ED:USERNAME:SP",
|
|
79
|
+
"users_dae_sp": "ED:USERNAME:DAE:SP",
|
|
80
|
+
"users_surname_sp": "ED:SURNAME",
|
|
81
|
+
"starttime": "DAE:STARTTIME",
|
|
82
|
+
"npratio": "DAE:NPRATIO",
|
|
83
|
+
"timingsource": "DAE:DAETIMINGSOURCE",
|
|
84
|
+
"periodtype": "DAE:PERIODTYPE",
|
|
85
|
+
"isiscycle": "DAE:ISISCYCLE",
|
|
86
|
+
"rawframesperiod": "DAE:RAWFRAMES_PD",
|
|
87
|
+
"runduration": "DAE:RUNDURATION",
|
|
88
|
+
"rundurationperiod": "DAE:RUNDURATION_PD",
|
|
89
|
+
"numtimechannels": "DAE:NUMTIMECHANNELS",
|
|
90
|
+
"memoryused": "DAE:DAEMEMORYUSED",
|
|
91
|
+
"numspectra": "DAE:NUMSPECTRA",
|
|
92
|
+
"monitorcounts": "DAE:MONITORCOUNTS",
|
|
93
|
+
"monitorspectrum": "DAE:MONITORSPECTRUM",
|
|
94
|
+
"periodseq": "DAE:PERIODSEQ",
|
|
95
|
+
"beamcurrent": "DAE:BEAMCURRENT",
|
|
96
|
+
"totaluamps": "DAE:TOTALUAMPS",
|
|
97
|
+
"totaldaecounts": "DAE:TOTALDAECOUNTS",
|
|
98
|
+
"monitorto": "DAE:MONITORTO",
|
|
99
|
+
"monitorfrom": "DAE:MONITORFROM",
|
|
100
|
+
"countrate": "DAE:COUNTRATE",
|
|
101
|
+
"eventmodefraction": "DAE:EVENTMODEFRACTION",
|
|
102
|
+
"daesettings": "DAE:DAESETTINGS",
|
|
103
|
+
"daesettings_sp": "DAE:DAESETTINGS:SP",
|
|
104
|
+
"tcbsettings": "DAE:TCBSETTINGS",
|
|
105
|
+
"tcbsettings_sp": "DAE:TCBSETTINGS:SP",
|
|
106
|
+
"periodsettings": "DAE:HARDWAREPERIODS",
|
|
107
|
+
"periodsettings_sp": "DAE:HARDWAREPERIODS:SP",
|
|
108
|
+
"getspectrum_x": "DAE:SPEC:{:d}:{:d}:X",
|
|
109
|
+
"getspectrum_x_size": "DAE:SPEC:{:d}:{:d}:X.NORD",
|
|
110
|
+
"getspectrum_y": "DAE:SPEC:{:d}:{:d}:Y",
|
|
111
|
+
"getspectrum_y_size": "DAE:SPEC:{:d}:{:d}:Y.NORD",
|
|
112
|
+
"getspectrum_yc": "DAE:SPEC:{:d}:{:d}:YC",
|
|
113
|
+
"getspectrum_yc_size": "DAE:SPEC:{:d}:{:d}:YC.NORD",
|
|
114
|
+
"errormessage": "DAE:ERRMSGS",
|
|
115
|
+
"allmessages": "DAE:ALLMSGS",
|
|
116
|
+
"statetrans": "DAE:STATETRANS",
|
|
117
|
+
"wiringtables": "DAE:WIRINGTABLES",
|
|
118
|
+
"spectratables": "DAE:SPECTRATABLES",
|
|
119
|
+
"detectortables": "DAE:DETECTORTABLES",
|
|
120
|
+
"periodfiles": "DAE:PERIODFILES",
|
|
121
|
+
"set_veto_true": "DAE:VETO:ENABLE:SP",
|
|
122
|
+
"set_veto_false": "DAE:VETO:DISABLE:SP",
|
|
123
|
+
"simulation_mode": "DAE:SIM_MODE",
|
|
124
|
+
"state_changing": "DAE:STATE:CHANGING",
|
|
125
|
+
"specintegrals": "DAE:SPECINTEGRALS",
|
|
126
|
+
"specintegrals_size": "DAE:SPECINTEGRALS.NORD",
|
|
127
|
+
"specdata": "DAE:SPECDATA",
|
|
128
|
+
"specdata_size": "DAE:SPECDATA.NORD",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
DAE_CONFIG_FILE_PATHS = [
|
|
132
|
+
r"C:\Labview modules\dae\icp_config.xml",
|
|
133
|
+
r"C:\Instrument\Apps\EPICS\ICP_Binaries\icp_config.xml",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
END_NOW_FILE_PATH = "C:\\data\\end_now.dae"
|
|
137
|
+
|
|
138
|
+
CLEAR_VETO = "clearall"
|
|
139
|
+
SMP_VETO = "smp"
|
|
140
|
+
TS2_VETO = "ts2"
|
|
141
|
+
HZ50_VETO = "hz50"
|
|
142
|
+
EXT0_VETO = "ext0"
|
|
143
|
+
EXT1_VETO = "ext1"
|
|
144
|
+
EXT2_VETO = "ext2"
|
|
145
|
+
EXT3_VETO = "ext3"
|
|
146
|
+
FIFO_VETO = "fifo"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Dae(object):
|
|
150
|
+
"""
|
|
151
|
+
Communications with the DAE pvs.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, api: "API", prefix: str = "") -> None:
|
|
155
|
+
"""
|
|
156
|
+
The constructor.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
api(genie_python.genie_epics_api.API): the API used for communication
|
|
160
|
+
prefix: the PV prefix
|
|
161
|
+
"""
|
|
162
|
+
self.api = api
|
|
163
|
+
self.inst_prefix = prefix
|
|
164
|
+
self.in_change = False
|
|
165
|
+
self.change_cache = ChangeCache()
|
|
166
|
+
self.verbose = False
|
|
167
|
+
|
|
168
|
+
# this is the default value to ensure dae settings are
|
|
169
|
+
# written before returning, only changed for testing
|
|
170
|
+
self.wait_for_completion_callback_dae_settings = True
|
|
171
|
+
|
|
172
|
+
def _prefix_pv_name(self, name: str) -> str:
|
|
173
|
+
"""
|
|
174
|
+
Adds the prefix to the PV name.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
name: the name to be prefixed
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
string: the full PV name
|
|
181
|
+
"""
|
|
182
|
+
if self.inst_prefix is not None:
|
|
183
|
+
name = self.inst_prefix + name
|
|
184
|
+
return name
|
|
185
|
+
|
|
186
|
+
def _get_dae_pv_name(self, name: str, base: bool = False) -> str:
|
|
187
|
+
"""
|
|
188
|
+
Retrieves the full pv name of a DAE variable.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
name: the short name for the DAE variable
|
|
192
|
+
base: return the underlying action PV name
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
string: the full PV name
|
|
196
|
+
"""
|
|
197
|
+
if base:
|
|
198
|
+
return self._prefix_pv_name(DAE_PVS_LOOKUP[name.lower()]) + "_"
|
|
199
|
+
else:
|
|
200
|
+
return self._prefix_pv_name(DAE_PVS_LOOKUP[name.lower()])
|
|
201
|
+
|
|
202
|
+
def _get_pv_value(
|
|
203
|
+
self, name: str, to_string: bool = False, use_numpy: bool | None = None
|
|
204
|
+
) -> "PVValue":
|
|
205
|
+
"""
|
|
206
|
+
Gets a PV's value.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
name: the PV name
|
|
210
|
+
to_string: whether to convert the value to a string
|
|
211
|
+
use_numpy (None|boolean): True use numpy to return arrays, False return a list;
|
|
212
|
+
None for use the default
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
object: the PV's value
|
|
216
|
+
"""
|
|
217
|
+
return self.api.get_pv_value(name, to_string, use_numpy=use_numpy)
|
|
218
|
+
|
|
219
|
+
def _set_pv_value(self, name: str, value: "PVValue", wait: bool = False) -> None:
|
|
220
|
+
"""
|
|
221
|
+
Sets a PV value via the API.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
name: the PV name
|
|
225
|
+
value: the value to set
|
|
226
|
+
wait: whether to wait for it to be set before returning
|
|
227
|
+
"""
|
|
228
|
+
self.api.set_pv_value(name, value, wait)
|
|
229
|
+
|
|
230
|
+
def _check_for_runstate_error(self, pv: str, header: str = "") -> None:
|
|
231
|
+
"""
|
|
232
|
+
Check for errors on the run state PV.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
pv: the PV name
|
|
236
|
+
header: information to include in the exception raised.
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
Exception: if there is an error on the specified PV
|
|
240
|
+
|
|
241
|
+
"""
|
|
242
|
+
status = self._get_pv_value(pv + ".STAT", to_string=True)
|
|
243
|
+
if status != "NO_ALARM":
|
|
244
|
+
raise Exception(
|
|
245
|
+
"{} {}".format(
|
|
246
|
+
header.strip(),
|
|
247
|
+
self._get_pv_value(self._get_dae_pv_name("errormessage"), to_string=True),
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def _print_verbose_messages(self) -> None:
|
|
252
|
+
"""
|
|
253
|
+
Prints all the messages.
|
|
254
|
+
"""
|
|
255
|
+
msgs = self._get_pv_value(self._get_dae_pv_name("allmessages"), to_string=True)
|
|
256
|
+
print(msgs)
|
|
257
|
+
|
|
258
|
+
def _write_to_end_now_file(self, file_content: str) -> None:
|
|
259
|
+
"""
|
|
260
|
+
Creates the end_now file if it doesn't exist and writes text to it, overwriting
|
|
261
|
+
any existing content
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
file_content: the new file content
|
|
265
|
+
"""
|
|
266
|
+
with open(END_NOW_FILE_PATH, "w+") as f:
|
|
267
|
+
f.write(file_content)
|
|
268
|
+
|
|
269
|
+
def set_verbose(self, verbose: bool) -> None:
|
|
270
|
+
"""
|
|
271
|
+
Sets the verbosity of the DAE messages printed
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
verbose: bool setting
|
|
275
|
+
|
|
276
|
+
Raise:
|
|
277
|
+
Exception: if the supplied value is not a bool
|
|
278
|
+
"""
|
|
279
|
+
if isinstance(verbose, bool):
|
|
280
|
+
self.verbose = verbose
|
|
281
|
+
if verbose:
|
|
282
|
+
print("Setting DAE messages to verbose mode")
|
|
283
|
+
else:
|
|
284
|
+
print("Setting DAE messages to non-verbose mode")
|
|
285
|
+
else:
|
|
286
|
+
raise Exception("Value must be boolean")
|
|
287
|
+
|
|
288
|
+
@require_runstate(["SETUP"])
|
|
289
|
+
def begin_run(
|
|
290
|
+
self,
|
|
291
|
+
period: int | None = None,
|
|
292
|
+
meas_id: str | None = None,
|
|
293
|
+
meas_type: str | None = None,
|
|
294
|
+
meas_subid: str | None = None,
|
|
295
|
+
sample_id: str | None = None,
|
|
296
|
+
delayed: bool = False,
|
|
297
|
+
quiet: bool = False,
|
|
298
|
+
paused: bool = False,
|
|
299
|
+
prepost: bool = True,
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Starts a data collection run.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
period - the period to begin data collection in [optional]
|
|
305
|
+
meas_id - the measurement id [optional]
|
|
306
|
+
meas_type - the type of measurement [optional]
|
|
307
|
+
meas_subid - the measurement sub-id[optional]
|
|
308
|
+
sample_id - the sample id [optional]
|
|
309
|
+
delayed - puts the period card to into delayed start mode [optional]
|
|
310
|
+
quiet - suppress the output to the screen [optional]
|
|
311
|
+
paused - begin in the paused state [optional]
|
|
312
|
+
prepost - run pre and post commands [optional]
|
|
313
|
+
"""
|
|
314
|
+
if self.in_change:
|
|
315
|
+
raise Exception("Cannot start in CHANGE mode, type change_finish()")
|
|
316
|
+
|
|
317
|
+
# Set sample parameters
|
|
318
|
+
sample_pars = {
|
|
319
|
+
"MEAS:ID": meas_id,
|
|
320
|
+
"MEAS:TYPE": meas_type,
|
|
321
|
+
"MEAS:SUBID": meas_subid,
|
|
322
|
+
"ID": sample_id,
|
|
323
|
+
}
|
|
324
|
+
for pv, value in sample_pars.items():
|
|
325
|
+
if value is not None:
|
|
326
|
+
self.api.set_sample_par(pv, str(value))
|
|
327
|
+
|
|
328
|
+
# Check PV exists
|
|
329
|
+
val = self._get_pv_value(self._get_dae_pv_name("beginrun"))
|
|
330
|
+
if val is None:
|
|
331
|
+
raise Exception("begin_run: could not connect to DAE")
|
|
332
|
+
|
|
333
|
+
if period is not None:
|
|
334
|
+
# Set the period before starting the run
|
|
335
|
+
self.set_period(period)
|
|
336
|
+
|
|
337
|
+
run_number = self.get_run_number()
|
|
338
|
+
if not quiet:
|
|
339
|
+
if self.get_simulation_mode():
|
|
340
|
+
self.simulation_mode_warning()
|
|
341
|
+
print("** Beginning Run {} at {}".format(run_number, strftime("%H:%M:%S %d/%m/%y ")))
|
|
342
|
+
## don't fail begin() if we are unabel to print rb/user details
|
|
343
|
+
try:
|
|
344
|
+
print(
|
|
345
|
+
"The following details will currently be used to determine"
|
|
346
|
+
"ownership of the data file"
|
|
347
|
+
)
|
|
348
|
+
print("* Proposal Number: {}".format(self.get_rb_number()))
|
|
349
|
+
print("* Experiment Team: {}".format(self.get_users()))
|
|
350
|
+
print("If this is incorrect, you can change it any time before the run is ENDed\n")
|
|
351
|
+
except Exception as e:
|
|
352
|
+
print(f"WARNING: Unable to read RB/Users from service: {e}")
|
|
353
|
+
self.api.logger.log_info_msg(f"BEGIN: run number: {run_number}")
|
|
354
|
+
try:
|
|
355
|
+
self.api.logger.log_info_msg(
|
|
356
|
+
f"BEGIN: Proposal number: {self.get_rb_number()} Team: {self.get_users()}"
|
|
357
|
+
)
|
|
358
|
+
except Exception as e:
|
|
359
|
+
self.api.logger.log_error_msg(f"BEGIN: Unable to read RB/Users from service: {e}")
|
|
360
|
+
|
|
361
|
+
# By choosing the value sent to the begin PV it can set pause and/or delayed
|
|
362
|
+
options = 0
|
|
363
|
+
if paused:
|
|
364
|
+
options += 1
|
|
365
|
+
if delayed:
|
|
366
|
+
options += 2
|
|
367
|
+
|
|
368
|
+
_cancel_monitor_fn = None
|
|
369
|
+
try:
|
|
370
|
+
|
|
371
|
+
def callback_function(
|
|
372
|
+
message: str, severity: AlarmSeverity, status: AlarmCondition
|
|
373
|
+
) -> None:
|
|
374
|
+
"""
|
|
375
|
+
Args:
|
|
376
|
+
message: the error message from the DAE as character waveform
|
|
377
|
+
severity: required by the CaChannelWrapper.add_monitor
|
|
378
|
+
status: required by the CaChannelWrapper.add_monitor
|
|
379
|
+
"""
|
|
380
|
+
message = waveform_to_string(message)
|
|
381
|
+
if message:
|
|
382
|
+
print("ISISICP error: {}".format(message))
|
|
383
|
+
|
|
384
|
+
_cancel_monitor_fn = CaChannelWrapper.add_monitor(
|
|
385
|
+
self._get_dae_pv_name("errormessage"), callback_function
|
|
386
|
+
)
|
|
387
|
+
# actually do begin
|
|
388
|
+
self._set_pv_value(
|
|
389
|
+
self._get_dae_pv_name("beginrun", base=not prepost), options, wait=True
|
|
390
|
+
)
|
|
391
|
+
finally:
|
|
392
|
+
if _cancel_monitor_fn is not None:
|
|
393
|
+
_cancel_monitor_fn()
|
|
394
|
+
|
|
395
|
+
def simulation_mode_warning(self) -> None:
|
|
396
|
+
"""
|
|
397
|
+
Warn user they are in simulation mode.
|
|
398
|
+
"""
|
|
399
|
+
print("\n=========== RUNNING IN SIMULATION MODE ===========\n")
|
|
400
|
+
print("Simulation mode can be stopped using: \n")
|
|
401
|
+
print(" >>>set_dae_simulation_mode(False) \n")
|
|
402
|
+
print("==================================================\n")
|
|
403
|
+
|
|
404
|
+
def post_begin_check(self, verbose: bool = False) -> None:
|
|
405
|
+
"""
|
|
406
|
+
Checks the BEGIN PV for errors after beginning a run.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
verbose: whether to print verbosely
|
|
410
|
+
"""
|
|
411
|
+
self._check_for_runstate_error(self._get_dae_pv_name("beginrun", base=True), "BEGIN")
|
|
412
|
+
if verbose or self.verbose:
|
|
413
|
+
self._print_verbose_messages()
|
|
414
|
+
|
|
415
|
+
@require_runstate(["RUNNING", "VETOING", "WAITING", "PAUSED"])
|
|
416
|
+
def abort_run(self, prepost: bool = True) -> None:
|
|
417
|
+
"""
|
|
418
|
+
Abort the current run.
|
|
419
|
+
prepost - run pre and post commands [optional]
|
|
420
|
+
"""
|
|
421
|
+
print(
|
|
422
|
+
(
|
|
423
|
+
"** Aborting Run {} at {} "
|
|
424
|
+
"(the run will not be saved, call g.recover() to undo this)".format(
|
|
425
|
+
self.get_run_number(), strftime("%H:%M:%S %d/%m/%y ")
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
self._set_pv_value(self._get_dae_pv_name("abortrun", base=not prepost), 1, wait=True)
|
|
430
|
+
|
|
431
|
+
def post_abort_check(self, verbose: bool = False) -> None:
|
|
432
|
+
"""
|
|
433
|
+
Checks the ABORT PV for errors after aborting a run.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
verbose: whether to print verbosely
|
|
437
|
+
"""
|
|
438
|
+
self._check_for_runstate_error(self._get_dae_pv_name("abortrun", base=True), "ABORT")
|
|
439
|
+
if verbose or self.verbose:
|
|
440
|
+
self._print_verbose_messages()
|
|
441
|
+
|
|
442
|
+
@require_runstate(["RUNNING", "VETOING", "WAITING", "PAUSED", "ENDING"])
|
|
443
|
+
def end_run(
|
|
444
|
+
self,
|
|
445
|
+
verbose: bool = False,
|
|
446
|
+
quiet: bool = False,
|
|
447
|
+
immediate: bool = False,
|
|
448
|
+
prepost: bool = True,
|
|
449
|
+
) -> None:
|
|
450
|
+
"""
|
|
451
|
+
End the current run.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
verbose: whether to print verbosely
|
|
455
|
+
quiet: suppress the output to the screen [optional]
|
|
456
|
+
immediate: end immediately, without waiting for a period sequence to finish [optional]
|
|
457
|
+
prepost: run pre and post commands [optional]
|
|
458
|
+
"""
|
|
459
|
+
if self.get_run_state() == "ENDING" and not immediate:
|
|
460
|
+
print(
|
|
461
|
+
"Please specify the 'immediate=True' flag to end a run " "while in the ENDING state"
|
|
462
|
+
)
|
|
463
|
+
return
|
|
464
|
+
|
|
465
|
+
run_number = self.get_run_number()
|
|
466
|
+
if not quiet:
|
|
467
|
+
print(("** Ending Run {} at {}".format(run_number, strftime("%H:%M:%S %d/%m/%y "))))
|
|
468
|
+
|
|
469
|
+
self.api.logger.log_info_msg(f"END: run number: {run_number}")
|
|
470
|
+
try:
|
|
471
|
+
self.api.logger.log_info_msg(
|
|
472
|
+
f"END: Proposal number: {self.get_rb_number()} Team: {self.get_users()}"
|
|
473
|
+
)
|
|
474
|
+
except Exception as e:
|
|
475
|
+
self.api.logger.log_error_msg(f"END: Unable to read RB/Users from service: {e}")
|
|
476
|
+
|
|
477
|
+
if immediate:
|
|
478
|
+
self._write_to_end_now_file("1")
|
|
479
|
+
|
|
480
|
+
self._set_pv_value(self._get_dae_pv_name("endrun", base=not prepost), 1, wait=True)
|
|
481
|
+
if verbose or self.verbose:
|
|
482
|
+
self._print_verbose_messages()
|
|
483
|
+
|
|
484
|
+
def post_end_check(self, verbose: bool = False) -> None:
|
|
485
|
+
"""
|
|
486
|
+
Checks the END PV for errors after ending a run.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
verbose: whether to print verbosely
|
|
490
|
+
"""
|
|
491
|
+
self._check_for_runstate_error(self._get_dae_pv_name("endrun", base=True), "END")
|
|
492
|
+
if verbose or self.verbose:
|
|
493
|
+
self._print_verbose_messages()
|
|
494
|
+
|
|
495
|
+
def recover_run(self) -> None:
|
|
496
|
+
"""
|
|
497
|
+
Recovers the run if it has been aborted.
|
|
498
|
+
|
|
499
|
+
The command should be run before the next run is started.
|
|
500
|
+
Note: the run will be recovered in the paused state.
|
|
501
|
+
"""
|
|
502
|
+
self._set_pv_value(self._get_dae_pv_name("recoverrun"), 1, wait=True)
|
|
503
|
+
|
|
504
|
+
def post_recover_check(self, verbose: bool = False) -> None:
|
|
505
|
+
"""
|
|
506
|
+
Checks the RECOVER PV for errors after recovering a run.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
verbose: whether to print verbosely
|
|
510
|
+
"""
|
|
511
|
+
self._check_for_runstate_error(self._get_dae_pv_name("recoverrun"), "RECOVER")
|
|
512
|
+
if verbose or self.verbose:
|
|
513
|
+
self._print_verbose_messages()
|
|
514
|
+
|
|
515
|
+
def update_store_run(self) -> None:
|
|
516
|
+
"""
|
|
517
|
+
Performs an update and a store operation in a combined operation.
|
|
518
|
+
|
|
519
|
+
This is more efficient than doing the commands separately.
|
|
520
|
+
"""
|
|
521
|
+
print(
|
|
522
|
+
("** Saving Run {} at {}".format(self.get_run_number(), strftime("%H:%M:%S %d/%m/%y ")))
|
|
523
|
+
)
|
|
524
|
+
self._set_pv_value(self._get_dae_pv_name("saverun"), 1, wait=True)
|
|
525
|
+
|
|
526
|
+
def post_update_store_check(self, verbose: bool = False) -> None:
|
|
527
|
+
"""
|
|
528
|
+
Checks the associated PV for errors after an update store.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
verbose: whether to print verbosely
|
|
532
|
+
"""
|
|
533
|
+
self._check_for_runstate_error(self._get_dae_pv_name("saverun"), "SAVE")
|
|
534
|
+
if verbose or self.verbose:
|
|
535
|
+
self._print_verbose_messages()
|
|
536
|
+
|
|
537
|
+
def update_run(self) -> None:
|
|
538
|
+
"""
|
|
539
|
+
Data is loaded from the DAE into the computer memory, but is not written to disk.
|
|
540
|
+
"""
|
|
541
|
+
self._set_pv_value(self._get_dae_pv_name("updaterun"), 1, wait=True)
|
|
542
|
+
|
|
543
|
+
def post_update_check(self, verbose: bool = False) -> None:
|
|
544
|
+
"""
|
|
545
|
+
Checks the associated PV for errors after an update.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
verbose: whether to print verbosely
|
|
549
|
+
"""
|
|
550
|
+
self._check_for_runstate_error(self._get_dae_pv_name("updaterun"), "UPDATE")
|
|
551
|
+
if verbose or self.verbose:
|
|
552
|
+
self._print_verbose_messages()
|
|
553
|
+
|
|
554
|
+
@require_runstate(["RUNNING", "VETOING", "WAITING", "PAUSED"])
|
|
555
|
+
def store_run(self) -> None:
|
|
556
|
+
"""
|
|
557
|
+
Data loaded into memory by a previous update_run command is now written to disk.
|
|
558
|
+
"""
|
|
559
|
+
self._set_pv_value(self._get_dae_pv_name("storerun"), 1, wait=True)
|
|
560
|
+
|
|
561
|
+
def post_store_check(self, verbose: bool = False) -> None:
|
|
562
|
+
"""
|
|
563
|
+
Checks the associated PV for errors after a store.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
verbose: whether to print verbosely
|
|
567
|
+
"""
|
|
568
|
+
self._check_for_runstate_error(self._get_dae_pv_name("storerun"), "STORE")
|
|
569
|
+
if verbose or self.verbose:
|
|
570
|
+
self._print_verbose_messages()
|
|
571
|
+
|
|
572
|
+
def snapshot_crpt(self, filename: str) -> None:
|
|
573
|
+
"""
|
|
574
|
+
Save a snapshot of the CRPT.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
filename - the name and location to save the file(s) to
|
|
578
|
+
"""
|
|
579
|
+
self._set_pv_value(self._get_dae_pv_name("snapshot"), filename, wait=True)
|
|
580
|
+
|
|
581
|
+
def post_snapshot_check(self, verbose: bool = False) -> None:
|
|
582
|
+
"""
|
|
583
|
+
Checks the associated PV for errors after a snapshot.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
verbose: whether to print verbosely
|
|
587
|
+
"""
|
|
588
|
+
self._check_for_runstate_error(self._get_dae_pv_name("snapshot"), "SNAPSHOTCRPT")
|
|
589
|
+
if verbose or self.verbose:
|
|
590
|
+
self._print_verbose_messages()
|
|
591
|
+
|
|
592
|
+
@require_runstate(["RUNNING", "VETOING", "WAITING", "PAUSING"])
|
|
593
|
+
def pause_run(self, immediate: bool = False, prepost: bool = True) -> None:
|
|
594
|
+
"""
|
|
595
|
+
Pause the current run.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
immediate: pause immediately, without waiting for a period sequence to complete
|
|
599
|
+
prepost: run pre and post commands
|
|
600
|
+
"""
|
|
601
|
+
if self.get_run_state() == "PAUSING" and not immediate:
|
|
602
|
+
print(
|
|
603
|
+
"Please specify the 'immediate=True' flag "
|
|
604
|
+
"to pause a run while in the PAUSING state"
|
|
605
|
+
)
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
print(
|
|
609
|
+
(
|
|
610
|
+
"** Pausing Run {} at {}".format(
|
|
611
|
+
self.get_run_number(), strftime("%H:%M:%S %d/%m/%y ")
|
|
612
|
+
)
|
|
613
|
+
)
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
if immediate:
|
|
617
|
+
self._write_to_end_now_file("1")
|
|
618
|
+
|
|
619
|
+
self._set_pv_value(self._get_dae_pv_name("pauserun", base=not prepost), 1, wait=True)
|
|
620
|
+
|
|
621
|
+
def post_pause_check(self, verbose: bool = False) -> None:
|
|
622
|
+
"""
|
|
623
|
+
Checks the PAUSE PV for errors after pausing.
|
|
624
|
+
|
|
625
|
+
Args:
|
|
626
|
+
verbose: whether to print verbosely
|
|
627
|
+
"""
|
|
628
|
+
self._check_for_runstate_error(self._get_dae_pv_name("pauserun", base=True), "PAUSE")
|
|
629
|
+
if verbose or self.verbose:
|
|
630
|
+
self._print_verbose_messages()
|
|
631
|
+
|
|
632
|
+
@require_runstate(["PAUSED"])
|
|
633
|
+
def resume_run(self, prepost: bool = True) -> None:
|
|
634
|
+
"""
|
|
635
|
+
Resume the current run after it has been paused.
|
|
636
|
+
prepost - run pre and post commands [optional]
|
|
637
|
+
"""
|
|
638
|
+
print(
|
|
639
|
+
(
|
|
640
|
+
"** Resuming Run {} at {}".format(
|
|
641
|
+
self.get_run_number(), strftime("%H:%M:%S %d/%m/%y ")
|
|
642
|
+
)
|
|
643
|
+
)
|
|
644
|
+
)
|
|
645
|
+
self._set_pv_value(self._get_dae_pv_name("resumerun", base=not prepost), 1, wait=True)
|
|
646
|
+
|
|
647
|
+
def post_resume_check(self, verbose: bool = False) -> None:
|
|
648
|
+
"""
|
|
649
|
+
Checks the RESUME PV for errors after resuming.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
verbose: whether to print verbosely
|
|
653
|
+
"""
|
|
654
|
+
self._check_for_runstate_error(self._get_dae_pv_name("resumerun", base=True), "RESUME")
|
|
655
|
+
if verbose or self.verbose:
|
|
656
|
+
self._print_verbose_messages()
|
|
657
|
+
|
|
658
|
+
def get_run_state(self) -> str:
|
|
659
|
+
"""
|
|
660
|
+
Gets the current state of the DAE.
|
|
661
|
+
|
|
662
|
+
Note: this value can take a few seconds to update after a change of state.
|
|
663
|
+
|
|
664
|
+
Returns:
|
|
665
|
+
string: the current run state
|
|
666
|
+
|
|
667
|
+
Raises:
|
|
668
|
+
Exception: if cannot retrieve value
|
|
669
|
+
"""
|
|
670
|
+
try:
|
|
671
|
+
return self._get_pv_value(self._get_dae_pv_name("runstate"), to_string=True)
|
|
672
|
+
except IOError:
|
|
673
|
+
raise IOError("get_run_state: could not get run state")
|
|
674
|
+
|
|
675
|
+
def get_run_number(self) -> str:
|
|
676
|
+
"""
|
|
677
|
+
Gets the current run number.
|
|
678
|
+
|
|
679
|
+
Returns:
|
|
680
|
+
string: the current run number
|
|
681
|
+
"""
|
|
682
|
+
return self._get_pv_value(self._get_dae_pv_name("runnumber"))
|
|
683
|
+
|
|
684
|
+
def get_period_type(self) -> str:
|
|
685
|
+
"""
|
|
686
|
+
Gets the period type.
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
string: the period type
|
|
690
|
+
"""
|
|
691
|
+
return self._get_pv_value(self._get_dae_pv_name("periodtype"))
|
|
692
|
+
|
|
693
|
+
def get_period_seq(self) -> int:
|
|
694
|
+
"""
|
|
695
|
+
Gets the period sequence.
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
object: the period sequence
|
|
699
|
+
"""
|
|
700
|
+
return self._get_pv_value(self._get_dae_pv_name("periodseq"))
|
|
701
|
+
|
|
702
|
+
def get_period(self) -> int:
|
|
703
|
+
"""
|
|
704
|
+
Gets the current period number.
|
|
705
|
+
|
|
706
|
+
Returns:
|
|
707
|
+
int: the current period
|
|
708
|
+
"""
|
|
709
|
+
return self._get_pv_value(self._get_dae_pv_name("period"))
|
|
710
|
+
|
|
711
|
+
def get_num_periods(self) -> int:
|
|
712
|
+
"""
|
|
713
|
+
Gets the number of periods.
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
int: the number of periods
|
|
717
|
+
"""
|
|
718
|
+
return cast(int, self._get_pv_value(self._get_dae_pv_name("numperiods")))
|
|
719
|
+
|
|
720
|
+
def set_period(self, period: int) -> None:
|
|
721
|
+
"""
|
|
722
|
+
Change to the specified period.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
period: the number of the period to change to
|
|
726
|
+
|
|
727
|
+
Raises:
|
|
728
|
+
IOError: if the DAE can not set the period to the given number.
|
|
729
|
+
"""
|
|
730
|
+
run_state = self.get_run_state()
|
|
731
|
+
if run_state == "SETUP" or run_state == "PAUSED":
|
|
732
|
+
self._set_pv_value(self._get_dae_pv_name("period_sp"), period, wait=True)
|
|
733
|
+
|
|
734
|
+
if self.api.get_pv_alarm(self._get_dae_pv_name("period_sp")) == "INVALID":
|
|
735
|
+
raise IOError(
|
|
736
|
+
f"You are trying to set an invalid period number {period}! "
|
|
737
|
+
f"The number must be between 1 and {self.get_num_periods()}."
|
|
738
|
+
)
|
|
739
|
+
else:
|
|
740
|
+
raise ValueError("Cannot change period whilst running")
|
|
741
|
+
|
|
742
|
+
def get_uamps(self, period: bool = False) -> float:
|
|
743
|
+
"""
|
|
744
|
+
Returns the current number of micro-amp hours.
|
|
745
|
+
|
|
746
|
+
Args:
|
|
747
|
+
period: whether to return the micro-amp hours for the current period [optional]
|
|
748
|
+
"""
|
|
749
|
+
if period:
|
|
750
|
+
return self._get_pv_value(self._get_dae_pv_name("uampsperiod"))
|
|
751
|
+
else:
|
|
752
|
+
return self._get_pv_value(self._get_dae_pv_name("uamps"))
|
|
753
|
+
|
|
754
|
+
def get_events(self) -> int:
|
|
755
|
+
"""
|
|
756
|
+
Gets the total number of events for all the detectors.
|
|
757
|
+
|
|
758
|
+
Returns:
|
|
759
|
+
int: the total number of events
|
|
760
|
+
"""
|
|
761
|
+
return self._get_pv_value(self._get_dae_pv_name("events"))
|
|
762
|
+
|
|
763
|
+
def get_mevents(self) -> float:
|
|
764
|
+
"""
|
|
765
|
+
Gets the total number of millions of events for all the detectors.
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
float: the total number of millions of events
|
|
769
|
+
"""
|
|
770
|
+
return self._get_pv_value(self._get_dae_pv_name("mevents"))
|
|
771
|
+
|
|
772
|
+
def get_total_counts(self) -> int:
|
|
773
|
+
"""
|
|
774
|
+
Gets the total counts for the current run.
|
|
775
|
+
|
|
776
|
+
Returns:
|
|
777
|
+
int: the total counts
|
|
778
|
+
"""
|
|
779
|
+
return self._get_pv_value(self._get_dae_pv_name("totalcounts"))
|
|
780
|
+
|
|
781
|
+
def get_good_frames(self, period: bool = False) -> int:
|
|
782
|
+
"""
|
|
783
|
+
Gets the current number of good frames.
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
period: whether to get for the current period only [optional]
|
|
787
|
+
|
|
788
|
+
Returns:
|
|
789
|
+
int: the number of good frames
|
|
790
|
+
"""
|
|
791
|
+
if period:
|
|
792
|
+
return self._get_pv_value(self._get_dae_pv_name("goodframesperiod"))
|
|
793
|
+
else:
|
|
794
|
+
return self._get_pv_value(self._get_dae_pv_name("goodframes"))
|
|
795
|
+
|
|
796
|
+
def get_raw_frames(self, period: bool = False) -> int:
|
|
797
|
+
"""
|
|
798
|
+
Gets the current number of raw frames.
|
|
799
|
+
|
|
800
|
+
Args:
|
|
801
|
+
period: whether to get for the current period only [optional]
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
int: the number of raw frames
|
|
805
|
+
"""
|
|
806
|
+
if period:
|
|
807
|
+
return self._get_pv_value(self._get_dae_pv_name("rawframesperiod"))
|
|
808
|
+
else:
|
|
809
|
+
return self._get_pv_value(self._get_dae_pv_name("rawframes"))
|
|
810
|
+
|
|
811
|
+
def sum_all_dae_memory(self) -> int:
|
|
812
|
+
"""
|
|
813
|
+
Gets the sum of the counts in the DAE.
|
|
814
|
+
|
|
815
|
+
Returns:
|
|
816
|
+
int: the sum
|
|
817
|
+
"""
|
|
818
|
+
return self._get_pv_value(self._get_dae_pv_name("histmemory"))
|
|
819
|
+
|
|
820
|
+
def get_memory_used(self) -> int:
|
|
821
|
+
"""
|
|
822
|
+
Gets the DAE memory used.
|
|
823
|
+
|
|
824
|
+
Returns:
|
|
825
|
+
int: the memory used
|
|
826
|
+
"""
|
|
827
|
+
return self._get_pv_value(self._get_dae_pv_name("memoryused"))
|
|
828
|
+
|
|
829
|
+
def sum_all_spectra(self) -> int:
|
|
830
|
+
"""
|
|
831
|
+
Returns the sum of all the spectra in the DAE.
|
|
832
|
+
|
|
833
|
+
Returns:
|
|
834
|
+
int: the sum of spectra
|
|
835
|
+
"""
|
|
836
|
+
return self._get_pv_value(self._get_dae_pv_name("spectrasum"))
|
|
837
|
+
|
|
838
|
+
def get_num_spectra(self) -> int:
|
|
839
|
+
"""
|
|
840
|
+
Gets the number of spectra.
|
|
841
|
+
|
|
842
|
+
Returns:
|
|
843
|
+
int: the number of spectra
|
|
844
|
+
"""
|
|
845
|
+
return cast(int, self._get_pv_value(self._get_dae_pv_name("numspectra")))
|
|
846
|
+
|
|
847
|
+
def get_rb_number(self) -> str:
|
|
848
|
+
"""
|
|
849
|
+
Gets the RB number for the current run.
|
|
850
|
+
|
|
851
|
+
Returns:
|
|
852
|
+
string: the current RB number
|
|
853
|
+
"""
|
|
854
|
+
return self._get_pv_value(self._get_dae_pv_name("rbnum"))
|
|
855
|
+
|
|
856
|
+
def set_rb_number(self, rbno: str) -> None:
|
|
857
|
+
"""
|
|
858
|
+
Set the RB number for the current run.
|
|
859
|
+
|
|
860
|
+
Args:
|
|
861
|
+
rbno (str): the new RB number
|
|
862
|
+
"""
|
|
863
|
+
self._set_pv_value(self._get_dae_pv_name("rbnum_sp"), rbno)
|
|
864
|
+
self.api.logger.log_info_msg(f"Proposal number changed to: {rbno}")
|
|
865
|
+
|
|
866
|
+
def get_title(self) -> str:
|
|
867
|
+
"""
|
|
868
|
+
Gets the title for the current run.
|
|
869
|
+
|
|
870
|
+
Returns
|
|
871
|
+
string: the current title
|
|
872
|
+
"""
|
|
873
|
+
return self._get_pv_value(self._get_dae_pv_name("title"), to_string=True)
|
|
874
|
+
|
|
875
|
+
def set_title(self, title: str) -> None:
|
|
876
|
+
"""
|
|
877
|
+
Set the title for the current/next run.
|
|
878
|
+
|
|
879
|
+
Args:
|
|
880
|
+
title: the title to set
|
|
881
|
+
"""
|
|
882
|
+
self._set_pv_value(self._get_dae_pv_name("title_sp"), title, wait=True)
|
|
883
|
+
self.api.logger.log_info_msg(f"Title changed to: {title}")
|
|
884
|
+
|
|
885
|
+
def get_display_title(self) -> bool:
|
|
886
|
+
"""
|
|
887
|
+
Gets the display title status for the current run.
|
|
888
|
+
|
|
889
|
+
Returns
|
|
890
|
+
boolean: the current display title status
|
|
891
|
+
"""
|
|
892
|
+
return self._get_pv_value(self._get_dae_pv_name("display_title"))
|
|
893
|
+
|
|
894
|
+
def set_display_title(self, display_title: bool) -> None:
|
|
895
|
+
"""
|
|
896
|
+
Set the display title status for the current/next run.
|
|
897
|
+
|
|
898
|
+
Args:
|
|
899
|
+
display_title: the display title status to set
|
|
900
|
+
"""
|
|
901
|
+
self._set_pv_value(self._get_dae_pv_name("display_title"), display_title, wait=True)
|
|
902
|
+
|
|
903
|
+
def get_users(self) -> str:
|
|
904
|
+
"""
|
|
905
|
+
Gets the users for the current run.
|
|
906
|
+
|
|
907
|
+
Returns:
|
|
908
|
+
string: the names
|
|
909
|
+
"""
|
|
910
|
+
try:
|
|
911
|
+
# Data comes as comma separated list
|
|
912
|
+
raw = str(self._get_pv_value(self._get_dae_pv_name("users_dae_sp"), to_string=True))
|
|
913
|
+
names_list = [x.strip() for x in raw.split(",")]
|
|
914
|
+
if len(names_list) > 1:
|
|
915
|
+
last = names_list.pop(-1)
|
|
916
|
+
names = ", ".join(names_list)
|
|
917
|
+
names += " and " + last
|
|
918
|
+
return names
|
|
919
|
+
else:
|
|
920
|
+
# Will throw if empty - that is okay
|
|
921
|
+
return names_list[0]
|
|
922
|
+
except Exception:
|
|
923
|
+
return ""
|
|
924
|
+
|
|
925
|
+
def set_users(self, users: str) -> None:
|
|
926
|
+
"""
|
|
927
|
+
Set the users for the current run.
|
|
928
|
+
|
|
929
|
+
Args:
|
|
930
|
+
users: the users as a comma-separated string
|
|
931
|
+
"""
|
|
932
|
+
split_users = users.split(",") if users else []
|
|
933
|
+
table_data = json.dumps([{"name": user.strip()} for user in split_users])
|
|
934
|
+
# Send just the username and database server will clear the table if only user is set
|
|
935
|
+
self._set_pv_value(
|
|
936
|
+
self._get_dae_pv_name("users_table_sp"), compress_and_hex(table_data), True
|
|
937
|
+
)
|
|
938
|
+
self.api.logger.log_info_msg(f"Users set to: {users}")
|
|
939
|
+
|
|
940
|
+
def get_starttime(self) -> str:
|
|
941
|
+
"""
|
|
942
|
+
Gets the start time for the current run.
|
|
943
|
+
|
|
944
|
+
Returns
|
|
945
|
+
string: the start time
|
|
946
|
+
"""
|
|
947
|
+
return self._get_pv_value(self._get_dae_pv_name("starttime"))
|
|
948
|
+
|
|
949
|
+
@require_runstate(
|
|
950
|
+
["PAUSING", "BEGINNING", "ABORTING", "RESUMING", "RUNNING", "VETOING", "WAITING", "PAUSED"]
|
|
951
|
+
)
|
|
952
|
+
def get_time_since_begin(self, get_timedelta: bool) -> float | timedelta:
|
|
953
|
+
"""
|
|
954
|
+
Gets the time since start of the current run in seconds or in datetime
|
|
955
|
+
Args:
|
|
956
|
+
get_timedelta (bool): If true return the value as a datetime object,
|
|
957
|
+
otherwise return seconds (defaults to false)
|
|
958
|
+
Returns
|
|
959
|
+
integer: the time since start in seconds if get_datetime is False
|
|
960
|
+
datetime: the time since start in (Year-Month-Day Hour:Minute:Second)
|
|
961
|
+
format if get_datetime is True
|
|
962
|
+
"""
|
|
963
|
+
|
|
964
|
+
current_time = datetime.now()
|
|
965
|
+
# Casting get_startime string to datetime object
|
|
966
|
+
datetime_object = datetime.strptime(self.get_starttime(), "%a %d-%b-%Y %H:%M:%S")
|
|
967
|
+
# Difference between current time and start time gives time since start
|
|
968
|
+
time_since_start = current_time - datetime_object
|
|
969
|
+
|
|
970
|
+
if get_timedelta:
|
|
971
|
+
return time_since_start
|
|
972
|
+
else:
|
|
973
|
+
return time_since_start.total_seconds()
|
|
974
|
+
|
|
975
|
+
def get_npratio(self) -> float:
|
|
976
|
+
"""
|
|
977
|
+
Gets the n/p ratio for the current run.
|
|
978
|
+
|
|
979
|
+
Returns:
|
|
980
|
+
float: the ratio
|
|
981
|
+
"""
|
|
982
|
+
return self._get_pv_value(self._get_dae_pv_name("npratio"))
|
|
983
|
+
|
|
984
|
+
def get_timing_source(self) -> str:
|
|
985
|
+
"""
|
|
986
|
+
Gets the DAE timing source.
|
|
987
|
+
|
|
988
|
+
Returns:
|
|
989
|
+
string: the current timing source being used
|
|
990
|
+
"""
|
|
991
|
+
return self._get_pv_value(self._get_dae_pv_name("timingsource"))
|
|
992
|
+
|
|
993
|
+
def get_run_duration(self, period: bool = False) -> int:
|
|
994
|
+
"""
|
|
995
|
+
Gets either the total run duration or the period duration
|
|
996
|
+
|
|
997
|
+
Args:
|
|
998
|
+
period: whether to return the duration for the current period [optional]
|
|
999
|
+
|
|
1000
|
+
Returns:
|
|
1001
|
+
int: the run duration in seconds
|
|
1002
|
+
"""
|
|
1003
|
+
if period:
|
|
1004
|
+
return self._get_pv_value(self._get_dae_pv_name("rundurationperiod"))
|
|
1005
|
+
else:
|
|
1006
|
+
return self._get_pv_value(self._get_dae_pv_name("runduration"))
|
|
1007
|
+
|
|
1008
|
+
def get_num_timechannels(self) -> int:
|
|
1009
|
+
"""
|
|
1010
|
+
Gets the number of time channels.
|
|
1011
|
+
|
|
1012
|
+
Returns:
|
|
1013
|
+
int: the number of time channels
|
|
1014
|
+
"""
|
|
1015
|
+
return cast(int, self._get_pv_value(self._get_dae_pv_name("numtimechannels")))
|
|
1016
|
+
|
|
1017
|
+
def get_monitor_counts(self) -> int:
|
|
1018
|
+
"""
|
|
1019
|
+
Gets the number of monitor counts.
|
|
1020
|
+
|
|
1021
|
+
Returns:
|
|
1022
|
+
int: the number of monitor counts
|
|
1023
|
+
"""
|
|
1024
|
+
return self._get_pv_value(self._get_dae_pv_name("monitorcounts"))
|
|
1025
|
+
|
|
1026
|
+
def get_monitor_spectrum(self) -> int:
|
|
1027
|
+
"""
|
|
1028
|
+
Gets the monitor spectrum.
|
|
1029
|
+
|
|
1030
|
+
Returns:
|
|
1031
|
+
int: the detector number of the monitor
|
|
1032
|
+
"""
|
|
1033
|
+
return self._get_pv_value(self._get_dae_pv_name("monitorspectrum"))
|
|
1034
|
+
|
|
1035
|
+
def get_monitor_to(self) -> float:
|
|
1036
|
+
"""
|
|
1037
|
+
Gets the monitor 'to' limit.
|
|
1038
|
+
|
|
1039
|
+
Returns:
|
|
1040
|
+
float: the 'to' time for the monitor
|
|
1041
|
+
"""
|
|
1042
|
+
return self._get_pv_value(self._get_dae_pv_name("monitorto"))
|
|
1043
|
+
|
|
1044
|
+
def get_monitor_from(self) -> float:
|
|
1045
|
+
"""
|
|
1046
|
+
Gets the monitor 'from' limit.
|
|
1047
|
+
|
|
1048
|
+
Returns:
|
|
1049
|
+
float: the 'from' time for the monitor
|
|
1050
|
+
"""
|
|
1051
|
+
return self._get_pv_value(self._get_dae_pv_name("monitorfrom"))
|
|
1052
|
+
|
|
1053
|
+
def get_beam_current(self) -> float:
|
|
1054
|
+
"""
|
|
1055
|
+
Gets the beam current.
|
|
1056
|
+
|
|
1057
|
+
Returns:
|
|
1058
|
+
float: the current value
|
|
1059
|
+
"""
|
|
1060
|
+
return self._get_pv_value(self._get_dae_pv_name("beamcurrent"))
|
|
1061
|
+
|
|
1062
|
+
def get_total_uamps(self) -> float:
|
|
1063
|
+
"""
|
|
1064
|
+
Gets the total microamp hours for the current run.
|
|
1065
|
+
|
|
1066
|
+
Returns:
|
|
1067
|
+
float: the total micro-amp hours.
|
|
1068
|
+
"""
|
|
1069
|
+
return self._get_pv_value(self._get_dae_pv_name("totaluamps"))
|
|
1070
|
+
|
|
1071
|
+
def get_total_dae_counts(self) -> int:
|
|
1072
|
+
"""
|
|
1073
|
+
Gets the total DAE counts for the current run.
|
|
1074
|
+
|
|
1075
|
+
Returns:
|
|
1076
|
+
int: the total count
|
|
1077
|
+
"""
|
|
1078
|
+
return self._get_pv_value(self._get_dae_pv_name("totaldaecounts"))
|
|
1079
|
+
|
|
1080
|
+
def get_countrate(self) -> float:
|
|
1081
|
+
"""
|
|
1082
|
+
Gets the count rate.
|
|
1083
|
+
|
|
1084
|
+
Returns:
|
|
1085
|
+
float: the count rate
|
|
1086
|
+
"""
|
|
1087
|
+
return self._get_pv_value(self._get_dae_pv_name("countrate"))
|
|
1088
|
+
|
|
1089
|
+
def get_eventmode_fraction(self) -> float:
|
|
1090
|
+
"""
|
|
1091
|
+
Gets the event mode fraction.
|
|
1092
|
+
|
|
1093
|
+
Returns:
|
|
1094
|
+
float: the fraction
|
|
1095
|
+
"""
|
|
1096
|
+
return self._get_pv_value(self._get_dae_pv_name("eventmodefraction"))
|
|
1097
|
+
|
|
1098
|
+
def get_spec_integrals(self) -> npt.NDArray:
|
|
1099
|
+
"""
|
|
1100
|
+
Gets the event mode spectrum integrals.
|
|
1101
|
+
This includes spectrum 0
|
|
1102
|
+
|
|
1103
|
+
Returns:
|
|
1104
|
+
numpy int array: the spectrum integrals
|
|
1105
|
+
"""
|
|
1106
|
+
# this return waveform NELM elements, but only NORD are valid
|
|
1107
|
+
data = cast(
|
|
1108
|
+
npt.NDArray, self._get_pv_value(self._get_dae_pv_name("specintegrals"), use_numpy=True)
|
|
1109
|
+
)
|
|
1110
|
+
spec_size = self._get_pv_value(self._get_dae_pv_name("specintegrals_size"))
|
|
1111
|
+
assert isinstance(spec_size, (int, float))
|
|
1112
|
+
size = int(spec_size)
|
|
1113
|
+
# this is an EPICS waveform so NORD <= NELM
|
|
1114
|
+
if size < data.size:
|
|
1115
|
+
data.resize(size)
|
|
1116
|
+
return data
|
|
1117
|
+
|
|
1118
|
+
def get_spec_data(self) -> npt.NDArray:
|
|
1119
|
+
"""
|
|
1120
|
+
Gets the event mode spectrum data.
|
|
1121
|
+
This includes spectrum 0 and time bin 0
|
|
1122
|
+
|
|
1123
|
+
Returns:
|
|
1124
|
+
numpy int array: the spectrum data
|
|
1125
|
+
"""
|
|
1126
|
+
self._set_pv_value(self._get_dae_pv_name("specdata") + ".PROC", 1, wait=True)
|
|
1127
|
+
# this return waveform NELM elements, but only NORD are valid
|
|
1128
|
+
data = cast(
|
|
1129
|
+
npt.NDArray, self._get_pv_value(self._get_dae_pv_name("specdata"), use_numpy=True)
|
|
1130
|
+
)
|
|
1131
|
+
spec_size = self._get_pv_value(self._get_dae_pv_name("specdata_size"))
|
|
1132
|
+
assert isinstance(spec_size, (int, float))
|
|
1133
|
+
size = int(spec_size)
|
|
1134
|
+
# this is an EPICS waveform so NORD <= NELM
|
|
1135
|
+
if size < data.size:
|
|
1136
|
+
data.resize(size)
|
|
1137
|
+
return data
|
|
1138
|
+
|
|
1139
|
+
def change_start(self) -> None:
|
|
1140
|
+
"""
|
|
1141
|
+
Start a change operation.
|
|
1142
|
+
|
|
1143
|
+
The operation is finished when change_finish is called.
|
|
1144
|
+
Between these two calls a sequence of other change commands can be called.
|
|
1145
|
+
For example: change_tables, change_tcb etc.
|
|
1146
|
+
|
|
1147
|
+
Raises:
|
|
1148
|
+
ValueError: if the run state is not SETUP or change already started
|
|
1149
|
+
"""
|
|
1150
|
+
# Check if we are in transition e.g. wiring tables being changed from GUI
|
|
1151
|
+
# because it can go in and out of transition 3 times very quickly during a
|
|
1152
|
+
# change we do a nested check
|
|
1153
|
+
if self.in_transition():
|
|
1154
|
+
print("Another DAE change operation is currently in progress, waiting...")
|
|
1155
|
+
while self.in_transition():
|
|
1156
|
+
while self.in_transition():
|
|
1157
|
+
sleep(1)
|
|
1158
|
+
sleep(0.1)
|
|
1159
|
+
print("Previous DAE change operation has now completed")
|
|
1160
|
+
|
|
1161
|
+
# Check in SETUP
|
|
1162
|
+
if self.get_run_state() != "SETUP":
|
|
1163
|
+
raise ValueError("Instrument must be in SETUP when changing settings!")
|
|
1164
|
+
if self.in_change:
|
|
1165
|
+
raise ValueError("Already in change - previous cached values will be used")
|
|
1166
|
+
else:
|
|
1167
|
+
self.in_change = True
|
|
1168
|
+
self.change_cache = ChangeCache()
|
|
1169
|
+
|
|
1170
|
+
def change_finish(self) -> None:
|
|
1171
|
+
"""
|
|
1172
|
+
End a change operation.
|
|
1173
|
+
|
|
1174
|
+
The operation is begun when change_start is called.
|
|
1175
|
+
Between these two calls a sequence of other change commands can be called.
|
|
1176
|
+
For example: change_tables, change_tcb etc.
|
|
1177
|
+
|
|
1178
|
+
Raises:
|
|
1179
|
+
ValueError: if the change has already finished
|
|
1180
|
+
"""
|
|
1181
|
+
if not self.in_change:
|
|
1182
|
+
raise ValueError("Change has already finished")
|
|
1183
|
+
if self.in_transition():
|
|
1184
|
+
raise ValueError(
|
|
1185
|
+
"Another DAE change operation is currently in progress - "
|
|
1186
|
+
"values will be inconsistent"
|
|
1187
|
+
)
|
|
1188
|
+
if self.get_run_state() != "SETUP":
|
|
1189
|
+
raise ValueError("Instrument must be in SETUP when changing settings!")
|
|
1190
|
+
if self.in_change:
|
|
1191
|
+
self.in_change = False
|
|
1192
|
+
self._change_dae_settings()
|
|
1193
|
+
self._change_tcb_settings()
|
|
1194
|
+
self._change_period_settings()
|
|
1195
|
+
self.change_cache = ChangeCache()
|
|
1196
|
+
|
|
1197
|
+
def change_tables(
|
|
1198
|
+
self, wiring: str | None = None, detector: str | None = None, spectra: str | None = None
|
|
1199
|
+
) -> None:
|
|
1200
|
+
"""
|
|
1201
|
+
Load the wiring, detector and/or spectra tables.
|
|
1202
|
+
|
|
1203
|
+
Args:
|
|
1204
|
+
wiring: the filename of the wiring table file [optional]
|
|
1205
|
+
detector: the filename of the detector table file [optional]
|
|
1206
|
+
spectra: the filename of the spectra table file [optional]
|
|
1207
|
+
"""
|
|
1208
|
+
did_change = False
|
|
1209
|
+
if not self.in_change:
|
|
1210
|
+
self.change_start()
|
|
1211
|
+
did_change = True
|
|
1212
|
+
if wiring is not None:
|
|
1213
|
+
self.change_cache.wiring = wiring
|
|
1214
|
+
if detector is not None:
|
|
1215
|
+
self.change_cache.detector = detector
|
|
1216
|
+
if spectra is not None:
|
|
1217
|
+
self.change_cache.spectra = spectra
|
|
1218
|
+
if did_change:
|
|
1219
|
+
self.change_finish()
|
|
1220
|
+
|
|
1221
|
+
def change_monitor(self, spec: int, low: float, high: float) -> None:
|
|
1222
|
+
"""
|
|
1223
|
+
Change the monitor to a specified spectrum and range.
|
|
1224
|
+
|
|
1225
|
+
Args:
|
|
1226
|
+
spec: the spectrum number (integer)
|
|
1227
|
+
low: the low end of the integral (float)
|
|
1228
|
+
high: the high end of the integral (float)
|
|
1229
|
+
|
|
1230
|
+
Raises:
|
|
1231
|
+
TypeError: if a value supplied is not correctly typed
|
|
1232
|
+
"""
|
|
1233
|
+
try:
|
|
1234
|
+
spec = int(spec)
|
|
1235
|
+
except ValueError:
|
|
1236
|
+
raise TypeError("Spectrum number must be an integer")
|
|
1237
|
+
try:
|
|
1238
|
+
low = float(low)
|
|
1239
|
+
except ValueError:
|
|
1240
|
+
raise TypeError("Low must be a float")
|
|
1241
|
+
try:
|
|
1242
|
+
high = float(high)
|
|
1243
|
+
except ValueError:
|
|
1244
|
+
raise TypeError("High must be a float")
|
|
1245
|
+
did_change = False
|
|
1246
|
+
if not self.in_change:
|
|
1247
|
+
self.change_start()
|
|
1248
|
+
did_change = True
|
|
1249
|
+
self.change_cache.set_monitor(spec, low, high)
|
|
1250
|
+
if did_change:
|
|
1251
|
+
self.change_finish()
|
|
1252
|
+
|
|
1253
|
+
def change_sync(self, source: str) -> None:
|
|
1254
|
+
"""
|
|
1255
|
+
Change the source the DAE using for synchronisation.
|
|
1256
|
+
|
|
1257
|
+
Args:
|
|
1258
|
+
source: the source to use ('isis', 'internal', 'smp', 'muon cerenkov',
|
|
1259
|
+
'muon ms', 'isis (first ts1)', 'isis (ts1 only)')
|
|
1260
|
+
|
|
1261
|
+
Raises:
|
|
1262
|
+
Exception: if an invalid source is entered
|
|
1263
|
+
"""
|
|
1264
|
+
did_change = False
|
|
1265
|
+
if not self.in_change:
|
|
1266
|
+
self.change_start()
|
|
1267
|
+
did_change = True
|
|
1268
|
+
source = source.strip().lower()
|
|
1269
|
+
if source == "isis":
|
|
1270
|
+
value = 0
|
|
1271
|
+
elif source == "internal":
|
|
1272
|
+
value = 1
|
|
1273
|
+
elif source == "smp":
|
|
1274
|
+
value = 2
|
|
1275
|
+
elif source == "muon cerenkov":
|
|
1276
|
+
value = 3
|
|
1277
|
+
elif source == "muon ms":
|
|
1278
|
+
value = 4
|
|
1279
|
+
elif source == "isis (first ts1)":
|
|
1280
|
+
value = 5
|
|
1281
|
+
elif source == "isis (ts1 only)":
|
|
1282
|
+
value = 6
|
|
1283
|
+
else:
|
|
1284
|
+
raise Exception("Invalid timing source entered, try help(change_sync)!")
|
|
1285
|
+
self.change_cache.dae_sync = value
|
|
1286
|
+
if did_change:
|
|
1287
|
+
self.change_finish()
|
|
1288
|
+
|
|
1289
|
+
def change_tcb_file(self, tcb_file: str | None = None, default: bool = False) -> None:
|
|
1290
|
+
"""
|
|
1291
|
+
Change the time channel boundaries.
|
|
1292
|
+
|
|
1293
|
+
Args:
|
|
1294
|
+
tcb_file: the file to load [optional]
|
|
1295
|
+
default: load the default file "c:\\labview modules\\dae\\tcb.dat" [optional]
|
|
1296
|
+
|
|
1297
|
+
Raises:
|
|
1298
|
+
Exception: if the TCB file is not specified or not found
|
|
1299
|
+
"""
|
|
1300
|
+
did_change = False
|
|
1301
|
+
if not self.in_change:
|
|
1302
|
+
self.change_start()
|
|
1303
|
+
did_change = True
|
|
1304
|
+
if tcb_file is not None:
|
|
1305
|
+
tcb_file = get_correct_path(tcb_file)
|
|
1306
|
+
print(("Reading TCB boundaries from {}".format(tcb_file)))
|
|
1307
|
+
elif default:
|
|
1308
|
+
tcb_file = "c:\\labview modules\\dae\\tcb.dat"
|
|
1309
|
+
else:
|
|
1310
|
+
raise Exception("No tcb file specified")
|
|
1311
|
+
if not os.path.exists(tcb_file):
|
|
1312
|
+
raise Exception("Tcb file could not be found")
|
|
1313
|
+
self.change_cache.tcb_file = tcb_file
|
|
1314
|
+
self.change_cache.tcb_calculation_method = 1
|
|
1315
|
+
if did_change:
|
|
1316
|
+
self.change_finish()
|
|
1317
|
+
|
|
1318
|
+
def _create_tcb_return_string(self, low: float, high: float, step: float, log: bool) -> str:
|
|
1319
|
+
"""
|
|
1320
|
+
Creates a human readable string when the tcb is changed.
|
|
1321
|
+
|
|
1322
|
+
Args:
|
|
1323
|
+
low: the lower limit
|
|
1324
|
+
high: the upper limit
|
|
1325
|
+
step: the step size
|
|
1326
|
+
log: whether to use LOG binning [optional]
|
|
1327
|
+
|
|
1328
|
+
Returns:
|
|
1329
|
+
str: The human readable string
|
|
1330
|
+
"""
|
|
1331
|
+
out = "Setting TCB "
|
|
1332
|
+
binning = "LOG binning" if log else "LINEAR binning"
|
|
1333
|
+
|
|
1334
|
+
low_changed, high_changed, step_changed = (c is not None for c in [low, high, step])
|
|
1335
|
+
|
|
1336
|
+
if low_changed and high_changed:
|
|
1337
|
+
out += "range {} to {} ".format(low, high)
|
|
1338
|
+
elif low_changed:
|
|
1339
|
+
out += "low limit to {} ".format(low)
|
|
1340
|
+
elif high_changed:
|
|
1341
|
+
out += "high limit to {} ".format(high)
|
|
1342
|
+
|
|
1343
|
+
if step_changed:
|
|
1344
|
+
out += "step {} ".format(step)
|
|
1345
|
+
|
|
1346
|
+
if not any([low_changed, high_changed, step_changed]):
|
|
1347
|
+
out += "to {}".format(binning)
|
|
1348
|
+
else:
|
|
1349
|
+
out += "({})".format(binning)
|
|
1350
|
+
|
|
1351
|
+
return out
|
|
1352
|
+
|
|
1353
|
+
def change_tcb(
|
|
1354
|
+
self, low: float, high: float, step: float, trange: int, log: bool = False, regime: int = 1
|
|
1355
|
+
) -> None:
|
|
1356
|
+
"""
|
|
1357
|
+
Change the time channel boundaries.
|
|
1358
|
+
|
|
1359
|
+
Args:
|
|
1360
|
+
low: the lower limit
|
|
1361
|
+
high: the upper limit
|
|
1362
|
+
step: the step size
|
|
1363
|
+
trange: the time range (1 to 5)
|
|
1364
|
+
log: whether to use LOG binning [optional]
|
|
1365
|
+
regime: the time regime to set (1 to 6)[optional]
|
|
1366
|
+
"""
|
|
1367
|
+
print((self._create_tcb_return_string(low, high, step, log)))
|
|
1368
|
+
did_change = False
|
|
1369
|
+
if not self.in_change:
|
|
1370
|
+
self.change_start()
|
|
1371
|
+
did_change = True
|
|
1372
|
+
if log:
|
|
1373
|
+
self.change_cache.tcb_tables.append((regime, trange, low, high, step, 2))
|
|
1374
|
+
else:
|
|
1375
|
+
self.change_cache.tcb_tables.append((regime, trange, low, high, step, 1))
|
|
1376
|
+
|
|
1377
|
+
self.change_cache.tcb_calculation_method = 0
|
|
1378
|
+
|
|
1379
|
+
if did_change:
|
|
1380
|
+
self.change_finish()
|
|
1381
|
+
|
|
1382
|
+
def change_vetos(self, **params: bool) -> None:
|
|
1383
|
+
"""
|
|
1384
|
+
Change the DAE veto settings.
|
|
1385
|
+
|
|
1386
|
+
Args:
|
|
1387
|
+
clearall: remove all vetoes [optional]
|
|
1388
|
+
smp: set SMP veto [optional]
|
|
1389
|
+
ts2: set TS2 veto [optional]
|
|
1390
|
+
hz50: set 50 hz veto [optional]
|
|
1391
|
+
ext0: set external veto 0 [optional]
|
|
1392
|
+
ext1: set external veto 1 [optional]
|
|
1393
|
+
ext2: set external veto 2 [optional]
|
|
1394
|
+
ext3: set external veto 3 [optional]
|
|
1395
|
+
|
|
1396
|
+
If clearall is specified then all vetoes are turned off,
|
|
1397
|
+
but it is possible to turn other vetoes back on at the same time.
|
|
1398
|
+
|
|
1399
|
+
Example:
|
|
1400
|
+
Turns all vetoes off then turns the SMP veto back on
|
|
1401
|
+
>>> change_vetos(clearall=True, smp=True)
|
|
1402
|
+
"""
|
|
1403
|
+
valid_vetoes = [
|
|
1404
|
+
CLEAR_VETO,
|
|
1405
|
+
SMP_VETO,
|
|
1406
|
+
TS2_VETO,
|
|
1407
|
+
HZ50_VETO,
|
|
1408
|
+
EXT0_VETO,
|
|
1409
|
+
EXT1_VETO,
|
|
1410
|
+
EXT2_VETO,
|
|
1411
|
+
EXT3_VETO,
|
|
1412
|
+
FIFO_VETO,
|
|
1413
|
+
]
|
|
1414
|
+
|
|
1415
|
+
# Change keys to be case insensitive
|
|
1416
|
+
params = dict((k.lower(), v) for k, v in params.items())
|
|
1417
|
+
|
|
1418
|
+
# Check for invalid veto names and invalid (non-boolean) values
|
|
1419
|
+
not_bool = []
|
|
1420
|
+
for k, v in params.items():
|
|
1421
|
+
if k not in valid_vetoes:
|
|
1422
|
+
raise Exception("Invalid veto name: {}".format(k))
|
|
1423
|
+
if not isinstance(v, bool):
|
|
1424
|
+
not_bool.append(k)
|
|
1425
|
+
if len(not_bool) > 0:
|
|
1426
|
+
raise Exception(
|
|
1427
|
+
"Vetoes must be set to True or False, "
|
|
1428
|
+
"the following vetoes were incorrect: {}".format(" ".join(not_bool))
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
# Set any runtime vetoes
|
|
1432
|
+
params = self._change_runtime_vetos(params)
|
|
1433
|
+
if len(params) == 0:
|
|
1434
|
+
return
|
|
1435
|
+
|
|
1436
|
+
did_change = False
|
|
1437
|
+
if not self.in_change:
|
|
1438
|
+
self.change_start()
|
|
1439
|
+
did_change = True
|
|
1440
|
+
|
|
1441
|
+
# Clearall must be done first.
|
|
1442
|
+
if CLEAR_VETO in params:
|
|
1443
|
+
if isinstance(params[CLEAR_VETO], bool) and params[CLEAR_VETO]:
|
|
1444
|
+
self.change_cache.clear_vetos()
|
|
1445
|
+
if SMP_VETO in params:
|
|
1446
|
+
if isinstance(params[SMP_VETO], bool):
|
|
1447
|
+
self.change_cache.smp_veto = int(params[SMP_VETO])
|
|
1448
|
+
if TS2_VETO in params:
|
|
1449
|
+
if isinstance(params[TS2_VETO], bool):
|
|
1450
|
+
self.change_cache.ts2_veto = int(params[TS2_VETO])
|
|
1451
|
+
if HZ50_VETO in params:
|
|
1452
|
+
if isinstance(params[HZ50_VETO], bool):
|
|
1453
|
+
self.change_cache.hz50_veto = int(params[HZ50_VETO])
|
|
1454
|
+
if EXT0_VETO in params:
|
|
1455
|
+
if isinstance(params[EXT0_VETO], bool):
|
|
1456
|
+
self.change_cache.ext0_veto = int(params[EXT0_VETO])
|
|
1457
|
+
if EXT1_VETO in params:
|
|
1458
|
+
if isinstance(params[EXT1_VETO], bool):
|
|
1459
|
+
self.change_cache.ext1_veto = int(params[EXT1_VETO])
|
|
1460
|
+
if EXT2_VETO in params:
|
|
1461
|
+
if isinstance(params[EXT2_VETO], bool):
|
|
1462
|
+
self.change_cache.ext2_veto = int(params[EXT2_VETO])
|
|
1463
|
+
if EXT3_VETO in params:
|
|
1464
|
+
if isinstance(params[EXT3_VETO], bool):
|
|
1465
|
+
self.change_cache.ext3_veto = int(params[EXT3_VETO])
|
|
1466
|
+
|
|
1467
|
+
if did_change:
|
|
1468
|
+
self.change_finish()
|
|
1469
|
+
|
|
1470
|
+
def _change_runtime_vetos(self, params: dict) -> None:
|
|
1471
|
+
"""
|
|
1472
|
+
Change the DAE veto settings whilst the DAE is running.
|
|
1473
|
+
|
|
1474
|
+
Args:
|
|
1475
|
+
params (dict): The vetoes to be set.
|
|
1476
|
+
|
|
1477
|
+
Returns:
|
|
1478
|
+
dict : The params passed in minus the ones set in this method.
|
|
1479
|
+
"""
|
|
1480
|
+
if FIFO_VETO in params:
|
|
1481
|
+
if isinstance(params[FIFO_VETO], bool):
|
|
1482
|
+
self._set_pv_value(
|
|
1483
|
+
self._get_dae_pv_name("set_veto_" + ("true" if params[FIFO_VETO] else "false")),
|
|
1484
|
+
"FIFO",
|
|
1485
|
+
)
|
|
1486
|
+
|
|
1487
|
+
# Check if in SETUP, if not SETUP warn the user that the setting will be set
|
|
1488
|
+
# to True automatically when a run begins.
|
|
1489
|
+
if self.get_run_state() == "SETUP" and not params[FIFO_VETO]:
|
|
1490
|
+
print(
|
|
1491
|
+
"FIFO veto will automatically revert to ENABLED when next run begins.\n"
|
|
1492
|
+
"Run this command again during the run to disable FIFO vetos."
|
|
1493
|
+
)
|
|
1494
|
+
del params[FIFO_VETO]
|
|
1495
|
+
else:
|
|
1496
|
+
raise Exception("FIFO veto must be set to True or False")
|
|
1497
|
+
return params
|
|
1498
|
+
|
|
1499
|
+
def set_fermi_veto(
|
|
1500
|
+
self, enable: bool | None = None, delay: float = 1.0, width: float = 1.0
|
|
1501
|
+
) -> None:
|
|
1502
|
+
"""
|
|
1503
|
+
Configure the fermi chopper veto.
|
|
1504
|
+
|
|
1505
|
+
Args:
|
|
1506
|
+
enable: enable the fermi veto
|
|
1507
|
+
delay: the veto delay
|
|
1508
|
+
width: the veto width
|
|
1509
|
+
|
|
1510
|
+
Raises:
|
|
1511
|
+
Exception: if invalid typed value supplied.
|
|
1512
|
+
"""
|
|
1513
|
+
if not isinstance(enable, bool):
|
|
1514
|
+
raise Exception("Fermi veto: enable must be a boolean value")
|
|
1515
|
+
if not isinstance(delay, float) and not isinstance(delay, int):
|
|
1516
|
+
raise Exception("Fermi veto: delay must be a numeric value")
|
|
1517
|
+
if not isinstance(width, float) and not isinstance(width, int):
|
|
1518
|
+
raise Exception("Fermi veto: width must be a numeric value")
|
|
1519
|
+
did_change = False
|
|
1520
|
+
if not self.in_change:
|
|
1521
|
+
self.change_start()
|
|
1522
|
+
did_change = True
|
|
1523
|
+
if enable:
|
|
1524
|
+
self.change_cache.set_fermi(1, delay, width)
|
|
1525
|
+
print(
|
|
1526
|
+
("SET_FERMI_VETO: requested status is ON, delay: {} width: {}".format(delay, width))
|
|
1527
|
+
)
|
|
1528
|
+
else:
|
|
1529
|
+
self.change_cache.set_fermi(0)
|
|
1530
|
+
print("SET_FERMI_VETO: requested status is OFF")
|
|
1531
|
+
if did_change:
|
|
1532
|
+
self.change_finish()
|
|
1533
|
+
|
|
1534
|
+
def set_num_soft_periods(self, number: int) -> None:
|
|
1535
|
+
"""
|
|
1536
|
+
Sets the number of software periods for the DAE.
|
|
1537
|
+
|
|
1538
|
+
Args:
|
|
1539
|
+
number: the number of periods to create
|
|
1540
|
+
|
|
1541
|
+
Raises:
|
|
1542
|
+
Exception: if wrongly typed value supplied
|
|
1543
|
+
"""
|
|
1544
|
+
if not isinstance(number, float) and not isinstance(number, int):
|
|
1545
|
+
raise Exception("Number of soft periods must be a numeric value")
|
|
1546
|
+
did_change = False
|
|
1547
|
+
if not self.in_change:
|
|
1548
|
+
self.change_start()
|
|
1549
|
+
did_change = True
|
|
1550
|
+
if number >= 0:
|
|
1551
|
+
self.change_cache.periods_soft_num = number
|
|
1552
|
+
if did_change:
|
|
1553
|
+
self.change_finish()
|
|
1554
|
+
|
|
1555
|
+
def set_period_mode(self, mode: str) -> None:
|
|
1556
|
+
"""
|
|
1557
|
+
Sets the period mode for the DAE.
|
|
1558
|
+
|
|
1559
|
+
Args:
|
|
1560
|
+
mode: the mode to switch to ('soft', 'int', 'ext')
|
|
1561
|
+
"""
|
|
1562
|
+
did_change = False
|
|
1563
|
+
if not self.in_change:
|
|
1564
|
+
self.change_start()
|
|
1565
|
+
did_change = True
|
|
1566
|
+
if mode.strip().lower() == "soft":
|
|
1567
|
+
self.change_cache.periods_type = 0
|
|
1568
|
+
else:
|
|
1569
|
+
self.configure_hard_periods(mode)
|
|
1570
|
+
if did_change:
|
|
1571
|
+
self.change_finish()
|
|
1572
|
+
|
|
1573
|
+
def configure_hard_periods(
|
|
1574
|
+
self,
|
|
1575
|
+
mode: str,
|
|
1576
|
+
period_file: str | None = None,
|
|
1577
|
+
sequences: int | None = None,
|
|
1578
|
+
output_delay: int | None = None,
|
|
1579
|
+
period: int | None = None,
|
|
1580
|
+
daq: bool = False,
|
|
1581
|
+
dwell: bool = False,
|
|
1582
|
+
unused: bool = False,
|
|
1583
|
+
frames: int | None = None,
|
|
1584
|
+
output: int | None = None,
|
|
1585
|
+
label: str | None = None,
|
|
1586
|
+
) -> None:
|
|
1587
|
+
"""
|
|
1588
|
+
Configures the DAE's hardware periods.
|
|
1589
|
+
|
|
1590
|
+
Args:
|
|
1591
|
+
mode: set the mode to internal ('int') or external ('ext')
|
|
1592
|
+
|
|
1593
|
+
Internal periods parameters [optional]:
|
|
1594
|
+
period_file: the file containing the internal period settings (ignores any
|
|
1595
|
+
other settings)
|
|
1596
|
+
sequences: the number of period sequences
|
|
1597
|
+
output_delay: the output delay in microseconds
|
|
1598
|
+
period: the number of the period to set the following for:
|
|
1599
|
+
daq: period is an acquisition; if period is not set then applies for all periods
|
|
1600
|
+
dwell: period is a dwell; if period is not set then applies for all periods
|
|
1601
|
+
unused: period is a unused; if period is not set then applies for all periods
|
|
1602
|
+
frames: the number of frames to count for the period; if period is not set then
|
|
1603
|
+
applies for all periods
|
|
1604
|
+
output: the binary output for the period; if period is not set then applies for
|
|
1605
|
+
all periods
|
|
1606
|
+
label: the label for the period; if period is not set then applies for all periods
|
|
1607
|
+
|
|
1608
|
+
Raises:
|
|
1609
|
+
Exception: if mode is not 'int' or 'ext'
|
|
1610
|
+
|
|
1611
|
+
Examples:
|
|
1612
|
+
Setting external periods
|
|
1613
|
+
>>> enable_hardware_periods("ext")
|
|
1614
|
+
|
|
1615
|
+
Setting internal periods from a file
|
|
1616
|
+
>>> enable_hardware_periods("int", "myperiods.txt")
|
|
1617
|
+
"""
|
|
1618
|
+
did_change = False
|
|
1619
|
+
if not self.in_change:
|
|
1620
|
+
self.change_start()
|
|
1621
|
+
did_change = True
|
|
1622
|
+
# Set the source to 'Use Parameters Below' by default
|
|
1623
|
+
self.change_cache.periods_src = 0
|
|
1624
|
+
if mode.strip().lower() == "int":
|
|
1625
|
+
self.change_cache.periods_type = 1
|
|
1626
|
+
if period_file is not None:
|
|
1627
|
+
period_file = get_correct_path(period_file)
|
|
1628
|
+
if not os.path.exists(period_file):
|
|
1629
|
+
raise Exception("Period file could not be found")
|
|
1630
|
+
self.change_cache.periods_src = 1
|
|
1631
|
+
self.change_cache.periods_file = period_file
|
|
1632
|
+
else:
|
|
1633
|
+
self.configure_internal_periods(
|
|
1634
|
+
sequences, output_delay, period, daq, dwell, unused, frames, output, label
|
|
1635
|
+
)
|
|
1636
|
+
elif mode.strip().lower() == "ext":
|
|
1637
|
+
self.change_cache.periods_type = 2
|
|
1638
|
+
else:
|
|
1639
|
+
raise Exception('Period mode invalid, it should be "int" or "ext"')
|
|
1640
|
+
if did_change:
|
|
1641
|
+
self.change_finish()
|
|
1642
|
+
|
|
1643
|
+
def configure_internal_periods(
|
|
1644
|
+
self,
|
|
1645
|
+
sequences: int | None = None,
|
|
1646
|
+
output_delay: int | None = None,
|
|
1647
|
+
period: int | None = None,
|
|
1648
|
+
daq: bool = False,
|
|
1649
|
+
dwell: bool = False,
|
|
1650
|
+
unused: bool = False,
|
|
1651
|
+
frames: int | None = None,
|
|
1652
|
+
output: int | None = None,
|
|
1653
|
+
label: str | None = None,
|
|
1654
|
+
) -> None:
|
|
1655
|
+
"""
|
|
1656
|
+
Configure the internal periods without switching to internal period mode.
|
|
1657
|
+
|
|
1658
|
+
Args:
|
|
1659
|
+
sequences: the number of period sequences [optional]
|
|
1660
|
+
output_delay: the output delay in microseconds [optional]
|
|
1661
|
+
period: the number of the period to set values for [optional]
|
|
1662
|
+
daq: the specified period is a aquisition period [optional]
|
|
1663
|
+
dwell: the specified period is a dwell period [optional]
|
|
1664
|
+
unused: the specified period is a unused period [optional]
|
|
1665
|
+
frames: the number of frames to count for the specified period [optional]
|
|
1666
|
+
output: the binary output the specified period [optional]
|
|
1667
|
+
label: the label for the period the specified period [optional]
|
|
1668
|
+
|
|
1669
|
+
Note: if the period number is unspecified then the settings will be applied to all periods.
|
|
1670
|
+
|
|
1671
|
+
Raises:
|
|
1672
|
+
Exception: if wrongly typed value supplied
|
|
1673
|
+
"""
|
|
1674
|
+
did_change = False
|
|
1675
|
+
if not self.in_change:
|
|
1676
|
+
self.change_start()
|
|
1677
|
+
did_change = True
|
|
1678
|
+
if sequences is not None:
|
|
1679
|
+
if isinstance(sequences, int):
|
|
1680
|
+
self.change_cache.periods_seq = sequences
|
|
1681
|
+
else:
|
|
1682
|
+
raise Exception("Number of period sequences must be an integer")
|
|
1683
|
+
if output_delay is not None:
|
|
1684
|
+
if isinstance(output_delay, int):
|
|
1685
|
+
self.change_cache.periods_delay = output_delay
|
|
1686
|
+
else:
|
|
1687
|
+
raise Exception("Output delay of periods must be an integer (microseconds)")
|
|
1688
|
+
self.define_hard_period(period, daq, dwell, unused, frames, output, label)
|
|
1689
|
+
if did_change:
|
|
1690
|
+
self.change_finish()
|
|
1691
|
+
|
|
1692
|
+
def define_hard_period(
|
|
1693
|
+
self,
|
|
1694
|
+
period: int | None = None,
|
|
1695
|
+
daq: bool = False,
|
|
1696
|
+
dwell: bool = False,
|
|
1697
|
+
unused: bool = False,
|
|
1698
|
+
frames: int | None = None,
|
|
1699
|
+
output: int | None = None,
|
|
1700
|
+
label: str | None = None,
|
|
1701
|
+
) -> None:
|
|
1702
|
+
"""
|
|
1703
|
+
Define the hardware periods.
|
|
1704
|
+
|
|
1705
|
+
Args:
|
|
1706
|
+
period: the number of the period to set values for [optional]
|
|
1707
|
+
daq: the specified period is a aquisition period [optional]
|
|
1708
|
+
dwell: the specified period is a dwell period [optional]
|
|
1709
|
+
unused: the specified period is a unused period [optional]
|
|
1710
|
+
frames: the number of frames to count for the specified period [optional]
|
|
1711
|
+
output: the binary output the specified period [optional]
|
|
1712
|
+
label: the label for the period the specified period [optional]
|
|
1713
|
+
|
|
1714
|
+
Note: if the period number is unspecified then the settings will be applied to all periods.
|
|
1715
|
+
|
|
1716
|
+
Raises:
|
|
1717
|
+
Exception: if supplied period is not a integer between 0 and 9
|
|
1718
|
+
"""
|
|
1719
|
+
did_change = False
|
|
1720
|
+
if not self.in_change:
|
|
1721
|
+
self.change_start()
|
|
1722
|
+
did_change = True
|
|
1723
|
+
if period is None:
|
|
1724
|
+
# Do for all periods (1 to 8)
|
|
1725
|
+
for i in range(1, 9):
|
|
1726
|
+
self.define_hard_period(i, daq, dwell, unused, frames, output, label)
|
|
1727
|
+
else:
|
|
1728
|
+
if isinstance(period, int) and 0 < period < 9:
|
|
1729
|
+
p_type = None # unchanged
|
|
1730
|
+
if unused:
|
|
1731
|
+
p_type = 0
|
|
1732
|
+
elif daq:
|
|
1733
|
+
p_type = 1
|
|
1734
|
+
elif dwell:
|
|
1735
|
+
p_type = 2
|
|
1736
|
+
p_frames = None # unchanged
|
|
1737
|
+
if frames is not None and isinstance(frames, int):
|
|
1738
|
+
p_frames = frames
|
|
1739
|
+
p_output = None # unchanged
|
|
1740
|
+
if output is not None and isinstance(output, int):
|
|
1741
|
+
p_output = output
|
|
1742
|
+
p_label = None # unchanged
|
|
1743
|
+
if label is not None:
|
|
1744
|
+
p_label = label
|
|
1745
|
+
self.change_cache.periods_settings.append(
|
|
1746
|
+
(period, p_type, p_frames, p_output, p_label)
|
|
1747
|
+
)
|
|
1748
|
+
else:
|
|
1749
|
+
raise Exception("Period number must be an integer from 1 to 8")
|
|
1750
|
+
if did_change:
|
|
1751
|
+
self.change_finish()
|
|
1752
|
+
|
|
1753
|
+
def _change_dae_settings(self) -> None:
|
|
1754
|
+
"""
|
|
1755
|
+
Changes the DAE settings.
|
|
1756
|
+
"""
|
|
1757
|
+
root = ET.fromstring(
|
|
1758
|
+
self._get_pv_value(self._get_dae_pv_name("daesettings"), to_string=True)
|
|
1759
|
+
)
|
|
1760
|
+
changed = self.change_cache.change_dae_settings(root)
|
|
1761
|
+
if changed:
|
|
1762
|
+
self._set_pv_value(
|
|
1763
|
+
self._get_dae_pv_name("daesettings_sp"),
|
|
1764
|
+
ET.tostring(root),
|
|
1765
|
+
wait=self.wait_for_completion_callback_dae_settings,
|
|
1766
|
+
)
|
|
1767
|
+
|
|
1768
|
+
"""confirm that the wiring tables for the dae_setting complete,
|
|
1769
|
+
must be done here due to equate for mulitiple change requests.
|
|
1770
|
+
"""
|
|
1771
|
+
tables_to_check = self._check_tables_not_empty()
|
|
1772
|
+
|
|
1773
|
+
if tables_to_check: # if there's nothing in the change cache we don't want to change tables
|
|
1774
|
+
if self._check_table_file_paths_correct(tables_to_check):
|
|
1775
|
+
print("All tables successfully changed.")
|
|
1776
|
+
|
|
1777
|
+
else: # there were some errors, report which tables failed to write.
|
|
1778
|
+
errors = " "
|
|
1779
|
+
for item in tables_to_check:
|
|
1780
|
+
if item.correctly_written is False:
|
|
1781
|
+
errors = "{}{} : {}, ".format(errors, item.table_type, item.cache_value)
|
|
1782
|
+
raise ValueError("{} table(s) failed to write.".format(errors))
|
|
1783
|
+
|
|
1784
|
+
def _check_table_file_paths_correct(self, tables_to_check: list[str]) -> None:
|
|
1785
|
+
"""Checks the wiring, detector and spectra tables in
|
|
1786
|
+
the dae settings against those provided in tables_to_check
|
|
1787
|
+
|
|
1788
|
+
@param tables_to_check : list containing the change_cache
|
|
1789
|
+
values for Wiring, Detector and Spectra tables.
|
|
1790
|
+
@returns written: boolean value True when all tables are correct.
|
|
1791
|
+
"""
|
|
1792
|
+
|
|
1793
|
+
written = True
|
|
1794
|
+
|
|
1795
|
+
for item in tables_to_check:
|
|
1796
|
+
if self.get_table_path(item.table_type) == item.cache_value:
|
|
1797
|
+
item = item._replace(correctly_written=True)
|
|
1798
|
+
written = written & item.correctly_written
|
|
1799
|
+
else:
|
|
1800
|
+
written = False
|
|
1801
|
+
return written
|
|
1802
|
+
|
|
1803
|
+
def _check_tables_not_empty(self) -> list[str]:
|
|
1804
|
+
"""Checks the wiring, detector and spectra tables in change_cache
|
|
1805
|
+
are not empty.
|
|
1806
|
+
|
|
1807
|
+
@returns tables_to_check: a list containing the change_cache
|
|
1808
|
+
values for Wiring, Detector and Spectra tables and a boolean state, used to
|
|
1809
|
+
indicate whether the file is correct in the dae settings.
|
|
1810
|
+
"""
|
|
1811
|
+
tables_to_check = []
|
|
1812
|
+
table_path = namedtuple("table_path", "table_type cache_value correctly_written")
|
|
1813
|
+
|
|
1814
|
+
if self.change_cache.wiring is not None:
|
|
1815
|
+
wiring = table_path("Wiring", self.change_cache.wiring, False)
|
|
1816
|
+
tables_to_check.append(wiring)
|
|
1817
|
+
if self.change_cache.detector is not None:
|
|
1818
|
+
detector = table_path("Detector", self.change_cache.detector, False)
|
|
1819
|
+
tables_to_check.append(detector)
|
|
1820
|
+
if self.change_cache.spectra is not None:
|
|
1821
|
+
spectra = table_path("Spectra", self.change_cache.spectra, False)
|
|
1822
|
+
tables_to_check.append(spectra)
|
|
1823
|
+
|
|
1824
|
+
return tables_to_check
|
|
1825
|
+
|
|
1826
|
+
def _get_tcb_xml(self) -> ET.Element:
|
|
1827
|
+
"""
|
|
1828
|
+
Reads the hexed and zipped TCB data.
|
|
1829
|
+
|
|
1830
|
+
Returns:
|
|
1831
|
+
The root of the xml.
|
|
1832
|
+
"""
|
|
1833
|
+
value = self._get_pv_value(self._get_dae_pv_name("tcbsettings"), to_string=True)
|
|
1834
|
+
xml = dehex_and_decompress(value)
|
|
1835
|
+
# Strip off any zlib checksum stuff at end of the string
|
|
1836
|
+
last = xml.rfind(">") + 1
|
|
1837
|
+
return ET.fromstring(xml[0:last].strip())
|
|
1838
|
+
|
|
1839
|
+
def _change_tcb_settings(self) -> None:
|
|
1840
|
+
"""
|
|
1841
|
+
Changes the TCB settings.
|
|
1842
|
+
"""
|
|
1843
|
+
root = self._get_tcb_xml()
|
|
1844
|
+
changed = self.change_cache.change_tcb_settings(root)
|
|
1845
|
+
if changed:
|
|
1846
|
+
ans = zlib.compress(ET.tostring(root))
|
|
1847
|
+
self._set_pv_value(self._get_dae_pv_name("tcbsettings_sp"), hexlify(ans), wait=True)
|
|
1848
|
+
|
|
1849
|
+
def _change_period_settings(self) -> None:
|
|
1850
|
+
"""
|
|
1851
|
+
Changes the period settings.
|
|
1852
|
+
|
|
1853
|
+
Raises:
|
|
1854
|
+
IOError: if the DAE could not set the number of periods.
|
|
1855
|
+
"""
|
|
1856
|
+
root = ET.fromstring(
|
|
1857
|
+
self._get_pv_value(self._get_dae_pv_name("periodsettings"), to_string=True)
|
|
1858
|
+
)
|
|
1859
|
+
changed = self.change_cache.change_period_settings(root)
|
|
1860
|
+
if changed:
|
|
1861
|
+
self._set_pv_value(
|
|
1862
|
+
self._get_dae_pv_name("periodsettings_sp"), ET.tostring(root).strip(), wait=True
|
|
1863
|
+
)
|
|
1864
|
+
|
|
1865
|
+
if self.api.get_pv_alarm(self._get_dae_pv_name("periodsettings_sp")) == "INVALID":
|
|
1866
|
+
raise IOError(
|
|
1867
|
+
"The DAE could not set the number of periods! "
|
|
1868
|
+
"This may be because you are trying to "
|
|
1869
|
+
"set a number that is too large for the DAE memory. Try a smaller number!"
|
|
1870
|
+
)
|
|
1871
|
+
|
|
1872
|
+
def get_spectrum(
|
|
1873
|
+
self, spectrum: int, period: int = 1, dist: bool = True, use_numpy: bool | None = None
|
|
1874
|
+
) -> dict:
|
|
1875
|
+
"""
|
|
1876
|
+
Gets a spectrum from the DAE via a PV.
|
|
1877
|
+
|
|
1878
|
+
Args:
|
|
1879
|
+
spectrum: the spectrum number
|
|
1880
|
+
period: the period number
|
|
1881
|
+
dist: True to return as a distribution (default), False to return as a histogram
|
|
1882
|
+
use_numpy (None|boolean): True use numpy to return arrays, False return a list;
|
|
1883
|
+
None for use the default
|
|
1884
|
+
|
|
1885
|
+
Returns:
|
|
1886
|
+
dict: all the spectrum data
|
|
1887
|
+
"""
|
|
1888
|
+
if dist:
|
|
1889
|
+
y_data = self._get_pv_value(
|
|
1890
|
+
self._get_dae_pv_name("getspectrum_y").format(period, spectrum), use_numpy=use_numpy
|
|
1891
|
+
)
|
|
1892
|
+
y_size = self._get_pv_value(
|
|
1893
|
+
self._get_dae_pv_name("getspectrum_y_size").format(period, spectrum)
|
|
1894
|
+
)
|
|
1895
|
+
y_data = y_data[:y_size]
|
|
1896
|
+
mode = "distribution"
|
|
1897
|
+
x_size = y_size
|
|
1898
|
+
else:
|
|
1899
|
+
y_data = self._get_pv_value(
|
|
1900
|
+
self._get_dae_pv_name("getspectrum_yc").format(period, spectrum),
|
|
1901
|
+
use_numpy=use_numpy,
|
|
1902
|
+
)
|
|
1903
|
+
y_size = self._get_pv_value(
|
|
1904
|
+
self._get_dae_pv_name("getspectrum_yc_size").format(period, spectrum)
|
|
1905
|
+
)
|
|
1906
|
+
y_data = y_data[:y_size]
|
|
1907
|
+
mode = "non-distribution"
|
|
1908
|
+
x_size = y_size + 1
|
|
1909
|
+
x_data = self._get_pv_value(
|
|
1910
|
+
self._get_dae_pv_name("getspectrum_x").format(period, spectrum), use_numpy=use_numpy
|
|
1911
|
+
)
|
|
1912
|
+
x_data = x_data[:x_size]
|
|
1913
|
+
|
|
1914
|
+
return {"time": x_data, "signal": y_data, "sum": None, "mode": mode}
|
|
1915
|
+
|
|
1916
|
+
def in_transition(self) -> bool:
|
|
1917
|
+
"""
|
|
1918
|
+
Checks whether the DAE is in transition.
|
|
1919
|
+
|
|
1920
|
+
Returns:
|
|
1921
|
+
bool: is the DAE in transition
|
|
1922
|
+
"""
|
|
1923
|
+
transition = self._get_pv_value(self._get_dae_pv_name("statetrans"))
|
|
1924
|
+
if transition == "Yes":
|
|
1925
|
+
return True
|
|
1926
|
+
else:
|
|
1927
|
+
return False
|
|
1928
|
+
|
|
1929
|
+
def get_wiring_tables(self) -> str:
|
|
1930
|
+
"""
|
|
1931
|
+
Gets a list of wiring table choices.
|
|
1932
|
+
|
|
1933
|
+
Returns:
|
|
1934
|
+
list: the table choices
|
|
1935
|
+
"""
|
|
1936
|
+
raw = dehex_and_decompress(
|
|
1937
|
+
self._get_pv_value(self._get_dae_pv_name("wiringtables"), to_string=True)
|
|
1938
|
+
)
|
|
1939
|
+
return json.loads(raw)
|
|
1940
|
+
|
|
1941
|
+
def get_spectra_tables(self) -> list[str]:
|
|
1942
|
+
"""
|
|
1943
|
+
Gets a list of spectra table choices.
|
|
1944
|
+
|
|
1945
|
+
Returns:
|
|
1946
|
+
list: the table choices
|
|
1947
|
+
"""
|
|
1948
|
+
raw = dehex_and_decompress(
|
|
1949
|
+
self._get_pv_value(self._get_dae_pv_name("spectratables"), to_string=True)
|
|
1950
|
+
)
|
|
1951
|
+
return json.loads(raw)
|
|
1952
|
+
|
|
1953
|
+
def get_detector_tables(self) -> list[str]:
|
|
1954
|
+
"""
|
|
1955
|
+
Gets a list of detector table choices.
|
|
1956
|
+
|
|
1957
|
+
Returns:
|
|
1958
|
+
list: the table choices
|
|
1959
|
+
"""
|
|
1960
|
+
raw = dehex_and_decompress(
|
|
1961
|
+
self._get_pv_value(self._get_dae_pv_name("detectortables"), to_string=True)
|
|
1962
|
+
)
|
|
1963
|
+
return json.loads(raw)
|
|
1964
|
+
|
|
1965
|
+
def get_period_files(self) -> list[str]:
|
|
1966
|
+
"""
|
|
1967
|
+
Gets a list of period file choices.
|
|
1968
|
+
|
|
1969
|
+
Returns:
|
|
1970
|
+
list: the table choices
|
|
1971
|
+
"""
|
|
1972
|
+
raw = dehex_and_decompress(
|
|
1973
|
+
self._get_pv_value(self._get_dae_pv_name("periodfiles"), to_string=True)
|
|
1974
|
+
)
|
|
1975
|
+
return json.loads(raw)
|
|
1976
|
+
|
|
1977
|
+
def get_tcb_settings(self, trange: int, regime: int = 1) -> dict:
|
|
1978
|
+
"""
|
|
1979
|
+
Gets a dictionary of the time channel settings.
|
|
1980
|
+
|
|
1981
|
+
Args:
|
|
1982
|
+
regime: the regime to read (1 to 6)
|
|
1983
|
+
trange: the time range to read (1 to 5) [optional]
|
|
1984
|
+
|
|
1985
|
+
Returns:
|
|
1986
|
+
dict: the low, high and step for the supplied range and regime
|
|
1987
|
+
"""
|
|
1988
|
+
root = self._get_tcb_xml()
|
|
1989
|
+
search_text = r"TR{} (\w+) {}".format(regime, trange)
|
|
1990
|
+
regex = re.compile(search_text)
|
|
1991
|
+
out = {}
|
|
1992
|
+
|
|
1993
|
+
for top in root.iter("DBL"):
|
|
1994
|
+
n = top.find("Name")
|
|
1995
|
+
match = regex.search(n.text)
|
|
1996
|
+
if match is not None:
|
|
1997
|
+
v = top.find("Val")
|
|
1998
|
+
out[match.group(1)] = v.text
|
|
1999
|
+
|
|
2000
|
+
return out
|
|
2001
|
+
|
|
2002
|
+
def get_table_path(self, table_type: str) -> str:
|
|
2003
|
+
dae_xml = self._get_dae_settings_xml()
|
|
2004
|
+
for top in dae_xml.iter("String"):
|
|
2005
|
+
n = top.find("Name")
|
|
2006
|
+
if n.text == "{} Table".format(table_type):
|
|
2007
|
+
val = top.find("Val")
|
|
2008
|
+
return val.text
|
|
2009
|
+
|
|
2010
|
+
def _get_dae_settings_xml(self) -> ET.Element:
|
|
2011
|
+
xml_value = self._get_pv_value(self._get_dae_pv_name("daesettings"))
|
|
2012
|
+
assert isinstance(xml_value, str)
|
|
2013
|
+
return ET.fromstring(xml_value)
|
|
2014
|
+
|
|
2015
|
+
def _wait_for_isis_dae_state(self, state: str, timeout: int) -> tuple[bool, str]:
|
|
2016
|
+
"""
|
|
2017
|
+
Wait for the isis dae to get to a state.
|
|
2018
|
+
:param state: state to reach
|
|
2019
|
+
:param timeout: timeout before reporting state wasn't reached
|
|
2020
|
+
:return: True if state was reached; False otherwise
|
|
2021
|
+
"""
|
|
2022
|
+
state_attained = False
|
|
2023
|
+
current_state = ""
|
|
2024
|
+
for _ in range(timeout):
|
|
2025
|
+
current_state = self._get_pv_value(self._prefix_pv_name("CS:PS:ISISDAE_01:STATUS"))
|
|
2026
|
+
if current_state == state:
|
|
2027
|
+
state_attained = True
|
|
2028
|
+
break
|
|
2029
|
+
else:
|
|
2030
|
+
sleep(1)
|
|
2031
|
+
return state_attained, current_state
|
|
2032
|
+
|
|
2033
|
+
def _isis_dae_triggered_state_was_reached(
|
|
2034
|
+
self,
|
|
2035
|
+
trigger_pv: str,
|
|
2036
|
+
state: str,
|
|
2037
|
+
timeout_per_trigger: int = 20,
|
|
2038
|
+
max_number_of_triggers: int = 5,
|
|
2039
|
+
) -> str:
|
|
2040
|
+
"""
|
|
2041
|
+
Trigger a state and wait for the state to be reached. For example stop the
|
|
2042
|
+
ISIS DAE and wait for it to be
|
|
2043
|
+
stopped. If the state isn't reached re-trigger the state
|
|
2044
|
+
:param trigger_pv: pv to trigger the state
|
|
2045
|
+
:param state: the state to reach
|
|
2046
|
+
:param timeout_per_trigger: timeout to wait to reach the state before retriggering
|
|
2047
|
+
:param max_number_of_triggers: The maximum number if triggers to do before exiting
|
|
2048
|
+
:return: True if state was reached; False otherwise
|
|
2049
|
+
"""
|
|
2050
|
+
self.api.logger.log_info_msg(
|
|
2051
|
+
"Trying to reach state '{}' using trigger pv '{}'".format(state, trigger_pv)
|
|
2052
|
+
)
|
|
2053
|
+
state_attained = False
|
|
2054
|
+
current_state = "Not set"
|
|
2055
|
+
for _ in range(max_number_of_triggers):
|
|
2056
|
+
self._set_pv_value(self._prefix_pv_name(trigger_pv), 1)
|
|
2057
|
+
state_attained, current_state = self._wait_for_isis_dae_state(
|
|
2058
|
+
state, timeout_per_trigger
|
|
2059
|
+
)
|
|
2060
|
+
if state_attained:
|
|
2061
|
+
break
|
|
2062
|
+
else:
|
|
2063
|
+
self.api.logger.log_error_msg(
|
|
2064
|
+
"Failed to get to state '{}' using trigger pv '{}' was in state '{}'".format(
|
|
2065
|
+
state, trigger_pv, current_state
|
|
2066
|
+
)
|
|
2067
|
+
)
|
|
2068
|
+
return state_attained
|
|
2069
|
+
|
|
2070
|
+
@contextmanager
|
|
2071
|
+
def temporarily_kill_icp(self) -> None:
|
|
2072
|
+
"""
|
|
2073
|
+
Context manager to temporarily kill ICP.
|
|
2074
|
+
"""
|
|
2075
|
+
try:
|
|
2076
|
+
if not self._isis_dae_triggered_state_was_reached("CS:PS:ISISDAE_01:STOP", "Shutdown"):
|
|
2077
|
+
raise IOError("Could not stop ISISDAE!")
|
|
2078
|
+
for p in psutil.process_iter():
|
|
2079
|
+
if p.name().lower() == "isisicp.exe":
|
|
2080
|
+
p.kill()
|
|
2081
|
+
yield
|
|
2082
|
+
finally:
|
|
2083
|
+
if not self._isis_dae_triggered_state_was_reached("CS:PS:ISISDAE_01:START", "Running"):
|
|
2084
|
+
raise IOError("Could not restart ISISDAE!")
|
|
2085
|
+
|
|
2086
|
+
if self._get_pv_value(self._prefix_pv_name("CS:PS:ISISDAE_01:AUTORESTART")) != "On":
|
|
2087
|
+
self._set_pv_value(self._prefix_pv_name("CS:PS:ISISDAE_01:TOGGLE"), 1)
|
|
2088
|
+
|
|
2089
|
+
@require_runstate(["SETUP", "PROCESSING"])
|
|
2090
|
+
def set_simulation_mode(self, mode: bool) -> None:
|
|
2091
|
+
"""
|
|
2092
|
+
Sets the DAE simulation mode by writing to ICP_config.xml and restarting the
|
|
2093
|
+
DAE IOC and ISISICP
|
|
2094
|
+
Args:
|
|
2095
|
+
mode (bool): True to simulate the DAE, False otherwise
|
|
2096
|
+
"""
|
|
2097
|
+
|
|
2098
|
+
existent_config_files = [p for p in DAE_CONFIG_FILE_PATHS if os.path.exists(p)]
|
|
2099
|
+
|
|
2100
|
+
if not len(existent_config_files) > 0:
|
|
2101
|
+
raise IOError("Could not find ICP configuration file")
|
|
2102
|
+
|
|
2103
|
+
with self.temporarily_kill_icp():
|
|
2104
|
+
for path in existent_config_files:
|
|
2105
|
+
xml = ET.parse(path).getroot()
|
|
2106
|
+
|
|
2107
|
+
node = xml.find(r"I32/[Name='Simulate']/Val")
|
|
2108
|
+
if node is None:
|
|
2109
|
+
raise ValueError("No 'simulate' tag in ISISICP config file.")
|
|
2110
|
+
node.text = "1" if mode else "0"
|
|
2111
|
+
|
|
2112
|
+
os.chmod(path, S_IWUSR | S_IREAD)
|
|
2113
|
+
|
|
2114
|
+
with open(path, "wb") as f:
|
|
2115
|
+
f.write(ET.tostring(xml))
|
|
2116
|
+
|
|
2117
|
+
def get_simulation_mode(self) -> bool:
|
|
2118
|
+
"""
|
|
2119
|
+
Gets the DAE simulation mode.
|
|
2120
|
+
Returns:
|
|
2121
|
+
True if the DAE is in simulation mode, False otherwise.
|
|
2122
|
+
"""
|
|
2123
|
+
return self._get_pv_value(self._prefix_pv_name(DAE_PVS_LOOKUP["simulation_mode"])) == "Yes"
|
|
2124
|
+
|
|
2125
|
+
def is_changing(self) -> bool:
|
|
2126
|
+
"""
|
|
2127
|
+
Gets whether the DAE is in state changing mode.
|
|
2128
|
+
Returns:
|
|
2129
|
+
True if the DAE is in state changing mode, False otherwise.
|
|
2130
|
+
"""
|
|
2131
|
+
return self._get_pv_value(self._prefix_pv_name(DAE_PVS_LOOKUP["state_changing"])) == "Yes"
|
|
2132
|
+
|
|
2133
|
+
def integrate_spectrum(
|
|
2134
|
+
self, spectrum: int, period: int = 1, t_min: float | None = None, t_max: float | None = None
|
|
2135
|
+
) -> float:
|
|
2136
|
+
"""
|
|
2137
|
+
Integrates the spectrum within the time period and returns neutron counts.
|
|
2138
|
+
|
|
2139
|
+
The underlying algorithm sums the counts from each bin, if a bin is split by the
|
|
2140
|
+
time region then a proportional fraction of the count for that bin is used.
|
|
2141
|
+
|
|
2142
|
+
Args:
|
|
2143
|
+
spectrum (int): the spectrum number
|
|
2144
|
+
period (int, optional): the period
|
|
2145
|
+
t_min (float, optional): time of flight to start from
|
|
2146
|
+
t_max (float, optional): time of flight to finish at
|
|
2147
|
+
|
|
2148
|
+
Returns:
|
|
2149
|
+
float: integral of the spectrum (neutron counts)
|
|
2150
|
+
"""
|
|
2151
|
+
spectrum = self.get_spectrum(spectrum, period, False, use_numpy=True)
|
|
2152
|
+
time = spectrum["time"]
|
|
2153
|
+
count = spectrum["signal"]
|
|
2154
|
+
|
|
2155
|
+
if time is None or count is None:
|
|
2156
|
+
return None
|
|
2157
|
+
|
|
2158
|
+
# Get index for first bin with data in (partial or not)
|
|
2159
|
+
if t_min is None:
|
|
2160
|
+
first_bin_included = 0
|
|
2161
|
+
t_min = time[first_bin_included]
|
|
2162
|
+
else:
|
|
2163
|
+
if t_min < time[0]:
|
|
2164
|
+
raise ValueError(
|
|
2165
|
+
"Argument from_time, {}, is less than lowest bin time, {}.".format(
|
|
2166
|
+
t_min, time[0]
|
|
2167
|
+
)
|
|
2168
|
+
)
|
|
2169
|
+
first_bin_included = time.searchsorted(t_min, side="left")
|
|
2170
|
+
|
|
2171
|
+
# Get index of highest bin from which all data is included
|
|
2172
|
+
if t_max is None:
|
|
2173
|
+
last_complete_bin = len(time) - 1
|
|
2174
|
+
t_max = time[last_complete_bin]
|
|
2175
|
+
else:
|
|
2176
|
+
if t_max > time[-1]:
|
|
2177
|
+
raise ValueError(
|
|
2178
|
+
"Argument to_time, {}, is greater than highest bin time, {}.".format(
|
|
2179
|
+
t_max, time[-1]
|
|
2180
|
+
)
|
|
2181
|
+
)
|
|
2182
|
+
last_complete_bin = time.searchsorted(t_max, side="left")
|
|
2183
|
+
|
|
2184
|
+
# Error check
|
|
2185
|
+
if t_max < t_min:
|
|
2186
|
+
raise ValueError("Time range is not valid, to_time is less than from_time.")
|
|
2187
|
+
|
|
2188
|
+
# Calculate extra counts from top bin if it is only a partial bin
|
|
2189
|
+
if t_max != time[last_complete_bin]:
|
|
2190
|
+
last_complete_bin -= 1
|
|
2191
|
+
|
|
2192
|
+
width = time[last_complete_bin + 1] - time[last_complete_bin]
|
|
2193
|
+
|
|
2194
|
+
partial_count_high = count[last_complete_bin]
|
|
2195
|
+
partial_count_high *= (t_max - time[last_complete_bin]) / width
|
|
2196
|
+
|
|
2197
|
+
else:
|
|
2198
|
+
partial_count_high = 0.0
|
|
2199
|
+
|
|
2200
|
+
# Calculate missing counts from the lowest bin that needs to be subtracted
|
|
2201
|
+
# if this is a partial bin
|
|
2202
|
+
if t_min != time[first_bin_included]:
|
|
2203
|
+
first_bin_included -= 1
|
|
2204
|
+
partial_count_low = count[first_bin_included]
|
|
2205
|
+
|
|
2206
|
+
width = time[first_bin_included + 1] - time[first_bin_included]
|
|
2207
|
+
partial_count_low *= (t_min - time[first_bin_included]) / width
|
|
2208
|
+
|
|
2209
|
+
else:
|
|
2210
|
+
partial_count_low = 0.0
|
|
2211
|
+
|
|
2212
|
+
# calculate sum from lowest bin with any counts in to hightest bin
|
|
2213
|
+
# that is completely included
|
|
2214
|
+
full_count = np.sum(count[first_bin_included:last_complete_bin])
|
|
2215
|
+
|
|
2216
|
+
# run sum of terms, note in the case that the high and low partials
|
|
2217
|
+
# are in the same bin this still works
|
|
2218
|
+
return full_count + partial_count_high - partial_count_low
|