ChessAnalysisPipeline 0.0.5__py3-none-any.whl → 0.0.6__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 (41) hide show
  1. CHAP/TaskManager.py +214 -0
  2. CHAP/common/models/integration.py +392 -249
  3. CHAP/common/models/map.py +350 -198
  4. CHAP/common/processor.py +227 -189
  5. CHAP/common/reader.py +52 -39
  6. CHAP/common/utils/fit.py +1197 -991
  7. CHAP/common/utils/general.py +629 -372
  8. CHAP/common/utils/material.py +158 -121
  9. CHAP/common/utils/scanparsers.py +735 -339
  10. CHAP/common/writer.py +31 -25
  11. CHAP/edd/models.py +63 -49
  12. CHAP/edd/processor.py +130 -109
  13. CHAP/edd/reader.py +1 -1
  14. CHAP/edd/writer.py +1 -1
  15. CHAP/inference/processor.py +35 -28
  16. CHAP/inference/reader.py +1 -1
  17. CHAP/inference/writer.py +1 -1
  18. CHAP/pipeline.py +14 -28
  19. CHAP/processor.py +44 -75
  20. CHAP/reader.py +49 -40
  21. CHAP/runner.py +73 -32
  22. CHAP/saxswaxs/processor.py +1 -1
  23. CHAP/saxswaxs/reader.py +1 -1
  24. CHAP/saxswaxs/writer.py +1 -1
  25. CHAP/server.py +130 -0
  26. CHAP/sin2psi/processor.py +1 -1
  27. CHAP/sin2psi/reader.py +1 -1
  28. CHAP/sin2psi/writer.py +1 -1
  29. CHAP/tomo/__init__.py +1 -4
  30. CHAP/tomo/models.py +53 -31
  31. CHAP/tomo/processor.py +1326 -900
  32. CHAP/tomo/reader.py +4 -2
  33. CHAP/tomo/writer.py +4 -2
  34. CHAP/writer.py +47 -41
  35. {ChessAnalysisPipeline-0.0.5.dist-info → ChessAnalysisPipeline-0.0.6.dist-info}/METADATA +1 -1
  36. ChessAnalysisPipeline-0.0.6.dist-info/RECORD +52 -0
  37. ChessAnalysisPipeline-0.0.5.dist-info/RECORD +0 -50
  38. {ChessAnalysisPipeline-0.0.5.dist-info → ChessAnalysisPipeline-0.0.6.dist-info}/LICENSE +0 -0
  39. {ChessAnalysisPipeline-0.0.5.dist-info → ChessAnalysisPipeline-0.0.6.dist-info}/WHEEL +0 -0
  40. {ChessAnalysisPipeline-0.0.5.dist-info → ChessAnalysisPipeline-0.0.6.dist-info}/entry_points.txt +0 -0
  41. {ChessAnalysisPipeline-0.0.5.dist-info → ChessAnalysisPipeline-0.0.6.dist-info}/top_level.txt +0 -0
@@ -3,29 +3,40 @@
3
3
  # -*- coding: utf-8 -*-
4
4
 
5
5
  # system modules
6
- import csv
7
- import fnmatch
8
- from functools import cache
9
- import json
6
+ from csv import reader
7
+ from fnmatch import filter as fnmatch_filter
8
+ from json import load
10
9
  import os
11
10
  import re
12
11
 
13
- # necessary for the base class, ScanParser:
12
+ # third party modules
14
13
  import numpy as np
15
- from pyspec.file.spec import FileSpec
14
+ from pyspec.file.spec import FileSpec
15
+
16
+
17
+ class ScanParser:
18
+ """Partial implementation of a class representing a SPEC scan and
19
+ some of its metadata.
20
+
21
+ :param spec_file_name: path to a SPEC file on the CLASSE DAQ
22
+ :type spec_file_name: str
23
+ :param scan_number: the number of a scan in the SPEC file provided
24
+ with `spec_file_name`
25
+ :type scan_number: int
26
+ """
16
27
 
17
- class ScanParser(object):
18
28
  def __init__(self,
19
29
  spec_file_name:str,
20
30
  scan_number:int):
31
+ """Constructor method"""
21
32
 
22
33
  self.spec_file_name = spec_file_name
23
34
  self.scan_number = scan_number
24
-
35
+
25
36
  self._scan_path = None
26
37
  self._scan_name = None
27
38
  self._scan_title = None
28
-
39
+
29
40
  self._spec_scan = None
30
41
  self._spec_command = None
31
42
  self._spec_macro = None
@@ -33,93 +44,162 @@ class ScanParser(object):
33
44
  self._spec_scan_npts = None
34
45
  self._spec_scan_data = None
35
46
  self._spec_positioner_values = None
36
-
47
+
37
48
  self._detector_data_path = None
38
-
49
+
39
50
  def __repr__(self):
40
- return(f'{self.__class__.__name__}({self.spec_file_name}, {self.scan_number}) -- {self.spec_command}')
41
-
51
+ return (f'{self.__class__.__name__}'
52
+ f'({self.spec_file_name}, {self.scan_number}) '
53
+ f'-- {self.spec_command}')
54
+
42
55
  @property
43
56
  def spec_file(self):
44
- # NB This FileSpec instance is not stored as a private attribute because
45
- # it cannot be pickled (and therefore could cause problems for
46
- # parallel code that uses ScanParsers).
47
- return(FileSpec(self.spec_file_name))
57
+ # NB This FileSpec instance is not stored as a private
58
+ # attribute because it cannot be pickled (and therefore could
59
+ # cause problems for parallel code that uses ScanParsers).
60
+ return FileSpec(self.spec_file_name)
61
+
48
62
  @property
49
63
  def scan_path(self):
50
64
  if self._scan_path is None:
51
65
  self._scan_path = self.get_scan_path()
52
66
  return self._scan_path
67
+
53
68
  @property
54
69
  def scan_name(self):
55
70
  if self._scan_name is None:
56
71
  self._scan_name = self.get_scan_name()
57
72
  return self._scan_name
73
+
58
74
  @property
59
75
  def scan_title(self):
60
76
  if self._scan_title is None:
61
77
  self._scan_title = self.get_scan_title()
62
78
  return self._scan_title
79
+
63
80
  @property
64
81
  def spec_scan(self):
65
82
  if self._spec_scan is None:
66
83
  self._spec_scan = self.get_spec_scan()
67
84
  return self._spec_scan
85
+
68
86
  @property
69
87
  def spec_command(self):
70
88
  if self._spec_command is None:
71
89
  self._spec_command = self.get_spec_command()
72
90
  return self._spec_command
91
+
73
92
  @property
74
93
  def spec_macro(self):
75
94
  if self._spec_macro is None:
76
95
  self._spec_macro = self.get_spec_macro()
77
96
  return self._spec_macro
97
+
78
98
  @property
79
99
  def spec_args(self):
80
100
  if self._spec_args is None:
81
101
  self._spec_args = self.get_spec_args()
82
102
  return self._spec_args
103
+
83
104
  @property
84
105
  def spec_scan_npts(self):
85
106
  if self._spec_scan_npts is None:
86
107
  self._spec_scan_npts = self.get_spec_scan_npts()
87
108
  return self._spec_scan_npts
109
+
88
110
  @property
89
111
  def spec_scan_data(self):
90
112
  if self._spec_scan_data is None:
91
113
  self._spec_scan_data = self.get_spec_scan_data()
92
114
  return self._spec_scan_data
115
+
93
116
  @property
94
117
  def spec_positioner_values(self):
95
118
  if self._spec_positioner_values is None:
96
119
  self._spec_positioner_values = self.get_spec_positioner_values()
97
120
  return self._spec_positioner_values
121
+
98
122
  @property
99
123
  def detector_data_path(self):
100
124
  if self._detector_data_path is None:
101
125
  self._detector_data_path = self.get_detector_data_path()
102
126
  return self._detector_data_path
103
-
127
+
104
128
  def get_scan_path(self):
105
- return(os.path.dirname(self.spec_file_name))
129
+ """Return the name of the directory containining the SPEC file
130
+ for this scan.
131
+
132
+ :rtype: str
133
+ """
134
+ return os.path.dirname(self.spec_file_name)
135
+
106
136
  def get_scan_name(self):
107
- return(None)
137
+ """Return the name of this SPEC scan (not unique to scans
138
+ within a single spec file).
139
+
140
+ :rtype: str
141
+ """
142
+ raise NotImplementedError
143
+
108
144
  def get_scan_title(self):
109
- return(None)
145
+ """Return the title of this spec scan (unique to each scan
146
+ within a spec file).
147
+
148
+ :rtype: str
149
+ """
150
+ raise NotImplementedError
151
+
110
152
  def get_spec_scan(self):
