ChessAnalysisPipeline 0.0.14__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.

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