ChessAnalysisPipeline 0.0.11__py3-none-any.whl → 0.0.13__py3-none-any.whl

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

Potentially problematic release.


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

CHAP/edd/processor.py CHANGED
@@ -7,14 +7,15 @@ Author : Keara Soloway, Rolf Verberg
7
7
  Description: Module for Processors used only by EDD experiments
8
8
  """
9
9
 
10
- # system modules
10
+ # System modules
11
+ from copy import deepcopy
11
12
  from json import dumps
12
13
  import os
13
14
 
14
- # third party modules
15
+ # Third party modules
15
16
  import numpy as np
16
17
 
17
- # local modules
18
+ # Local modules
18
19
  from CHAP.processor import Processor
19
20
 
20
21
 
@@ -30,29 +31,30 @@ class DiffractionVolumeLengthProcessor(Processor):
30
31
  outputdir='.',
31
32
  inputdir='.',
32
33
  interactive=False):
33
- """Return calculated value of the DV length.
34
+ """Return the calculated value of the DV length.
34
35
 
35
- :param data: input configuration for the raw scan data & DVL
36
+ :param data: Input configuration for the raw scan data & DVL
36
37
  calculation procedure.
37
38
  :type data: list[PipelineData]
38
- :param config: initialization parameters for an instance of
39
+ :param config: Initialization parameters for an instance of
39
40
  CHAP.edd.models.DiffractionVolumeLengthConfig, defaults to
40
- None
41
+ `None`.
41
42
  :type config: dict, optional
42
- :param save_figures: save .pngs of plots for checking inputs &
43
- outputs of this Processor, defaults to False
43
+ :param save_figures: Save .pngs of plots for checking inputs &
44
+ outputs of this Processor, defaults to False.
44
45
  :type save_figures: bool, optional
45
- :param outputdir: directory to which any output figures will
46
+ :param outputdir: Directory to which any output figures will
46
47
  be saved, defaults to '.'
47
48
  :type outputdir: str, optional
48
- :param inputdir: input directory, used only if files in the
49
- input configuration are not absolute paths.
50
- be saved, defaults to '.'
49
+ :param inputdir: Input directory, used only if files in the
50
+ input configuration are not absolute paths,
51
+ defaults to '.'.
51
52
  :type inputdir: str, optional
52
- :param interactive: allow for user interactions, defaults to
53
- False
53
+ :param interactive: Allows for user interactions, defaults to
54
+ False.
54
55
  :type interactive: bool, optional
55
- :return: complete DVL configuraiton dictionary
56
+ :raises RuntimeError: Unable to get a valid DVL configuration.
57
+ :return: Complete DVL configuraiton dictionary.
56
58
  :rtype: dict
57
59
  """
58
60
 
@@ -64,7 +66,9 @@ class DiffractionVolumeLengthProcessor(Processor):
64
66
  self.logger.info('No valid DVL config in input pipeline data, '
65
67
  + 'using config parameter instead.')
66
68
  try:
69
+ # Local modules
67
70
  from CHAP.edd.models import DiffractionVolumeLengthConfig
71
+
68
72
  dvl_config = DiffractionVolumeLengthConfig(
69
73
  **config, inputdir=inputdir)
70
74
  except Exception as dict_exc:
@@ -93,53 +97,60 @@ class DiffractionVolumeLengthProcessor(Processor):
93
97
  computed diffraction volume length is approximately equal to
94
98
  the standard deviation of the fitted peak.
95
99
 
96
- :param dvl_config: configuration for the DVL calculation
97
- procedure
98
- :type dvl_config: DiffractionVolumeLengthConfig
99
- :param detector: A single MCA detector element configuration
100
- :type detector: CHAP.edd.models.MCAElementDiffractionVolumeLengthConfig
101
- :param save_figures: save .pngs of plots for checking inputs &
102
- outputs of this Processor, defaults to False
100
+ :param dvl_config: Configuration for the DVL calculation
101
+ procedure.
102
+ :type dvl_config: CHAP.edd.models.DiffractionVolumeLengthConfig
103
+ :param detector: A single MCA detector element configuration.
104
+ :type detector:
105
+ CHAP.edd.models.MCAElementDiffractionVolumeLengthConfig
106
+ :param save_figures: Save .pngs of plots for checking inputs &
107
+ outputs of this Processor, defaults to False.
103
108
  :type save_figures: bool, optional
104
- :param outputdir: directory to which any output figures will
105
- be saved, defaults to '.'
109
+ :param outputdir: Directory to which any output figures will
110
+ be saved, defaults to '.'.
106
111
  :type outputdir: str, optional
107
- :param interactive: allow for user interactions, defaults to
108
- False
112
+ :param interactive: Allows for user interactions, defaults to
113
+ False.
109
114
  :type interactive: bool, optional
110
- :return: calculated diffraction volume length
115
+ :raises ValueError: No value provided for included bin ranges
116
+ for the MCA detector element.
117
+ :return: Calculated diffraction volume length.
111
118
  :rtype: float
