ChessAnalysisPipeline 0.0.12__py3-none-any.whl → 0.0.14__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/processor.py CHANGED
@@ -8,19 +8,165 @@ Description: Module for Processors used in multiple experiment-specific
8
8
  workflows.
9
9
  """
10
10
 
11
- # system modules
12
- from json import dumps
13
- from time import time
11
+ # Third party modules
12
+ import numpy as np
14
13
 
15
- # local modules
14
+ # Local modules
16
15
  from CHAP import Processor
17
16
 
18
17
 
18
+ class AnimationProcessor(Processor):
19
+ """A Processor to show and return an animation.
20
+ """
21
+ def process(
22
+ self, data, num_frames, vmin=None, vmax=None, axis=None,
23
+ interval=1000, blit=True, repeat=True, repeat_delay=1000,
24
+ interactive=False):
25
+ """Show and return an animation of image slices from a dataset
26
+ contained in `data`.
27
+
28
+ :param data: Input data.
29
+ :type data: list[PipelineData]
30
+ :param num_frames: Number of frames for the animation.
31
+ :type num_frames: int
32
+ :param vmin: Minimum array value in image slice, default to
33
+ `None`, which uses the actual minimum value in the slice.
34
+ :type vmin: float
35
+ :param vmax: Maximum array value in image slice, default to
36
+ `None`, which uses the actual maximum value in the slice.
37
+ :type vmax: float
38
+ :param axis: Axis direction or name of the image slices,
39
+ defaults to `0`
40
+ :type axis: Union[int, str], optional
41
+ :param interval: Delay between frames in milliseconds (only
42
+ used when interactive=True), defaults to `1000`
43
+ :type interval: int, optional
44
+ :param blit: Whether blitting is used to optimize drawing,
45
+ default to `True`
46
+ :type blit: bool, optional
47
+ :param repeat: Whether the animation repeats when the sequence
48
+ of frames is completed (only used when interactive=True),
49
+ defaults to `True`
50
+ :type repeat: bool, optional
51
+ :param repeat_delay: Delay in milliseconds between consecutive
52
+ animation runs if repeat is `True` (only used when
53
+ interactive=True), defaults to `1000`
54
+ :type repeat_delay: int, optional
55
+ :param interactive: Allows for user interactions, defaults to
56
+ `False`.
57
+ :type interactive: bool, optional
58
+ :return: The matplotlib animation.
59
+ :rtype: matplotlib.animation.ArtistAnimation
60
+ """
61
+ # System modules
62
+ from os.path import (
63
+ isabs,
64
+ join,
65
+ )
66
+
67
+ # Third party modules
68
+ import matplotlib.animation as animation
69
+ import matplotlib.pyplot as plt
70
+
71
+ # Get the default Nexus NXdata object
72
+ data = self.unwrap_pipelinedata(data)[0]
73
+ try:
74
+ nxdata = data.get_default()
75
+ except:
76
+ if nxdata.nxclass != 'NXdata':
77
+ raise ValueError('Invalid default pathway to an NXdata object '
78
+ f'in ({data})')
79
+
80
+ # Get the frames
81
+ axes = nxdata.attrs.get('axes', None)
82
+ title = f'{nxdata.nxpath}/{nxdata.signal}'
83
+ if nxdata.nxsignal.ndim == 2:
84
+ exit('AnimationProcessor not tested yet for a 2D dataset')
85
+ elif nxdata.nxsignal.ndim == 3:
86
+ if isinstance(axis, int):
87
+ if not 0 <= axis < nxdata.nxsignal.ndim:
88
+ raise ValueError(f'axis index out of range ({axis} not in '
89
+ f'[0, {nxdata.nxsignal.ndim-1}])')
90
+ axis_name = 'axis {axis}'
91
+ elif isinstance(axis, str):
92
+ if axes is None or axis not in list(axes.nxdata):
93
+ raise ValueError(
94
+ f'Unable to match axis = {axis} in {nxdata.tree}')
95
+ axes = list(axes.nxdata)
96
+ axis_name = axis
97
+ axis = axes.index(axis)
98
+ else:
99
+ raise ValueError(f'Invalid parameter axis ({axis})')
100
+ delta = int(nxdata.nxsignal.shape[axis]/(num_frames+1))
101
+ indices = np.linspace(
102
+ delta, nxdata.nxsignal.shape[axis]-delta, num_frames)
103
+ if not axis:
104
+ frames = [nxdata[nxdata.signal][int(index),:,:]
105
+ for index in indices]
106
+ elif axis == 1:
107
+ frames = [nxdata[nxdata.signal][:,int(index),:]
108
+ for index in indices]
109
+ elif axis == 2:
110
+ frames = [nxdata[nxdata.signal][:,:,int(index)]
111
+ for index in indices]
112
+ if axes is None:
113
+ axes = [i for i in range(3) if i != axis]
114
+ row_coords = range(a.shape[1])
115
+ row_label = f'axis {axes[1]} index'
116
+ column_coords = range(a.shape[0])
117
+ column_label = f'axis {axes[0]} index'
118
+ else:
119
+ axes.pop(axis)
120
+ row_coords = nxdata[axes[1]].nxdata
121
+ row_label = axes[1]
122
+ if 'units' in nxdata[axes[1]].attrs:
123
+ row_label += f' ({nxdata[axes[1]].units})'
124
+ column_coords = nxdata[axes[0]].nxdata
125
+ column_label = axes[0]
126
+ if 'units' in nxdata[axes[0]].attrs:
127
+ column_label += f' ({nxdata[axes[0]].units})'
128
+ else:
129
+ raise ValueError('Invalid data dimension (must be 2D or 3D)')
130
+
131
+
132
+ # Create the movie
133
+ if vmin is None or vmax is None:
134
+ a_max = frames[0].max()
135
+ for n in range(1, num_frames):
136
+ a_max = min(a_max, frames[n].max())
137
+ if vmin is None:
138
+ vmin = -a_max
139
+ if vmax is None:
140
+ vmax = a_max
141
+ extent = (
142
+ row_coords[0], row_coords[-1], column_coords[-1], column_coords[0])
143
+ fig, ax = plt.subplots(figsize=(11, 8.5))
144
+ ax.set_title(title, fontsize='xx-large', pad=20)
145
+ ax.set_xlabel(row_label, fontsize='x-large')
146
+ ax.set_ylabel(column_label, fontsize='x-large')
147
+ fig.tight_layout()
148
+ ims = [[plt.imshow(
149
+ frames[n], extent=extent, origin='lower',
150
+ vmin=vmin, vmax=vmax, cmap='gray',
151
+ animated=True)]
152
+ for n in range(num_frames)]
153
+ plt.colorbar()
154
+ if interactive:
155
+ ani = animation.ArtistAnimation(
156
+ fig, ims, interval=interval, blit=blit, repeat=repeat,
157
+ repeat_delay=repeat_delay)
158
+ plt.show()
159
+ else:
160
+ ani = animation.ArtistAnimation(fig, ims, blit=blit)
161
+
162
+ return ani
163
+
164
+
19
165
  class AsyncProcessor(Processor):
20
166
  """A Processor to process multiple sets of input data via asyncio
