ChessAnalysisPipeline 0.0.15__py3-none-any.whl → 0.0.16__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.

Potentially problematic release.


This version of ChessAnalysisPipeline might be problematic. Click here for more details.

CHAP/utils/scanparsers.py DELETED
@@ -1,1544 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- # -*- coding: utf-8 -*-
4
-
5
- """Parsing data from certain CHESS SPEC scans is supported by a family
6
- of classes derived from the base class, `ScanParser` (defined
7
- below). An instance of `ScanParser` represents a single SPEC scan --
8
- each instance is initialized with the name of a specific spec file and
9
- integer scan number. Access to certain data collected by that scan
10
- (counter data, positioner values, scan shape, detector data, etc.) are
11
- made available through the properties and methods of that object.
12
-
13
- `ScanParser` is just an incomplete abstraction -- one should not
14
- declare or work with an instance of `ScanParser` directly. Instead,
15
- one must find the appropriate concrete subclass to use for the
16
- particular type of scan one wishes to parse, then declare an instance
17
- of that specific class to begin accessing data from that scan.
18
-
19
- Basic usage examples:
20
- 1. Print out the position of a the SPEC positioner motor with mnemonic
21
- `'mne0'` for a SAXS/WAXS scan collected at FMB:
22
- ```python
23
- from CHAP.utils.scanparsers import FMBSAXSWAXSScanParser
24
- sp = FMBSAXSWAXSScanParser('/path/to/fmb/saxswaxs/spec/file', 1)
25
- print(sp.get_spec_positioner_value('mne0'))
26
- ```
27
- 1. Store all the detector data collected by the detector with prefix
28
- `'det'` over a rotation series collected at SMB in the variable
29
- `data`:
30
- ```python
31
- from CHAP.utils.scanparsers import SMBRotationScanParser
32
- sp = SMBRotationScanParser('/path/to/smb/rotation/spec/file', 1)
33
- data = sp.get_detector_data('det')
34
- ```
35
- """
36
-
37
- # System modules
38
- from csv import reader
39
- from fnmatch import filter as fnmatch_filter
40
- from functools import cache
41
- from json import load
42
- import os
43
- import re
44
-
45
- # Third party modules
46
- import numpy as np
47
- from pyspec.file.spec import FileSpec
48
- from pyspec.file.tiff import TiffFile
49
-
50
- @cache
51
- def filespec(spec_file_name):
52
- return FileSpec(spec_file_name)
53
-
54
- class ScanParser:
55
- """Partial implementation of a class representing a SPEC scan and
56
- some of its metadata.
57
-
58
- :param spec_file_name: path to a SPEC file on the CLASSE DAQ
59
- :type spec_file_name: str
60
- :param scan_number: the number of a scan in the SPEC file provided
61
- with `spec_file_name`
62
- :type scan_number: int
63
- """
64
-
65
- def __init__(self,
66
- spec_file_name:str,
67
- scan_number:int):
68
- """Constructor method"""
69
-
70
- self.spec_file_name = spec_file_name
71
- self.scan_number = scan_number
72
-
73
- self._scan_path = None
74
- self._scan_name = None
75
- self._scan_title = None
76
-
77
- self._spec_scan = None
78
- self._spec_command = None
79
- self._spec_macro = None
80
- self._spec_args = None
81
- self._spec_scan_npts = None
82
- self._spec_scan_data = None
83
- self._spec_positioner_values = None
84
-
85
- self._detector_data_path = None
86
-
87
- if isinstance(self, FMBRotationScanParser) and scan_number > 1:
88
- scanparser = FMBRotationScanParser(spec_file_name, scan_number-1)
89
- if (scanparser.spec_macro in ('rams4_step_ome', 'rams4_fly_ome')
90
- and len(scanparser.spec_args) == 5):
91
- self._rams4_args = scanparser.spec_args
92
-
93
- def __repr_(self):
94
- return (f'{self.__class__.__name__}'
95
- f'({self.spec_file_name}, {self.scan_number}) '
96
- f'-- {self.spec_command}')
97
-
98
- @property
99
- def spec_file(self):
100
- # NB This FileSpec instance is not stored as a private
101
- # attribute because it cannot be pickled (and therefore could
102
- # cause problems for parallel code that uses ScanParsers).
103
- return filespec(self.spec_file_name)
104
-
105
- @property
106
- def scan_path(self):
107
- if self._scan_path is None:
108
- self._scan_path = self.get_scan_path()
109
- return self._scan_path
110
-
111
- @property
112
- def scan_name(self):
113
- if self._scan_name is None:
114
- self._scan_name = self.get_scan_name()
115
- return self._scan_name
116
-
117
- @property
118
- def scan_title(self):
119
- if self._scan_title is None:
120
- self._scan_title = self.get_scan_title()
121
- return self._scan_title
122
-
123
- @property
124
- def spec_scan(self):
125
- if self._spec_scan is None:
126
- self._spec_scan = self.get_spec_scan()
127
- return self._spec_scan
128
-
129
- @property
130
- def spec_command(self):
131
- if self._spec_command is None:
132
- self._spec_command = self.get_spec_command()
133
- return self._spec_command
134
-
135
- @property
136
- def spec_macro(self):
137
- if self._spec_macro is None:
138
- self._spec_macro = self.get_spec_macro()
139
- return self._spec_macro
140
-
141
- @property
142
- def spec_args(self):
143
- if self._spec_args is None:
144
- self._spec_args = self.get_spec_args()
145
- return self._spec_args
146
-
147
- @property
148
- def spec_scan_npts(self):
149
- if self._spec_scan_npts is None:
150
- self._spec_scan_npts = self.get_spec_scan_npts()
151
- return self._spec_scan_npts
152
-
153
- @property
154
- def spec_scan_data(self):
155
- if self._spec_scan_data is None:
156
- self._spec_scan_data = self.get_spec_scan_data()
157
- return self._spec_scan_data
158
-
159
- @property
160
- def spec_positioner_values(self):
161
- if self._spec_positioner_values is None:
162
- self._spec_positioner_values = self.get_spec_positioner_values()
163
- return self._spec_positioner_values
164
-
165
- @property
166
- def detector_data_path(self):
167
- if self._detector_data_path is None:
168
- self._detector_data_path = self.get_detector_data_path()
169
- return self._detector_data_path
170
-
171
- def get_scan_path(self):
172
- """Return the name of the directory containining the SPEC file
173
- for this scan.
174
-
175
- :rtype: str
176
- """
177
- return os.path.dirname(self.spec_file_name)
178
-
179
- def get_scan_name(self):
180
- """Return the name of this SPEC scan (not unique to scans
181
- within a single spec file).
182
-
183
- :rtype: str
184
- """
185
- raise NotImplementedError
186
-
187
- def get_scan_title(self):
188
- """Return the title of this spec scan (unique to each scan
189
- within a spec file).
190
-
191
- :rtype: str
192
- """
193
- raise NotImplementedError
194
-
195
- def get_spec_scan(self):
196
- """Return the `pyspec.file.spec.Scan` object parsed from the
197
- spec file and scan number provided to the constructor.
198
-
199
- :rtype: pyspec.file.spec.Scan
200
- """
201
- return self.spec_file.getScanByNumber(self.scan_number)
202
-
203
- def get_spec_command(self):
204
- """Return the string command of this SPEC scan.
205
-
206
- :rtype: str
207
- """
208
- return self.spec_scan.command
209
-
210
- def get_spec_macro(self):
211
- """Return the macro used in this scan's SPEC command.
212
-
213
- :rtype: str
214
- """
215
- return self.spec_command.split()[0]
216
-
217
- def get_spec_args(self):
218
- """Return a list of the arguments provided to the macro for
219
- this SPEC scan.
220
-
221
- :rtype: list[str]
222
- """
223
- return self.spec_command.split()[1:]
224
-
225
- def get_spec_scan_npts(self):
226
- """Return the number of points collected in this SPEC scan
227
-
228
- :rtype: int
229
- """
230
- raise NotImplementedError
231
-
232
- def get_spec_scan_data(self):
233
- """Return a dictionary of all the counter data collected by
234
- this SPEC scan.
235
-
236
- :rtype: dict[str, numpy.ndarray]
237
- """
238
- return dict(zip(self.spec_scan.labels, self.spec_scan.data.T))
239
-
240
- def get_spec_positioner_values(self):
241
- """Return a dictionary of all the SPEC positioner values
242
- recorded by SPEC just before the scan began.
243
-
244
- :rtype: dict[str,str]
245
- """
246
- positioner_values = dict(self.spec_scan.motor_positions)
247
- names = list(positioner_values.keys())
248
- mnemonics = self.spec_scan.motors
249
- if mnemonics is not None:
250
- for name,mnemonic in zip(names,mnemonics):
251
- if name != mnemonic:
252
- positioner_values[mnemonic] = positioner_values[name]
253
- return positioner_values
254
-
255
- def get_detector_data_path(self):
256
- """Return the name of the directory containing detector data
257
- collected by this scan.
258
-
259
- :rtype: str
260
- """
261
- raise NotImplementedError
262
-
263
- def get_detector_data_file(self, detector_prefix, scan_step_index:int):
264
- """Return the name of the file containing detector data
265
- collected at a certain step of this scan.
266
-
267
- :param detector_prefix: the prefix used in filenames for the
268
- detector
269
- :type detector_prefix: str
270
- :param scan_step_index: the index of the point in this scan
271
- whose detector file name should be returned.
272
- :type scan_step_index: int
273
- :rtype: str
274
- """
275
- raise NotImplementedError
276
-
277
- def get_detector_data(self, detector_prefix, scan_step_index:int):
278
- """Return the detector data collected at a certain step of
279
- this scan.
280
-
281
- :param detector_prefix: the prefix used in filenames for the
282
- detector
283
- :type detector_prefix: str
284
- :param scan_step_index: the index of the point in this scan
285
- whose detector data should be returned.
286
- :type scan_step_index: int
287
- :rtype: numpy.ndarray
288
- """
289
- raise NotImplementedError
290
-
291
- def get_spec_positioner_value(self, positioner_name):
292
- """Return the value of a spec positioner recorded before this
293
- scan began.
294
-
295
- :param positioner_name: the name or mnemonic of a SPEC motor
296
- whose position should be returned.
297
- :raises KeyError: if `positioner_name` is not the name or
298
- mnemonic of a SPEC motor recorded for this scan.
299
- :raises ValueError: if the recorded string value of the
300
- positioner in the SPEC file cannot be converted to a
301
- float.
302
- :rtype: float
303
- """
304
- try:
305
- positioner_value = self.spec_positioner_values[positioner_name]
306
- positioner_value = float(positioner_value)
307
- except KeyError:
308
- raise KeyError(f'{self.scan_title}: motor {positioner_name} '
309
- 'not found for this scan')
310
- except ValueError:
311
- raise ValueError(f'{self.scan_title}: could not convert value of'
312
- f' {positioner_name} to float: '
313
- f'{positioner_value}')
314
- return positioner_value
315
-
316
-
317
- class FMBScanParser(ScanParser):
318
- """Partial implementation of a class representing a SPEC scan
319
- collected at FMB.
320
- """
321
-
322
- def get_scan_name(self):
323
- return os.path.basename(self.spec_file.abspath)
324
-
325
- def get_scan_title(self):
326
- return f'{self.scan_name}_{self.scan_number:03d}'
327
-
328
-
329
- class SMBScanParser(ScanParser):
330
- """Partial implementation of a class representing a SPEC scan
331
- collected at SMB or FAST.
332
- """
333
-
334
- def __init__(self, spec_file_name, scan_number):
335
- super().__init__(spec_file_name, scan_number)
336
-
337
- self._pars = None
338
- self._par_file_pattern = f'*-*-{self.scan_name}'
339
- self._par_file = None
340
-
341
- def get_scan_name(self):
342
- return os.path.basename(self.scan_path)
343
-
344
- def get_scan_title(self):
345
- return f'{self.scan_name}_{self.scan_number}'
346
-
347
- @property
348
- def pars(self):
349
- if self._pars is None:
350
- self._pars = self.get_pars()
351
- return self._pars
352
-
353
- def get_pars(self):
354
- """Return a dictionary of values recorded in the .par file
355
- associated with this SPEC scan.
356
-
357
- :rtype: dict[str,object]
358
- """
359
- # JSON file holds titles for columns in the par file
360
- json_files = fnmatch_filter(
361
- os.listdir(self.scan_path),
362
- f'{self._par_file_pattern}.json')
363
- if not json_files:
364
- json_files = fnmatch_filter(
365
- os.listdir(self.scan_path),
366
- f'*.json')
367
- if len(json_files) != 1:
368
- raise RuntimeError(f'{self.scan_title}: cannot find the '
369
- '.json file to decode the .par file')
370
- with open(os.path.join(self.scan_path, json_files[0])) as json_file:
371
- par_file_cols = load(json_file)
372
- try:
373
- par_col_names = list(par_file_cols.values())
374
- scann_val_idx = par_col_names.index('SCAN_N')
375
- scann_col_idx = int(list(par_file_cols.keys())[scann_val_idx])
376
- except:
377
- raise RuntimeError(f'{self.scan_title}: cannot find scan pars '
378
- 'without a "SCAN_N" column in the par file')
379
-
380
- if getattr(self, '_par_file'):
381
- par_file = self._par_file
382
- else:
383
- par_files = fnmatch_filter(
384
- os.listdir(self.scan_path),
385
- f'{self._par_file_pattern}.par')
386
- if len(par_files) != 1:
387
- raise RuntimeError(f'{self.scan_title}: cannot find the .par '
388
- 'file for this scan directory')
389
- par_file = os.path.join(self.scan_path, par_files[0])
390
- self._par_file = par_file
391
- par_dict = None
392
- with open(par_file) as f:
393
- par_reader = reader(f, delimiter=' ')
394
- for row in par_reader:
395
- if len(row) == len(par_col_names):
396
- row_scann = int(row[scann_col_idx])
397
- if row_scann == self.scan_number:
398
- par_dict = {}
399
- for par_col_idx,par_col_name in par_file_cols.items():
400
- # Convert the string par value from the
401
- # file to an int or float, if possible.
402
- par_value = row[int(par_col_idx)]
403
- try:
404
- par_value = int(par_value)
405
- except ValueError:
406
- try:
407
- par_value = float(par_value)
408
- except:
409
- pass
410
- par_dict[par_col_name] = par_value
411
- if par_dict is None:
412
- raise RuntimeError(f'{self.scan_title}: could not find scan pars '
413
- f'for scan number {self.scan_number}')
414
- return par_dict
415
-
416
- def get_counter_gain(self, counter_name):
417
- """Return the gain of a counter as recorded in the user lines
418
- of a scan in a SPEC file converted to nA/V.
419
-
420
- :param counter_name: the name of the counter
421
- :type counter_name: str
422
- :rtype: str
423
- """
424
- counter_gain = None
425
- for comment in self.spec_scan.comments + self.spec_scan.user_lines:
426
- match = re.search(
427
- f'{counter_name} gain: ' # start of counter gain comments
428
- '(?P<gain_value>\d+) ' # gain numerical value
429
- '(?P<unit_prefix>[m|u|n])A/V', # gain units
430
- comment)
431
- if match:
432
- unit_prefix = match['unit_prefix']
433
- gain_scalar = 1 if unit_prefix == 'n' \
434
- else 1e3 if unit_prefix == 'u' else 1e6
435
- counter_gain = f'{float(match["gain_value"])*gain_scalar} nA/V'
436
- break
437
-
438
- if counter_gain is None:
439
- raise RuntimeError(f'{self.scan_title}: could not get gain for '
440
- f'counter {counter_name}')
441
- return counter_gain
442
-
443
-
444
- class LinearScanParser(ScanParser):
445
- """Partial implementation of a class representing a typical line
446
- or mesh scan in SPEC.
447
- """
448
- def __init__(self, spec_file_name, scan_number):
449
- super().__init__(spec_file_name, scan_number)
450
-
451
- self._spec_scan_motor_mnes = None
452
- self._spec_scan_motor_vals = None
453
- self._spec_scan_motor_vals_relative = None
454
- self._spec_scan_shape = None
455
- self._spec_scan_dwell = None
456
-
457
- @property
458
- def spec_scan_motor_mnes(self):
459
- if self._spec_scan_motor_mnes is None:
460
- self._spec_scan_motor_mnes = self.get_spec_scan_motor_mnes()
461
- return self._spec_scan_motor_mnes
462
-
463
- @property
464
- def spec_scan_motor_vals(self):
465
- if self._spec_scan_motor_vals is None:
466
- self._spec_scan_motor_vals = self.get_spec_scan_motor_vals(
467
- relative=False)
468
- return self._spec_scan_motor_vals
469
-
470
- @property
471
- def spec_scan_motor_vals_relative(self):
472
- if self._spec_scan_motor_vals_relative is None:
473
- self._spec_scan_motor_vals_relative = \
474
- self.get_spec_scan_motor_vals(relative=True)
475
- return self._spec_scan_motor_vals_relative
476
-
477
- @property
478
- def spec_scan_shape(self):
479
- if self._spec_scan_shape is None:
480
- self._spec_scan_shape = self.get_spec_scan_shape()
481
- return self._spec_scan_shape
482
-
483
- @property
484
- def spec_scan_dwell(self):
485
- if self._spec_scan_dwell is None:
486
- self._spec_scan_dwell = self.get_spec_scan_dwell()
487
- return self._spec_scan_dwell
488
-
489
- def get_spec_scan_motor_mnes(self):
490
- """Return the mnemonics of the SPEC motor(s) provided to the
491
- macro for this scan. If there is more than one motor scanned
492
- (in a "flymesh" scan, for example), the order of motors in the
493
- returned tuple will go from the fastest moving motor first to
494
- the slowest moving motor last.
495
-
496
- :rtype: tuple
497
- """
498
- raise NotImplementedError
499
-
500
- def get_spec_scan_motor_vals(self, relative=False):
501
- """Return the values visited by each of the scanned motors. If
502
- there is more than one motor scanned (in a "flymesh" scan, for
503
- example), the order of motor values in the returned tuple will
504
- go from the fastest moving motor's values first to the slowest
505
- moving motor's values last.
506
-
507
- :param relative: If `True`, return scanned motor positions
508
- *relative* to the scanned motors' positions before the scan
509
- started, defaults to False.
510
- :type relative: bool, optional
511
- :rtype: tuple
512
- """
513
- raise NotImplementedError
514
-
515
- def get_spec_scan_shape(self):
516
- """Return the number of points visited by each of the scanned
517
- motors. If there is more than one motor scanned (in a
518
- "flymesh" scan, for example), the order of number of motor
519
- values in the returned tuple will go from the number of points
520
- visited by the fastest moving motor first to the the number of
521
- points visited by the slowest moving motor last.
522
-
523
- :rtype: tuple
524
- """
525
- raise NotImplementedError
526
-
527
- def get_spec_scan_dwell(self):
528
- """Return the dwell time for each point in the scan as it
529
- appears in the command string.
530
-
531
- :rtype: float
532
- """
533
- raise NotImplementedError
534
-
535
- def get_spec_scan_npts(self):
536
- """Return the number of points collected in this SPEC scan.
537
-
538
- :rtype: int
539
- """
540
- return np.prod(self.spec_scan_shape)
541
-
542
- def get_scan_step(self, scan_step_index:int):
543
- """Return the index of each motor coordinate corresponding to
544
- the index of a single point in the scan. If there is more than
545
- one motor scanned (in a "flymesh" scan, for example), the
546
- order of indices in the returned tuple will go from the index
547
- of the value of the fastest moving motor first to the index of
548
- the value of the slowest moving motor last.
549
-
550
- :param scan_step_index: the index of a single point in the
551
- scan.
552
- :type scan_step_index: int
553
- :rtype: tuple
554
- """
555
- scan_steps = np.ndindex(self.spec_scan_shape[::-1])
556
- i = 0
557
- while i <= scan_step_index:
558
- scan_step = next(scan_steps)
559
- i += 1
560
- return scan_step
561
-
562
- def get_scan_step_index(self, scan_step:tuple):
563
- """Return the index of a single scan point corresponding to a
564
- tuple of indices for each scanned motor coordinate.
565
-
566
- :param scan_step: a tuple of the indices of each scanned motor
567
- coordinate. If there is more than one motor scanned (in a
568
- "flymesh" scan, for example), the order of indices should
569
- go from the index of the value of the fastest moving motor
570
- first to the index of the value of the slowest moving
571
- motor last.
572
- :type scan_step: tuple
573
- :trype: int
574
- """
575
- scan_steps = np.ndindex(self.spec_scan_shape[::-1])
576
- scan_step_found = False
577
- scan_step_index = -1
578
- while not scan_step_found:
579
- next_scan_step = next(scan_steps)
580
- scan_step_index += 1
581
- if next_scan_step == scan_step:
582
- scan_step_found = True
583
- break
584
- return scan_step_index
585
-
586
-
587
- class FMBLinearScanParser(LinearScanParser, FMBScanParser):
588
- """Partial implementation of a class representing a typical line
589
- or mesh scan in SPEC collected at FMB.
590
- """
591
-
592
- def get_spec_scan_motor_mnes(self):
593
- if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
594
- m1_mne = self.spec_args[0]
595
- try:
596
- # Try post-summer-2022 format
597
- dwell = float(self.spec_args[4])
598
- except:
599
- # Accommodate pre-summer-2022 format
600
- m2_mne_i = 4
601
- else:
602
- m2_mne_i = 5
603
- m2_mne = self.spec_args[m2_mne_i]
604
- return (m1_mne, m2_mne)
605
- if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
606
- return (self.spec_args[0],)
607
- if self.spec_macro in ('tseries', 'loopscan'):
608
- return ('Time',)
609
- raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
610
- f'for scans of type {self.spec_macro}')
611
-
612
- def get_spec_scan_motor_vals(self, relative=False):
613
- if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
614
- m1_start = float(self.spec_args[1])
615
- m1_end = float(self.spec_args[2])
616
- m1_npt = int(self.spec_args[3]) + 1
617
- try:
618
- # Try post-summer-2022 format
619
- dwell = float(self.spec_args[4])
620
- except:
621
- # Accommodate pre-summer-2022 format
622
- m2_start_i = 5
623
- m2_end_i = 6
624
- m2_nint_i = 7
625
- else:
626
- m2_start_i = 6
627
- m2_end_i = 7
628
- m2_nint_i = 8
629
- m2_start = float(self.spec_args[m2_start_i])
630
- m2_end = float(self.spec_args[m2_end_i])
631
- m2_npt = int(self.spec_args[m2_nint_i]) + 1
632
- fast_mot_vals = np.linspace(m1_start, m1_end, m1_npt)
633
- slow_mot_vals = np.linspace(m2_start, m2_end, m2_npt)
634
- if relative:
635
- fast_mot_vals -= self.get_spec_positioner_value(
636
- self.spec_scan_motor_mnes[0])
637
- slow_mot_vals -= self.get_spec_positioner_value(
638
- self.spec_scan_motor_mnes[1])
639
- return (fast_mot_vals, slow_mot_vals)
640
- if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
641
- mot_vals = np.linspace(float(self.spec_args[1]),
642
- float(self.spec_args[2]),
643
- int(self.spec_args[3])+1)
644
- if relative:
645
- mot_vals -= self.get_spec_positioner_value(
646
- self.spec_scan_motor_mnes[0])
647
- return (mot_vals,)
648
- if self.spec_macro in ('tseries', 'loopscan'):
649
- return (self.spec_scan.data[:,0],)
650
- raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
651
- f'for scans of type {self.spec_macro}')
652
-
653
- def get_spec_scan_shape(self):
654
- if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
655
- fast_mot_npts = int(self.spec_args[3]) + 1
656
- try:
657
- # Try post-summer-2022 format
658
- dwell = float(self.spec_args[4])
659
- except:
660
- # Accommodate pre-summer-2022 format
661
- m2_nint_i = 7
662
- else:
663
- m2_nint_i = 8
664
- slow_mot_npts = int(self.spec_args[m2_nint_i]) + 1
665
- return (fast_mot_npts, slow_mot_npts)
666
- if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
667
- mot_npts = int(self.spec_args[3])+1
668
- return (mot_npts,)
669
- if self.spec_macro in ('tseries', 'loopscan'):
670
- return (len(np.array(self.spec_scan.data[:,0])),)
671
- raise RuntimeError(f'{self.scan_title}: cannot determine scan shape '
672
- f'for scans of type {self.spec_macro}')
673
-
674
- def get_spec_scan_dwell(self):
675
- if self.macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
676
- try:
677
- # Try post-summer-2022 format
678
- dwell = float(self.spec_args[4])
679
- except:
680
- # Accommodate pre-summer-2022 format
681
- dwell = float(self.spec_args[8])
682
- return dwell
683
- if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
684
- return float(self.spec_args[4])
685
- if self.spec_macro in ('tseries', 'loopscan'):
686
- return float(self.spec_args[1])
687
- raise RuntimeError(f'{self.scan_title}: cannot determine dwell for '
688
- f'scans of type {self.spec_macro}')
689
-
690
- def get_detector_data_path(self):
691
- return os.path.join(self.scan_path, self.scan_title)
692
-
693
-
694
-
695
- @cache
696
- def list_fmb_saxswaxs_detector_files(detector_data_path, detector_prefix):
697
- """Return a sorted list of all data files for the given detector
698
- in the given directory. This function is cached to improve
699
- performace for carrying our full FAMB SAXS/WAXS data-processing
700
- workflows.
701
-
702
- :param detector_data_path: directory in which to look for detector
703
- data files
704
- :type detector_data_path: str
705
- :param detector_prefix: detector name to list files for
706
- :type detector_prefix: str
707
- :return: list of detector filenames
708
- :rtype: list[str]
709
- """
710
- return sorted(
711
- [f for f in os.listdir(detector_data_path)
712
- if detector_prefix in f
713
- and not f.endswith('.log')])
714
-
715
- class FMBSAXSWAXSScanParser(FMBLinearScanParser):
716
- """Concrete implementation of a class representing a scan taken
717
- with the typical SAXS/WAXS setup at FMB.
718
- """
719
-
720
- def get_scan_title(self):
721
- return f'{self.scan_name}_{self.scan_number:03d}'
722
-
723
- def get_detector_data_file(self, detector_prefix, scan_step_index:int):
724
- detector_files = list_fmb_saxswaxs_detector_files(
725
- self.detector_data_path, detector_prefix)
726
- if len(detector_files) == self.spec_scan_npts:
727
- return os.path.join(
728
- self.detector_data_path, detector_files[scan_step_index])
729
- else:
730
- scan_step = self.get_scan_step(scan_step_index)
731
- for f in detector_files:
732
- filename, _ = os.path.splitext(f)
733
- file_indices = tuple(
734
- [int(i) for i in \
735
- filename.split('_')[-len(self.spec_scan_shape):]])
736
- if file_indices == scan_step:
737
- return os.path.join(self.detector_data_path, f)
738
- raise RuntimeError(
739
- 'Could not find a matching detector data file for detector '
740
- + f'{detector_prefix} at scan step index {scan_step_index}')
741
-
742
- def get_detector_data(self, detector_prefix, scan_step_index:int):
743
- import fabio
744
- image_file = self.get_detector_data_file(detector_prefix,
745
- scan_step_index)
746
- with fabio.open(image_file) as det_file:
747
- image_data = det_file.data
748
- return image_data
749
-
750
-
751
- class FMBXRFScanParser(FMBLinearScanParser):
752
- """Concrete implementation of a class representing a scan taken
753
- with the typical XRF setup at FMB.
754
- """
755
-
756
- def get_scan_title(self):
757
- return f'{self.scan_name}_scan{self.scan_number}'
758
-
759
- def get_detector_data_file(self, detector_prefix, scan_step_index:int):
760
- scan_step = self.get_scan_step(scan_step_index)
761
- file_name = f'scan{self.scan_number}_{scan_step[0]:03d}.hdf5'
762
- file_name_full = os.path.join(self.detector_data_path, file_name)
763
- if os.path.isfile(file_name_full):
764
- return file_name_full
765
- raise RuntimeError(f'{self.scan_title}: could not find detector image '
766
- f'file for detector {detector_prefix} scan step '
767
- f'({scan_step_index})')
768
-
769
- def get_detector_data(self, detector_prefix, scan_step_index:int):
770
- # Third party modules
771
- from h5py import File
772
-
773
- detector_file = self.get_detector_data_file(
774
- detector_prefix, scan_step_index)
775
- scan_step = self.get_scan_step(scan_step_index)
776
- with File(detector_file) as h5_file:
777
- detector_data = \
778
- h5_file['/entry/instrument/detector/data'][scan_step[0]]
779
- return detector_data
780
-
781
-
782
- class SMBLinearScanParser(LinearScanParser, SMBScanParser):
783
- """Concrete implementation of a class representing a scan taken
784
- with the typical powder diffraction setup at SMB.
785
- """
786
-
787
- def get_spec_scan_dwell(self):
788
- if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
789
- try:
790
- # Try post-summer-2022 format
791
- dwell = float(self.spec_args[4])
792
- except:
793
- # Accommodate pre-summer-2022 format
794
- dwell = float(self.spec_args[8])
795
- return dwell
796
- if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
797
- return float(self.spec_args[4])
798
- if self.spec_macro == 'tseries':
799
- return float(self.spec_args[1])
800
- if self.spec_macro == 'wbslew_scan':
801
- return float(self.spec_args[3])
802
- raise RuntimeError(f'{self.scan_title}: cannot determine dwell time '
803
- f'for scans of type {self.spec_macro}')
804
-
805
- def get_spec_scan_motor_mnes(self):
806
- if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
807
- m1_mne = self.spec_args[0]
808
- try:
809
- # Try post-summer-2022 format
810
- dwell = float(self.spec_args[4])
811
- except:
812
- # Accommodate pre-summer-2022 format
813
- m2_mne_i = 4
814
- else:
815
- m2_mne_i = 5
816
- m2_mne = self.spec_args[m2_mne_i]
817
- return (m1_mne, m2_mne)
818
- if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
819
- return (self.spec_args[0],)
820
- if self.spec_macro in ('tseries', 'loopscan'):
821
- return ('Time',)
822
- raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
823
- f'for scans of type {self.spec_macro}')
824
-
825
- def get_spec_scan_motor_vals(self, relative=False):
826
- if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
827
- m1_start = float(self.spec_args[1])
828
- m1_end = float(self.spec_args[2])
829
- m1_npt = int(self.spec_args[3]) + 1
830
- try:
831
- # Try post-summer-2022 format
832
- dwell = float(self.spec_args[4])
833
- except:
834
- # Accommodate pre-summer-2022 format
835
- m2_start_i = 5
836
- m2_end_i = 6
837
- m2_nint_i = 7
838
- else:
839
- m2_start_i = 6
840
- m2_end_i = 7
841
- m2_nint_i = 8
842
- m2_start = float(self.spec_args[m2_start_i])
843
- m2_end = float(self.spec_args[m2_end_i])
844
- m2_npt = int(self.spec_args[m2_nint_i]) + 1
845
- fast_mot_vals = np.linspace(m1_start, m1_end, m1_npt)
846
- slow_mot_vals = np.linspace(m2_start, m2_end, m2_npt)
847
- if relative:
848
- fast_mot_vals -= self.spec_positioner_values[
849
- self.spec_scan_motor_mnes[0]]
850
- slow_mot_vals -= self.spec_positioner_values[
851
- self.spec_scan_motor_mnes[1]]
852
- return (fast_mot_vals, slow_mot_vals)
853
- if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
854
- mot_vals = np.linspace(float(self.spec_args[1]),
855
- float(self.spec_args[2]),
856
- int(self.spec_args[3])+1)
857
- if relative:
858
- mot_vals -= self.get_spec_positioner_value(
859
- self.spec_scan_motor_mnes[0])
860
- return (mot_vals,)
861
- if self.spec_macro in ('tseries', 'loopscan'):
862
- return (self.spec_scan.data[:,0],)
863
- raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
864
- f'for scans of type {self.spec_macro}')
865
-
866
- def get_spec_scan_shape(self):
867
- if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
868
- fast_mot_npts = int(self.spec_args[3]) + 1
869
- try:
870
- # Try post-summer-2022 format
871
- dwell = float(self.spec_args[4])
872
- except:
873
- # Accommodate pre-summer-2022 format
874
- m2_nint_i = 7
875
- else:
876
- m2_nint_i = 8
877
- slow_mot_npts = int(self.spec_args[m2_nint_i]) + 1
878
- return (fast_mot_npts, slow_mot_npts)
879
- if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
880
- mot_npts = int(self.spec_args[3])+1
881
- return (mot_npts,)
882
- if self.spec_macro in ('tseries', 'loopscan'):
883
- return (len(np.array(self.spec_scan.data[:,0])),)
884
- raise RuntimeError(f'{self.scan_title}: cannot determine scan shape '
885
- f'for scans of type {self.spec_macro}')
886
-
887
- def get_detector_data_path(self):
888
- return os.path.join(self.scan_path, str(self.scan_number))
889
-
890
- def get_detector_data_file(self, detector_prefix, scan_step_index:int):
891
- scan_step = self.get_scan_step(scan_step_index)
892
- if len(scan_step) == 1:
893
- scan_step = (0, *scan_step)
894
- file_name_pattern = (f'{detector_prefix}_'
895
- f'{self.scan_name}_*_'
896
- f'{scan_step[0]}_data_'
897
- f'{(scan_step[1]+1):06d}.h5')
898
- file_name_matches = fnmatch_filter(
899
- os.listdir(self.detector_data_path),
900
- file_name_pattern)
901
- if len(file_name_matches) == 1:
902
- return os.path.join(self.detector_data_path, file_name_matches[0])
903
- raise RuntimeError(f'{self.scan_title}: could not find detector image '
904
- f'file for detector {detector_prefix} scan step '
905
- f'({scan_step_index})')
906
-
907
- def get_detector_data(self, detector_prefix, scan_step_index:int):
908
- # Third party modules
909
- from h5py import File
910
-
911
- image_file = self.get_detector_data_file(
912
- detector_prefix, scan_step_index)
913
- with File(image_file) as h5_file:
914
- image_data = h5_file['/entry/data/data'][0]
915
- return image_data
916
-
917
-
918
- class RotationScanParser(ScanParser):
919
- """Partial implementation of a class representing a rotation
920
- scan.
921
- """
922
-
923
- def __init__(self, spec_file_name, scan_number):
924
- super().__init__(spec_file_name, scan_number)
925
- self._starting_image_index = None
926
- self._starting_image_offset = None
927
-
928
- @property
929
- def starting_image_index(self):
930
- if self._starting_image_index is None:
931
- self._starting_image_index = self.get_starting_image_index()
932
- return self._starting_image_index
933
-
934
- @property
935
- def starting_image_offset(self):
936
- if self._starting_image_offset is None:
937
- self._starting_image_offset = self.get_starting_image_offset()
938
- return self._starting_image_offset
939
-
940
- def get_starting_image_index(self):
941
- """Return the first frame of the detector data collected by
942
- this scan from the index of the first frame of detector data
943
- collected by this scan.
944
-
945
- :rtype: int
946
- """
947
- raise NotImplementedError
948
-
949
- def get_starting_image_offset(self):
950
- """Return the offset of the index of the first "good" frame of
951
- detector data collected by this scan from the index of the
952
- first frame of detector data collected by this scan.
953
-
954
- :rtype: int
955
- """
956
- raise NotImplementedError
957
-
958
-
959
- class FMBRotationScanParser(RotationScanParser, FMBScanParser):
960
- """Concrete implementation of a class representing a scan taken
961
- with the typical tomography setup at FMB.
962
- """
963
-
964
- def get_spec_scan_data(self):
965
- spec_scan_data = super().get_spec_scan_data()
966
- if hasattr(self, '_rams4_args'):
967
- spec_scan_data['theta'] = np.linspace(
968
- float(self._rams4_args[0]), float(self._rams4_args[1]),
969
- 1+int(self._rams4_args[2]))
970
- return spec_scan_data
971
-
972
- def get_spec_scan_npts(self):
973
- if hasattr(self, '_rams4_args'):
974
- return 1+int(self._rams4_args[2])
975
- if self.spec_macro == 'flyscan':
976
- if len(self.spec_args) == 2:
977
- return 1+int(self.spec_args[0])
978
- if len(self.spec_args) == 5:
979
- return 1+int(self.spec_args[3])
980
- raise RuntimeError(f'{self.scan_title}: cannot obtain number of '
981
- f'points from {self.spec_macro} with arguments '
982
- f'{self.spec_args}')
983
- # if self.spec_macro == 'ascan':
984
- # if len(self.spec_args) == 5:
985
- # return int(self.spec_args[3])
986
- # raise RuntimeError(f'{self.scan_title}: cannot obtain number of '
987
- # f'points from {self.spec_macro} with arguments '
988
- # f'{self.spec_args}')
989
- raise RuntimeError(f'{self.scan_title}: cannot determine rotation '
990
- f' angles for scans of type {self.spec_macro}')
991
-
992
- def get_starting_image_index(self):
993
- return 0
994
-
995
- def get_starting_image_offset(self):
996
- if hasattr(self, '_rams4_args'):
997
- return int(self.spec_args[0]) - self.spec_scan_npts
998
- if self.spec_macro == 'flyscan':
999
- return 1
1000
- raise RuntimeError(f'{self.scan_title}: cannot determine starting '
1001
- f'image offset for scans of type {self.spec_macro}')
1002
-
1003
- def get_detector_data_path(self):
1004
- return self.scan_path
1005
-
1006
- def get_detector_data_file(self, detector_prefix):
1007
- prefix = detector_prefix.upper()
1008
- file_name = f'{self.scan_name}_{prefix}_{self.scan_number:03d}.h5'
1009
- file_name_full = os.path.join(self.detector_data_path, file_name)
1010
- if os.path.isfile(file_name_full):
1011
- return file_name_full
1012
- raise RuntimeError(f'{self.scan_title}: could not find detector image '
1013
- f'file for detector {detector_prefix}')
1014
-
1015
- def get_all_detector_data_in_file(
1016
- self, detector_prefix, scan_step_index=None):
1017
- # Third party modules
1018
- from h5py import File
1019
-
1020
- detector_file = self.get_detector_data_file(detector_prefix)
1021
- with File(detector_file) as h5_file:
1022
- if scan_step_index is None:
1023
- detector_data = h5_file['/entry/instrument/detector/data'][
1024
- self.starting_image_offset:]
1025
- elif isinstance(scan_step_index, int):
1026
- detector_data = h5_file['/entry/instrument/detector/data'][
1027
- self.starting_image_offset+scan_step_index]
1028
- elif (isinstance(scan_step_index, (list, tuple))
1029
- and len(scan_step_index) == 2):
1030
- detector_data = h5_file['/entry/instrument/detector/data'][
1031
- self.starting_image_offset+scan_step_index[0]:
1032
- self.starting_image_offset+scan_step_index[1]]
1033
- else:
1034
- raise ValueError('Invalid parameter scan_step_index '
1035
- f'({scan_step_index})')
1036
- return detector_data
1037
-
1038
- def get_detector_data(self, detector_prefix, scan_step_index=None):
1039
- try:
1040
- # Detector files in h5 format
1041
- detector_data = self.get_all_detector_data_in_file(
1042
- detector_prefix, scan_step_index)
1043
- except:
1044
- # Detector files in tiff format
1045
- if scan_step_index is None:
1046
- detector_data = []
1047
- for index in range(self.spec_scan_npts):
1048
- detector_data.append(
1049
- self.get_detector_data(detector_prefix, index))
1050
- detector_data = np.asarray(detector_data)
1051
- elif isinstance(scan_step_index, int):
1052
- image_file = self._get_detector_tiff_file(
1053
- detector_prefix, scan_step_index)
1054
- if image_file is None:
1055
- detector_data = None
1056
- else:
1057
- with TiffFile(image_file) as tiff_file:
1058
- detector_data = tiff_file.asarray()
1059
- elif (isinstance(scan_step_index, (list, tuple))
1060
- and len(scan_step_index) == 2):
1061
- detector_data = []
1062
- for index in range(scan_step_index[0], scan_step_index[1]):
1063
- detector_data.append(
1064
- self.get_detector_data(detector_prefix, index))
1065
- detector_data = np.asarray(detector_data)
1066
- else:
1067
- raise ValueError('Invalid parameter scan_step_index '
1068
- f'({scan_step_index})')
1069
- return detector_data
1070
-
1071
- def _get_detector_tiff_file(self, detector_prefix, scan_step_index):
1072
- file_name_full = f'{self.spec_file_name}_{detector_prefix.upper()}_' \
1073
- f'{self.scan_number:03d}_' \
1074
- f'{self.starting_image_offset+scan_step_index:03d}.tiff'
1075
- if os.path.isfile(file_name_full):
1076
- return file_name_full
1077
- return None
1078
- # raise RuntimeError(f'{self.scan_title}: could not find detector image '
1079
- # f'file for scan step ({scan_step_index})')
1080
-
1081
-
1082
- class SMBRotationScanParser(RotationScanParser, SMBScanParser):
1083
- """Concrete implementation of a class representing a scan taken
1084
- with the typical tomography setup at SMB.
1085
- """
1086
-
1087
- def __init__(self, spec_file_name, scan_number, par_file=None):
1088
- self._scan_type = None
1089
- super().__init__(spec_file_name, scan_number)
1090
-
1091
- self._katefix = 0 # RV remove when no longer needed
1092
- self._par_file_pattern = f'id*-*tomo*-{self.scan_name}'
1093
- if par_file is not None:
1094
- self._par_file = par_file
1095
-
1096
- @property
1097
- def scan_type(self):
1098
- if self._scan_type is None:
1099
- self._scan_type = self.get_scan_type()
1100
- return self._scan_type
1101
-
1102
- def get_spec_scan_data(self):
1103
- spec_scan_data = super().get_spec_scan_data()
1104
- spec_scan_data['theta'] = np.linspace(
1105
- float(self.pars['ome_start_real']),
1106
- float(self.pars['ome_end_real']), int(self.pars['nframes_real']))
1107
- return spec_scan_data
1108
-
1109
- def get_spec_scan_npts(self):
1110
- return int(self.pars['nframes_real'])
1111
-
1112
- def get_scan_type(self):
1113
- scan_type = self.pars.get(
1114
- 'tomo_type', self.pars.get(
1115
- 'tomotype', self.pars.get('scan_type', None)))
1116
- if scan_type is None:
1117
- raise RuntimeError(
1118
- f'{self.scan_title}: cannot determine the scan_type')
1119
- return scan_type
1120
-
1121
- def get_starting_image_index(self):
1122
- try:
1123
- junkstart = int(self.pars['junkstart'])
1124
- #RV temp fix for error in par files at Kate's beamline
1125
- #Remove this and self._katefix when no longer needed
1126
- file_name = f'nf_{junkstart:06d}.tif'
1127
- file_name_full = os.path.join(self.detector_data_path, file_name)
1128
- if not os.path.isfile(file_name_full):
1129
- self._katefix = min([
1130
- int(re.findall(r'\d+', f)[0])
1131
- for f in os.listdir(self.detector_data_path)
1132
- if re.match(r'nf_\d+\.tif', f)])
1133
- return junkstart
1134
- #return int(self.pars['junkstart'])
1135
- except:
1136
- raise RuntimeError(f'{self.scan_title}: cannot determine first '
1137
- 'detector image index')
1138
-
1139
- def get_starting_image_offset(self):
1140
- try:
1141
- return (int(self.pars['goodstart'])-self.starting_image_index)
1142
- except:
1143
- raise RuntimeError(f'{self.scan_title}: cannot determine index '
1144
- 'offset of first good detector image')
1145
-
1146
- def get_detector_data_path(self):
1147
- return os.path.join(self.scan_path, str(self.scan_number), 'nf')
1148
-
1149
- def get_detector_data_file(self, scan_step_index:int):
1150
- index = self.starting_image_index + self.starting_image_offset \
1151
- + scan_step_index
1152
- file_name = f'nf_{index:06d}.tif'
1153
- file_name_full = os.path.join(self.detector_data_path, file_name)
1154
- if os.path.isfile(file_name_full):
1155
- return file_name_full
1156
- #RV temp fix for error in par files at Kate's beamline
1157
- #Remove this and self._katefix when no longer needed
1158
- index += self._katefix
1159
- file_name = f'nf_{index:06d}.tif'
1160
- file_name_full = os.path.join(self.detector_data_path, file_name)
1161
- if os.path.isfile(file_name_full):
1162
- return file_name_full
1163
- raise RuntimeError(f'{self.scan_title}: could not find detector image '
1164
- f'file ({file_name_full}) for scan step '
1165
- f'({scan_step_index})')
1166
-
1167
- def get_detector_data(self, detector_prefix, scan_step_index=None):
1168
- if scan_step_index is None:
1169
- detector_data = []
1170
- for index in range(self.spec_scan_npts):
1171
- detector_data.append(
1172
- self.get_detector_data(detector_prefix, index))
1173
- detector_data = np.asarray(detector_data)
1174
- elif isinstance(scan_step_index, int):
1175
- image_file = self.get_detector_data_file(scan_step_index)
1176
- with TiffFile(image_file) as tiff_file:
1177
- detector_data = tiff_file.asarray()
1178
- elif (isinstance(scan_step_index, (list, tuple))
1179
- and len(scan_step_index) == 2):
1180
- detector_data = []
1181
- for index in range(scan_step_index[0], scan_step_index[1]):
1182
- detector_data.append(
1183
- self.get_detector_data(detector_prefix, index))
1184
- detector_data = np.asarray(detector_data)
1185
- else:
1186
- raise ValueError('Invalid parameter scan_step_index '
1187
- f'({scan_step_index})')
1188
- return detector_data
1189
-
1190
-
1191
- class MCAScanParser(ScanParser):
1192
- """Partial implementation of a class representing a scan taken
1193
- while collecting SPEC MCA data.
1194
- """
1195
-
1196
- def __init__(self, spec_file_name, scan_number):
1197
- super().__init__(spec_file_name, scan_number)
1198
-
1199
- self._detector_num_bins = None
1200
-
1201
- def get_detector_num_bins(self, detector_prefix):
1202
- """Return the number of bins for the detector with the given
1203
- prefix.
1204
-
1205
- :param detector_prefix: the detector prefix as used in SPEC
1206
- MCA data files
1207
- :type detector_prefix: str
1208
- :rtype: int
1209
- """
1210
- raise NotImplementedError
1211
-
1212
-
1213
- class SMBMCAScanParser(MCAScanParser, SMBLinearScanParser):
1214
- """Concrete implementation of a class representing a scan taken
1215
- with the typical EDD setup at SMB or FAST.
1216
- """
1217
- detector_data_formats = ('spec', 'h5')
1218
- def __init__(self, spec_file_name, scan_number, detector_data_format=None):
1219
- """Constructor for SMBMCAScnaParser.
1220
-
1221
- :param spec_file: Path to scan's SPEC file
1222
- :type spec_file: str
1223
- :param scan_number: Number of the SPEC scan
1224
- :type scan_number: int
1225
- :param detector_data_format: Format of the MCA data collected,
1226
- defaults to None
1227
- :type detector_data_format: Optional[Literal["spec", "h5"]]
1228
- """
1229
- super().__init__(spec_file_name, scan_number)
1230
-
1231
- self.detector_data_format = None
1232
- if detector_data_format is None:
1233
- self.init_detector_data_format()
1234
- else:
1235
- if detector_data_format.lower() in self.detector_data_formats:
1236
- self.detector_data_format = detector_data_format.lower()
1237
- else:
1238
- raise ValueError(
1239
- 'Unrecognized value for detector_data_format: '
1240
- + f'{detector_data_format}. Allowed values are: '
1241
- + ', '.join(self.detector_data_formats))
1242
-
1243
- def get_spec_scan_motor_vals(self, relative=True):
1244
- if not relative:
1245
- # The scanned motor's recorded position in the spec.log
1246
- # file's "#P" lines does not always give the right offset
1247
- # to use to obtain absolute motor postions from relative
1248
- # motor positions (or relative from actual). Sometimes,
1249
- # the labx/y/z/ometotal value from the scan's .par file is
1250
- # the quantity for the offset that _should_ be used, but
1251
- # there is currently no consistent way to determine when
1252
- # to use the labx/y/z/ometotal .par file value and when to
1253
- # use the spec file "#P" lines value. Because the relative
1254
- # motor values are the only ones currently used in EDD
1255
- # workflows, obtain them from relevant values available in
1256
- # the .par file, and defer implementation for absolute
1257
- # motor postions to later.
1258
- return super().get_spec_scan_motor_vals(relative=True)
1259
- # raise NotImplementedError('Only relative motor values are available.')
1260
- if self.spec_macro in ('flymesh', 'mesh', 'flydmesh', 'dmesh'):
1261
- mot_vals_axis0 = np.linspace(self.pars['fly_axis0_start'],
1262
- self.pars['fly_axis0_end'],
1263
- self.pars['fly_axis0_npts'])
1264
- mot_vals_axis1 = np.linspace(self.pars['fly_axis1_start'],
1265
- self.pars['fly_axis1_end'],
1266
- self.pars['fly_axis1_npts'])
1267
- return (mot_vals_axis0, mot_vals_axis1)
1268
- if self.spec_macro in ('flyscan', 'ascan', 'flydscan', 'dscan'):
1269
- mot_vals = np.linspace(self.pars['fly_axis0_start'],
1270
- self.pars['fly_axis0_end'],
1271
- self.pars['fly_axis0_npts'])
1272
- return (mot_vals,)
1273
- if self.spec_macro in ('tseries', 'loopscan'):
1274
- return (self.spec_scan.data[:,0],)
1275
- raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
1276
- f'for scans of type {self.spec_macro}')
1277
-
1278
- def init_detector_data_format(self):
1279
- """Determine and set a value for the instance variable
1280
- `detector_data_format` based on the presence / absence of
1281
- detector data files of different formats conventionally
1282
- associated with this scan. Also set the corresponding
1283
- appropriate value for `_detector_data_path`.
1284
- """
1285
- try:
1286
- self._detector_data_path = self.scan_path
1287
- detector_file = self.get_detector_data_file_spec()
1288
- except OSError:
1289
- try:
1290
- self._detector_data_path = os.path.join(
1291
- self.scan_path, str(self.scan_number), 'edd')
1292
- detector_file = self.get_detector_data_file_h5()
1293
- except OSError:
1294
- raise RuntimeError(
1295
- f"{self.scan_title}: Can't determine detector data format")
1296
- else:
1297
- self.detector_data_format = 'h5'
1298
- else:
1299
- self.detector_data_format = 'spec'
1300
-
1301
- def get_detector_data_path(self):
1302
- raise NotImplementedError
1303
-
1304
- def get_detector_num_bins(self, element_index=0):
1305
- if self.detector_data_format == 'spec':
1306
- return self.get_detector_num_bins_spec()
1307
- elif self.detector_data_format == 'h5':
1308
- return self.get_detector_num_bins_h5(element_index)
1309
-
1310
- def get_detector_num_bins_spec(self):
1311
- with open(self.get_detector_data_file_spec()) as detector_file:
1312
- lines = detector_file.readlines()
1313
- for line in lines:
1314
- if line.startswith('#@CHANN'):
1315
- try:
1316
- line_prefix, number_saved, first_saved, last_saved, \
1317
- reduction_coef = line.split()
1318
- return int(number_saved)
1319
- except:
1320
- continue
1321
- raise RuntimeError(f'{self.scan_title}: could not find num_bins')
1322
-
1323
- def get_detector_num_bins_h5(self, element_index):
1324
- from h5py import File
1325
- detector_file = self.get_detector_data_file_h5()
1326
- with File(detector_file) as h5_file:
1327
- dset_shape = h5_file['/entry/data/data'].shape
1328
- return dset_shape[-1]
1329
-
1330
- def get_detector_data_file(self, scan_step_index=0):
1331
- if self.detector_data_format == 'spec':
1332
- return self.get_detector_data_file_spec()
1333
- elif self.detector_data_format == 'h5':
1334
- return self.get_detector_data_file_h5(
1335
- scan_step_index=scan_step_index)
1336
-
1337
- def get_detector_data_file_spec(self):
1338
- """Return the filename (full absolute path) to the file
1339
- containing spec-formatted MCA data for this scan.
1340
- """
1341
- file_name = f'spec.log.scan{self.scan_number}.mca1.mca'
1342
- file_name_full = os.path.join(self.detector_data_path, file_name)
1343
- if os.path.isfile(file_name_full):
1344
- return file_name_full
1345
- raise OSError(
1346
- '{self.scan_title}: could not find detector image file'
1347
- )
1348
-
1349
- def get_detector_data_file_h5(self, scan_step_index=0):
1350
- """Return the filename (full absolute path) to the file
1351
- containing h5-formatted MCA data for this scan.
1352
-
1353
- :param scan_step_index:
1354
- """
1355
- scan_step = self.get_scan_step(scan_step_index)
1356
- if len(self.spec_scan_shape) == 1:
1357
- filename_index = 0
1358
- elif len(self.spec_scan_shape) == 2:
1359
- scan_step = self.get_scan_step(scan_step_index)
1360
- filename_index = scan_step[0]
1361
- else:
1362
- raise NotImplementedError(
1363
- 'Cannot find detector file for scans with dimension > 2')
1364
- file_name = list_smb_mca_detector_files_h5(
1365
- self.detector_data_path)[filename_index]
1366
- file_name_full = os.path.join(self.detector_data_path, file_name)
1367
- if os.path.isfile(file_name_full):
1368
- return file_name_full
1369
- raise OSError(
1370
- '{self.scan_title}: could not find detector image file'
1371
- )
1372
-
1373
-
1374
- def get_all_detector_data(self, detector):
1375
- """Return a 2D array of all MCA spectra collected in this scan
1376
- by the detector element indicated with `detector`.
1377
-
1378
- :param detector: For detector data collected in SPEC format,
1379
- this is the detector prefix as it appears in the spec MCA
1380
- data file. For detector data collected in H5 format, this
1381
- is the index of a particular detector element.
1382
- :type detector: Union[str, int]
1383
- :rtype: numpy.ndarray
1384
- """
1385
- if self.detector_data_format == 'spec':
1386
- return self.get_all_detector_data_spec(detector)
1387
- elif self.detector_data_format == 'h5':
1388
- try:
1389
- element_index = int(detector)
1390
- except:
1391
- raise TypeError(f'{detector} is not an integer element index')
1392
- return self.get_all_detector_data_h5(element_index)
1393
-
1394
- def get_all_detector_data_spec(self, detector_prefix):
1395
- """Return a 2D array of all MCA spectra collected by a
1396
- detector in the spec MCA file format during the scan.
1397
-
1398
- :param detector_prefix: Detector name at is appears in the
1399
- spec MCA file.
1400
- :type detector_prefix: str
1401
- :returns: 2D array of MCA spectra
1402
- :rtype: numpy.ndarray
1403
- """
1404
- # This should be easy with pyspec, but there are bugs in
1405
- # pyspec for MCA data..... or is the 'bug' from a nonstandard
1406
- # implementation of some macro on our end? According to spec
1407
- # manual and pyspec code, mca data should always begin w/ '@A'
1408
- # In example scans, it begins with '@{detector_prefix}'
1409
- # instead
1410
- data = []
1411
-
1412
- with open(self.get_detector_data_file_spec()) as detector_file:
1413
- lines = [line.strip("\\\n") for line in detector_file.readlines()]
1414
-
1415
- num_bins = self.get_detector_num_bins()
1416
-
1417
- counter = 0
1418
- for line in lines:
1419
- a = line.split()
1420
-
1421
- if len(a) > 0:
1422
- if a[0] == ("@"+detector_prefix):
1423
- counter = 1
1424
- spectrum = np.zeros(num_bins)
1425
- if counter == 1:
1426
- b = np.array(a[1:]).astype('uint16')
1427
- spectrum[(counter-1) * 25:((counter-1) * 25 + 25)] = b
1428
- counter = counter + 1
1429
- elif counter > 1 and counter <= (np.floor(num_bins / 25.)):
1430
- b = np.array(a).astype('uint16')
1431
- spectrum[(counter-1) * 25:((counter-1) * 25 + 25)] = b
1432
- counter = counter + 1
1433
- elif counter == (np.ceil(num_bins/25.)):
1434
- b = np.array(a).astype('uint16')
1435
- spectrum[(counter-1) * 25:
1436
- ((counter-1) * 25 + (np.mod(num_bins, 25)))] = b
1437
- data.append(spectrum)
1438
- counter = 0
1439
-
1440
- return np.array(data)
1441
-
1442
- def get_all_detector_data_h5(self, element_index):
1443
- """Return a 2D array of all MCA spectra collected by a
1444
- detector in the h5 file format during the scan.
1445
-
1446
- :param element_index: The index of a particualr MCA element to
1447
- return data for.
1448
- :type element_index: int
1449
- :returns: 2D array of MCA spectra
1450
- :rtype: numpy.ndarray
1451
- """
1452
- detector_data = np.empty(
1453
- (self.spec_scan_npts,
1454
- self.get_detector_num_bins_h5(element_index)))
1455
- detector_files = list_smb_mca_detector_files_h5(
1456
- self.detector_data_path)
1457
- for i, detector_file in enumerate(detector_files):
1458
- full_filename = os.path.join(
1459
- self.detector_data_path, detector_file)
1460
- element_data = get_all_mca_data_h5(
1461
- full_filename)[:,element_index,:]
1462
- i_0 = i * self.spec_scan_shape[0]
1463
- if len(self.spec_scan_shape) == 2:
1464
- i_f = i_0 + self.spec_scan_shape[0]
1465
- else:
1466
- i_f = self.spec_scan_npts
1467
- detector_data[i_0:i_f] = element_data
1468
- return detector_data
1469
-
1470
- def get_detector_data(self, detector, scan_step_index:int):
1471
- """Return a single MCA spectrum for the detector indicated.
1472
-
1473
- :param detector: If this scan collected MCA data in "spec"
1474
- format, this is the detector prefix as it appears in the
1475
- spec MCA data file. If this scan collected data in .h5
1476
- format, this is the index of the detector element of
1477
- interest.:type detector: typing.Union[str, int]
1478
- :param scan_step_index: Index of the scan step to return the
1479
- spectrum from.
1480
- :type scan_step_index: int
1481
- :returns: A single MCA spectrum
1482
- :rtype: numpy.ndarray
1483
- """
1484
- detector_data = self.get_all_detector_data(detector)
1485
- return detector_data[scan_step_index]
1486
-
1487
- @cache
1488
- def list_smb_mca_detector_files_h5(detector_data_path):
1489
- """Return a sorted list of all *.hdf5 files in a directory
1490
-
1491
- :param detector_data_path: Directory to return *.hdf5 files from
1492
- :type detector_data_path: str
1493
- :returns: Sorted list of detector data filenames
1494
- :rtype: list[str]
1495
- """
1496
- return sorted(
1497
- [f for f in os.listdir(detector_data_path) if f.endswith('.hdf5')])
1498
-
1499
- @cache
1500
- def get_all_mca_data_h5(filename):
1501
- """Return all data from all elements from an MCA data file
1502
-
1503
- :param filename: Name of the MCA h5 data file
1504
- :type filename: str
1505
- :returns: 3D array of MCA spectra where the first axis is scan
1506
- step, second index is detector element, third index is channel
1507
- energy.
1508
- :rtype: numpy.ndarray
1509
- """
1510
- import os
1511
-
1512
- from h5py import File
1513
- import numpy as np
1514
-
1515
- with File(filename) as h5_file:
1516
- data = h5_file['/entry/data/data'][:]
1517
-
1518
- # Prior to 2023-12-12, there was an issue where the XPS23 detector
1519
- # was capturing one or two frames of all 0s at the start of the
1520
- # dataset in every hdf5 file. In both cases, there is only ONE
1521
- # extra frame of data relative to the number of frames that should
1522
- # be there (based on the number of points in the spec scan). If
1523
- # one frame of all 0s is present: skip it and deliver only the
1524
- # real data. If two frames of all 0s are present: detector data
1525
- # will be missing for the LAST step in the scan. Skip the first
1526
- # two frames of all 0s in the hdf5 dataset, then add a frame of
1527
- # fake data (all 0-s) to the end of that real data so that the
1528
- # number of detector data frames matches the number of points in
1529
- # the spec scan.
1530
- check_zeros_before = 1702357200
1531
- file_mtime = os.path.getmtime(filename)
1532
- if file_mtime <= check_zeros_before:
1533
- if not np.any(data[0]):
1534
- # If present, remove first frame of blank data
1535
- print('Warning: removing blank first frame of detector data')
1536
- data = data[1:]
1537
- if not np.any(data[0]):
1538
- # If present, shift second frame of blank data to the
1539
- # end
1540
- print('Warning: shifting second frame of blank detector data '
1541
- + 'to the end of the scan')
1542
- data = np.concatenate((data[1:], np.asarray([data[0]])))
1543
-
1544
- return data