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

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

Potentially problematic release.


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

CHAP/edd/processor.py CHANGED
@@ -28,8 +28,8 @@ class DiffractionVolumeLengthProcessor(Processor):
28
28
  data,
29
29
  config=None,
30
30
  save_figures=False,
31
- outputdir='.',
32
31
  inputdir='.',
32
+ outputdir='.',
33
33
  interactive=False):
34
34
  """Return the calculated value of the DV length.
35
35
 
@@ -63,6 +63,7 @@ class DiffractionVolumeLengthProcessor(Processor):
63
63
  data, 'edd.models.DiffractionVolumeLengthConfig',
64
64
  inputdir=inputdir)
65
65
  except Exception as data_exc:
66
+ self.logger.error(data_exc)
66
67
  self.logger.info('No valid DVL config in input pipeline data, '
67
68
  + 'using config parameter instead.')
68
69
  try:
@@ -134,27 +135,30 @@ class DiffractionVolumeLengthProcessor(Processor):
134
135
 
135
136
  self.logger.info(
136
137
  'Interactively select a mask in the matplotlib figure')
138
+
137
139
  fig, mask, include_bin_ranges = select_mask_1d(
138
140
  np.sum(mca_data, axis=0),
139
- x = np.arange(detector.num_bins),
141
+ x=np.linspace(0, detector.max_energy_kev, detector.num_bins),
140
142
  label='Sum of MCA spectra over all scan points',
141
143
  preselected_index_ranges=detector.include_bin_ranges,
142
144
  title='Click and drag to select data range to include when '
143
145
  'measuring diffraction volume length',
144
- xlabel='MCA channel (index)',
146
+ xlabel='Uncalibrated Energy (keV)',
145
147
  ylabel='MCA intensity (counts)',
146
148
  min_num_index_ranges=1,
147
149
  interactive=interactive)
148
- detector.include_bin_ranges = include_bin_ranges
149
- self.logger.debug('Mask selected. Including detector bin ranges: '
150
- + str(detector.include_bin_ranges))
150
+ detector.include_energy_ranges = detector.get_energy_ranges(
151
+ include_bin_ranges)
152
+ self.logger.debug(
153
+ 'Mask selected. Including detector energy ranges: '
154
+ + str(detector.include_energy_ranges))
151
155
  if save_figures:
152
156
  fig.savefig(os.path.join(
153
157
  outputdir, f'{detector.detector_name}_dvl_mask.png'))
154
158
  plt.close()
155
- if detector.include_bin_ranges is None:
159
+ if not detector.include_energy_ranges:
156
160
  raise ValueError(
157
- 'No value provided for include_bin_ranges. '
161
+ 'No value provided for include_energy_ranges. '
158
162
  + 'Provide them in the Diffraction Volume Length '
159
163
  + 'Measurement Configuration, or re-run the pipeline '
160
164
  + 'with the --interactive flag.')
@@ -184,26 +188,21 @@ class DiffractionVolumeLengthProcessor(Processor):
184
188
  fit = Fit.fit_data(masked_sum, ('constant', 'gaussian'), x=x)
185
189
 
186
190
  # Calculate / manually select diffraction volume length
187
- dvl = fit.best_values['sigma'] * detector.sigma_to_dvl_factor
191
+ dvl = fit.best_values['sigma'] * detector.sigma_to_dvl_factor \
192
+ - dvl_config.sample_thickness
193
+ detector.fit_amplitude = fit.best_values['amplitude']
194
+ detector.fit_center = scan_center + fit.best_values['center']
195
+ detector.fit_sigma = fit.best_values['sigma']
188
196
  if detector.measurement_mode == 'manual':
189
197
  if interactive:
190
198
  _, _, dvl_bounds = select_mask_1d(
191
199
  masked_sum, x=x,
192
200
  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
201
  preselected_index_ranges=[
202
202
  (index_nearest(x, -dvl/2), index_nearest(x, dvl/2))],
203
203
  title=('Click and drag to indicate the boundary '
204
204
  'of the diffraction volume'),
205
- xlabel=(dvl_config.scanned_dim_lbl
206
- + ' (offset from scan "center")'),
205
+ xlabel=('Beam Direction (offset from scan "center")'),
207
206
  ylabel='MCA intensity (normalized)',
208
207
  min_num_index_ranges=1,
209
208
  max_num_index_ranges=1)
@@ -221,27 +220,28 @@ class DiffractionVolumeLengthProcessor(Processor):
221
220
 
222
221
  fig, ax = plt.subplots()
223
222
  ax.set_title(f'Diffraction Volume ({detector.detector_name})')
224
- ax.set_xlabel(dvl_config.scanned_dim_lbl \
225
- + ' (offset from scan "center")')
223
+ ax.set_xlabel('Beam Direction (offset from scan "center")')
226
224
  ax.set_ylabel('MCA intensity (normalized)')
227
225
  ax.plot(x, masked_sum, label='total (masked & normalized)')
228
226
  ax.plot(x, fit.best_fit, label='gaussian fit (to total)')
229
227
  ax.plot(x, masked_max, label='maximum (masked)')
230
228
  ax.plot(x, unmasked_sum, label='total (unmasked)')
231
- ax.axvspan(-dvl / 2., dvl / 2.,
232
- color='gray', alpha=0.5,
233
- label='diffraction volume'
234
- + f' ({detector.measurement_mode})')
229
+ ax.axvspan(
230
+ fit.best_values['center']- dvl/2.,
231
+ fit.best_values['center'] + dvl/2.,
232
+ color='gray', alpha=0.5,
233
+ label=f'diffraction volume ({detector.measurement_mode})')
235
234
  ax.legend()
236
- ax.text(
237
- 0, 1,
235
+ plt.figtext(
236
+ 0.5, 0.95,
238
237
  f'Diffraction volume length: {dvl:.2f}',
239
- ha='left', va='top',
240
- #transform=ax.get_xaxis_transform())
241
- transform=ax.transAxes)
238
+ fontsize='x-large',
239
+ horizontalalignment='center',
240
+ verticalalignment='bottom')
242
241
  if save_figures:
243
- figfile = os.path.join(outputdir,
244
- f'{detector.detector_name}_dvl.png')
242
+ fig.tight_layout(rect=(0, 0, 1, 0.95))
243
+ figfile = os.path.join(
244
+ outputdir, f'{detector.detector_name}_dvl.png')
245
245
  plt.savefig(figfile)
246
246
  self.logger.info(f'Saved figure to {figfile}')
247
247
  if interactive:
@@ -249,6 +249,348 @@ class DiffractionVolumeLengthProcessor(Processor):
249
249
 
250
250
  return dvl
251
251
 
252
+
253
+ class LatticeParameterRefinementProcessor(Processor):
254
+ """Processor to get a refined estimate for a sample's lattice
255
+ parameters"""
256
+ def process(self,
257
+ data,
258
+ config=None,
259
+ save_figures=False,
260
+ outputdir='.',
261
+ inputdir='.',
262
+ interactive=False):
263
+ """Given a strain analysis configuration, return a copy
264
+ contining refined values for the materials' lattice
265
+ parameters."""
266
+ ceria_calibration_config = self.get_config(
267
+ data, 'edd.models.MCACeriaCalibrationConfig', inputdir=inputdir)
268
+ try:
269
+ strain_analysis_config = self.get_config(
270
+ data, 'edd.models.StrainAnalysisConfig', inputdir=inputdir)
271
+ except Exception as data_exc:
272
+ # Local modules
273
+ from CHAP.edd.models import StrainAnalysisConfig
274
+
275
+ self.logger.info('No valid strain analysis config in input '
276
+ + 'pipeline data, using config parameter instead')
277
+ try:
278
+ strain_analysis_config = StrainAnalysisConfig(
279
+ **config, inputdir=inputdir)
280
+ except Exception as dict_exc:
281
+ raise RuntimeError from dict_exc
282
+
283
+ if len(strain_analysis_config.materials) > 1:
284
+ msg = 'Not implemented for multiple materials'
285
+ self.logger.error('Not implemented for multiple materials')
286
+ raise NotImplementedError(msg)
287
+
288
+ lattice_parameters = self.refine_lattice_parameters(
289
+ strain_analysis_config, ceria_calibration_config, 0,
290
+ interactive, save_figures, outputdir)
291
+ self.logger.debug(f'Refined lattice parameters: {lattice_parameters}')
292
+
293
+ strain_analysis_config.materials[0].lattice_parameters = \
294
+ lattice_parameters
295
+ return strain_analysis_config.dict()
296
+
297
+ def refine_lattice_parameters(
298
+ self, strain_analysis_config, ceria_calibration_config,
299
+ detector_i, interactive, save_figures, outputdir):
300
+ """Return refined values for the lattice parameters of the
301
+ materials indicated in `strain_analysis_config`. Method: given
302
+ a scan of a material, fit the peaks of each MCA
303
+ spectrum. Based on those fitted peak locations, calculate the
304
+ lattice parameters that would produce them. Return the
305
+ avearaged value of the calculated lattice parameters across
306
+ all spectra.
307
+
308
+ :param strain_analysis_config: Strain analysis configuration
309
+ :type strain_analysis_config: CHAP.edd.models.StrainAnalysisConfig
310
+ :param detector_i: Index of the detector in
311
+ `strain_analysis_config` whose data will be used for the
312
+ refinement
313
+ :type detector_i: int
314
+ :param interactive: Boolean to indicate whether interactive
315
+ matplotlib figures should be presented
316
+ :type interactive: bool
317
+ :param save_figures: Boolean to indicate whether figures
318
+ indicating the selection should be saved
319
+ :type save_figures: bool
320
+ :param outputdir: Where to save figures (if `save_figures` is
321
+ `True`)
322
+ :type outputdir: str
323
+ :returns: List of refined lattice parameters for materials in
324
+ `strain_analysis_config`
325
+ :rtype: list[numpy.ndarray]
326
+ """
327
+ import numpy as np
328
+ from CHAP.edd.utils import get_unique_hkls_ds, get_spectra_fits
329
+
330
+ self.add_detector_calibrations(
331
+ strain_analysis_config, ceria_calibration_config)
332
+
333
+ detector = strain_analysis_config.detectors[detector_i]
334
+ mca_bin_energies = self.get_mca_bin_energies(strain_analysis_config)
335
+ mca_data = strain_analysis_config.mca_data()
336
+ hkls, ds = get_unique_hkls_ds(
337
+ strain_analysis_config.materials,
338
+ tth_tol=detector.hkl_tth_tol,
339
+ tth_max=detector.tth_max)
340
+
341
+ self.select_material_params(
342
+ strain_analysis_config, detector_i, mca_data, mca_bin_energies,
343
+ interactive, save_figures, outputdir)
344
+ self.logger.debug(
345
+ 'Starting lattice parameters: '
346
+ + str(strain_analysis_config.materials[0].lattice_parameters))
347
+ self.select_fit_mask_hkls(
348
+ strain_analysis_config, detector_i, mca_data, mca_bin_energies,
349
+ hkls, ds,
350
+ interactive, save_figures, outputdir)
351
+
352
+ (uniform_fit_centers, uniform_fit_centers_errors,
353
+ uniform_fit_amplitudes, uniform_fit_amplitudes_errors,
354
+ uniform_fit_sigmas, uniform_fit_sigmas_errors,
355
+ uniform_best_fit, uniform_residuals,
356
+ uniform_redchi, uniform_success,
357
+ unconstrained_fit_centers, unconstrained_fit_centers_errors,
358
+ unconstrained_fit_amplitudes, unconstrained_fit_amplitudes_errors,
359
+ unconstrained_fit_sigmas, unconstrained_fit_sigmas_errors,
360
+ unconstrained_best_fit, unconstrained_residuals,
361
+ unconstrained_redchi, unconstrained_success) = \
362
+ self.get_spectra_fits(
363
+ strain_analysis_config, detector_i,
364
+ mca_data, mca_bin_energies, hkls, ds)
365
+
366
+ # Get the interplanar spacings measured for each fit HKL peak
367
+ # at every point in the map.
368
+ from scipy.constants import physical_constants
369
+ hc = 1e7 * physical_constants['Planck constant in eV/Hz'][0] \
370
+ * physical_constants['speed of light in vacuum'][0]
371
+ d_measured = hc / \
372
+ (2.0 \
373
+ * unconstrained_fit_centers \
374
+ * np.sin(np.radians(detector.tth_calibrated / 2.0)))
375
+ # Convert interplanar spacings to lattice parameters
376
+ self.logger.warning('Implemented for cubic materials only!')
377
+ fit_hkls = np.asarray([hkls[i] for i in detector.hkl_indices])
378
+ Rs = np.sqrt(np.sum(fit_hkls**2, 1))
379
+ a_measured = Rs[:, None] * d_measured
380
+ # Average all computed lattice parameters for every fit HKL
381
+ # peak at every point in the map to get the refined estimate
382
+ # for the material's lattice parameter
383
+ a_refined = float(np.mean(a_measured))
384
+ return [a_refined, a_refined, a_refined, 90.0, 90.0, 90.0]
385
+
386
+ def get_mca_bin_energies(self, strain_analysis_config):
387
+ """Return a list of the MCA bin energies for each detector.
388
+
389
+ :param strain_analysis_config: Strain analysis configuration
390
+ containing a list of detectors to return the bin energies
391
+ for.
392
+ :type strain_analysis_config: CHAP.edd.models.StrainAnalysisConfig
393
+ :returns: List of MCA bin energies
394
+ :rtype: list[numpy.ndarray]
395
+ """
396
+ mca_bin_energies = []
397
+ for i, detector in enumerate(strain_analysis_config.detectors):
398
+ mca_bin_energies.append(
399
+ detector.slope_calibrated
400
+ * np.linspace(0, detector.max_energy_kev, detector.num_bins)
401
+ + detector.intercept_calibrated)
402
+ return mca_bin_energies
403
+
404
+ def add_detector_calibrations(
405
+ self, strain_analysis_config, ceria_calibration_config):
406
+ """Add calibrated quantities to the detectors configured in
407
+ `strain_analysis_config`, modifying `strain_analysis_config`
408
+ in place.
409
+
410
+ :param strain_analysis_config: Strain analysisi configuration
411
+ containing a list of detectors to add calibration values
412
+ to.
413
+ :type strain_analysis_config: CHAP.edd.models.StrainAnalysisConfig
414
+ :param ceria_calibration_config: Configuration of a completed
415
+ ceria calibration containing a list of detector swith the
416
+ same names as those in `strain_analysis_config`
417
+ :returns: None"""
418
+ for detector in strain_analysis_config.detectors:
419
+ calibration = [
420
+ d for d in ceria_calibration_config.detectors \
421
+ if d.detector_name == detector.detector_name][0]
422
+ detector.add_calibration(calibration)
423
+
424
+ def select_fit_mask_hkls(
425
+ self, strain_analysis_config, detector_i,
426
+ mca_data, mca_bin_energies, hkls, ds,
427
+ interactive, save_figures, outputdir):
428
+ """Select the maks & HKLs to use for fitting for each
429
+ detector. Update `strain_analysis_config` with new values for
430
+ `hkl_indices` and `include_bin_ranges` if needed.
431
+
432
+ :param strain_analysis_config: Strain analysis configuration
433
+ :type strain_analysis_config: CHAP.edd.models.StrainAnalysisConfig
434
+ :param detector_i: Index of the detector in
435
+ `strain_analysis_config` to select mask & HKLs for.
436
+ :type detector_i: int
437
+ :param mca_data: List of maps of MCA spectra for all detectors
438
+ in `strain_analysis_config`
439
+ :type mca_data: list[numpy.ndarray]
440
+ :param mca_bin_energies: List of MCA bin energies for all
441
+ detectors in `strain_analysis_config`
442
+ :type mca_bin_energies: list[numpy.ndarray]
443
+ :param hkls: Nominal HKL peak energy locations for the
444
+ material in `strain_analysis_config`
445
+ :type hkls: list[float]
446
+ :param ds: Nominal d-spacing for the material in
447
+ `strain_analysis_config`
448
+ :type ds: list[float]
449
+ :param interactive: Boolean to indicate whether interactive
450
+ matplotlib figures should be presented
451
+ :type interactive: bool
452
+ :param save_figures: Boolean to indicate whether figures
453
+ indicating the selection should be saved
454
+ :type save_figures: bool
455
+ :param outputdir: Where to save figures (if `save_figures` is
456
+ `True`)
457
+ :type outputdir: str
458
+ :returns: None
459
+ """
460
+ if not interactive and not save_figures:
461
+ return
462
+ import matplotlib.pyplot as plt
463
+ import numpy as np
464
+ from CHAP.edd.utils import select_mask_and_hkls
465
+
466
+ detector = strain_analysis_config.detectors[detector_i]
467
+ fig, include_bin_ranges, hkl_indices = \
468
+ select_mask_and_hkls(
469
+ mca_bin_energies[detector_i],
470
+ np.sum(mca_data[detector_i], axis=0),
471
+ hkls, ds,
472
+ detector.tth_calibrated,
473
+ detector.include_bin_ranges, detector.hkl_indices,
474
+ detector.detector_name, mca_data[detector_i],
475
+ calibration_bin_ranges=detector.calibration_bin_ranges,
476
+ label='Sum of all spectra in the map',
477
+ interactive=interactive)
478
+ detector.include_energy_ranges = detector.get_energy_ranges(
479
+ include_bin_ranges)
480
+ detector.hkl_indices = hkl_indices
481
+ if save_figures:
482
+ fig.savefig(os.path.join(
483
+ outputdir,
484
+ f'{detector.detector_name}_strainanalysis_'
485
+ 'fit_mask_hkls.png'))
486
+ plt.close()
487
+
488
+ def select_material_params(self, strain_analysis_config, detector_i,
489
+ mca_data, mca_bin_energies,
490
+ interactive, save_figures, outputdir):
491
+ """Select initial material parameters to use for determining
492
+ nominal HKL peak locations. Modify `strain_analysis_config` in
493
+ place if needed.
494
+
495
+ :param strain_analysis_config: Strain analysis configuration
496
+ :type strain_analysis_config: CHAP.edd.models.StrainAnalysisConfig
497
+ :param detector_i: Index of the detector in
498
+ `strain_analysis_config` to select mask & HKLs for.
499
+ :type detector_i: int
500
+ :param mca_data: List of maps of MCA spectra for all detectors
501
+ in `strain_analysis_config`
502
+ :type mca_data: list[numpy.ndarray]
503
+ :param mca_bin_energies: List of MCA bin energies for all
504
+ detectors in `strain_analysis_config`
505
+ :type mca_bin_energies: list[numpy.ndarray]
506
+ :param interactive: Boolean to indicate whether interactive
507
+ matplotlib figures should be presented
508
+ :type interactive: bool
509
+ :param save_figures: Boolean to indicate whether figures
510
+ indicating the selection should be saved
511
+ :type save_figures: bool
512
+ :param outputdir: Where to save figures (if `save_figures` is
513
+ `True`)
514
+ :type outputdir: str
515
+ :returns: None
516
+ """
517
+ if not interactive and not save_figures:
518
+ return
519
+ from CHAP.edd.utils import select_material_params
520
+ import matplotlib.pyplot as plt
521
+ import numpy as np
522
+ fig, strain_analysis_config.materials = select_material_params(
523
+ mca_bin_energies[detector_i], np.sum(mca_data[detector_i], axis=0),
524
+ strain_analysis_config.detectors[detector_i].tth_calibrated,
525
+ strain_analysis_config.materials,
526
+ label='Sum of all spectra in the map', interactive=interactive)
527
+ self.logger.debug(
528
+ f'materials: {strain_analysis_config.materials}')
529
+ if save_figures:
530
+ detector_name = \
531
+ strain_analysis_config.detectors[detector_i].detector_name
532
+ fig.savefig(os.path.join(
533
+ outputdir,
534
+ f'{detector_name}_strainanalysis_'
535
+ 'material_config.png'))
536
+ plt.close()
537
+
538
+ def get_spectra_fits(
539
+ self, strain_analysis_config, detector_i,
540
+ mca_data, mca_bin_energies, hkls, ds):
541
+ """Return uniform and unconstrained fit results for all
542
+ spectra from a single detector.
543
+
544
+ :param strain_analysis_config: Strain analysis configuration
545
+ :type strain_analysis_config: CHAP.edd.models.StrainAnalysisConfig
546
+ :param detector_i: Index of the detector in
547
+ `strain_analysis_config` to select mask & HKLs for.
548
+ :type detector_i: int
549
+ :param mca_data: List of maps of MCA spectra for all detectors
550
+ in `strain_analysis_config`
551
+ :type mca_data: list[numpy.ndarray]
552
+ :param mca_bin_energies: List of MCA bin energies for all
553
+ detectors in `strain_analysis_config`
554
+ :type mca_bin_energies: list[numpy.ndarray]
555
+ :param hkls: Nominal HKL peak energy locations for the
556
+ material in `strain_analysis_config`
557
+ :type hkls: list[float]
558
+ :param ds: Nominal d-spacing for the material in
559
+ `strain_analysis_config`
560
+ :type ds: list[float]
561
+ :returns: Uniform and unconstrained centers, amplitdues,
562
+ sigmas (and errors for all three), best fits, residuals
563
+ between the best fits and the input spectra, reduced chi,
564
+ and fit success statuses.
565
+ :rtype: tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray,
566
+ numpy.ndarray, numpy.ndarray, numpy.ndarray,
567
+ numpy.ndarray, numpy.ndarray, numpy.ndarray,
568
+ numpy.ndarray, numpy.ndarray, numpy.ndarray,
569
+ numpy.ndarray, numpy.ndarray, numpy.ndarray,
570
+ numpy.ndarray, numpy.ndarray, numpy.ndarray,
571
+ numpy.ndarray, numpy.ndarray]
572
+ """
573
+ from CHAP.edd.utils import get_peak_locations, get_spectra_fits
574
+ detector = strain_analysis_config.detectors[detector_i]
575
+ self.logger.debug(
576
+ f'Fitting spectra from detector {detector.detector_name}')
577
+ mask = detector.mca_mask()
578
+ energies = mca_bin_energies[detector_i][mask]
579
+ intensities = np.empty(
580
+ (*strain_analysis_config.map_config.shape, len(energies)),
581
+ dtype='uint16')
582
+ for j, map_index in \
583
+ enumerate(np.ndindex(strain_analysis_config.map_config.shape)):
584
+ intensities[map_index] = \
585
+ mca_data[detector_i][j].astype('uint16')[mask]
586
+ fit_hkls = np.asarray([hkls[i] for i in detector.hkl_indices])
587
+ fit_ds = np.asarray([ds[i] for i in detector.hkl_indices])
588
+ peak_locations = get_peak_locations(
589
+ fit_ds, detector.tth_calibrated)
590
+ return get_spectra_fits(
591
+ intensities, energies, peak_locations, detector)
592
+
593
+
252
594
  class MCACeriaCalibrationProcessor(Processor):
253
595
  """A Processor using a CeO2 scan to obtain tuned values for the
