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.
Files changed (43) hide show
  1. genie_python/.pylintrc +539 -0
  2. genie_python/__init__.py +1 -0
  3. genie_python/block_names.py +123 -0
  4. genie_python/channel_access_exceptions.py +45 -0
  5. genie_python/genie.py +2462 -0
  6. genie_python/genie_advanced.py +418 -0
  7. genie_python/genie_alerts.py +195 -0
  8. genie_python/genie_api_setup.py +451 -0
  9. genie_python/genie_blockserver.py +64 -0
  10. genie_python/genie_cachannel_wrapper.py +545 -0
  11. genie_python/genie_change_cache.py +151 -0
  12. genie_python/genie_dae.py +2218 -0
  13. genie_python/genie_epics_api.py +906 -0
  14. genie_python/genie_experimental_data.py +186 -0
  15. genie_python/genie_logging.py +200 -0
  16. genie_python/genie_p4p_wrapper.py +203 -0
  17. genie_python/genie_plot.py +77 -0
  18. genie_python/genie_pre_post_cmd_manager.py +21 -0
  19. genie_python/genie_pv_connection_protocol.py +36 -0
  20. genie_python/genie_script_checker.py +507 -0
  21. genie_python/genie_script_generator.py +212 -0
  22. genie_python/genie_simulate.py +69 -0
  23. genie_python/genie_simulate_impl.py +1265 -0
  24. genie_python/genie_startup.py +29 -0
  25. genie_python/genie_toggle_settings.py +58 -0
  26. genie_python/genie_wait_for_move.py +154 -0
  27. genie_python/genie_waitfor.py +576 -0
  28. genie_python/matplotlib_backend/__init__.py +0 -0
  29. genie_python/matplotlib_backend/ibex_websocket_backend.py +366 -0
  30. genie_python/mysql_abstraction_layer.py +272 -0
  31. genie_python/run_tests.py +56 -0
  32. genie_python/scanning_instrument_pylint_plugin.py +31 -0
  33. genie_python/typings/CaChannel/CaChannel.pyi +893 -0
  34. genie_python/typings/CaChannel/__init__.pyi +9 -0
  35. genie_python/typings/CaChannel/_version.pyi +6 -0
  36. genie_python/typings/CaChannel/ca.pyi +31 -0
  37. genie_python/utilities.py +406 -0
  38. genie_python/version.py +1 -0
  39. genie_python-15.1.0rc1.dist-info/LICENSE +28 -0
  40. genie_python-15.1.0rc1.dist-info/METADATA +95 -0
  41. genie_python-15.1.0rc1.dist-info/RECORD +43 -0
  42. genie_python-15.1.0rc1.dist-info/WHEEL +5 -0
  43. 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