21
- module
167
+ module.
22
168
 
23
- :ivar mgr: The `Processor` used to process every set of input data
169
+ :ivar mgr: The `Processor` used to process every set of input data.
24
170
  :type mgr: Processor
25
171
  """
26
172
  def __init__(self, mgr):
@@ -31,31 +177,31 @@ class AsyncProcessor(Processor):
31
177
  """Asynchronously process the input documents with the
32
178
  `self.mgr` `Processor`.
33
179
 
34
- :param data: input data documents to process
180
+ :param data: Input data documents to process.
35
181
  :type docs: iterable
36
182
  """
37
-
183
+ # System modules
38
184
  import asyncio
39
185
 
40
186
  async def task(mgr, doc):
41
- """Process given data using provided `Processor`
187
+ """Process given data using provided `Processor`.
42
188
 
43
- :param mgr: the object that will process given data
189
+ :param mgr: The object that will process given data.
44
190
  :type mgr: Processor
45
- :param doc: the data to process
191
+ :param doc: The data to process.
46
192
  :type doc: object
47
- :return: processed data
193
+ :return: The processed data.
48
194
  :rtype: object
49
195
  """
50
196
  return mgr.process(doc)
51
197
 
52
198
  async def execute_tasks(mgr, docs):
53
199
  """Process given set of documents using provided task
54
- manager
200
+ manager.
55
201
 
56
- :param mgr: the object that will process all documents
202
+ :param mgr: The object that will process all documents.
57
203
  :type mgr: Processor
58
- :param docs: the set of data documents to process
204
+ :param docs: The set of data documents to process.
59
205
  :type doc: iterable
60
206
  """
61
207
  coroutines = [task(mgr, d) for d in docs]
@@ -64,22 +210,574 @@ class AsyncProcessor(Processor):
64
210
  asyncio.run(execute_tasks(self.mgr, data))
65
211
 
66
212
 