254
596
  bragg diffraction angle and linear correction parameters for MCA
@@ -259,8 +601,8 @@ class MCACeriaCalibrationProcessor(Processor):
259
601
  data,
260
602
  config=None,
261
603
  save_figures=False,
262
- outputdir='.',
263
604
  inputdir='.',
605
+ outputdir='.',
264
606
  interactive=False):
265
607
  """Return tuned values for 2&theta and linear correction
266
608
  parameters for the MCA channel energies.
@@ -361,7 +703,6 @@ class MCACeriaCalibrationProcessor(Processor):
361
703
  mca_bin_energies = np.linspace(
362
704
  0, detector.max_energy_kev, detector.num_bins)
363
705
  mca_data = calibration_config.mca_data(detector)
364
-
365
706
  if interactive or save_figures:
366
707
  # Third party modules
367
708
  import matplotlib.pyplot as plt
@@ -389,27 +730,29 @@ class MCACeriaCalibrationProcessor(Processor):
389
730
  detector.tth_initial_guess, detector.include_bin_ranges,
390
731
  detector.hkl_indices, detector.detector_name,
391
732
  flux_energy_range=calibration_config.flux_file_energy_range,
733
+ label='MCA data',
392
734
  interactive=interactive)
393
- detector.include_bin_ranges = include_bin_ranges
735
+ detector.include_energy_ranges = detector.get_energy_ranges(
736
+ include_bin_ranges)
394
737
  detector.hkl_indices = hkl_indices
395
738
  if save_figures:
396
739
  fig.savefig(os.path.join(
397
- outputdir,
740
+ outputdir,
398
741
  f'{detector.detector_name}_calibration_fit_mask_hkls.png'))
399
742
  plt.close()
400
743
  self.logger.debug(f'tth_initial_guess = {detector.tth_initial_guess}')
401
744
  self.logger.debug(
402
- f'include_bin_ranges = {detector.include_bin_ranges}')
403
- if detector.include_bin_ranges is None:
745
+ f'include_energy_ranges = {detector.include_energy_ranges}')
746
+ if not detector.include_energy_ranges:
404
747
  raise ValueError(
405
- 'No value provided for include_bin_ranges. '
406
- 'Provide them in the MCA Ceria Calibration Configuration, '
748
+ 'No value provided for include_energy_ranges. '
749
+ 'Provide them in the MCA Ceria Calibration Configuration '
407
750
  'or re-run the pipeline with the --interactive flag.')
408
- if detector.hkl_indices is None:
751
+ if not detector.hkl_indices:
409
752
  raise ValueError(
410
753
  '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.')
754
+ 'the detector\'s MCA Ceria Calibration Configuration or '
755
+ 're-run the pipeline with the --interactive flag.')
413
756
  mca_mask = detector.mca_mask()
414
757
  fit_mca_energies = mca_bin_energies[mca_mask]
415
758
  fit_mca_intensities = mca_data[mca_mask]
@@ -422,52 +765,59 @@ class MCACeriaCalibrationProcessor(Processor):
422
765
 
423
766
  # Get the HKLs and lattice spacings that will be used for
424
767
  # fitting
768
+ # Restrict the range for the centers with some margin to
769
+ # prevent having centers near the edge of the fitting range
770
+ delta = 0.1 * (fit_mca_energies[-1]-fit_mca_energies[0])
771
+ centers_range = (
772
+ max(0.0, fit_mca_energies[0]-delta), fit_mca_energies[-1]+delta)
425
773
  fit_hkls = np.asarray([hkls[i] for i in detector.hkl_indices])
426
774
  fit_ds = np.asarray([ds[i] for i in detector.hkl_indices])
427
775
  c_1 = fit_hkls[:,0]**2 + fit_hkls[:,1]**2 + fit_hkls[:,2]**2
428
776
  tth = detector.tth_initial_guess
777
+ fit_E0 = get_peak_locations(fit_ds, tth)
429
778
  for iter_i in range(calibration_config.max_iter):
430
- self.logger.debug(f'Tuning tth: iteration no. {iter_i}, '
431
- + f'starting tth value = {tth} ')
779
+ self.logger.debug(
780
+ f'Tuning tth: iteration no. {iter_i}, starting value = {tth} ')
432
781
 
433
782
  # Perform the uniform fit first
434
783
 
435
784
  # Get expected peak energy locations for this iteration's
436
785
  # starting value of tth
437
- fit_E0 = get_peak_locations(fit_ds, tth)
786
+ _fit_E0 = get_peak_locations(fit_ds, tth)
438
787
 
439
788
  # Run the uniform fit
440
- uniform_fit = Fit(fit_mca_intensities, x=fit_mca_energies)
441
- uniform_fit.create_multipeak_model(
442
- fit_E0, fit_type='uniform')
443
- #fit_E0, fit_type='uniform', background='constant')
444
- uniform_fit.fit()
789
+ fit = Fit(fit_mca_intensities, x=fit_mca_energies)
790
+ fit.create_multipeak_model(
791
+ _fit_E0, fit_type='uniform', background=detector.background,
792
+ centers_range=centers_range)
793
+ fit.fit()
445
794
 
446
795
  # Extract values of interest from the best values for the
447
796
  # uniform fit parameters
797
+ uniform_best_fit = fit.best_fit
798
+ uniform_residual = fit.residual
448
799
  uniform_fit_centers = [
449
- uniform_fit.best_values[f'peak{i+1}_center']
800
+ fit.best_values[f'peak{i+1}_center']
450
801
  for i in range(len(fit_hkls))]
451
- uniform_a = uniform_fit.best_values['scale_factor']
802
+ uniform_a = fit.best_values['scale_factor']
452
803
  uniform_strain = np.log(
453
804
  (uniform_a
454
805
  / calibration_config.material.lattice_parameters)) # CeO2 is cubic, so this is fine here.
455
806
 
456
807
  # Next, perform the unconstrained fit
457
808
 
458
- # Use the peak locations found in the uniform fit as the
809
+ # Use the peak parameters from the uniform fit as the
459
810
  # initial guesses for peak locations in the unconstrained
460
811
  # fit
461
- unconstrained_fit = Fit(fit_mca_intensities, x=fit_mca_energies)
462
- unconstrained_fit.create_multipeak_model(
463
- uniform_fit_centers, fit_type='unconstrained',
464
- )#background='constant')
465
- unconstrained_fit.fit()
812
+ fit.create_multipeak_model(fit_type='unconstrained')
813
+ fit.fit()
466
814
 
467
815
  # Extract values of interest from the best values for the
468
816
  # unconstrained fit parameters
817
+ unconstrained_best_fit = fit.best_fit
818
+ unconstrained_residual = fit.residual
469
819
  unconstrained_fit_centers = np.array(
470
- [unconstrained_fit.best_values[f'peak{i+1}_center']
820
+ [fit.best_values[f'peak{i+1}_center']
471
821
  for i in range(len(fit_hkls))])
472
822
  unconstrained_a = np.sqrt(c_1)*abs(get_peak_locations(
473
823
  unconstrained_fit_centers, tth))
@@ -509,13 +859,13 @@ class MCACeriaCalibrationProcessor(Processor):
509
859
  axs[0,0].text(hkl_E, 1, str(fit_hkls[i])[1:-1],
510
860
  ha='right', va='top', rotation=90,
511
861
  transform=axs[0,0].get_xaxis_transform())
512
- axs[0,0].plot(fit_mca_energies, uniform_fit.best_fit,
513
- label='Single Strain')
514
- axs[0,0].plot(fit_mca_energies, unconstrained_fit.best_fit,
515
- label='Unconstrained')
862
+ axs[0,0].plot(fit_mca_energies, uniform_best_fit,
863
+ label='Single Strain')
864
+ axs[0,0].plot(fit_mca_energies, unconstrained_best_fit,
865
+ label='Unconstrained')
516
866
  #axs[0,0].plot(fit_mca_energies, MISSING?, label='least squares')
517
867
  axs[0,0].plot(fit_mca_energies, fit_mca_intensities,
518
- label='Flux-Corrected & Masked MCA Data')
868
+ label='Flux-Corrected & Masked MCA Data')
519
869
  axs[0,0].legend()
520
870
 
521
871
  # Lower left axes: fit residuals
@@ -523,10 +873,10 @@ class MCACeriaCalibrationProcessor(Processor):
523
873
  axs[1,0].set_xlabel('Energy (keV)')
524
874
  axs[1,0].set_ylabel('Residual (a.u)')
525
875
  axs[1,0].plot(fit_mca_energies,
526
- uniform_fit.residual,
876
+ uniform_residual,
527
877
  label='Single Strain')
528
878
  axs[1,0].plot(fit_mca_energies,
529
- unconstrained_fit.residual,
879
+ unconstrained_residual,
530
880
  label='Unconstrained')
531
881
  axs[1,0].legend()
532
882
 
@@ -556,6 +906,19 @@ class MCACeriaCalibrationProcessor(Processor):
556
906
  color='C1', label='Unconstrained: Linear Fit')
557
907
  axs[1,1].legend()
558
908
 
909
+ # Add a text box showing final calibrated values
910
+ axs[1,1].text(
911
+ 0.98, 0.02,
912
+ 'Calibrated Values:\n\n'
913
+ + f'Takeoff Angle:\n {tth:.5f}$^\circ$\n\n'
914
+ + f'Slope:\n {slope:.5f}\n\n'
915
+ + f'Intercept:\n {intercept:.5f} $keV$',
916
+ ha='right', va='bottom', ma='left',
917
+ transform=axs[1,1].transAxes,
918
+ bbox=dict(boxstyle='round',
919
+ ec=(1., 0.5, 0.5),
920
+ fc=(1., 0.8, 0.8, 0.8)))
921
+
559
922
  fig.tight_layout()
560
923
 
561
924
  if save_figures:
@@ -568,6 +931,141 @@ class MCACeriaCalibrationProcessor(Processor):
568
931
  return float(tth), float(slope), float(intercept)
569
932
 
570
933
 
934
+ class MCAEnergyCalibrationProcessor(Processor):
935
+ """Processor to return parameters for linearly transforming MCA
936
+ channel indices to energies (in keV). Procedure: provide a
937
+ spectrum from the MCA element to be calibrated and the theoretical
938
+ location of at least one peak present in that spectrum (peak
939
+ locations must be given in keV). It is strongly recommended to use
940
+ the location of fluorescence peaks whenever possible, _not_
941
+ diffraction peaks, as this Processor does not account for
942
+ 2&theta."""
943
+ def process(self,
944
+ data,
945
+ max_energy,
946
+ peak_energies,
947
+ peak_initial_guesses=None,
948
+ peak_center_fit_delta=2.0,
949
+ fit_ranges=None,
950
+ save_figures=False,
951
+ interactive=False,
952
+ outputdir='.'):
953
+ """Fit the specified peaks in the MCA spectrum provided. Using
954
+ the difference between the provided peak locations and the fit
955
+ centers of those peaks, compute linear correction parameters
956
+ to convert MCA channel indices to energies in keV. Return
957
+ those parameters as a dictionary.
958
+
959
+ :param data: An MCA spectrum
960
+ :type data: PipelineData
961
+ :param max_energy: The (uncalibrated) maximum energy measured
962
+ by the MCA spectrum provided.
963
+ :type max_energy: float
964
+ :param peak_energies: Theoretical locations of peaks to use
965
+ for calibrating the MCA channel energies. It is _strongly_
966
+ recommended to use fluorescence peaks.
967
+ :type peak_energies: list[float]
968
+ :param peak_initial_guesses: A list of values to use for the
969
+ initial guesses for peak locations when performing the fit
970
+ of the spectrum. Providing good values to this parameter
971
+ can greatly improve the quality of the spectrum fit when
972
+ the uncalibrated detector channel energies are too far off
973
+ to use the values in `peak_energies` for the initial
974
+ guesses for peak centers. Defaults to None.
975
+ :type peak_inital_guesses: Optional[list[float]]
976
+ :param peak_center_fit_delta: Set boundaries on the fit peak
977
+ centers when performing the fit. The min/max possible
978
+ values for the peak centers will be the values provided in
979
+ `peak_energies` (or `peak_initial_guesses`, if used) ±
980
+ `peak_center_fit_delta`. Defaults to 2.0.
981
+ :type peak_center_fit_delta: float
982
+ :param fit_ranges: Explicit ranges of MCA channel indices
983
+ (_not_ energies) to include when performing a fit of the
984
+ given peaks to the provied MCA spectrum. Use this
985
+ parameter or select it interactively by running a pipeline
986
+ with `config.interactive: True`. Defaults to []
987
+ :type fit_ranges: Optional[list[tuple[int, int]]]
988
+ :param save_figures: Save .pngs of plots for checking inputs &
989
+ outputs of this Processor, defaults to False.
990
+ :type save_figures: bool, optional
991
+ :param interactive: Allows for user interactions, defaults to
992
+ False.
993
+ :type interactive: bool, optional
994
+ :param outputdir: Directory to which any output figures will
995
+ be saved, defaults to '.'.
996
+ :type outputdir: str, optional
997
+ :returns: Dictionary containing linear energy correction
998
+ parameters for the MCA element
999
+ :rtype: dict[str, float]
1000
+ """
1001
+ # Validate arguments: fit_ranges & interactive
1002
+ if not (fit_ranges or interactive):
1003
+ self.logger.exception(
1004
+ RuntimeError(
1005
+ 'If `fit_ranges` is not explicitly provided, '
1006
+ + self.__class__.__name__
1007
+ + ' must be run with `interactive=True`.'))
1008
+ # Validate arguments: peak_energies & peak_initial_guesses
1009
+ if peak_initial_guesses is None:
1010
+ peak_initial_guesses = peak_energies
1011
+ else:
1012
+ from CHAP.utils.general import is_num_series
1013
+ is_num_series(peak_initial_guesses, raise_error=True)
1014
+ if len(peak_initial_guesses) != len(peak_energies):
1015
+ self.logger.exception(
1016
+ ValueError(
1017
+ 'peak_initial_guesses must have the same number of '
1018
+ + 'values as peak_energies'))
1019
+
1020
+ import matplotlib.pyplot as plt
1021
+ import numpy as np
1022
+ from CHAP.utils.fit import Fit
1023
+ from CHAP.utils.general import select_mask_1d
1024
+
1025
+ spectrum = self.unwrap_pipelinedata(data)[0]
1026
+ num_bins = len(spectrum)
1027
+ uncalibrated_energies = np.linspace(0, max_energy, num_bins)
1028
+
1029
+ fig, mask, fit_ranges = select_mask_1d(
1030
+ spectrum, x=uncalibrated_energies,
1031
+ preselected_index_ranges=fit_ranges,
1032
+ xlabel='Uncalibrated Energy', ylabel='Intensity',
1033
+ min_num_index_ranges=1, interactive=interactive)
1034
+ if save_figures:
1035
+ fig.savefig(os.path.join(
1036
+ outputdir, 'mca_energy_calibration_mask.png'))
1037
+ plt.close()
1038
+ self.logger.debug(f'Selected index ranges to fit: {fit_ranges}')
1039
+
1040
+ spectrum_fit = Fit(spectrum[mask], x=uncalibrated_energies[mask])
1041
+ for i, (peak_energy, initial_guess) in enumerate(
1042
+ zip(peak_energies, peak_initial_guesses)):
1043
+ spectrum_fit.add_model(
1044
+ 'gaussian', prefix=f'peak{i+1}_', parameters=(
1045
+ {'name': 'amplitude', 'min': 0.0},
1046
+ {'name': 'center', 'value': initial_guess,
1047
+ 'min': initial_guess - peak_center_fit_delta,
1048
+ 'max': initial_guess + peak_center_fit_delta}
1049
+ ))
1050
+ self.logger.debug('Fitting spectrum')
1051
+ spectrum_fit.fit()
1052
+ fit_peak_energies = [
1053
+ spectrum_fit.best_values[f'peak{i+1}_center']
1054
+ for i in range(len(peak_energies))]
1055
+ self.logger.debug(f'Fit peak centers: {fit_peak_energies}')
1056
+
1057
+ energy_fit = Fit.fit_data(
1058
+ peak_energies, 'linear', x=fit_peak_energies, nan_policy='omit')
1059
+ slope = energy_fit.best_values['slope']
1060
+ intercept = energy_fit.best_values['intercept']
1061
+
1062
+ # Rescale slope so results are a linear correction from
1063
+ # channel indices -> calibrated energies, not uncalibrated
1064
+ # energies -> calibrated energies
1065
+ slope = (max_energy / num_bins) * slope
1066
+ return({'slope': slope, 'intercept': intercept})
1067
+
1068
+
571
1069
  class MCADataProcessor(Processor):
572
1070
  """A Processor to return data from an MCA, restuctured to
