-
Notifications
You must be signed in to change notification settings - Fork 0
/
dectris_eiger_ioc.py
417 lines (357 loc) · 20.3 KB
/
dectris_eiger_ioc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
import logging
from pathlib import Path
import sys
import attrs
from datetime import datetime, timezone
from caproto.server import PVGroup, pvproperty, template_arg_parser, run
import numpy as np
from deigerclient import DEigerClient
import os
import asyncio
from validators import validate_ip_address, validate_port_number, ensure_directory_exists_and_is_writeable
from custom_operations import CustomPostExposureOperation
import time
from typing import Callable
import logging
logger = logging.getLogger("DEigerIOC")
# logger.setLevel(logging.INFO)
@attrs.define
class DEigerIOC(PVGroup):
"""
A caproto-based IOC (Input/Output Controller) for managing Dectris Eiger detectors.
This class facilitates setting, triggering, and collecting data from Dectris Eiger detectors.
It integrates network configuration, data handling, and detector management by wrapping
various detector functionalities including energy values, timing configuration, and file writing.
See README.md for information on how to use it.
Attributes:
host (str): IP address of the detector.
port (int): Port number for the detector connection.
client (DEigerClient): Client interface for communicating with the detector.
LocalFileDumpPath (Path): Path where the detector files are stored locally.
_nframes (int): Number of frames to be taken in a single exposure.
_starttime (datetime): Start time of the exposure.
authors: Brian R. Pauw, Anja Hörmann.
DEigerClient from Dectris
License: MIT
"""
host: str = attrs.field(default="172.17.1.2", validator=validate_ip_address, converter=str)
port: int = attrs.field(default=80, validator=validate_port_number, converter=int)
client: DEigerClient = attrs.field(init=False, validator=attrs.validators.optional(attrs.validators.instance_of(DEigerClient)))
# files measured on the detector are stored here.
LocalFileDumpPath: Path = attrs.field(default=Path("/tmp"), converter=Path, validator=[attrs.validators.instance_of(Path), ensure_directory_exists_and_is_writeable])
# number of frames to be taken in a single exposure
_nframes: int = attrs.field(default=1, validator=attrs.validators.optional(attrs.validators.instance_of(int)))
_nimages_per_file:int = attrs.field(default=1800, validator=attrs.validators.instance_of(int))
# start time of the exposure
_starttime: datetime = attrs.field(default=None, validator=attrs.validators.optional(attrs.validators.instance_of(datetime)))
# for any location-specific operations that need to be performed after data collection
custom_post_exposure_operation: CustomPostExposureOperation = attrs.field(factory=CustomPostExposureOperation)
# if we tried writing while the detector was initializing or measuring:
_detector_initialized:bool = attrs.field(default=False, validator=attrs.validators.instance_of(bool))
_detector_configured:bool = attrs.field(default=False, validator=attrs.validators.instance_of(bool))
def __init__(self, *args, **kwargs) -> None:
for k in list(kwargs.keys()):
if k in ['host', 'port']:
setattr(self, k, kwargs.pop(k))
self.LocalFileDumpPath = kwargs.pop('localPath', Path("/tmp"))
print(f'{self.LocalFileDumpPath=}')
self.client = DEigerClient(self.host, port=self.port)
self._starttime = None #datetime.now(timezone.utc)
self._nimages_per_file = 1800
self._detector_initialized = False
self._nframes = 0
super().__init__(*args, **kwargs)
def empty_data_store(self):
self.client.sendFileWriterCommand("clear")
# writing of files needs to be enabled again after
self.client.setFileWriterConfig("mode", "enabled")
def restart_detector(self):
print(" restarting detector")
self.client.sendSystemCommand("restart")
time.sleep(.1)
def initialize_detector(self):
print(" sending init command")
Trouble=False
try:
self.client.sendDetectorCommand("initialize")
print(" finished sending init command")
except RuntimeError:
print(" Trouble initializing")
Trouble=True
ntry = 5
while (self.DetectorState.value in ['na', 'error']) or Trouble:
print(f'failure to initialize detector, trying again {ntry} times out of 5')
time.sleep(1)
self.client.sendDetectorCommand("initialize")
ntry -=1
if ntry <0:
print(f'FAILURE TO INITIALIZE detector')
self._detector_initialized = True
return
def set_energy_values(self, PhotonEnergy = None, ThresholdEnergy = None):
if PhotonEnergy is None:
PhotonEnergy = self.PhotonEnergy.value
if ThresholdEnergy is None:
ThresholdEnergy = self.ThresholdEnergy.value
self.client.setDetectorConfig("photon_energy", PhotonEnergy)
self.client.setDetectorConfig("threshold_energy", ThresholdEnergy)
def set_timing_values(self, FrameTime = None, CountTime = None):
if FrameTime is None:
FrameTime = self.FrameTime.value
if CountTime is None:
CountTime = self.CountTime.value
""" this also sets _nframes to the correct value"""
print("count_time to be set: ", CountTime)
self.client.setDetectorConfig("count_time", CountTime)
print("frame_time to be set: ", FrameTime)
self.client.setDetectorConfig("frame_time", FrameTime)
# maybe something else needs to be added here to account for deadtime between frames.
self._nframes = int(np.ceil(CountTime/ FrameTime))
self.client.setDetectorConfig("nimages",self._nframes)
self.client.setDetectorConfig("ntrigger", 1) # one trigger per sequence. (trigger_mode = ints)
self.client.setDetectorConfig("trigger_mode","ints") # as seen in the dectris example notebook
def set_filewriter_config(self):
self.client.setFileWriterConfig("mode", "enabled") # write HDF5 files
self.client.setFileWriterConfig("name_pattern", f"{self.OutputFilePrefix.value}$id")
self.client.setFileWriterConfig("nimages_per_file", self._nimages_per_file) # maximum 1800 frames per file
self.client.fileWriterConfig("compression_enabled")
self.client.setDetectorConfig("compression", "bslz4")
def set_monitor_and_stream_config(self):
self.client.monitorConfig("mode","disabled")
# zmq stream config
# self.client.setStreamConfig('format','cbor')
self.client.setStreamConfig("mode","disabled")
# self.client.setStreamConfig("header_detail", "all")
def configure_detector(self):
""" runs all the required detector initializations before a measurement"""
if not self._detector_initialized:
print('before configuring the detector, I will intialize it at least once...')
self.initialize_detector()
self.set_energy_values()
self.set_timing_values()
self.empty_data_store()
self.set_filewriter_config()
self.client.setDetectorConfig("countrate_correction_applied", self.CountRateCorrection.value)
self.client.setDetectorConfig("flatfield_correction_applied", self.FlatFieldCorrection.value)
self.client.setDetectorConfig("pixel_mask_applied", self.PixelMaskCorrection.value)
def read_detector_configuration_safely(self, key:str="", default=None, readMethod: str = 'detectorStatus'):
""" reads the detector configuration of a particular key and returns it as a dictionary. Safely handles errors"""
try:
if readMethod == 'detectorStatus':
answer = self.client.detectorStatus(key)
else:
answer = self.client.detectorConfig(key)
if not isinstance(answer, dict):
return default
else:
return answer["value"]
except:
return default
def read_and_dump_files(self):
""" reads all files in the data store and dumps them to disk at the location specified upon IOC init"""
expected_number_of_files = np.ceil(self._nframes/self._nimages_per_file)+1
filenames = self.client.fileWriterFiles()# returns all files in datastore
ntry = 200 # 20 seconds...
while not len(filenames)>=expected_number_of_files:
time.sleep(.1)
filenames = self.client.fileWriterFiles() #['value'] # returns all files in datastore
if ntry <0:
print('did not find the needed number of files after 20 seconds')
return
ntry -= 1
print(f'filenames found: {filenames}')
for filename in filenames:
if (filename in os.listdir(self.LocalFileDumpPath)) or not filename.startswith(self.OutputFilePrefix.value):
continue # skip if file already exists or is one we're not looking for
print(f'retrieving: {filename}')
self.client.fileWriterSave(filename, self.LocalFileDumpPath)
asyncio.run(self.LatestFile.write(str(filename)))
if 'master' in filename:
asyncio.run(self.LatestFileMain.write(str(filename)))
elif 'data' in filename:
asyncio.run(self.LatestFileData.write(str(filename)))
def retrieve_all_and_clear_files(self):
""" retrieves all files from the data store and clears the data store"""
self.read_and_dump_files()
self.empty_data_store()
async def wait_for_init_complete(self):
reconfigure = False
if self.DetectorState.value in ['error']:
print('error state detected in detector, restarting before reinitializing...')
await self.Restart.write(True)
await asyncio.sleep(2)
reconfigure = True
if self.DetectorState.value in ['na', 'error', 'ready']:
print('error state detected in detector, reinitializing before triggering...')
await self.Initialize.write(True)
await asyncio.sleep(.1)
reconfigure = True
counter = 0
while self.Initialize_RBV.value not in ['Off', False]:
counter += 1
await asyncio.sleep(.1)
if (counter % 10 == 0):
print('waiting for initialization to complete...')
if counter>250:
break
if reconfigure:
await self.Configure.write(True)
await asyncio.sleep(.1)
reconfigure = False
counter = 0
while self.Configure_RBV.value not in ['Off', False]:
counter += 1
await asyncio.sleep(.1)
if (counter % 10 == 0):
print('waiting for configuration to complete...')
if counter>250:
break
async def arm_trigger_disarm(self):
print('arming detector')
counter = 0
loop = asyncio.get_event_loop()
# await loop.run_in_executor(None, self.initialize_detector)
while counter <10:
counter += 1
try:
arm_answer = await loop.run_in_executor(None, self.client.sendDetectorCommand, "arm")
print(f'{arm_answer =}')
break # done if we got to this stage
except RuntimeError:
print(f'trouble arming detector in attempt {counter}, waiting a second before trying again')
await asyncio.sleep(1)
print('triggering detector')
counter = 0
self._starttime = datetime.now(timezone.utc)
while counter <10:
counter += 1
try:
trigger_answer = await loop.run_in_executor(None, self.client.sendDetectorCommand, "trigger")
print(f'{trigger_answer =}')
break # done if we got to this stage
except RuntimeError:
print(f'trouble triggering detector in attempt {counter}, waiting a second before trying again')
await asyncio.sleep(1)
print('disarming detector')
counter = 0
while counter <10:
counter += 1
try:
disarm_answer = await loop.run_in_executor(None, self.client.sendDetectorCommand, "disarm")
print(f'{disarm_answer =}')
break # done if we got to this stage
except RuntimeError:
print(f'trouble disarming detector in attempt {counter}, waiting a second before trying again')
await asyncio.sleep(1)
# Detector state readouts
DetectorState = pvproperty(value = '', doc="State of the detector, can be 'busy' or 'idle'", dtype=str, record='stringin',
report_as_string=True)
DetectorTemperature = pvproperty(value = -999.9, doc="Temperature of the detector", dtype=float, record='ai')
DetectorTime = pvproperty(value = '', doc="Timestamp on the detector", dtype=str, record='stringin', report_as_string=True)
CountTime_RBV = pvproperty(value = 600.0, doc="Gets the actual total exposure time the detector", dtype=float, record='ai')
FrameTime_RBV = pvproperty(value = 10.0, doc="Gets the actual frame time from the detector", dtype=float, record='ai')
# settables for the detector
ThresholdEnergy = pvproperty(value = 4025.0, doc="Sets the energy threshold on the detector, normally 0.5 * PhotonEnergy", record='ao')
PhotonEnergy = pvproperty(value = 8050.0, doc="Sets the photon energy on the detector", record='ai')
FrameTime = pvproperty(value = 10.0, doc="Sets the frame time on the detector. nominally should be <= CountTime", record='ai')
CountTime = pvproperty(value = 600.0, doc="Sets the total exposure time the detector", record='ai')
CountRateCorrection = pvproperty(value = True, doc="do you want count rate correction applied by the detector (using int maths)", record='bi')
FlatFieldCorrection = pvproperty(value = False, doc="do you want flat field correction applied by the detector (using int maths)", record='bi')
PixelMaskCorrection = pvproperty(value = False, doc="do you want pixel mask correction applied by the detector", record='bi')
# operating the detector
Restart = pvproperty(doc="Restart the detector, resets to False immediately", dtype=bool, record='bi')
Restart_RBV = pvproperty(doc="True while detector is restarting", dtype=bool, record='bo')
Initialize = pvproperty(doc="Initialize the detector, resets to False immediately", dtype=bool, record='bi')
Initialize_RBV = pvproperty(doc="True while detector is initializing", dtype=bool, record='bo')
Configure = pvproperty(doc="Configures the detector, resets to False immediately", dtype=bool, record='bi')
Configure_RBV = pvproperty(doc="True while detector is Configuring", dtype=bool, record='bo')
Trigger = pvproperty(doc="Trigger the detector to take an image, resets to False immediately. Adjusts detector_state to 'busy' for the duration of the measurement.", record='bi', dtype=bool)
Trigger_RBV = pvproperty(doc="True while the detector capture subroutine in the IOC is busy", dtype=bool, record='bo')
OutputFilePrefix = pvproperty(value="eiger_", doc="Set the prefix of the main and data output files", dtype=str, record='stringin', report_as_string=True)
LatestFile = pvproperty(value = '', doc="Shows the name of the latest output file retrieved", dtype=str, record='stringin', report_as_string=True)
LatestFileData = pvproperty(value = '', doc="Shows the name of the latest output data file retrieved", dtype=str, record='stringin', report_as_string=True)
LatestFileMain = pvproperty(value = '', doc="Shows the name of the latest output main file retrieved", dtype=str, record='stringin', report_as_string=True)
SecondsRemaining = pvproperty(value = 0.0, doc="Shows the seconds remaining for the current exposure", dtype=float, record='ai')
@DetectorState.scan(period=1, use_scan_field=True, subtract_elapsed=True)
async def DetectorState(self, instance, async_lib):
await self.DetectorState.write(self.read_detector_configuration_safely("state", "unknown", readMethod='detectorStatus'))
@DetectorTemperature.scan(period=5, use_scan_field=True, subtract_elapsed=True)
async def DetectorTemperature(self, instance, async_lib):
await self.DetectorTemperature.write(float(self.read_detector_configuration_safely("board_000/th0_temp", -999.0, readMethod='detectorStatus')))
@DetectorTime.scan(period=5, use_scan_field=True, subtract_elapsed=True)
async def DetectorTime(self, instance, async_lib):
await self.DetectorTime.write(self.read_detector_configuration_safely("time", "unknown", readMethod='detectorStatus'))
@SecondsRemaining.scan(period=1, use_scan_field=True, subtract_elapsed=True)
async def SecondsRemaining(self, instance, async_lib):
if self._starttime is not None:
elapsed = datetime.now(timezone.utc) - self._starttime
remaining = self.CountTime.value - elapsed.total_seconds()
await self.SecondsRemaining.write(int(np.maximum(remaining, 0)))
else:
await self.SecondsRemaining.write(-999)
@CountTime_RBV.getter
async def CountTime_RBV(self, instance):
await self.CountTime_RBV.write(float(self.read_detector_configuration_safely("count_time", -999.0, readMethod='detectorConfig')))
@CountTime.getter
async def CountTime(self, instance):
await self.CountTime_RBV.write(float(self.read_detector_configuration_safely("count_time", -999.0, readMethod='detectorConfig')))
@FrameTime_RBV.getter
async def FrameTime_RBV(self, instance):
await self.FrameTime_RBV.write(float(self.read_detector_configuration_safely("frame_time", -999.0, readMethod='detectorConfig')))
@FrameTime.getter
async def FrameTime(self, instance):
await self.FrameTime_RBV.write(float(self.read_detector_configuration_safely("frame_time", -999.0, readMethod='detectorConfig')))
@Initialize.putter
async def Initialize(self, instance, value: bool):
# await self.ReadyToTrigger.write(False)
if value:
await self.Initialize_RBV.write(True)
loop = asyncio.get_event_loop()
print('Initializer running self.initialize_detector')
await loop.run_in_executor(None, self.initialize_detector)
await self.Initialize_RBV.write(False)
@Configure.putter
async def Configure(self, instance, value: bool):
if value:
await self.Configure_RBV.write(True)
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, self.configure_detector)
await self.Configure_RBV.write(False)
@Restart.putter
async def Restart(self, instance, value: bool):
if value:
await self.Restart_RBV.write(True)
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, self.restart_detector)
await self.Restart_RBV.write(False)
@Trigger.putter
async def Trigger(self, instance, value: bool):
if value:
await self.Trigger_RBV.write(True)
# ensure initialisation is complete first..
print('running wait_for_init_complete()')
await self.wait_for_init_complete()
loop = asyncio.get_event_loop()
await self.arm_trigger_disarm()
print('retrieving files')
await loop.run_in_executor(None, self.retrieve_all_and_clear_files)
print('done retrieving files')
await self.Trigger_RBV.write(False)
def main(args=None):
parser, split_args = template_arg_parser(
default_prefix="detector_eiger:",
desc="EPICS IOC for setting and capturing images using the Dectris Eiger detector",
)
if args is None:
args = sys.argv[1:]
parser.add_argument("--host", required=True, type=str, help="IP address of the host/device")
parser.add_argument("--port", type=int, default=80, help="Port number of the device")
parser.add_argument("--localPath", "-L", type=Path, default=Path("/tmp"),
help="Path where the detector files are stored locally")
args = parser.parse_args()
logging.info(f"Running Dectis Eiger IOC on {args}")
ioc_options, run_options = split_args(args)
ioc = DEigerIOC(host=args.host, port=args.port, localPath=args.localPath, **ioc_options)
run(ioc.pvdb, **run_options)
if __name__ == "__main__":
main()