112
119
  """
113
-
120
+ # Local modules
114
121
  from CHAP.utils.fit import Fit
115
- from CHAP.utils.general import draw_mask_1d
122
+ from CHAP.utils.general import (
123
+ index_nearest,
124
+ select_mask_1d,
125
+ )
116
126
 
117
127
  # Get raw MCA data from raster scan
118
128
  mca_data = dvl_config.mca_data(detector)
119
129
 
120
130
  # Interactively set mask, if needed & possible.
121
131
  if interactive or save_figures:
132
+ # Third party modules
133
+ import matplotlib.pyplot as plt
134
+
122
135
  self.logger.info(
123
136
  'Interactively select a mask in the matplotlib figure')
124
- mask, include_bin_ranges, figure = draw_mask_1d(
137
+ fig, mask, include_bin_ranges = select_mask_1d(
125
138
  np.sum(mca_data, axis=0),
126
- xdata = np.arange(detector.num_bins),
127
- current_index_ranges=detector.include_bin_ranges,
128
- label='sum of MCA spectra over all scan points',
129
- title='Click and drag to select ranges of MCA data to\n'
130
- + 'include when measuring the diffraction volume length.',
139
+ x = np.arange(detector.num_bins),
140
+ label='Sum of MCA spectra over all scan points',
141
+ preselected_index_ranges=detector.include_bin_ranges,
142
+ title='Click and drag to select data range to include when '
143
+ 'measuring diffraction volume length',
131
144
  xlabel='MCA channel (index)',
132
145
  ylabel='MCA intensity (counts)',
133
- test_mode=not interactive,
134
- return_figure=True
135
- )
146
+ min_num_index_ranges=1,
147
+ interactive=interactive)
136
148
  detector.include_bin_ranges = include_bin_ranges
137
149
  self.logger.debug('Mask selected. Including detector bin ranges: '
138
150
  + str(detector.include_bin_ranges))
139
151
  if save_figures:
140
- figure.savefig(os.path.join(
152
+ fig.savefig(os.path.join(
141
153
  outputdir, f'{detector.detector_name}_dvl_mask.png'))
142
- import matplotlib.pyplot as plt
143
154
  plt.close()
144
155
  if detector.include_bin_ranges is None:
145
156
  raise ValueError(
@@ -154,10 +165,7 @@ class DiffractionVolumeLengthProcessor(Processor):
154
165
  # 3) sum of intensities in detector bins after mask is applied
155
166
  unmasked_sum = np.sum(mca_data, axis=1)
156
167
  mask = detector.mca_mask()
157
- masked_mca_data = np.empty(
158
- (mca_data.shape[0], *mca_data[0][mask].shape))
159
- for i in range(mca_data.shape[0]):
160
- masked_mca_data[i] = mca_data[i][mask]
168
+ masked_mca_data = mca_data[:,mask]
161
169
  masked_max = np.amax(masked_mca_data, axis=1)
162
170
  masked_sum = np.sum(masked_mca_data, axis=1)
163
171
 
@@ -167,31 +175,38 @@ class DiffractionVolumeLengthProcessor(Processor):
167
175
  scan_center = np.sum(scanned_vals * masked_sum) / np.sum(masked_sum)
168
176
  x = scanned_vals - scan_center
169
177
 
170
- # "Normalize" the masked summed data and fit a gaussian to it
171
- y = (masked_sum - min(masked_sum)) / max(masked_sum)
172
- fit = Fit.fit_data(y, 'gaussian', x=x, normalize=False)
178
+ # Normalize the data
179
+ unmasked_sum = unmasked_sum / max(unmasked_sum)
180
+ masked_max = masked_max / max(masked_max)
181
+ masked_sum = masked_sum / max(masked_sum)
182
+
183
+ # Fit the masked summed data with a gaussian
184
+ fit = Fit.fit_data(masked_sum, ('constant', 'gaussian'), x=x)
173
185
 
174
186
  # Calculate / manually select diffraction volume length
175
187
  dvl = fit.best_values['sigma'] * detector.sigma_to_dvl_factor
176
188
  if detector.measurement_mode == 'manual':
177
189
  if interactive:
178
- mask, dvl_bounds = draw_mask_1d(
179
- y, xdata=x,
180
- label='total (masked & normalized)',
181
- ref_data=[
182
- ((x, fit.best_fit),
183
- {'label': 'gaussian fit (to total)'}),
184
- ((x, masked_max / max(masked_max)),
185
- {'label': 'maximum (masked)'}),
186
- ((x, unmasked_sum / max(unmasked_sum)),
187
- {'label': 'total (unmasked)'})
188
- ],
189
- num_index_ranges_max=1,
190
- title=('Click and drag to indicate the\n'
191
- + 'boundary of the diffraction volume'),
190
+ _, _, dvl_bounds = select_mask_1d(
191
+ masked_sum, x=x,
192
+ label='Total (masked & normalized)',
193
+ #RV TODO ref_data=[
194
+ # ((x, fit.best_fit),
195
+ # {'label': 'gaussian fit (to total)'}),
196
+ # ((x, masked_max),
197
+ # {'label': 'maximum (masked)'}),
198
+ # ((x, unmasked_sum),
199
+ # {'label': 'total (unmasked)'})
200
+ # ],
201
+ preselected_index_ranges=[
202
+ (index_nearest(x, -dvl/2), index_nearest(x, dvl/2))],
203
+ title=('Click and drag to indicate the boundary '
204
+ 'of the diffraction volume'),
192
205
  xlabel=(dvl_config.scanned_dim_lbl
193
206
  + ' (offset from scan "center")'),
194
- ylabel='MCA intensity (normalized)')
207
+ ylabel='MCA intensity (normalized)',
208
+ min_num_index_ranges=1,
209
+ max_num_index_ranges=1)
195
210
  dvl_bounds = dvl_bounds[0]
196
211
  dvl = abs(x[dvl_bounds[1]] - x[dvl_bounds[0]])
197
212
  else:
@@ -201,24 +216,29 @@ class DiffractionVolumeLengthProcessor(Processor):
201
216
  + 'Using default DVL calcluation instead.')
202
217
 
203
218
  if interactive or save_figures:
219
+ # Third party modules
204
220
  import matplotlib.pyplot as plt
221
+
205
222
  fig, ax = plt.subplots()
206
223
  ax.set_title(f'Diffraction Volume ({detector.detector_name})')
207
224
  ax.set_xlabel(dvl_config.scanned_dim_lbl \
208
225
  + ' (offset from scan "center")')
209
226
  ax.set_ylabel('MCA intensity (normalized)')
210
- ax.plot(x, y, label='total (masked & normalized)')
227
+ ax.plot(x, masked_sum, label='total (masked & normalized)')
211
228
  ax.plot(x, fit.best_fit, label='gaussian fit (to total)')
212
- ax.plot(x, masked_max / max(masked_max),
213
- label='maximum (masked)')
214
- ax.plot(x, unmasked_sum / max(unmasked_sum),
215
- label='total (unmasked)')
229
+ ax.plot(x, masked_max, label='maximum (masked)')
230
+ ax.plot(x, unmasked_sum, label='total (unmasked)')
216
231
  ax.axvspan(-dvl / 2., dvl / 2.,
217
232
  color='gray', alpha=0.5,
218
233
  label='diffraction volume'
219
234
  + f' ({detector.measurement_mode})')
220
235
  ax.legend()
221
-
236
+ ax.text(
237
+ 0, 1,
238
+ f'Diffraction volume length: {dvl:.2f}',
239
+ ha='left', va='top',
240
+ #transform=ax.get_xaxis_transform())
241
+ transform=ax.transAxes)
222
242
  if save_figures:
223
243
  figfile = os.path.join(outputdir,
224
244
  f'{detector.detector_name}_dvl.png')
@@ -245,40 +265,42 @@ class MCACeriaCalibrationProcessor(Processor):
245
265
  """Return tuned values for 2&theta and linear correction
246
266
  parameters for the MCA channel energies.
247
267
 
248
- :param data: input configuration for the raw data & tuning
249
- procedure
250
- :type data: list[dict[str,object]]
251
- :param config: initialization parameters for an instance of
268
+ :param data: Input configuration for the raw data & tuning
269
+ procedure.
270
+ :type data: list[PipelineData]
271
+ :param config: Initialization parameters for an instance of
252
272
  CHAP.edd.models.MCACeriaCalibrationConfig, defaults to
253
- None
273
+ None.
254
274
  :type config: dict, optional
255
- :param save_figures: save .pngs of plots for checking inputs &
256
- outputs of this Processor, defaults to False
275
+ :param save_figures: Save .pngs of plots for checking inputs &
276
+ outputs of this Processor, defaults to False.
257
277
  :type save_figures: bool, optional
258
- :param outputdir: directory to which any output figures will
259
- be saved, defaults to '.'
278
+ :param outputdir: Directory to which any output figures will
279
+ be saved, defaults to '.'.
260
280
  :type outputdir: str, optional
261
- :param inputdir: input directory, used only if files in the
262
- input configuration are not absolute paths.
263
- be saved, defaults to '.'
281
+ :param inputdir: Input directory, used only if files in the
282
+ input configuration are not absolute paths,
283
+ defaults to '.'.
264
284
  :type inputdir: str, optional
265
- :param interactive: allow for user interactions, defaults to
266
- False
285
+ :param interactive: Allows for user interactions, defaults to
286
+ False.
267
287
  :type interactive: bool, optional
268
- :return: original configuration dictionary with tuned values
269
- added
288
+ :raises RuntimeError: Invalid or missing input configuration.
289
+ :return: Original configuration with the tuned values for
290
+ 2&theta and the linear correction parameters added.
270
291
  :rtype: dict[str,float]
271
292
  """
272
-
273
293
  try:
274
294
  calibration_config = self.get_config(
275
295
  data, 'edd.models.MCACeriaCalibrationConfig',
276
296
  inputdir=inputdir)
277
297
  except Exception as data_exc:
278
298
  self.logger.info('No valid calibration config in input pipeline '
279
- + 'data, using config parameter instead.')
299
+ 'data, using config parameter instead.')
280
300
  try:
301
+ # Local modules
281
302
  from CHAP.edd.models import MCACeriaCalibrationConfig
303
+
282
304
  calibration_config = MCACeriaCalibrationConfig(
283
305
  **config, inputdir=inputdir)
284
306
  except Exception as dict_exc:
@@ -286,10 +308,8 @@ class MCACeriaCalibrationProcessor(Processor):
286
308
 
287
309
  for detector in calibration_config.detectors:
288
310
  tth, slope, intercept = self.calibrate(
289
- calibration_config, detector,
290
- save_figures=save_figures,
311
+ calibration_config, detector, save_figures=save_figures,
291
312
  interactive=interactive, outputdir=outputdir)
292
-
293
313
  detector.tth_calibrated = tth
294
314
  detector.slope_calibrated = slope
295
315
  detector.intercept_calibrated = intercept
@@ -305,63 +325,91 @@ class MCACeriaCalibrationProcessor(Processor):
305
325
  """Iteratively calibrate 2&theta by fitting selected peaks of
306
326
  an MCA spectrum until the computed strain is sufficiently
307
327
  small. Use the fitted peak locations to determine linear
308
- correction parameters for the MCA's channel energies.
328
+ correction parameters for the MCA channel energies.
309
329
 
310
- :param calibration_config: object configuring the CeO2
311
- calibration procedure
312
- :type calibration_config: MCACeriaCalibrationConfig
313
- :param detector: a single MCA detector element configuration
330
+ :param calibration_config: Object configuring the CeO2
331
+ calibration procedure for an MCA detector.
332
+ :type calibration_config:
333
+ CHAP.edd.models.MCACeriaCalibrationConfig
334
+ :param detector: A single MCA detector element configuration.
314
335
  :type detector: CHAP.edd.models.MCAElementCalibrationConfig
315
- :param save_figures: save .pngs of plots for checking inputs &
316
- outputs of this Processor, defaults to False
336
+ :param save_figures: Save .pngs of plots for checking inputs &
337
+ outputs of this Processor, defaults to False.
317
338
  :type save_figures: bool, optional