573
1071
  incorporate the shape & metadata associated with a map
@@ -579,8 +1077,8 @@ class MCADataProcessor(Processor):
579
1077
  data,
580
1078
  config=None,
581
1079
  save_figures=False,
582
- outputdir='.',
583
1080
  inputdir='.',
1081
+ outputdir='.',
584
1082
  interactive=False):
585
1083
  """Process configurations for a map and MCA detector(s), and
586
1084
  return the calibrated MCA data collected over the map.
@@ -681,8 +1179,8 @@ class StrainAnalysisProcessor(Processor):
681
1179
  data,
682
1180
  config=None,
683
1181
  save_figures=False,
684
- outputdir='.',
685
1182
  inputdir='.',
1183
+ outputdir='.',
686
1184
  interactive=False):
687
1185
  """Return strain analysis maps & associated metadata in an NXprocess.
688
1186
 
@@ -791,8 +1289,8 @@ class StrainAnalysisProcessor(Processor):
791
1289
  from CHAP.edd.utils import (
792
1290
  get_peak_locations,
793
1291
  get_unique_hkls_ds,
1292
+ get_spectra_fits
794
1293
  )
795
- from CHAP.utils.fit import FitMap
796
1294
 
797
1295
  def linkdims(nxgroup, field_dims=[]):
