ChessAnalysisPipeline 0.0.13__py3-none-any.whl → 0.0.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

CHAP/common/models/map.py CHANGED
@@ -32,7 +32,7 @@ class Sample(BaseModel):
32
32
  :ivar name: The name of the sample.
33
33
  :type name: str
34
34
  :ivar description: A description of the sample.
35
- :type description: Optional[str]
35
+ :type description: str, optional
36
36
  """
37
37
  name: constr(min_length=1)
38
38
  description: Optional[str]
@@ -45,9 +45,12 @@ class SpecScans(BaseModel):
45
45
  :type spec_file: str
46
46
  :ivar scan_numbers: List of scan numbers to use.
47
47
  :type scan_numbers: list[int]
48
+ :ivar par_file: Path to a non-default SMB par file.
49
+ :type par_file: str, optional
48
50
  """
49
51
  spec_file: FilePath
50
52
  scan_numbers: conlist(item_type=conint(gt=0), min_items=1)
53
+ par_file: Optional[FilePath]
51
54
 
52
55
  @validator('spec_file', allow_reuse=True)
53
56
  def validate_spec_file(cls, spec_file, values):
@@ -55,6 +58,8 @@ class SpecScans(BaseModel):
55
58
 
56
59
  :param spec_file: Path to the SPEC file.
57
60
  :type spec_file: str
61
+ :param values: Dictionary of validated class field values.
62
+ :type values: dict
58
63
  :raises ValueError: If the SPEC file is invalid.
59
64
  :return: Absolute path to the SPEC file, if it is valid.
60
65
  :rtype: str
@@ -72,7 +77,7 @@ class SpecScans(BaseModel):
72
77
 
73
78
  :param scan_numbers: List of scan numbers.
74
79
  :type scan_numbers: list of int
75
- :param values: Dictionary of values for all fields of the model.
80
+ :param values: Dictionary of validated class field values.
76
81
  :type values: dict
77
82
  :raises ValueError: If a specified scan number is not found in
78
83
  the SPEC file.
@@ -95,6 +100,25 @@ class SpecScans(BaseModel):
95
100
  f'No scan number {scan_number} in {spec_file}')
96
101
  return scan_numbers
97
102
 
103
+ @validator('par_file', allow_reuse=True)
104
+ def validate_par_file(cls, par_file, values):
105
+ """Validate the specified SMB par file.
106
+
107
+ :param par_file: Path to a non-default SMB par file.
108
+ :type par_file: str
109
+ :param values: Dictionary of validated class field values.
110
+ :type values: dict
111
+ :raises ValueError: If the SMB par file is invalid.
112
+ :return: Absolute path to the SMB par file, if it is valid.
113
+ :rtype: str
114
+ """
115
+ if par_file is None or not par_file:
116
+ return ''
117
+ par_file = os.path.abspath(par_file)
118
+ if not os.path.isfile(par_file):
119
+ raise ValueError(f'Invalid SMB par file {par_file}')
120
+ return par_file
121
+
98
122
  @property
99
123
  def scanparsers(self):
100
124
  """A list of `ScanParser`s for each of the scans specified by
@@ -107,25 +131,29 @@ class SpecScans(BaseModel):
107
131
  """This method returns a `ScanParser` for the specified scan
108
132
  number in the specified SPEC file.
109
133
 
110
- :param scan_number: Scan number to get a `ScanParser` for
134
+ :param scan_number: Scan number to get a `ScanParser` for.
111
135
  :type scan_number: int
112
- :return: `ScanParser` for the specified scan number
136
+ :return: `ScanParser` for the specified scan number.
113
137
  :rtype: ScanParser
114
138
  """
115
- return get_scanparser(self.spec_file, scan_number)
139
+ if self.par_file:
140
+ return get_scanparser(
141
+ self.spec_file, scan_number, par_file=self.par_file)
142
+ else:
143
+ return get_scanparser(self.spec_file, scan_number)
116
144
 
117
145
  def get_index(self, scan_number:int, scan_step_index:int, map_config):
118
146
  """This method returns a tuple representing the index of a
119
147
  specific step in a specific SPEC scan within a map.
120
148
 
121
- :param scan_number: Scan number to get index for
149
+ :param scan_number: Scan number to get index for.
122
150
  :type scan_number: int
123
- :param scan_step_index: Scan step index to get index for
151
+ :param scan_step_index: Scan step index to get index for.
124
152
  :type scan_step_index: int
125
- :param map_config: Map configuration to get index for
153
+ :param map_config: Map configuration to get index for.
126
154
  :type map_config: MapConfig
127
155
  :return: Index for the specified scan number and scan step
128
- index within the specified map configuration
156
+ index within the specified map configuration.
129
157
  :rtype: tuple
130
158
  """
131
159
  index = ()
@@ -133,7 +161,8 @@ class SpecScans(BaseModel):
133
161
  coordinate_index = list(
134
162
  map_config.coords[independent_dimension.label]).index(
135
163
  independent_dimension.get_value(
136
- self, scan_number, scan_step_index))
164
+ self, scan_number, scan_step_index,
165
+ map_config.scalar_data))
137
166
  index = (coordinate_index, *index)
138
167
  return index
139
168
 
@@ -144,14 +173,15 @@ class SpecScans(BaseModel):
144
173
  """Return the raw data from the specified detectors at the
145
174
  specified scan number and scan step index.
146
175
 
147
- :param detectors: List of detector prefixes to get raw data for
176
+ :param detectors: List of detector prefixes to get raw data
177
+ for.
148
178
  :type detectors: list[str]