67
- class IntegrationProcessor(Processor):
68
- """A processor for integrating 2D data with pyFAI"""
213
+ class BinarizeProcessor(Processor):
214
+ """A Processor to binarize a dataset.
215
+ """
216
+ def process(
217
+ self, data, nxpath='', interactive=False, method='CHAP',
218
+ num_bin=256, axis=None, remove_original_data=False):
219
+ """Show and return a binarized dataset from a dataset
220
+ contained in `data`. The dataset must either be of type
221
+ `numpy.ndarray` or a NeXus NXobject object with a default path
222
+ to a NeXus NXfield object.
223
+
224
+ :param data: Input data.
225
+ :type data: list[PipelineData]
226
+ :param nxpath: The relative path to a specific NeXus NXentry or
227
+ NeXus NXdata object in the NeXus file tree to read the
228
+ input data from (ignored for Numpy or NeXus NXfield input
229
+ datasets), defaults to `''`
230
+ :type nxpath: str, optional
231
+ :param interactive: Allows for user interactions (ignored
232
+ for any method other than `'manual'`), defaults to `False`.
233
+ :type interactive: bool, optional
234
+ :param method: Binarization method, defaults to `'CHAP'`
235
+ (CHAP's internal implementation of Otzu's method).
236
+ :type method: Literal['CHAP', 'manual', 'otsu', 'yen', 'isodata',
237
+ 'minimum']
238
+ :param num_bin: The number of bins used to calculate the
239
+ histogram in the binarization algorithms (ignored for
240
+ method = `'manual'`), defaults to `256`.
241
+ :type num_bin: int, optional
242
+ :param axis: Axis direction of the image slices (ignored
243
+ for any method other than `'manual'`), defaults to `None`
244
+ :type axis: int, optional
245
+ :param remove_original_data: Removes the original data field
246
+ (ignored for Numpy input datasets), defaults to `False`.
247
+ :type force_remove_original_data: bool, optional
248
+ :raises ValueError: Upon invalid input parameters.
249
+ :return: The binarized dataset with a return type equal to
250
+ that of the input dataset.
251
+ :rtype: numpy.ndarray, nexusformat.nexus.NXobject
252
+ """
253
+ # System modules
254
+ from os.path import join as os_join
255
+ from os.path import relpath
256
+
257
+ # Local modules
258
+ from CHAP.utils.general import (
259
+ is_int,
260
+ nxcopy,
261
+ )
262
+ from nexusformat.nexus import (
263
+ NXdata,
264
+ NXfield,
265
+ NXlink,
266
+ NXprocess,
267
+ nxsetconfig,
268
+ )
269
+
270
+ if method not in [
271
+ 'CHAP', 'manual', 'otsu', 'yen', 'isodata', 'minimum']:
272
+ raise ValueError(f'Invalid parameter method ({method})')
273
+ if not is_int(num_bin, gt=0):
274
+ raise ValueError(f'Invalid parameter num_bin ({num_bin})')
275
+ if not isinstance(remove_original_data, bool):
276
+ raise ValueError('Invalid parameter remove_original_data '
277
+ f'({remove_original_data})')
278
+
279
+ nxsetconfig(memory=100000)
280
+
281
+ # Get the dataset and make a copy if it is a NeXus NXgroup
282
+ dataset = self.unwrap_pipelinedata(data)[-1]
283
+ if isinstance(dataset, np.ndarray):
284
+ if method == 'manual':
285
+ if axis is not None and not is_int(axis, gt=0, lt=3):
286
+ raise ValueError(f'Invalid parameter axis ({axis})')
287
+ axes = ['i', 'j', 'k']
288
+ data = dataset
289
+ elif isinstance(dataset, NXfield):
290
+ if method == 'manual':
291
+ if axis is not None and not is_int(axis, gt=0, lt=3):
292
+ raise ValueError(f'Invalid parameter axis ({axis})')
293
+ axes = ['i', 'j', 'k']
294
+ if isinstance(dataset, NXfield):
295
+ if nxpath not in ('', '/'):
296
+ self.logger.warning('Ignoring parameter nxpath')
297
+ data = dataset.nxdata
298
+ else:
299
+ try:
300
+ data = dataset[nxpath].nxdata
301
+ except:
302
+ raise ValueError(f'Invalid parameter nxpath ({nxpath})')
303
+ else:
304
+ # Get the default Nexus NXdata object
305
+ try:
306
+ nxdefault = dataset.get_default()
307
+ except:
308
+ nxdefault = None
309
+ if nxdefault is not None and nxdefault.nxclass != 'NXdata':
310
+ raise ValueError('Invalid default pathway NXobject type '
311
+ f'({nxdefault.nxclass})')
312
+ # Get the requested NeXus NXdata object to binarize
313
+ if nxpath is None:
314
+ nxclass = dataset.nxclass
315
+ else:
316
+ try:
317
+ nxclass = dataset[nxpath].nxclass
318
+ except:
319
+ raise ValueError(f'Invalid parameter nxpath ({nxpath})')
320
+ if nxclass == 'NXdata':
321
+ nxdata = dataset[nxpath]
322
+ else:
323
+ if nxdefault is None:
324
+ raise ValueError(f'No default pathway to a NXdata object')
325
+ nxdata = nxdefault
326
+ nxsignal = nxdata.nxsignal
327
+ if method == 'manual':
328
+ if hasattr(nxdata.attrs, 'axes'):
329
+ axes = nxdata.attrs['axes']
330
+ if isinstance(axis, str):
331
+ if axis not in axes:
332
+ raise ValueError(f'Invalid parameter axis ({axis})')
333
+ axis = axes.index(axis)
334
+ elif axis is not None and not is_int(axis, gt=0, lt=3):
335
+ raise ValueError(f'Invalid parameter axis ({axis})')
336
+ else:
337
+ axes = ['i', 'j', 'k']
338
+ if nxsignal.ndim != 3:
339
+ raise ValueError('Invalid data dimension (must be 3D)')
340
+ data = nxsignal.nxdata
341
+ # Create a copy of the input NeXus object, removing the
342
+ # default NeXus NXdata object as well as the original
343
+ # dateset if the remove_original_data parameter is set
344
+ exclude_nxpaths = []
345
+ if nxdefault is not None:
346
+ exclude_nxpaths.append(
347
+ os_join(relpath(nxdefault.nxpath, dataset.nxpath)))
348
+ if remove_original_data:
349
+ if (nxdefault is None
350
+ or nxdefault.nxpath != nxdata.nxpath):
351
+ relpath_nxdata = relpath(nxdata.nxpath, dataset.nxpath)
352
+ keys = list(nxdata.keys())
353
+ keys.remove(nxsignal.nxname)
354
+ for axis in nxdata.axes:
355
+ keys.remove(axis)
356
+ if len(keys):
357
+ raise RuntimeError('Not tested yet')
358
+ exclude_nxpaths.append(os_join(
359
+ relpath(nxsignal.nxpath, dataset.nxpath)))
360
+ elif relpath_nxdata == '.':
361
+ exclude_nxpaths.append(nxsignal.nxname)
362
+ if dataset.nxclass != 'NXdata':
363
+ exclude_nxpaths += nxdata.axes
364
+ else:
365
+ exclude_nxpaths.append(relpath_nxdata)
366
+ if not (dataset.nxclass == 'NXdata'
367
+ or nxdata.nxsignal.nxtarget is None):
368
+ nxsignal = dataset[nxsignal.nxtarget]
369
+ nxgroup = nxsignal.nxgroup
370
+ keys = list(nxgroup.keys())
371
+ keys.remove(nxsignal.nxname)
372
+ for axis in nxgroup.axes:
373
+ keys.remove(axis)
374
+ if len(keys):
375
+ raise RuntimeError('Not tested yet')
376
+ exclude_nxpaths.append(os_join(
377
+ relpath(nxsignal.nxpath, dataset.nxpath)))
378
+ else:
379
+ exclude_nxpaths.append(os_join(
380
+ relpath(nxgroup.nxpath, dataset.nxpath)))
381
+ nxobject = nxcopy(dataset, exclude_nxpaths=exclude_nxpaths)
382
+
383
+ # Get a histogram of the data
384
+ if method not in ['manual', 'yen']:
385
+ counts, edges = np.histogram(data, bins=num_bin)
386
+ centers = edges[:-1] + 0.5 * np.diff(edges)
387
+
388
+ # Calculate the data cutoff threshold
389
+ if method == 'CHAP':
390
+ weights = np.cumsum(counts)
391
+ means = np.cumsum(counts * centers)
392
+ weights = weights[0:-1]/weights[-1]
393
+ means = means[0:-1]/means[-1]
394
+ variances = (means-weights)**2/(weights*(1.-weights))
395
+ threshold = centers[np.argmax(variances)]
396
+ elif method == 'otsu':
397
+ # Third party modules
398
+ from skimage.filters import threshold_otsu
399
+
400
+ threshold = threshold_otsu(hist=(counts, centers))
401
+ elif method == 'yen':
402
+ # Third party modules
403
+ from skimage.filters import threshold_yen
404
+
405
+ _min = data.min()
406
+ _max = data.max()
407
+ data = 1+(num_bin-1)*(data-_min)/(_max-_min)
408
+ counts, edges = np.histogram(data, bins=num_bin)
409
+ centers = edges[:-1] + 0.5 * np.diff(edges)
410
+
411
+ threshold = threshold_yen(hist=(counts, centers))
412
+ elif method == 'isodata':
413
+ # Third party modules
414
+ from skimage.filters import threshold_isodata
415
+
416
+ threshold = threshold_isodata(hist=(counts, centers))
417
+ elif method == 'minimum':
418
+ # Third party modules
419
+ from skimage.filters import threshold_minimum
420
+
421
+ threshold = threshold_minimum(hist=(counts, centers))
422
+ else:
423
+ # Third party modules
424
+ import matplotlib.pyplot as plt
425
+ from matplotlib.widgets import RadioButtons, Button
426
+
427
+ # Local modules
428
+ from CHAP.utils.general import (
429
+ select_roi_1d,
430
+ select_roi_2d,
431
+ )
432
+
433
+ def select_direction(direction):
434
+ """Callback function for the "Select direction" input."""
435
+ selected_direction.append(radio_btn.value_selected)
436
+ plt.close()
437
+
438
+ def accept(event):
439
+ """Callback function for the "Accept" button."""
440
+ selected_direction.append(radio_btn.value_selected)
441
+ plt.close()
442
+
443
+ # Select the direction for data averaging
444
+ if axis is not None:
445
+ mean_data = data.mean(axis=axis)
446
+ subaxes = [i for i in range(3) if i != axis]
447
+ else:
448
+ selected_direction = []
449
+
450
+ # Setup figure
451
+ title_pos = (0.5, 0.95)
452
+ title_props = {'fontsize': 'xx-large',
453
+ 'horizontalalignment': 'center',
454
+ 'verticalalignment': 'bottom'}
455
+ fig, axs = plt.subplots(ncols=3, figsize=(17, 8.5))
456
+ mean_data = []
457
+ for i, ax in enumerate(axs):
458
+ mean_data.append(data.mean(axis=i))
459
+ subaxes = [a for a in axes if a != axes[i]]
460
+ ax.imshow(mean_data[i], aspect='auto', cmap='gray')
461
+ ax.set_title(
462
+ f'Data averaged in {axes[i]}-direction',
463
+ fontsize='x-large')
464
+ ax.set_xlabel(subaxes[1], fontsize='x-large')
465
+ ax.set_ylabel(subaxes[0], fontsize='x-large')
466
+ fig_title = plt.figtext(
467
+ *title_pos,
468
+ 'Select a direction or press "Accept" for the default one '
469
+ f'({axes[0]}) to obtain the binary threshold value',
470
+ **title_props)
471
+ fig.subplots_adjust(bottom=0.25, top=0.85)
472
+
473
+ # Setup RadioButtons
474
+ select_text = plt.figtext(
475
+ 0.225, 0.175, 'Averaging direction', fontsize='x-large',
476
+ horizontalalignment='center', verticalalignment='center')
477
+ radio_btn = RadioButtons(
478
+ plt.axes([0.175, 0.05, 0.1, 0.1]), labels=axes, active=0)
479
+ radio_cid = radio_btn.on_clicked(select_direction)
480
+
481
+ # Setup "Accept" button
482
+ accept_btn = Button(
483
+ plt.axes([0.7, 0.05, 0.15, 0.075]), 'Accept')
484
+ accept_cid = accept_btn.on_clicked(accept)
485
+
486
+ plt.show()
487
+
488
+ axis = axes.index(selected_direction[0])
489
+ mean_data = mean_data[axis]
490
+ subaxes = [a for a in axes if a != axes[axis]]
491
+
492
+ plt.close()
493
+
494
+ # Select the ROI's orthogonal to the selected averaging direction
495
+ bounds = []
496
+ for i, bound in enumerate(['"0"', '"1"']):
497
+ _, roi = select_roi_2d(
498
+ mean_data,
499
+ title=f'Select the ROI to obtain the {bound} data value',
500
+ title_a=f'Data averaged in the {axes[axis]}-direction',
501
+ row_label=subaxes[0], column_label=subaxes[1])
502
+ plt.close()
503
+
504
+ # Select the index range in the selected averaging direction
505
+ if not axis:
506
+ mean_roi_data = data[:,roi[2]:roi[3],roi[0]:roi[1]].mean(
507
+ axis=(1,2))
508
+ elif axis == 1:
509
+ mean_roi_data = data[roi[2]:roi[3],:,roi[0]:roi[1]].mean(
510
+ axis=(0,2))
511
+ elif axis == 2:
512
+ mean_roi_data = data[roi[2]:roi[3],roi[0]:roi[1],:].mean(
513
+ axis=(0,1))
514
+
515
+ _, _range = select_roi_1d(
516
+ mean_roi_data, preselected_roi=(0, data.shape[axis]),
517
+ title=f'Select the {axes[axis]}-direction range to obtain '
518
+ f'the {bound} data bound',
519
+ xlabel=axes[axis], ylabel='Average data')
520
+ plt.close()
521
+
522
+ # Obtain the lower/upper data bound
523
+ if not axis:
524
+ bounds.append(
525
+ data[
526
+ _range[0]:_range[1],roi[2]:roi[3],roi[0]:roi[1]
527
+ ].mean())
528
+ elif axis == 1:
529
+ bounds.append(
530
+ data[
531
+ roi[2]:roi[3],_range[0]:_range[1],roi[0]:roi[1]
532
+ ].mean())
533
+ elif axis == 2:
534
+ bounds.append(
535
+ data[
536
+ roi[2]:roi[3],roi[0]:roi[1],_range[0]:_range[1]
537
+ ].mean())
538
+
539
+ # Get the data cutoff threshold
540
+ threshold = np.mean(bounds)
541
+
542
+ # Apply the data cutoff threshold and return the output
543
+ data = np.where(data<threshold, 0, 1).astype(np.ubyte)
544
+ # from CHAP.utils.general import quick_imshow
545
+ # quick_imshow(data[int(data.shape[0]/2),:,:], block=True)
546
+ # quick_imshow(data[:,int(data.shape[1]/2),:], block=True)
547
+ # quick_imshow(data[:,:,int(data.shape[2]/2)], block=True)
548
+ if isinstance(dataset, np.ndarray):
549
+ return data
550
+ if isinstance(dataset, NXfield):
551
+ attrs = dataset.attrs
552
+ attrs.pop('target', None)
553
+ return NXfield(
554
+ value=data, name=dataset.nxname, attrs=dataset.attrs)
555
+ name = nxsignal.nxname + '_binarized'
556
+ if nxobject.nxclass == 'NXdata':
557
+ nxobject[name] = data
558
+ nxobject.attrs['signal'] = name
559
+ return nxobject
560
+ if nxobject.nxclass == 'NXroot':
561
+ nxentry = nxobject[nxobject.default]
562
+ else:
563
+ nxentry = nxobject
564
+ axes = []
565
+ for axis in nxdata.axes:
566
+ attrs = nxdata[axis].attrs
567
+ attrs.pop('target', None)
568
+ axes.append(
569
+ NXfield(nxdata[axis], name=axis, attrs=attrs))
570
+ nxentry[name] = NXprocess(
571
+ NXdata(NXfield(data, name=name), axes),
572
+ attrs={'source': nxsignal.nxpath})
573
+ nxdata = nxentry[name].data
574
+ nxentry.data = NXdata(
575
+ NXlink(nxdata.nxsignal.nxpath),
576
+ [NXlink(os_join(nxdata.nxpath, axis)) for axis in nxdata.axes])
577
+ return nxobject
578
+
579
+
580
+ class ImageProcessor(Processor):
581
+ """A Processor to plot an image (slice) from a NeXus object.
582
+ """
583
+ def process(
584
+ self, data, vmin=None, vmax=None, axis=0, index=None,
585
+ coord=None, interactive=False, save_figure=True, outputdir='.',
586
+ filename='image.png'):
587
+ """Plot and/or save an image (slice) from a NeXus NXobject object with
588
+ a default data path contained in `data` and return the NeXus NXdata
589
+ data object.
590
+
591
+ :param data: Input data.
592
+ :type data: list[PipelineData]
593
+ :param vmin: Minimum array value in image slice, default to
594
+ `None`, which uses the actual minimum value in the slice.
595
+ :type vmin: float
596
+ :param vmax: Maximum array value in image slice, default to
597
+ `None`, which uses the actual maximum value in the slice.
598
+ :type vmax: float
599
+ :param axis: Axis direction or name of the image slice,
600
+ defaults to `0`
601
+ :type axis: Union[int, str], optional
602
+ :param index: Array index of the slice of data to plot,
603
+ defaults to `None`
604
+ :type index: int, optional
605
+ :param coord: Coordinate value of the slice of data to plot,
606
+ defaults to `None`
607
+ :type coord: Union[int, float], optional
608
+ :param interactive: Allows for user interactions, defaults to
609
+ `False`.
610
+ :type interactive: bool, optional
611
+ :param save_figure: Save a .png of the image, defaults to `True`.
612
+ :type save_figure: bool, optional
613
+ :param outputdir: Directory to which any output figure will
614
+ be saved, defaults to `'.'`
615
+ :type outputdir: str, optional
616
+ :param filename: Image filename, defaults to `"image.png"`.
617
+ :type filename: str, optional
618
+ :return: The input data object.
619
+ :rtype: nexusformat.nexus.NXdata
620
+ """
621
+ # System modules
622
+ from os.path import (
623
+ isabs,
624
+ join,
625
+ )
626
+
627
+ # Third party modules
628
+ import matplotlib.pyplot as plt
629
+
630
+ # Local modules
631
+ from CHAP.utils.general import index_nearest
632
+
633
+ # Validate input parameters
634
+ if not isinstance(interactive, bool):
635
+ raise ValueError(f'Invalid parameter interactive ({interactive})')
636
+ if not isinstance(save_figure, bool):
637
+ raise ValueError(f'Invalid parameter save_figure ({save_figure})')
638
+ if not isinstance(outputdir, str):
639
+ raise ValueError(f'Invalid parameter outputdir ({outputdir})')
640
+ if not isinstance(filename, str):
641
+ raise ValueError(f'Invalid parameter filename ({filename})')
642
+ if not isabs(filename):
643
+ filename = join(outputdir, filename)
644
+
645
+ # Get the default Nexus NXdata object
646
+ data = self.unwrap_pipelinedata(data)[0]
647
+ try:
648
+ nxdata = data.get_default()
649
+ except:
650
+ if nxdata.nxclass != 'NXdata':
651
+ raise ValueError('Invalid default pathway to an NXdata object '
652
+ f'in ({data})')
653
+
654
+ # Get the data slice
655
+ axes = nxdata.attrs.get('axes', None)
656
+ if axes is not None:
657
+ axes = list(axes.nxdata)
658
+ coords = None
659
+ title = f'{nxdata.nxpath}/{nxdata.signal}'
660
+ if nxdata.nxsignal.ndim == 2:
661
+ exit('ImageProcessor not tested yet for a 2D dataset')
662
+ if axis is not None:
663
+ axis = None
664
+ self.logger.warning('Ignoring parameter axis')
665
+ if index is not None:
666
+ index = None
667
+ self.logger.warning('Ignoring parameter index')
668
+ if coord is not None:
669
+ coord = None
670
+ self.logger.warning('Ignoring parameter coord')
671
+ a = nxdata.nxsignal
672
+ elif nxdata.nxsignal.ndim == 3:
673
+ if isinstance(axis, int):
674
+ if not 0 <= axis < nxdata.nxsignal.ndim:
675
+ raise ValueError(f'axis index out of range ({axis} not in '
676
+ f'[0, {nxdata.nxsignal.ndim-1}])')
677
+ elif isinstance(axis, str):
678
+ if axes is None or axis not in axes:
679
+ raise ValueError(
680
+ f'Unable to match axis = {axis} in {nxdata.tree}')
681
+ axis = axes.index(axis)
682
+ else:
683
+ raise ValueError(f'Invalid parameter axis ({axis})')
684
+ if axes is not None and hasattr(nxdata, axes[axis]):
685
+ coords = nxdata[axes[axis]].nxdata
686
+ axis_name = axes[axis]
687
+ else:
688
+ axis_name = f'axis {axis}'
689
+ if index is None and coord is None:
690
+ index = nxdata.nxsignal.shape[axis] // 2
691
+ else:
692
+ if index is not None:
693
+ if coord is not None:
694
+ coord = None
695
+ self.logger.warning('Ignoring parameter coord')
696
+ if not isinstance(index, int):
697
+ raise ValueError(f'Invalid parameter index ({index})')
698
+ elif not 0 <= index < nxdata.nxsignal.shape[axis]:
699
+ raise ValueError(
700
+ f'index value out of range ({index} not in '
701
+ f'[0, {nxdata.nxsignal.shape[axis]-1}])')
702
+ else:
703
+ if not isinstance(coord, (int, float)):
704
+ raise ValueError(f'Invalid parameter coord ({coord})')
705
+ if coords is None:
706
+ raise ValueError(
707
+ f'Unable to get coordinates for {axis_name} '
708
+ f'in {nxdata.tree}')
709
+ index = index_nearest(nxdata[axis_name], coord)
710
+ if coords is None:
711
+ slice_info = f'slice at {axis_name} and index {index}'
712
+ else:
713
+ coord = coords[index]
714
+ slice_info = f'slice at {axis_name} = '\
715
+ f'{nxdata[axis_name][index]:.3f}'
716
+ if 'units' in nxdata[axis_name].attrs:
717
+ slice_info += f' ({nxdata[axis_name].units})'
718
+ if not axis:
719
+ a = nxdata[nxdata.signal][index,:,:]
720
+ elif axis == 1:
721
+ a = nxdata[nxdata.signal][:,index,:]
722
+ elif axis == 2:
723
+ a = nxdata[nxdata.signal][:,:,index]
724
+ if coords is None:
725
+ axes = [i for i in range(3) if i != axis]
726
+ row_coords = range(a.shape[1])
727
+ row_label = f'axis {axes[1]} index'
728
+ column_coords = range(a.shape[0])
729
+ column_label = f'axis {axes[0]} index'
730
+ else:
731
+ axes.pop(axis)
732
+ row_coords = nxdata[axes[1]].nxdata
733
+ row_label = axes[1]
734
+ if 'units' in nxdata[axes[1]].attrs:
735
+ row_label += f' ({nxdata[axes[1]].units})'
736
+ column_coords = nxdata[axes[0]].nxdata
737
+ column_label = axes[0]
738
+ if 'units' in nxdata[axes[0]].attrs:
739
+ column_label += f' ({nxdata[axes[0]].units})'
740
+ else:
741
+ raise ValueError('Invalid data dimension (must be 2D or 3D)')
742
+
743
+ # Create figure
744
+ a_max = a.max()
745
+ if vmin is None:
746
+ vmin = -a_max
747
+ if vmax is None:
748
+ vmax = a_max
749
+ extent = (
750
+ row_coords[0], row_coords[-1], column_coords[-1], column_coords[0])
751
+ fig, ax = plt.subplots(figsize=(11, 8.5))
752
+ plt.imshow(
753
+ a, extent=extent, origin='lower', vmin=vmin, vmax=vmax,
754
+ cmap='gray')
755
+ fig.suptitle(title, fontsize='xx-large')
756
+ ax.set_title(slice_info, fontsize='xx-large', pad=20)
757
+ ax.set_xlabel(row_label, fontsize='x-large')
758
+ ax.set_ylabel(column_label, fontsize='x-large')
759
+ plt.colorbar()
760
+ fig.tight_layout()
761
+ if interactive:
762
+ plt.show()
763
+ if save_figure:
764
+ fig.savefig(filename)
765
+ plt.close()
766
+
767
+ return nxdata
768
+
69
769
 