798
1296
  if isinstance(field_dims, dict):
@@ -864,11 +1362,11 @@ class StrainAnalysisProcessor(Processor):
864
1362
  # calibration_mask = detector.mca_mask()
865
1363
  calibration_bin_ranges = detector.include_bin_ranges
866
1364
 
867
-
868
1365
  tth = strain_analysis_config.detectors[0].tth_calibrated
869
1366
  fig, strain_analysis_config.materials = select_material_params(
870
- mca_bin_energies[0], mca_data[0][0], tth,
1367
+ mca_bin_energies[0], np.sum(mca_data, axis=1)[0], tth,
871
1368
  materials=strain_analysis_config.materials,
1369
+ label='Sum of all spectra in the map',
872
1370
  interactive=interactive)
873
1371
  self.logger.debug(
874
1372
  f'materials: {strain_analysis_config.materials}')
@@ -888,14 +1386,18 @@ class StrainAnalysisProcessor(Processor):
888
1386
  for i, detector in enumerate(strain_analysis_config.detectors):
889
1387
  fig, include_bin_ranges, hkl_indices = \
890
1388
  select_mask_and_hkls(
891
- mca_bin_energies[i], mca_data[i][0], hkls, ds,
1389
+ mca_bin_energies[i],
1390
+ np.sum(mca_data[i], axis=0),
1391
+ hkls, ds,
892
1392
  detector.tth_calibrated,
893
1393
  detector.include_bin_ranges, detector.hkl_indices,
894
1394
  detector.detector_name, mca_data[i],
895
1395
  # calibration_mask=calibration_mask,
896
1396
  calibration_bin_ranges=calibration_bin_ranges,
1397
+ label='Sum of all spectra in the map',
897
1398
  interactive=interactive)