149
- :param scan_number: Scan number to get data for
179
+ :param scan_number: Scan number to get data for.
150
180
  :type scan_number: int
151
- :param scan_step_index: Scan step index to get data for
181
+ :param scan_step_index: Scan step index to get data for.
152
182
  :type scan_step_index: int
153
183
  :return: Data from the specified detectors for the specified
154
- scan number and scan step index
184
+ scan number and scan step index.
155
185
  :rtype: list[np.ndarray]
156
186
  """
157
187
  return get_detector_data(
@@ -169,10 +199,13 @@ def get_available_scan_numbers(spec_file:str):
169
199
 
170
200
 
171
201
  @cache
172
- def get_scanparser(spec_file:str, scan_number:int):
202
+ def get_scanparser(spec_file:str, scan_number:int, par_file=None):
173
203
  if scan_number not in get_available_scan_numbers(spec_file):
174
204
  return None
175
- return ScanParser(spec_file, scan_number)
205
+ if par_file is None:
206
+ return ScanParser(spec_file, scan_number)
207
+ else:
208
+ return ScanParser(spec_file, scan_number, par_file=par_file)
176
209
 
177
210
 
178
211
  @lru_cache(maxsize=10)
@@ -207,7 +240,8 @@ class PointByPointScanData(BaseModel):
207
240
  """
208
241
  label: constr(min_length=1)
209
242
  units: constr(strip_whitespace=True, min_length=1)
210
- data_type: Literal['spec_motor', 'scan_column', 'smb_par']
243
+ data_type: Literal['spec_motor', 'spec_motor_absolute', 'scan_column',
244
+ 'smb_par', 'expression']
211
245
  name: constr(strip_whitespace=True, min_length=1)
212
246
 
213
247
  @validator('label')
@@ -216,7 +250,7 @@ class PointByPointScanData(BaseModel):
216
250
  any of the values for `label` reserved for certain data needed
217
251
  to perform corrections.
218
252
 
219
- :param label: The value of `label` to validate
253
+ :param label: The value of `label` to validate.
220
254
  :type label: str
221
255
  :raises ValueError: If `label` is one of the reserved values.
222
256
  :return: The original supplied value `label`, if it is
@@ -239,8 +273,6 @@ class PointByPointScanData(BaseModel):
239
273
  :raises TypeError: If the station is not compatible with the
240
274
  value of the `data_type` attribute for this instance of
241
275
  PointByPointScanData.
242
- :return: None
243
- :rtype: None
244
276
  """
245
277
  if (station.lower() not in ('id1a3', 'id3a')
246
278
  and self.data_type == 'smb_par'):
@@ -256,15 +288,13 @@ class PointByPointScanData(BaseModel):
256
288
 
257
289
  :param spec_scans: A list of `SpecScans` whose raw data will
258
290
  be checked for the presence of the data represented by
259
- this instance of `PointByPointScanData`
291
+ this instance of `PointByPointScanData`.
260
292
  :type spec_scans: list[SpecScans]
261
293
  :param scan_step_index: A specific scan step index to validate,
262
294
  defaults to `'all'`.
263
295
  :type scan_step_index: Union[Literal['all'],int], optional
264
296
  :raises RuntimeError: If the data represented by this instance of
265
297
  `PointByPointScanData` is missing for the specified scan steps.
266
- :return: None
267
- :rtype: None
268
298
  """
269
299
  for scans in spec_scans:
270
300
  for scan_number in scans.scan_numbers:
@@ -285,8 +315,48 @@ class PointByPointScanData(BaseModel):
285
315
  f'for index {index} '
286
316
  f'in spec file {scans.spec_file}')
287
317
 
288
- def get_value(self, spec_scans:SpecScans,
289
- scan_number:int, scan_step_index:int=0):
318
+ def validate_for_scalar_data(self, scalar_data):
319
+ """Used for `PointByPointScanData` objects with a `data_type`
320
+ of `'expression'`. Validate that the `scalar_data` field of a
321
+ `MapConfig` object contains all the items necessary for
322
+ evaluating the expression.
323
+
324
+ :param scalar_data: the `scalar_data` field of a `MapConfig`
325
+ that this `PointByPointScanData` object will be validated
326
+ against
327
+ :type scalar_data: list[PointByPointScanData]
328
+ :raises ValueError: if `scalar_data` does not contain items
329
+ needed for evaluating the expression.
330
+ :return: None
331
+ """
332
+ from ast import parse
333
+ from asteval import get_ast_names
334
+
335
+ labels = get_ast_names(parse(self.name))
336
+ for label in ('round', 'np', 'numpy'):
337
+ try:
338
+ labels.remove(label)
339
+ except:
340
+ pass
341
+ for l in labels:
342
+ if l == 'round':
343
+ symtable[l] = round
344
+ continue
345
+ if l in ('np', 'numpy'):
346
+ symtable[l] = np
347
+ continue
348
+ label_found = False
349
+ for s_d in scalar_data:
350
+ if s_d.label == l:
351
+ label_found = True
352
+ break
353
+ if not label_found:
354
+ raise ValueError(
355
+ f'{l} is not the label of an item in scalar_data')
356
+
357
+ def get_value(
358
+ self, spec_scans:SpecScans, scan_number:int, scan_step_index:int=0,
359
+ scalar_data=[], relative=True, ndigits=None):
290
360
  """Return the value recorded for this instance of
291
361
  `PointByPointScanData` at a specific scan step.
292
362
 
@@ -296,18 +366,32 @@ class PointByPointScanData(BaseModel):
296
366
  :param scan_number: The number of the scan in which the
297
367
  requested scan step occurs.
298
368
  :type scan_number: int
299
- :param scan_step_index: The index of the requested scan step.
300
- :type scan_step_index: int
369
+ :param scan_step_index: The index of the requested scan step,
370
+ defaults to `0`.
371
+ :type scan_step_index: int, optional
372
+ :param scalar_data: list of scalar data configurations used to
373
+ get values for `PointByPointScanData` objects with
374
+ `data_type == 'expression'`, defaults to `[]`.
375
+ :type scalar_data: list[PointByPointScanData], optional
376
+ :param relative: Whether to return a relative value or not,
377
+ defaults to `True` (only applies to SPEC motor values).
378
+ :type relative: bool, optional
379
+ :params ndigits: Round SPEC motor values to the specified
380
+ number of decimals if set, defaults to `None`.
381
+ :type ndigits: int, optional
301
382
  :return: The value recorded of the data represented by this
302
383
  instance of `PointByPointScanData` at the scan step
303
- requested
384
+ requested.
304
385
  :rtype: float
305
386
  """