318
- :param outputdir: directory to which any output figures will
319
- be saved, defaults to '.'
339
+ :param outputdir: Directory to which any output figures will
340
+ be saved, defaults to '.'.
320
341
  :type outputdir: str, optional
321
- :param interactive: allow for user interactions, defaults to
322
- False
342
+ :param interactive: Allows for user interactions, defaults to
343
+ False.
323
344
  :type interactive: bool, optional
324
- :return: calibrated values of 2&theta and linear correction
325
- parameters for MCA channel energies : tth, slope,
326
- intercept
345
+ :raises ValueError: No value provided for included bin ranges
346
+ or the fitted HKLs for the MCA detector element.
347
+ :return: Calibrated values of 2&theta and the linear correction
348
+ parameters for MCA channel energies: tth, slope, intercept.
327
349
  :rtype: float, float, float
328
350
  """
329
- from CHAP.edd.utils import hc
351
+ # Local modules
352
+ from CHAP.edd.utils import get_peak_locations
330
353
  from CHAP.utils.fit import Fit
331
354
 
355
+ # Get the unique HKLs and lattice spacings for the calibration
356
+ # material
357
+ hkls, ds = calibration_config.material.unique_hkls_ds(
358
+ tth_tol=detector.hkl_tth_tol, tth_max=detector.tth_max)
359
+
332
360
  # Collect raw MCA data of interest
361
+ mca_bin_energies = np.linspace(
362
+ 0, detector.max_energy_kev, detector.num_bins)
333
363
  mca_data = calibration_config.mca_data(detector)
334
- mca_bin_energies = np.arange(0, detector.num_bins) \
335
- * (detector.max_energy_kev / detector.num_bins)
336
-
337
- if interactive:
338
- # Interactively adjust initial tth guess
339
- from CHAP.edd.utils import select_tth_initial_guess
340
- select_tth_initial_guess(detector, calibration_config.material,
341
- mca_data, mca_bin_energies)
342
- self.logger.debug(f'tth_initial_guess = {detector.tth_initial_guess}')
343
364
 
344
- # Mask out the corrected MCA data for fitting
345
- if interactive:
346
- from CHAP.utils.general import draw_mask_1d
347
- self.logger.info(
348
- 'Interactively select a mask in the matplotlib figure')
349
- mask, include_bin_ranges = draw_mask_1d(
350
- mca_data,
351
- xdata=mca_bin_energies,
352
- current_index_ranges=detector.include_bin_ranges,
353
- title='Click and drag to select ranges of Ceria'
354
- +' calibration data to include',
355
- xlabel='MCA channel energy (keV)',
356
- ylabel='MCA intensity (counts)')
365
+ if interactive or save_figures:
366
+ # Third party modules
367
+ import matplotlib.pyplot as plt
368
+
369
+ # Local modules
370
+ from CHAP.edd.utils import (
371
+ select_tth_initial_guess,
372
+ select_mask_and_hkls,
373
+ )
374
+
375
+ # Adjust initial tth guess
376
+ fig, detector.tth_initial_guess = select_tth_initial_guess(
377
+ mca_bin_energies, mca_data, hkls, ds,
378
+ detector.tth_initial_guess, interactive)
379
+ if save_figures:
380
+ fig.savefig(os.path.join(
381
+ outputdir,
382
+ f'{detector.detector_name}_calibration_'
383
+ 'tth_initial_guess.png'))
384
+ plt.close()
385
+
386
+ # Select mask & HKLs for fitting
387
+ fig, include_bin_ranges, hkl_indices = select_mask_and_hkls(
388
+ mca_bin_energies, mca_data, hkls, ds,
389
+ detector.tth_initial_guess, detector.include_bin_ranges,
390
+ detector.hkl_indices, detector.detector_name,
391
+ flux_energy_range=calibration_config.flux_file_energy_range,
392
+ interactive=interactive)
357
393
  detector.include_bin_ranges = include_bin_ranges
358
- self.logger.debug('Mask selected. Including detector bin ranges: '
359
- + str(detector.include_bin_ranges))
394
+ detector.hkl_indices = hkl_indices
395
+ if save_figures:
396
+ fig.savefig(os.path.join(
397
+ outputdir,
398
+ f'{detector.detector_name}_calibration_fit_mask_hkls.png'))
399
+ plt.close()
400
+ self.logger.debug(f'tth_initial_guess = {detector.tth_initial_guess}')
401
+ self.logger.debug(
402
+ f'include_bin_ranges = {detector.include_bin_ranges}')
360
403
  if detector.include_bin_ranges is None:
361
404
  raise ValueError(
362
405
  'No value provided for include_bin_ranges. '
363
406
  'Provide them in the MCA Ceria Calibration Configuration, '
364
407
  'or re-run the pipeline with the --interactive flag.')
408
+ if detector.hkl_indices is None:
409
+ raise ValueError(
410
+ 'No value provided for hkl_indices. Provide them in '
411
+ 'the detector\'s MCA Ceria Calibration Configuration, or'
412
+ ' re-run the pipeline with the --interactive flag.')
365
413
  mca_mask = detector.mca_mask()
366
414
  fit_mca_energies = mca_bin_energies[mca_mask]
367
415
  fit_mca_intensities = mca_data[mca_mask]
@@ -374,26 +422,10 @@ class MCACeriaCalibrationProcessor(Processor):
374
422
 
375
423
  # Get the HKLs and lattice spacings that will be used for
376
424
  # fitting
377
- tth = detector.tth_initial_guess
378
- if interactive or save_figures:
379
- import matplotlib.pyplot as plt
380
- from CHAP.edd.utils import select_hkls
381
- fig = select_hkls(detector, [calibration_config.material], tth,
382
- mca_data, mca_bin_energies, interactive)
383
- if save_figures:
384
- fig.savefig(os.path.join(
385
- outputdir,
386
- f'{detector.detector_name}_calibration_hkls.png'))
387
- plt.close()
388
- self.logger.debug(f'HKLs selected: {detector.fit_hkls}')
389
- if detector.fit_hkls is None:
390
- raise ValueError(
391
- 'No value provided for fit_hkls. Provide them in '
392
- 'the detector\'s MCA Ceria Calibration Configuration, or'
393
- ' re-run the pipeline with the --interactive flag.')
394
- fit_hkls, fit_ds = detector.fit_ds(calibration_config.material)
425
+ fit_hkls = np.asarray([hkls[i] for i in detector.hkl_indices])
426
+ fit_ds = np.asarray([ds[i] for i in detector.hkl_indices])
395
427
  c_1 = fit_hkls[:,0]**2 + fit_hkls[:,1]**2 + fit_hkls[:,2]**2
396
-
428
+ tth = detector.tth_initial_guess
397
429
  for iter_i in range(calibration_config.max_iter):
398
430
  self.logger.debug(f'Tuning tth: iteration no. {iter_i}, '
399
431
  + f'starting tth value = {tth} ')
@@ -402,19 +434,20 @@ class MCACeriaCalibrationProcessor(Processor):
402
434
 
403
435
  # Get expected peak energy locations for this iteration's
404
436
  # starting value of tth
405
- fit_lambda = 2.0*fit_ds*np.sin(0.5*np.radians(tth))
406
- fit_E0 = hc / fit_lambda
437
+ fit_E0 = get_peak_locations(fit_ds, tth)
407
438
 
408
439
  # Run the uniform fit
409
440
  uniform_fit = Fit(fit_mca_intensities, x=fit_mca_energies)
410
- uniform_fit.create_multipeak_model(fit_E0, fit_type='uniform')
441
+ uniform_fit.create_multipeak_model(
442
+ fit_E0, fit_type='uniform')
443
+ #fit_E0, fit_type='uniform', background='constant')
411
444
  uniform_fit.fit()
412
445
 
413
446
  # Extract values of interest from the best values for the
414
447
  # uniform fit parameters
415
448
  uniform_fit_centers = [
416
449
  uniform_fit.best_values[f'peak{i+1}_center']
417
- for i in range(len(detector.fit_hkls))]
450
+ for i in range(len(fit_hkls))]
418
451
  uniform_a = uniform_fit.best_values['scale_factor']
419
452
  uniform_strain = np.log(
420
453
  (uniform_a
@@ -427,16 +460,17 @@ class MCACeriaCalibrationProcessor(Processor):
427
460
  # fit
428
461
  unconstrained_fit = Fit(fit_mca_intensities, x=fit_mca_energies)
429
462
  unconstrained_fit.create_multipeak_model(
430
- uniform_fit_centers, fit_type='unconstrained')
463
+ uniform_fit_centers, fit_type='unconstrained',
464
+ )#background='constant')
431
465
  unconstrained_fit.fit()
432
466
 
433
467
  # Extract values of interest from the best values for the
434
468
  # unconstrained fit parameters
435
469
  unconstrained_fit_centers = np.array(
436
470
  [unconstrained_fit.best_values[f'peak{i+1}_center']
437
- for i in range(len(detector.fit_hkls))])
438
- unconstrained_a = 0.5*hc*np.sqrt(c_1) \
439
- / (unconstrained_fit_centers*abs(np.sin(0.5*np.radians(tth))))
471
+ for i in range(len(fit_hkls))])
472
+ unconstrained_a = np.sqrt(c_1)*abs(get_peak_locations(
473
+ unconstrained_fit_centers, tth))
440
474
  unconstrained_strains = np.log(
441
475
  (unconstrained_a
442
476
  / calibration_config.material.lattice_parameters))
@@ -455,14 +489,14 @@ class MCACeriaCalibrationProcessor(Processor):
455
489
  # Fit line to expected / computed peak locations from the last
456
490
  # unconstrained fit.
457
491
  fit = Fit.fit_data(
458
- fit_E0,
459
- 'linear',
460
- x=unconstrained_fit_centers,
461
- nan_policy='omit')
492
+ fit_E0, 'linear', x=unconstrained_fit_centers, nan_policy='omit')
462
493
  slope = fit.best_values['slope']
463
494
  intercept = fit.best_values['intercept']
464
495
 
465
496
  if interactive or save_figures:
497
+ # Third party modules
498
+ import matplotlib.pyplot as plt
499
+
466
500
  fig, axs = plt.subplots(2, 2, sharex='all', figsize=(11, 8.5))
467
501
 
468
502
  # Upper left axes: Input data & best fits
@@ -541,22 +575,30 @@ class MCADataProcessor(Processor):
541
575
  transformed according to the results of a ceria calibration.
542
576
  """