111
- return(self.spec_file.getScanByNumber(self.scan_number))
153
+ """Return the `pyspec.file.spec.Scan` object parsed from the
154
+ spec file and scan number provided to the constructor.
155
+
156
+ :rtype: pyspec.file.spec.Scan
157
+ """
158
+ return self.spec_file.getScanByNumber(self.scan_number)
159
+
112
160
  def get_spec_command(self):
113
- return(self.spec_scan.command)
161
+ """Return the string command of this SPEC scan.
162
+
163
+ :rtype: str
164
+ """
165
+ return self.spec_scan.command
166
+
114
167
  def get_spec_macro(self):
115
- return(self.spec_command.split()[0])
168
+ """Return the macro used in this scan's SPEC command.
169
+
170
+ :rtype: str
171
+ """
172
+ return self.spec_command.split()[0]
173
+
116
174
  def get_spec_args(self):
117
- return(self.spec_command.split()[1:])
175
+ """Return a list of the arguments provided to the macro for
176
+ this SPEC scan.
177
+
178
+ :rtype: list[str]
179
+ """
180
+ return self.spec_command.split()[1:]
181
+
118
182
  def get_spec_scan_npts(self):
119
- raise(NotImplementedError)
183
+ """Return the number of points collected in this SPEC scan
184
+
185
+ :rtype: int
186
+ """
187
+ raise NotImplementedError
188
+
120
189
  def get_spec_scan_data(self):
121
- return(dict(zip(self.spec_scan.labels, self.spec_scan.data.T)))
190
+ """Return a dictionary of all the counter data collected by
191
+ this SPEC scan.
192
+
193
+ :rtype: dict[str, numpy.ndarray]
194
+ """
195
+ return dict(zip(self.spec_scan.labels, self.spec_scan.data.T))
196
+
122
197
  def get_spec_positioner_values(self):
198
+ """Return a dictionary of all the SPEC positioner values
199
+ recorded by SPEC just before the scan began.
200
+
201
+ :rtype: dict[str,str]
202
+ """
123
203
  positioner_values = dict(self.spec_scan.motor_positions)
124
204
  names = list(positioner_values.keys())
125
205
  mnemonics = self.spec_scan.motors
@@ -127,94 +207,145 @@ class ScanParser(object):
127
207
  for name,mnemonic in zip(names,mnemonics):
128
208
  if name != mnemonic:
129
209
  positioner_values[mnemonic] = positioner_values[name]
130
- return(positioner_values)
210
+ return positioner_values
211
+
131
212
  def get_detector_data_path(self):
132
- raise(NotImplementedError)
133
-
134
- def get_detector_data_file(self, detector_prefix, scan_step_index:int):
135
- raise(NotImplementedError)
136
- def get_detector_data(self, detector_prefix, scan_step_index:int):
137
- '''
138
- Return a np.ndarray of detector data.
213
+ """Return the name of the directory containing detector data
214
+ collected by this scan.
139
215
 
140
- :param detector_prefix: The detector's name in any data files, often
141
- the EPICS macro $(P).
142
- :type detector_substring: str
216
+ :rtype: str
217
+ """
218
+ raise NotImplementedError
219
+
220
+ def get_detector_data_file(self, detector_prefix, scan_step_index:int):
221
+ """Return the name of the file containing detector data
222
+ collected at a certain step of this scan.
143
223
 
144
- :param scan_step_index: The index of the scan step for which detector
145
- data will be returned.
224
+ :param detector_prefix: the prefix used in filenames for the
225
+ detector
226
+ :type detector_prefix: str
227
+ :param scan_step_index: the index of the point in this scan
228
+ whose detector file name should be returned.
146
229
  :type scan_step_index: int
230
+ :rtype: str
231
+ """
232
+ raise NotImplementedError
147
233
 
148
- :return: The detector data
149
- :rtype: np.ndarray
150
- '''
151
- raise(NotImplementedError)
234
+ def get_detector_data(self, detector_prefix, scan_step_index:int):
235
+ """Return the detector data collected at a certain step of
236
+ this scan.
237
+
238
+ :param detector_prefix: the prefix used in filenames for the
239
+ detector
240
+ :type detector_prefix: str
241
+ :param scan_step_index: the index of the point in this scan
242
+ whose detector data should be returned.
243
+ :type scan_step_index: int
244
+ :rtype: numpy.ndarray
245
+ """
246
+ raise NotImplementedError
152
247
 
153
248
  def get_spec_positioner_value(self, positioner_name):
249
+ """Return the value of a spec positioner recorded before this
250
+ scan began.
251
+
252
+ :param positioner_name: the name or mnemonic of a SPEC motor
253
+ whose position should be returned.
254
+ :raises KeyError: if `positioner_name` is not the name or
255
+ mnemonic of a SPEC motor recorded for this scan.
256
+ :raises ValueError: if the recorded string value of the
257
+ positioner in the SPEC file cannot be converted to a
258
+ float.
259
+ :rtype: float
260
+ """
154
261
  try:
155
262
  positioner_value = self.spec_positioner_values[positioner_name]
156
263
  positioner_value = float(positioner_value)
157
- return(positioner_value)
158
264
  except KeyError:
159
- raise(KeyError(f'{self.scan_title}: motor {positioner_name} not found for this scan'))
265
+ raise KeyError(f'{self.scan_title}: motor {positioner_name} '
266
+ 'not found for this scan')
160
267
  except ValueError:
161
- raise(ValueError(f'{self.scan_title}: ccould not convert value of {positioner_name} to float: {positioner_value}'))
268
+ raise ValueError(f'{self.scan_title}: could not convert value of'
269
+ f' {positioner_name} to float: '
270
+ f'{positioner_value}')
271
+ return positioner_value
162
272
 
163
273
 
164
274
  class FMBScanParser(ScanParser):
165
- def __init__(self, spec_file_name, scan_number):
166
- super().__init__(spec_file_name, scan_number)
275
+ """Partial implementation of a class representing a SPEC scan
276
+ collected at FMB.
277
+ """
278
+
167
279
  def get_scan_name(self):
168
- return(os.path.basename(self.spec_file.abspath))
169
- def get_scan_title(self):
170
- return(f'{self.scan_name}_{self.scan_number:03d}')
280
+ return os.path.basename(self.spec_file.abspath)
171
281
 
282
+ def get_scan_title(self):
283
+ return f'{self.scan_name}_{self.scan_number:03d}'
172
284
 
173
285
 
174
286
  class SMBScanParser(ScanParser):
287
+ """Partial implementation of a class representing a SPEC scan
288
+ collected at SMB or FAST.
289
+ """
290
+
175
291
  def __init__(self, spec_file_name, scan_number):
176
292
  super().__init__(spec_file_name, scan_number)
177
-
178
- self._pars = None # purpose: store values found in the .par file as a dictionary
293
+
294
+ self._pars = None
179
295
  self.par_file_pattern = f'*-*-{self.scan_name}'
180
-
296
+
181
297
  def get_scan_name(self):
182
- return(os.path.basename(self.scan_path))
298
+ return os.path.basename(self.scan_path)
299
+
183
300
  def get_scan_title(self):
184
- return(f'{self.scan_name}_{self.scan_number}')
185
-
301
+ return f'{self.scan_name}_{self.scan_number}'
302
+
186
303
  @property
187
304
  def pars(self):
188
305
  if self._pars is None:
189
306
  self._pars = self.get_pars()
190
- return(self._pars)
191
-
307
+ return self._pars
308
+
192
309
  def get_pars(self):
310
+ """Return a dictionary of values recorded in the .par file
311
+ associated with this SPEC scan.
312
+
313
+ :rtype: dict[str,object]
314
+ """
193
315
  # JSON file holds titles for columns in the par file
194
- json_files = fnmatch.filter(os.listdir(self.scan_path), f'{self.par_file_pattern}.json')
195
- if not len(json_files) == 1:
196
- raise(RuntimeError(f'{self.scan_title}: cannot find the .json file to decode the .par file'))
316
+ json_files = fnmatch_filter(
317
+ os.listdir(self.scan_path),
318
+ f'{self.par_file_pattern}.json')
319
+ if len(json_files) != 1:
320
+ raise RuntimeError(f'{self.scan_title}: cannot find the '
321
+ '.json file to decode the .par file')
197
322
  with open(os.path.join(self.scan_path, json_files[0])) as json_file:
198
- par_file_cols = json.load(json_file)
323
+ par_file_cols = load(json_file)
199
324
  try:
200
325
  par_col_names = list(par_file_cols.values())
201
326
  scann_val_idx = par_col_names.index('SCAN_N')
202
327
  scann_col_idx = int(list(par_file_cols.keys())[scann_val_idx])
203
328
  except:
204
- raise(RuntimeError(f'{self.scan_title}: cannot find scan pars without a "SCAN_N" column in the par file'))
205
-
206
- par_files = fnmatch.filter(os.listdir(self.scan_path), f'{self.par_file_pattern}.par')
207
- if not len(par_files) == 1:
208
- raise(RuntimeError(f'{self.scan_title}: cannot find the .par file for this scan directory'))
329
+ raise RuntimeError(f'{self.scan_title}: cannot find scan pars '
330
+ 'without a "SCAN_N" column in the par file')
331
+
332
+ par_files = fnmatch_filter(
333
+ os.listdir(self.scan_path),
334
+ f'{self.par_file_pattern}.par')
335
+ if len(par_files) != 1:
336
+ raise RuntimeError(f'{self.scan_title}: cannot find the .par '
337
+ 'file for this scan directory')
338
+ par_dict = None
209
339
  with open(os.path.join(self.scan_path, par_files[0])) as par_file:
210
- par_reader = csv.reader(par_file, delimiter=' ')
340
+ par_reader = reader(par_file, delimiter=' ')
211
341
  for row in par_reader:
212
342
  if len(row) == len(par_col_names):
213
343
  row_scann = int(row[scann_col_idx])
214
344
  if row_scann == self.scan_number:
215
345
  par_dict = {}
216
346
  for par_col_idx,par_col_name in par_file_cols.items():
217
- # Convert the string par value from the file to an int or float, if possible.
347
+ # Convert the string par value from the
348
+ # file to an int or float, if possible.
218
349
  par_value = row[int(par_col_idx)]
219
350
  try:
220
351
  par_value = int(par_value)
@@ -224,27 +355,49 @@ class SMBScanParser(ScanParser):
224
355
  except:
225
356
  pass
226
357
  par_dict[par_col_name] = par_value
227
- return(par_dict)
228
- raise(RuntimeError(f'{self.scan_title}: could not find scan pars for scan number {self.scan_number}'))
229
-
358
+
359
+ if par_dict is None:
360
+ raise RuntimeError(f'{self.scan_title}: could not find scan pars '
361
+ 'for scan number {self.scan_number}')
362
+ return par_dict
363
+
230
364
  def get_counter_gain(self, counter_name):
365
+ """Return the gain of a counter as recorded in the comments of
366
+ a scan in a SPEC file converted to nA/V.
367
+
368
+ :param counter_name: the name of the counter
369
+ :type counter_name: str
370
+ :rtype: str
371
+ """
372
+ counter_gain = None
231
373
  for comment in self.spec_scan.comments:
232
- match = re.search(f'{counter_name} gain: (?P<gain_value>\d+) (?P<unit_prefix>[m|u|n])A/V', comment)
374
+ match = re.search(
375
+ f'{counter_name} gain: ' # start of counter gain comments
376
+ '(?P<gain_value>\d+) ' # gain numerical value
377
+ '(?P<unit_prefix>[m|u|n])A/V', # gain units
378
+ comment)
233
379
  if match:
234
380
  unit_prefix = match['unit_prefix']
235
- gain_scalar = 1 if unit_prefix == 'n' else 1e3 if unit_prefix == 'u' else 1e6
381
+ gain_scalar = 1 if unit_prefix == 'n' \
382
+ else 1e3 if unit_prefix == 'u' else 1e6
236
383
  counter_gain = f'{float(match["gain_value"])*gain_scalar} nA/V'
237
- return(counter_gain)
238
- raise(RuntimeError(f'{self.scan_title}: could not get gain for counter {counter_name}'))
384
+
385
+ if counter_gain is None:
386
+ raise RuntimeError(f'{self.scan_title}: could not get gain for '
387
+ f'counter {counter_name}')
388
+ return counter_gain
239
389
 
240
390
 
241
391
  class LinearScanParser(ScanParser):
392
+ """Partial implementation of a class representing a typical line
393
+ or mesh scan in SPEC.
394
+ """
242
395
  def __init__(self, spec_file_name, scan_number):
243
396
  super().__init__(spec_file_name, scan_number)
244
-
397
+
245
398
  self._spec_scan_motor_mnes = None
246
399
  self._spec_scan_motor_vals = None
247
- self._spec_scan_shape = None
400
+ self._spec_scan_shape = None
248
401
  self._spec_scan_dwell = None
249
402
 
250
403
  @property
@@ -252,38 +405,107 @@ class LinearScanParser(ScanParser):
252
405
  if self._spec_scan_motor_mnes is None:
253
406
  self._spec_scan_motor_mnes = self.get_spec_scan_motor_mnes()
254
407
  return self._spec_scan_motor_mnes
408
+
255
409
  @property
256
410
  def spec_scan_motor_vals(self):
257
411
  if self._spec_scan_motor_vals is None:
258
412
  self._spec_scan_motor_vals = self.get_spec_scan_motor_vals()
259
413
  return self._spec_scan_motor_vals
414
+
260
415
  @property
261
416
  def spec_scan_shape(self):
262
417
  if self._spec_scan_shape is None:
263
418
  self._spec_scan_shape = self.get_spec_scan_shape()
264
419
  return self._spec_scan_shape
420
+
265
421
  @property
266
422
  def spec_scan_dwell(self):
267
423
  if self._spec_scan_dwell is None:
268
424
  self._spec_scan_dwell = self.get_spec_scan_dwell()
269
- return(self._spec_scan_dwell)
425
+ return self._spec_scan_dwell
426
+
427
+ def get_spec_scan_motor_mnes(self):
428
+ """Return the mnemonics of the SPEC motor(s) provided to the
429
+ macro for this scan. If there is more than one motor scanned
430
+ (in a "flymesh" scan, for example), the order of motors in the
431
+ returned tuple will go from the fastest moving motor first to
432
+ the slowest moving motor last.
433
+
434
+ :rtype: tuple
435
+ """
436
+ raise NotImplementedError
270
437
 
271
- def get_spec_scan_motor_names(self):
272
- raise(NotImplementedError)
273
438
  def get_spec_scan_motor_vals(self):
274
- raise(NotImplementedError)
439
+ """Return the values visited by each of the scanned motors. If
440
+ there is more than one motor scanned (in a "flymesh" scan, for
441
+ example), the order of motor values in the returned tuple will
442
+ go from the fastest moving motor's values first to the slowest
443
+ moving motor's values last.
444
+
445
+ :rtype: tuple
446
+ """
447
+ raise NotImplementedError
448
+
275
449
  def get_spec_scan_shape(self):
276
- raise(NotImplementedError)
450
+ """Return the number of points visited by each of the scanned
451
+ motors. If there is more than one motor scanned (in a
452
+ "flymesh" scan, for example), the order of number of motor
453
+ values in the returned tuple will go from the number of points
454
+ visited by the fastest moving motor first to the the number of
455
+ points visited by the slowest moving motor last.
456
+
457
+ :rtype: tuple
458
+ """
459
+ raise NotImplementedError
460
+
461
+ def get_spec_scan_dwell(self):
462
+ """Return the dwell time for each point in the scan as it
463
+ appears in the command string.
464
+
465
+ :rtype: float
466
+ """
467
+ raise NotImplementedError
468
+
277
469
  def get_spec_scan_npts(self):
278
- return(np.prod(self.spec_scan_shape))
470
+ """Return the number of points collected in this SPEC scan.
471
+
472
+ :rtype: int
473
+ """
474
+ return np.prod(self.spec_scan_shape)
475
+
279
476
  def get_scan_step(self, scan_step_index:int):
477
+ """Return the index of each motor coordinate corresponding to
478
+ the index of a single point in the scan. If there is more than
479
+ one motor scanned (in a "flymesh" scan, for example), the
480
+ order of indices in the returned tuple will go from the index
481
+ of the value of the fastest moving motor first to the index of
482
+ the value of the slowest moving motor last.
483
+
484
+ :param scan_step_index: the index of a single point in the
485
+ scan.
486
+ :type scan_step_index: int
487
+ :rtype: tuple
488
+ """
280
489
  scan_steps = np.ndindex(self.spec_scan_shape[::-1])
281
490
  i = 0
282
491
  while i <= scan_step_index:
283
492
  scan_step = next(scan_steps)
284
493
  i += 1
285
- return(scan_step)
494
+ return scan_step
495
+
286
496
  def get_scan_step_index(self, scan_step:tuple):
497
+ """Return the index of a single scan point corresponding to a
498
+ tuple of indices for each scanned motor coordinate.
499
+
500
+ :param scan_step: a tuple of the indices of each scanned motor
501
+ coordinate. If there is more than one motor scanned (in a
502
+ "flymesh" scan, for example), the order of indices should
503
+ go from the index of the value of the fastest moving motor
504
+ first to the index of the value of the slowest moving
505
+ motor last.
506
+ :type scan_step: tuple
507
+ :trype: int
508
+ """
287
509
  scan_steps = np.ndindex(self.spec_scan_shape[::-1])