770
+ class IntegrationProcessor(Processor):
771
+ """A processor for integrating 2D data with pyFAI.
772
+ """
70
773
  def process(self, data):
71
774
  """Integrate the input data with the integration method and
72
- keyword arguments supplied and return the results.
775
+ keyword arguments supplied in `data` and return the results.
73
776
 
74
- :param data: input data, including raw data, integration
777
+ :param data: Input data, containing the raw data, integration
75
778
  method, and keyword args for the integration method.
76
- :type data: tuple[typing.Union[numpy.ndarray,
77
- list[numpy.ndarray]], callable, dict]
78
- :param integration_method: the method of a
79
- `pyFAI.azimuthalIntegrator.AzimuthalIntegrator` or
80
- `pyFAI.multi_geometry.MultiGeometry` that returns the
81
- desired integration results.
82
- :return: integrated raw data
779
+ :type data: list[PipelineData]
780
+ :return: Integrated raw data.
83
781
  :rtype: pyFAI.containers.IntegrateResult
84
782
  """
85
783
  detector_data, integration_method, integration_kwargs = data
@@ -88,26 +786,23 @@ class IntegrationProcessor(Processor):
88
786
 
89
787
 
90
788
  class IntegrateMapProcessor(Processor):
91
- """Class representing a process that takes a map and integration
92
- configuration and returns a `nexusformat.nexus.NXprocess`
93
- containing a map of the integrated detector data requested.
789
+ """A processor that takes a map and integration configuration and
790
+ returns a NeXus NXprocesss object containing a map of the
791
+ integrated detector data requested.
94
792
  """
95
-
96
793
  def process(self, data):
97
794
  """Process the output of a `Reader` that contains a map and
