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