288
510
  scan_step_found = False
289
511
  scan_step_index = -1
@@ -293,166 +515,223 @@ class LinearScanParser(ScanParser):
293
515
  if next_scan_step == scan_step:
294
516
  scan_step_found = True
295
517
  break
296
- return(scan_step_index)
518
+ return scan_step_index
297
519
 
298
520
 
299
521
  class FMBLinearScanParser(LinearScanParser, FMBScanParser):
300
- def __init__(self, spec_file_name, scan_number):
301
- super().__init__(spec_file_name, scan_number)
302
-
522
+ """Partial implementation of a class representing a typical line
523
+ or mesh scan in SPEC collected at FMB.
524
+ """
525
+
303
526
  def get_spec_scan_motor_mnes(self):
304
527
  if self.spec_macro == 'flymesh':
305
- return((self.spec_args[0], self.spec_args[5]))
306
- elif self.spec_macro == 'flyscan':
307
- return((self.spec_args[0],))
308
- elif self.spec_macro in ('tseries', 'loopscan'):
309
- return(('Time',))
310
- else:
311
- raise(RuntimeError(f'{self.scan_title}: cannot determine scan motors for scans of type {self.spec_macro}'))
528
+ return (self.spec_args[0], self.spec_args[5])
529
+ if self.spec_macro == 'flyscan':
530
+ return (self.spec_args[0],)
531
+ if self.spec_macro in ('tseries', 'loopscan'):
532
+ return ('Time',)
533
+ raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
534
+ f'for scans of type {self.spec_macro}')
535
+
312
536
  def get_spec_scan_motor_vals(self):
313
537
  if self.spec_macro == 'flymesh':
314
- fast_mot_vals = np.linspace(float(self.spec_args[1]), float(self.spec_args[2]), int(self.spec_args[3])+1)
315
- slow_mot_vals = np.linspace(float(self.spec_args[6]), float(self.spec_args[7]), int(self.spec_args[8])+1)
316
- return((fast_mot_vals, slow_mot_vals))
317
- elif self.spec_macro == 'flyscan':
318
- mot_vals = np.linspace(float(self.spec_args[1]), float(self.spec_args[2]), int(self.spec_args[3])+1)
319
- return((mot_vals,))
320
- elif self.spec_macro in ('tseries', 'loopscan'):
321
- return(self.spec_scan.data[:,0])
322
- else:
323
- raise(RuntimeError(f'{self.scan_title}: cannot determine scan motors for scans of type {self.spec_macro}'))
538
+ fast_mot_vals = np.linspace(float(self.spec_args[1]),
539
+ float(self.spec_args[2]),
540
+ int(self.spec_args[3])+1)
541
+ slow_mot_vals = np.linspace(float(self.spec_args[6]),
542
+ float(self.spec_args[7]),
543
+ int(self.spec_args[8])+1)
544
+ return (fast_mot_vals, slow_mot_vals)
545
+ if self.spec_macro == 'flyscan':
546
+ mot_vals = np.linspace(float(self.spec_args[1]),
547
+ float(self.spec_args[2]),
548
+ int(self.spec_args[3])+1)
549
+ return (mot_vals,)
550
+ if self.spec_macro in ('tseries', 'loopscan'):
551
+ return self.spec_scan.data[:,0]
552
+ raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
553
+ f'for scans of type {self.spec_macro}')
554
+
324
555
  def get_spec_scan_shape(self):
325
556
  if self.spec_macro == 'flymesh':
326
557
  fast_mot_npts = int(self.spec_args[3])+1
327
558
  slow_mot_npts = int(self.spec_args[8])+1
328
- return((fast_mot_npts, slow_mot_npts))
329
- elif self.spec_macro == 'flyscan':
559
+ return (fast_mot_npts, slow_mot_npts)
560
+ if self.spec_macro == 'flyscan':
330
561
  mot_npts = int(self.spec_args[3])+1
331
- return((mot_npts,))
332
- elif self.spec_macro in ('tseries', 'loopscan'):
333
- return(len(np.array(self.spec_scan.data[:,0])))
334
- else:
335
- raise(RuntimeError(f'{self.scan_title}: cannot determine scan shape for scans of type {self.spec_macro}'))
562
+ return (mot_npts,)
563
+ if self.spec_macro in ('tseries', 'loopscan'):
564
+ return len(np.array(self.spec_scan.data[:,0]))
565
+ raise RuntimeError(f'{self.scan_title}: cannot determine scan shape '
566
+ f'for scans of type {self.spec_macro}')
567
+
336
568
  def get_spec_scan_dwell(self):
337
569
  if self.spec_macro in ('flymesh', 'flyscan'):
338
- return(float(self.spec_args[4]))
339
- elif self.spec_macro in ('tseries', 'loopscan'):
340
- return(float(self.spec_args[1]))
341
- else:
342
- raise(RuntimeError(f'{self.scan_title}: cannot determine dwell for scans of type {self.spec_macro}'))
570
+ return float(self.spec_args[4])
571
+ if self.spec_macro in ('tseries', 'loopscan'):
572
+ return float(self.spec_args[1])
573
+ raise RuntimeError(f'{self.scan_title}: cannot determine dwell for '
574
+ f'scans of type {self.spec_macro}')
575
+
343
576
  def get_detector_data_path(self):
344
- return(os.path.join(self.scan_path, self.scan_title))
577
+ return os.path.join(self.scan_path, self.scan_title)
345
578
 
346
579
 
347
580
  class FMBSAXSWAXSScanParser(FMBLinearScanParser):
348
- def __init__(self, spec_file_name, scan_number):
349
- super().__init__(spec_file_name, scan_number)
581
+ """Concrete implementation of a class representing a scan taken
582
+ with the typical SAXS/WAXS setup at FMB.
583
+ """
350
584
 
351
585
  def get_scan_title(self):
352
- return(f'{self.scan_name}_{self.scan_number:03d}')
586
+ return f'{self.scan_name}_{self.scan_number:03d}'
587
+
353
588
  def get_detector_data_file(self, detector_prefix, scan_step_index:int):
354
589
  scan_step = self.get_scan_step(scan_step_index)
355
- file_indices = [f'{scan_step[i]:03d}' for i in range(len(self.spec_scan_shape)) if self.spec_scan_shape[i] != 1]
356
- file_name = f'{self.scan_name}_{detector_prefix}_{self.scan_number:03d}_{"_".join(file_indices)}.tiff'
590
+ file_indices = [f'{scan_step[i]:03d}'
591
+ for i in range(len(self.spec_scan_shape))
592
+ if self.spec_scan_shape[i] != 1]
593
+ file_name = f'{self.scan_name}_{detector_prefix}_' \
594
+ f'{self.scan_number:03d}_{"_".join(file_indices)}.tiff'
357
595
  file_name_full = os.path.join(self.detector_data_path, file_name)
358
596
  if os.path.isfile(file_name_full):
359
- return(file_name_full)
360
- else:
361
- raise(RuntimeError(f'{self.scan_title}: could not find detector image file for detector {detector_prefix} scan step ({scan_step})'))
597
+ return file_name_full
598
+ raise RuntimeError(f'{self.scan_title}: could not find detector image '
599
+ f'file for detector {detector_prefix} scan step '
600
+ f'({scan_step})')
601
+
362
602
  def get_detector_data(self, detector_prefix, scan_step_index:int):
603
+ # third party modules
363
604
  from pyspec.file.tiff import TiffFile
364
- image_file = self.get_detector_data_file(detector_prefix, scan_step_index)
605
+
606
+ image_file = self.get_detector_data_file(detector_prefix,
607
+ scan_step_index)
365
608
  with TiffFile(image_file) as tiff_file:
366
609
  image_data = tiff_file.asarray()
367
- return(image_data)
610
+ return image_data
368
611
 
369
612
 
370
613
  class FMBXRFScanParser(FMBLinearScanParser):
371
- def __init__(self, spec_file_name, scan_number):
372
- super().__init__(spec_file_name, scan_number)
614
+ """Concrete implementation of a class representing a scan taken
615
+ with the typical XRF setup at FMB.
616
+ """
617
+
373
618
  def get_scan_title(self):
374
- return(f'{self.scan_name}_scan{self.scan_number}')
619
+ return f'{self.scan_name}_scan{self.scan_number}'
620
+
375
621
  def get_detector_data_file(self, detector_prefix, scan_step_index:int):
376
622
  scan_step = self.get_scan_step(scan_step_index)
377
623
  file_name = f'scan{self.scan_number}_{scan_step[1]:03d}.hdf5'
378
624
  file_name_full = os.path.join(self.detector_data_path, file_name)
