ChessAnalysisPipeline 0.0.2__py3-none-any.whl → 0.0.4__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 (47) hide show
  1. CHAP/__init__.py +3 -0
  2. CHAP/common/__init__.py +19 -0
  3. CHAP/common/models/__init__.py +2 -0
  4. CHAP/common/models/integration.py +515 -0
  5. CHAP/common/models/map.py +535 -0
  6. CHAP/common/processor.py +644 -0
  7. CHAP/common/reader.py +119 -0
  8. CHAP/common/utils/__init__.py +37 -0
  9. CHAP/common/utils/fit.py +2613 -0
  10. CHAP/common/utils/general.py +1225 -0
  11. CHAP/common/utils/material.py +231 -0
  12. CHAP/common/utils/scanparsers.py +785 -0
  13. CHAP/common/writer.py +96 -0
  14. CHAP/edd/__init__.py +7 -0
  15. CHAP/edd/models.py +215 -0
  16. CHAP/edd/processor.py +321 -0
  17. CHAP/edd/reader.py +5 -0
  18. CHAP/edd/writer.py +5 -0
  19. CHAP/inference/__init__.py +3 -0
  20. CHAP/inference/processor.py +68 -0
  21. CHAP/inference/reader.py +5 -0
  22. CHAP/inference/writer.py +5 -0
  23. CHAP/pipeline.py +1 -1
  24. CHAP/processor.py +11 -818
  25. CHAP/reader.py +18 -113
  26. CHAP/saxswaxs/__init__.py +6 -0
  27. CHAP/saxswaxs/processor.py +5 -0
  28. CHAP/saxswaxs/reader.py +5 -0
  29. CHAP/saxswaxs/writer.py +5 -0
  30. CHAP/sin2psi/__init__.py +7 -0
  31. CHAP/sin2psi/processor.py +5 -0
  32. CHAP/sin2psi/reader.py +5 -0
  33. CHAP/sin2psi/writer.py +5 -0
  34. CHAP/tomo/__init__.py +5 -0
  35. CHAP/tomo/models.py +125 -0
  36. CHAP/tomo/processor.py +2009 -0
  37. CHAP/tomo/reader.py +5 -0
  38. CHAP/tomo/writer.py +5 -0
  39. CHAP/writer.py +17 -167
  40. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/METADATA +1 -1
  41. ChessAnalysisPipeline-0.0.4.dist-info/RECORD +50 -0
  42. CHAP/async.py +0 -56
  43. ChessAnalysisPipeline-0.0.2.dist-info/RECORD +0 -17
  44. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/LICENSE +0 -0
  45. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/WHEEL +0 -0
  46. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/entry_points.txt +0 -0
  47. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,535 @@