98
- integration configuration and return a
99
- `nexusformat.nexus.NXprocess` containing a map of the
100
- integrated detector data requested
795
+ integration configuration and return a NeXus NXprocess object
796
+ containing a map of the integrated detector data requested.
101
797
 
102
- :param data: Result of `Reader.read` where at least one item
103
- has the value `'MapConfig'` for the `'schema'` key, and at
104
- least one item has the value `'IntegrationConfig'` for the
798
+ :param data: Input data, containing at least one item
799
+ with the value `'MapConfig'` for the `'schema'` key, and at
800
+ least one item with the value `'IntegrationConfig'` for the
105
801
  `'schema'` key.
106
- :type data: list[dict[str,object]]
107
- :return: integrated data and process metadata
802
+ :type data: list[PipelineData]
803
+ :return: Integrated data and process metadata.
108
804
  :rtype: nexusformat.nexus.NXprocess
109
805
  """
110
-
111
806
  map_config = self.get_config(
112
807
  data, 'common.models.map.MapConfig')
113
808
  integration_config = self.get_config(
@@ -118,27 +813,31 @@ class IntegrateMapProcessor(Processor):
118
813
 
119
814
  def get_nxprocess(self, map_config, integration_config):
120
815
  """Use a `MapConfig` and `IntegrationConfig` to construct a