379
625
  if os.path.isfile(file_name_full):
380
- return(file_name_full)
381
- else:
382
- raise(RuntimeError(f'{self.scan_title}: could not find detector image file for detector {detector_prefix} scan step ({scan_step_index})'))
626
+ return file_name_full
627
+ raise RuntimeError(f'{self.scan_title}: could not find detector image '
628
+ f'file for detector {detector_prefix} scan step '
629
+ f'({scan_step_index})')
630
+
383
631
  def get_detector_data(self, detector_prefix, scan_step_index:int):
384
- import h5py
385
- detector_file = self.get_detector_data_file(detector_prefix, scan_step_index)
632
+ # third party modules
633
+ from h5py import File
634
+
635
+ detector_file = self.get_detector_data_file(
636
+ detector_prefix, scan_step_index)
386
637
  scan_step = self.get_scan_step(scan_step_index)
387
- with h5py.File(detector_file) as h5_file:
388
- detector_data = h5_file['/entry/instrument/detector/data'][scan_step[0]]
389
- return(detector_data)
638
+ with File(detector_file) as h5_file:
639
+ detector_data = \
640
+ h5_file['/entry/instrument/detector/data'][scan_step[0]]
641
+ return detector_data
390
642
 
391
643
 
392
644
  class SMBLinearScanParser(LinearScanParser, SMBScanParser):
393
- def __init__(self, spec_file_name, scan_number):
394
- super().__init__(spec_file_name, scan_number)
645
+ """Concrete implementation of a class representing a scan taken
646
+ with the typical powder diffraction setup at SMB.
647
+ """
648
+
395
649
  def get_spec_scan_motor_mnes(self):
396
650
  if self.spec_macro == 'flymesh':
397
- return((self.spec_args[0], self.spec_args[5]))
398
- elif self.spec_macro == 'flyscan':
399
- return((self.spec_args[0],))
400
- elif self.spec_macro in ('tseries', 'loopscan'):
401
- return(('Time',))
402
- else:
403
- raise(RuntimeError(f'{self.scan_title}: cannot determine scan motors for scans of type {self.spec_macro}'))
651
+ return (self.spec_args[0], self.spec_args[5])
652
+ if self.spec_macro == 'flyscan':
653
+ return (self.spec_args[0],)
654
+ if self.spec_macro in ('tseries', 'loopscan'):
655
+ return ('Time',)
656
+ raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
657
+ f'for scans of type {self.spec_macro}')
658
+
404
659
  def get_spec_scan_motor_vals(self):
405
660
  if self.spec_macro == 'flymesh':
406
- fast_mot_vals = np.linspace(float(self.spec_args[1]), float(self.spec_args[2]), int(self.spec_args[3])+1)
407
- slow_mot_vals = np.linspace(float(self.spec_args[6]), float(self.spec_args[7]), int(self.spec_args[8])+1)
408
- return((fast_mot_vals, slow_mot_vals))
409
- elif self.spec_macro == 'flyscan':
410
- mot_vals = np.linspace(float(self.spec_args[1]), float(self.spec_args[2]), int(self.spec_args[3])+1)
411
- return((mot_vals,))
412
- elif self.spec_macro in ('tseries', 'loopscan'):
413
- return(self.spec_scan.data[:,0])
414
- else:
415
- raise(RuntimeError(f'{self.scan_title}: cannot determine scan motors for scans of type {self.spec_macro}'))
661
+ fast_mot_vals = np.linspace(float(self.spec_args[1]),
662
+ float(self.spec_args[2]),
663
+ int(self.spec_args[3])+1)
664
+ slow_mot_vals = np.linspace(float(self.spec_args[6]),
665
+ float(self.spec_args[7]),
666
+ int(self.spec_args[8])+1)
667
+ return (fast_mot_vals, slow_mot_vals)
668
+ if self.spec_macro == 'flyscan':
669
+ mot_vals = np.linspace(float(self.spec_args[1]),
670
+ float(self.spec_args[2]),
671
+ int(self.spec_args[3])+1)
672
+ return (mot_vals,)
673
+ if self.spec_macro in ('tseries', 'loopscan'):
674
+ return self.spec_scan.data[:,0]
675
+ raise RuntimeError(f'{self.scan_title}: cannot determine scan motors '
676
+ f'for scans of type {self.spec_macro}')
677
+
416
678
  def get_spec_scan_shape(self):
417
679
  if self.spec_macro == 'flymesh':
418
680
  fast_mot_npts = int(self.spec_args[3])+1
419
681
  slow_mot_npts = int(self.spec_args[8])+1
420
- return((fast_mot_npts, slow_mot_npts))
421
- elif self.spec_macro == 'flyscan':
682
+ return (fast_mot_npts, slow_mot_npts)
683
+ if self.spec_macro == 'flyscan':
422
684
  mot_npts = int(self.spec_args[3])+1
423
- return((mot_npts,))
424
- elif self.spec_macro in ('tseries', 'loopscan'):
425
- return(len(np.array(self.spec_scan.data[:,0])))
426
- else:
427
- raise(RuntimeError(f'{self.scan_title}: cannot determine scan shape for scans of type {self.spec_macro}'))
685
+ return (mot_npts,)
686
+ if self.spec_macro in ('tseries', 'loopscan'):
687
+ return len(np.array(self.spec_scan.data[:,0]))
688
+ raise RuntimeError(f'{self.scan_title}: cannot determine scan shape '
689
+ f'for scans of type {self.spec_macro}')
690
+
428
691
  def get_spec_scan_dwell(self):
429
692
  if self.spec_macro == 'flymesh':
430
- return(float(self.spec_args[4]))
431
- elif self.spec_macro == 'flyscan':
432
- return(float(self.spec_args[-1]))
433
- else:
434
- raise(RuntimeError(f'{self.scan_title}: cannot determine dwell time for scans of type {self.spec_macro}'))
693
+ return float(self.spec_args[4])
694
+ if self.spec_macro == 'flyscan':
695
+ return float(self.spec_args[-1])
696
+ raise RuntimeError(f'{self.scan_title}: cannot determine dwell time '
697
+ f'for scans of type {self.spec_macro}')
698
+
435
699
  def get_detector_data_path(self):
436
- return(os.path.join(self.scan_path, str(self.scan_number)))
700
+ return os.path.join(self.scan_path, str(self.scan_number))
701
+
437
702
  def get_detector_data_file(self, detector_prefix, scan_step_index:int):
438
703
  scan_step = self.get_scan_step(scan_step_index)
439
704
  if len(scan_step) == 1:
440
705
  scan_step = (0, *scan_step)
441
- file_name_pattern = f'{detector_prefix}_{self.scan_name}_*_{scan_step[0]}_data_{(scan_step[1]+1):06d}.h5'
442
- file_name_matches = fnmatch.filter(os.listdir(self.detector_data_path), file_name_pattern)
706
+ file_name_pattern = (f'{detector_prefix}_'
707
+ f'{self.scan_name}_*_'
708
+ f'{scan_step[0]}_data_'
709
+ f'{(scan_step[1]+1):06d}.h5')
710
+ file_name_matches = fnmatch_filter(
711
+ os.listdir(self.detector_data_path),
712
+ file_name_pattern)
443
713
  if len(file_name_matches) == 1:
444
- return(os.path.join(self.detector_data_path, file_name_matches[0]))
445
- else:
446
- raise(RuntimeError(f'{self.scan_title}: could not find detector image file for detector {detector_prefix} scan step ({scan_step_index})'))
714
+ return os.path.join(self.detector_data_path, file_name_matches[0])
715
+ raise RuntimeError(f'{self.scan_title}: could not find detector image '
716
+ f'file for detector {detector_prefix} scan step '
717
+ f'({scan_step_index})')
718
+
447
719
  def get_detector_data(self, detector_prefix, scan_step_index:int):
448
- import h5py
449
- image_file = self.get_detector_data_file(detector_prefix, scan_step_index)
450
- with h5py.File(image_file) as h5_file:
720
+ # third party modules
721
+ from h5py import File
722
+
723
+ image_file = self.get_detector_data_file(
724
+ detector_prefix, scan_step_index)
725
+ with File(image_file) as h5_file:
451
726
  image_data = h5_file['/entry/data/data'][0]
452
- return(image_data)
727
+ return image_data
453
728
 
454
729
 
455
730
  class RotationScanParser(ScanParser):
731
+ """Partial implementation of a class representing a rotation
732
+ scan.
733
+ """
734
+
456
735
  def __init__(self, spec_file_name, scan_number):
457
736
  super().__init__(spec_file_name, scan_number)
458
737
 
@@ -467,291 +746,407 @@ class RotationScanParser(ScanParser):
467
746
  def scan_type(self):
468
747
  if self._scan_type is None:
469
748
  self._scan_type = self.get_scan_type()
