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/__init__.py +2 -0
- CHAP/common/__init__.py +7 -2
- CHAP/common/models/map.py +95 -70
- CHAP/common/processor.py +844 -153
- CHAP/common/reader.py +168 -131
- CHAP/common/writer.py +166 -96
- CHAP/edd/__init__.py +2 -0
- CHAP/edd/models.py +94 -48
- CHAP/edd/processor.py +625 -169
- CHAP/edd/utils.py +186 -6
- CHAP/pipeline.py +35 -3
- CHAP/runner.py +40 -13
- CHAP/tomo/models.py +18 -9
- CHAP/tomo/processor.py +1134 -902
- CHAP/utils/fit.py +98 -45
- CHAP/utils/general.py +196 -63
- CHAP/utils/scanparsers.py +403 -94
- {ChessAnalysisPipeline-0.0.12.dist-info → ChessAnalysisPipeline-0.0.14.dist-info}/METADATA +1 -1
- {ChessAnalysisPipeline-0.0.12.dist-info → ChessAnalysisPipeline-0.0.14.dist-info}/RECORD +23 -23
- {ChessAnalysisPipeline-0.0.12.dist-info → ChessAnalysisPipeline-0.0.14.dist-info}/WHEEL +1 -1
- {ChessAnalysisPipeline-0.0.12.dist-info → ChessAnalysisPipeline-0.0.14.dist-info}/LICENSE +0 -0
- {ChessAnalysisPipeline-0.0.12.dist-info → ChessAnalysisPipeline-0.0.14.dist-info}/entry_points.txt +0 -0
- {ChessAnalysisPipeline-0.0.12.dist-info → ChessAnalysisPipeline-0.0.14.dist-info}/top_level.txt +0 -0
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
|
-
#
|
|
12
|
-
|
|
13
|
-
from time import time
|
|
11
|
+
# Third party modules
|
|
12
|
+
import numpy as np
|
|
14
13
|
|
|
15
|
-
#
|
|
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:
|
|
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:
|
|
189
|
+
:param mgr: The object that will process given data.
|
|
44
190
|
:type mgr: Processor
|
|
45
|
-
:param doc:
|
|
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:
|
|
202
|
+
:param mgr: The object that will process all documents.
|
|
57
203
|
:type mgr: Processor
|
|
58
|
-
:param docs:
|
|
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
|
|
68
|
-
"""A
|
|
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:
|
|
777
|
+
:param data: Input data, containing the raw data, integration
|
|
75
778
|
method, and keyword args for the integration method.
|
|
76
|
-
:type data:
|
|
77
|
-
|
|
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
|
-
"""
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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:
|
|
103
|
-
|
|
104
|
-
least one item
|
|
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[
|
|
107
|
-
:return:
|
|
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
|
-
|
|
816
|
+
NeXus NXprocess object.
|
|
122
817
|
|
|
123
|
-
:param map_config:
|
|
818
|
+
:param map_config: A valid map configuration.
|
|
124
819
|
:type map_config: MapConfig
|
|
125
|
-
:param integration_config:
|
|
126
|
-
:type integration_config: IntegrationConfig
|
|
127
|
-
:return:
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
267
|
-
|
|
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[
|
|
272
|
-
:return: Map data
|
|
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:
|
|
981
|
+
:param map_config: A valid map configuration.
|
|
287
982
|
:type map_config: MapConfig
|
|
288
|
-
:return:
|
|
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
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
356
|
-
|
|
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
|
|
361
|
-
`numpy.ndarray`.
|
|
362
|
-
|
|
363
|
-
:param data:
|
|
364
|
-
:type data: nexusformat.nexus.
|
|
365
|
-
:raises ValueError:
|
|
366
|
-
signal
|
|
367
|
-
:return: default plottable data signal
|
|
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
|
|
398
|
-
|
|
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
|
|
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:
|
|
406
|
-
:type data: nexusformat.nexus.
|
|
407
|
-
:raises ValueError:
|
|
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
|
|
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
|
|
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:
|
|
492
|
-
:type data: list[
|
|
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:
|
|
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[
|
|
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
|
|
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:
|
|
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:
|
|
1239
|
+
:return: A map of the raw detector data.
|
|
548
1240
|
:rtype: nexusformat.nexus.NXroot
|
|
549
1241
|
"""
|
|
550
|
-
#
|
|
551
|
-
from nexusformat.nexus import (
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
|
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:
|
|
1309
|
+
:param data: Results of `MutlipleReader.read` containing input
|
|
618
1310
|
map detector data and strain analysis configuration
|
|
619
|
-
:type data:
|
|
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
|
|
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[
|
|
1326
|
+
:type data: list[PipelineData]
|
|
636
1327
|
:raises Exception: If valid config objects cannot be
|
|
637
1328
|
constructed from `data`.
|
|
638
|
-
:return: valid
|
|
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
|
|
659
|
-
|
|
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
|
|
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
|
|
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()
|