306
- if self.data_type == 'spec_motor':
387
+ if 'spec_motor' in self.data_type:
388
+ if 'absolute' in self.data_type:
389
+ relative = False
307
390
  return get_spec_motor_value(spec_scans.spec_file,
308
391
  scan_number,
309
392
  scan_step_index,
310
- self.name)
393
+ self.name,
394
+ relative, ndigits)
311
395
  if self.data_type == 'scan_column':
312
396
  return get_spec_counter_value(spec_scans.spec_file,
313
397
  scan_number,
@@ -317,12 +401,19 @@ class PointByPointScanData(BaseModel):
317
401
  return get_smb_par_value(spec_scans.spec_file,
318
402
  scan_number,
319
403
  self.name)
404
+ elif self.data_type == 'expression':
405
+ return get_expression_value(spec_scans,
406
+ scan_number,
407
+ scan_step_index,
408
+ self.name,
409
+ scalar_data)
320
410
  return None
321
411
 
322
412
 
323
413
  @cache
324
414
  def get_spec_motor_value(spec_file:str, scan_number:int,
325
- scan_step_index:int, spec_mnemonic:str):
415
+ scan_step_index:int, spec_mnemonic:str,
416
+ relative=True, ndigits=None):
326
417
  """Return the value recorded for a SPEC motor at a specific scan
327
418
  step.
328
419
 
@@ -336,7 +427,13 @@ def get_spec_motor_value(spec_file:str, scan_number:int,
336
427
  :type scan_step_index: int
337
428
  :param spec_mnemonic: The menmonic of a SPEC motor.
338
429
  :type spec_mnemonic: str
339
- :return: The value of the motor at the scan step requested
430
+ :param relative: Whether to return a relative value or not,
431
+ defaults to `True`.
432
+ :type relative: bool, optional
433
+ :params ndigits: Round SPEC motor values to the specified
434
+ number of decimals if set, defaults to `None`.
435
+ :type ndigits: int, optional
436
+ :return: The value of the motor at the scan step requested.
340
437
  :rtype: float
341
438
  """
342
439
  scanparser = get_scanparser(spec_file, scan_number)
@@ -349,11 +446,15 @@ def get_spec_motor_value(spec_file:str, scan_number:int,
349
446
  scanparser.spec_scan_shape,
350
447
  order='F')
351
448
  motor_value = \
352
- scanparser.spec_scan_motor_vals[motor_i][scan_step[motor_i]]
449
+ scanparser.get_spec_scan_motor_vals(
450
+ relative)[motor_i][scan_step[motor_i]]
353
451
  else:
354
- motor_value = scanparser.spec_scan_motor_vals[motor_i]
452
+ motor_value = scanparser.get_spec_scan_motor_vals(
453
+ relative)[motor_i]
355
454
  else:
356
455
  motor_value = scanparser.get_spec_positioner_value(spec_mnemonic)
456
+ if ndigits is not None:
457
+ motor_value = round(motor_value, 3)
357
458
  return motor_value
358
459
 
359
460
 
