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