1
+ from functools import cache, lru_cache
2
+ import os
3
+ from typing import Literal, Optional, Union
4
+
5
+ import numpy as np
6
+ from pydantic import (BaseModel,
7
+ conint,
8
+ conlist,
9
+ confloat,
10
+ constr,
11
+ FilePath,
12
+ PrivateAttr,
13
+ ValidationError,
14
+ validator)
15
+ from pyspec.file.spec import FileSpec
16
+
17
+ class Sample(BaseModel):
18
+ """
19
+ Class representing a sample metadata configuration.
20
+
21
+ :ivar name: The name of the sample.
22
+ :type name: str
23
+ :ivar description: A description of the sample.
24
+ :type description: Optional[str]
25
+ """
26
+ name: constr(min_length=1)
27
+ description: Optional[str]
28
+
29
+ class SpecScans(BaseModel):
30
+ """
31
+ Class representing a set of scans from a single SPEC file.
32
+
33
+ :ivar spec_file: Path to the SPEC file.
34
+ :type spec_file: str
35
+ :ivar scan_numbers: List of scan numbers to use.
36
+ :type scan_numbers: list[int]
37
+ """
38
+ spec_file: FilePath
39
+ scan_numbers: conlist(item_type=conint(gt=0), min_items=1)
40
+ @validator('spec_file', allow_reuse=True)
41
+ def validate_spec_file(cls, spec_file):
42
+ """
43
+ Validate the specified SPEC file.
44
+
45
+ :param spec_file: Path to the SPEC file.
46
+ :type spec_file: str
47
+ :raises ValueError: If the SPEC file is invalid.
48
+ :return: Absolute path to the SPEC file, if it is valid.
49
+ :rtype: str
50
+ """
51
+ try:
52
+ spec_file = os.path.abspath(spec_file)
53
+ sspec_file = FileSpec(spec_file)
54
+ except:
55
+ raise(ValueError(f'Invalid SPEC file {spec_file}'))
56
+ else:
57
+ return(spec_file)
58
+ @validator('scan_numbers', allow_reuse=True)
59
+ def validate_scan_numbers(cls, scan_numbers, values):
60
+ """
61
+ Validate the specified list of scan numbers.
62
+
63
+ :param scan_numbers: List of scan numbers.
64
+ :type scan_numbers: list of int
65
+ :param values: Dictionary of values for all fields of the model.
66
+ :type values: dict
67
+ :raises ValueError: If a specified scan number is not found in the SPEC file.
68
+ :return: List of scan numbers.
69
+ :rtype: list of int
70
+ """
71
+ spec_file = values.get('spec_file')
72
+ if spec_file is not None:
73
+ spec_scans = FileSpec(spec_file)
74
+ for scan_number in scan_numbers:
75
+ scan = spec_scans.get_scan_by_number(scan_number)
76
+ if scan is None:
77
+ raise(ValueError(f'There is no scan number {scan_number} in {spec_file}'))
78
+ return(scan_numbers)
79
+
80
+ @property
81
+ def scanparsers(self):
82
+ '''A list of `ScanParser`s for each of the scans specified by the SPEC
83
+ file and scan numbers belonging to this instance of `SpecScans`
84
+ '''
85
+ return([self.get_scanparser(scan_no) for scan_no in self.scan_numbers])
86
+
87
+ def get_scanparser(self, scan_number):
88
+ """This method returns a `ScanParser` for the specified scan number in
89
+ the specified SPEC file.
90
+
91
+ :param scan_number: Scan number to get a `ScanParser` for
92
+ :type scan_number: int
93
+ :return: `ScanParser` for the specified scan number
94
+ :rtype: ScanParser
95
+ """
96
+ return(get_scanparser(self.spec_file, scan_number))
97
+ def get_index(self, scan_number:int, scan_step_index:int, map_config):
98
+ """This method returns a tuple representing the index of a specific step
99
+ in a specific spec scan within a map.
100
+
101
+ :param scan_number: Scan number to get index for
102
+ :type scan_number: int
103
+ :param scan_step_index: Scan step index to get index for
104
+ :type scan_step_index: int
105
+ :param map_config: Map configuration to get index for
106
+ :type map_config: MapConfig
107
+ :return: Index for the specified scan number and scan step index within
108
+ the specified map configuration
109
+ :rtype: tuple
110
+ """
111
+ index = ()
112
+ for independent_dimension in map_config.independent_dimensions:
113
+ coordinate_index = list(map_config.coords[independent_dimension.label]).index(independent_dimension.get_value(self, scan_number, scan_step_index))
114
+ index = (coordinate_index, *index)
115
+ return(index)
116
+ def get_detector_data(self, detectors:list, scan_number:int, scan_step_index:int):
117
+ """
118
+ Return the raw data from the specified detectors at the specified scan
119
+ number and scan step index.
120
+
121
+ :param detectors: List of detector prefixes to get raw data for
122
+ :type detectors: list[str]
123
+ :param scan_number: Scan number to get data for
124
+ :type scan_number: int
125
+ :param scan_step_index: Scan step index to get data for
126
+ :type scan_step_index: int
127
+ :return: Data from the specified detectors for the specified scan number
128
+ and scan step index
129
+ :rtype: list[np.ndarray]
130
+ """
131
+ return(get_detector_data(tuple([detector.prefix for detector in detectors]), self.spec_file, scan_number, scan_step_index))
132
+ @cache
133
+ def get_available_scan_numbers(spec_file:str):
134
+ scans = FileSpec(spec_file).scans
135
+ scan_numbers = list(scans.keys())
136
+ return(scan_numbers)
137
+ @cache
138
+ def get_scanparser(spec_file:str, scan_number:int):
139
+ if scan_number not in get_available_scan_numbers(spec_file):
140
+ return(None)
141
+ else:
142
+ return(ScanParser(spec_file, scan_number))
143
+ @lru_cache(maxsize=10)
144
+ def get_detector_data(detector_prefixes:tuple, spec_file:str, scan_number:int, scan_step_index:int):
145
+ detector_data = []
146
+ scanparser = get_scanparser(spec_file, scan_number)
147
+ for prefix in detector_prefixes:
148
+ image_data = scanparser.get_detector_data(prefix, scan_step_index)
149
+ detector_data.append(image_data)
150
+ return(detector_data)
151
+
152
+ class PointByPointScanData(BaseModel):
153
+ """Class representing a source of raw scalar-valued data for which a value
154
+ was recorded at every point in a `MapConfig`.
155
+
156
+ :ivar label: A user-defined label for referring to this data in the NeXus
157
+ file and in other tools.
158
+ :type label: str
159
+ :ivar units: The units in which the data were recorded.
160
+ :type units: str
161
+ :ivar data_type: Represents how these data were recorded at time of data
162
+ collection.
163
+ :type data_type: Literal['spec_motor', 'scan_column', 'smb_par']
164
+ :ivar name: Represents the name with which these raw data were recorded at
165
+ time of data collection.
166
+ :type name: str
167
+ """
168
+ label: constr(min_length=1)
169
+ units: constr(strip_whitespace=True, min_length=1)
170
+ data_type: Literal['spec_motor', 'scan_column', 'smb_par']
171
+ name: constr(strip_whitespace=True, min_length=1)
172
+ @validator('label')
173
+ def validate_label(cls, label):
174
+ """Validate that the supplied `label` does not conflict with any of the
175
+ values for `label` reserved for certain data needed to perform
176
+ corrections.
177
+
178
+ :param label: The value of `label` to validate
179
+ :type label: str
180
+ :raises ValueError: If `label` is one of the reserved values.
181
+ :return: The original supplied value `label`, if it is allowed.
182
+ :rtype: str
183
+ """
184
+ #if (not issubclass(cls,CorrectionsData)) and label in CorrectionsData.__fields__['label'].type_.__args__:
185
+ if (not issubclass(cls,CorrectionsData)) and label in CorrectionsData.reserved_labels():
186
+ raise(ValueError(f'{cls.__name__}.label may not be any of the following reserved values: {CorrectionsData.reserved_labels()}'))
187
+ return(label)
188
+ def validate_for_station(self, station:str):
189
+ """Validate this instance of `PointByPointScanData` for a certain choice
190
+ of station (beamline).
191
+
192
+ :param station: The name of the station (in 'idxx' format).
193
+ :type station: str
194
+ :raises TypeError: If the station is not compatible with the value of the
195
+ `data_type` attribute for this instance of PointByPointScanData.
196
+ :return: None
197
+ :rtype: None
198
+ """
199
+ if station.lower() not in ('id1a3', 'id3a') and self.data_type == 'smb_par':
200
+ raise(TypeError(f'{self.__class__.__name__}.data_type may not be "smb_par" when station is "{station}"'))
201
+ def validate_for_spec_scans(self, spec_scans:list[SpecScans], scan_step_index:Union[Literal['all'],int]='all'):
202
+ """Validate this instance of `PointByPointScanData` for a list of
203
+ `SpecScans`.
204
+
205
+ :param spec_scans: A list of `SpecScans` whose raw data will be checked
206
+ for the presence of the data represented by this instance of
207
+ `PointByPointScanData`
208
+ :type spec_scans: list[SpecScans]
209
+ :param scan_step_index: A specific scan step index to validate, defaults
210
+ to `'all'`.
211
+ :type scan_step_index: Union[Literal['all'],int], optional
212
+ :raises RuntimeError: If the data represented by this instance of
213
+ `PointByPointScanData` is missing for the specified scan steps.
214
+ :return: None
215
+ :rtype: None
216
+ """
217
+ for scans in spec_scans:
218
+ for scan_number in scans.scan_numbers:
219
+ scanparser = scans.get_scanparser(scan_number)
220
+ if scan_step_index == 'all':
221
+ scan_step_index_range = range(scanparser.spec_scan_npts)
222
+ else:
223
+ scan_step_index_range = range(scan_step_index,scan_step_index+1)
224
+ for index in scan_step_index_range:
225
+ try:
226
+ self.get_value(scans, scan_number, index)
227
+ except:
228
+ raise(RuntimeError(f'Could not find data for {self.name} (data_type "{self.data_type}") on scan number {scan_number} for index {index} in spec file {scans.spec_file}'))
229
+ def get_value(self, spec_scans:SpecScans, scan_number:int, scan_step_index:int):
230
+ """Return the value recorded for this instance of `PointByPointScanData`
231
+ at a specific scan step.
232
+
233
+ :param spec_scans: An instance of `SpecScans` in which the requested scan step occurs.
234
+ :type spec_scans: SpecScans
235
+ :param scan_number: The number of the scan in which the requested scan step occurs.
236
+ :type scan_number: int
237
+ :param scan_step_index: The index of the requested scan step.
238
+ :type scan_step_index: int
239
+ :return: The value recorded of the data represented by this instance of
240
+ `PointByPointScanData` at the scan step requested
241
+ :rtype: float
242
+ """
243
+ if self.data_type == 'spec_motor':
244
+ return(get_spec_motor_value(spec_scans.spec_file, scan_number, scan_step_index, self.name))
245
+ elif self.data_type == 'scan_column':
246
+ return(get_spec_counter_value(spec_scans.spec_file, scan_number, scan_step_index, self.name))
247
+ elif self.data_type == 'smb_par':
248
+ return(get_smb_par_value(spec_scans.spec_file, scan_number, self.name))
249
+ @cache
250
+ def get_spec_motor_value(spec_file:str, scan_number:int, scan_step_index:int, spec_mnemonic:str):
251
+ """Return the value recorded for a SPEC motor at a specific scan step.
252
+
253
+ :param spec_file: Location of a SPEC file in which the requested scan step occurs.
254
+ :type spec_scans: str
255
+ :param scan_number: The number of the scan in which the requested scan step occurs.
256
+ :type scan_number: int
257
+ :param scan_step_index: The index of the requested scan step.
258
+ :type scan_step_index: int
259
+ :param spec_mnemonic: The menmonic of a SPEC motor.
260
+ :type spec_mnemonic: str
261
+ :return: The value of the motor at the scan step requested
262
+ :rtype: float
263
+ """
264
+ scanparser = get_scanparser(spec_file, scan_number)
265
+ if spec_mnemonic in scanparser.spec_scan_motor_mnes:
266
+ motor_i = scanparser.spec_scan_motor_mnes.index(spec_mnemonic)
267
+ if scan_step_index >= 0:
268
+ scan_step = np.unravel_index(scan_step_index, scanparser.spec_scan_shape, order='F')
269
+ motor_value = scanparser.spec_scan_motor_vals[motor_i][scan_step[motor_i]]
270
+ else:
271
+ motor_value = scanparser.spec_scan_motor_vals[motor_i]
272
+ else:
273
+ motor_value = scanparser.get_spec_positioner_value(spec_mnemonic)
274
+ return(motor_value)
275
+ @cache
276
+ def get_spec_counter_value(spec_file:str, scan_number:int, scan_step_index:int, spec_column_label:str):
277
+ """Return the value recorded for a SPEC counter at a specific scan step.
278
+
279
+ :param spec_file: Location of a SPEC file in which the requested scan step occurs.
280
+ :type spec_scans: str
281
+ :param scan_number: The number of the scan in which the requested scan step occurs.
282
+ :type scan_number: int
283
+ :param scan_step_index: The index of the requested scan step.
284
+ :type scan_step_index: int
285
+ :param spec_column_label: The label of a SPEC data column.
286
+ :type spec_column_label: str
287
+ :return: The value of the counter at the scan step requested
288
+ :rtype: float
289
+ """
290
+ scanparser = get_scanparser(spec_file, scan_number)
291
+ if scan_step_index >= 0:
292
+ return(scanparser.spec_scan_data[spec_column_label][scan_step_index])
293
+ else:
294
+ return(scanparser.spec_scan_data[spec_column_label])
295
+ @cache
296
+ def get_smb_par_value(spec_file:str, scan_number:int, par_name:str):
297
+ """Return the value recorded for a specific scan in SMB-tyle .par file.
298
+
299
+ :param spec_file: Location of a SPEC file in which the requested scan step occurs.
300
+ :type spec_scans: str
301
+ :param scan_number: The number of the scan in which the requested scan step occurs.
302
+ :type scan_number: int
303
+ :param par_name: The name of the column in the .par file
304
+ :type par_name: str
305
+ :return: The value of the .par file value for the scan requested.
306
+ :rtype: float
307
+ """
308
+ scanparser = get_scanparser(spec_file, scan_number)
309
+ return(scanparser.pars[par_name])
310
+ def validate_data_source_for_map_config(data_source, values):
311
+ if data_source is not None:
312
+ import_scanparser(values.get('station'), values.get('experiment_type'))
313
+ data_source.validate_for_station(values.get('station'))
314
+ data_source.validate_for_spec_scans(values.get('spec_scans'))
315
+ return(data_source)
316
+
317
+ class CorrectionsData(PointByPointScanData):
318
+ """Class representing the special instances of `PointByPointScanData` that
319
+ are used by certain kinds of `CorrectionConfig` tools.
320
+
321
+ :ivar label: One of the reserved values required by `CorrectionConfig`,
322
+ `'presample_intensity'`, `'postsample_intensity'`, or
323
+ `'dwell_time_actual'`.
324
+ :type label: Literal['presample_intensity','postsample_intensity','dwell_time_actual']
325
+ :ivar units: The units in which the data were recorded.
326
+ :type units: str
327
+ :ivar data_type: Represents how these data were recorded at time of data
328
+ collection.
329
+ :type data_type: Literal['scan_column', 'smb_par']
330
+ :ivar name: Represents the name with which these raw data were recorded at
331
+ time of data collection.
332
+ :type name: str
333
+ """
334
+ label: Literal['presample_intensity','postsample_intensity','dwell_time_actual']
335
+ data_type: Literal['scan_column','smb_par']
336
+ @classmethod
337
+ def reserved_labels(cls):
338
+ """Return a list of all the labels reserved for corrections-related
339
+ scalar data.
340
+
341
+ :return: A list of reserved labels
342
+ :rtype: list[str]
343
+ """
344
+ return(list(cls.__fields__['label'].type_.__args__))
345
+ class PresampleIntensity(CorrectionsData):
346
+ """Class representing a source of raw data for the intensity of the beam that
347
+ is incident on the sample.
348
+
349
+ :ivar label: Must be `"presample_intensity"`
350
+ :type label: Literal["presample_intensity"]
351
+ :ivar units: Must be `"counts"`
352
+ :type units: Literal["counts"]
353
+ :ivar data_type: Represents how these data were recorded at time of data
354
+ collection.
355
+ :type data_type: Literal['scan_column', 'smb_par']
356
+ :ivar name: Represents the name with which these raw data were recorded at
357
+ time of data collection.
358
+ :type name: str
359
+ """
360
+ label: Literal['presample_intensity'] = 'presample_intensity'
361
+ units: Literal['counts'] = 'counts'
362
+ class PostsampleIntensity(CorrectionsData):
363
+ """Class representing a source of raw data for the intensity of the beam that
364
+ has passed through the sample.
365
+
366
+ :ivar label: Must be `"postsample_intensity"`
367
+ :type label: Literal["postsample_intensity"]
368
+ :ivar units: Must be `"counts"`
369
+ :type units: Literal["counts"]
370
+ :ivar data_type: Represents how these data were recorded at time of data
371
+ collection.
372
+ :type data_type: Literal['scan_column', 'smb_par']
373
+ :ivar name: Represents the name with which these raw data were recorded at
374
+ time of data collection.
375
+ :type name: str
376
+ """
377
+ label: Literal['postsample_intensity'] = 'postsample_intensity'
378
+ units: Literal['counts'] = 'counts'
379
+ class DwellTimeActual(CorrectionsData):
380
+ """Class representing a source of raw data for the actual dwell time at each
381
+ scan point in SPEC (with some scan types, this value can vary slightly
382
+ point-to-point from the dwell time specified in the command).
383
+
384
+ :ivar label: Must be `"dwell_time_actual"`
385
+ :type label: Literal["dwell_time_actual"]
386
+ :ivar units: Must be `"counts"`
387
+ :type units: Literal["counts"]
388
+ :ivar data_type: Represents how these data were recorded at time of data
389
+ collection.
390
+ :type data_type: Literal['scan_column', 'smb_par']
391
+ :ivar name: Represents the name with which these raw data were recorded at
392
+ time of data collection.
393
+ :type name: str
394
+ """
395
+ label: Literal['dwell_time_actual'] = 'dwell_time_actual'
396
+ units: Literal['s'] = 's'
397
+
398
+ class MapConfig(BaseModel):
399
+ """Class representing an experiment consisting of one or more SPEC scans.
400
+
401
+ :ivar title: The title for the map configuration.
402
+ :type title: str
403
+ :ivar station: The name of the station at which the map was collected.
404
+ :type station: Literal['id1a3','id3a','id3b']
405
+ :ivar spec_scans: A list of the spec scans that compose the map.
406
+ :type spec_scans: list[SpecScans]
407
+ :ivar independent_dimensions: A list of the sources of data representing the
408
+ raw values of each independent dimension of the map.
409
+ :type independent_dimensions: list[PointByPointScanData]
410
+ :ivar presample_intensity: A source of point-by-point presample beam
411
+ intensity data. Required when applying a CorrectionConfig tool.
412
+ :type presample_intensity: Optional[PresampleIntensity]
413
+ :ivar dwell_time_actual: A source of point-by-point actual dwell times for
414
+ spec scans. Required when applying a CorrectionConfig tool.
415
+ :type dwell_time_actual: Optional[DwellTimeActual]
416
+ :ivar presample_intensity: A source of point-by-point postsample beam
417
+ intensity data. Required when applying a CorrectionConfig tool with
418
+ `correction_type="flux_absorption"` or
419
+ `correction_type="flux_absorption_background"`.
420
+ :type presample_intensity: Optional[PresampleIntensity]
421
+ :ivar scalar_data: A list of the sources of data representing other scalar
422
+ raw data values collected at each point ion the map. In the NeXus file
423
+ representation of the map, datasets for these values will be included.
424
+ :type scalar_values: Optional[list[PointByPointScanData]]
425
+ """
426
+ title: constr(strip_whitespace=True, min_length=1)
427
+ station: Literal['id1a3','id3a','id3b']
428
+ experiment_type: Literal['SAXSWAXS', 'EDD', 'XRF', 'TOMO']
429
+ sample: Sample
430
+ spec_scans: conlist(item_type=SpecScans, min_items=1)
431
+ independent_dimensions: conlist(item_type=PointByPointScanData, min_items=1)
432
+ presample_intensity: Optional[PresampleIntensity]
433
+ dwell_time_actual: Optional[DwellTimeActual]
434
+ postsample_intensity: Optional[PostsampleIntensity]
435
+ scalar_data: Optional[list[PointByPointScanData]] = []
436
+ _coords: dict = PrivateAttr()
437
+ _validate_independent_dimensions = validator('independent_dimensions', each_item=True, allow_reuse=True)(validate_data_source_for_map_config)
438
+ _validate_presample_intensity = validator('presample_intensity', allow_reuse=True)(validate_data_source_for_map_config)
439
+ _validate_dwell_time_actual = validator('dwell_time_actual', allow_reuse=True)(validate_data_source_for_map_config)
440
+ _validate_postsample_intensity = validator('postsample_intensity', allow_reuse=True)(validate_data_source_for_map_config)
441
+ _validate_scalar_data = validator('scalar_data', each_item=True, allow_reuse=True)(validate_data_source_for_map_config)
442
+ @validator('experiment_type')
443
+ def validate_experiment_type(cls, value, values):
444
+ '''Ensure values for the station and experiment_type fields are compatible'''
445
+ station = values.get('station')
446
+ if station == 'id1a3':
447
+ allowed_experiment_types = ['SAXSWAXS', 'EDD', 'TOMO']
448
+ elif station == 'id3a':
449
+ allowed_experiment_types = ['EDD', 'TOMO']
450
+ elif station == 'id3b':
451
+ allowed_experiment_types = ['SAXSWAXS', 'XRF', 'TOMO']
452
+ else:
453
+ allowed_experiment_types = []
454
+ if value not in allowed_experiment_types:
455
+ raise(ValueError(f'For station {station}, allowed experiment types are {allowed_experiment_types} (suuplied experiment type {value} is not allowed)'))
456
+ return(value)
457
+ @property
458
+ def coords(self):
459
+ """Return a dictionary of the values of each independent dimension across
460
+ the map.
461
+
462
+ :returns: A dictionary ofthe map's coordinate values.
463
+ :rtype: dict[str,list[float]]
464
+ """
465
+ try:
466
+ return(self._coords)
467
+ except:
468
+ coords = {}
469
+ for independent_dimension in self.independent_dimensions:
470
+ coords[independent_dimension.label] = []
471
+ for scans in self.spec_scans:
472
+ for scan_number in scans.scan_numbers:
473
+ scanparser = scans.get_scanparser(scan_number)
474
+ for scan_step_index in range(scanparser.spec_scan_npts):
475
+ coords[independent_dimension.label].append(independent_dimension.get_value(scans, scan_number, scan_step_index))
476
+ coords[independent_dimension.label] = np.unique(coords[independent_dimension.label])
477
+ self._coords = coords
478
+ return(self._coords)
479
+ @property
480
+ def dims(self):
481
+ """Return a tuple of the independent dimension labels for the map."""
482
+ return([point_by_point_scan_data.label for point_by_point_scan_data in self.independent_dimensions[::-1]])
483
+ @property
484
+ def shape(self):
485
+ """Return the shape of the map -- a tuple representing the number of
486
+ unique values of each dimension across the map.
487
+ """
488
+ return(tuple([len(values) for key,values in self.coords.items()][::-1]))
489
+ @property
490
+ def all_scalar_data(self):
491
+ """Return a list of all instances of `PointByPointScanData` for which
492
+ this map configuration will collect dataset-like data (as opposed to
493
+ axes-like data).
494
+
495
+ This will be any and all of the items in the corrections-data-related
496
+ fields, as well as any additional items in the optional `scalar_data`
497
+ field."""
498
+ return([getattr(self,l,None) for l in CorrectionsData.reserved_labels() if getattr(self,l,None) is not None] + self.scalar_data)
499
+
500
+ def import_scanparser(station, experiment):
501
+ '''Given the name of a CHESS station and experiment type, import the
502
+ corresponding subclass of `ScanParser` as `ScanParser`.
503
+
504
+ :param station: The station name ("IDxx", not the beamline acronym)
505
+ :type station: str
506
+ :param experiment: The experiment type
507
+ :type experiment: Literal["SAXSWAXS","EDD","XRF","Tomo","Powder"]
508
+ :return: None
509
+ '''
510
+
511
+ station = station.lower()
512
+ experiment = experiment.lower()
513
+
514
+ if station in ('id1a3', 'id3a'):
515
+ if experiment in ('saxswaxs', 'powder'):
516
+ from CHAP.common.utils.scanparsers import SMBLinearScanParser as ScanParser
517
+ elif experiment == 'edd':
518
+ from CHAP.common.utils.scanparsers import SMBMCAScanParser as ScanParser
519
+ elif experiment == 'tomo':
520
+ from CHAP.common.utils.scanparsers import SMBRotationScanParser as ScanParser
521
+ else:
522
+ raise(ValueError(f'Invalid experiment type for station {station}: {experiment}'))
523
+ elif station == 'id3b':
524
+ if experiment == 'saxswaxs':
525
+ from CHAP.common.utils.scanparsers import FMBSAXSWAXSScanParser as ScanParser
526
+ elif experiment == 'tomo':
527
+ from CHAP.common.utils.scanparsers import FMBRotationScanParser as ScanParser
528
+ elif experiment == 'xrf':
529
+ from CHAP.common.utils.scanparsers import FMBXRFScanParser as ScanParser
530
+ else:
531
+ raise(ValueError(f'Invalid experiment type for station {station}: {experiment}'))
532
+ else:
533
+ raise(ValueError(f'Invalid station: {station}'))
534
+
535
+ globals()['ScanParser'] = ScanParser