898
- detector.include_bin_ranges = include_bin_ranges
1399
+ detector.include_energy_ranges = detector.get_energy_ranges(
1400
+ include_bin_ranges)
899
1401
  detector.hkl_indices = hkl_indices
900
1402
  if save_figures:
901
1403
  fig.savefig(os.path.join(
@@ -915,6 +1417,16 @@ class StrainAnalysisProcessor(Processor):
915
1417
  tth_max=strain_analysis_config.detectors[0].tth_max)
916
1418
 
917
1419
  for i, detector in enumerate(strain_analysis_config.detectors):
1420
+ if not detector.include_energy_ranges:
1421
+ raise ValueError(
1422
+ 'No value provided for include_energy_ranges. '
1423
+ 'Provide them in the MCA Ceria Calibration Configuration, '
1424
+ 'or re-run the pipeline with the --interactive flag.')
1425
+ if not detector.hkl_indices:
1426
+ raise ValueError(
1427
+ 'No value provided for hkl_indices. Provide them in '
1428
+ 'the detector\'s MCA Ceria Calibration Configuration, or'
1429
+ ' re-run the pipeline with the --interactive flag.')
918
1430
  # Setup NXdata group
919
1431
  self.logger.debug(
920
1432
  f'Setting up NXdata group for {detector.detector_name}')
@@ -962,48 +1474,20 @@ class StrainAnalysisProcessor(Processor):
962
1474
  fit_ds = np.asarray([ds[i] for i in detector.hkl_indices])
963
1475
  peak_locations = get_peak_locations(
964
1476
  fit_ds, detector.tth_calibrated)
965
- num_peak = len(peak_locations)
966
- # KLS: Use the below def of peak_locations when
967
- # FitMap.create_multipeak_model can accept a list of maps
968
- # for centers.
969
- # tth = np.radians(detector.map_tth(map_config))
970
- # peak_locations = [get_peak_locations(d0, tth) for d0 in fit_ds]
971
-
972
- # Perform initial fit: assume uniform strain for all HKLs
973
- self.logger.debug('Performing uniform fit')
974
- fit = FitMap(det_nxdata.intensity.nxdata, x=energies)
975
- fit.create_multipeak_model(
976
- peak_locations,
977
- fit_type='uniform',
978
- peak_models=detector.peak_models,
979
- background=detector.background,
980
- fwhm_min=detector.fwhm_min,
981
- fwhm_max=detector.fwhm_max)
982
- fit.fit()
983
- uniform_fit_centers = [
984
- fit.best_values[
985
- fit.best_parameters().index(f'peak{i+1}_center')]
986
- for i in range(num_peak)]
987
- uniform_fit_centers_errors = [
988
- fit.best_errors[
989
- fit.best_parameters().index(f'peak{i+1}_center')]
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)]
1477
+
1478
+ (uniform_fit_centers, uniform_fit_centers_errors,
1479
+ uniform_fit_amplitudes, uniform_fit_amplitudes_errors,
1480
+ uniform_fit_sigmas, uniform_fit_sigmas_errors,
1481
+ uniform_best_fit, uniform_residuals,
1482
+ uniform_redchi, uniform_success,
1483
+ unconstrained_fit_centers, unconstrained_fit_centers_errors,
1484
+ unconstrained_fit_amplitudes, unconstrained_fit_amplitudes_errors,
1485
+ unconstrained_fit_sigmas, unconstrained_fit_sigmas_errors,
1486
+ unconstrained_best_fit, unconstrained_residuals,
1487
+ unconstrained_redchi, unconstrained_success) = \
1488
+ get_spectra_fits(
1489
+ det_nxdata.intensity.nxdata, energies,
1490
+ peak_locations, detector)
1007
1491
 