543
577
 
544
- def process(self, data):
578
+ def process(self,
579
+ data,
580
+ config=None,
581
+ save_figures=False,
582
+ outputdir='.',
583
+ inputdir='.',
584
+ interactive=False):
545
585
  """Process configurations for a map and MCA detector(s), and
546
586
  return the calibrated MCA data collected over the map.
547
587
 
548
- :param data: input map configuration and results of ceria
549
- calibration
588
+ :param data: Input map configuration and results of ceria
589
+ calibration.
550
590
  :type data: list[dict[str,object]]
551
- :return: calibrated and flux-corrected MCA data
591
+ :return: Calibrated and flux-corrected MCA data.
552
592
  :rtype: nexusformat.nexus.NXentry
553
593
  """
554
594
 
595
+ print(f'data:\n{data}')
596
+ exit('Done Here')
555
597
  map_config = self.get_config(
556
- data, 'common.models.map.MapConfig')
557
- calibration_config = self.get_config(
558
- data, 'edd.models.MCACeriaCalibrationConfig')
559
- nxroot = self.get_nxroot(map_config, calibration_config)
598
+ data, 'common.models.map.MapConfig', inputdir=inputdir)
599
+ ceria_calibration_config = self.get_config(
600
+ data, 'edd.models.MCACeriaCalibrationConfig', inputdir=inputdir)
601
+ nxroot = self.get_nxroot(map_config, ceria_calibration_config)
560
602
 
561
603
  return nxroot
562
604
 
@@ -567,20 +609,23 @@ class MCADataProcessor(Processor):
567
609
  `calibration_config`. The data will be returned along with
568
610
  relevant metadata in the form of a NeXus structure.
569
611
 
570
- :param map_config: the map configuration
571
- :type map_config: MapConfig
572
- :param calibration_config: the calibration configuration
573
- :type calibration_config: MCACeriaCalibrationConfig
574
- :return: a map of the calibrated and flux-corrected MCA data
612
+ :param map_config: The map configuration.
613
+ :type map_config: CHAP.common.models.MapConfig.
614
+ :param calibration_config: The calibration configuration.
615
+ :type calibration_config:
616
+ CHAP.edd.models.MCACeriaCalibrationConfig
617
+ :return: A map of the calibrated and flux-corrected MCA data.
575
618
  :rtype: nexusformat.nexus.NXroot
576
619
  """
577
- # third party modules
578
- from nexusformat.nexus import (NXdata,
579
- NXdetector,
580
- NXinstrument,
581
- NXroot)
582
-
583
- # local modules
620
+ # Third party modules
621
+ from nexusformat.nexus import (
622
+ NXdata,
623
+ NXdetector,
624
+ NXinstrument,
625
+ NXroot,
626
+ )
627
+
628
+ # Local modules
584
629
  from CHAP.common import MapProcessor
585
630
 
586
631
  nxroot = NXroot()
@@ -600,8 +645,7 @@ class MCADataProcessor(Processor):
600
645
  detector.num_bins))
601
646
  nxdata.raw.attrs['units'] = 'counts'
602
647
  nxdata.channel_energy = detector.slope_calibrated \
603
- * np.arange(0, detector.num_bins) \
604
- * (detector.max_energy_kev / detector.num_bins) \
648
+ * np.linspace(0, detector.max_energy_kev, detector.num_bins) \
605
649
  + detector.intercept_calibrated
606
650
  nxdata.channel_energy.attrs['units'] = 'keV'
607
651
 
@@ -642,27 +686,29 @@ class StrainAnalysisProcessor(Processor):
642
686
  interactive=False):
643
687
  """Return strain analysis maps & associated metadata in an NXprocess.
644
688
 
645
- :param data: input data containing configurations for a map,
689
+ :param data: Input data containing configurations for a map,
646
690
  completed ceria calibration, and parameters for strain
647
691
  analysis
648
692
  :type data: list[PipelineData]
649
- :param config: initialization parameters for an instance of
693
+ :param config: Initialization parameters for an instance of
650
694
  CHAP.edd.models.StrainAnalysisConfig, defaults to
651
- None
695
+ None.
652
696
  :type config: dict, optional
653
- :param save_figures: save .pngs of plots for checking inputs &
654
- outputs of this Processor, defaults to False
697
+ :param save_figures: Save .pngs of plots for checking inputs &
698
+ outputs of this Processor, defaults to False.
655
699
  :type save_figures: bool, optional
656
- :param outputdir: directory to which any output figures will
657
- be saved, defaults to '.'
700
+ :param outputdir: Directory to which any output figures will
701
+ be saved, defaults to '.'.
658
702
  :type outputdir: str, optional
659
- :param inputdir: input directory, used only if files in the
660
- input configuration are not absolute paths.
661
- be saved, defaults to '.'
703
+ :param inputdir: Input directory, used only if files in the
704
+ input configuration are not absolute paths,
705
+ defaults to '.'.
662
706
  :type inputdir: str, optional
663
- :param interactive: allow for user interactions, defaults to
664
- False
707
+ :param interactive: Allows for user interactions, defaults to
708
+ False.
665
709
  :type interactive: bool, optional
710
+ :raises RuntimeError: Unable to get a valid strain analysis
711
+ configuration.
666
712
  :return: NXprocess containing metadata about strain analysis
667
713
  processing parameters and empty datasets for strain maps
668
714
  to be filled in later.
@@ -670,17 +716,17 @@ class StrainAnalysisProcessor(Processor):
670
716
 
671
717
  """
672
718
  # Get required configuration models from input data
673
- # map_config = self.get_config(
674
- # data, 'common.models.map.MapConfig')
675
719
  ceria_calibration_config = self.get_config(
676
720
  data, 'edd.models.MCACeriaCalibrationConfig', inputdir=inputdir)
677
721
  try:
678
722
  strain_analysis_config = self.get_config(
679
- data, 'edd.models.StrainAnalysisConfig')
723
+ data, 'edd.models.StrainAnalysisConfig', inputdir=inputdir)
680
724
  except Exception as data_exc:
725
+ # Local modules
726
+ from CHAP.edd.models import StrainAnalysisConfig
727
+
681
728
  self.logger.info('No valid strain analysis config in input '
682
729
  + 'pipeline data, using config parameter instead')
683
- from CHAP.edd.models import StrainAnalysisConfig
684
730
  try:
685
731
  strain_analysis_config = StrainAnalysisConfig(
686
732
  **config, inputdir=inputdir)
@@ -688,7 +734,6 @@ class StrainAnalysisProcessor(Processor):
688
734
  raise RuntimeError from dict_exc
689
735
 
690
736
  nxroot = self.get_nxroot(
691
- #map_config,
692
737
  strain_analysis_config.map_config,
693
738
  ceria_calibration_config,
694
739
  strain_analysis_config,
@@ -696,8 +741,8 @@ class StrainAnalysisProcessor(Processor):
696
741
  outputdir=outputdir,
697
742
  interactive=interactive)
698
743
  self.logger.debug(nxroot.tree)
699
- return nxroot
700
744
 
745
+ return nxroot
701
746
 
702
747
  def get_nxroot(self,
703
748
  map_config,
@@ -709,38 +754,70 @@ class StrainAnalysisProcessor(Processor):
709
754
  """Return NXroot containing strain maps.
