ChessAnalysisPipeline 0.0.2__py3-none-any.whl → 0.0.4__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 (47) hide show
  1. CHAP/__init__.py +3 -0
  2. CHAP/common/__init__.py +19 -0
  3. CHAP/common/models/__init__.py +2 -0
  4. CHAP/common/models/integration.py +515 -0
  5. CHAP/common/models/map.py +535 -0
  6. CHAP/common/processor.py +644 -0
  7. CHAP/common/reader.py +119 -0
  8. CHAP/common/utils/__init__.py +37 -0
  9. CHAP/common/utils/fit.py +2613 -0
  10. CHAP/common/utils/general.py +1225 -0
  11. CHAP/common/utils/material.py +231 -0
  12. CHAP/common/utils/scanparsers.py +785 -0
  13. CHAP/common/writer.py +96 -0
  14. CHAP/edd/__init__.py +7 -0
  15. CHAP/edd/models.py +215 -0
  16. CHAP/edd/processor.py +321 -0
  17. CHAP/edd/reader.py +5 -0
  18. CHAP/edd/writer.py +5 -0
  19. CHAP/inference/__init__.py +3 -0
  20. CHAP/inference/processor.py +68 -0
  21. CHAP/inference/reader.py +5 -0
  22. CHAP/inference/writer.py +5 -0
  23. CHAP/pipeline.py +1 -1
  24. CHAP/processor.py +11 -818
  25. CHAP/reader.py +18 -113
  26. CHAP/saxswaxs/__init__.py +6 -0
  27. CHAP/saxswaxs/processor.py +5 -0
  28. CHAP/saxswaxs/reader.py +5 -0
  29. CHAP/saxswaxs/writer.py +5 -0
  30. CHAP/sin2psi/__init__.py +7 -0
  31. CHAP/sin2psi/processor.py +5 -0
  32. CHAP/sin2psi/reader.py +5 -0
  33. CHAP/sin2psi/writer.py +5 -0
  34. CHAP/tomo/__init__.py +5 -0
  35. CHAP/tomo/models.py +125 -0
  36. CHAP/tomo/processor.py +2009 -0
  37. CHAP/tomo/reader.py +5 -0
  38. CHAP/tomo/writer.py +5 -0
  39. CHAP/writer.py +17 -167
  40. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/METADATA +1 -1
  41. ChessAnalysisPipeline-0.0.4.dist-info/RECORD +50 -0
  42. CHAP/async.py +0 -56
  43. ChessAnalysisPipeline-0.0.2.dist-info/RECORD +0 -17
  44. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/LICENSE +0 -0
  45. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/WHEEL +0 -0
  46. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/entry_points.txt +0 -0
  47. {ChessAnalysisPipeline-0.0.2.dist-info → ChessAnalysisPipeline-0.0.4.dist-info}/top_level.txt +0 -0
CHAP/processor.py CHANGED
@@ -102,831 +102,24 @@ class TFaaSImageProcessor(Processor):
102
102
 
103
103
  return(data)
104
104
 