470
- return(self._scan_type)
749
+ return self._scan_type
750
+
471
751
  @property
472
752
  def theta_vals(self):
473
753
  if self._theta_vals is None:
474
754
  self._theta_vals = self.get_theta_vals()
475
- return(self._theta_vals)
755
+ return self._theta_vals
756
+
476
757
  @property
477
758
  def horizontal_shift(self):
478
759
  if self._horizontal_shift is None:
479
760
  self._horizontal_shift = self.get_horizontal_shift()
480
- return(self._horizontal_shift)
761
+ return self._horizontal_shift
762
+
481
763
  @property
482
764
  def vertical_shift(self):
483
765
  if self._vertical_shift is None:
484
766
  self._vertical_shift = self.get_vertical_shift()
485
- return(self._vertical_shift)
767
+ return self._vertical_shift
768
+
486
769
  @property
487
770
  def starting_image_index(self):
488
771
  if self._starting_image_index is None:
489
772
  self._starting_image_index = self.get_starting_image_index()
490
- return(self._starting_image_index)
773
+ return self._starting_image_index
774
+
491
775
  @property
492
776
  def starting_image_offset(self):
493
777
  if self._starting_image_offset is None:
494
778
  self._starting_image_offset = self.get_starting_image_offset()
495
- return(self._starting_image_offset)
496
-
779
+ return self._starting_image_offset
780
+
497
781
  def get_scan_type(self):
498
- return(None)
782
+ """Return a string identifier for the type of tomography data
783
+ being collected by this scan: df1 (dark field), bf1 (bright
784
+ field), or tf1 (sample tomography data).
785
+
786
+ :rtype: typing.Literal['df1', 'bf1', 'tf1']
787
+ """
788
+ return None
789
+
499
790
  def get_theta_vals(self):
500
- raise(NotImplementedError)
791
+ """Return a dictionary of information about the angular values
792
+ visited by the rotating motor at each point in the scan. The
793
+ dictionary may contain a single key, "num", or three keys:
794
+ "num", "start", and "end"
795
+
796
+ :rtype: dict[str, float]"""
797
+ raise NotImplementedError
798
+
501
799
  def get_horizontal_shift(self):
502
- raise(NotImplementedError)
800
+ """Return the value of the motor that shifts the sample in the
801
+ +x direction (hutch frame). Useful when tomography scans are
802
+ taken in a series of stacks when the sample is wider than the
803
+ width of the beam.
804
+
805
+ :rtype: float
806
+ """
807
+ raise NotImplementedError
808
+
503
809
  def get_vertical_shift(self):
504
- raise(NotImplementedError)
810
+ """Return the value of the motor that shifts the sample in the
811
+ +z direction (hutch frame). Useful when tomography scans are
812
+ taken in a series of stacks when the sample is taller than the
813
+ height of the beam.
814
+
815
+ :rtype: float
816
+ """
817
+ raise NotImplementedError
818
+
505
819
  def get_starting_image_index(self):
506
- raise(NotImplementedError)
820
+ """Return the index of the first frame of detector data
821
+ collected by this scan.
822
+
823
+ :rtype: int
824
+ """
825
+ raise NotImplementedError
826
+
507
827
  def get_starting_image_offset(self):
508
- raise(NotImplementedError)
828
+ """Return the offet of the index of the first "good" frame of
829
+ detector data collected by this scan from the index of the
830
+ first frame of detector data collected by this scan.
831
+
832
+ :rtype: int
833
+ """
834
+ raise NotImplementedError
835
+
509
836
  def get_num_image(self, detector_prefix):
510
- raise(NotImplementedError)
837
+ """Return the total number of "good" frames of detector data
838
+ collected by this scan
839
+
840
+ :rtype: int
841
+ """
842
+ raise NotImplementedError
511
843
 
512
844
 
513
845
  class FMBRotationScanParser(RotationScanParser, FMBScanParser):
514
- def __init__(self, spec_file_name, scan_number):
515
- super().__init__(spec_file_name, scan_number)
846
+ """Concrete implementation of a class representing a scan taken
847
+ with the typical tomography setup at FMB.
848
+ """
849
+
516
850
  def get_spec_scan_npts(self):
517
851
  if self.spec_macro == 'flyscan':
518
852
  if len(self.spec_args) == 2:
519
853
  # Flat field (dark or bright)
520
- return(int(self.spec_args[0])+1)
521
- elif len(self.spec_args) == 5:
522
- return(int(self.spec_args[3])+1)
523
- else:
524
- raise(RuntimeError(f'{self.scan_title}: cannot obtain number of points from '+
525
- f'{self.spec_macro} with arguments {self.spec_args}'))
526
- else:
527
- raise(RuntimeError(f'{self.scan_title}: cannot determine number of points for scans '+
528
- f'of type {self.spec_macro}'))
854
+ return int(self.spec_args[0])+1
855
+ if len(self.spec_args) == 5:
856
+ return int(self.spec_args[3])+1
857
+ raise RuntimeError(f'{self.scan_title}: cannot obtain number of '
858
+ f'points from {self.spec_macro} with '
859
+ f'arguments {self.spec_args}')
860
+ raise RuntimeError(f'{self.scan_title}: cannot determine number of '
861
+ f'points for scans of type {self.spec_macro}')
862
+
529
863
  def get_theta_vals(self):
530
864
  if self.spec_macro == 'flyscan':
531
865
  if len(self.spec_args) == 2:
532
866
  # Flat field (dark or bright)
533
- return({'num': int(self.spec_args[0])})
534
- elif len(self.spec_args) == 5:
535
- return({'start': float(self.spec_args[1]), 'end': float(self.spec_args[2]),
536
- 'num': int(self.spec_args[3])+1})
537
- else:
538
- raise(RuntimeError(f'{self.scan_title}: cannot obtain theta values from '+
539
- f'{self.spec_macro} with arguments {self.spec_args}'))
540
- else:
541
- raise(RuntimeError(f'{self.scan_title}: cannot determine theta values for scans '+
542
- f'of type {self.spec_macro}'))
867
+ return {'num': int(self.spec_args[0])}
868
+ if len(self.spec_args) == 5:
869
+ return {'start': float(self.spec_args[1]),
870
+ 'end': float(self.spec_args[2]),
871
+ 'num': int(self.spec_args[3])+1}
872
+ raise RuntimeError(f'{self.scan_title}: cannot obtain theta values'
873
+ f' from {self.spec_macro} with arguments '
874
+ f'{self.spec_args}')
875
+ raise RuntimeError(f'{self.scan_title}: cannot determine theta values '
876
+ f'for scans of type {self.spec_macro}')
877
+
543
878
  def get_horizontal_shift(self):
544
- return(0.0)
879
+ return 0.0
880
+
545
881
  def get_vertical_shift(self):
546
- return(float(self.get_spec_positioner_value('4C_samz')))
882
+ return float(self.get_spec_positioner_value('4C_samz'))
883
+
547
884
  def get_starting_image_index(self):
548
- return(0)
885
+ return 0
886
+
549
887
  def get_starting_image_offset(self):
550
- return(1)
888
+ return 1
889
+
551
890
  def get_num_image(self, detector_prefix):
552
- import h5py
891
+ # third party modules
892
+ from h5py import File
893
+
553
894
  detector_file = self.get_detector_data_file(detector_prefix)
554
- with h5py.File(detector_file) as h5_file:
895
+ with File(detector_file) as h5_file:
555
896
  num_image = h5_file['/entry/instrument/detector/data'].shape[0]
556
- return(num_image-self.starting_image_offset)
897
+ return num_image-self.starting_image_offset
898
+
557
899
  def get_detector_data_path(self):
558
- return(self.scan_path)
900
+ return self.scan_path
901
+
559
902
  def get_detector_data_file(self, detector_prefix):
560
903
  prefix = detector_prefix.upper()
561
904
  file_name = f'{self.scan_name}_{prefix}_{self.scan_number:03d}.h5'
562
905
  file_name_full = os.path.join(self.detector_data_path, file_name)
563
906
  if os.path.isfile(file_name_full):
564
- return(file_name_full)
565
- else:
566
- raise(RuntimeError(f'{self.scan_title}: could not find detector image file for '+
567
- f'detector {detector_prefix}'))
568
- #@cache
569
- def get_all_detector_data_in_file(self, detector_prefix, scan_step_index=None):
570
- import h5py
907
+ return file_name_full
908
+ raise RuntimeError(f'{self.scan_title}: could not find detector image '
909
+ f'file for detector {detector_prefix}')
910
+
911
+ def get_all_detector_data_in_file(
912
+ self, detector_prefix, scan_step_index=None):
913
+ # third party modules
914
+ from h5py import File
915
+
571
916
  detector_file = self.get_detector_data_file(detector_prefix)