710
755
 
711
756
 
712
- :param map_config: Input map configuration
757
+ :param map_config: The map configuration.
713
758
  :type map_config: CHAP.common.models.map.MapConfig
714
- :param ceria_calibration_config: Results of ceria calibration
759
+ :param ceria_calibration_config: The calibration configuration.
715
760
  :type ceria_calibration_config:
716
761
  'CHAP.edd.models.MCACeriaCalibrationConfig'
717
762
  :param strain_analysis_config: Strain analysis processing
718
- configuration
719
- :type strain_analysis_config: CHAP.edd.models.StrainAnalysisConfig
720
- :param save_figures: save .pngs of plots for checking inputs &
721
- outputs of this Processor, defaults to False
763
+ configuration.
764
+ :type strain_analysis_config:
765
+ CHAP.edd.models.StrainAnalysisConfig
766
+ :param save_figures: Save .pngs of plots for checking inputs &
767
+ outputs of this Processor, defaults to False.
722
768
  :type save_figures: bool, optional
723
- :param outputdir: directory to which any output figures will
724
- be saved, defaults to '.'
769
+ :param outputdir: Directory to which any output figures will
770
+ be saved, defaults to '.'.
725
771
  :type outputdir: str, optional
726
- :param interactive: allow for user interactions, defaults to
727
- False
772
+ :param interactive: Allows for user interactions, defaults to
773
+ False.
728
774
  :type interactive: bool, optional
729
- :return: NXroot containing strain maps
775
+ :return: NXroot containing strain maps.
730
776
  :rtype: nexusformat.nexus.NXroot