121
- `nexusformat.nexus.NXprocess`
816
+ NeXus NXprocess object.
122
817
 
123
- :param map_config: a valid map configuration
818
+ :param map_config: A valid map configuration.
124
819
  :type map_config: MapConfig
125
- :param integration_config: a valid integration configuration
126
- :type integration_config: IntegrationConfig
127
- :return: the integrated detector data and metadata contained
128
- in a NeXus structure
820
+ :param integration_config: A valid integration configuration
821
+ :type integration_config: IntegrationConfig.
822
+ :return: The integrated detector data and metadata.
129
823
  :rtype: nexusformat.nexus.NXprocess
130
824
  """
825
+ # System modules
826
+ from json import dumps
827
+ from time import time
828
+
829
+ # Third party modules
830
+ from nexusformat.nexus import (
831
+ NXdata,
832
+ NXdetector,
833
+ NXfield,
834
+ NXprocess,
835
+ )
836
+ import pyFAI
131
837
 
132
838
  self.logger.debug('Constructing NXprocess')
133
839
  t0 = time()
134
840
 
135
- from nexusformat.nexus import (NXdata,
136
- NXdetector,
137
- NXfield,
138
- NXprocess)
139
- import numpy as np
140
- import pyFAI
141
-
142
841
  nxprocess = NXprocess(name=integration_config.title)
143
842
 
144
843
  nxprocess.map_config = dumps(map_config.dict())
@@ -255,24 +954,21 @@ class IntegrateMapProcessor(Processor):
255
954
 
256
955
 
257
956
  class MapProcessor(Processor):
258
- """A Processor to take a map configuration and return a
259
- `nexusformat.nexus.NXentry` representing that map's metadata and
260
- any scalar-valued raw data requseted by the supplied map
261
- configuration.
957
+ """A Processor that takes a map configuration and returns a NeXus
958
+ NXentry object representing that map's metadata and any
959
+ scalar-valued raw data requested by the supplied map configuration.
262
960
  """
263
-
264
961
  def process(self, data):
265
962
  """Process the output of a `Reader` that contains a map
266
- configuration and return a `nexusformat.nexus.NXentry`
267
- representing the map.
963
+ configuration and returns a NeXus NXentry object representing
964
+ the map.
268
965
 
269
966
  :param data: Result of `Reader.read` where at least one item
270
967
  has the value `'MapConfig'` for the `'schema'` key.
271
- :type data: list[dict[str,object]]
272
- :return: Map data & metadata
968
+ :type data: list[PipelineData]
969
+ :return: Map data and metadata.
273
970
  :rtype: nexusformat.nexus.NXentry
274
971
  """
275
-
276
972
  map_config = self.get_config(data, 'common.models.map.MapConfig')
277
973
  nxentry = self.__class__.get_nxentry(map_config)
278
974
 
@@ -280,29 +976,29 @@ class MapProcessor(Processor):
280
976
 
281
977
  @staticmethod
282
978
  def get_nxentry(map_config):