@@ -373,7 +474,7 @@ def get_spec_counter_value(spec_file:str, scan_number:int,
373
474
  :type scan_step_index: int
374
475
  :param spec_column_label: The label of a SPEC data column.
375
476
  :type spec_column_label: str
376
- :return: The value of the counter at the scan step requested
477
+ :return: The value of the counter at the scan step requested.
377
478
  :rtype: float
378
479
  """
379
480
  scanparser = get_scanparser(spec_file, scan_number)
@@ -393,7 +494,7 @@ def get_smb_par_value(spec_file:str, scan_number:int, par_name:str):
393
494
  :param scan_number: The number of the scan in which the requested
394
495
  scan step occurs.
395
496
  :type scan_number: int
396
- :param par_name: The name of the column in the .par file
497
+ :param par_name: The name of the column in the .par file.
397
498
  :type par_name: str
398
499
  :return: The value of the .par file value for the scan requested.
399
500
  :rtype: float
@@ -402,24 +503,72 @@ def get_smb_par_value(spec_file:str, scan_number:int, par_name:str):
402
503
  return scanparser.pars[par_name]
403
504
 
404
505
 
506
+ def get_expression_value(spec_scans:SpecScans, scan_number:int,
507
+ scan_step_index:int, expression:str,
508
+ scalar_data:list[PointByPointScanData]):
509
+ """Return the value of an evaluated expression of other sources of
510
+ point-by-point scalar scan data for a single point.
511
+
512
+ :param spec_scans: An instance of `SpecScans` in which the
513
+ requested scan step occurs.
514
+ :type spec_scans: SpecScans
515
+ :param scan_number: The number of the scan in which the requested
516
+ scan step occurs.
517
+ :type scan_number: int
518
+ :param scan_step_index: The index of the requested scan step.
519
+ :type scan_step_index: int
520
+ :param expression: the string expression to evaluate
521
+ :type expression: str
522
+ :param scalar_data: the `scalar_data` field of a `MapConfig`
523
+ object (used to provide values for variables used in
524
+ `expression`)
525
+ :type scalar_data: list[PointByPointScanData]
526
+ :return: The value of the .par file value for the scan requested.
527
+ :rtype: float
528
+ """
529
+ from ast import parse
530
+ from asteval import get_ast_names, Interpreter
531
+ labels = get_ast_names(parse(expression))
532
+ symtable = {}
533
+ for l in labels:
534
+ if l == 'round':
535
+ symtable[l] = round
536
+ for s_d in scalar_data:
537
+ if s_d.label == l:
538
+ symtable[l] = s_d.get_value(
539
+ spec_scans, scan_number, scan_step_index, scalar_data)
540
+ aeval = Interpreter(symtable=symtable)
541
+ return aeval(expression)
542
+
405
543
  def validate_data_source_for_map_config(data_source, values):
406
544
  """Confirm that an instance of PointByPointScanData is valid for
407
545
  the station and scans provided by a map configuration dictionary.
408
546
 
409
- :param data_source: the input object to validate
547
+ :param data_source: The input object to validate.
410
548
  :type data_source: PintByPointScanData
411
- :param values: the map configuration dictionary
549
+ :param values: The map configuration dictionary.
412
550
  :type values: dict
413
- :raises Exception: if `data_source` cannot be validated for
551
+ :raises Exception: If `data_source` cannot be validated for
414
552
  `values`.
415
- :return: `data_source`, iff it is valid.
553
+ :return: `data_source`, if it is valid.
416
554
  :rtype: PointByPointScanData
417
555
  """
418
- if data_source is not None:
419
- import_scanparser(values.get('station'), values.get('experiment_type'))
420
- data_source.validate_for_station(values.get('station'))
421
- data_source.validate_for_spec_scans(values.get('spec_scans'))
422
- return data_source
556
+ def _validate_data_source_for_map_config(
557
+ data_source, values, parent_list=None):
558
+ if isinstance(data_source, list):
559
+ return [_validate_data_source_for_map_config(
560
+ d_s, values, parent_list=data_source) for d_s in data_source]
561
+ if data_source is not None:
562
+ if data_source.data_type == 'expression':
563
+ data_source.validate_for_scalar_data(
564
+ values.get('scalar_data', parent_list))
565
+ else:
566
+ import_scanparser(
567
+ values.get('station'), values.get('experiment_type'))
568
+ data_source.validate_for_station(values.get('station'))
569
+ data_source.validate_for_spec_scans(values.get('spec_scans'))
570
+ return(data_source)
571
+ return _validate_data_source_for_map_config(data_source, values)
423
572
 
424
573
 
425
574
  class IndependentDimension(PointByPointScanData):
@@ -437,15 +586,15 @@ class IndependentDimension(PointByPointScanData):
437
586
  :ivar name: Represents the name with which these raw data were
438
587
  recorded at time of data collection.
439
588
  :type name: str
440
- :param start: Sarting index for slicing all datasets of a
441
- `MapConfig` along this axis, defaults to 0
589
+ :ivar start: Sarting index for slicing all datasets of a
590
+ `MapConfig` along this axis, defaults to `0`.
442
591
  :type start: int, optional
443
- :param end: Ending index for slicing all datasets of a `MapConfig`
592
+ :ivar end: Ending index for slicing all datasets of a `MapConfig`
444
593
  along this axis, defaults to the total number of unique values
445
- along this axis in the associated `MapConfig`
594
+ along this axis in the associated `MapConfig`.
446
595
  :type end: int, optional
447
- :param step: Step for slicing all datasets of a `MapConfig` along
448
- this axis, defaults to 1
596
+ :ivar step: Step for slicing all datasets of a `MapConfig` along
597
+ this axis, defaults to `1`.
449
598
  :type step: int, optional
450
599
  """
451
600
  start: Optional[conint(ge=0)] = 0
@@ -489,20 +638,20 @@ class CorrectionsData(PointByPointScanData):
489
638
  """Return a list of all the labels reserved for
490
639
  corrections-related scalar data.
491
640
 
492
- :return: A list of reserved labels
641
+ :return: A list of reserved labels.
493
642
  :rtype: list[str]
494
643
  """
495
- return list(cls.__fields__['label'].type_.__args__)
644
+ return list((*cls.__fields__['label'].type_.__args__, 'round'))
496
645
 
497
646
 
498
647
  class PresampleIntensity(CorrectionsData):
499
648
  """Class representing a source of raw data for the intensity of
500
649
  the beam that is incident on the sample.
501
650
 
502
- :ivar label: Must be `"presample_intensity"`
503
- :type label: Literal["presample_intensity"]
504
- :ivar units: Must be `"counts"`
505
- :type units: Literal["counts"]
651
+ :ivar label: Must be `'presample_intensity"`.
652
+ :type label: Literal['presample_intensity']
653
+ :ivar units: Must be `'counts'`.
654
+ :type units: Literal['counts']
506
655
  :ivar data_type: Represents how these data were recorded at time
507
656
  of data collection.
508
657
  :type data_type: Literal['scan_column', 'smb_par']
@@ -518,10 +667,10 @@ class PostsampleIntensity(CorrectionsData):
518
667
  """Class representing a source of raw data for the intensity of
519
668
  the beam that has passed through the sample.
520
669
 
521
- :ivar label: Must be `"postsample_intensity"`
522
- :type label: Literal["postsample_intensity"]
523
- :ivar units: Must be `"counts"`
524
- :type units: Literal["counts"]
670
+ :ivar label: Must be `'postsample_intensity'`.
671
+ :type label: Literal['postsample_intensity']
672
+ :ivar units: Must be `'counts'`.
673
+ :type units: Literal['counts']
525
674
  :ivar data_type: Represents how these data were recorded at time
526
675
  of data collection.
527
676
  :type data_type: Literal['scan_column', 'smb_par']
@@ -539,10 +688,10 @@ class DwellTimeActual(CorrectionsData):
539
688
  can vary slightly point-to-point from the dwell time specified in
540
689
  the command).