1008
1492
  # Add uniform fit results to the NeXus structure
1009
1493
  nxdetector.uniform_fit = NXcollection()
@@ -1015,10 +1499,10 @@ class StrainAnalysisProcessor(Processor):
1015
1499
  linkdims(
1016
1500
  fit_nxdata, {'axes': 'energy', 'index': len(map_config.shape)})
1017
1501
  fit_nxdata.makelink(det_nxdata.energy)
1018
- fit_nxdata.best_fit= fit.best_fit
1019
- fit_nxdata.residuals = fit.residual
1020
- fit_nxdata.redchi = fit.redchi
1021
- fit_nxdata.success = fit.success
1502
+ fit_nxdata.best_fit= uniform_best_fit
1503
+ fit_nxdata.residuals = uniform_residuals
1504
+ fit_nxdata.redchi = uniform_redchi
1505
+ fit_nxdata.success = uniform_success
1022
1506
 
1023
1507
  # Peak-by-peak results
1024
1508
  # fit_nxgroup.fit_hkl_centers = NXdata()
@@ -1064,39 +1548,6 @@ class StrainAnalysisProcessor(Processor):
1064
1548
  value=sigmas_error)
1065
1549
  fit_nxgroup[hkl_name].sigmas.attrs['signal'] = 'values'