283
- """Use a `MapConfig` to construct a
284
- `nexusformat.nexus.NXentry`
979
+ """Use a `MapConfig` to construct a NeXus NXentry object.
285
980
 
286
- :param map_config: a valid map configuration
981
+ :param map_config: A valid map configuration.
287
982
  :type map_config: MapConfig
288
- :return: the map's data and metadata contained in a NeXus
289
- structure
983
+ :return: The map's data and metadata contained in a NeXus
984
+ structure.
290
985
  :rtype: nexusformat.nexus.NXentry
291
986
  """
292
-
293
- from nexusformat.nexus import (NXcollection,
294
- NXdata,
295
- NXentry,
296
- NXfield,
297
- NXsample)
298
- import numpy as np
987
+ # System modules
988
+ from json import dumps
989
+
990
+ # Third party modules
991
+ from nexusformat.nexus import (
992
+ NXcollection,
993
+ NXdata,
994
+ NXentry,
995
+ NXfield,
996
+ NXsample,
997
+ )
299
998
 
300
999
  nxentry = NXentry(name=map_config.title)
301
-
302
1000
  nxentry.map_config = dumps(map_config.dict())
303
-
304
1001
  nxentry[map_config.sample.name] = NXsample(**map_config.sample.dict())
305
-
306
1002
  nxentry.attrs['station'] = map_config.station
307
1003
 
308
1004
  nxentry.spec_scans = NXcollection()
@@ -352,22 +1048,21 @@ class MapProcessor(Processor):
352
1048
 
353
1049
 
354
1050
  class NexusToNumpyProcessor(Processor):
355
- """A Processor to convert the default plottable data in an
356
- `NXobject` into an `numpy.ndarray`.
1051
+ """A Processor to convert the default plottable data in a NeXus
1052
+ object into a `numpy.ndarray`.
357
1053
  """
358
-
359
1054
  def process(self, data):
360
- """Return the default plottable data signal in `data` as an
361
- `numpy.ndarray`.
362
-
363
- :param data: input NeXus structure
364
- :type data: nexusformat.nexus.tree.NXobject
365
- :raises ValueError: if `data` has no default plottable data
366
- signal
367
- :return: default plottable data signal in `data`
1055
+ """Return the default plottable data signal in a NeXus object
1056
+ contained in `data` as an `numpy.ndarray`.
1057
+
1058
+ :param data: Input data.
1059
+ :type data: nexusformat.nexus.NXobject
1060
+ :raises ValueError: If `data` has no default plottable data
1061
+ signal.
1062
+ :return: The default plottable data signal.
368
1063
  :rtype: numpy.ndarray
369
1064
  """
370
-
1065
+ # Third party modules
371
1066
  from nexusformat.nexus import NXdata
372
1067
 
373
1068
  data = self.unwrap_pipelinedata(data)[-1]
@@ -394,22 +1089,21 @@ class NexusToNumpyProcessor(Processor):
394
1089
 
395
1090
 
396
1091
  class NexusToXarrayProcessor(Processor):
397
- """A Processor to convert the default plottable data in an
398
- `NXobject` into an `xarray.DataArray`.
1092
+ """A Processor to convert the default plottable data in a
1093
+ NeXus object into an `xarray.DataArray`.
399
1094
  """
400
-
401
1095
  def process(self, data):
402
- """Return the default plottable data signal in `data` as an
403
- `xarray.DataArray`.
1096
+ """Return the default plottable data signal in a NeXus object
1097
+ contained in `data` as an `xarray.DataArray`.
404
1098
 
405
- :param data: input NeXus structure
406
- :type data: nexusformat.nexus.tree.NXobject
407
- :raises ValueError: if metadata for `xarray` is absent from
1099
+ :param data: Input data.
1100
+ :type data: nexusformat.nexus.NXobject
1101
+ :raises ValueError: If metadata for `xarray` is absent from
408
1102
  `data`
409
- :return: default plottable data signal in `data`
1103
+ :return: The default plottable data signal.
410
1104
  :rtype: xarray.DataArray
411
1105
  """
412
-
1106
+ # Third party modules
413
1107
  from nexusformat.nexus import NXdata
414
1108
  from xarray import DataArray
415
1109
 
@@ -458,18 +1152,15 @@ class PrintProcessor(Processor):
458
1152
  """A Processor to simply print the input data to stdout and return
459
1153
  the original input data, unchanged in any way.
460
1154
  """
461
-
462
1155
  def process(self, data):
463
1156
  """Print and return the input data.
464
1157
 
465
- :param data: Input data
1158
+ :param data: Input data.
466
1159
  :type data: object
467
1160
  :return: `data`
468
1161
  :rtype: object
469
1162
  """
470
-
471
1163
  print(f'{self.__name__} data :')
472
-
473
1164
  if callable(getattr(data, '_str_tree', None)):
474
1165
  # If data is likely an NXobject, print its tree
475
1166
  # representation (since NXobjects' str representations are
@@ -482,23 +1173,23 @@ class PrintProcessor(Processor):
482
1173
 
483
1174
 
484
1175
  class RawDetectorDataMapProcessor(Processor):
485
- """A Processor to return a map of raw derector data in an NXroot"""
486
-
1176
+ """A Processor to return a map of raw derector data in a
1177
+ NeXus NXroot object.
1178
+ """
487
1179
  def process(self, data, detector_name, detector_shape):
488
1180
  """Process configurations for a map and return the raw
489
1181
  detector data data collected over the map.
490
1182
 
491
- :param data: input map configuration
492
- :type data: list[dict[str,object]]
493
- :param detector_name: detector prefix
1183
+ :param data: Input map configuration.
1184
+ :type data: list[PipelineData]
1185
+ :param detector_name: The detector prefix.
494
1186
  :type detector_name: str
495
- :param detector_shape: shape of detector data for a single
496
- scan step
1187
+ :param detector_shape: The shape of detector data for a single
1188
+ scan step.
497
1189
  :type detector_shape: list
498
- :return: map of raw detector data
1190
+ :return: Map of raw detector data.
499
1191
  :rtype: nexusformat.nexus.NXroot
500
1192
  """
501
-
502
1193
  map_config = self.get_config(data)
503
1194
  nxroot = self.get_nxroot(map_config, detector_name, detector_shape)
504
1195
 
@@ -506,17 +1197,18 @@ class RawDetectorDataMapProcessor(Processor):
506
1197
 
507
1198
  def get_config(self, data):
508
1199
  """Get instances of the map configuration object needed by this
509
- `Processor`
1200
+ `Processor`.
510
1201
 
511
1202
  :param data: Result of `Reader.read` where at least one item
512
- has the value `'MapConfig'` for the `'schema'` key
513
- :type data: list[dict[str,object]]
1203
+ has the value `'MapConfig'` for the `'schema'` key.
1204
+ :type data: list[PipelineData]
514
1205
  :raises Exception: If a valid map config object cannot be