572
- with h5py.File(detector_file) as h5_file:
917
+ with File(detector_file) as h5_file:
573
918
  if scan_step_index is None:
574
919
  detector_data = h5_file['/entry/instrument/detector/data'][
575
- self.starting_image_index:]
920
+ self.starting_image_index:]
576
921
  elif isinstance(scan_step_index, int):
577
922
  detector_data = h5_file['/entry/instrument/detector/data'][
578
- self.starting_image_index+scan_step_index]
579
- elif isinstance(scan_step_index, (list, tuple)) and len(scan_step_index) == 2:
923
+ self.starting_image_index+scan_step_index]
924
+ elif (isinstance(scan_step_index, (list, tuple))
925
+ and len(scan_step_index) == 2):
580
926
  detector_data = h5_file['/entry/instrument/detector/data'][
581
- self.starting_image_index+scan_step_index[0]:
582
- self.starting_image_index+scan_step_index[1]]
927
+ self.starting_image_index+scan_step_index[0]:
928
+ self.starting_image_index+scan_step_index[1]]
583
929
  else:
584
- raise(ValueError(f'Invalid parameter scan_step_index ({scan_step_index})'))
585
- return(detector_data)
930
+ raise ValueError('Invalid parameter scan_step_index '
931
+ f'({scan_step_index})')
932
+ return detector_data
933
+
586
934
  def get_detector_data(self, detector_prefix, scan_step_index=None):
587
- return(self.get_all_detector_data_in_file(detector_prefix, scan_step_index))
935
+ return self.get_all_detector_data_in_file(
936
+ detector_prefix, scan_step_index)
588
937
 
589
938
 
590
939
  class SMBRotationScanParser(RotationScanParser, SMBScanParser):
940
+ """Concrete implementation of a class representing a scan taken
941
+ with the typical tomography setup at SMB.
942
+ """
943
+
591
944
  def __init__(self, spec_file_name, scan_number):
592
945
  super().__init__(spec_file_name, scan_number)
946
+
593
947
  self.par_file_pattern = f'id*-*tomo*-{self.scan_name}'
948
+
594
949
  def get_spec_scan_npts(self):
595
- if self.spec_macro == 'slew_ome' or self.spec_macro == 'rams4_slew_ome':
596
- return(int(self.pars['nframes_real']))
597
- else:
598
- raise(RuntimeError(f'{self.scan_title}: cannot determine number of points for scans of type {self.spec_macro}'))
950
+ if self.spec_macro in ('slew_ome','rams4_slew_ome'):
951
+ return int(self.pars['nframes_real'])
952
+ raise RuntimeError(f'{self.scan_title}: cannot determine number of '
953
+ f'points for scans of type {self.spec_macro}')
954
+
599
955
  def get_scan_type(self):
600
- try:
601
- return(self.pars['tomo_type'])
602
- except:
603
- try:
604
- return(self.pars['tomotype'])
605
- except:
606
- raise(RuntimeError(f'{self.scan_title}: cannot determine the scan_type'))
956
+ scan_type = self.pars.get('tomo_type',
957
+ self.pars.get('tomotype', None))
958
+ if scan_type is None:
959
+ raise RuntimeError(f'{self.scan_title}: cannot determine '
960
+ 'the scan_type')
961
+ return scan_type
962
+
607
963
  def get_theta_vals(self):
608
- return({'start': float(self.pars['ome_start_real']),
609
- 'end': float(self.pars['ome_end_real']), 'num': int(self.pars['nframes_real'])})
964
+ return {'start': float(self.pars['ome_start_real']),
965
+ 'end': float(self.pars['ome_end_real']),
966
+ 'num': int(self.pars['nframes_real'])}
967
+
610
968
  def get_horizontal_shift(self):
611
- try:
612
- return(float(self.pars['rams4x']))
613
- except:
614
- try:
615
- return(float(self.pars['ramsx']))
616
- except:
617
- raise(RuntimeError(f'{self.scan_title}: cannot determine the horizontal shift'))
969
+ horizontal_shift = self.pars.get(
970
+ 'rams4x', self.pars.get('ramsx', None))
971
+ if horizontal_shift is None:
972
+ raise RuntimeError(
973
+ f'{self.scan_title}: cannot determine the horizontal shift')
974
+ return horizontal_shift
975
+
618
976
  def get_vertical_shift(self):
619
- try:
620
- return(float(self.pars['rams4z']))
621
- except:
622
- try:
623
- return(float(self.pars['ramsz']))
624
- except:
625
- raise(RuntimeError(f'{self.scan_title}: cannot determine the vertical shift'))
977
+ vertical_shift = self.pars.get(
978
+ 'rams4z', self.pars.get('ramsz', None))
979
+ if vertical_shift is None:
980
+ raise RuntimeError(
981
+ f'{self.scan_title}: cannot determine the vertical shift')
982
+ return vertical_shift
983
+
626
984
  def get_starting_image_index(self):
627
985
  try:
628
- return(int(self.pars['junkstart']))
986
+ return int(self.pars['junkstart'])
629
987
  except:
630
- raise(RuntimeError(f'{self.scan_title}: cannot determine first detector image index'))
988
+ raise RuntimeError(f'{self.scan_title}: cannot determine first '
989
+ 'detector image index')
990
+
631
991
  def get_starting_image_offset(self):
632
992
  try:
633
- return(int(self.pars['goodstart'])-self.get_starting_image_index())
993
+ return (int(self.pars['goodstart'])
994
+ - self.get_starting_image_index())
634
995
  except:
635
- raise(RuntimeError(f'{self.scan_title}: cannot determine index offset of first good '+
636
- 'detector image'))
996
+ raise RuntimeError(f'{self.scan_title}: cannot determine index '
997
+ 'offset of first good detector image')
998
+
637
999
  def get_num_image(self, detector_prefix=None):
638
1000
  try:
639
- return(int(self.pars['nframes_real']))
640
- # indexRegex = re.compile(r'\d+')
1001
+ return int(self.pars['nframes_real'])
1002
+ # index_regex = re.compile(r'\d+')
641
1003
  # # At this point only tiffs
642
1004
  # path = self.get_detector_data_path()
643
- # files = sorted([f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) and
644
- # f.endswith('.tif') and indexRegex.search(f)])
645
- # return(len(files)-self.starting_image_offset)
1005
+ # files = sorted([f for f in os.listdir(path) \
1006
+ # if os.path.isfile(os.path.join(path, f)) \
1007
+ # and f.endswith('.tif') \
1008
+ # and index_regex.search(f)])
1009
+ # return len(files)-self.starting_image_offset
646
1010
  except:
647
- raise(RuntimeError(f'{self.scan_title}: cannot determine the number of good '+
648
- 'detector images'))
1011
+ raise RuntimeError(f'{self.scan_title}: cannot determine the '
1012
+ 'number of good detector images')
1013
+
649
1014
  def get_detector_data_path(self):
650
- return(os.path.join(self.scan_path, str(self.scan_number), 'nf'))
1015
+ return os.path.join(self.scan_path, str(self.scan_number), 'nf')
1016
+
651
1017
  def get_detector_data_file(self, scan_step_index:int):
652
1018
  file_name = f'nf_{self.starting_image_index+scan_step_index:06d}.tif'
653
1019
  file_name_full = os.path.join(self.detector_data_path, file_name)
654
1020
  if os.path.isfile(file_name_full):
655
- return(file_name_full)
656
- else:
657
- raise(RuntimeError(f'{self.scan_title}: could not find detector image file for '+
658
- f'scan step ({scan_step_index})'))
1021
+ return file_name_full
1022
+ raise RuntimeError(f'{self.scan_title}: could not find detector image '
1023
+ f'file for scan step ({scan_step_index})')
1024
+
659
1025
  def get_detector_data(self, detector_prefix, scan_step_index=None):
660
1026
  if scan_step_index is None:
661
1027
  detector_data = []
662
1028
  for index in range(len(self.get_num_image(detector_prefix))):
663
- detector_data.append(self.get_detector_data(detector_prefix, index))
1029
+ detector_data.append(
1030
+ self.get_detector_data(detector_prefix, index))
664
1031
  detector_data = np.asarray(detector_data)
665
1032
  elif isinstance(scan_step_index, int):
666
- image_file = self.get_detector_data_file(scan_step_index)
1033
+ # third party modules
667
1034
  from pyspec.file.tiff import TiffFile
1035
+
1036
+ image_file = self.get_detector_data_file(scan_step_index)
668
1037
  with TiffFile(image_file) as tiff_file:
669
1038
  detector_data = tiff_file.asarray()
670
- elif isinstance(scan_step_index, (list, tuple)) and len(scan_step_index) == 2:
1039
+ elif (isinstance(scan_step_index, (list, tuple))
1040
+ and len(scan_step_index) == 2):
671
1041
  detector_data = []