541
690
 
542
- :ivar label: Must be `"dwell_time_actual"`
543
- :type label: Literal["dwell_time_actual"]
544
- :ivar units: Must be `"counts"`
545
- :type units: Literal["counts"]
691
+ :ivar label: Must be `'dwell_time_actual'`.
692
+ :type label: Literal['dwell_time_actual']
693
+ :ivar units: Must be `'counts'`.
694
+ :type units: Literal['counts']
546
695
  :ivar data_type: Represents how these data were recorded at time
547
696
  of data collection.
548
697
  :type data_type: Literal['scan_column', 'smb_par']
@@ -559,11 +708,11 @@ class SpecConfig(BaseModel):
559
708
 
560
709
  :ivar station: The name of the station at which the data was
561
710
  collected.
562
- :type station: Literal['id1a3','id3a','id3b']
711
+ :type station: Literal['id1a3', 'id3a', 'id3b']
563
712
  :ivar spec_scans: A list of the SPEC scans that compose the set.
564
713
  :type spec_scans: list[SpecScans]
565
714
  """
566
- station: Literal['id1a3','id3a','id3b']
715
+ station: Literal['id1a3', 'id3a', 'id3b']
567
716
  experiment_type: Literal['SAXSWAXS', 'EDD', 'XRF', 'TOMO']
568
717
  spec_scans: conlist(item_type=SpecScans, min_items=1)
569
718
 
@@ -572,7 +721,7 @@ class SpecConfig(BaseModel):
572
721
  """Ensure that a valid configuration was provided and finalize
573
722
  spec_file filepaths.
574
723
 
575
- :param values: Dictionary of class field values.
724
+ :param values: Dictionary of validated class field values.
576
725
  :type values: dict
577
726
  :return: The validated list of `values`.
578
727
  :rtype: dict
@@ -593,6 +742,14 @@ class SpecConfig(BaseModel):
593
742
  def validate_experiment_type(cls, value, values):
594
743
  """Ensure values for the station and experiment_type fields are
595
744
  compatible
745
+
746
+ :param value: Field value to validate (`experiment_type`).
747
+ :type value: str
748
+ :param values: Dictionary of validated class field values.
749
+ :type values: dict
750
+ :raises ValueError: Invalid experiment type.
751
+ :return: The validated field for `experiment_type`.
752
+ :rtype: str
596
753
  """
597
754
  station = values.get('station')
598
755
  if station == 'id1a3':
@@ -620,7 +777,7 @@ class MapConfig(BaseModel):
620
777
  :type title: str
621
778
  :ivar station: The name of the station at which the map was
622
779
  collected.
623
- :type station: Literal['id1a3','id3a','id3b']
780
+ :type station: Literal['id1a3', 'id3a', 'id3b']
624
781
  :ivar spec_scans: A list of the SPEC scans that compose the map.
625
782
  :type spec_scans: list[SpecScans]
626
783
  :ivar independent_dimensions: A list of the sources of data
@@ -630,27 +787,27 @@ class MapConfig(BaseModel):
630
787
  :ivar presample_intensity: A source of point-by-point presample
631
788
  beam intensity data. Required when applying a CorrectionConfig
632
789
  tool.
633
- :type presample_intensity: Optional[PresampleIntensity]
790
+ :type presample_intensity: PresampleIntensity, optional
634
791
  :ivar dwell_time_actual: A source of point-by-point actual dwell
635
792
  times for SPEC scans. Required when applying a
636
793
  CorrectionConfig tool.
637
- :type dwell_time_actual: Optional[DwellTimeActual]
638
- :ivar presample_intensity: A source of point-by-point postsample
794
+ :type dwell_time_actual: DwellTimeActual, optional
795
+ :ivar postsample_intensity: A source of point-by-point postsample
639
796
  beam intensity data. Required when applying a CorrectionConfig
640
- tool with `correction_type="flux_absorption"` or
641
- `correction_type="flux_absorption_background"`.
642
- :type presample_intensity: Optional[PresampleIntensity]
797
+ tool with `correction_type='flux_absorption'` or
798
+ `correction_type='flux_absorption_background'`.
799
+ :type postsample_intensity: PresampleIntensity, optional
643
800
  :ivar scalar_data: A list of the sources of data representing
644
801
  other scalar raw data values collected at each point on the
645
802
  map. In the NeXus file representation of the map, datasets for