515
1206
  constructed from `data`.
516
- :return: valid instances of the map configuration object with
1207
+ :return: A valid instance of the map configuration object with
517
1208
  field values taken from `data`.
518
1209
  :rtype: MapConfig
519
1210
  """
1211
+ # Local modules
520
1212
  from CHAP.common.models.map import MapConfig
521
1213
 
522
1214
  map_config = False
@@ -534,27 +1226,28 @@ class RawDetectorDataMapProcessor(Processor):
534
1226
 
535
1227
  def get_nxroot(self, map_config, detector_name, detector_shape):
536
1228
  """Get a map of the detector data collected by the scans in
537
- `map_config`.The data will be returned along with some
1229
+ `map_config`. The data will be returned along with some
538
1230
  relevant metadata in the form of a NeXus structure.
539
1231
 
540
- :param map_config: the map configuration
1232
+ :param map_config: The map configuration.
541
1233
  :type map_config: MapConfig
542
- :param detector_name: detector prefix
1234
+ :param detector_name: The detector prefix.
543
1235
  :type detector_name: str
544
- :param detector_shape: shape of detector data for a single
545
- scan step
1236
+ :param detector_shape: The shape of detector data for a single
1237
+ scan step.
546
1238
  :type detector_shape: list
547
- :return: a map of the raw detector data
1239
+ :return: A map of the raw detector data.
548
1240
  :rtype: nexusformat.nexus.NXroot
549
1241
  """
550
- # third party modules
551
- from nexusformat.nexus import (NXdata,
552
- NXdetector,
553
- NXinstrument,
554
- NXroot)
555
- import numpy as np
556
-
557
- # local modules
1242
+ # Third party modules
1243
+ from nexusformat.nexus import (
1244
+ NXdata,
1245
+ NXdetector,
1246
+ NXinstrument,
1247
+ NXroot,
1248
+ )
1249
+
1250
+ # Local modules
558
1251
  from CHAP.common import MapProcessor
559
1252
 
560
1253
  nxroot = NXroot()
@@ -604,42 +1297,39 @@ class RawDetectorDataMapProcessor(Processor):
604
1297
 
605
1298
 
606
1299
  class StrainAnalysisProcessor(Processor):
607
- """A Processor to compute a map of sample strains by fitting bragg
1300
+ """A Processor to compute a map of sample strains by fitting Bragg
608
1301
  peaks in 1D detector data and analyzing the difference between
609
1302
  measured peak locations and expected peak locations for the sample
610
1303
  measured.
611
1304
  """
612
-
613
1305
  def process(self, data):
614
1306
  """Process the input map detector data & configuration for the
615
1307
  strain analysis procedure, and return a map of sample strains.
616
1308
 
617
- :param data: results of `MutlipleReader.read` containing input
1309
+ :param data: Results of `MutlipleReader.read` containing input
618
1310
  map detector data and strain analysis configuration
619
- :type data: dict[list[str,object]]
620
- :return: map of sample strains
1311
+ :type data: list[PipelineData]
1312
+ :return: A map of sample strains.
621
1313
  :rtype: xarray.Dataset
622
1314
  """
623
-
624
1315
  strain_analysis_config = self.get_config(data)
625
1316
 
626
1317
  return data
627
1318
 
628
1319
  def get_config(self, data):
629
1320
  """Get instances of the configuration objects needed by this
630
- `Processor` from a returned value of `Reader.read`
1321
+ `Processor`.
631
1322
 
632
1323
  :param data: Result of `Reader.read` where at least one item
633
1324
  has the value `'StrainAnalysisConfig'` for the `'schema'`
634
1325
  key.
635
- :type data: list[dict[str,object]]
1326
+ :type data: list[PipelineData]
636
1327
  :raises Exception: If valid config objects cannot be
637
1328
  constructed from `data`.
638
- :return: valid instances of the configuration objects with
1329
+ :return: A valid instance of the configuration object with
639
1330
  field values taken from `data`.
640
1331
  :rtype: StrainAnalysisConfig
641
1332
  """
642
-
643
1333
  strain_analysis_config = False
644
1334
  if isinstance(data, list):
645
1335
  for item in data:
@@ -655,25 +1345,25 @@ class StrainAnalysisProcessor(Processor):
655
1345
 
656
1346
 
657
1347
  class XarrayToNexusProcessor(Processor):
658
- """A Processor to convert the data in an `xarray` structure to an
659
- `nexusformat.nexus.NXdata`.
1348
+ """A Processor to convert the data in an `xarray` structure to a
1349
+ NeXus NXdata object.
660
1350
  """
661
-
662
1351
  def process(self, data):
663
- """Return `data` represented as an `nexusformat.nexus.NXdata`.
1352
+ """Return `data` represented as a NeXus NXdata object.
664
1353
 
665
- :param data: The input `xarray` structure
1354
+ :param data: The input `xarray` structure.
666
1355
  :type data: typing.Union[xarray.DataArray, xarray.Dataset]
667
- :return: The data and metadata in `data`
1356
+ :return: The data and metadata in `data`.
668
1357
  :rtype: nexusformat.nexus.NXdata
669
1358
  """
670
-
671
- from nexusformat.nexus import NXdata, NXfield
1359
+ # Third party modules
1360
+ from nexusformat.nexus import (
1361
+ NXdata,
1362
+ NXfield,
1363
+ )
672
1364
 
673
1365
  data = self.unwrap_pipelinedata(data)[-1]
674
-
675
1366
  signal = NXfield(value=data.data, name=data.name, attrs=data.attrs)
676
-
677
1367
  axes = []
678
1368
  for name, coord in data.coords.items():
679
1369
  axes.append(
@@ -687,13 +1377,12 @@ class XarrayToNumpyProcessor(Processor):
687
1377
  """A Processor to convert the data in an `xarray.DataArray`
688
1378
  structure to an `numpy.ndarray`.
689
1379
  """
690
-
691
1380
  def process(self, data):
692
1381
  """Return just the signal values contained in `data`.
693
1382
 
694
- :param data: The input `xarray.DataArray`
1383
+ :param data: The input `xarray.DataArray`.
695
1384
  :type data: xarray.DataArray
696
- :return: The data in `data`
1385
+ :return: The data in `data`.
697
1386
  :rtype: numpy.ndarray
698
1387
  """
699
1388
 
@@ -701,5 +1390,7 @@ class XarrayToNumpyProcessor(Processor):
701
1390
 
702
1391
 
703
1392
  if __name__ == '__main__':
1393
+ # Local modules
704
1394
  from CHAP.processor import main
1395
+
705
1396
  main()