105
- class URLResponseProcessor(Processor):
106
- def _process(self, data):
107
- '''Take data returned from URLReader.read and return a decoded version of
108
- the content.
109
-
110
- :param data: input data (output of URLReader.read)
111
- :type data: list[dict]
112
- :return: decoded data contents
113
- :rtype: object
114
- '''
115
-
116
- data = data[0]
117
-
118
- content = data['data']
119
- encoding = data['encoding']
120
-
121
- self.logger.debug(f'Decoding content of type {type(content)} with {encoding}')
122
-
123
- try:
124
- content = content.decode(encoding)
125
- except:
126
- self.logger.warning(f'Failed to decode content of type {type(content)} with {encoding}')
127
-
128
- return(content)
129
-
130
- class PrintProcessor(Processor):
131
- '''A Processor to simply print the input data to stdout and return the
132
- original input data, unchanged in any way.
133
- '''
134
-
135
- def _process(self, data):
136
- '''Print and return the input data.
137
-
138
- :param data: Input data
139
- :type data: object
140
- :return: `data`
141
- :rtype: object
142
- '''
143
-
144
- print(f'{self.__name__} data :')
145
-
146
- if callable(getattr(data, '_str_tree', None)):
147
- # If data is likely an NXobject, print its tree representation
148
- # (since NXobjects' str representations are just their nxname -- not
149
- # very helpful).
150
- print(data._str_tree(attrs=True, recursive=True))
151
- else:
152
- print(str(data))
153
-
154
- return(data)
155
-
156
- class NexusToNumpyProcessor(Processor):
157
- '''A class to convert the default plottable data in an `NXobject` into an
158
- `numpy.ndarray`.
159
- '''
160
-
161
- def _process(self, data):
162
- '''Return the default plottable data signal in `data` as an
163
- `numpy.ndarray`.
164
-
165
- :param data: input NeXus structure
166
- :type data: nexusformat.nexus.tree.NXobject
167
- :raises ValueError: if `data` has no default plottable data signal
168
- :return: default plottable data signal in `data`
169
- :rtype: numpy.ndarray
170
- '''
171
-
172
- default_data = data.plottable_data
173
-
174
- if default_data is None:
175
- default_data_path = data.attrs['default']
176
- default_data = data.get(default_data_path)
177
- if default_data is None:
178
- raise(ValueError(f'The structure of {data} contains no default data'))
179
-
180
- default_signal = default_data.attrs.get('signal')
181
- if default_signal is None:
182
- raise(ValueError(f'The signal of {default_data} is unknown'))
183
- default_signal = default_signal.nxdata
184
-
185
- np_data = default_data[default_signal].nxdata
186
-
187
- return(np_data)
188
-
189
- class NexusToXarrayProcessor(Processor):
190
- '''A class to convert the default plottable data in an `NXobject` into an
191
- `xarray.DataArray`.'''
192
-
193
- def _process(self, data):
194
- '''Return the default plottable data signal in `data` as an
195
- `xarray.DataArray`.
196
-
197
- :param data: input NeXus structure
198
- :type data: nexusformat.nexus.tree.NXobject
199
- :raises ValueError: if metadata for `xarray` is absen from `data`
200
- :return: default plottable data signal in `data`
201
- :rtype: xarray.DataArray
202
- '''
203
-
204
- from xarray import DataArray
205
-
206
- default_data = data.plottable_data
207
-
208
- if default_data is None:
209
- default_data_path = data.attrs['default']
210
- default_data = data.get(default_data_path)
211
- if default_data is None:
212
- raise(ValueError(f'The structure of {data} contains no default data'))
213
-
214
- default_signal = default_data.attrs.get('signal')
215
- if default_signal is None:
216
- raise(ValueError(f'The signal of {default_data} is unknown'))
217
- default_signal = default_signal.nxdata
218
-
219
- signal_data = default_data[default_signal].nxdata
220
-
221
- axes = default_data.attrs['axes']
222
- coords = {}
223
- for axis_name in axes:
224
- axis = default_data[axis_name]
225
- coords[axis_name] = (axis_name,
226
- axis.nxdata,
227
- axis.attrs)
228
-
229
- dims = tuple(axes)
230
-
231
- name = default_signal
232
-
233
- attrs = default_data[default_signal].attrs
234
-
235
- return(DataArray(data=signal_data,
236
- coords=coords,
237
- dims=dims,
238
- name=name,
239
- attrs=attrs))
240
-
241
- class XarrayToNexusProcessor(Processor):
242
- '''A class to convert the data in an `xarray` structure to an
243
- `nexusformat.nexus.NXdata`.
244
- '''
245
-
246
- def _process(self, data):
247
- '''Return `data` represented as an `nexusformat.nexus.NXdata`.
248
-
249
- :param data: The input `xarray` structure
250
- :type data: typing.Union[xarray.DataArray, xarray.Dataset]
251
- :return: The data and metadata in `data`
252
- :rtype: nexusformat.nexus.NXdata
253
- '''
254
-
255
- from nexusformat.nexus import NXdata, NXfield
256
-
257
- signal = NXfield(value=data.data, name=data.name, attrs=data.attrs)
258
-
259
- axes = []
260
- for name, coord in data.coords.items():
261
- axes.append(NXfield(value=coord.data, name=name, attrs=coord.attrs))
262
- axes = tuple(axes)
263
-
264
- return(NXdata(signal=signal, axes=axes))
265
-
266
- class XarrayToNumpyProcessor(Processor):
267
- '''A class to convert the data in an `xarray.DataArray` structure to an
268
- `numpy.ndarray`.
269
- '''
270
-
271
- def _process(self, data):
272
- '''Return just the signal values contained in `data`.
273
-
274
- :param data: The input `xarray.DataArray`
275
- :type data: xarray.DataArray
276
- :return: The data in `data`
277
- :rtype: numpy.ndarray
278
- '''
279
-
280
- return(data.data)
281
-
282
- class MapProcessor(Processor):
283
- '''Class representing a process that takes a map configuration and returns a
284
- `nexusformat.nexus.NXentry` representing that map's metadata and any
285
- scalar-valued raw data requseted by the supplied map configuration.
286
- '''
287
-
288
- def _process(self, data):
289
- '''Process the output of a `Reader` that contains a map configuration and
290
- return a `nexusformat.nexus.NXentry` representing the map.
291
-
292
- :param data: Result of `Reader.read` where at least one item has the
293
- value `'MapConfig'` for the `'schema'` key.
294
- :type data: list[dict[str,object]]
295
- :return: Map data & metadata (SPEC only, no detector)
296
- :rtype: nexusformat.nexus.NXentry
297
- '''
298
-
299
- map_config = self.get_map_config(data)
300
- nxentry = self.__class__.get_nxentry(map_config)
301
-
302
- return(nxentry)
303
-
304
- def get_map_config(self, data):
305
- '''Get an instance of `MapConfig` from a returned value of `Reader.read`
306
-
307
- :param data: Result of `Reader.read` where at least one item has the
308
- value `'MapConfig'` for the `'schema'` key.
309
- :type data: list[dict[str,object]]
310
- :raises Exception: If a valid `MapConfig` cannot be constructed from `data`.
311
- :return: a valid instance of `MapConfig` with field values taken from `data`.
312
- :rtype: MapConfig
313
- '''
314
-
315
- from CHAP.models.map import MapConfig
316
-
317
- map_config = False
318
- if isinstance(data, list):
319
- for item in data:
320
- if isinstance(item, dict):
321
- if item.get('schema') == 'MapConfig':
322
- map_config = item.get('data')
323
- break
324
-
325
- if not map_config:
326
- raise(ValueError('No map configuration found'))
327
-
328
- return(MapConfig(**map_config))
329
-
330
- @staticmethod
331
- def get_nxentry(map_config):
332
- '''Use a `MapConfig` to construct a `nexusformat.nexus.NXentry`
333
-
334
- :param map_config: a valid map configuration
335
- :type map_config: MapConfig
336
- :return: the map's data and metadata contained in a NeXus structure
337
- :rtype: nexusformat.nexus.NXentry
338
- '''
339
-
340
- from nexusformat.nexus import (NXcollection,
341
- NXdata,
342
- NXentry,
343
- NXfield,
344
- NXsample)
345
- import numpy as np
346
-
347
- nxentry = NXentry(name=map_config.title)
348
-
349
- nxentry.map_config = json.dumps(map_config.dict())
350
-
351
- nxentry[map_config.sample.name] = NXsample(**map_config.sample.dict())
352
-
353
- nxentry.attrs['station'] = map_config.station
354
-
355
- nxentry.spec_scans = NXcollection()
356
- for scans in map_config.spec_scans:
357
- nxentry.spec_scans[scans.scanparsers[0].scan_name] = \
358
- NXfield(value=scans.scan_numbers,
359
- dtype='int8',
360
- attrs={'spec_file':str(scans.spec_file)})
361
-
362
- nxentry.data = NXdata()
363
- nxentry.data.attrs['axes'] = map_config.dims
364
- for i,dim in enumerate(map_config.independent_dimensions[::-1]):
365
- nxentry.data[dim.label] = NXfield(value=map_config.coords[dim.label],
366
- units=dim.units,
367
- attrs={'long_name': f'{dim.label} ({dim.units})',
368
- 'data_type': dim.data_type,
369
- 'local_name': dim.name})
370
- nxentry.data.attrs[f'{dim.label}_indices'] = i
371
-
372
- signal = False
373
- auxilliary_signals = []
374
- for data in map_config.all_scalar_data:
375
- nxentry.data[data.label] = NXfield(value=np.empty(map_config.shape),
376
- units=data.units,
377
- attrs={'long_name': f'{data.label} ({data.units})',
378
- 'data_type': data.data_type,
379
- 'local_name': data.name})
380
- if not signal:
381
- signal = data.label
382
- else:
383
- auxilliary_signals.append(data.label)
384
-
385
- if signal:
386
- nxentry.data.attrs['signal'] = signal
387
- nxentry.data.attrs['auxilliary_signals'] = auxilliary_signals
388
-
389
- for scans in map_config.spec_scans:
390
- for scan_number in scans.scan_numbers:
391
- scanparser = scans.get_scanparser(scan_number)
392
- for scan_step_index in range(scanparser.spec_scan_npts):
393
- map_index = scans.get_index(scan_number, scan_step_index, map_config)
394
- for data in map_config.all_scalar_data:
395
- nxentry.data[data.label][map_index] = data.get_value(scans, scan_number, scan_step_index)
396
-
397
- return(nxentry)
398
-
399
- class IntegrationProcessor(Processor):
400
- '''Class for integrating 2D detector data
401
- '''
402
-
403
- def _process(self, data):
404
- '''Integrate the input data with the integration method and keyword
405
- arguments supplied and return the results.
406
-
407
- :param data: input data, including raw data, integration method, and
408
- keyword args for the integration method.
409
- :type data: tuple[typing.Union[numpy.ndarray, list[numpy.ndarray]],
410
- callable,
411
- dict]
412
- :param integration_method: the method of a
413
- `pyFAI.azimuthalIntegrator.AzimuthalIntegrator` or
414
- `pyFAI.multi_geometry.MultiGeometry` that returns the desired
415
- integration results.
416
- :return: integrated raw data
417
- :rtype: pyFAI.containers.IntegrateResult
418
- '''
419
-
420
- detector_data, integration_method, integration_kwargs = data
421
-
422
- return(integration_method(detector_data, **integration_kwargs))
423
-
424
- class IntegrateMapProcessor(Processor):
425
- '''Class representing a process that takes a map and integration
426
- configuration and returns a `nexusformat.nexus.NXprocess` containing a map of
427
- the integrated detector data requested.
428
- '''
429
-
430
- def _process(self, data):
431
- '''Process the output of a `Reader` that contains a map and integration
432
- configuration and return a `nexusformat.nexus.NXprocess` containing a map
433
- of the integrated detector data requested
434
-
435
- :param data: Result of `Reader.read` where at least one item has the
436
- value `'MapConfig'` for the `'schema'` key, and at least one item has
437
- the value `'IntegrationConfig'` for the `'schema'` key.
438
- :type data: list[dict[str,object]]
439
- :return: integrated data and process metadata
440
- :rtype: nexusformat.nexus.NXprocess
441
- '''
442
-
443
- map_config, integration_config = self.get_configs(data)
444
- nxprocess = self.get_nxprocess(map_config, integration_config)
445
-
446
- return(nxprocess)
447
-
448
- def get_configs(self, data):
449
- '''Return valid instances of `MapConfig` and `IntegrationConfig` from the
450
- input supplied by `MultipleReader`.
451
-
452
- :param data: Result of `Reader.read` where at least one item has the
453
- value `'MapConfig'` for the `'schema'` key, and at least one item has
454
- the value `'IntegrationConfig'` for the `'schema'` key.
455
- :type data: list[dict[str,object]]
456
- :raises ValueError: if `data` cannot be parsed into map and integration configurations.
457
- :return: valid map and integration configuration objects.
458
- :rtype: tuple[MapConfig, IntegrationConfig]
459
- '''
460
-
461
- self.logger.debug('Getting configuration objects')
462
- t0 = time()
463
-
464
- from CHAP.models.map import MapConfig
465
- from CHAP.models.integration import IntegrationConfig
466
-
467
- map_config = False
468
- integration_config = False
469
- if isinstance(data, list):
470
- for item in data:
471
- if isinstance(item, dict):
472
- schema = item.get('schema')
473
- if schema == 'MapConfig':
474
- map_config = item.get('data')
475
- elif schema == 'IntegrationConfig':
476
- integration_config = item.get('data')
477
-
478
- if not map_config:
479
- raise(ValueError('No map configuration found'))
480
- if not integration_config:
481
- raise(ValueError('No integration configuration found'))
482
-
483
- map_config = MapConfig(**map_config)
484
- integration_config = IntegrationConfig(**integration_config)
485
-
486
- self.logger.debug(f'Got configuration objects in {time()-t0:.3f} seconds')
487
-
488
- return(map_config, integration_config)
489
-
490
- def get_nxprocess(self, map_config, integration_config):
491
- '''Use a `MapConfig` and `IntegrationConfig` to construct a
492
- `nexusformat.nexus.NXprocess`
493
-
494
- :param map_config: a valid map configuration
495
- :type map_config: MapConfig
496
- :param integration_config: a valid integration configuration
497
- :type integration_config" IntegrationConfig
498
- :return: the integrated detector data and metadata contained in a NeXus
499
- structure
500
- :rtype: nexusformat.nexus.NXprocess
501
- '''
502
-
503
- self.logger.debug('Constructing NXprocess')
504
- t0 = time()
505
-
506
- from nexusformat.nexus import (NXdata,
507
- NXdetector,
508
- NXfield,
509
- NXprocess)
510
- import numpy as np
511
- import pyFAI
512
-
513
- nxprocess = NXprocess(name=integration_config.title)
514
-
515
- nxprocess.map_config = json.dumps(map_config.dict())
516
- nxprocess.integration_config = json.dumps(integration_config.dict())
517
-
518
- nxprocess.program = 'pyFAI'
519
- nxprocess.version = pyFAI.version
520
-
521
- for k,v in integration_config.dict().items():
522
- if k == 'detectors':
523
- continue
524
- nxprocess.attrs[k] = v
525
-
526
- for detector in integration_config.detectors:
527
- nxprocess[detector.prefix] = NXdetector()
528
- nxprocess[detector.prefix].local_name = detector.prefix
529
- nxprocess[detector.prefix].distance = detector.azimuthal_integrator.dist
530
- nxprocess[detector.prefix].distance.attrs['units'] = 'm'
531
- nxprocess[detector.prefix].calibration_wavelength = detector.azimuthal_integrator.wavelength
532
- nxprocess[detector.prefix].calibration_wavelength.attrs['units'] = 'm'
533
- nxprocess[detector.prefix].attrs['poni_file'] = str(detector.poni_file)
534
- nxprocess[detector.prefix].attrs['mask_file'] = str(detector.mask_file)
535
- nxprocess[detector.prefix].raw_data_files = np.full(map_config.shape, '', dtype='|S256')
536
-
537
- nxprocess.data = NXdata()
538
-
539
- nxprocess.data.attrs['axes'] = (*map_config.dims, *integration_config.integrated_data_dims)
540
- for i,dim in enumerate(map_config.independent_dimensions[::-1]):
541
- nxprocess.data[dim.label] = NXfield(value=map_config.coords[dim.label],
542
- units=dim.units,
543
- attrs={'long_name': f'{dim.label} ({dim.units})',
544
- 'data_type': dim.data_type,
545
- 'local_name': dim.name})
546
- nxprocess.data.attrs[f'{dim.label}_indices'] = i
547
-
548
- for i,(coord_name,coord_values) in enumerate(integration_config.integrated_data_coordinates.items()):
549
- if coord_name == 'radial':
550
- type_ = pyFAI.units.RADIAL_UNITS
551
- elif coord_name == 'azimuthal':
552
- type_ = pyFAI.units.AZIMUTHAL_UNITS
553
- coord_units = pyFAI.units.to_unit(getattr(integration_config, f'{coord_name}_units'), type_=type_)
554
- nxprocess.data[coord_units.name] = coord_values
555
- nxprocess.data.attrs[f'{coord_units.name}_indices'] = i+len(map_config.coords)
556
- nxprocess.data[coord_units.name].units = coord_units.unit_symbol
557
- nxprocess.data[coord_units.name].attrs['long_name'] = coord_units.label
558
-
559
- nxprocess.data.attrs['signal'] = 'I'
560
- nxprocess.data.I = NXfield(value=np.empty((*tuple([len(coord_values) for coord_name,coord_values in map_config.coords.items()][::-1]), *integration_config.integrated_data_shape)),
561
- units='a.u',
562
- attrs={'long_name':'Intensity (a.u)'})
563
-
564
- integrator = integration_config.get_multi_geometry_integrator()
565
- if integration_config.integration_type == 'azimuthal':
566
- integration_method = integrator.integrate1d
567
- integration_kwargs = {
568
- 'lst_mask': [detector.mask_array for detector in integration_config.detectors],
569
- 'npt': integration_config.radial_npt
570
- }
571
- elif integration_config.integration_type == 'cake':
572
- integration_method = integrator.integrate2d
573
- integration_kwargs = {
574
- 'lst_mask': [detector.mask_array for detector in integration_config.detectors],
575
- 'npt_rad': integration_config.radial_npt,
576
- 'npt_azim': integration_config.azimuthal_npt,
577
- 'method': 'bbox'
578
- }
579
-
580
- integration_processor = IntegrationProcessor()
581
- integration_processor.logger.setLevel(self.logger.getEffectiveLevel())
582
- integration_processor.logger.addHandler(self.logger.handlers[0])
583
- lst_args = []
584
- for scans in map_config.spec_scans:
585
- for scan_number in scans.scan_numbers:
586
- scanparser = scans.get_scanparser(scan_number)
587
- for scan_step_index in range(scanparser.spec_scan_npts):
588
- map_index = scans.get_index(scan_number, scan_step_index, map_config)
589
- detector_data = scans.get_detector_data(integration_config.detectors, scan_number, scan_step_index)
590
- result = integration_processor.process((detector_data, integration_method, integration_kwargs))
591
- nxprocess.data.I[map_index] = result.intensity
592
- for detector in integration_config.detectors:
593
- nxprocess[detector.prefix].raw_data_files[map_index] = scanparser.get_detector_data_file(detector.prefix, scan_step_index)
594
-
595
- self.logger.debug(f'Constructed NXprocess in {time()-t0:.3f} seconds')
596
-
597
- return(nxprocess)
598
-
599
- class MCACeriaCalibrationProcessor(Processor):
600
- '''Class representing the procedure to use a CeO2 scan to obtain tuned values
601
- for the bragg diffraction angle and linear correction parameters for MCA
602
- channel energies for an EDD experimental setup.
603
- '''
604
-
605
- def _process(self, data):
606
- '''Return tuned values for 2&theta and linear correction parameters for
607
- the MCA channel energies.
608
-
609
- :param data: input configuration for the raw data & tuning procedure
610
- :type data: list[dict[str,object]]
611
- :return: original configuration dictionary with tuned values added
612
- :rtype: dict[str,float]
613
- '''
614
-
615
- calibration_config = self.get_config(data)
616
-
617
- tth, slope, intercept = self.calibrate(calibration_config)
618
-
619
- calibration_config.tth_calibrated = tth
620
- calibration_config.slope_calibrated = slope
621
- calibration_config.intercept_calibrated = intercept
622
-
623
- return(calibration_config.dict())
624
-
625
- def get_config(self, data):
626
- '''Get an instance of the configuration object needed by this
627
- `Processor` from a returned value of `Reader.read`
628
-
629
- :param data: Result of `Reader.read` where at least one item has the
630
- value `'MCACeriaCalibrationConfig'` for the `'schema'` key.
631
- :type data: list[dict[str,object]]
632
- :raises Exception: If a valid config object cannot be constructed from `data`.
633
- :return: a valid instance of a configuration object with field values
634
- taken from `data`.
635
- :rtype: MCACeriaCalibrationConfig
636
- '''
637
-
638
- from CHAP.models.edd import MCACeriaCalibrationConfig
639
-
640
- calibration_config = False
641
- if isinstance(data, list):
642
- for item in data:
643
- if isinstance(item, dict):
644
- if item.get('schema') == 'MCACeriaCalibrationConfig':
645
- calibration_config = item.get('data')
646
- break
647
-
648
- if not calibration_config:
649
- raise(ValueError('No MCA ceria calibration configuration found in input data'))
650
-
651
- return(MCACeriaCalibrationConfig(**calibration_config))
652
-
653
- def calibrate(self, calibration_config):
654
- '''Iteratively calibrate 2&theta by fitting selected peaks of an MCA
655
- spectrum until the computed strain is sufficiently small. Use the fitted
656
- peak locations to determine linear correction parameters for the MCA's
657
- channel energies.
658
-
659
- :param calibration_config: object configuring the CeO2 calibration procedure
660
- :type calibration_config: MCACeriaCalibrationConfig
661
- :return: calibrated values of 2&theta and linear correction parameters
662
- for MCA channel energies : tth, slope, intercept
663
- :rtype: float, float, float
664
- '''
665
-
666
- from msnctools.fit import Fit, FitMultipeak
667
- import numpy as np
668
- from scipy.constants import physical_constants
669
-
670
- hc = physical_constants['Planck constant in eV/Hz'][0] * \
671
- physical_constants['speed of light in vacuum'][0] * \
672
- 1e7 # We'll work in keV and A, not eV and m.
673
-
674
- # Collect raw MCA data of interest
675
- mca_data = calibration_config.mca_data()
676
- mca_bin_energies = np.arange(0, calibration_config.num_bins) * \
677
- (calibration_config.max_energy_kev / calibration_config.num_bins)
678
-
679
- # Mask out the corrected MCA data for fitting
680
- mca_mask = calibration_config.mca_mask()
681
- fit_mca_energies = mca_bin_energies[mca_mask]
682
- fit_mca_intensities = mca_data[mca_mask]
683
-
684
- # Correct raw MCA data for variable flux at different energies
685
- flux_correct = calibration_config.flux_correction_interpolation_function()
686
- mca_intensity_weights = flux_correct(fit_mca_energies)
687
- fit_mca_intensities = fit_mca_intensities / mca_intensity_weights
688
-
689
- # Get the HKLs and lattice spacings that will be used for fitting
690
- tth = calibration_config.tth_initial_guess
691
- fit_hkls, fit_ds = calibration_config.fit_ds()
692
- c_1 = fit_hkls[:,0]**2 + fit_hkls[:,1]**2 + fit_hkls[:,2]**2
693
-
694
- for iter_i in range(calibration_config.max_iter):
695
-
696
- ### Perform the uniform fit first ###
697
-
698
- # Get expected peak energy locations for this iteration's starting
699
- # value of tth
700
- fit_lambda = 2.0 * fit_ds * np.sin(0.5*np.radians(tth))
701
- fit_E0 = hc / fit_lambda
702
-
703
- # Run the uniform fit
704
- best_fit, residual, best_values, best_errors, redchi, success = \
705
- FitMultipeak.fit_multipeak(fit_mca_intensities,
706
- fit_E0,
707
- x=fit_mca_energies,
708
- fit_type='uniform')
709
-
710
- # Extract values of interest from the best values for the uniform fit
711
- # parameters
712
- uniform_fit_centers = [best_values[f'peak{i+1}_center'] for i in range(len(calibration_config.fit_hkls))]
713
- # uniform_a = best_values['scale_factor']
714
- # uniform_strain = np.log(uniform_a / calibration_config.lattice_parameter_angstrom)
715
- # uniform_tth = tth * (1.0 + uniform_strain)
716
- # uniform_rel_rms_error = np.linalg.norm(residual) / np.linalg.norm(fit_mca_intensities)
717
-
718
- ### Next, perform the unconstrained fit ###
719
-
720
- # Use the peak locations found in the uniform fit as the initial
721
- # guesses for peak locations in the unconstrained fit
722
- best_fit, residual, best_values, best_errors, redchi, success = \
723
- FitMultipeak.fit_multipeak(fit_mca_intensities,
724
- uniform_fit_centers,
725
- x=fit_mca_energies,
726
- fit_type='unconstrained')
727
-
728
- # Extract values of interest from the best values for the
729
- # unconstrained fit parameters
730
- unconstrained_fit_centers = np.array([best_values[f'peak{i+1}_center'] for i in range(len(calibration_config.fit_hkls))])
731
- unconstrained_a = 0.5 * hc * np.sqrt(c_1) / (unconstrained_fit_centers * abs(np.sin(0.5*np.radians(tth))))
732
- unconstrained_strains = np.log(unconstrained_a / calibration_config.lattice_parameter_angstrom)
733
- unconstrained_strain = np.mean(unconstrained_strains)
734
- unconstrained_tth = tth * (1.0 + unconstrained_strain)
735
- # unconstrained_rel_rms_error = np.linalg.norm(residual) / np.linalg.norm(fit_mca_intensities)
736
-
737
-
738
- # Update tth for the next iteration of tuning
739
- prev_tth = tth
740
- tth = unconstrained_tth
741
-
742
- # Stop tuning tth at this iteration if differences are small enough
743
- if abs(tth - prev_tth) < calibration_config.tune_tth_tol:
744
- break
745
-
746
- # Fit line to expected / computed peak locations from the last
747
- # unconstrained fit.
748
- fit = Fit.fit_data(fit_E0,'linear', x=unconstrained_fit_centers, nan_policy='omit')
749
- slope = fit.best_values['slope']
750
- intercept = fit.best_values['intercept']
751
-
752
- return(float(tth), float(slope), float(intercept))
753
-
754
- class MCADataProcessor(Processor):
755
- '''Class representing a process to return data from a MCA, restuctured to
756
- incorporate the shape & metadata associated with a map configuration to
757
- which the MCA data belongs, and linearly transformed according to the
758
- results of a ceria calibration.
759
- '''
760
-
761
- def _process(self, data):
762
- '''Process configurations for a map and MCA detector(s), and return the
763
- raw MCA data collected over the map.
764
-
765
- :param data: input map configuration and results of ceria calibration
766
- :type data: list[dict[str,object]]
767
- :return: calibrated and flux-corrected MCA data
768
- :rtype: nexusformat.nexus.NXentry
769
- '''
770
-
771
- map_config, calibration_config = self.get_configs(data)
772
- nxroot = self.get_nxroot(map_config, calibration_config)
773
-
774
- return(nxroot)
775
-
776
- def get_configs(self, data):
777
- '''Get instances of the configuration objects needed by this
778
- `Processor` from a returned value of `Reader.read`
779
-
780
- :param data: Result of `Reader.read` where at least one item has the
781
- value `'MapConfig'` for the `'schema'` key, and at least one item has
782
- the value `'MCACeriaCalibrationConfig'` for the `'schema'` key.
783
- :type data: list[dict[str,object]]
784
- :raises Exception: If valid config objects cannot be constructed from `data`.
785
- :return: valid instances of the configuration objects with field values
786
- taken from `data`.
787
- :rtype: tuple[MapConfig, MCACeriaCalibrationConfig]
788
- '''
789
-
790
- from CHAP.models.map import MapConfig
791
- from CHAP.models.edd import MCACeriaCalibrationConfig
792
-
793
- map_config = False
794
- calibration_config = False
795
- if isinstance(data, list):
796
- for item in data:
797
- if isinstance(item, dict):
798
- schema = item.get('schema')
799
- if schema == 'MapConfig':
800
- map_config = item.get('data')
801
- elif schema == 'MCACeriaCalibrationConfig':
802
- calibration_config = item.get('data')
803
-
804
- if not map_config:
805
- raise(ValueError('No map configuration found in input data'))
806
- if not calibration_config:
807
- raise(ValueError('No MCA ceria calibration configuration found in input data'))
808
-
809
- return(MapConfig(**map_config), MCACeriaCalibrationConfig(**calibration_config))
810
-
811
- def get_nxroot(self, map_config, calibration_config):
812
- '''Get a map of the MCA data collected by the scans in `map_config`. The
813
- MCA data will be calibrated and flux-corrected according to the
814
- parameters included in `calibration_config`. The data will be returned
815
- along with relevant metadata in the form of a NeXus structure.
816
-
817
- :param map_config: the map configuration
818
- :type map_config: MapConfig
819
- :param calibration_config: the calibration configuration
820
- :type calibration_config: MCACeriaCalibrationConfig
821
- :return: a map of the calibrated and flux-corrected MCA data
822
- :rtype: nexusformat.nexus.NXroot
823
- '''
824
-
825
- from nexusformat.nexus import (NXdata,
826
- NXdetector,
827
- NXentry,
828
- NXinstrument,
829
- NXroot)
830
- import numpy as np
831
-
832
- nxroot = NXroot()
833
-
834
- nxroot[map_config.title] = MapProcessor.get_nxentry(map_config)
835
- nxentry = nxroot[map_config.title]
836
-
837
- nxentry.instrument = NXinstrument()
838
- nxentry.instrument.detector = NXdetector()
839
- nxentry.instrument.detector.calibration_configuration = json.dumps(calibration_config.dict())
840
-
841
- nxentry.instrument.detector.data = NXdata()
842
- nxdata = nxentry.instrument.detector.data
843
- nxdata.raw = np.empty((*map_config.shape, calibration_config.num_bins))
844
- nxdata.raw.attrs['units'] = 'counts'
845
- nxdata.channel_energy = calibration_config.slope_calibrated * \
846
- np.arange(0, calibration_config.num_bins) * \
847
- (calibration_config.max_energy_kev / calibration_config.num_bins) + \
848
- calibration_config.intercept_calibrated
849
- nxdata.channel_energy.attrs['units'] = 'keV'
850
-
851
- for scans in map_config.spec_scans:
852
- for scan_number in scans.scan_numbers:
853
- scanparser = scans.get_scanparser(scan_number)
854
- for scan_step_index in range(scanparser.spec_scan_npts):
855
- map_index = scans.get_index(scan_number, scan_step_index, map_config)
856
- nxdata.raw[map_index] = scanparser.get_detector_data(calibration_config.detector_name, scan_step_index)
857
-
858
- nxentry.data.makelink(nxdata.raw, name=calibration_config.detector_name)
859
- nxentry.data.makelink(nxdata.channel_energy, name=f'{calibration_config.detector_name}_channel_energy')
860
- if isinstance(nxentry.data.attrs['axes'], str):
861
- nxentry.data.attrs['axes'] = [nxentry.data.attrs['axes'], f'{calibration_config.detector_name}_channel_energy']
862
- else:
863
- nxentry.data.attrs['axes'] += [f'{calibration_config.detector_name}_channel_energy']
864
- nxentry.data.attrs['signal'] = calibration_config.detector_name
865
-
866
- return(nxroot)
867
-
868
- class StrainAnalysisProcessor(Processor):
869
- '''Class representing a process to compute a map of sample strains by fitting
870
- bragg peaks in 1D detector data and analyzing the difference between measured
871
- peak locations and expected peak locations for the sample measured.
872
- '''
873
-
874
- def _process(self, data):
875
- '''Process the input map detector data & configuration for the strain
876
- analysis procedure, and return a map of sample strains.
877
-
878
- :param data: results of `MutlipleReader.read` containing input map
879
- detector data and strain analysis configuration
880
- :type data: dict[list[str,object]]
881
- :return: map of sample strains
882
- :rtype: xarray.Dataset
883
- '''
884
-
885
- strain_analysis_config = self.get_config(data)
886
-
887
- return(data)
888
-
889
- def get_config(self, data):
890
- '''Get instances of the configuration objects needed by this
891
- `Processor` from a returned value of `Reader.read`
892
-
893
- :param data: Result of `Reader.read` where at least one item has the
894
- value `'StrainAnalysisConfig'` for the `'schema'` key.
895
- :type data: list[dict[str,object]]
896
- :raises Exception: If valid config objects cannot be constructed from `data`.
897
- :return: valid instances of the configuration objects with field values
898
- taken from `data`.
899
- :rtype: StrainAnalysisConfig
900
- '''
901
-
902
- strain_analysis_config = False
903
- if isinstance(data, list):
904
- for item in data:
905
- if isinstance(item, dict):
906
- schema = item.get('schema')
907
- if item.get('schema') == 'StrainAnalysisConfig':
908
- strain_analysis_config = item.get('data')
909
-
910
- if not strain_analysis_config:
911
- raise(ValueError('No strain analysis configuration found in input data'))
912
-
913
- return(strain_analysis_config)
914
-
915
-
916
105
  class OptionParser():
917
106
  '''User based option parser'''
918
107
  def __init__(self):
919
108
  self.parser = argparse.ArgumentParser(prog='PROG')
920
- self.parser.add_argument("--data", action="store",
921
- dest="data", default="", help="Input data")
922
- self.parser.add_argument("--processor", action="store",
923
- dest="processor", default="Processor", help="Processor class name")
924
- self.parser.add_argument('--log-level', choices=logging._nameToLevel.keys(),
109
+ self.parser.add_argument(
110
+ '--data', action='store',
111
+ dest='data', default='', help='Input data')
112
+ self.parser.add_argument(
113
+ '--processor', action='store',
114
+ dest='processor', default='Processor', help='Processor class name')
115
+ self.parser.add_argument(
116
+ '--log-level', choices=logging._nameToLevel.keys(),
925
117
  dest='log_level', default='INFO', help='logging level')
926
118
 
927
- def main():
119
+ def main(opt_parser=OptionParser):
928
120
  '''Main function'''
929
- optmgr = OptionParser()
121
+
122
+ optmgr = opt_parser()
930
123
  opts = optmgr.parser.parse_args()
931
124
  clsName = opts.processor
932
125
  try: