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

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

Potentially problematic release.


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

Files changed (38) hide show
  1. CHAP/__init__.py +1 -1
  2. CHAP/common/__init__.py +13 -0
  3. CHAP/common/models/integration.py +29 -26
  4. CHAP/common/models/map.py +395 -224
  5. CHAP/common/processor.py +1725 -93
  6. CHAP/common/reader.py +265 -28
  7. CHAP/common/writer.py +191 -18
  8. CHAP/edd/__init__.py +9 -2
  9. CHAP/edd/models.py +886 -665
  10. CHAP/edd/processor.py +2592 -936
  11. CHAP/edd/reader.py +889 -0
  12. CHAP/edd/utils.py +846 -292
  13. CHAP/foxden/__init__.py +6 -0
  14. CHAP/foxden/processor.py +42 -0
  15. CHAP/foxden/writer.py +65 -0
  16. CHAP/giwaxs/__init__.py +8 -0
  17. CHAP/giwaxs/models.py +100 -0
  18. CHAP/giwaxs/processor.py +520 -0
  19. CHAP/giwaxs/reader.py +5 -0
  20. CHAP/giwaxs/writer.py +5 -0
  21. CHAP/pipeline.py +48 -10
  22. CHAP/runner.py +161 -72
  23. CHAP/tomo/models.py +31 -29
  24. CHAP/tomo/processor.py +169 -118
  25. CHAP/utils/__init__.py +1 -0
  26. CHAP/utils/fit.py +1292 -1315
  27. CHAP/utils/general.py +411 -53
  28. CHAP/utils/models.py +594 -0
  29. CHAP/utils/parfile.py +10 -2
  30. ChessAnalysisPipeline-0.0.16.dist-info/LICENSE +60 -0
  31. {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/METADATA +1 -1
  32. ChessAnalysisPipeline-0.0.16.dist-info/RECORD +62 -0
  33. {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/WHEEL +1 -1
  34. CHAP/utils/scanparsers.py +0 -1431
  35. ChessAnalysisPipeline-0.0.14.dist-info/LICENSE +0 -21
  36. ChessAnalysisPipeline-0.0.14.dist-info/RECORD +0 -54
  37. {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/entry_points.txt +0 -0
  38. {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/top_level.txt +0 -0
CHAP/common/reader.py CHANGED
@@ -8,7 +8,9 @@ Description: Module for Writers used in multiple experiment-specific
8
8
 
9
9
  # System modules
10
10
  from os.path import (
11
+ isabs,
11
12
  isfile,
13
+ join,
12
14
  splitext,
13
15
  )
14
16
  from sys import modules
@@ -36,6 +38,35 @@ class BinaryFileReader(Reader):
36
38
  return data
37
39
 
38
40
 
41
+ class FabioImageReader(Reader):
42
+ """Reader for images using the python package
43
+ [`fabio`](https://fabio.readthedocs.io/en/main/).
44
+ """
45
+ def read(self, filename, frame=None, inputdir='.'):
46
+ """Return the data from the image file(s) provided.
47
+
48
+ :param filename: The image filename, or glob pattern for image
49
+ filenames, to read.
50
+ :type filename: str
51
+ :param frame: The index of a specific frame to read from the
52
+ file(s), defaults to `None`.
53
+ :type filename: int, optional
54
+ :returns: Image data as a numpy array (or list of numpy
55
+ arrays, if a glob pattern matching more than one file was
56
+ provided).
57
+ """
58
+ from glob import glob
59
+ import fabio
60
+
61
+ filenames = glob(filename)
62
+ data = []
63
+ for f in filenames:
64
+ image = fabio.open(f, frame=frame)
65
+ data.append(image.data)
66
+ image.close()
67
+ return data
68
+
69
+
39
70
  class H5Reader(Reader):
40
71
  """Reader for h5 files.
41
72
  """
@@ -97,6 +128,8 @@ class MapReader(Reader):
97
128
  # Local modules
98
129
  from CHAP.common.models.map import MapConfig
99
130
 
131
+ raise RuntimeError('MapReader is obsolete, use MapProcessor')
132
+
100
133
  if filename is not None:
101
134
  if map_config is not None:
102
135
  raise RuntimeError('Specify either filename or map_config '
@@ -130,13 +163,14 @@ class MapReader(Reader):
130
163
  attrs={'spec_file': str(scans.spec_file)})
131
164
 
132
165
  # Add sample metadata
133
- nxentry[map_config.sample.name] = NXsample(**map_config.sample.dict())
166
+ nxentry[map_config.sample.name] = NXsample(
167
+ **map_config.sample.dict())
134
168
 
135
169
  # Set up default data group
136
170
  nxentry.data = NXdata()
137
171
  if map_config.map_type == 'structured':
138
172
  nxentry.data.attrs['axes'] = map_config.dims
139
- for i, dim in enumerate(map_config.independent_dimensions[::-1]):
173
+ for i, dim in enumerate(map_config.independent_dimensions):
140
174
  nxentry.data[dim.label] = NXfield(
141
175
  value=map_config.coords[dim.label],
142
176
  units=dim.units,
@@ -214,9 +248,149 @@ class NexusReader(Reader):
214
248
  return nxobject
215
249
 
216
250
 
251
+ class NXdataReader(Reader):
252
+ """Reader for constructing an NXdata object from components"""
253
+ def read(self, name, nxfield_params, signal_name, axes_names, attrs={},
254
+ inputdir='.'):
255
+ """Return a basic NXdata object constructed from components.
256
+
257
+ :param name: The name of the NXdata group.
258
+ :type name: str
259
+ :param nxfield_params: List of sets of parameters for
260
+ `NXfieldReader` specifying the NXfields belonging to the
261
+ NXdata.
262
+ :type nxfield_params: list[dict]
263
+ :param signal_name: Name of the signal for the NXdata (must be
264
+ one of the names of the NXfields indicated in `nxfields`)
265
+ :type signal: str
266
+ :param axes_names: Name or names of the coordinate axes
267
+ NXfields associated with the signal (must be names of
268
+ NXfields indicated in `nxfields`)
269
+ :type axes_names: Union[str, list[str]]
270
+ :param attrs: Optional dictionary of additional attributes for
271
+ the NXdata
272
+ :type attrs: dict
273
+ :param inputdir: Input directory to use for `NXfieldReader`s,
274
+ defaults to `"."`
275
+ :type inputdir: str
276
+ :returns: A new NXdata object
277
+ :rtype: nexusformat.nexus.NXdata
278
+ """
279
+ from nexusformat.nexus import NXdata
280
+
281
+ # Read in NXfields
282
+ nxfields = [NXfieldReader().read(**params, inputdir=inputdir)
283
+ for params in nxfield_params]
284
+ nxfields = {nxfield.nxname: nxfield for nxfield in nxfields}
285
+
286
+ # Get signal NXfield
287
+ try:
288
+ nxsignal = nxfields[signal_name]
289
+ except:
290
+ raise ValueError(
291
+ '`signal_name` must be the name of one of the NXfields '
292
+ + 'indicated in `nxfields`: '
293
+ + ', '.join(nxfields.keys())
294
+ )
295
+
296
+ # Get axes NXfield(s)
297
+ if isinstance(axes_names, str):
298
+ axes_names = [axes_names]
299
+ try:
300
+ nxaxes = [nxfields[axis_name] for axis_name in axes_names]
301
+ except:
302
+ raise ValueError(
303
+ '`axes_names` must contain only names of NXfields indicated '
304
+ + 'in `nxfields`: ' + ', '.join(nxfields.keys())
305
+ )
306
+ for i, nxaxis in enumerate(nxaxes):
307
+ if len(nxaxis) != nxsignal.shape[i]:
308
+ raise ValueError(
309
+ f'Shape mismatch on signal dimension {i}: signal '
310
+ + f'"{nxsignal.nxname}" has {nxsignal.shape[i]} values, '
311
+ + f'but axis "{nxaxis.nxname}" has {len(nxaxis)} values.')
312
+
313
+ result = NXdata(signal=nxsignal, axes=nxaxes, name=name, attrs=attrs,
314
+ **nxfields)
315
+ self.logger.info(result.tree)
316
+ return result
317
+
318
+
319
+ class NXfieldReader(Reader):
320
+ """Reader for an NXfield with options to modify certain attributes."""
321
+ def read(self, filename, nxpath, nxname=None, update_attrs=None,
322
+ slice_params=None, inputdir='.'):
323
+ """Return a copy of the indicated NXfield from the file. Name
324
+ and attributes of the returned copy may be modified with the
325
+ `nxname` and `update_attrs` keyword arguments.
326
+
327
+ :param filename: Name of the NeXus file containing the NXfield to read.
328
+ :type filename: str
329
+ :param nxpath: Path in `nxfile` pointing to the NXfield to read.
330
+ :type nxpath: str
331
+ :param nxname: Optional new name for the returned NXfield,
332
+ defaults to None
333
+ :type nxname: str, optional
334
+ :param update_attrs: Optional dictonary used to add to /
335
+ update the original NXfield's attributes, defaults to None
336
+ :type update_attrs: dict, optional
337
+ :param slice_params: Parameters for returning just a slice of
338
+ the full field data. Slice parameters are provided in a
339
+ list dictionaries with integer values for any / all of the
340
+ following keys: `"start"`, `"end"`, `"step"`. Default
341
+ values used are: `"start"` - `0`, `"end"` -- `None`,
342
+ `"step"` -- `1`. The order of the list must correspond to
343
+ the order of the field's axes. Defaults to `None`.
344
+ :type slice_params: list[dict[str, int]], optional
345
+ :param inputdir: Directory containing `nxfile`, defaults to `"."`
346
+ :type inputdir: str
347
+ :returns: A copy of the indicated NXfield (with name and
348
+ attributes optionally modified).
349
+ :rtype: nexusformat.nexus.NXfield
350
+ """
351
+ # Third party modules
352
+ from nexusformat.nexus import nxload, NXfield
353
+
354
+ # Local modules
355
+ from CHAP.utils.general import nxcopy
356
+
357
+ if not isabs(filename):
358
+ filename = join(inputdir, filename)
359
+ nxroot = nxload(filename)
360
+ nxfield = nxroot[nxpath]
361
+
362
+ if nxname is None:
363
+ nxname = nxfield.nxname
364
+
365
+ attrs = nxfield.attrs
366
+ if update_attrs is not None:
367
+ attrs.update(update_attrs)
368
+
369
+ if slice_params is None:
370
+ value = nxfield.nxdata
371
+ else:
372
+ if len(slice_params) < nxfield.ndim:
373
+ slice_params.extend([{}] * (nxfield.ndim - len(slice_params)))
374
+ if len(slice_params) > nxfield.ndim:
375
+ slice_params = slice_params[0:nxfield.ndim]
376
+ slices = ()
377
+ default_slice = {'start': 0, 'end': None, 'step': 1}
378
+ for s in slice_params:
379
+ for k, v in default_slice.items():
380
+ if k not in s:
381
+ s[k] = v
382
+ slices = (*slices, slice(s['start'], s['end'], s['step']))
383
+ value = nxfield.nxdata[slices]
384
+
385
+ nxfield = NXfield(value=value, name=nxname, attrs=attrs)
386
+ self.logger.debug(f'Result -- nxfield.tree =\n{nxfield.tree}')
387
+
388
+ return nxfield
389
+
390
+
217
391
  class SpecReader(Reader):
218
392
  """Reader for CHESS SPEC scans"""
219
- def read(self, filename=None, spec_config=None, detector_names=[],
393
+ def read(self, filename=None, config=None, detector_names=None,
220
394
  inputdir=None):
221
395
  """Take a SPEC configuration filename or dictionary and return
222
396
  the raw data as a Nexus NXentry object.
@@ -225,15 +399,17 @@ class SpecReader(Reader):
225
399
  to read from to pass onto the constructor of
226
400
  `CHAP.common.models.map.SpecConfig`, defaults to `None`.
227
401
  :type filename: str, optional
228
- :param spec_config: A SPEC configuration to be passed directly
402
+ :param config: A SPEC configuration to be passed directly
229
403
  to the constructor of `CHAP.common.models.map.SpecConfig`,
230
404
  defaults to `None`.
231
- :type spec_config: dict, optional
232
- :param detector_names: Detector prefixes to include raw data
233
- for in the returned NeXus NXentry object, defaults to `[]`.
234
- :type detector_names: list[str], optional
405
+ :type config: dict, optional
406
+ :param detector_names: Detector names/prefixes to include raw
407
+ data for in the returned NeXus NXentry object,
408
+ defaults to `None`.
409
+ :type detector_names: Union(int, str, list[int], list[str]),
410
+ optional
235
411
  :return: The data from the provided SPEC configuration.
236
- :rtype: nexusformat.nexus.NXentry
412
+ :rtype: nexusformat.nexus.NXroot
237
413
  """
238
414
  # Third party modules
239
415
  from json import dumps
@@ -242,14 +418,15 @@ class SpecReader(Reader):
242
418
  NXdata,
243
419
  NXentry,
244
420
  NXfield,
421
+ NXroot,
245
422
  )
246
423
 
247
424
  # Local modules
248
425
  from CHAP.common.models.map import SpecConfig
249
426
 
250
427
  if filename is not None:
251
- if spec_config is not None:
252
- raise RuntimeError('Specify either filename or spec_config '
428
+ if config is not None:
429
+ raise RuntimeError('Specify either filename or config '
253
430
  'in common.SpecReader, not both')
254
431
  # Read the map configuration from file
255
432
  if not isfile(filename):
@@ -260,22 +437,62 @@ class SpecReader(Reader):
260
437
  else:
261
438
  raise RuntimeError('input file has a non-implemented '
262
439
  f'extension ({filename})')
263
- spec_config = reader.read(filename)
264
- elif not isinstance(spec_config, dict):
265
- raise RuntimeError('Invalid parameter spec_config in '
266
- f'common.SpecReader ({spec_config})')
440
+ config = reader.read(filename)
441
+ elif not isinstance(config, dict):
442
+ raise RuntimeError('Invalid parameter config in '
443
+ f'common.SpecReader ({config})')
267
444
 
268
445
  # Validate the SPEC configuration provided by constructing a
269
446
  # SpecConfig
270
- spec_config = SpecConfig(**spec_config, inputdir=inputdir)
271
-
272
- # Set up NXentry and add misc. CHESS-specific metadata
273
- # as well as all spec_motors, scan_columns, and smb_pars
274
- nxentry = NXentry(name=spec_config.experiment_type)
275
- nxentry.spec_config = dumps(spec_config.dict())
276
- nxentry.attrs['station'] = spec_config.station
447
+ config = SpecConfig(**config, inputdir=inputdir)
448
+
449
+ # Validate the detector names/prefixes
450
+ if config.experiment_type == 'EDD':
451
+ if detector_names is not None:
452
+ if isinstance(detector_names, (int, str)):
453
+ detector_names = [str(detector_names)]
454
+ for i, detector_name in enumerate(detector_names):
455
+ if isinstance(detector_name, int):
456
+ detector_names[i] = str(detector_name)
457
+ elif not isinstance(detector_name, str):
458
+ raise ValueError('Invalid "detector_names" parameter '
459
+ f'({detector_names})')
460
+ else:
461
+ # Local modules
462
+ from CHAP.utils.general import is_str_series
463
+
464
+ if detector_names is None:
465
+ raise ValueError(
466
+ 'Missing "detector_names" parameter')
467
+ if isinstance(detector_names, str):
468
+ detector_names = [detector_names]
469
+ if not is_str_series(detector_names, log=False):
470
+ raise ValueError(
471
+ 'Invalid "detector_names" parameter ({detector_names})')
472
+
473
+ # Create the NXroot object
474
+ nxroot = NXroot()
475
+ nxentry = NXentry(name=config.experiment_type)
476
+ nxroot[nxentry.nxname] = nxentry
477
+ nxentry.set_default()
478
+
479
+ # Set up NXentry and add misc. CHESS-specific metadata as well
480
+ # as all spec_motors, scan_columns, and smb_pars, and the
481
+ # detector info and raw detector data
482
+ nxentry.config = dumps(config.dict())
483
+ nxentry.attrs['station'] = config.station
484
+ if config.experiment_type == 'EDD':
485
+ if detector_names is None:
486
+ detector_indices = None
487
+ else:
488
+ nxentry.detector_names = detector_names
489
+ detector_indices = [int(d) for d in detector_names]
490
+ else:
491
+ if detector_names is not None:
492
+ nxentry.detector_names = detector_names
277
493
  nxentry.spec_scans = NXcollection()
278
- for scans in spec_config.spec_scans:
494
+ # nxpaths = []
495
+ for scans in config.spec_scans:
279
496
  nxscans = NXcollection()
280
497
  nxentry.spec_scans[f'{scans.scanparsers[0].scan_name}'] = nxscans
281
498
  nxscans.attrs['spec_file'] = str(scans.spec_file)
@@ -283,25 +500,45 @@ class SpecReader(Reader):
283
500
  for scan_number in scans.scan_numbers:
284
501
  scanparser = scans.get_scanparser(scan_number)
285
502
  nxscans[scan_number] = NXcollection()
286
- if hasattr(scanparser, 'spec_positioner_values'):
503
+ try:
287
504
  nxscans[scan_number].spec_motors = dumps(
288
505
  {k:float(v) for k,v
289
506
  in scanparser.spec_positioner_values.items()})
290
- if hasattr(scanparser, 'spec_scan_data'):
507
+ except:
508
+ pass
509
+ try:
291
510
  nxscans[scan_number].scan_columns = dumps(
292
511
  {k:list(v) for k,v
293
512
  in scanparser.spec_scan_data.items() if len(v)})
294
- if hasattr(scanparser, 'pars'):
513
+ except:
514
+ pass
515
+ try:
295
516
  nxscans[scan_number].smb_pars = dumps(
296
517
  {k:v for k,v in scanparser.pars.items()})
297
- if detector_names:
518
+ except:
519
+ pass
520
+ if config.experiment_type == 'EDD':
298
521
  nxdata = NXdata()
299
522
  nxscans[scan_number].data = nxdata
523
+ # nxpaths.append(
524
+ # f'spec_scans/{nxscans.nxname}/{scan_number}/data')
525
+ nxdata.data = NXfield(
526
+ value=scanparser.get_detector_data(detector_indices))
527
+ else:
528
+ nxdata = NXdata()
529
+ nxscans[scan_number].data = nxdata
530
+ # nxpaths.append(
531
+ # f'spec_scans/{nxscans.nxname}/{scan_number}/data')
300
532
  for detector_name in detector_names:
301
533
  nxdata[detector_name] = NXfield(
302
534
  value=scanparser.get_detector_data(detector_name))
303
535
 
304
- return nxentry
536
+ if config.experiment_type == 'EDD' and detector_names is None:
537
+ nxentry.detector_names = [
538
+ str(i) for i in range(nxdata.data.shape[1])]
539
+
540
+ #return nxroot, nxpaths
541
+ return nxroot
305
542
 
306
543
 
307
544
  class URLReader(Reader):
CHAP/common/writer.py CHANGED
@@ -89,13 +89,15 @@ def write_yaml(data, filename, force_overwrite=False):
89
89
 
90
90
  def write_filetree(data, outputdir, force_overwrite=False):
91
91
  # System modules
92
- from os import mkdir
92
+ from os import makedirs
93
93
 
94
94
  # Third party modules
95
95
  from nexusformat.nexus import (
96
96
  NXentry,
97
+ NXsubentry,
97
98
  NXgroup,
98
99
  NXobject,
100
+ NXroot,
99
101
  NXsubentry,
100
102
  )
101
103
 
@@ -103,8 +105,10 @@ def write_filetree(data, outputdir, force_overwrite=False):
103
105
  raise TypeError('Cannot write object of type'
104
106
  f'{type(data).__name__} as a file tree to disk.')
105
107
 
108
+ # FIX: Right now this can bomb if MultiplePipelineItem
109
+ # is called simultaneously from multiple nodes in MPI
106
110
  if not os_path.isdir(outputdir):
107
- mkdir(outputdir)
111
+ makedirs(outputdir)
108
112
 
109
113
  for k, v in data.items():
110
114
  if isinstance(v, NXsubentry) and 'schema' in v.attrs:
@@ -114,15 +118,28 @@ def write_filetree(data, outputdir, force_overwrite=False):
114
118
  write_txt(list(v.data), filename, force_overwrite)
115
119
  elif schema == 'json':
116
120
  write_txt(str(v.data), filename, force_overwrite)
121
+ elif schema == 'yml' or schema == 'yaml':
122
+ from json import loads
123
+ write_yaml(loads(v.data.nxdata), filename, force_overwrite)
117
124
  elif schema == 'tif' or schema == 'tiff':
118
125
  write_tif(v.data, filename, force_overwrite)
119
126
  elif schema == 'h5':
120
- nxentry = NXentry()
127
+ if any(isinstance(vv, NXsubentry) for vv in v.values()):
128
+ nxbase = NXroot()
129
+ else:
130
+ nxbase = NXentry()
121
131
  for kk, vv in v.attrs.items():
122
- nxentry.attrs[kk] = vv
132
+ if kk not in ('schema', 'filename'):
133
+ nxbase.attrs[kk] = vv
123
134
  for kk, vv in v.items():
124
- nxentry[kk] = vv
125
- write_nexus(nxentry, filename, force_overwrite)
135
+ if isinstance(vv, NXsubentry):
136
+ nxentry = NXentry()
137
+ nxbase[vv.nxname] = nxentry
138
+ for kkk, vvv in vv.items():
139
+ nxentry[kkk] = vvv
140
+ else:
141
+ nxbase[kk] = vv
142
+ write_nexus(nxbase, filename, force_overwrite)
126
143
  else:
127
144
  raise TypeError(f'Files of type {schema} not yet implemented')
128
145
  elif isinstance(v, NXgroup):
@@ -204,6 +221,44 @@ class FileTreeWriter(Writer):
204
221
  return data
205
222
 
206
223
 
224
+ class H5Writer(Writer):
225
+ """Writer for H5 files from an `nexusformat.nexus.NXdata` object"""
226
+ def write(self, data, filename, force_overwrite=False):
227
+ """Write the NeXus object contained in `data` to hdf5 file.
228
+
229
+ :param data: The data to write to file.
230
+ :type data: CHAP.pipeline.PipelineData
231
+ :param filename: The name of the file to write to.
232
+ :param force_overwrite: Flag to allow data in `filename` to be
233
+ overwritten if it already exists, defaults to `False`.
234
+ :type force_overwrite: bool, optional
235
+ :raises RuntimeError: If `filename` already exists and
236
+ `force_overwrite` is `False`.
237
+ :return: The data written to file.
238
+ :rtype: nexusformat.nexus.NXobject
239
+ """
240
+ # Third party modules
241
+ from h5py import File
242
+ from nexusformat.nexus import NXdata
243
+
244
+ data = self.unwrap_pipelinedata(data)[-1]
245
+ if not isinstance(data, NXdata):
246
+ raise ValueError('Invalid data parameter {(data)}')
247
+
248
+ mode = 'w' if force_overwrite else 'w-'
249
+ with File(filename, mode) as f:
250
+ f[data.signal] = data.nxsignal
251
+ for i, axes in enumerate(data.attrs['axes']):
252
+ f[axes] = data[axes]
253
+ f[data.signal].dims[i].label = \
254
+ f'{axes} ({data[axes].units})' \
255
+ if 'units' in data[axes].attrs else axes
256
+ f[axes].make_scale(axes)
257
+ f[data.signal].dims[i].attach_scale(f[axes])
258
+
259
+ return data
260
+
261
+
207
262
  class MatplotlibAnimationWriter(Writer):
208
263
  """Writer for saving matplotlib animations."""
209
264
  def write(self, data, filename, fps=1):
@@ -261,7 +316,7 @@ class MatplotlibFigureWriter(Writer):
261
316
 
262
317
  class NexusWriter(Writer):
263
318
  """Writer for NeXus files from `NXobject`-s"""
264
- def write(self, data, filename, force_overwrite=False):
319
+ def write(self, data, filename, nxpath=None, force_overwrite=False):
265
320
  """Write the NeXus object contained in `data` to file.
266
321
 
267
322
  :param data: The data to write to file.
@@ -275,26 +330,138 @@ class NexusWriter(Writer):
275
330
  :return: The data written to file.
276
331
  :rtype: nexusformat.nexus.NXobject
277
332
  """
333
+ # System modules
334
+ import os
335
+
336
+ # Third party modules
278
337
  from nexusformat.nexus import (
338
+ NXFile,
279
339
  NXentry,
340
+ NXobject,
280
341
  NXroot,
281
342
  )
343
+
282
344
  data = self.unwrap_pipelinedata(data)[-1]
283
- nxclass = data.nxclass
284
- nxname = data.nxname
285
- if nxclass == 'NXentry':
286
- data = NXroot(data)
287
- data[nxname].set_default()
288
- elif nxclass != 'NXroot':
289
- data = NXroot(NXentry(data))
290
- if nxclass == 'NXdata':
291
- data.entry[nxname].set_default()
292
- data.entry.set_default()
293
- write_nexus(data, filename, force_overwrite)
345
+ if not isinstance(data, NXobject):
346
+ return data
294
347
 
348
+ nxname = data.nxname
349
+ if not os.path.isfile(filename) and nxpath is not None:
350
+ self.logger.warning(
351
+ f'{filename} does not yet exist. Argument for nxpath '
352
+ '({nxpath}) will be ignored.')
353
+ nxpath = None
354
+ if nxpath is None:
355
+ nxclass = data.nxclass
356
+ if nxclass == 'NXentry':
357
+ data = NXroot(data)
358
+ data[nxname].set_default()
359
+ elif nxclass != 'NXroot':
360
+ data = NXroot(NXentry(data))
361
+ if nxclass == 'NXdata':
362
+ data.entry[nxname].set_default()
363
+ data.entry.set_default()
364
+ write_nexus(data, filename, force_overwrite)
365
+ else:
366
+ nxfile = NXFile(filename, 'rw')
367
+ root = nxfile.readfile()
368
+ if nxfile.get(nxpath) is None:
369
+ nxpath = root.NXentry[0].nxpath
370
+ self.logger.warning(
371
+ f'Path "{nxpath}" not present in {filename}. '
372
+ + f'Using {nxpath} instead.')
373
+ full_nxpath = os.path.join(nxpath, nxname)
374
+ self.logger.debug(f'Full path for object to write: {full_nxpath}')
375
+ if nxfile.get(full_nxpath) is not None:
376
+ self.logger.debug(
377
+ f'{os.path.join(nxpath, nxname)} already exists in '
378
+ f'{filename}')
379
+ if force_overwrite:
380
+ self.logger.warning(
381
+ 'Deleting existing NXobject at '
382
+ + f'{os.path.join(nxpath, nxname)} in {filename}')
383
+ del root[full_nxpath]
384
+ try:
385
+ root[full_nxpath] = data
386
+ except Exception as e:
387
+ nxfile.close()
388
+ raise e
389
+ nxfile.close()
295
390
  return data
296
391
 
297
392
 
393
+ class PyfaiResultsWriter(Writer):
394
+ """Writer for results of one or more pyFAI integrations. Able to
395
+ handle multiple output formats. Currently supported formats are:
396
+ .npz, .nxs.
397
+ """
398
+ def write(self, data, filename, force_overwrite=False):
399
+ """Save pyFAI integration results to a file. Format is
400
+ determined automatically form the extension of `filename`.
401
+
402
+ :param data: Integration results to save.
403
+ :type data: Union[PipelineData,
404
+ list[pyFAI.containers.IntegrateResult]]
405
+ :param filename: Name of the file to which results will be
406
+ saved. Format of output is determined ffrom the
407
+ extension. Currently supported formats are: `.npz`,
408
+ `.nxs`
409
+ :type filename: str
410
+ """
411
+ import os
412
+
413
+ from pyFAI.containers import Integrate1dResult, Integrate2dResult
414
+
415
+ try:
416
+ results = self.unwrap_pipelinedata(data)[0]
417
+ except:
418
+ results = data
419
+ if not isinstance(results, list):
420
+ results = [results]
421
+ if not all([isinstance(r, Integrate1dResult) for r in results]) \
422
+ and not all([isinstance(r, Integrate2dResult) for r in results]):
423
+ raise Exception(
424
+ 'Bad input data: all items must have the same type -- either '
425
+ + 'all pyFAI.containers.Integrate1dResult, or all '
426
+ + 'pyFAI.containers.Integrate2dResult.')
427
+
428
+ if os.path.isfile(filename):
429
+ if force_overwrite:
430
+ self.logger.warning(f'Removing existing file {filename}')
431
+ os.remove(filename)
432
+ else:
433
+ raise Exception(f'{filename} already exists.')
434
+ _, ext = os.path.splitext(filename)
435
+ if ext.lower() == '.npz':
436
+ self.write_npz(results, filename)
437
+ elif ext.lower() == '.nxs':
438
+ self.write_nxs(results, filename)
439
+ else:
440
+ raise Exception(f'Unsupported file format: {ext}')
441
+ self.logger.info(f'Wrote to {filename}')
442
+ return results
443
+
444
+ def write_npz(self, results, filename):
445
+ """Save `results` to the .npz file, `filename`"""
446
+ import numpy as np
447
+
448
+ data = {'radial': results[0].radial,
449
+ 'intensity': [r.intensity for r in results]}
450
+ if hasattr(results[0], 'azimuthal'):
451
+ # 2d results
452
+ data['azimuthal'] = results[0].azimuthal
453
+ if all([r.sigma for r in results]):
454
+ # errors were included
455
+ data['sigma'] = [r.sigma for r in results]
456
+
457
+ np.savez(filename, **data)
458
+
459
+ def write_nxs(results, filename):
460
+ """Save `results` to the .nxs file, `filename`"""
461
+ raise NotImplementedError
462
+
463
+
464
+
298
465
  class TXTWriter(Writer):
299
466
  """Writer for plain text files from string or tuples or lists of
300
467
  strings."""
@@ -345,6 +512,12 @@ class YAMLWriter(Writer):
345
512
  :rtype: dict
346
513
  """
347
514
  data = self.unwrap_pipelinedata(data)[-1]
515
+ try:
516
+ from pydantic import BaseModel
517
+ if isinstance(data, BaseModel):
518
+ data = data.dict()
519
+ except:
520
+ pass
348
521
  write_yaml(data, filename, force_overwrite)
349
522
  return data
350
523
 
CHAP/edd/__init__.py CHANGED
@@ -1,12 +1,19 @@
1
1
  """This subpackage contains `PipelineItems` unique to EDD data
2
2
  processing workflows.
3
3
  """
4
- # from CHAP.edd.reader import
4
+ from CHAP.edd.reader import (EddMapReader,
5
+ EddMPIMapReader,
6
+ ScanToMapReader,
7
+ SetupNXdataReader,
8
+ UpdateNXdataReader,
9
+ NXdataSliceReader)
5
10
  from CHAP.edd.processor import (DiffractionVolumeLengthProcessor,
6
11
  LatticeParameterRefinementProcessor,
7
- MCACeriaCalibrationProcessor,
12
+ MCAEnergyCalibrationProcessor,
13
+ MCATthCalibrationProcessor,
8
14
  MCADataProcessor,
9
15
  MCAEnergyCalibrationProcessor,
16
+ MCACalibratedDataPlotter,
10
17
  StrainAnalysisProcessor)
11
18
  # from CHAP.edd.writer import
12
19