1066
1550
 
1067
- # Perform second fit: do not assume uniform strain for all
1068
- # HKLs, and use the fit peak centers from the uniform fit
1069
- # as inital guesses
1070
- self.logger.debug('Performing unconstrained fit')
1071
- fit.create_multipeak_model(fit_type='unconstrained')
1072
- fit.fit(rel_amplitude_cutoff=detector.rel_amplitude_cutoff)
1073
- unconstrained_fit_centers = np.array(
1074
- [fit.best_values[
1075
- fit.best_parameters()\
1076
- .index(f'peak{i+1}_center')]
1077
- for i in range(num_peak)])
1078
- unconstrained_fit_centers_errors = np.array(
1079
- [fit.best_errors[
1080
- fit.best_parameters()\
1081
- .index(f'peak{i+1}_center')]
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
1551
  if interactive or save_figures:
1101
1552
  # Third party modules
1102
1553
  import matplotlib.animation as animation
@@ -1113,13 +1564,15 @@ class StrainAnalysisProcessor(Processor):
1113
1564
  intensity.set_ydata(
1114
1565
  det_nxdata.intensity.nxdata[map_index]
1115
1566
  / 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])
1567
+ best_fit.set_ydata(
1568
+ unconstrained_best_fit[map_index]
1569
+ / unconstrained_best_fit[map_index].max())
1570
+ # residual.set_ydata(unconstrained_residuals[map_index])
1119
1571
  index.set_text('\n'.join(f'{k}[{i}] = {v}'
1120
1572
  for k, v in map_config.get_coords(map_index).items()))
1121
1573
  if save_figures:
1122
- plt.savefig(os.path.join(path, f'frame_{i}.png'))
1574
+ plt.savefig(os.path.join(
1575
+ path, f'frame_{str(i).zfill(num_digit)}.png'))
1123
1576
  #return intensity, best_fit, residual, index
1124
1577
  return intensity, best_fit, index
1125
1578
 
@@ -1130,12 +1583,12 @@ class StrainAnalysisProcessor(Processor):
1130
1583
  / det_nxdata.intensity.nxdata[map_index].max())
