ChessAnalysisPipeline 0.0.14__py3-none-any.whl → 0.0.16__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (38) hide show
  1. CHAP/__init__.py +1 -1
  2. CHAP/common/__init__.py +13 -0
  3. CHAP/common/models/integration.py +29 -26
  4. CHAP/common/models/map.py +395 -224
  5. CHAP/common/processor.py +1725 -93
  6. CHAP/common/reader.py +265 -28
  7. CHAP/common/writer.py +191 -18
  8. CHAP/edd/__init__.py +9 -2
  9. CHAP/edd/models.py +886 -665
  10. CHAP/edd/processor.py +2592 -936
  11. CHAP/edd/reader.py +889 -0
  12. CHAP/edd/utils.py +846 -292
  13. CHAP/foxden/__init__.py +6 -0
  14. CHAP/foxden/processor.py +42 -0
  15. CHAP/foxden/writer.py +65 -0
  16. CHAP/giwaxs/__init__.py +8 -0
  17. CHAP/giwaxs/models.py +100 -0
  18. CHAP/giwaxs/processor.py +520 -0
  19. CHAP/giwaxs/reader.py +5 -0
  20. CHAP/giwaxs/writer.py +5 -0
  21. CHAP/pipeline.py +48 -10
  22. CHAP/runner.py +161 -72
  23. CHAP/tomo/models.py +31 -29
  24. CHAP/tomo/processor.py +169 -118
  25. CHAP/utils/__init__.py +1 -0
  26. CHAP/utils/fit.py +1292 -1315
  27. CHAP/utils/general.py +411 -53
  28. CHAP/utils/models.py +594 -0
  29. CHAP/utils/parfile.py +10 -2
  30. ChessAnalysisPipeline-0.0.16.dist-info/LICENSE +60 -0
  31. {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/METADATA +1 -1
  32. ChessAnalysisPipeline-0.0.16.dist-info/RECORD +62 -0
  33. {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/WHEEL +1 -1
  34. CHAP/utils/scanparsers.py +0 -1431
  35. ChessAnalysisPipeline-0.0.14.dist-info/LICENSE +0 -21
  36. ChessAnalysisPipeline-0.0.14.dist-info/RECORD +0 -54
  37. {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/entry_points.txt +0 -0
  38. {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/top_level.txt +0 -0
CHAP/edd/reader.py CHANGED
@@ -1,5 +1,894 @@
1
1
  #!/usr/bin/env python
2
2
 
3
+ # System modules
4
+ import os
5
+
6
+ # Third party modules
7
+ import numpy as np
8
+
9
+ # Local modules
10
+ from CHAP.reader import Reader
11
+
12
+ class EddMapReader(Reader):
13
+ """Reader for taking an EDD-style .par file and returning a
14
+ `MapConfig` representing one of the datasets in the
15
+ file. Independent dimensions are determined automatically, and a
16
+ specific set of items to use for extra scalar datasets to include
17
+ are hard-coded in. The raw data is read if detector_names are
18
+ specified."""
19
+ def read(self, parfile, dataset_id, detector_names=None):
20
+ """Return a validated `MapConfig` object representing an EDD
21
+ dataset.
22
+
23
+ :param parfile: Name of the EDD-style .par file containing the
24
+ dataset.
25
+ :type parfile: str
26
+ :param dataset_id: Number of the dataset in the .par file
27
+ to return as a map.
28
+ :type dataset_id: int
29
+ :param detector_names: Detector prefixes for the raw data.
30
+ :type detector_names: list[str], optional
31
+ :returns: Map configuration packaged with the appropriate
32
+ value for 'schema'.
33
+ :rtype: PipelineData
34
+ """
35
+ # Local modules
36
+ from CHAP.common.models.map import MapConfig
37
+ from CHAP.pipeline import PipelineData
38
+ from CHAP.utils.general import list_to_string
39
+ from CHAP.utils.parfile import ParFile
40
+ from chess_scanparsers import SMBMCAScanParser as ScanParser
41
+
42
+ if detector_names is not None:
43
+ assert is_str_series(detector_names, raise_error=True)
44
+
45
+ parfile = ParFile(parfile)
46
+ self.logger.debug(f'spec_file: {parfile.spec_file}')
47
+
48
+ # Get list of scan numbers for the dataset
49
+ dataset_ids = np.asarray(parfile.get_values('dataset_id'))
50
+ dataset_rows_i = np.argwhere(
51
+ np.where(
52
+ np.asarray(dataset_ids) == dataset_id, 1, 0)).flatten()
53
+ scan_nos = [parfile.data[i][parfile.scann_i] for i in dataset_rows_i\
54
+ if parfile.data[i][parfile.scann_i] in \
55
+ parfile.good_scan_numbers()]
56
+ self.logger.debug(f'Scan numbers: {list_to_string(scan_nos)}')
57
+ spec_scans = [dict(spec_file=parfile.spec_file, scan_numbers=scan_nos)]
58
+
59
+ # Get scan type for this dataset
60
+ scan_types = parfile.get_values('scan_type', scan_numbers=scan_nos)
61
+ if any([st != scan_types[0] for st in scan_types]):
62
+ msg = 'Only one scan type per dataset is suported.'
63
+ self.logger.error(msg)
64
+ raise Exception(msg)
65
+ scan_type = scan_types[0]
66
+ self.logger.debug(f'Scan type: {scan_type}')
67
+
68
+ # Based on scan type, get independent_dimensions for the map
69
+ # Start by adding labx, laby, labz, and omega. Any "extra"
70
+ # dimensions will be sqeezed out of the map later.
71
+ independent_dimensions = [
72
+ dict(label='labx', units='mm', data_type='smb_par',
73
+ name='labx'),
74
+ dict(label='laby', units='mm', data_type='smb_par',
75
+ name='laby'),
76
+ dict(label='labz', units='mm', data_type='smb_par',
77
+ name='labz'),
78
+ dict(label='ometotal', units='degrees', data_type='smb_par',
79
+ name='ometotal')
80
+ ]
81
+ scalar_data = []
82
+ attrs = {}
83
+ if scan_type != 0:
84
+ self.logger.warning(
85
+ 'Assuming all fly axes parameters are identical for all scans')
86
+ attrs['fly_axis_labels'] = []
87
+ axes_labels = {1: 'fly_labx', 2: 'fly_laby', 3: 'fly_labz',
88
+ 4: 'fly_ometotal'}
89
+ axes_units = {1: 'mm', 2: 'mm', 3: 'mm', 4: 'degrees'}
90
+ axes_added = []
91
+ scanparser = ScanParser(parfile.spec_file, scan_nos[0])
92
+ def add_fly_axis(fly_axis_index):
93
+ if fly_axis_index in axes_added:
94
+ return
95
+ fly_axis_key = scanparser.pars[f'fly_axis{fly_axis_index}']
96
+ independent_dimensions.append(dict(
97
+ label=axes_labels[fly_axis_key],
98
+ data_type='spec_motor',
99
+ units=axes_units[fly_axis_key],
100
+ name=scanparser.spec_scan_motor_mnes[fly_axis_index]))
101
+ axes_added.append(fly_axis_index)
102
+ attrs['fly_axis_labels'].append(axes_labels[fly_axis_key])
103
+ add_fly_axis(0)
104
+ if scan_type in (2, 3, 5):
105
+ add_fly_axis(1)
106
+ if scan_type == 5:
107
+ scalar_data.append(dict(
108
+ label='bin_axis', units='n/a', data_type='smb_par',
109
+ name='bin_axis'))
110
+ attrs['bin_axis_label'] = axes_labels[
111
+ scanparser.pars['bin_axis']].replace('fly_', '')
112
+
113
+ # Add in the usual extra scalar data maps for EDD
114
+ scalar_data.extend([
115
+ dict(label='SCAN_N', units='n/a', data_type='smb_par',
116
+ name='SCAN_N'),
117
+ dict(label='rsgap_size', units='mm', data_type='smb_par',
118
+ name='rsgap_size'),
119
+ dict(label='x_effective', units='mm', data_type='smb_par',
120
+ name='x_effective'),
121
+ dict(label='z_effective', units='mm', data_type='smb_par',
122
+ name='z_effective'),
123
+ ])
124
+
125
+ # Construct initial map config dictionary
126
+ scanparser = ScanParser(parfile.spec_file, scan_nos[0])
127
+ map_config_dict = dict(
128
+ title=f'{scanparser.scan_name}_dataset{dataset_id}',
129
+ station='id1a3',
130
+ experiment_type='EDD',
131
+ sample=dict(name=scanparser.scan_name),
132
+ spec_scans=[
133
+ dict(spec_file=parfile.spec_file, scan_numbers=scan_nos)],
134
+ independent_dimensions=independent_dimensions,
135
+ scalar_data=scalar_data,
136
+ presample_intensity=dict(name='a3ic1', data_type='scan_column'),
137
+ postsample_intensity=dict(name='diode', data_type='scan_column'),
138
+ dwell_time_actual=dict(name='sec', data_type='scan_column'),
139
+ attrs=attrs,
140
+ )
141
+ map_config = MapConfig(**map_config_dict)
142
+
143
+ # Add lab coordinates to the map's scalar_data only if they
144
+ # are NOT already one of the sqeezed map's
145
+ # independent_dimensions.
146
+ lab_dims = [
147
+ dict(label='labx', units='mm', data_type='smb_par', name='labx'),
148
+ dict(label='laby', units='mm', data_type='smb_par', name='laby'),
149
+ dict(label='labz', units='mm', data_type='smb_par', name='labz'),
150
+ dict(label='ometotal', units='degrees', data_type='smb_par',
151
+ name='ometotal')
152
+ ]
153
+ for dim in lab_dims:
154
+ if dim not in independent_dimensions:
155
+ scalar_data.append(dim)
156
+
157
+ # Convert list of scan_numbers to string notation
158
+ scan_numbers = map_config_dict['spec_scans'][0]['scan_numbers']
159
+ map_config_dict['spec_scans'][0]['scan_numbers'] = list_to_string(
160
+ scan_numbers)
161
+
162
+ # For now overrule the map type to be always unstructured
163
+ # Later take out the option of structured entirely from
164
+ # MapConfig
165
+ map_config_dict['map_type'] = 'unstructured'
166
+
167
+ return map_config_dict
168
+
169
+
170
+ class EddMPIMapReader(Reader):
171
+ """Reader for taking an EDD-style .par file and returning a
172
+ representing one of the datasets in the file as a NeXus NXentry
173
+ object. Independent dimensions are determined automatically, and a
174
+ specific set of items to use for extra scalar datasets to include
175
+ are hard-coded in."""
176
+ def read(self, parfile, dataset_id, detector_names):
177
+ """Return a NeXus NXentry object after validating the
178
+ `MapConfig` object representing an EDD dataset.
179
+
180
+ :param parfile: Name of the EDD-style .par file containing the
181
+ dataset.
182
+ :type parfile: str
183
+ :param dataset_id: Number of the dataset in the .par file
184
+ to return as a map.
185
+ :type dataset_id: int
186
+ :param detector_names: Detector prefixes for the raw data.
187
+ :type detector_names: list[str]
188
+ :returns: The EDD map including the raw data packaged with the
189
+ appropriate value for 'schema'.
190
+ :rtype: PipelineData
191
+ """
192
+ # Third party modules
193
+ from json import dumps
194
+ from nexusformat.nexus import (
195
+ NXcollection,
196
+ NXdata,
197
+ NXentry,
198
+ NXfield,
199
+ NXsample,
200
+ )
201
+
202
+ # Local modules
203
+ from CHAP.common.models.map import MapConfig
204
+ from CHAP.pipeline import PipelineData
205
+ from CHAP.utils.general import list_to_string
206
+ from CHAP.utils.parfile import ParFile
207
+ from chess_scanparsers import SMBMCAScanParser as ScanParser
208
+
209
+ assert is_str_series(detector_names, raise_error=True)
210
+
211
+ parfile = ParFile(parfile)
212
+ self.logger.debug(f'spec_file: {parfile.spec_file}')
213
+
214
+ # Get list of scan numbers for the dataset
215
+ dataset_ids = np.asarray(parfile.get_values('dataset_id'))
216
+ dataset_rows_i = np.argwhere(
217
+ np.where(
218
+ np.asarray(dataset_ids) == dataset_id, 1, 0)).flatten()
219
+ scan_nos = [parfile.data[i][parfile.scann_i] for i in dataset_rows_i\
220
+ if parfile.data[i][parfile.scann_i] in \
221
+ parfile.good_scan_numbers()]
222
+ self.logger.debug(f'Scan numbers: {scan_nos}')
223
+ spec_scans = [dict(spec_file=parfile.spec_file, scan_numbers=scan_nos)]
224
+
225
+ # Get scan type for this dataset
226
+ scan_types = parfile.get_values('scan_type', scan_numbers=scan_nos)
227
+ if any([st != scan_types[0] for st in scan_types]):
228
+ msg = 'Only one scan type per dataset is suported.'
229
+ self.logger.error(msg)
230
+ raise Exception(msg)
231
+ scan_type = scan_types[0]
232
+ self.logger.debug(f'Scan type: {scan_type}')
233
+
234
+ # Based on scan type, get independent_dimensions for the map
235
+ # Start by adding labx, laby, labz, and omega. Any "extra"
236
+ # dimensions will be sqeezed out of the map later.
237
+ independent_dimensions = [
238
+ dict(label='labx', units='mm', data_type='smb_par',
239
+ name='labx'),
240
+ dict(label='laby', units='mm', data_type='smb_par',
241
+ name='laby'),
242
+ dict(label='labz', units='mm', data_type='smb_par',
243
+ name='labz'),
244
+ dict(label='ometotal', units='degrees', data_type='smb_par',
245
+ name='ometotal')
246
+ ]
247
+ scalar_data = []
248
+ attrs = {}
249
+ if scan_type != 0:
250
+ self.logger.warning(
251
+ 'Assuming all fly axes parameters are identical for all scans')
252
+ attrs['fly_axis_labels'] = []
253
+ axes_labels = {1: 'fly_labx', 2: 'fly_laby', 3: 'fly_labz',
254
+ 4: 'fly_ometotal'}
255
+ axes_units = {1: 'mm', 2: 'mm', 3: 'mm', 4: 'degrees'}
256
+ axes_added = []
257
+ scanparser = ScanParser(parfile.spec_file, scan_nos[0])
258
+ def add_fly_axis(fly_axis_index):
259
+ if fly_axis_index in axes_added:
260
+ return
261
+ fly_axis_key = scanparser.pars[f'fly_axis{fly_axis_index}']
262
+ independent_dimensions.append(dict(
263
+ label=axes_labels[fly_axis_key],
264
+ data_type='spec_motor',
265
+ units=axes_units[fly_axis_key],
266
+ name=scanparser.spec_scan_motor_mnes[fly_axis_index]))
267
+ axes_added.append(fly_axis_index)
268
+ attrs['fly_axis_labels'].append(axes_labels[fly_axis_key])
269
+ add_fly_axis(0)
270
+ if scan_type in (2, 3, 5):
271
+ add_fly_axis(1)
272
+ if scan_type == 5:
273
+ scalar_data.append(dict(
274
+ label='bin_axis', units='n/a', data_type='smb_par',
275
+ name='bin_axis'))
276
+ attrs['bin_axis_label'] = axes_labels[
277
+ scanparser.pars['bin_axis']].replace('fly_', '')
278
+
279
+ # Add in the usual extra scalar data maps for EDD
280
+ scalar_data.extend([
281
+ dict(label='SCAN_N', units='n/a', data_type='smb_par',
282
+ name='SCAN_N'),
283
+ dict(label='rsgap_size', units='mm', data_type='smb_par',
284
+ name='rsgap_size'),
285
+ dict(label='x_effective', units='mm', data_type='smb_par',
286
+ name='x_effective'),
287
+ dict(label='z_effective', units='mm', data_type='smb_par',
288
+ name='z_effective'),
289
+ ])
290
+
291
+ # Construct initial map config dictionary
292
+ scanparser = ScanParser(parfile.spec_file, scan_nos[0])
293
+ map_config_dict = dict(
294
+ title=f'{scanparser.scan_name}_dataset{dataset_id}',
295
+ station='id1a3',
296
+ experiment_type='EDD',
297
+ sample=dict(name=scanparser.scan_name),
298
+ spec_scans=[
299
+ dict(spec_file=parfile.spec_file, scan_numbers=scan_nos)],
300
+ independent_dimensions=independent_dimensions,
301
+ scalar_data=scalar_data,
302
+ presample_intensity=dict(name='a3ic1', data_type='scan_column'),
303
+ postsample_intensity=dict(name='diode', data_type='scan_column'),
304
+ dwell_time_actual=dict(name='sec', data_type='scan_column'),
305
+ attrs=attrs
306
+ )
307
+ map_config = MapConfig(**map_config_dict)
308
+
309
+ # Squeeze out extraneous independent dimensions (dimensions
310
+ # along which data were taken at only one unique coordinate
311
+ # value)
312
+ while 1 in map_config.shape:
313
+ remove_dim_index = map_config.shape.index(1)
314
+ self.logger.debug(
315
+ 'Map dimensions: '
316
+ + str([dim["label"] for dim in independent_dimensions]))
317
+ self.logger.debug(f'Map shape: {map_config.shape}')
318
+ self.logger.debug(
319
+ 'Sqeezing out independent dimension '
320
+ + independent_dimensions[remove_dim_index]['label'])
321
+ independent_dimensions.pop(remove_dim_index)
322
+ map_config = MapConfig(**map_config_dict)
323
+ self.logger.debug(
324
+ 'Map dimensions: '
325
+ + str([dim["label"] for dim in independent_dimensions]))
326
+ self.logger.debug(f'Map shape: {map_config.shape}')
327
+
328
+ # Add lab coordinates to the map's scalar_data only if they
329
+ # are NOT already one of the sqeezed map's
330
+ # independent_dimensions.
331
+ lab_dims = [
332
+ dict(label='labx', units='mm', data_type='smb_par', name='labx'),
333
+ dict(label='laby', units='mm', data_type='smb_par', name='laby'),
334
+ dict(label='labz', units='mm', data_type='smb_par', name='labz'),
335
+ dict(label='ometotal', units='degrees', data_type='smb_par',
336
+ name='ometotal')
337
+ ]
338
+ for dim in lab_dims:
339
+ if dim not in independent_dimensions:
340
+ scalar_data.append(dim)
341
+
342
+ # Set up NXentry and add misc. CHESS-specific metadata
343
+ nxentry = NXentry(name=map_config.title)
344
+ nxentry.attrs['station'] = map_config.station
345
+ nxentry.map_config = dumps(map_config.dict())
346
+ nxentry.spec_scans = NXcollection()
347
+ for scans in map_config.spec_scans:
348
+ nxentry.spec_scans[scans.scanparsers[0].scan_name] = \
349
+ NXfield(value=scans.scan_numbers,
350
+ attrs={'spec_file': str(scans.spec_file)})
351
+
352
+ # Add sample metadata
353
+ nxentry[map_config.sample.name] = NXsample(
354
+ **map_config.sample.dict())
355
+
356
+ # Set up default data group
357
+ nxentry.data = NXdata()
358
+ independent_dimensions = map_config.independent_dimensions
359
+ for dim in independent_dimensions:
360
+ nxentry.data[dim.label] = NXfield(
361
+ units=dim.units,
362
+ attrs={'long_name': f'{dim.label} ({dim.units})',
363
+ 'data_type': dim.data_type,
364
+ 'local_name': dim.name})
365
+
366
+ # Read the raw data and independent dimensions
367
+ data = [[] for _ in detector_names]
368
+ dims = [[] for _ in independent_dimensions]
369
+ for scans in map_config.spec_scans:
370
+ for scan_number in scans.scan_numbers:
371
+ scanparser = scans.get_scanparser(scan_number)
372
+ for i, detector_name in enumerate(detector_names):
373
+ if isinstance(detector_name, int):
374
+ detector_name = str(detector_name)
375
+ ddata = scanparser.get_detector_data(detector_name)
376
+ data[i].append(ddata)
377
+ for i, dim in enumerate(independent_dimensions):
378
+ dims[i].append(dim.get_value(
379
+ scans, scan_number, scan_step_index=-1, relative=True))
380
+
381
+ return map_config_dict
382
+
383
+
384
+ class ScanToMapReader(Reader):
385
+ """Reader for turning a single SPEC scan into a MapConfig."""
386
+ def read(self, spec_file, scan_number):
387
+ """Return a dictionary representing a valid map configuration
388
+ consisting of the single SPEC scan specified.
389
+
390
+ :param spec_file: Name of the SPEC file.
391
+ :type spec_file: str
392
+ :param scan_number: Number of the SPEC scan.
393
+ :type scan_number: int
394
+ :returns: Map configuration dictionary
395
+ :rtype: dict
396
+ """
397
+ # Local modules
398
+ from chess_scanparsers import SMBMCAScanParser as ScanParser
399
+
400
+ scanparser = ScanParser(spec_file, scan_number)
401
+
402
+ if scanparser.spec_macro in ('tseries', 'loopscan') or \
403
+ (scanparser.spec_macro == 'flyscan' and \
404
+ not len(scanparser.spec_args) ==5):
405
+ independent_dimensions = [
406
+ {'label': 'Time',
407
+ 'units': 'seconds',
408
+ 'data_type': 'scan_column',
409
+ 'name': 'Time'}]
410
+ else:
411
+ independent_dimensions = [
412
+ {'label': mne,
413
+ 'units': 'unknown units',
414
+ 'data_type': 'spec_motor',
415
+ 'name': mne}
416
+ for mne in scanparser.spec_scan_motor_mnes]
417
+
418
+ map_config_dict = dict(
419
+ title=f'{scanparser.scan_name}_{scan_number:03d}',
420
+ station='id1a3',
421
+ experiment_type='EDD',
422
+ sample=dict(name=scanparser.scan_name),
423
+ spec_scans=[
424
+ dict(spec_file=spec_file, scan_numbers=[scan_number])],
425
+ independent_dimensions=independent_dimensions,
426
+ presample_intensity=dict(name='a3ic1', data_type='scan_column'),
427
+ postsample_intensity=dict(name='diode', data_type='scan_column'),
428
+ dwell_time_actual=dict(name='sec', data_type='scan_column')
429
+ )
430
+
431
+ return map_config_dict
432
+
433
+
434
+ class SetupNXdataReader(Reader):
435
+ """Reader for converting the SPEC input .txt file for EDD dataset
436
+ collection to an approporiate input argument for
437
+ `CHAP.common.SetupNXdataProcessor`.
438
+
439
+ Example of use in a `Pipeline` configuration:
440
+ ```yaml
441
+ config:
442
+ inputdir: /rawdata/samplename
443
+ outputdir: /reduceddata/samplename
444
+ pipeline:
445
+ - edd.SetupNXdataReader:
446
+ filename: SpecInput.txt
447
+ dataset_id: 1
448
+ - common.SetupNXdataProcessor:
449
+ nxname: samplename_dataset_1
450
+ - common.NexusWriter:
451
+ filename: data.nxs
452
+ ```
453
+ """
454
+ def read(self, filename, dataset_id):
455
+ """Return a dictionary containing the `coords`, `signals`, and
456
+ `attrs` arguments appropriate for use with
457
+ `CHAP.common.SetupNXdataProcessor.process` to set up an
458
+ initial `NXdata` object representing a complete and organized
459
+ structured EDD dataset.
460
+
461
+ :param filename: Name of the input .txt file provided to SPEC
462
+ for EDD dataset collection.
463
+ :type filename: str
464
+ :param dataset_id: Number of the dataset in the .txt file to
465
+ return
466
+ `CHAP.common.SetupNXdataProcessor.process`
467
+ arguments for.
468
+ :type dataset_id: int
469
+ :returns: The dataset's coordinate names, values, attributes,
470
+ and signal names, shapes, and attributes.
471
+ :rtype: dict
472
+ """
473
+ # Columns in input .txt file:
474
+ # 0: scan number
475
+ # 1: dataset index
476
+ # 2: configuration descriptor
477
+ # 3: labx
478
+ # 4: laby
479
+ # 5: labz
480
+ # 6: omega (reference)
481
+ # 7: omega (offset)
482
+ # 8: dwell time
483
+ # 9: beam width
484
+ # 10: beam height
485
+ # 11: detector slit gap width
486
+ # 12: scan type
487
+
488
+ # Following columns used only for scan types 1 and up and
489
+ # specify flyscan/flymesh parameters.
490
+ # 13 + 4n: scan direction axis index
491
+ # 14 + 4n: lower bound
492
+ # 15 + 4n: upper bound
493
+ # 16 + 4n: no. points
494
+ # (For scan types 1, 4: n = 1)
495
+ # (For scan types 2, 3, 5: n = 1 or 2)
496
+
497
+ # For scan type 5 only:
498
+ # 21: bin axis
499
+
500
+ # Parse dataset from the input .txt file.
501
+ with open(filename) as inf:
502
+ file_lines = inf.readlines()
503
+ dataset_lines = []
504
+ for l in file_lines:
505
+ vals = l.split()
506
+ for i, v in enumerate(vals):
507
+ try:
508
+ vals[i] = int(v)
509
+ except:
510
+ try:
511
+ vals[i] = float(v)
512
+ except:
513
+ pass
514
+ if vals[1] == dataset_id:
515
+ dataset_lines.append(vals)
516
+
517
+ # Start inferring coords and signals lists for EDD experiments
518
+ self.logger.warning(
519
+ 'Assuming the following parameters are identical across the '
520
+ + 'entire dataset: scan type, configuration descriptor')
521
+ scan_type = dataset_lines[0][12]
522
+ self.logger.debug(f'scan_type = {scan_type}')
523
+ coords = [
524
+ dict(name='labx',
525
+ values=np.unique([l[3] for l in dataset_lines]),
526
+ attrs=dict(
527
+ units='mm', local_name='labx', data_type='smb_par')),
528
+ dict(name='laby',
529
+ values=np.unique([l[4] for l in dataset_lines]),
530
+ attrs=dict(
531
+ units='mm', local_name='laby', data_type='smb_par')),
532
+ dict(name='labz',
533
+ values=np.unique([l[5] for l in dataset_lines]),
534
+ attrs=dict(
535
+ units='mm', local_name='labz', data_type='smb_par')),
536
+ dict(name='ometotal',
537
+ values=np.unique([l[6] + l[7] for l in dataset_lines]),
538
+ attrs=dict(
539
+ units='degrees', local_name='ometotal',
540
+ data_type='smb_par'))
541
+ ]
542
+ signals = [
543
+ dict(name='presample_intensity', shape=[],
544
+ attrs=dict(units='counts', local_name='a3ic1',
545
+ data_type='scan_column')),
546
+ dict(name='postsample_intensity', shape=[],
547
+ attrs=dict(units='counts', local_name='diode',
548
+ data_type='scan_column')),
549
+ dict(name='dwell_time_actual', shape=[],
550
+ attrs=dict(units='seconds', local_name='sec',
551
+ data_type='scan_column')),
552
+ dict(name='SCAN_N', shape=[],
553
+ attrs=dict(units='n/a', local_name='SCAN_N',
554
+ data_type='smb_par')),
555
+ dict(name='rsgap_size', shape=[],
556
+ attrs=dict(units='mm', local_name='rsgap_size',
557
+ data_type='smb_par')),
558
+ dict(name='x_effective', shape=[],
559
+ attrs=dict(units='mm', local_name='x_effective',
560
+ data_type='smb_par')),
561
+ dict(name='z_effective', shape=[],
562
+ attrs=dict(units='mm', local_name='z_effective',
563
+ data_type='smb_par')),
564
+ ]
565
+ for i in range(23):
566
+ signals.append(dict(
567
+ name=str(i), shape=[4096,],
568
+ attrs=dict(
569
+ units='counts', local_name=f'XPS23 element {i}',
570
+ eta='unknown')))
571
+
572
+ attrs = dict(dataset_id=dataset_id,
573
+ config_id=dataset_lines[0][2],
574
+ scan_type=scan_type)
575
+
576
+ # For potential coordinate axes w/ only one unique value, do
577
+ # not consider them a coordinate. Make them a signal instead.
578
+ _coords = []
579
+ for i, c in enumerate(coords):
580
+ if len(c['values']) == 1:
581
+ self.logger.debug(f'Moving {c["name"]} from coords to signals')
582
+ # signal = coords.pop(i)
583
+ del c['values']
584
+ c['shape'] = []
585
+ signals.append(c)
586
+ else:
587
+ _coords.append(c)
588
+ coords = _coords
589
+
590
+ # Append additional coords depending on the scan type of the
591
+ # dataset. Also find the number of points / scan.
592
+ if scan_type == 0:
593
+ scan_npts = 1
594
+ else:
595
+ self.logger.warning(
596
+ 'Assuming scan parameters are identical for all scans.')
597
+ axes_labels = {1: 'scan_labx', 2: 'scan_laby', 3: 'scan_labz',
598
+ 4: 'scan_ometotal'}
599
+ axes_units = {1: 'mm', 2: 'mm', 3: 'mm', 4: 'degrees'}
600
+ coords.append(
601
+ dict(name=axes_labels[dataset_lines[0][13]],
602
+ values=np.round(np.linspace(
603
+ dataset_lines[0][14], dataset_lines[0][15],
604
+ dataset_lines[0][16]), 3),
605
+ attrs=dict(units=axes_units[dataset_lines[0][13]],
606
+ relative=True)))
607
+ scan_npts = len(coords[-1]['values'])
608
+ if scan_type in (2, 3, 5):
609
+ coords.append(
610
+ dict(name=axes_labels[dataset_lines[0][17]],
611
+ values=np.round(np.linspace(
612
+ dataset_lines[0][18], dataset_lines[0][19],
613
+ dataset_lines[0][20]), 3),
614
+ attrs=dict(units=axes_units[dataset_lines[0][17]],
615
+ relative=True)))
616
+ scan_npts *= len(coords[-1]['values'])
617
+ if scan_type == 5:
618
+ attrs['bin_axis'] = axes_labels[dataset_lines[0][21]]
619
+
620
+ # Determine if the datset is structured or unstructured.
621
+ total_npts = len(dataset_lines) * scan_npts
622
+ self.logger.debug(f'Total # of points in the dataset: {total_npts}')
623
+ self.logger.debug(
624
+ 'Determined number of unique coordinate values: '
625
+ + str({c['name']: len(c['values']) for c in coords}))
626
+ coords_npts = np.prod([len(c['values']) for c in coords])
627
+ self.logger.debug(
628
+ f'If dataset is structured, # of points should be: {coords_npts}')
629
+ if coords_npts != total_npts:
630
+ attrs['unstructured_axes'] = []
631
+ self.logger.warning(
632
+ 'Dataset is unstructured. All coordinates will be treated as '
633
+ + 'singals, and the dataset will have a single coordinate '
634
+ + 'instead: data point index.')
635
+ for c in coords:
636
+ del c['values']
637
+ c['shape'] = []
638
+ signals.append(c)
639
+ attrs['unstructured_axes'].append(c['name'])
640
+ coords = [dict(name='dataset_point_index',
641
+ values=np.arange(total_npts),
642
+ attrs=dict(units='n/a'))]
643
+ else:
644
+ signals.append(dict(name='dataset_point_index', shape=[],
645
+ attrs=dict(units='n/a')))
646
+
647
+ return dict(coords=coords, signals=signals, attrs=attrs)
648
+
649
+
650
+ class UpdateNXdataReader(Reader):
651
+ """Companion to `edd.SetupNXdataReader` and
652
+ `common.UpdateNXDataProcessor`. Constructs a list of data points
653
+ to pass as pipeline data to `common.UpdateNXDataProcessor` so that
654
+ an `NXdata` constructed by `edd.SetupNXdataReader` and
655
+ `common.SetupNXdataProcessor` can be updated live as individual
656
+ scans in an EDD dataset are completed.
657
+
658
+ Example of use in a `Pipeline` configuration:
659
+ ```yaml
660
+ config:
661
+ inputdir: /rawdata/samplename
662
+ pipeline:
663
+ - edd.UpdateNXdataReader:
664
+ spec_file: spec.log
665
+ scan_number: 1
666
+ - common.SetupNXdataProcessor:
667
+ nxfilename: /reduceddata/samplename/data.nxs
668
+ nxdata_path: /entry/samplename_dataset_1
669
+ ```
670
+ """
671
+ def read(self, spec_file, scan_number, inputdir='.'):
672
+ """Return a list of data points containing raw data values for
673
+ a single EDD spec scan. The returned values can be passed
674
+ along to `common.UpdateNXdataProcessor` to fill in an existing
675
+ `NXdata` set up with `common.SetupNXdataProcessor`.
676
+
677
+ :param spec_file: Name of the spec file containing the spec
678
+ scan (a relative or absolute path).
679
+ :type spec_file: str
680
+ :param scan_number: Number of the spec scan.
681
+ :type scan_number: int
682
+ :param inputdir: Parent directory of `spec_file`, used only if
683
+ `spec_file` is a relative path. Will be ignored if
684
+ `spec_file` is an absolute path. Defaults to `'.'`.
685
+ :type inputdir: str
686
+ :returs: List of data points appropriate for input to
687
+ `common.UpdateNXdataProcessor`.
688
+ :rtype: list[dict[str, object]]
689
+ """
690
+ # Local modules
691
+ from CHAP.utils.parfile import ParFile
692
+ from chess_scanparsers import SMBMCAScanParser as ScanParser
693
+
694
+ if not os.path.isabs(spec_file):
695
+ spec_file = os.path.join(inputdir, spec_file)
696
+ scanparser = ScanParser(spec_file, scan_number)
697
+ self.logger.debug('Parsed scan')
698
+
699
+ # A label / counter mne dict for convenience
700
+ counters = dict(presample_intensity='a3ic0',
701
+ postsample_intensity='diode',
702
+ dwell_time_actual='sec')
703
+ # Determine the scan's own coordinate axes based on scan type
704
+ scan_type = scanparser.pars['scan_type']
705
+ self.logger.debug(f'scan_type = {scan_type}')
706
+ if scan_type == 0:
707
+ scan_axes = []
708
+ else:
709
+ axes_labels = {1: 'scan_labx', 2: 'scan_laby', 3: 'scan_labz',
710
+ 4: 'scan_ometotal'}
711
+ scan_axes = [axes_labels[scanparser.pars['fly_axis0']]]
712
+ if scan_type in (2, 3, 5):
713
+ scan_axes.append(axes_labels[scanparser.pars['fly_axis1']])
714
+ self.logger.debug(f'Determined scan axes: {scan_axes}')
715
+
716
+ # Par file values will be the same for all points in any scan
717
+ smb_par_values = {}
718
+ for smb_par in ('labx', 'laby', 'labz', 'ometotal', 'SCAN_N',
719
+ 'rsgap_size', 'x_effective', 'z_effective'):
720
+ smb_par_values[smb_par] = scanparser.pars[smb_par]
721
+
722
+ # Get offset for the starting index of this scan's points in
723
+ # the entire dataset.
724
+ dataset_id = scanparser.pars['dataset_id']
725
+ parfile = ParFile(scanparser._par_file)
726
+ good_scans = parfile.good_scan_numbers()
727
+ n_prior_dataset_scans = sum(
728
+ [1 if did == dataset_id and scan_n < scan_number else 0 \
729
+ for did, scan_n in zip(
730
+ parfile.get_values(
731
+ 'dataset_id', scan_numbers=good_scans),
732
+ good_scans)])
733
+ dataset_point_index_offset = n_prior_dataset_scans \
734
+ * scanparser.spec_scan_npts
735
+ self.logger.debug(
736
+ f'dataset_point_index_offset = {dataset_point_index_offset}')
737
+
738
+ # Get full data point for every point in the scan
739
+ data_points = []
740
+ self.logger.info(f'Getting {scanparser.spec_scan_npts} data points')
741
+ for i in range(scanparser.spec_scan_npts):
742
+ self.logger.debug(f'Getting data point for scan step index {i}')
743
+ step = scanparser.get_scan_step(i)
744
+ data_points.append(dict(
745
+ dataset_point_index=dataset_point_index_offset + i,
746
+ **smb_par_values,
747
+ **{str(_i): scanparser.get_detector_data(_i, i) \
748
+ for _i in range(23)},
749
+ **{c: scanparser.spec_scan_data[counters[c]][i] \
750
+ for c in counters},
751
+ **{a: round(
752
+ scanparser.spec_scan_motor_vals_relative[_i][step[_i]], 3)\
753
+ for _i, a in enumerate(scan_axes)}))
754
+
755
+ return data_points
756
+
757
+
758
+ class NXdataSliceReader(Reader):
759
+ """Reader for returning a sliced verison of an `NXdata` (which
760
+ represents a full EDD dataset) that contains data from just a
761
+ single SPEC scan.
762
+
763
+ Example of use in a `Pipeline` configuration:
764
+ ```yaml
765
+ config:
766
+ inputdir: /rawdata/samplename
767
+ outputdir: /reduceddata/samplename
768
+ pipeline:
769
+ - edd.NXdataSliceReader:
770
+ filename: /reduceddata/samplename/data.nxs
771
+ nxpath: /path/to/nxdata
772
+ spec_file: spec.log
773
+ scan_number: 1
774
+ - common.NexusWriter:
775
+ filename: scan_1.nxs
776
+ ```
777
+ """
778
+ def read(self, filename, nxpath, spec_file, scan_number, inputdir='.'):
779
+ """Return a "slice" of an EDD dataset's NXdata that represents
780
+ just the data from one scan in the dataset.
781
+
782
+ :param filename: Name of the NeXus file in which the
783
+ existing full EDD dataset's NXdata resides.
784
+ :type filename: str
785
+ :param nxpath: Path to the existing full EDD dataset's NXdata
786
+ group in `filename`.
787
+ :type nxpath: str
788
+ :param spec_file: Name of the spec file containing whose data
789
+ will be the only contents of the returned `NXdata`.
790
+ :type spec_file: str
791
+ :param scan_number: Number of the spec scan whose data will be
792
+ the only contents of the returned `NXdata`.
793
+ :type scan_number: int
794
+ :param inputdir: Directory containing `filename` and/or
795
+ `spec_file`, if either one / both of them are not absolute
796
+ paths. Defaults to `'.'`.
797
+ :type inputdir: str, optional
798
+ :returns: An `NXdata` similar to the one at `nxpath` in
799
+ `filename`, but containing only the data collected by the
800
+ specified spec scan.
801
+ :rtype: nexusformat.nexus.NXdata
802
+ """
803
+ # Third party modules
804
+ from nexusformat.nexus import nxload
805
+
806
+ # Local modules
807
+ from CHAP.common import NXdataReader
808
+ from CHAP.utils.parfile import ParFile
809
+ from chess_scanparsers import SMBMCAScanParser as ScanParser
810
+
811
+ # Parse existing NXdata
812
+ root = nxload(filename)
813
+ nxdata = root[nxpath]
814
+ if nxdata.nxclass != 'NXdata':
815
+ raise TypeError(
816
+ f'Object at {nxpath} in {filename} is not an NXdata')
817
+ self.logger.debug('Loaded existing NXdata')
818
+
819
+ # Parse scan
820
+ if not os.path.isabs(spec_file):
821
+ spec_file = os.path.join(inputdir, spec_file)
822
+ scanparser = ScanParser(spec_file, scan_number)
823
+ self.logger.debug('Parsed scan')
824
+
825
+ # Assemble arguments for NXdataReader
826
+ name = f'{nxdata.nxname}_scan_{scan_number}'
827
+ axes_names = [a.nxname for a in nxdata.nxaxes]
828
+ if nxdata.nxsignal is not None:
829
+ signal_name = nxdata.nxsignal.nxname
830
+ else:
831
+ signal_name = list(nxdata.entries.keys())[0]
832
+ attrs = nxdata.attrs
833
+ nxfield_params = []
834
+ if 'dataset_point_index' in nxdata:
835
+ # Get offset for the starting index of this scan's points in
836
+ # the entire dataset.
837
+ dataset_id = scanparser.pars['dataset_id']
838
+ parfile = ParFile(scanparser._par_file)
839
+ good_scans = parfile.good_scan_numbers()
840
+ n_prior_dataset_scans = sum(
841
+ [1 if did == dataset_id and scan_n < scan_number else 0 \
842
+ for did, scan_n in zip(
843
+ parfile.get_values(
844
+ 'dataset_id', scan_numbers=good_scans),
845
+ good_scans)])
846
+ dataset_point_index_offset = n_prior_dataset_scans \
847
+ * scanparser.spec_scan_npts
848
+ self.logger.debug(
849
+ f'dataset_point_index_offset = {dataset_point_index_offset}')
850
+ slice_params = dict(
851
+ start=dataset_point_index_offset,
852
+ end=dataset_point_index_offset + scanparser.spec_scan_npts + 1)
853
+ nxfield_params = [dict(filename=filename,
854
+ nxpath=entry.nxpath,
855
+ slice_params=[slice_params]) \
856
+ for entry in nxdata]
857
+ else:
858
+ signal_slice_params = []
859
+ for a in nxdata.nxaxes:
860
+ if a.nxname.startswith('scan_'):
861
+ slice_params = {}
862
+ else:
863
+ value = scanparser.pars[a.nxname]
864
+ try:
865
+ index = np.where(a.nxdata == value)[0][0]
866
+ except:
867
+ index = np.argmin(np.abs(a.nxdata - value))
868
+ self.logger.warning(
869
+ f'Nearest match for coordinate value {a.nxname}: '
870
+ f'{a.nxdata[index]} (actual value: {value})')
871
+ slice_params = dict(start=index, end=index+1)
872
+ signal_slice_params.append(slice_params)
873
+ nxfield_params.append(dict(
874
+ filename=filename,
875
+ nxpath=os.path.join(nxdata.nxpath, a.nxname),
876
+ slice_params=[slice_params]))
877
+ for name, entry in nxdata.entries.items():
878
+ if entry in nxdata.nxaxes:
879
+ continue
880
+ nxfield_params.append(dict(
881
+ filename=filename, nxpath=entry.nxpath,
882
+ slice_params=signal_slice_params))
883
+
884
+ # Return the "sliced" NXdata
885
+ reader = NXdataReader()
886
+ reader.logger = self.logger
887
+ return reader.read(name=nxdata.nxname, nxfield_params=nxfield_params,
888
+ signal_name=signal_name, axes_names=axes_names,
889
+ attrs=attrs)
890
+
891
+
3
892
  if __name__ == '__main__':
4
893
  from CHAP.reader import main
5
894
  main()