646
- these values will be included.
647
- :type scalar_values: Optional[list[PointByPointScanData]]
803
+ these values will be included, defaults to `[]`.
804
+ :type scalar_data: list[PointByPointScanData], optional
648
805
  :ivar map_type: Type of map, structured or unstructured,
649
806
  defaults to `'structured'`.
650
- :type map_type: Optional[Literal['structured', 'unstructured']]
807
+ :type map_type: Literal['structured', 'unstructured'], optional
651
808
  """
652
809
  title: constr(strip_whitespace=True, min_length=1)
653
- station: Literal['id1a3','id3a','id3b']
810
+ station: Literal['id1a3', 'id3a', 'id3b']
654
811
  experiment_type: Literal['SAXSWAXS', 'EDD', 'XRF', 'TOMO']
655
812
  sample: Sample
656
813
  spec_scans: conlist(item_type=SpecScans, min_items=1)
@@ -660,6 +817,7 @@ class MapConfig(BaseModel):
660
817
  dwell_time_actual: Optional[DwellTimeActual]
661
818
  postsample_intensity: Optional[PostsampleIntensity]
662
819
  scalar_data: Optional[list[PointByPointScanData]] = []
820
+ attrs: Optional[dict] = {}
663
821
  map_type: Optional[Literal['structured', 'unstructured']] = 'structured'
664
822
  _coords: dict = PrivateAttr()
665
823
  _dims: tuple = PrivateAttr()
@@ -681,7 +839,6 @@ class MapConfig(BaseModel):
681
839
  allow_reuse=True)(validate_data_source_for_map_config)
682
840
  _validate_scalar_data = validator(
683
841
  'scalar_data',
684
- each_item=True,
685
842
  allow_reuse=True)(validate_data_source_for_map_config)
686
843
 
687
844
  @root_validator(pre=True)
@@ -689,7 +846,7 @@ class MapConfig(BaseModel):
689
846
  """Ensure that a valid configuration was provided and finalize
690
847
  spec_file filepaths.
691
848
 
692
- :param values: Dictionary of class field values.
849
+ :param values: Dictionary of validated class field values.
693
850
  :type values: dict
694
851
  :return: The validated list of `values`.
695
852
  :rtype: dict
@@ -706,6 +863,67 @@ class MapConfig(BaseModel):
706
863
  values['spec_scans'] = spec_scans
707
864
  return values
708
865
 
866
+ @validator('experiment_type')
867
+ def validate_experiment_type(cls, value, values):
868
+ """Ensure values for the station and experiment_type fields are
869
+ compatible.
870
+
871
+ :param value: Field value to validate (`experiment_type`).
872
+ :type value: dict
873
+ :param values: Dictionary of validated class field values.
874
+ :type values: dict
875
+ :raises ValueError: Invalid experiment type.
876
+ :return: The validated field for `experiment_type`.
877
+ :rtype: str
878
+ """
879
+ station = values['station']
880
+ if station == 'id1a3':
881
+ allowed_experiment_types = ['SAXSWAXS', 'EDD', 'TOMO']
882
+ elif station == 'id3a':
883
+ allowed_experiment_types = ['EDD', 'TOMO']
884
+ elif station == 'id3b':
885
+ allowed_experiment_types = ['SAXSWAXS', 'XRF', 'TOMO']
886
+ else:
887
+ allowed_experiment_types = []
888
+ if value not in allowed_experiment_types:
889
+ raise ValueError(
890
+ f'For station {station}, allowed experiment types are '
891
+ f'{", ".join(allowed_experiment_types)}. '
892
+ f'Supplied experiment type {value} is not allowed.')
893
+ return value
894
+
895
+ @validator('attrs', always=True)
896
+ def validate_attrs(cls, value, values):
897
+ """Read any additional attributes depending on the values for
898
+ the station and experiment_type fields.
899
+
900
+ :param value: Field value to validate (`attrs`).
901
+ :type value: dict
902
+ :param values: Dictionary of validated class field values.
903
+ :type values: dict
904
+ :raises ValueError: Invalid attribute.
905
+ :return: The validated field for `attrs`.
906
+ :rtype: dict
907
+ """
908
+ # Get the map's scan_type for EDD experiments
909
+ station = values['station']
910
+ experiment_type = values['experiment_type']
911
+ if station in ['id1a3', 'id3a'] and experiment_type == 'EDD':
912
+ value['scan_type'] = cls.get_smb_par_attr(values, 'scan_type')
913
+ value['config_id'] = cls.get_smb_par_attr(values, 'config_id')
914
+ value['dataset_id'] = cls.get_smb_par_attr(values, 'dataset_id')
915
+ axes_labels = {1: 'fly_labx', 2: 'fly_laby', 3: 'fly_labz',
916
+ 4: 'fly_ometotal'}
917
+ if value['scan_type'] is None:
918
+ return value
919
+ if value['scan_type'] != 0:
920
+ value['fly_axis_labels'] = [
921
+ axes_labels[cls.get_smb_par_attr(values, 'fly_axis0')]]
922
+ if value['scan_type'] in (2, 3, 5):
923
+ value['fly_axis_labels'].append(
924
+ axes_labels[cls.get_smb_par_attr(values, 'fly_axis1')])
925
+ return value
926
+
709
927
  @validator('map_type', pre=True, always=True)
710
928
  def validate_map_type(cls, map_type, values):
711
929
  """Validate the map_type field.
@@ -713,16 +931,26 @@ class MapConfig(BaseModel):
713
931
  :param map_type: Type of map, structured or unstructured,
714
932
  defaults to `'structured'`.
715
933
  :type map_type: Literal['structured', 'unstructured']]
716
- :param values: Dictionary of values for all fields of the model.
934
+ :param values: Dictionary of validated class field values.
717
935
  :type values: dict
718
936
  :return: The validated value for map_type.
719
937
  :rtype: str
720
938
  """