1131
1584
  intensity, = ax.plot(
1132
1585
  energies, data_normalized, 'b.', label='data')
1133
- fit_normalized = (fit.best_fit[map_index]
1134
- / fit.best_fit[map_index].max())
1586
+ fit_normalized = (unconstrained_best_fit[map_index]
1587
+ / unconstrained_best_fit[map_index].max())
1135
1588
  best_fit, = ax.plot(
1136
1589
  energies, fit_normalized, 'k-', label='fit')
1137
1590
  # residual, = ax.plot(
1138
- # energies, fit.residual[map_index], 'r-',
1591
+ # energies, unconstrained_residuals[map_index], 'r-',
1139
1592
  # label='residual')
1140
1593
  ax.set(
1141
1594
  title='Unconstrained fits',
@@ -1145,8 +1598,9 @@ class StrainAnalysisProcessor(Processor):
1145
1598
  index = ax.text(
1146
1599
  0.05, 0.95, '', transform=ax.transAxes, va='top')
1147
1600
 
1148
- num_frames = int(det_nxdata.intensity.nxdata.size
1601
+ num_frame = int(det_nxdata.intensity.nxdata.size
1149
1602
  / det_nxdata.intensity.nxdata.shape[-1])
1603
+ num_digit = len(str(num_frame))
1150
1604
  if not save_figures:
1151
1605
  ani = animation.FuncAnimation(
1152
1606
  fig, animate,
@@ -1154,16 +1608,18 @@ class StrainAnalysisProcessor(Processor):
1154
1608
  / det_nxdata.intensity.nxdata.shape[-1]),
1155
1609
  interval=1000, blit=True, repeat=False)
1156
1610
  else:
1157
- for i in range(num_frames):
1611
+ for i in range(num_frame):
1158
1612
  animate(i)
1159
1613
 
1160
1614
  plt.close()
1161
1615
  plt.subplots_adjust(top=1, bottom=0, left=0, right=1)
1162
1616
 
1163
1617
  frames = []
1164
- for i in range(num_frames):
1618
+ for i in range(num_frame):
1165
1619
  frame = plt.imread(
1166
- os.path.join(path, f'frame_{i}.png'))
1620
+ os.path.join(
1621
+ path,
1622
+ f'frame_{str(i).zfill(num_digit)}.png'))
1167
1623
  im = plt.imshow(frame, animated=True)
1168
1624
  if not i:
1169
1625
  plt.imshow(frame)
@@ -1180,7 +1636,7 @@ class StrainAnalysisProcessor(Processor):
1180
1636
  path = os.path.join(
1181
1637
  outputdir,
1182
1638
  f'{detector.detector_name}_strainanalysis_'
1183
- 'unconstrained_fits.mp4')
1639
+ 'unconstrained_fits.gif')
1184
1640
  ani.save(path)
1185
1641
  plt.close()
1186
1642
 
@@ -1203,10 +1659,10 @@ class StrainAnalysisProcessor(Processor):
1203
1659
  linkdims(
1204
1660
  fit_nxdata, {'axes': 'energy', 'index': len(map_config.shape)})
1205
1661
  fit_nxdata.makelink(det_nxdata.energy)
1206
- fit_nxdata.best_fit= fit.best_fit
1207
- fit_nxdata.residuals = fit.residual
1208
- fit_nxdata.redchi = fit.redchi
1209
- fit_nxdata.success = fit.success
1662
+ fit_nxdata.best_fit= unconstrained_best_fit
1663
+ fit_nxdata.residuals = unconstrained_residuals
1664
+ fit_nxdata.redchi = unconstrained_redchi
1665
+ fit_nxdata.success = unconstrained_success
1210
1666
 
1211
1667
  # Peak-by-peak results
1212
1668
  fit_nxgroup.fit_hkl_centers = NXdata()