731
777
  """
732
- from nexusformat.nexus import (NXcollection,
733
- NXdata,
734
- NXdetector,
735
- NXfield,
736
- NXparameters,
737
- NXprocess,
738
- NXroot)
739
- import numpy as np
778
+ # Third party modules
779
+ from nexusformat.nexus import (
780
+ NXcollection,
781
+ NXdata,
782
+ NXdetector,
783
+ NXfield,
784
+ NXparameters,
785
+ NXprocess,
786
+ NXroot,
787
+ )
788
+
789
+ # Local modules
740
790
  from CHAP.common import MapProcessor
741
- from CHAP.edd.utils import hc
791
+ from CHAP.edd.utils import (
792
+ get_peak_locations,
793
+ get_unique_hkls_ds,
794
+ )
742
795
  from CHAP.utils.fit import FitMap
743
796
 
797
+ def linkdims(nxgroup, field_dims=[]):
798
+ if isinstance(field_dims, dict):
799
+ field_dims = [field_dims]
800
+ if map_config.map_type == 'structured':
801
+ axes = deepcopy(map_config.dims)
802
+ for dims in field_dims:
803
+ axes.append(dims['axes'])
804
+ nxgroup.attrs['axes'] = axes
805
+ else:
806
+ axes = ['map_index']
807
+ for dims in field_dims:
808
+ axes.append(dims['axes'])
809
+ nxgroup.attrs['axes'] = axes
810
+ nxgroup.attrs[f'map_index_indices'] = 0
811
+ for dim in map_config.dims:
812
+ nxgroup.makelink(nxentry.data[dim])
813
+ if f'{dim}_indices' in nxentry.data.attrs:
814
+ nxgroup.attrs[f'{dim}_indices'] = \
815
+ nxentry.data.attrs[f'{dim}_indices']
816
+ for dims in field_dims:
817
+ nxgroup.attrs[f'{dims["axes"]}_indices'] = dims['index']
818
+
819
+ if len(strain_analysis_config.detectors) != 1:
820
+ raise RuntimeError('Multiple detectors not tested')
744
821
  for detector in strain_analysis_config.detectors:
745
822
  calibration = [
746
823
  d for d in ceria_calibration_config.detectors \
@@ -758,73 +835,86 @@ class StrainAnalysisProcessor(Processor):
758
835
  nxprocess.data = NXdata()
759
836
  nxprocess.default = 'data'
760
837
  nxdata = nxprocess.data
761
- nxdata.attrs['axes'] = map_config.dims
762
- def linkdims(nxgroup):
763
- for dim in map_config.dims:
764
- nxgroup.makelink(nxentry.data[dim])
765
- nxgroup.attrs[f'{dim}_indices'] = \
766
- nxentry.data.attrs[f'{dim}_indices']
767
838
  linkdims(nxdata)
768
839
 
840
+ # Collect raw MCA data of interest
841
+ mca_bin_energies = []
842
+ for i, detector in enumerate(strain_analysis_config.detectors):
843
+ mca_bin_energies.append(
844
+ detector.slope_calibrated
845
+ * np.linspace(0, detector.max_energy_kev, detector.num_bins)
846
+ + detector.intercept_calibrated)
847
+ mca_data = strain_analysis_config.mca_data()
848
+
769
849
  # Select interactive params / save figures
770
- if save_figures or interactive:
850
+ if interactive or save_figures:
851
+ # Third party modules
771
852
  import matplotlib.pyplot as plt
772
- from CHAP.edd.utils import select_hkls
773
- from CHAP.utils.general import draw_mask_1d
774
- for detector in strain_analysis_config.detectors:
775
- x = np.linspace(detector.intercept_calibrated,
776
- detector.max_energy_kev \
777
- * detector.slope_calibrated,
778
- detector.num_bins)
779
- y = strain_analysis_config.mca_data(
780
- detector,
781
- (0,) * len(strain_analysis_config.map_config.shape))
782
- fig = select_hkls(detector,
783
- strain_analysis_config.materials,
784
- detector.tth_calibrated,
785
- y, x, interactive)
786
- if save_figures:
787
- fig.savefig(os.path.join(
788
- outputdir,
789
- f'{detector.detector_name}_strainanalysis_hkls.png'))
790
- plt.close()
791
853
 
792
- if interactive:
793
- self.logger.info(
794
- 'Interactively select a mask in the matplotlib figure')
795
- mask, include_bin_ranges, figure = draw_mask_1d(
796
- y, xdata=x,
797
- current_index_ranges=detector.include_bin_ranges,
798
- label='reference spectrum',
799
- title='Click and drag to select ranges of MCA data to\n'
800
- + 'include when analyzing strain.',
801
- xlabel='MCA channel (index)',
802
- ylabel='MCA intensity (counts)',
803
- test_mode=not interactive,
804
- return_figure=True
805
- )
854
+ # Local modules
855
+ from CHAP.edd.utils import (
856
+ select_material_params,
857
+ select_mask_and_hkls,
858
+ )
859
+
860
+ # Mask during calibration
861
+ if len(ceria_calibration_config.detectors) != 1:
862
+ raise RuntimeError('Multiple detectors not implemented')
863
+ for detector in ceria_calibration_config.detectors:
864
+ # calibration_mask = detector.mca_mask()
865
+ calibration_bin_ranges = detector.include_bin_ranges
866
+
867
+
868
+ tth = strain_analysis_config.detectors[0].tth_calibrated
869
+ fig, strain_analysis_config.materials = select_material_params(
870
+ mca_bin_energies[0], mca_data[0][0], tth,
871
+ materials=strain_analysis_config.materials,
872
+ interactive=interactive)
873
+ self.logger.debug(
874
+ f'materials: {strain_analysis_config.materials}')
875
+ if save_figures:
876
+ fig.savefig(os.path.join(
877
+ outputdir,
878
+ f'{detector.detector_name}_strainanalysis_'
879
+ 'material_config.png'))
880
+ plt.close()
881
+
882
+ # ASK: can we assume same hkl_tth_tol and tth_max for
883
+ # every detector in this part?
884
+ hkls, ds = get_unique_hkls_ds(
885
+ strain_analysis_config.materials,
886
+ tth_tol=strain_analysis_config.detectors[0].hkl_tth_tol,
887
+ tth_max=strain_analysis_config.detectors[0].tth_max)
888
+ for i, detector in enumerate(strain_analysis_config.detectors):
889
+ fig, include_bin_ranges, hkl_indices = \
890
+ select_mask_and_hkls(
891
+ mca_bin_energies[i], mca_data[i][0], hkls, ds,
892
+ detector.tth_calibrated,
893
+ detector.include_bin_ranges, detector.hkl_indices,
894
+ detector.detector_name, mca_data[i],
895
+ # calibration_mask=calibration_mask,
896
+ calibration_bin_ranges=calibration_bin_ranges,
897
+ interactive=interactive)
806
898
  detector.include_bin_ranges = include_bin_ranges
899
+ detector.hkl_indices = hkl_indices
807
900
  if save_figures:
808
- figure.savefig(os.path.join(
901
+ fig.savefig(os.path.join(
809
902
  outputdir,
810
- f'{detector.detector_name}_strainanalysis_mask.png'))
903
+ f'{detector.detector_name}_strainanalysis_'
904
+ 'fit_mask_hkls.png'))
811
905
  plt.close()
812
-
813
- if interactive:
814
- from CHAP.edd.utils import select_material_params
815
- x = np.linspace(
816
- strain_analysis_config.detectors[0].intercept_calibrated,
817
- detector.max_energy_kev \
818
- * detector.slope_calibrated,
819
- detector.num_bins)
820
- y = strain_analysis_config.mca_data(
821
- strain_analysis_config.detectors[0],
822
- (0,) * len(strain_analysis_config.map_config.shape))
823
- tth = strain_analysis_config.detectors[0].tth_calibrated
824
- strain_analysis_config.materials = select_material_params(
825
- x, y, tth, materials=strain_analysis_config.materials)
826
-
827
- for detector in strain_analysis_config.detectors:
906
+ else:
907
+ # ASK: can we assume same hkl_tth_tol and tth_max for
908
+ # every detector in this part?
909
+ # Get the unique HKLs and lattice spacings for the strain
910
+ # analysis materials (assume hkl_tth_tol and tth_max are the
911
+ # same for each detector)
912
+ hkls, ds = get_unique_hkls_ds(
913
+ strain_analysis_config.materials,
914
+ tth_tol=strain_analysis_config.detectors[0].hkl_tth_tol,
915
+ tth_max=strain_analysis_config.detectors[0].tth_max)
916
+
917
+ for i, detector in enumerate(strain_analysis_config.detectors):
828
918
  # Setup NXdata group
829
919
  self.logger.debug(
830
920
  f'Setting up NXdata group for {detector.detector_name}')
@@ -834,21 +924,21 @@ class StrainAnalysisProcessor(Processor):
834
924
  nxdetector.detector_config = dumps(detector.dict())
835
925
  nxdetector.data = NXdata()
836
926
  det_nxdata = nxdetector.data
837
- det_nxdata.attrs['axes'] = map_config.dims + ['energy']
838
- linkdims(det_nxdata)
839
- all_energies = np.arange(0, detector.num_bins) \
840
- * (detector.max_energy_kev / detector.num_bins) \
841
- * detector.slope_calibrated \
842
- + detector.intercept_calibrated
927
+ linkdims(
928
+ det_nxdata, {'axes': 'energy', 'index': len(map_config.shape)})
843
929
  mask = detector.mca_mask()
844
- energies = all_energies[mask]
930
+ energies = mca_bin_energies[i][mask]
845
931
  det_nxdata.energy = NXfield(value=energies,
846
932
  attrs={'units': 'keV'})
847
- det_nxdata.attrs['energy_indices'] = len(map_config.dims)
848
933
  det_nxdata.intensity = NXfield(
849
934
  dtype='uint16',
850
935
  shape=(*map_config.shape, len(energies)),
851
936
  attrs={'units': 'counts'})
937
+ det_nxdata.tth = NXfield(
938
+ dtype='float64',
939
+ shape=map_config.shape,
940
+ attrs={'units':'degrees', 'long_name': '2\u03B8 (degrees)'}
941
+ )
852
942
  det_nxdata.microstrain = NXfield(
853
943
  dtype='float64',
854
944
  shape=map_config.shape,
@@ -857,32 +947,27 @@ class StrainAnalysisProcessor(Processor):
857
947
  # Gather detector data
858
948
  self.logger.debug(
859
949
  f'Gathering detector data for {detector.detector_name}')
860
- for map_index in np.ndindex(map_config.shape):
861
- try:
862
- scans, scan_number, scan_step_index = \
863
- map_config.get_scan_step_index(map_index)
864
- except:
865
- continue
866
- scanparser = scans.get_scanparser(scan_number)
867
- intensity = scanparser.get_detector_data(
868
- detector.detector_name, scan_step_index)\
869
- .astype('uint16')[mask]
870
- det_nxdata.intensity[map_index] = intensity
950
+ for j, map_index in enumerate(np.ndindex(map_config.shape)):
951
+ det_nxdata.intensity[map_index] = \
952
+ mca_data[i][j].astype('uint16')[mask]
871
953
  det_nxdata.summed_intensity = det_nxdata.intensity.sum(axis=-1)
872
954
 
873
955
  # Perform strain analysis
874
956
  self.logger.debug(
875
957
  f'Beginning strain analysis for {detector.detector_name}')
876
- fit_hkls, fit_ds = detector.fit_ds(
877
- strain_analysis_config.materials)
878
- peak_locations = hc / (
879
- 2. * fit_ds * np.sin(0.5*np.radians(detector.tth_calibrated)))
958
+
959
+ # Get the HKLs and lattice spacings that will be used for
960
+ # fitting
961
+ fit_hkls = np.asarray([hkls[i] for i in detector.hkl_indices])
962
+ fit_ds = np.asarray([ds[i] for i in detector.hkl_indices])
963
+ peak_locations = get_peak_locations(
964
+ fit_ds, detector.tth_calibrated)
965
+ num_peak = len(peak_locations)
880
966
  # KLS: Use the below def of peak_locations when
881
967
  # FitMap.create_multipeak_model can accept a list of maps
882
968
  # for centers.
883
969
  # tth = np.radians(detector.map_tth(map_config))
884
- # peak_locations = [hc / (2. * d0 * np.sin(0.5*tth)) \
885
- # for d0 in fit_ds]
970
+ # peak_locations = [get_peak_locations(d0, tth) for d0 in fit_ds]
886
971
 
887
972
  # Perform initial fit: assume uniform strain for all HKLs
888
973
  self.logger.debug('Performing uniform fit')
@@ -891,118 +976,293 @@ class StrainAnalysisProcessor(Processor):
891
976
  peak_locations,
892
977
  fit_type='uniform',
893
978
  peak_models=detector.peak_models,
894
- background=detector.background)
979
+ background=detector.background,
980
+ fwhm_min=detector.fwhm_min,
981
+ fwhm_max=detector.fwhm_max)
895
982
  fit.fit()
896
983
  uniform_fit_centers = [
897
984
  fit.best_values[
898
985
  fit.best_parameters().index(f'peak{i+1}_center')]
899
- for i in range(len(peak_locations))]
900
- uniform_fit_errors = [
986
+ for i in range(num_peak)]
987
+ uniform_fit_centers_errors = [
901
988
  fit.best_errors[
902
989
  fit.best_parameters().index(f'peak{i+1}_center')]
903
- for i in range(len(peak_locations))]
990
+ for i in range(num_peak)]
991
+ uniform_fit_amplitudes = [
992
+ fit.best_values[
993
+ fit.best_parameters().index(f'peak{i+1}_amplitude')]
994
+ for i in range(num_peak)]
995
+ uniform_fit_amplitudes_errors = [
996
+ fit.best_errors[
997
+ fit.best_parameters().index(f'peak{i+1}_amplitude')]
998
+ for i in range(num_peak)]
999
+ uniform_fit_sigmas = [
1000
+ fit.best_values[
1001
+ fit.best_parameters().index(f'peak{i+1}_sigma')]
1002
+ for i in range(num_peak)]
1003
+ uniform_fit_sigmas_errors = [
1004
+ fit.best_errors[
1005
+ fit.best_parameters().index(f'peak{i+1}_sigma')]
1006
+ for i in range(num_peak)]
904
1007
 
905
1008
  # Add uniform fit results to the NeXus structure
906
1009
  nxdetector.uniform_fit = NXcollection()
907
1010
  fit_nxgroup = nxdetector.uniform_fit
1011
+
908
1012
  # Full map of results
909
1013
  fit_nxgroup.results = NXdata()
910
1014
  fit_nxdata = fit_nxgroup.results
911
- fit_nxdata.attrs['axes'] = map_config.dims + ['energy']
912
- linkdims(fit_nxdata)
1015
+ linkdims(
1016
+ fit_nxdata, {'axes': 'energy', 'index': len(map_config.shape)})
913
1017
  fit_nxdata.makelink(det_nxdata.energy)
914
- fit_nxdata.attrs['energy_indices'] = len(map_config.dims)
915
- for d in fit.best_results:
916
- if d.endswith('_fit'):
917
- fit_nxdata.fits = fit.best_results[d]
1018
+ fit_nxdata.best_fit= fit.best_fit
918
1019
  fit_nxdata.residuals = fit.residual
1020
+ fit_nxdata.redchi = fit.redchi
1021
+ fit_nxdata.success = fit.success
919
1022
 
920
1023
  # Peak-by-peak results
921
- fit_nxgroup.fit_hkl_centers = NXdata()
922
- fit_nxdata = fit_nxgroup.fit_hkl_centers
923
- fit_nxdata.attrs['axes'] = map_config.dims
924
- linkdims(fit_nxdata)
925
- for hkl, center_guessed, centers_fit, centers_errors in \
926
- zip(fit_hkls, peak_locations,
927
- uniform_fit_centers, uniform_fit_errors):
1024
+ # fit_nxgroup.fit_hkl_centers = NXdata()
1025
+ # fit_nxdata = fit_nxgroup.fit_hkl_centers
1026
+ # linkdims(fit_nxdata)
1027
+ for (hkl, center_guess, centers_fit, centers_error,
1028
+ amplitudes_fit, amplitudes_error, sigmas_fit,
1029
+ sigmas_error) in zip(
1030
+ fit_hkls, peak_locations,
1031
+ uniform_fit_centers, uniform_fit_centers_errors,
1032
+ uniform_fit_amplitudes, uniform_fit_amplitudes_errors,
1033
+ uniform_fit_sigmas, uniform_fit_sigmas_errors):
928
1034
  hkl_name = '_'.join(str(hkl)[1:-1].split(' '))
929
1035
  fit_nxgroup[hkl_name] = NXparameters()
930
- fit_nxgroup[hkl_name].initial_guess = center_guessed
931
- fit_nxgroup[hkl_name].initial_guess.attrs['units'] = 'keV'
1036
+ # Report initial HKL peak centers
1037
+ fit_nxgroup[hkl_name].center_initial_guess = center_guess
1038
+ fit_nxgroup[hkl_name].center_initial_guess.attrs['units'] = \
1039
+ 'keV'
1040
+ # Report HKL peak centers
932
1041
  fit_nxgroup[hkl_name].centers = NXdata()
933
- fit_nxgroup[hkl_name].centers.attrs['axes'] = map_config.dims
934
1042
  linkdims(fit_nxgroup[hkl_name].centers)
935
1043
  fit_nxgroup[hkl_name].centers.values = NXfield(
936
1044
  value=centers_fit, attrs={'units': 'keV'})
937
1045
  fit_nxgroup[hkl_name].centers.errors = NXfield(
938
- value=centers_errors)
939
- fit_nxdata.makelink(fit_nxgroup[f'{hkl_name}/centers/values'],
940
- name=hkl_name)
1046
+ value=centers_error)
1047
+ fit_nxgroup[hkl_name].centers.attrs['signal'] = 'values'
1048
+ # fit_nxdata.makelink(
1049
+ # fit_nxgroup[f'{hkl_name}/centers/values'], name=hkl_name)
1050
+ # Report HKL peak amplitudes
1051
+ fit_nxgroup[hkl_name].amplitudes = NXdata()
1052
+ linkdims(fit_nxgroup[hkl_name].amplitudes)
1053
+ fit_nxgroup[hkl_name].amplitudes.values = NXfield(
1054
+ value=amplitudes_fit, attrs={'units': 'counts'})
1055
+ fit_nxgroup[hkl_name].amplitudes.errors = NXfield(
1056
+ value=amplitudes_error)
1057
+ fit_nxgroup[hkl_name].amplitudes.attrs['signal'] = 'values'
1058
+ # Report HKL peak FWHM
1059
+ fit_nxgroup[hkl_name].sigmas = NXdata()
1060
+ linkdims(fit_nxgroup[hkl_name].sigmas)
1061
+ fit_nxgroup[hkl_name].sigmas.values = NXfield(
1062
+ value=sigmas_fit, attrs={'units': 'keV'})
1063
+ fit_nxgroup[hkl_name].sigmas.errors = NXfield(
1064
+ value=sigmas_error)
1065
+ fit_nxgroup[hkl_name].sigmas.attrs['signal'] = 'values'
941
1066
 
942
1067
  # Perform second fit: do not assume uniform strain for all
943
1068
  # HKLs, and use the fit peak centers from the uniform fit
944
1069
  # as inital guesses
945
1070
  self.logger.debug('Performing unconstrained fit')
946
1071
  fit.create_multipeak_model(fit_type='unconstrained')
947
- fit.fit()
1072
+ fit.fit(rel_amplitude_cutoff=detector.rel_amplitude_cutoff)
948
1073
  unconstrained_fit_centers = np.array(
949
1074
  [fit.best_values[
950
1075
  fit.best_parameters()\
951
1076
  .index(f'peak{i+1}_center')]
952
- for i in range(len(peak_locations))])
953
- unconstrained_fit_errors = np.array(
1077
+ for i in range(num_peak)])
1078
+ unconstrained_fit_centers_errors = np.array(
954
1079
  [fit.best_errors[
955
1080
  fit.best_parameters()\
956
1081
  .index(f'peak{i+1}_center')]
957
- for i in range(len(peak_locations))])
958
- unconstrained_strains = np.empty_like(unconstrained_fit_centers)
959
- for i, peak_loc in enumerate(peak_locations):
960
- unconstrained_strains[i] = np.log(
961
- peak_loc / unconstrained_fit_centers[i])
1082
+ for i in range(num_peak)])
1083
+ unconstrained_fit_amplitudes = [
1084
+ fit.best_values[
1085
+ fit.best_parameters().index(f'peak{i+1}_amplitude')]
1086
+ for i in range(num_peak)]
1087
+ unconstrained_fit_amplitudes_errors = [
1088
+ fit.best_errors[
1089
+ fit.best_parameters().index(f'peak{i+1}_amplitude')]
1090
+ for i in range(num_peak)]
1091
+ unconstrained_fit_sigmas = [
1092
+ fit.best_values[
1093
+ fit.best_parameters().index(f'peak{i+1}_sigma')]
1094
+ for i in range(num_peak)]
1095
+ unconstrained_fit_sigmas_errors = [
1096
+ fit.best_errors[
1097
+ fit.best_parameters().index(f'peak{i+1}_sigma')]
1098
+ for i in range(num_peak)]
1099
+
1100
+ if interactive or save_figures:
1101
+ # Third party modules
1102
+ import matplotlib.animation as animation
1103
+
1104
+ if save_figures:
1105
+ path = os.path.join(
1106
+ outputdir, f'{detector.detector_name}_strainanalysis_'
1107
+ 'unconstrained_fits')
1108
+ if not os.path.isdir(path):
1109
+ os.mkdir(path)
1110
+
1111
+ def animate(i):
1112
+ map_index = np.unravel_index(i, map_config.shape)
1113
+ intensity.set_ydata(
1114
+ det_nxdata.intensity.nxdata[map_index]
1115
+ / det_nxdata.intensity.nxdata[map_index].max())
1116
+ best_fit.set_ydata(fit.best_fit[map_index]
1117
+ / fit.best_fit[map_index].max())
1118
+ # residual.set_ydata(fit.residual[map_index])
1119
+ index.set_text('\n'.join(f'{k}[{i}] = {v}'
1120
+ for k, v in map_config.get_coords(map_index).items()))
1121
+ if save_figures:
1122
+ plt.savefig(os.path.join(path, f'frame_{i}.png'))
1123
+ #return intensity, best_fit, residual, index
1124
+ return intensity, best_fit, index
1125
+
1126
+ fig, ax = plt.subplots()
1127
+ map_index = np.unravel_index(0, map_config.shape)
1128
+ data_normalized = (
1129
+ det_nxdata.intensity.nxdata[map_index]
1130
+ / det_nxdata.intensity.nxdata[map_index].max())
1131
+ intensity, = ax.plot(
1132
+ energies, data_normalized, 'b.', label='data')
1133
+ fit_normalized = (fit.best_fit[map_index]
1134
+ / fit.best_fit[map_index].max())
1135
+ best_fit, = ax.plot(
1136
+ energies, fit_normalized, 'k-', label='fit')
1137
+ # residual, = ax.plot(
1138
+ # energies, fit.residual[map_index], 'r-',
1139
+ # label='residual')
1140
+ ax.set(
1141
+ title='Unconstrained fits',
1142
+ xlabel='Energy (keV)',
1143
+ ylabel='Normalized Intensity (-)')
1144
+ ax.legend(loc='upper right')
1145
+ index = ax.text(
1146
+ 0.05, 0.95, '', transform=ax.transAxes, va='top')
1147
+
1148
+ num_frames = int(det_nxdata.intensity.nxdata.size
1149
+ / det_nxdata.intensity.nxdata.shape[-1])
1150
+ if not save_figures:
1151
+ ani = animation.FuncAnimation(
1152
+ fig, animate,
1153
+ frames=int(det_nxdata.intensity.nxdata.size
1154
+ / det_nxdata.intensity.nxdata.shape[-1]),
1155
+ interval=1000, blit=True, repeat=False)
1156
+ else:
1157
+ for i in range(num_frames):
1158
+ animate(i)
1159
+
1160
+ plt.close()
1161
+ plt.subplots_adjust(top=1, bottom=0, left=0, right=1)
1162
+
1163
+ frames = []
1164
+ for i in range(num_frames):
1165
+ frame = plt.imread(
1166
+ os.path.join(path, f'frame_{i}.png'))
1167
+ im = plt.imshow(frame, animated=True)
1168
+ if not i:
1169
+ plt.imshow(frame)
1170
+ frames.append([im])
1171
+
1172
+ ani = animation.ArtistAnimation(
1173
+ plt.gcf(), frames, interval=1000, blit=True,
1174
+ repeat=False)
1175
+
1176
+ if interactive:
1177
+ plt.show()
1178
+
1179
+ if save_figures:
1180
+ path = os.path.join(
1181
+ outputdir,
1182
+ f'{detector.detector_name}_strainanalysis_'
1183
+ 'unconstrained_fits.mp4')
1184
+ ani.save(path)
1185
+ plt.close()
1186
+
1187
+ tth_map = detector.get_tth_map(map_config)
1188
+ det_nxdata.tth.nxdata = tth_map
1189
+ nominal_centers = np.asarray(
1190
+ [get_peak_locations(d0, tth_map) for d0 in fit_ds])
1191
+ unconstrained_strains = np.log(
1192
+ nominal_centers / unconstrained_fit_centers)
962
1193
  unconstrained_strain = np.mean(unconstrained_strains, axis=0)
963
1194
  det_nxdata.microstrain.nxdata = unconstrained_strain * 1e6
964
1195
 
965
1196
  # Add unconstrained fit results to the NeXus structure
966
1197
  nxdetector.unconstrained_fit = NXcollection()
967
1198
  fit_nxgroup = nxdetector.unconstrained_fit
1199
+
968
1200
  # Full map of results
969
- fit_nxgroup.data = NXdata()
970
- fit_nxdata = fit_nxgroup.data
971
- fit_nxdata.attrs['axes'] = map_config.dims + ['energy']
972
- linkdims(fit_nxdata)
1201
+ fit_nxgroup.results = NXdata()
1202
+ fit_nxdata = fit_nxgroup.results
1203
+ linkdims(
1204
+ fit_nxdata, {'axes': 'energy', 'index': len(map_config.shape)})
973
1205
  fit_nxdata.makelink(det_nxdata.energy)
974
- fit_nxdata.attrs['energy_indices'] = len(map_config.dims)
975
- for d in fit.best_results:
976
- if d.endswith('_fit'):
977
- fit_nxdata.fits = fit.best_results[d]
1206
+ fit_nxdata.best_fit= fit.best_fit
978
1207
  fit_nxdata.residuals = fit.residual
1208
+ fit_nxdata.redchi = fit.redchi
1209
+ fit_nxdata.success = fit.success
1210
+
979
1211
  # Peak-by-peak results
980
1212
  fit_nxgroup.fit_hkl_centers = NXdata()
981
1213
  fit_nxdata = fit_nxgroup.fit_hkl_centers
982
- fit_nxdata.attrs['axes'] = map_config.dims
983
1214
  linkdims(fit_nxdata)
984
- for (hkl, unconstrained_center_guesses,
985
- centers_fit, centers_errors) in \
986
- zip(fit_hkls, peak_locations,
987
- unconstrained_fit_centers, unconstrained_fit_errors):
1215
+ for (hkl, center_guesses, centers_fit, centers_error,
1216
+ amplitudes_fit, amplitudes_error, sigmas_fit,
1217
+ sigmas_error) in zip(
1218
+ fit_hkls, uniform_fit_centers,
1219
+ unconstrained_fit_centers,
1220
+ unconstrained_fit_centers_errors,
1221
+ unconstrained_fit_amplitudes,
1222
+ unconstrained_fit_amplitudes_errors,
1223
+ unconstrained_fit_sigmas, unconstrained_fit_sigmas_errors):
988
1224
  hkl_name = '_'.join(str(hkl)[1:-1].split(' '))
989
1225
  fit_nxgroup[hkl_name] = NXparameters()
990
- fit_nxgroup[hkl_name].initial_guess = center_guessed
991
- fit_nxgroup[hkl_name].initial_guess.attrs['units'] = 'keV'
1226
+ # Report initial guesses HKL peak centers
1227
+ fit_nxgroup[hkl_name].center_initial_guess = NXdata()
1228
+ linkdims(fit_nxgroup[hkl_name].center_initial_guess)
1229
+ fit_nxgroup[hkl_name].center_initial_guess.makelink(
1230
+ nxdetector.uniform_fit[f'{hkl_name}/centers/values'],
1231
+ name='values')
1232
+ fit_nxgroup[hkl_name].center_initial_guess.attrs['signal'] = \
1233
+ 'values'
1234
+ # Report HKL peak centers
992
1235
  fit_nxgroup[hkl_name].centers = NXdata()
993
- fit_nxgroup[hkl_name].centers.attrs['axes'] = map_config.dims
994
1236
  linkdims(fit_nxgroup[hkl_name].centers)
995
1237
  fit_nxgroup[hkl_name].centers.values = NXfield(
996
1238
  value=centers_fit, attrs={'units': 'keV'})
997
1239
  fit_nxgroup[hkl_name].centers.errors = NXfield(
998
- value=centers_errors)
999
- fit_nxdata.makelink(fit_nxgroup[f'{hkl_name}/centers/values'],
1000
- name=hkl_name)
1240
+ value=centers_error)
1241
+ # fit_nxdata.makelink(fit_nxgroup[f'{hkl_name}/centers/values'],
1242
+ # name=hkl_name)
1243
+ fit_nxgroup[hkl_name].centers.attrs['signal'] = 'values'
1244
+ # Report HKL peak amplitudes
1245
+ fit_nxgroup[hkl_name].amplitudes = NXdata()
1246
+ linkdims(fit_nxgroup[hkl_name].amplitudes)
1247
+ fit_nxgroup[hkl_name].amplitudes.values = NXfield(
1248
+ value=amplitudes_fit, attrs={'units': 'counts'})
1249
+ fit_nxgroup[hkl_name].amplitudes.errors = NXfield(
1250
+ value=amplitudes_error)
1251
+ fit_nxgroup[hkl_name].amplitudes.attrs['signal'] = 'values'
1252
+ # Report HKL peak sigmas
1253
+ fit_nxgroup[hkl_name].sigmas = NXdata()
1254
+ linkdims(fit_nxgroup[hkl_name].sigmas)
1255
+ fit_nxgroup[hkl_name].sigmas.values = NXfield(
1256
+ value=sigmas_fit, attrs={'units': 'keV'})
1257
+ fit_nxgroup[hkl_name].sigmas.errors = NXfield(
1258
+ value=sigmas_error)
1259
+ fit_nxgroup[hkl_name].sigmas.attrs['signal'] = 'values'
1260
+
1001
1261
  return nxroot
1002
1262
 
1003
1263
 
1004
1264
  if __name__ == '__main__':
1005
- # local modules
1265
+ # Local modules
1006
1266
  from CHAP.processor import main
1007
1267
 
1008
1268
  main()