721
939
  dims = {}
722
- spec_scans = values.get('spec_scans')
723
- independent_dimensions = values.get('independent_dimensions')
724
- import_scanparser(values.get('station'), values.get('experiment_type'))
940
+ attrs = values.get('attrs', {})
941
+ scan_type = attrs.get('scan_type', -1)
942
+ fly_axis_labels = attrs.get('fly_axis_labels', [])
943
+ spec_scans = values['spec_scans']
944
+ independent_dimensions = values['independent_dimensions']
945
+ scalar_data = values['scalar_data']
946
+ import_scanparser(values['station'], values['experiment_type'])
725
947
  for i, dim in enumerate(deepcopy(independent_dimensions)):
948
+ if dim.label in fly_axis_labels:
949
+ relative = True
950
+ ndigits = 3
951
+ else:
952
+ relative = False
953
+ ndigits = None
726
954
  dims[dim.label] = []
727
955
  for scans in spec_scans:
728
956
  for scan_number in scans.scan_numbers:
@@ -730,7 +958,8 @@ class MapConfig(BaseModel):
730
958
  for scan_step_index in range(
731
959
  scanparser.spec_scan_npts):
732
960
  dims[dim.label].append(dim.get_value(
733
- scans, scan_number, scan_step_index))
961
+ scans, scan_number, scan_step_index,
962
+ scalar_data, relative, ndigits))
734
963
  dims[dim.label] = np.unique(dims[dim.label])
735
964
  if dim.end is None:
736
965
  dim.end = len(dims[dim.label])
@@ -745,34 +974,42 @@ class MapConfig(BaseModel):
745
974
  for scan_step_index in range(scanparser.spec_scan_npts):
746
975
  coords[tuple([
747
976
  list(dims[dim.label]).index(
748
- dim.get_value(scans, scan_number, scan_step_index))
977
+ dim.get_value(scans, scan_number, scan_step_index,
978
+ scalar_data, True, 3))
979
+ if dim.label in fly_axis_labels else
980
+ list(dims[dim.label]).index(
981
+ dim.get_value(scans, scan_number, scan_step_index,
982
+ scalar_data))
749
983
  for dim in independent_dimensions])] += 1
750
984
  if any(True for v in coords.flatten() if v == 0 or v > 1):
751
985
  return 'unstructured'
752
986
  else:
753
987
  return 'structured'
754
988
 
755
-
756
- @validator('experiment_type')
757
- def validate_experiment_type(cls, value, values):
758
- """Ensure values for the station and experiment_type fields are
759
- compatible
760
- """
761
- station = values.get('station')
762
- if station == 'id1a3':
763
- allowed_experiment_types = ['SAXSWAXS', 'EDD', 'TOMO']
764
- elif station == 'id3a':
765
- allowed_experiment_types = ['EDD', 'TOMO']
766
- elif station == 'id3b':
767
- allowed_experiment_types = ['SAXSWAXS', 'XRF', 'TOMO']
768
- else:
769
- allowed_experiment_types = []
770
- if value not in allowed_experiment_types:
771
- raise ValueError(
772
- f'For station {station}, allowed experiment types are '
773
- f'{", ".join(allowed_experiment_types)}. '
774
- f'Supplied experiment type {value} is not allowed.')
775
- return value
989
+ @staticmethod
990
+ def get_smb_par_attr(class_fields, label, units='-', name=None):
991
+ """Read an SMB par file attribute."""
992
+ if name is None:
993
+ name = label
994
+ scalar_data = PointByPointScanData(
995
+ label=label, data_type='smb_par', units=units, name=name)
996
+ values = []
997
+ for scans in class_fields.get('spec_scans'):
998
+ for scan_number in scans.scan_numbers:
999
+ scanparser = scans.get_scanparser(scan_number)
1000
+ try:
1001
+ values.append(scanparser.pars[name])
1002
+ except:
1003
+ print(
1004
+ f'Warning: No value found for .par file value "{name}"'
1005
+ + f' on scan {scan_number} in spec file '
1006
+ + f'{scans.spec_file}.')
1007
+ values.append(None)
1008
+ values = list(set(values))
1009
+ if len(values) != 1:
1010
+ raise ValueError(f'More than one {name} in map not allowed '
1011
+ f'({values})')
1012
+ return values[0]
776
1013
 
777
1014
  @property
778
1015
  def all_scalar_data(self):
@@ -793,9 +1030,17 @@ class MapConfig(BaseModel):
793
1030
  """Return a dictionary of the values of each independent
794
1031
  dimension across the map.
795
1032
  """
796
- if not hasattr(self, "_coords"):
1033
+ if not hasattr(self, '_coords'):
1034
+ scan_type = self.attrs.get('scan_type', -1)
1035
+ fly_axis_labels = self.attrs.get('fly_axis_labels', [])
797
1036
  coords = {}
798
1037
  for dim in self.independent_dimensions:
1038
+ if dim.label in fly_axis_labels:
1039
+ relative = True
1040
+ ndigits = 3
1041
+ else:
1042
+ relative = False
1043
+ ndigits = None
799
1044
  coords[dim.label] = []
800
1045
  for scans in self.spec_scans:
801
1046
  for scan_number in scans.scan_numbers:
@@ -803,7 +1048,8 @@ class MapConfig(BaseModel):
803
1048
  for scan_step_index in range(
804
1049
  scanparser.spec_scan_npts):
805
1050
  coords[dim.label].append(dim.get_value(
806
- scans, scan_number, scan_step_index))
1051
+ scans, scan_number, scan_step_index,
1052
+ self.scalar_data, relative, ndigits))
807
1053
  if self.map_type == 'structured':
808
1054
  coords[dim.label] = np.unique(coords[dim.label])
809
1055
  self._coords = coords
@@ -814,7 +1060,7 @@ class MapConfig(BaseModel):
814
1060
  """Return a tuple of the independent dimension labels for the
815
1061
  map.
816
1062
  """
817
- if not hasattr(self, "_dims"):
1063
+ if not hasattr(self, '_dims'):
818
1064
  self._dims = [
819
1065
  dim.label for dim in self.independent_dimensions[::-1]]
820
1066
  return self._dims
@@ -825,7 +1071,7 @@ class MapConfig(BaseModel):
825
1071
  object, the scan number, and scan step index for every point
826
1072
  on the map.
827
1073
  """
828
- if not hasattr(self, "_scan_step_indices"):
1074
+ if not hasattr(self, '_scan_step_indices'):
829
1075
  scan_step_indices = []
830
1076
  for scans in self.spec_scans:
831
1077
  for scan_number in scans.scan_numbers:
@@ -841,7 +1087,7 @@ class MapConfig(BaseModel):
841
1087
  """Return the shape of the map -- a tuple representing the
842
1088
  number of unique values of each dimension across the map.
843
1089
  """
844
- if not hasattr(self, "_shape"):
1090
+ if not hasattr(self, '_shape'):
845
1091
  if self.map_type == 'structured':
846
1092
  self._shape = tuple(
847
1093
  [len(v) for k, v in self.coords.items()][::-1])
@@ -859,6 +1105,14 @@ class MapConfig(BaseModel):
859
1105
  :rtype: dict
860
1106
  """
861
1107
  if self.map_type == 'structured':
1108
+ scan_type = self.attrs.get('scan_type', -1)
1109
+ fly_axis_labels = self.attrs.get('fly_axis_labels', [])
1110
+ if (scan_type in (3, 5)
1111
+ and len(self.dims) ==
1112
+ len(map_index) + len(fly_axis_labels)):
1113
+ dims = [dim for dim in self.dims if dim not in fly_axis_labels]
1114
+ return {dim:self.coords[dim][i]
1115
+ for dim, i in zip(dims, map_index)}
862
1116
  return {dim:self.coords[dim][i]
863
1117
  for dim, i in zip(self.dims, map_index)}
864
1118
  else:
@@ -893,12 +1147,21 @@ class MapConfig(BaseModel):
893
1147
  step index.
894
1148
  :rtype: tuple[SpecScans, int, int]
895
1149
  """
1150
+ scan_type = self.attrs.get('scan_type', -1)
1151
+ fly_axis_labels = self.attrs.get('fly_axis_labels', [])
896
1152
  if self.map_type == 'structured':
897
1153
  map_coords = self.get_coords(map_index)
898
1154
  for scans, scan_number, scan_step_index in self.scan_step_indices:
899
- coords = {dim.label:dim.get_value(
900
- scans, scan_number, scan_step_index)
901
- for dim in self.independent_dimensions}
1155
+ coords = {dim.label:(
1156
+ dim.get_value(
1157
+ scans, scan_number, scan_step_index,
1158
+ self.scalar_data, True, 3)
1159
+ if dim.label in fly_axis_labels
1160
+ else
1161
+ dim.get_value(
1162
+ scans, scan_number, scan_step_index,
1163
+ self.scalar_data))
1164
+ for dim in self.independent_dimensions}
902
1165
  if coords == map_coords:
903
1166
  return scans, scan_number, scan_step_index
904
1167
  raise RuntimeError(f'Unable to match coordinates {coords}')
@@ -910,26 +1173,28 @@ class MapConfig(BaseModel):
910
1173
  single point in the map.
911
1174
 
912
1175
  :param data: The device configuration to return a value of raw
913
- data for
1176
+ data for.
914
1177
  :type data: PointByPointScanData
915
- :param map_index: The map index to return raw data for
1178
+ :param map_index: The map index to return raw data for.
916
1179
  :type map_index: tuple
917
- :return: Raw data value
1180
+ :return: Raw data value.
918
1181
  """
919
1182
  scans, scan_number, scan_step_index = \
920
1183
  self.get_scan_step_index(map_index)
921
- return data.get_value(scans, scan_number, scan_step_index)
1184
+ return data.get_value(scans, scan_number, scan_step_index,
1185
+ self.scalar_data)
922
1186
 
923
1187
 
924
1188
  def import_scanparser(station, experiment):
925
1189
  """Given the name of a CHESS station and experiment type, import
926
1190
  the corresponding subclass of `ScanParser` as `ScanParser`.
927
1191
 
928
- :param station: The station name ("IDxx", not the beamline acronym)
1192
+ :param station: The station name
1193
+ ('IDxx', not the beamline acronym).
929
1194
  :type station: str
930
- :param experiment: The experiment type
931
- :type experiment: Literal["SAXSWAXS","EDD","XRF","Tomo","Powder"]
932
- :return: None
1195
+ :param experiment: The experiment type.
1196
+ :type experiment: Literal[
1197
+ 'SAXSWAXS', 'EDD', 'XRF', 'Tomo', 'Powder']
933
1198
  """
934
1199
 
935
1200
  station = station.lower()