672
1042
  for index in range(scan_step_index[0], scan_step_index[1]):
673
- detector_data.append(self.get_detector_data(detector_prefix, index))
1043
+ detector_data.append(
1044
+ self.get_detector_data(detector_prefix, index))
674
1045
  detector_data = np.asarray(detector_data)
675
1046
  else:
676
- raise(ValueError(f'Invalid parameter scan_step_index ({scan_step_index})'))
677
- return(detector_data)
1047
+ raise ValueError('Invalid parameter scan_step_index '
1048
+ f'({scan_step_index})')
1049
+ return detector_data
678
1050
 
679
1051
 
680
1052
  class MCAScanParser(ScanParser):
1053
+ """Partial implementation of a class representing a scan taken
1054
+ while collecting SPEC MCA data.
1055
+ """
1056
+
681
1057
  def __init__(self, spec_file_name, scan_number):
682
1058
  super().__init__(spec_file_name, scan_number)
683
-
1059
+
684
1060
  self._dwell_time = None
685
1061
  self._detector_num_bins = None
686
-
1062
+
687
1063
  @property
688
1064
  def dwell_time(self):
689
1065
  if self._dwell_time is None:
690
1066
  self._dwell_time = self.get_dwell_time()
691
- return(self._dwell_time)
692
-
1067
+ return self._dwell_time
1068
+
693
1069
  def get_dwell_time(self):
694
- raise(NotImplementedError)
695
- @cache
1070
+ """Return the dwell time for each scan point as it appears in
1071
+ the SPEC command.
1072
+
1073
+ :rtype: float
1074
+ """
1075
+ raise NotImplementedError
1076
+
696
1077
  def get_detector_num_bins(self, detector_prefix):
697
- raise(NotImplementedError)
1078
+ """Return the number of bins for the detector with the given
1079
+ prefix.
1080
+
1081
+ :param detector_prefix: the detector prefix as used in SPEC
1082
+ MCA data files
1083
+ :type detector_prefix: str
1084
+ :rtype: int
1085
+ """
1086
+ raise NotImplementedError
1087
+
698
1088
 
699
1089
  class SMBMCAScanParser(MCAScanParser, SMBScanParser):
700
- def __init__(self, spec_file_name, scan_number):
701
- super().__init__(spec_file_name, scan_number)
702
-
1090
+ """Concrete implementation of a class representing a scan taken
1091
+ with the typical EDD setup at SMB or FAST.
1092
+ """
1093
+
703
1094
  def get_spec_scan_npts(self):
704
1095
  if self.spec_macro == 'tseries':
705
- return(1)
706
- elif self.spec_macro == 'ascan':
707
- return(int(self.spec_args[3]))
708
- elif self.spec_scan == 'wbslew_scan':
709
- return(1)
710
- else:
711
- raise(RuntimeError(f'{self.scan_title}: cannot determine number of points for scans of type {self.spec_macro}'))
1096
+ return 1
1097
+ if self.spec_macro == 'ascan':
1098
+ return int(self.spec_args[3])
1099
+ if self.spec_scan == 'wbslew_scan':
1100
+ return 1
1101
+ raise RuntimeError(f'{self.scan_title}: cannot determine number of '
1102
+ f'points for scans of type {self.spec_macro}')
712
1103
 
713
1104
  def get_dwell_time(self):
714
1105
  if self.spec_macro == 'tseries':
715
- return(float(self.spec_args[1]))
716
- elif self.spec_macro == 'ascan':
717
- return(float(self.spec_args[4]))
718
- elif self.spec_macro == 'wbslew_scan':
719
- return(float(self.spec_args[3]))
720
- else:
721
- raise(RuntimeError(f'{self.scan_title}: cannot determine dwell time for scans of type {self.spec_macro}'))
1106
+ return float(self.spec_args[1])
1107
+ if self.spec_macro == 'ascan':
1108
+ return float(self.spec_args[4])
1109
+ if self.spec_macro == 'wbslew_scan':
1110
+ return float(self.spec_args[3])
1111
+ raise RuntimeError(f'{self.scan_title}: cannot determine dwell time '
1112
+ f'for scans of type {self.spec_macro}')
722
1113
 
723
1114
  def get_detector_num_bins(self, detector_prefix):
724
- with open(self.get_detector_file(detector_prefix)) as detector_file:
1115
+ with open(self.get_detector_data_file(detector_prefix)) \
1116
+ as detector_file:
725
1117
  lines = detector_file.readlines()
726
1118
  for line in lines:
727
1119
  if line.startswith('#@CHANN'):
728
1120
  try:
729
- line_prefix, number_saved, first_saved, last_saved, reduction_coef = line.split()
730
- return(int(number_saved))
1121
+ line_prefix, number_saved, first_saved, last_saved, \
1122
+ reduction_coef = line.split()
1123
+ return int(number_saved)
731
1124
  except:
732
1125
  continue
733
- raise(RuntimeError(f'{self.scan_title}: could not find num_bins for detector {detector_prefix}'))
734
-
1126
+ raise RuntimeError(f'{self.scan_title}: could not find num_bins for '
1127
+ f'detector {detector_prefix}')
1128
+
735
1129
  def get_detector_data_path(self):
736
- return(self.scan_path)
1130
+ return self.scan_path
737
1131
 
738
- def get_detector_file(self, detector_prefix, scan_step_index:int=0):
1132
+ def get_detector_data_file(self, detector_prefix, scan_step_index=0):
739
1133
  file_name = f'spec.log.scan{self.scan_number}.mca1.mca'
740
1134
  file_name_full = os.path.join(self.detector_data_path, file_name)
741
1135
  if os.path.isfile(file_name_full):
742
- return(file_name_full)
743
- else:
744
- raise(RuntimeError(f'{self.scan_title}: could not find detector image file'))
1136
+ return file_name_full
1137
+ raise RuntimeError(
1138
+ f'{self.scan_title}: could not find detector image file')
745
1139
 
746
- @cache
747
1140
  def get_all_detector_data(self, detector_prefix):
748
- # This should be easy with pyspec, but there are bugs in pyspec for MCA data.....
749
- # or is the 'bug' from a nonstandard implementation of some macro on our end?
750
- # According to spec manual and pyspec code, mca data should always begin w/ '@A'
1141
+ # This should be easy with pyspec, but there are bugs in
1142
+ # pyspec for MCA data..... or is the 'bug' from a nonstandard
1143
+ # implementation of some macro on our end? According to spec
1144
+ # manual and pyspec code, mca data should always begin w/ '@A'
751
1145
  # In example scans, it begins with '@mca1' instead
752
1146
  data = []
753
-
754
- with open(self.get_detector_file(detector_prefix)) as detector_file:
1147
+
1148
+ with open(self.get_detector_data_file(detector_prefix)) \
1149
+ as detector_file:
755
1150
  lines = [line.strip("\\\n") for line in detector_file.readlines()]
756
1151
 
757
1152
  num_bins = self.get_detector_num_bins(detector_prefix)
@@ -766,20 +1161,21 @@ class SMBMCAScanParser(MCAScanParser, SMBScanParser):
766
1161
  spectrum = np.zeros(num_bins)
767
1162
  if counter == 1:
768
1163
  b = np.array(a[1:]).astype('uint16')
769
- spectrum[(counter-1)*25:((counter-1)*25+25)] = b
1164
+ spectrum[(counter-1) * 25:((counter-1) * 25 + 25)] = b
770
1165
  counter = counter + 1
771
- elif counter > 1 and counter <= (np.floor(num_bins/25.)):
1166
+ elif counter > 1 and counter <= (np.floor(num_bins / 25.)):
772
1167
  b = np.array(a).astype('uint16')
773
- spectrum[(counter-1)*25:((counter-1)*25+25)] = b
774
- counter = counter+1
1168
+ spectrum[(counter-1) * 25:((counter-1) * 25 + 25)] = b
1169
+ counter = counter + 1
775
1170
  elif counter == (np.ceil(num_bins/25.)):
776
1171
  b = np.array(a).astype('uint16')
777
- spectrum[(counter-1)*25:((counter-1)*25+(np.mod(num_bins,25)))] = b
1172
+ spectrum[(counter-1) * 25:
1173
+ ((counter-1) * 25 + (np.mod(num_bins, 25)))] = b
778
1174
  data.append(spectrum)
779
1175
  counter = 0
780
1176
 
781
- return(data)
1177
+ return data
782
1178
 
783
1179
  def get_detector_data(self, detector_prefix, scan_step_index:int):
784
1180
  detector_data = self.get_all_detector_data(detector_prefix)
785
- return(detector_data[scan_step_index])
1181
+ return detector_data[scan_step_index]