ChessAnalysisPipeline 0.0.13__py3-none-any.whl → 0.0.15__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
@@ -18,6 +18,8 @@ import numpy as np
18
18
  # Local modules
19
19
  from CHAP.processor import Processor
20
20
 
21
+ # Current good detector channels for the 23 channel EDD detector:
22
+ # 0, 2, 3, 5, 6, 7, 8, 10, 13, 14, 16, 17, 18, 19, 21, 22
21
23
 
22
24
  class DiffractionVolumeLengthProcessor(Processor):
23
25
  """A Processor using a steel foil raster scan to calculate the
@@ -28,8 +30,8 @@ class DiffractionVolumeLengthProcessor(Processor):
28
30
  data,
29
31
  config=None,
30
32
  save_figures=False,
31
- outputdir='.',
32
33
  inputdir='.',
34
+ outputdir='.',
33
35
  interactive=False):
34
36
  """Return the calculated value of the DV length.
35
37
 
@@ -63,6 +65,7 @@ class DiffractionVolumeLengthProcessor(Processor):
63
65
  data, 'edd.models.DiffractionVolumeLengthConfig',
64
66
  inputdir=inputdir)
65
67
  except Exception as data_exc:
68
+ self.logger.error(data_exc)
66
69
  self.logger.info('No valid DVL config in input pipeline data, '
67
70
  + 'using config parameter instead.')
68
71
  try:
@@ -127,37 +130,43 @@ class DiffractionVolumeLengthProcessor(Processor):
127
130
  # Get raw MCA data from raster scan
128
131
  mca_data = dvl_config.mca_data(detector)
129
132
 
130
- # Interactively set mask, if needed & possible.
131
- if interactive or save_figures:
132
- # Third party modules
133
- import matplotlib.pyplot as plt
133
+ # Blank out data below bin 500 (~25keV) as well as the last bin
134
+ # RV Not backward compatible with old detector
135
+ energy_mask = np.ones(detector.num_bins, dtype=np.int16)
136
+ energy_mask[:500] = 0
137
+ energy_mask[-1] = 0
138
+ mca_data = mca_data*energy_mask
134
139
 
135
- self.logger.info(
136
- 'Interactively select a mask in the matplotlib figure')
137
- fig, mask, include_bin_ranges = select_mask_1d(
140
+ # Interactively set or update mask, if needed & possible.
141
+ if interactive or save_figures:
142
+ if interactive:
143
+ self.logger.info(
144
+ 'Interactively select a mask in the matplotlib figure')
145
+ if save_figures:
146
+ filename = os.path.join(
147
+ outputdir, f'{detector.detector_name}_dvl_mask.png')
148
+ else:
149
+ filename = None
150
+ _, detector.include_bin_ranges = select_mask_1d(
138
151
  np.sum(mca_data, axis=0),
139
- x = np.arange(detector.num_bins),
152
+ x=np.arange(detector.num_bins, dtype=np.int16),
140
153
  label='Sum of MCA spectra over all scan points',
141
154
  preselected_index_ranges=detector.include_bin_ranges,
142
155
  title='Click and drag to select data range to include when '
143
156
  'measuring diffraction volume length',
144
- xlabel='MCA channel (index)',
157
+ xlabel='Uncalibrated energy (keV)',
145
158
  ylabel='MCA intensity (counts)',
146
159
  min_num_index_ranges=1,
147
- 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))
151
- if save_figures:
152
- fig.savefig(os.path.join(
153
- outputdir, f'{detector.detector_name}_dvl_mask.png'))
154
- plt.close()
155
- if detector.include_bin_ranges is None:
160
+ interactive=interactive, filename=filename)
161
+ self.logger.debug(
162
+ 'Mask selected. Including detector bin ranges: '
163
+ + str(detector.include_bin_ranges))
164
+ if not detector.include_bin_ranges:
156
165
  raise ValueError(
157
166
  'No value provided for include_bin_ranges. '
158
- + 'Provide them in the Diffraction Volume Length '
159
- + 'Measurement Configuration, or re-run the pipeline '
160
- + 'with the --interactive flag.')
167
+ 'Provide them in the Diffraction Volume Length '
168
+ 'Measurement Configuration, or re-run the pipeline '
169
+ 'with the --interactive flag.')
161
170
 
162
171
  # Reduce the raw MCA data in 3 ways:
163
172
  # 1) sum of intensities in all detector bins
@@ -184,29 +193,25 @@ class DiffractionVolumeLengthProcessor(Processor):
184
193
  fit = Fit.fit_data(masked_sum, ('constant', 'gaussian'), x=x)
185
194
 
186
195
  # Calculate / manually select diffraction volume length
187
- dvl = fit.best_values['sigma'] * detector.sigma_to_dvl_factor
196
+ dvl = fit.best_values['sigma'] * detector.sigma_to_dvl_factor \
197
+ - dvl_config.sample_thickness
198
+ detector.fit_amplitude = fit.best_values['amplitude']
199
+ detector.fit_center = scan_center + fit.best_values['center']
200
+ detector.fit_sigma = fit.best_values['sigma']
188
201
  if detector.measurement_mode == 'manual':
189
202
  if interactive:
190
- _, _, dvl_bounds = select_mask_1d(
203
+ _, dvl_bounds = select_mask_1d(
191
204
  masked_sum, x=x,
192
205
  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
206
  preselected_index_ranges=[
202
207
  (index_nearest(x, -dvl/2), index_nearest(x, dvl/2))],
203
208
  title=('Click and drag to indicate the boundary '
204
209
  'of the diffraction volume'),
205
- xlabel=(dvl_config.scanned_dim_lbl
206
- + ' (offset from scan "center")'),
210
+ xlabel=('Beam direction (offset from scan "center")'),
207
211
  ylabel='MCA intensity (normalized)',
208
212
  min_num_index_ranges=1,
209
- max_num_index_ranges=1)
213
+ max_num_index_ranges=1,
214
+ interactive=interactive)
210
215
  dvl_bounds = dvl_bounds[0]
211
216
  dvl = abs(x[dvl_bounds[1]] - x[dvl_bounds[0]])
212
217
  else:
@@ -221,27 +226,28 @@ class DiffractionVolumeLengthProcessor(Processor):
221
226
 
222
227
  fig, ax = plt.subplots()
223
228
  ax.set_title(f'Diffraction Volume ({detector.detector_name})')
224
- ax.set_xlabel(dvl_config.scanned_dim_lbl \
225
- + ' (offset from scan "center")')
229
+ ax.set_xlabel('Beam direction (offset from scan "center")')
226
230
  ax.set_ylabel('MCA intensity (normalized)')
227
231
  ax.plot(x, masked_sum, label='total (masked & normalized)')
228
232
  ax.plot(x, fit.best_fit, label='gaussian fit (to total)')
229
233
  ax.plot(x, masked_max, label='maximum (masked)')
230
234
  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})')
235
+ ax.axvspan(
236
+ fit.best_values['center']- dvl/2.,
237
+ fit.best_values['center'] + dvl/2.,
238
+ color='gray', alpha=0.5,
239
+ label=f'diffraction volume ({detector.measurement_mode})')
235
240
  ax.legend()
236
- ax.text(
237
- 0, 1,
241
+ plt.figtext(
242
+ 0.5, 0.95,
238
243
  f'Diffraction volume length: {dvl:.2f}',
239
- ha='left', va='top',
240
- #transform=ax.get_xaxis_transform())
241
- transform=ax.transAxes)
244
+ fontsize='x-large',
245
+ horizontalalignment='center',
246
+ verticalalignment='bottom')
242
247
  if save_figures:
243
- figfile = os.path.join(outputdir,
244
- f'{detector.detector_name}_dvl.png')
248
+ fig.tight_layout(rect=(0, 0, 1, 0.95))
249
+ figfile = os.path.join(
250
+ outputdir, f'{detector.detector_name}_dvl.png')
245
251
  plt.savefig(figfile)
246
252
  self.logger.info(f'Saved figure to {figfile}')
247
253
  if interactive:
@@ -249,12 +255,10 @@ class DiffractionVolumeLengthProcessor(Processor):
249
255
 
250
256
  return dvl
251
257
 
252
- class MCACeriaCalibrationProcessor(Processor):
253
- """A Processor using a CeO2 scan to obtain tuned values for the
254
- bragg diffraction angle and linear correction parameters for MCA
255
- channel energies for an EDD experimental setup.
256
- """
257
258
 
259
+ class LatticeParameterRefinementProcessor(Processor):
260
+ """Processor to get a refined estimate for a sample's lattice
261
+ parameters"""
258
262
  def process(self,
259
263
  data,
260
264
  config=None,
@@ -262,16 +266,1068 @@ class MCACeriaCalibrationProcessor(Processor):
262
266
  outputdir='.',
263
267
  inputdir='.',
264
268
  interactive=False):
265
- """Return tuned values for 2&theta and linear correction
266
- parameters for the MCA channel energies.
269
+ """Given a strain analysis configuration, return a copy
270
+ contining refined values for the materials' lattice
271
+ parameters."""
272
+ # Third party modules
273
+ from nexusformat.nexus import (
274
+ NXdata,
275
+ NXentry,
276
+ NXsubentry,
277
+ NXprocess,
278
+ NXroot,
279
+ )
280
+
281
+ # Local modules
282
+ from CHAP.common import MapProcessor
283
+
284
+ calibration_config = self.get_config(
285
+ data, 'edd.models.MCATthCalibrationConfig', inputdir=inputdir)
286
+ try:
287
+ strain_analysis_config = self.get_config(
288
+ data, 'edd.models.StrainAnalysisConfig', inputdir=inputdir)
289
+ except Exception as data_exc:
290
+ # Local modules
291
+ from CHAP.edd.models import StrainAnalysisConfig
292
+
293
+ self.logger.info('No valid strain analysis config in input '
294
+ + 'pipeline data, using config parameter instead')
295
+ try:
296
+ strain_analysis_config = StrainAnalysisConfig(
297
+ **config, inputdir=inputdir)
298
+ except Exception as dict_exc:
299
+ raise RuntimeError from dict_exc
300
+
301
+ if len(strain_analysis_config.materials) > 1:
302
+ msg = 'Not implemented for multiple materials'
303
+ self.logger.error('Not implemented for multiple materials')
304
+ raise NotImplementedError(msg)
305
+
306
+ # Collect the raw MCA data
307
+ self.logger.debug(f'Reading data ...')
308
+ mca_data = strain_analysis_config.mca_data()
309
+ self.logger.debug(f'... done')
310
+ self.logger.debug(f'mca_data.shape: {mca_data.shape}')
311
+ if mca_data.ndim == 2:
312
+ mca_data_summed = mca_data
313
+ else:
314
+ mca_data_summed = np.mean(
315
+ mca_data, axis=tuple(np.arange(1, mca_data.ndim-1)))
316
+ effective_map_shape = mca_data.shape[1:-1]
317
+ self.logger.debug(f'mca_data_summed.shape: {mca_data_summed.shape}')
318
+ self.logger.debug(f'effective_map_shape: {effective_map_shape}')
319
+
320
+ # Create the NXroot object
321
+ nxroot = NXroot()
322
+ nxentry = NXentry()
323
+ nxroot.entry = nxentry
324
+ nxentry.set_default()
325
+ nxsubentry = NXsubentry()
326
+ nxentry.nexus_output = nxsubentry
327
+ nxsubentry.attrs['schema'] = 'h5'
328
+ nxsubentry.attrs['filename'] = 'lattice_parameter_map.nxs'
329
+ map_config = strain_analysis_config.map_config
330
+ nxsubentry[map_config.title] = MapProcessor.get_nxentry(map_config)
331
+ nxsubentry[f'{map_config.title}_lat_par_refinement'] = NXprocess()
332
+ nxprocess = nxsubentry[f'{map_config.title}_lat_par_refinement']
333
+ nxprocess.strain_analysis_config = dumps(strain_analysis_config.dict())
334
+ nxprocess.calibration_config = dumps(calibration_config.dict())
335
+
336
+ lattice_parameters = []
337
+ for i, detector in enumerate(strain_analysis_config.detectors):
338
+ lattice_parameters.append(self.refine_lattice_parameters(
339
+ strain_analysis_config, calibration_config, detector,
340
+ mca_data[i], mca_data_summed[i], nxsubentry, interactive,
341
+ save_figures, outputdir))
342
+ lattice_parameters_mean = np.asarray(lattice_parameters).mean(axis=0)
343
+ self.logger.debug(
344
+ 'Lattice parameters refinement averaged over all detectors: '
345
+ f'{lattice_parameters_mean}')
346
+ strain_analysis_config.materials[0].lattice_parameters = [
347
+ float(v) for v in lattice_parameters_mean]
348
+ nxprocess.lattice_parameters = lattice_parameters_mean
349
+
350
+ nxentry.lat_par_output = NXsubentry()
351
+ nxentry.lat_par_output.attrs['schema'] = 'yaml'
352
+ nxentry.lat_par_output.attrs['filename'] = 'lattice_parameters.yaml'
353
+ nxentry.lat_par_output.data = dumps(strain_analysis_config.dict())
354
+
355
+ return nxroot
356
+
357
+ def refine_lattice_parameters(
358
+ self, strain_analysis_config, calibration_config, detector,
359
+ mca_data, mca_data_summed, nxsubentry, interactive, save_figures,
360
+ outputdir):
361
+ """Return refined values for the lattice parameters of the
362
+ materials indicated in `strain_analysis_config`. Method: given
363
+ a scan of a material, fit the peaks of each MCA spectrum for a
364
+ given detector. Based on those fitted peak locations,
365
+ calculate the lattice parameters that would produce them.
366
+ Return the averaged value of the calculated lattice parameters
367
+ across all spectra.
368
+
369
+ :param strain_analysis_config: Strain analysis configuration
370
+ :type strain_analysis_config:
371
+ CHAP.edd.models.StrainAnalysisConfig
372
+ :param calibration_config: Energy calibration configuration.
373
+ :type calibration_config: edd.models.MCATthCalibrationConfig
374
+ :param detector: A single MCA detector element configuration.
375
+ :type detector: CHAP.edd.models.MCAElementStrainAnalysisConfig
376
+ :param mca_data: Raw specta for the current MCA detector.
377
+ :type mca_data: np.ndarray
378
+ :param mca_data_summed: Raw specta for the current MCA detector
379
+ summed over all data point.
380
+ :type mca_data_summed: np.ndarray
381
+ :param nxsubentry: NeXus subentry to store the detailed refined
382
+ lattice parameters for each detector.
383
+ :type nxsubentry: nexusformat.nexus.NXprocess
384
+ :param interactive: Boolean to indicate whether interactive
385
+ matplotlib figures should be presented
386
+ :type interactive: bool
387
+ :param save_figures: Boolean to indicate whether figures
388
+ indicating the selection should be saved
389
+ :type save_figures: bool
390
+ :param outputdir: Where to save figures (if `save_figures` is
391
+ `True`)
392
+ :type outputdir: str
393
+ :returns: List of refined lattice parameters for materials in
394
+ `strain_analysis_config`
395
+ :rtype: list[numpy.ndarray]
396
+ """
397
+ # Third party modules
398
+ from nexusformat.nexus import (
399
+ NXcollection,
400
+ NXdata,
401
+ NXdetector,
402
+ NXfield,
403
+ )
404
+ from scipy.constants import physical_constants
405
+
406
+ # Local modules
407
+ from CHAP.edd.utils import (
408
+ get_peak_locations,
409
+ get_spectra_fits,
410
+ get_unique_hkls_ds,
411
+ )
412
+
413
+ def linkdims(nxgroup, field_dims=[]):
414
+ if isinstance(field_dims, dict):
415
+ field_dims = [field_dims]
416
+ if map_config.map_type == 'structured':
417
+ axes = deepcopy(map_config.dims)
418
+ for dims in field_dims:
419
+ axes.append(dims['axes'])
420
+ else:
421
+ axes = ['map_index']
422
+ for dims in field_dims:
423
+ axes.append(dims['axes'])
424
+ nxgroup.attrs[f'map_index_indices'] = 0
425
+ for dim in map_config.dims:
426
+ nxgroup.makelink(nxsubentry[map_config.title].data[dim])
427
+ if f'{dim}_indices' in nxsubentry[map_config.title].data.attrs:
428
+ nxgroup.attrs[f'{dim}_indices'] = \
429
+ nxsubentry[map_config.title].data.attrs[
430
+ f'{dim}_indices']
431
+ nxgroup.attrs['axes'] = axes
432
+ for dims in field_dims:
433
+ nxgroup.attrs[f'{dims["axes"]}_indices'] = dims['index']
434
+
435
+ # Get and add the calibration info to the detector
436
+ calibration = [
437
+ d for d in calibration_config.detectors \
438
+ if d.detector_name == detector.detector_name][0]
439
+ detector.add_calibration(calibration)
440
+
441
+ # Get the MCA bin energies
442
+ mca_bin_energies = detector.energies
443
+
444
+ # Blank out data below 25 keV as well as the last bin
445
+ energy_mask = np.where(mca_bin_energies >= 25.0, 1, 0)
446
+ energy_mask[-1] = 0
447
+
448
+ # Subtract the baseline
449
+ if detector.baseline:
450
+ # Local modules
451
+ from CHAP.edd.models import BaselineConfig
452
+ from CHAP.common.processor import ConstructBaseline
453
+
454
+ if isinstance(detector.baseline, bool):
455
+ detector.baseline = BaselineConfig()
456
+ if save_figures:
457
+ filename = os.path.join(
458
+ outputdir,
459
+ f'{detector.detector_name}_lat_param_refinement_'
460
+ 'baseline.png')
461
+ else:
462
+ filename = None
463
+ baseline, baseline_config = \
464
+ ConstructBaseline.construct_baseline(
465
+ mca_data_summed, mask=energy_mask,
466
+ tol=detector.baseline.tol, lam=detector.baseline.lam,
467
+ max_iter=detector.baseline.max_iter,
468
+ title=
469
+ f'Baseline for detector {detector.detector_name}',
470
+ xlabel='Energy (keV)', ylabel='Intensity (counts)',
471
+ interactive=interactive, filename=filename)
472
+
473
+ mca_data_summed -= baseline
474
+ detector.baseline.lam = baseline_config['lambda']
475
+ detector.baseline.attrs['num_iter'] = \
476
+ baseline_config['num_iter']
477
+ detector.baseline.attrs['error'] = baseline_config['error']
478
+
479
+ # Get the unique HKLs and lattice spacings for the strain
480
+ # analysis materials
481
+ hkls, ds = get_unique_hkls_ds(
482
+ strain_analysis_config.materials,
483
+ tth_tol=detector.hkl_tth_tol,
484
+ tth_max=detector.tth_max)
485
+
486
+ if interactive or save_figures:
487
+ # Local modules
488
+ from CHAP.edd.utils import (
489
+ select_material_params,
490
+ select_mask_and_hkls,
491
+ )
492
+
493
+ # Interactively adjust the initial material parameters
494
+ tth = detector.tth_calibrated
495
+ if save_figures:
496
+ filename = os.path.join(
497
+ outputdir,
498
+ f'{detector.detector_name}_lat_param_refinement_'
499
+ 'initial_material_config.png')
500
+ else:
501
+ filename = None
502
+ strain_analysis_config.materials = select_material_params(
503
+ mca_bin_energies, mca_data_summed*energy_mask, tth,
504
+ preselected_materials=strain_analysis_config.materials,
505
+ label='Sum of all spectra in the map',
506
+ interactive=interactive, filename=filename)
507
+ self.logger.debug(
508
+ f'materials: {strain_analysis_config.materials}')
509
+
510
+ # Interactively adjust the mask and HKLs used in the
511
+ # lattice parameter refinement
512
+ if save_figures:
513
+ filename = os.path.join(
514
+ outputdir,
515
+ f'{detector.detector_name}_lat_param_refinement_'
516
+ 'fit_mask_hkls.png')
517
+ else:
518
+ filename = None
519
+ include_bin_ranges, hkl_indices = \
520
+ select_mask_and_hkls(
521
+ mca_bin_energies, mca_data_summed*energy_mask,
522
+ hkls, ds, detector.tth_calibrated,
523
+ preselected_bin_ranges=detector.include_bin_ranges,
524
+ preselected_hkl_indices=detector.hkl_indices,
525
+ detector_name=detector.detector_name,
526
+ ref_map=mca_data*energy_mask,
527
+ calibration_bin_ranges=detector.calibration_bin_ranges,
528
+ label='Sum of all spectra in the map',
529
+ interactive=interactive, filename=filename)
530
+ detector.include_energy_ranges = \
531
+ detector.get_include_energy_ranges(include_bin_ranges)
532
+ detector.hkl_indices = hkl_indices
533
+ self.logger.debug(
534
+ f'include_energy_ranges for detector {detector.detector_name}:'
535
+ f' {detector.include_energy_ranges}')
536
+ self.logger.debug(
537
+ f'hkl_indices for detector {detector.detector_name}:'
538
+ f' {detector.hkl_indices}')
539
+ if not detector.include_energy_ranges:
540
+ raise ValueError(
541
+ 'No value provided for include_energy_ranges. '
542
+ 'Provide them in the MCA Tth Calibration Configuration, '
543
+ 'or re-run the pipeline with the --interactive flag.')
544
+ if not detector.hkl_indices:
545
+ raise ValueError(
546
+ 'No value provided for hkl_indices. Provide them in '
547
+ 'the detector\'s MCA Tth Calibration Configuration, or'
548
+ ' re-run the pipeline with the --interactive flag.')
549
+
550
+ effective_map_shape = mca_data.shape[:-1]
551
+ mask = detector.mca_mask()
552
+ energies = mca_bin_energies[mask]
553
+ intensities = np.empty(
554
+ (*effective_map_shape, len(energies)), dtype=np.float64)
555
+ for map_index in np.ndindex(effective_map_shape):
556
+ if detector.baseline:
557
+ intensities[map_index] = \
558
+ (mca_data[map_index]-baseline).astype(
559
+ np.float64)[mask]
560
+ else:
561
+ intensities[map_index] = \
562
+ mca_data[map_index].astype(np.float64)[mask]
563
+ mean_intensity = np.mean(
564
+ intensities, axis=tuple(range(len(effective_map_shape))))
565
+ hkls_fit = np.asarray([hkls[i] for i in detector.hkl_indices])
566
+ ds_fit = np.asarray([ds[i] for i in detector.hkl_indices])
567
+ Rs = np.sqrt(np.sum(hkls_fit**2, 1))
568
+ peak_locations = get_peak_locations(
569
+ ds_fit, detector.tth_calibrated)
570
+
571
+ map_config = strain_analysis_config.map_config
572
+ nxprocess = nxsubentry[f'{map_config.title}_lat_par_refinement']
573
+ nxprocess[detector.detector_name] = NXdetector()
574
+ nxdetector = nxprocess[detector.detector_name]
575
+ nxdetector.local_name = detector.detector_name
576
+ nxdetector.detector_config = dumps(detector.dict())
577
+ nxdetector.data = NXdata()
578
+ det_nxdata = nxdetector.data
579
+ linkdims(
580
+ det_nxdata,
581
+ {'axes': 'energy', 'index': len(effective_map_shape)})
582
+ det_nxdata.energy = NXfield(value=energies, attrs={'units': 'keV'})
583
+ det_nxdata.intensity = NXfield(
584
+ value=intensities,
585
+ shape=(*effective_map_shape, len(energies)),
586
+ dtype=np.float64,
587
+ attrs={'units': 'counts'})
588
+ det_nxdata.mean_intensity = mean_intensity
589
+
590
+ # Get the interplanar spacings measured for each fit HKL peak
591
+ # at every point in the map to get the refined estimate
592
+ # for the material's lattice parameter
593
+ (uniform_fit_centers, uniform_fit_centers_errors,
594
+ uniform_fit_amplitudes, uniform_fit_amplitudes_errors,
595
+ uniform_fit_sigmas, uniform_fit_sigmas_errors,
596
+ uniform_best_fit, uniform_residuals,
597
+ uniform_redchi, uniform_success,
598
+ unconstrained_fit_centers, unconstrained_fit_centers_errors,
599
+ unconstrained_fit_amplitudes, unconstrained_fit_amplitudes_errors,
600
+ unconstrained_fit_sigmas, unconstrained_fit_sigmas_errors,
601
+ unconstrained_best_fit, unconstrained_residuals,
602
+ unconstrained_redchi, unconstrained_success) = \
603
+ get_spectra_fits(
604
+ intensities, energies, peak_locations, detector)
605
+ Rs_map = Rs.repeat(np.prod(effective_map_shape))
606
+ d_uniform = get_peak_locations(
607
+ np.asarray(uniform_fit_centers), detector.tth_calibrated)
608
+ a_uniform = (Rs_map * d_uniform.flatten()).reshape(d_uniform.shape)
609
+ a_uniform = a_uniform.mean(axis=0)
610
+ a_uniform_mean = float(a_uniform.mean())
611
+ d_unconstrained = get_peak_locations(
612
+ unconstrained_fit_centers, detector.tth_calibrated)
613
+ a_unconstrained = (Rs_map * d_unconstrained.flatten()).reshape(d_unconstrained.shape)
614
+ a_unconstrained = np.moveaxis(a_unconstrained, 0, -1)
615
+ a_unconstrained_mean = float(a_unconstrained.mean())
616
+ self.logger.warning(
617
+ 'Lattice parameter refinement assumes cubic lattice')
618
+ self.logger.info(
619
+ f'Refined lattice parameter from uniform fit: {a_uniform_mean}')
620
+ self.logger.info(
621
+ 'Refined lattice parameter from unconstrained fit: '
622
+ f'{a_unconstrained_mean}')
623
+ nxdetector.lat_pars = NXcollection()
624
+ nxdetector.lat_pars.uniform = NXdata()
625
+ nxdata = nxdetector.lat_pars.uniform
626
+ nxdata.nxsignal = NXfield(
627
+ value=a_uniform, name='a_uniform', attrs={'units': r'\AA'})
628
+ linkdims(nxdata)
629
+ nxdetector.lat_pars.a_uniform_mean = float(a_uniform.mean())
630
+ nxdetector.lat_pars.unconstrained = NXdata()
631
+ nxdata = nxdetector.lat_pars.unconstrained
632
+ nxdata.nxsignal = NXfield(
633
+ value=a_unconstrained, name='a_unconstrained',
634
+ attrs={'units': r'\AA'})
635
+ nxdata.hkl_index = detector.hkl_indices
636
+ linkdims(
637
+ nxdata,
638
+ {'axes': 'hkl_index', 'index': len(effective_map_shape)})
639
+ nxdetector.lat_pars.a_unconstrained_mean = a_unconstrained_mean
640
+
641
+ # Get the interplanar spacings measured for each fit HKL peak
642
+ # at the spectrum averaged over every point in the map to get
643
+ # the refined estimate for the material's lattice parameter
644
+ (uniform_fit_centers, uniform_fit_centers_errors,
645
+ uniform_fit_amplitudes, uniform_fit_amplitudes_errors,
646
+ uniform_fit_sigmas, uniform_fit_sigmas_errors,
647
+ uniform_best_fit, uniform_residuals,
648
+ uniform_redchi, uniform_success,
649
+ unconstrained_fit_centers, unconstrained_fit_centers_errors,
650
+ unconstrained_fit_amplitudes, unconstrained_fit_amplitudes_errors,
651
+ unconstrained_fit_sigmas, unconstrained_fit_sigmas_errors,
652
+ unconstrained_best_fit, unconstrained_residuals,
653
+ unconstrained_redchi, unconstrained_success) = \
654
+ get_spectra_fits(
655
+ mean_intensity, energies, peak_locations, detector)
656
+ d_uniform = get_peak_locations(
657
+ np.asarray(uniform_fit_centers), detector.tth_calibrated)
658
+ d_unconstrained = get_peak_locations(
659
+ np.asarray(unconstrained_fit_centers), detector.tth_calibrated)
660
+ a_uniform = float((Rs * d_uniform).mean())
661
+ a_unconstrained = (Rs * d_unconstrained)
662
+ self.logger.warning(
663
+ 'Lattice parameter refinement assumes cubic lattice')
664
+ self.logger.info(
665
+ 'Refined lattice parameter from uniform fit over averaged '
666
+ f'spectrum: {a_uniform}')
667
+ self.logger.info(
668
+ 'Refined lattice parameter from unconstrained fit over averaged '
669
+ f'spectrum: {a_unconstrained}')
670
+ nxdetector.lat_pars_mean_intensity = NXcollection()
671
+ nxdetector.lat_pars_mean_intensity.a_uniform = a_uniform
672
+ nxdetector.lat_pars_mean_intensity.a_unconstrained = a_unconstrained
673
+ nxdetector.lat_pars_mean_intensity.a_unconstrained_mean = \
674
+ float(a_unconstrained.mean())
675
+
676
+ if interactive or save_figures:
677
+ # Third party modules
678
+ import matplotlib.pyplot as plt
679
+
680
+ fig, ax = plt.subplots(figsize=(11, 8.5))
681
+ ax.set_title(f'Detector {detector.detector_name}: '
682
+ 'Lattice Parameter Refinement')
683
+ ax.set_xlabel('Detector energy (keV)')
684
+ ax.set_ylabel('Mean intensity (a.u.)')
685
+ ax.plot(energies, mean_intensity, 'k.', label='MCA data')
686
+ ax.plot(energies, uniform_best_fit, 'r', label='Best uniform fit')
687
+ ax.plot(
688
+ energies, unconstrained_best_fit, 'b',
689
+ label='Best unconstrained fit')
690
+ ax.legend()
691
+ for i, loc in enumerate(peak_locations):
692
+ ax.axvline(loc, c='k', ls='--')
693
+ ax.text(loc, 1, str(hkls_fit[i])[1:-1],
694
+ ha='right', va='top', rotation=90,
695
+ transform=ax.get_xaxis_transform())
696
+ if save_figures:
697
+ fig.tight_layout()#rect=(0, 0, 1, 0.95))
698
+ figfile = os.path.join(
699
+ outputdir, f'{detector.detector_name}_lat_param_fits.png')
700
+ plt.savefig(figfile)
701
+ self.logger.info(f'Saved figure to {figfile}')
702
+ if interactive:
703
+ plt.show()
704
+
705
+ return [
706
+ a_uniform, a_uniform, a_uniform, 90., 90., 90.]
707
+
708
+
709
+ class MCAEnergyCalibrationProcessor(Processor):
710
+ """Processor to return parameters for linearly transforming MCA
711
+ channel indices to energies (in keV). Procedure: provide a
712
+ spectrum from the MCA element to be calibrated and the theoretical
713
+ location of at least one peak present in that spectrum (peak
714
+ locations must be given in keV). It is strongly recommended to use
715
+ the location of fluorescence peaks whenever possible, _not_
716
+ diffraction peaks, as this Processor does not account for
717
+ 2&theta."""
718
+ def process(self,
719
+ data,
720
+ config=None,
721
+ centers_range=20,
722
+ fwhm_min=None,
723
+ fwhm_max=None,
724
+ max_energy_kev=200.0,
725
+ background=None,
726
+ baseline=False,
727
+ save_figures=False,
728
+ interactive=False,
729
+ inputdir='.',
730
+ outputdir='.'):
731
+ """For each detector in the `MCAEnergyCalibrationConfig`
732
+ provided with `data`, fit the specified peaks in the MCA
733
+ spectrum specified. Using the difference between the provided
734
+ peak locations and the fit centers of those peaks, compute
735
+ the correction coefficients to convert uncalibrated MCA
736
+ channel energies to calibrated channel energies. For each
737
+ detector, set `energy_calibration_coeffs` in the calibration
738
+ config provided to these values and return the updated
739
+ configuration.
740
+
741
+ :param data: An energy Calibration configuration.
742
+ :type data: PipelineData
743
+ :param config: Initialization parameters for an instance of
744
+ CHAP.edd.models.MCAEnergyCalibrationConfig, defaults to
745
+ `None`.
746
+ :type config: dict, optional
747
+ :param centers_range: Set boundaries on the peak centers in
748
+ MCA channels when performing the fit. The min/max
749
+ possible values for the peak centers will be the initial
750
+ values ± `centers_range`. Defaults to `20`.
751
+ :type centers_range: int, optional
752
+ :param fwhm_min: Lower bound on the peak FWHM in MCA channels
753
+ when performing the fit, defaults to `None`.
754
+ :type fwhm_min: float, optional
755
+ :param fwhm_max: Lower bound on the peak FWHM in MCA channels
756
+ when performing the fit, defaults to `None`.
757
+ :type fwhm_max: float, optional
758
+ :param max_energy_kev: Maximum channel energy of the MCA in
759
+ keV, defaults to 200.0.
760
+ :type max_energy_kev: float, optional
761
+ :param background: Background model for peak fitting.
762
+ :type background: str, list[str], optional
763
+ :param baseline: Automated baseline subtraction configuration,
764
+ defaults to `False`.
765
+ :type baseline: bool, BaselineConfig, optional
766
+ :param save_figures: Save .pngs of plots for checking inputs &
767
+ outputs of this Processor, defaults to `False`.
768
+ :type save_figures: bool, optional
769
+ :param interactive: Allows for user interactions, defaults to
770
+ `False`.
771
+ :type interactive: bool, optional
772
+ :param inputdir: Input directory, used only if files in the
773
+ input configuration are not absolute paths,
774
+ defaults to `'.'`.
775
+ :type inputdir: str, optional
776
+ :param outputdir: Directory to which any output figures will
777
+ be saved, defaults to `'.'`.
778
+ :type outputdir: str, optional
779
+ :returns: Dictionary representing the energy-calibrated
780
+ version of the calibrated configuration.
781
+ :rtype: dict
782
+ """
783
+ # Local modules
784
+ from CHAP.edd.models import BaselineConfig
785
+ from CHAP.utils.general import (
786
+ is_int,
787
+ is_num,
788
+ is_str_series,
789
+ )
790
+
791
+ # Load the validated energy calibration configuration
792
+ try:
793
+ calibration_config = self.get_config(
794
+ data, 'edd.models.MCAEnergyCalibrationConfig',
795
+ inputdir=inputdir)
796
+ except Exception as data_exc:
797
+ self.logger.info('No valid calibration config in input pipeline '
798
+ 'data, using config parameter instead.')
799
+ try:
800
+ # Local modules
801
+ from CHAP.edd.models import MCAEnergyCalibrationConfig
802
+
803
+ calibration_config = MCAEnergyCalibrationConfig(
804
+ **config, inputdir=inputdir)
805
+ except Exception as dict_exc:
806
+ raise RuntimeError from dict_exc
807
+
808
+ # Validate the fit index range
809
+ if calibration_config.fit_index_ranges is None and not interactive:
810
+ raise RuntimeError(
811
+ 'If `fit_index_ranges` is not explicitly provided, '
812
+ + self.__class__.__name__
813
+ + ' must be run with `interactive=True`.')
814
+
815
+ # Validate the optional inputs
816
+ if not is_int(centers_range, gt=0, log=False):
817
+ raise RuntimeError(
818
+ f'Invalid centers_range parameter ({centers_range})')
819
+ if fwhm_min is not None and not is_int(fwhm_min, gt=0, log=False):
820
+ raise RuntimeError(f'Invalid fwhm_min parameter ({fwhm_min})')
821
+ if fwhm_max is not None and not is_int(fwhm_max, gt=0, log=False):
822
+ raise RuntimeError(f'Invalid fwhm_max parameter ({fwhm_max})')
823
+ if not is_num(max_energy_kev, gt=0, log=False):
824
+ raise RuntimeError(
825
+ f'Invalid max_energy_kev parameter ({max_energy_kev})')
826
+ if background is not None:
827
+ if isinstance(background, str):
828
+ background = [background]
829
+ elif not is_str_series(background, log=False):
830
+ raise RuntimeError(
831
+ f'Invalid background parameter ({background})')
832
+ if isinstance(baseline, bool):
833
+ if baseline:
834
+ baseline = BaselineConfig()
835
+ else:
836
+ try:
837
+ baseline = BaselineConfig(**baseline)
838
+ except Exception as dict_exc:
839
+ raise RuntimeError from dict_exc
840
+
841
+ # Calibrate detector channel energies based on fluorescence peaks.
842
+ for detector in calibration_config.detectors:
843
+ if background is not None:
844
+ detector.background = background.copy()
845
+ if baseline:
846
+ detector.baseline = baseline.copy()
847
+ detector.energy_calibration_coeffs = self.calibrate(
848
+ calibration_config, detector, centers_range, fwhm_min,
849
+ fwhm_max, max_energy_kev, save_figures, interactive, outputdir)
850
+
851
+ return calibration_config.dict()
852
+
853
+ def calibrate(self, calibration_config, detector, centers_range,
854
+ fwhm_min, fwhm_max, max_energy_kev, save_figures, interactive,
855
+ outputdir):
856
+ """Return energy_calibration_coeffs (a, b, and c) for
857
+ quadratically converting the current detector's MCA channels
858
+ to bin energies.
859
+
860
+ :param calibration_config: Energy calibration configuration.
861
+ :type calibration_config: MCAEnergyCalibrationConfig
862
+ :param detector: Configuration of the current detector.
863
+ :type detector: MCAElementCalibrationConfig
864
+ :param centers_range: Set boundaries on the peak centers in
865
+ MCA channels when performing the fit. The min/max
866
+ possible values for the peak centers will be the initial
867
+ values ± `centers_range`. Defaults to `20`.
868
+ :type centers_range: int, optional
869
+ :param fwhm_min: Lower bound on the peak FWHM in MCA channels
870
+ when performing the fit, defaults to `None`.
871
+ :type fwhm_min: float, optional
872
+ :param fwhm_max: Lower bound on the peak FWHM in MCA channels
873
+ when performing the fit, defaults to `None`.
874
+ :type fwhm_max: float, optional
875
+ :param max_energy_kev: Maximum channel energy of the MCA in
876
+ keV, defaults to 200.0.
877
+ :type max_energy_kev: float
878
+ :param save_figures: Save .pngs of plots for checking inputs &
879
+ outputs of this Processor.
880
+ :type save_figures: bool
881
+ :param interactive: Allows for user interactions.
882
+ :type interactive: bool
883
+ :param outputdir: Directory to which any output figures will
884
+ be saved.
885
+ :type outputdir: str
886
+ :returns: Slope and intercept for linearly correcting the
887
+ detector's MCA channels to bin energies.
888
+ :rtype: tuple[float, float]
889
+ """
890
+ # Third party modules
891
+ from nexusformat.nexus import (
892
+ NXdata,
893
+ NXfield,
894
+ )
895
+
896
+ # Local modules
897
+ from CHAP.utils.fit import FitProcessor
898
+ from CHAP.utils.general import (
899
+ index_nearest,
900
+ index_nearest_down,
901
+ index_nearest_up,
902
+ select_mask_1d,
903
+ )
904
+
905
+ self.logger.info(f'Calibrating detector {detector.detector_name}')
906
+
907
+ spectrum = calibration_config.mca_data(detector)
908
+ uncalibrated_energies = np.linspace(
909
+ 0, max_energy_kev, detector.num_bins)
910
+ bins = np.arange(detector.num_bins, dtype=np.int16)
911
+
912
+ # Blank out data below 25keV as well as the last bin
913
+ energy_mask = np.where(uncalibrated_energies >= 25.0, 1, 0)
914
+ energy_mask[-1] = 0
915
+ spectrum = spectrum*energy_mask
916
+
917
+ # Subtract the baseline
918
+ if detector.baseline:
919
+ # Local modules
920
+ from CHAP.common.processor import ConstructBaseline
921
+
922
+ if save_figures:
923
+ filename = os.path.join(outputdir,
924
+ f'{detector.detector_name}_energy_'
925
+ 'calibration_baseline.png')
926
+ else:
927
+ filename = None
928
+ baseline, baseline_config = ConstructBaseline.construct_baseline(
929
+ spectrum, mask=energy_mask, tol=detector.baseline.tol,
930
+ lam=detector.baseline.lam, max_iter=detector.baseline.max_iter,
931
+ title=f'Baseline for detector {detector.detector_name}',
932
+ xlabel='Energy (keV)', ylabel='Intensity (counts)',
933
+ interactive=interactive, filename=filename)
934
+
935
+ spectrum -= baseline
936
+ detector.baseline.lam = baseline_config['lambda']
937
+ detector.baseline.attrs['num_iter'] = baseline_config['num_iter']
938
+ detector.baseline.attrs['error'] = baseline_config['error']
939
+
940
+ # Select the mask/detector channel ranges for fitting
941
+ if save_figures:
942
+ filename = os.path.join(
943
+ outputdir,
944
+ f'{detector.detector_name}_mca_energy_calibration_mask.png')
945
+ else:
946
+ filename = None
947
+ mask, fit_index_ranges = select_mask_1d(
948
+ spectrum, x=bins,
949
+ preselected_index_ranges=calibration_config.fit_index_ranges,
950
+ xlabel='Detector channel', ylabel='Intensity',
951
+ min_num_index_ranges=1, interactive=interactive,
952
+ filename=filename)
953
+ self.logger.debug(
954
+ f'Selected index ranges to fit: {fit_index_ranges}')
955
+
956
+ # Get the intial peak positions for fitting
957
+ max_peak_energy = calibration_config.peak_energies[
958
+ calibration_config.max_peak_index]
959
+ peak_energies = list(np.sort(calibration_config.peak_energies))
960
+ max_peak_index = peak_energies.index(max_peak_energy)
961
+ if save_figures:
962
+ filename = os.path.join(
963
+ outputdir,
964
+ f'{detector.detector_name}'
965
+ '_mca_energy_calibration_initial_peak_positions.png')
966
+ else:
967
+ filename = None
968
+ input_indices = [index_nearest(uncalibrated_energies, energy)
969
+ for energy in peak_energies]
970
+ initial_peak_indices = self._get_initial_peak_positions(
971
+ spectrum*np.asarray(mask).astype(np.int32), fit_index_ranges,
972
+ input_indices, max_peak_index, interactive, filename,
973
+ detector.detector_name)
974
+
975
+ # Construct the fit model
976
+ models = []
977
+ if detector.background is not None:
978
+ if isinstance(detector.background, str):
979
+ models.append(
980
+ {'model': detector.background, 'prefix': 'bkgd_'})
981
+ else:
982
+ for model in detector.background:
983
+ models.append({'model': model, 'prefix': f'{model}_'})
984
+ models.append(
985
+ {'model': 'multipeak', 'centers': initial_peak_indices,
986
+ 'centers_range': centers_range, 'fwhm_min': fwhm_min,
987
+ 'fwhm_max': fwhm_max})
988
+ self.logger.debug('Fitting spectrum')
989
+ fit = FitProcessor()
990
+ spectrum_fit = fit.process(
991
+ NXdata(NXfield(spectrum[mask], 'y'), NXfield(bins[mask], 'x')),
992
+ {'models': models, 'method': 'trf'})
993
+
994
+ fit_peak_indices = sorted([
995
+ spectrum_fit.best_values[f'peak{i+1}_center']
996
+ for i in range(len(initial_peak_indices))])
997
+ self.logger.debug(f'Fit peak centers: {fit_peak_indices}')
998
+
999
+ #RV for now stick with a linear energy correction
1000
+ fit = FitProcessor()
1001
+ energy_fit = fit.process(
1002
+ NXdata(
1003
+ NXfield(peak_energies, 'y'),
1004
+ NXfield(fit_peak_indices, 'x')),
1005
+ {'models': [{'model': 'linear'}]})
1006
+ a = 0.0
1007
+ b = float(energy_fit.best_values['slope'])
1008
+ c = float(energy_fit.best_values['intercept'])
1009
+
1010
+ # Reference plot to see fit results:
1011
+ if interactive or save_figures:
1012
+ # Third part modules
1013
+ import matplotlib.pyplot as plt
1014
+
1015
+ fig, axs = plt.subplots(1, 2, figsize=(11, 4.25))
1016
+ fig.suptitle(
1017
+ f'Detector {detector.detector_name} Energy Calibration')
1018
+ # Left plot: raw MCA data & best fit of peaks
1019
+ axs[0].set_title(f'MCA Spectrum Peak Fit')
1020
+ axs[0].set_xlabel('Detector channel')
1021
+ axs[0].set_ylabel('Intensity (a.u)')
1022
+ axs[0].plot(bins[mask], spectrum[mask], 'b.', label='MCA data')
1023
+ axs[0].plot(
1024
+ bins[mask], spectrum_fit.best_fit, 'r', label='Best fit')
1025
+ axs[0].legend()
1026
+ # Right plot: linear fit of theoretical peak energies vs
1027
+ # fit peak locations
1028
+ axs[1].set_title(
1029
+ 'Channel Energies vs. Channel Indices')
1030
+ axs[1].set_xlabel('Detector channel')
1031
+ axs[1].set_ylabel('Channel energy (keV)')
1032
+ axs[1].plot(fit_peak_indices, peak_energies,
1033
+ c='b', marker='o', ms=6, mfc='none', ls='',
1034
+ label='Initial peak positions')
1035
+ axs[1].plot(fit_peak_indices, energy_fit.best_fit,
1036
+ c='k', marker='+', ms=6, ls='',
1037
+ label='Fitted peak positions')
1038
+ axs[1].plot(bins[mask], b*bins[mask] + c, 'r',
1039
+ label='Best linear fit')
1040
+ axs[1].legend()
1041
+ # Add text box showing computed values of linear E
1042
+ # correction parameters
1043
+ axs[1].text(
1044
+ 0.98, 0.02,
1045
+ 'Calibrated values:\n\n'
1046
+ f'Linear coefficient:\n {b:.5f} $keV$/channel\n\n'
1047
+ f'Constant offset:\n {c:.5f} $keV$',
1048
+ ha='right', va='bottom', ma='left',
1049
+ transform=axs[1].transAxes,
1050
+ bbox=dict(boxstyle='round',
1051
+ ec=(1., 0.5, 0.5),
1052
+ fc=(1., 0.8, 0.8, 0.8)))
1053
+
1054
+ fig.tight_layout()
1055
+
1056
+ if save_figures:
1057
+ figfile = os.path.join(
1058
+ outputdir,
1059
+ f'{detector.detector_name}_energy_calibration_fit.png')
1060
+ plt.savefig(figfile)
1061
+ self.logger.info(f'Saved figure to {figfile}')
1062
+ if interactive:
1063
+ plt.show()
1064
+
1065
+ return [a, b, c]
1066
+
1067
+ def _get_initial_peak_positions(
1068
+ self, y, index_ranges, input_indices, input_max_peak_index,
1069
+ interactive, filename, detector_name, reset_flag=0):
1070
+ # Third party modules
1071
+ import matplotlib.pyplot as plt
1072
+ from matplotlib.widgets import TextBox, Button
1073
+
1074
+ def change_fig_title(title):
1075
+ if fig_title:
1076
+ fig_title[0].remove()
1077
+ fig_title.pop()
1078
+ fig_title.append(plt.figtext(*title_pos, title, **title_props))
1079
+
1080
+ def change_error_text(error=''):
1081
+ if error_texts:
1082
+ error_texts[0].remove()
1083
+ error_texts.pop()
1084
+ error_texts.append(plt.figtext(*error_pos, error, **error_props))
1085
+
1086
+ def reset(event):
1087
+ """Callback function for the "Reset" button."""
1088
+ peak_indices.clear()
1089
+ plt.close()
1090
+
1091
+ def confirm(event):
1092
+ """Callback function for the "Confirm" button."""
1093
+ if error_texts:
1094
+ error_texts[0].remove()
1095
+ error_texts.pop()
1096
+ plt.close()
1097
+
1098
+ def find_peaks(min_height=0.05, min_width=5, tolerance=0.05):
1099
+ # Third party modules
1100
+ from scipy.signal import find_peaks as find_peaks_scipy
1101
+
1102
+ # Find peaks
1103
+ peaks = find_peaks_scipy(y, height=min_height,
1104
+ prominence=0.05*y.max(), width=min_width)
1105
+ available_peak_indices = list(peaks[0])
1106
+ max_peak_index = np.asarray(peaks[1]).argmax()
1107
+ ratio = (available_peak_indices[max_peak_index]
1108
+ / input_indices[input_max_peak_index])
1109
+ peak_indices = [-1]*num_peak
1110
+ peak_indices[input_max_peak_index] = \
1111
+ available_peak_indices[max_peak_index]
1112
+ available_peak_indices.pop(max_peak_index)
1113
+ for i, input_index in enumerate(input_indices):
1114
+ if i != input_max_peak_index:
1115
+ index_guess = int(input_index * ratio)
1116
+ for index in available_peak_indices.copy():
1117
+ if abs(index_guess-index) < tolerance*index:
1118
+ index_guess = index
1119
+ available_peak_indices.remove(index)
1120
+ break
1121
+ peak_indices[i] = index_guess
1122
+ return peak_indices
1123
+
1124
+ def select_peaks():
1125
+ """Manually select initial peak indices."""
1126
+ peak_indices = []
1127
+ while len(set(peak_indices)) < num_peak:
1128
+ change_fig_title(f'Select {num_peak} peak positions')
1129
+ peak_indices = [
1130
+ int(pt[0]) for pt in plt.ginput(num_peak, timeout=15)]
1131
+ if len(set(peak_indices)) < num_peak:
1132
+ error_text = f'Choose {num_peak} unique position'
1133
+ peak_indices.clear()
1134
+ outside_indices = []
1135
+ for index in peak_indices:
1136
+ if not any(True if low <= index <= upp else False
1137
+ for low, upp in index_ranges):
1138
+ outside_indices.append(index)
1139
+ if len(outside_indices) == 1:
1140
+ error_text = \
1141
+ f'Index {outside_indices[0]} outside of selection ' \
1142
+ f'region ({index_ranges}), try again'
1143
+ peak_indices.clear()
1144
+ elif outside_indices:
1145
+ error_text = \
1146
+ f'Indices {outside_indices} outside of selection ' \
1147
+ 'region, try again'
1148
+ peak_indices.clear()
1149
+ if not peak_indices:
1150
+ plt.close()
1151
+ fig, ax = plt.subplots(figsize=(11, 8.5))
1152
+ ax.set_xlabel('Detector channel', fontsize='x-large')
1153
+ ax.set_ylabel('Intensity', fontsize='x-large')
1154
+ ax.set_xlim(index_ranges[0][0], index_ranges[-1][1])
1155
+ fig.subplots_adjust(bottom=0.0, top=0.85)
1156
+ ax.plot(np.arange(y.size), y, color='k')
1157
+ fig.subplots_adjust(bottom=0.2)
1158
+ change_error_text(error_text)
1159
+ plt.draw()
1160
+ return peak_indices
1161
+
1162
+ peak_indices = []
1163
+ fig_title = []
1164
+ error_texts = []
1165
+
1166
+ y = np.asarray(y)
1167
+ if detector_name is None:
1168
+ detector_name = ''
1169
+ elif not isinstance(detector_name, str):
1170
+ raise ValueError(
1171
+ f'Invalid parameter `detector_name`: {detector_name}')
1172
+ elif not reset_flag:
1173
+ detector_name = f' on detector {detector_name}'
1174
+ num_peak = len(input_indices)
1175
+
1176
+ # Setup the Matplotlib figure
1177
+ title_pos = (0.5, 0.95)
1178
+ title_props = {'fontsize': 'xx-large', 'horizontalalignment': 'center',
1179
+ 'verticalalignment': 'bottom'}
1180
+ error_pos = (0.5, 0.90)
1181
+ error_props = {'fontsize': 'x-large', 'horizontalalignment': 'center',
1182
+ 'verticalalignment': 'bottom'}
1183
+ selected_peak_props = {
1184
+ 'color': 'red', 'linestyle': '-', 'linewidth': 2,
1185
+ 'marker': 10, 'markersize': 10, 'fillstyle': 'full'}
1186
+
1187
+ fig, ax = plt.subplots(figsize=(11, 8.5))
1188
+ ax.plot(np.arange(y.size), y, color='k')
1189
+ ax.set_xlabel('Detector channel', fontsize='x-large')
1190
+ ax.set_ylabel('Intensity', fontsize='x-large')
1191
+ ax.set_xlim(index_ranges[0][0], index_ranges[-1][1])
1192
+ fig.subplots_adjust(bottom=0.0, top=0.85)
1193
+
1194
+ if not interactive:
1195
+
1196
+ peak_indices += find_peaks()
1197
+
1198
+ for index in peak_indices:
1199
+ ax.axvline(index, **selected_peak_props)
1200
+ change_fig_title('Initial peak positions from peak finding '
1201
+ f'routine{detector_name}')
1202
+
1203
+ else:
1204
+
1205
+ fig.subplots_adjust(bottom=0.2)
1206
+
1207
+ # Get initial peak indices
1208
+ if not reset_flag:
1209
+ peak_indices += find_peaks()
1210
+ change_fig_title('Initial peak positions from peak finding '
1211
+ f'routine{detector_name}')
1212
+ if peak_indices:
1213
+ for index in peak_indices:
1214
+ if not any(True if low <= index <= upp else False
1215
+ for low, upp in index_ranges):
1216
+ peak_indices.clear
1217
+ break
1218
+ if not peak_indices:
1219
+ peak_indices += select_peaks()
1220
+ change_fig_title(
1221
+ 'Selected initial peak positions{detector_name}')
1222
+
1223
+ for index in peak_indices:
1224
+ ax.axvline(index, **selected_peak_props)
1225
+
1226
+ # Setup "Reset" button
1227
+ if not reset_flag:
1228
+ reset_btn = Button(
1229
+ plt.axes([0.1, 0.05, 0.15, 0.075]), 'Manually select')
1230
+ else:
1231
+ reset_btn = Button(
1232
+ plt.axes([0.1, 0.05, 0.15, 0.075]), 'Reset')
1233
+ reset_cid = reset_btn.on_clicked(reset)
1234
+
1235
+ # Setup "Confirm" button
1236
+ confirm_btn = Button(
1237
+ plt.axes([0.75, 0.05, 0.15, 0.075]), 'Confirm')
1238
+ confirm_cid = confirm_btn.on_clicked(confirm)
1239
+
1240
+ plt.show()
1241
+
1242
+ # Disconnect all widget callbacks when figure is closed
1243
+ reset_btn.disconnect(reset_cid)
1244
+ confirm_btn.disconnect(confirm_cid)
1245
+
1246
+ # ... and remove the buttons before returning the figure
1247
+ reset_btn.ax.remove()
1248
+ confirm_btn.ax.remove()
1249
+
1250
+ if filename is not None:
1251
+ fig_title[0].set_in_layout(True)
1252
+ fig.tight_layout(rect=(0, 0, 1, 0.95))
1253
+ fig.savefig(filename)
1254
+ plt.close()
1255
+
1256
+ if interactive and len(peak_indices) != num_peak:
1257
+ reset_flag += 1
1258
+ return self._get_initial_peak_positions(
1259
+ y, index_ranges, input_indices, input_max_peak_index,
1260
+ interactive, filename, detector_name, reset_flag=reset_flag)
1261
+ return peak_indices
1262
+
1263
+
1264
+ class MCATthCalibrationProcessor(Processor):
1265
+ """Processor to calibrate the 2&theta angle and fine tune the
1266
+ energy calibration coefficients for an EDD experimental setup.
1267
+ """
1268
+
1269
+ def process(self,
1270
+ data,
1271
+ config=None,
1272
+ tth_initial_guess=None,
1273
+ include_energy_ranges=None,
1274
+ calibration_method='iterate_tth',
1275
+ quadratic_energy_calibration=False,
1276
+ centers_range=20,
1277
+ fwhm_min=None,
1278
+ fwhm_max=None,
1279
+ background=None,
1280
+ baseline=False,
1281
+ save_figures=False,
1282
+ inputdir='.',
1283
+ outputdir='.',
1284
+ interactive=False):
1285
+ """Return the calibrated 2&theta value and the fine tuned
1286
+ energy calibration coefficients to convert MCA channel
1287
+ indices to MCA channel energies.
267
1288
 
268
1289
  :param data: Input configuration for the raw data & tuning
269
1290
  procedure.
270
1291
  :type data: list[PipelineData]
271
1292
  :param config: Initialization parameters for an instance of
272
- CHAP.edd.models.MCACeriaCalibrationConfig, defaults to
1293
+ CHAP.edd.models.MCATthCalibrationConfig, defaults to
273
1294
  None.
274
1295
  :type config: dict, optional
1296
+ :param tth_initial_guess: Initial guess for 2&theta to supercede
1297
+ the values from the energy calibration detector cofiguration
1298
+ on each of the detectors.
1299
+ :type tth_initial_guess: float, optional
1300
+ :param include_energy_ranges: List of MCA channel energy ranges
1301
+ in keV whose data should be included after applying a mask
1302
+ (bounds are inclusive). If specified, these supercede the
1303
+ values from the energy calibration detector cofiguration on
1304
+ each of the detectors.
1305
+ :type include_energy_ranges: list[[float, float]], optional
1306
+ :param calibration_method: Type of calibration method,
1307
+ defaults to `'iterate_tth'`.
1308
+ :type calibration_method:
1309
+ Union['direct_fit_residual', 'direct_fit_peak_energies',
1310
+ 'direct_fit_combined', 'iterate_tth'], optional
1311
+ :param quadratic_energy_calibration: Adds a quadratic term to
1312
+ the detector channel index to energy conversion, defaults
1313
+ to `False` (linear only).
1314
+ :type quadratic_energy_calibration: bool, optional
1315
+ :param centers_range: Set boundaries on the peak centers in
1316
+ MCA channels when performing the fit. The min/max
1317
+ possible values for the peak centers will be the initial
1318
+ values &pm; `centers_range`. Defaults to `20`.
1319
+ :type centers_range: int, optional
1320
+ :param fwhm_min: Lower bound on the peak FWHM in MCA channels
1321
+ when performing the fit, defaults to `None`.
1322
+ :type fwhm_min: float, optional
1323
+ :param fwhm_max: Lower bound on the peak FWHM in MCA channels
1324
+ when performing the fit, defaults to `None`.
1325
+ :type fwhm_max: float, optional
1326
+ :param background: Background model for peak fitting.
1327
+ :type background: str, list[str], optional
1328
+ :param baseline: Automated baseline subtraction configuration,
1329
+ defaults to `False`.
1330
+ :type baseline: bool, BaselineConfig, optional
275
1331
  :param save_figures: Save .pngs of plots for checking inputs &
276
1332
  outputs of this Processor, defaults to False.
277
1333
  :type save_figures: bool, optional
@@ -290,38 +1346,81 @@ class MCACeriaCalibrationProcessor(Processor):
290
1346
  2&theta and the linear correction parameters added.
291
1347
  :rtype: dict[str,float]
292
1348
  """
1349
+ # Local modules
1350
+ from CHAP.edd.models import BaselineConfig
1351
+ from CHAP.utils.general import (
1352
+ is_int,
1353
+ is_str_series,
1354
+ )
1355
+
293
1356
  try:
294
1357
  calibration_config = self.get_config(
295
- data, 'edd.models.MCACeriaCalibrationConfig',
1358
+ data, 'edd.models.MCATthCalibrationConfig',
1359
+ calibration_method=calibration_method,
296
1360
  inputdir=inputdir)
297
1361
  except Exception as data_exc:
298
1362
  self.logger.info('No valid calibration config in input pipeline '
299
1363
  'data, using config parameter instead.')
300
1364
  try:
301
1365
  # Local modules
302
- from CHAP.edd.models import MCACeriaCalibrationConfig
1366
+ from CHAP.edd.models import MCATthCalibrationConfig
303
1367
 
304
- calibration_config = MCACeriaCalibrationConfig(
305
- **config, inputdir=inputdir)
1368
+ calibration_config = MCATthCalibrationConfig(
1369
+ **config, calibration_method=calibration_method,
1370
+ inputdir=inputdir)
306
1371
  except Exception as dict_exc:
307
1372
  raise RuntimeError from dict_exc
308
1373
 
1374
+ # Validate the optional inputs
1375
+ if not is_int(centers_range, gt=0, log=False):
1376
+ RuntimeError(f'Invalid centers_range parameter ({centers_range})')
1377
+ if fwhm_min is not None and not is_int(fwhm_min, gt=0, log=False):
1378
+ RuntimeError(f'Invalid fwhm_min parameter ({fwhm_min})')
1379
+ if fwhm_max is not None and not is_int(fwhm_max, gt=0, log=False):
1380
+ RuntimeError(f'Invalid fwhm_max parameter ({fwhm_max})')
1381
+ if background is not None:
1382
+ if isinstance(background, str):
1383
+ background = [background]
1384
+ elif not is_str_series(background, log=False):
1385
+ RuntimeError(f'Invalid background parameter ({background})')
1386
+ if isinstance(baseline, bool):
1387
+ if baseline:
1388
+ baseline = BaselineConfig()
1389
+ else:
1390
+ try:
1391
+ baseline = BaselineConfig(**baseline)
1392
+ except Exception as dict_exc:
1393
+ raise RuntimeError from dict_exc
1394
+
1395
+ self.logger.debug(f'In process: save_figures = {save_figures}; '
1396
+ f'interactive = {interactive}')
1397
+
309
1398
  for detector in calibration_config.detectors:
310
- tth, slope, intercept = self.calibrate(
311
- calibration_config, detector, save_figures=save_figures,
312
- interactive=interactive, outputdir=outputdir)
313
- detector.tth_calibrated = tth
314
- detector.slope_calibrated = slope
315
- detector.intercept_calibrated = intercept
1399
+ if tth_initial_guess is not None:
1400
+ detector.tth_initial_guess = tth_initial_guess
1401
+ if include_energy_ranges is not None:
1402
+ detector.include_energy_ranges = include_energy_ranges
1403
+ if background is not None:
1404
+ detector.background = background.copy()
1405
+ if baseline:
1406
+ detector.baseline = baseline
1407
+ self.calibrate(
1408
+ calibration_config, detector, quadratic_energy_calibration,
1409
+ centers_range, fwhm_min, fwhm_max, save_figures, interactive,
1410
+ outputdir)
316
1411
 
317
1412
  return calibration_config.dict()
318
1413
 
319
1414
  def calibrate(self,
320
1415
  calibration_config,
321
1416
  detector,
1417
+ quadratic_energy_calibration=False,
1418
+ centers_range=20,
1419
+ fwhm_min=None,
1420
+ fwhm_max=None,
322
1421
  save_figures=False,
323
- outputdir='.',
324
- interactive=False):
1422
+ interactive=False,
1423
+ outputdir='.'):
325
1424
  """Iteratively calibrate 2&theta by fitting selected peaks of
326
1425
  an MCA spectrum until the computed strain is sufficiently
327
1426
  small. Use the fitted peak locations to determine linear
@@ -330,27 +1429,56 @@ class MCACeriaCalibrationProcessor(Processor):
330
1429
  :param calibration_config: Object configuring the CeO2
331
1430
  calibration procedure for an MCA detector.
332
1431
  :type calibration_config:
333
- CHAP.edd.models.MCACeriaCalibrationConfig
1432
+ CHAP.edd.models.MCATthCalibrationConfig
334
1433
  :param detector: A single MCA detector element configuration.
335
1434
  :type detector: CHAP.edd.models.MCAElementCalibrationConfig
1435
+ :param quadratic_energy_calibration: Adds a quadratic term to
1436
+ the detector channel index to energy conversion, defaults
1437
+ to `False` (linear only).
1438
+ :type quadratic_energy_calibration: bool, optional
1439
+ :param centers_range: Set boundaries on the peak centers in
1440
+ MCA channels when performing the fit. The min/max
1441
+ possible values for the peak centers will be the initial
1442
+ values &pm; `centers_range`. Defaults to `20`.
1443
+ :type centers_range: int, optional
1444
+ :param fwhm_min: Lower bound on the peak FWHM in MCA channels
1445
+ when performing the fit, defaults to `None`.
1446
+ :type fwhm_min: float, optional
1447
+ :param fwhm_max: Lower bound on the peak FWHM in MCA channels
1448
+ when performing the fit, defaults to `None`.
1449
+ :type fwhm_max: float, optional
336
1450
  :param save_figures: Save .pngs of plots for checking inputs &
337
1451
  outputs of this Processor, defaults to False.
338
1452
  :type save_figures: bool, optional
339
- :param outputdir: Directory to which any output figures will
340
- be saved, defaults to '.'.
341
- :type outputdir: str, optional
342
1453
  :param interactive: Allows for user interactions, defaults to
343
1454
  False.
344
1455
  :type interactive: bool, optional
1456
+ :param outputdir: Directory to which any output figures will
1457
+ be saved, defaults to '.'.
1458
+ :type outputdir: str, optional
345
1459
  :raises ValueError: No value provided for included bin ranges
346
1460
  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.
349
- :rtype: float, float, float
350
1461
  """
1462
+ # System modules
1463
+ from sys import float_info
1464
+
1465
+ # Third party modules
1466
+ from nexusformat.nexus import NXdata, NXfield
1467
+ from scipy.constants import physical_constants
1468
+
351
1469
  # Local modules
1470
+ if interactive or save_figures:
1471
+ from CHAP.edd.utils import (
1472
+ select_tth_initial_guess,
1473
+ select_mask_and_hkls,
1474
+ )
352
1475
  from CHAP.edd.utils import get_peak_locations
353
- from CHAP.utils.fit import Fit
1476
+ from CHAP.utils.fit import FitProcessor
1477
+ from CHAP.utils.general import index_nearest
1478
+
1479
+ self.logger.info(f'Calibrating detector {detector.detector_name}')
1480
+
1481
+ calibration_method = calibration_config.calibration_method
354
1482
 
355
1483
  # Get the unique HKLs and lattice spacings for the calibration
356
1484
  # material
@@ -358,235 +1486,789 @@ class MCACeriaCalibrationProcessor(Processor):
358
1486
  tth_tol=detector.hkl_tth_tol, tth_max=detector.tth_max)
359
1487
 
360
1488
  # Collect raw MCA data of interest
361
- mca_bin_energies = np.linspace(
362
- 0, detector.max_energy_kev, detector.num_bins)
1489
+ mca_bin_energies = detector.energies
363
1490
  mca_data = calibration_config.mca_data(detector)
364
1491
 
365
- if interactive or save_figures:
366
- # Third party modules
367
- import matplotlib.pyplot as plt
1492
+ # Blank out data below 25 keV as well as the last bin
1493
+ energy_mask = np.where(mca_bin_energies >= 25.0, 1, 0)
1494
+ energy_mask[-1] = 0
1495
+ mca_data = mca_data*energy_mask
368
1496
 
1497
+ # Subtract the baseline
1498
+ if detector.baseline:
369
1499
  # 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()
1500
+ from CHAP.common.processor import ConstructBaseline
385
1501
 
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)
393
- detector.include_bin_ranges = include_bin_ranges
394
- detector.hkl_indices = hkl_indices
395
1502
  if save_figures:
396
- fig.savefig(os.path.join(
397
- outputdir,
398
- f'{detector.detector_name}_calibration_fit_mask_hkls.png'))
399
- plt.close()
1503
+ filename = os.path.join(outputdir,
1504
+ f'{detector.detector_name}_tth_'
1505
+ 'calibration_baseline.png')
1506
+ else:
1507
+ filename = None
1508
+ baseline, baseline_config = ConstructBaseline.construct_baseline(
1509
+ mca_data, mask=energy_mask, tol=detector.baseline.tol,
1510
+ lam=detector.baseline.lam, max_iter=detector.baseline.max_iter,
1511
+ title=f'Baseline for detector {detector.detector_name}',
1512
+ xlabel='Energy (keV)', ylabel='Intensity (counts)',
1513
+ interactive=interactive, filename=filename)
1514
+
1515
+ mca_data -= baseline
1516
+ detector.baseline.lam = baseline_config['lambda']
1517
+ detector.baseline.attrs['num_iter'] = baseline_config['num_iter']
1518
+ detector.baseline.attrs['error'] = baseline_config['error']
1519
+
1520
+ # Adjust initial tth guess
1521
+ if save_figures:
1522
+ filename = os.path.join(
1523
+ outputdir,
1524
+ f'{detector.detector_name}_calibration_tth_initial_guess.png')
1525
+ else:
1526
+ filename = None
1527
+ tth_init = select_tth_initial_guess(
1528
+ mca_bin_energies, mca_data, hkls, ds,
1529
+ detector.tth_initial_guess, interactive, filename)
1530
+ detector.tth_initial_guess = tth_init
400
1531
  self.logger.debug(f'tth_initial_guess = {detector.tth_initial_guess}')
1532
+
1533
+ # Select the mask and HKLs for the Bragg peaks
1534
+ if save_figures:
1535
+ filename = os.path.join(
1536
+ outputdir,
1537
+ f'{detector.detector_name}_calibration_fit_mask_hkls.png')
1538
+ if calibration_method == 'iterate_tth':
1539
+ num_hkl_min = 2
1540
+ else:
1541
+ num_hkl_min = 1
1542
+ include_bin_ranges, hkl_indices = select_mask_and_hkls(
1543
+ mca_bin_energies, mca_data, hkls, ds,
1544
+ detector.tth_initial_guess,
1545
+ preselected_bin_ranges=detector.include_bin_ranges,
1546
+ num_hkl_min=num_hkl_min, detector_name=detector.detector_name,
1547
+ flux_energy_range=calibration_config.flux_file_energy_range(),
1548
+ label='MCA data', interactive=interactive, filename=filename)
1549
+
1550
+ # Add the mask for the fluorescence peaks
1551
+ if calibration_method != 'iterate_tth':
1552
+ include_bin_ranges = (
1553
+ calibration_config.fit_index_ranges + include_bin_ranges)
1554
+
1555
+ # Apply the mask
1556
+ detector.include_energy_ranges = detector.get_include_energy_ranges(
1557
+ include_bin_ranges)
1558
+ detector.set_hkl_indices(hkl_indices)
1559
+ self.logger.debug(
1560
+ f'include_energy_ranges = {detector.include_energy_ranges}')
401
1561
  self.logger.debug(
402
- f'include_bin_ranges = {detector.include_bin_ranges}')
403
- if detector.include_bin_ranges is None:
1562
+ f'hkl_indices = {detector.hkl_indices}')
1563
+ if not detector.include_energy_ranges:
404
1564
  raise ValueError(
405
- 'No value provided for include_bin_ranges. '
406
- 'Provide them in the MCA Ceria Calibration Configuration, '
1565
+ 'No value provided for include_energy_ranges. '
1566
+ 'Provide them in the MCA Tth Calibration Configuration '
407
1567
  'or re-run the pipeline with the --interactive flag.')
408
- if detector.hkl_indices is None:
1568
+ if not detector.hkl_indices:
409
1569
  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.')
1570
+ 'Unable to get values for hkl_indices for the provided '
1571
+ 'value of include_energy_ranges. Change its value in '
1572
+ 'the detector\'s MCA Tth Calibration Configuration or '
1573
+ 're-run the pipeline with the --interactive flag.')
413
1574
  mca_mask = detector.mca_mask()
414
- fit_mca_energies = mca_bin_energies[mca_mask]
415
- fit_mca_intensities = mca_data[mca_mask]
1575
+ mca_data_fit = mca_data[mca_mask]
416
1576
 
417
1577
  # Correct raw MCA data for variable flux at different energies
418
1578
  flux_correct = \
419
1579
  calibration_config.flux_correction_interpolation_function()
420
- mca_intensity_weights = flux_correct(fit_mca_energies)
421
- fit_mca_intensities = fit_mca_intensities / mca_intensity_weights
422
-
423
- # Get the HKLs and lattice spacings that will be used for
424
- # fitting
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])
427
- c_1 = fit_hkls[:,0]**2 + fit_hkls[:,1]**2 + fit_hkls[:,2]**2
428
- tth = detector.tth_initial_guess
429
- 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} ')
432
-
433
- # Perform the uniform fit first
434
-
435
- # Get expected peak energy locations for this iteration's
436
- # starting value of tth
437
- fit_E0 = get_peak_locations(fit_ds, tth)
438
-
439
- # 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()
445
-
446
- # Extract values of interest from the best values for the
447
- # uniform fit parameters
448
- uniform_fit_centers = [
449
- uniform_fit.best_values[f'peak{i+1}_center']
450
- for i in range(len(fit_hkls))]
451
- uniform_a = uniform_fit.best_values['scale_factor']
452
- uniform_strain = np.log(
453
- (uniform_a
454
- / calibration_config.material.lattice_parameters)) # CeO2 is cubic, so this is fine here.
455
-
456
- # Next, perform the unconstrained fit
457
-
458
- # Use the peak locations found in the uniform fit as the
459
- # initial guesses for peak locations in the unconstrained
460
- # 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()
466
-
467
- # Extract values of interest from the best values for the
468
- # unconstrained fit parameters
469
- unconstrained_fit_centers = np.array(
470
- [unconstrained_fit.best_values[f'peak{i+1}_center']
471
- for i in range(len(fit_hkls))])
472
- unconstrained_a = np.sqrt(c_1)*abs(get_peak_locations(
473
- unconstrained_fit_centers, tth))
474
- unconstrained_strains = np.log(
475
- (unconstrained_a
1580
+ if flux_correct is not None:
1581
+ mca_intensity_weights = flux_correct(
1582
+ mca_bin_energies[mca_mask])
1583
+ mca_data_fit = mca_data_fit / mca_intensity_weights
1584
+
1585
+ # Get the fluorescence peak info
1586
+ e_xrf = calibration_config.peak_energies
1587
+ num_xrf = len(e_xrf)
1588
+
1589
+ # Get the Bragg peak HKLs, lattice spacings and energies
1590
+ hkls_fit = np.asarray([hkls[i] for i in detector.hkl_indices])
1591
+ ds_fit = np.asarray([ds[i] for i in detector.hkl_indices])
1592
+ c_1_fit = hkls_fit[:,0]**2 + hkls_fit[:,1]**2 + hkls_fit[:,2]**2
1593
+ e_bragg_init = get_peak_locations(ds_fit, tth_init)
1594
+ num_bragg = len(e_bragg_init)
1595
+
1596
+ # Perform the fit
1597
+ if calibration_method == 'direct_fit_residual':
1598
+
1599
+ # Get the initial free fit parameters
1600
+ tth_init = np.radians(tth_init)
1601
+ a_init, b_init, c_init = detector.energy_calibration_coeffs
1602
+
1603
+ # For testing: hardwired limits:
1604
+ if False:
1605
+ min_value = None
1606
+ tth_min = None
1607
+ tth_max = None
1608
+ b_min = None
1609
+ b_max = None
1610
+ sig_min = None
1611
+ sig_max = None
1612
+ else:
1613
+ min_value = float_info.min
1614
+ tth_min = 0.9*tth_init
1615
+ tth_max = 1.1*tth_init
1616
+ b_min = 0.1*b_init
1617
+ b_max = 10.0*b_init
1618
+ if isinstance(fwhm_min, (int,float)):
1619
+ sig_min = fwhm_min/2.35482
1620
+ else:
1621
+ sig_min = None
1622
+ if isinstance(fwhm_max, (int,float)):
1623
+ sig_max = fwhm_max/2.35482
1624
+ else:
1625
+ sig_max = None
1626
+
1627
+ # Construct the free fit parameters
1628
+ parameters = [
1629
+ {'name': 'tth', 'value': tth_init, 'min': tth_min,
1630
+ 'max': tth_max}]
1631
+ if quadratic_energy_calibration:
1632
+ parameters.append({'name': 'a', 'value': a_init})
1633
+ parameters.append(
1634
+ {'name': 'b', 'value': b_init, 'min': b_min, 'max': b_max})
1635
+ parameters.append({'name': 'c', 'value': c_init})
1636
+
1637
+ # Construct the fit model
1638
+ models = []
1639
+
1640
+ # Add the background
1641
+ if detector.background is not None:
1642
+ if isinstance(detector.background, str):
1643
+ models.append(
1644
+ {'model': detector.background, 'prefix': 'bkgd_'})
1645
+ else:
1646
+ for model in detector.background:
1647
+ models.append({'model': model, 'prefix': f'{model}_'})
1648
+
1649
+ # Add the fluorescent peaks
1650
+ for i, e_peak in enumerate(e_xrf):
1651
+ expr = f'({e_peak}-c)/b'
1652
+ if quadratic_energy_calibration:
1653
+ expr = '(' + expr + f')*(1.0-a*(({e_peak}-c)/(b*b)))'
1654
+ models.append(
1655
+ {'model': 'gaussian', 'prefix': f'xrf{i+1}_',
1656
+ 'parameters': [
1657
+ {'name': 'amplitude', 'min': min_value},
1658
+ {'name': 'center', 'expr': expr},
1659
+ {'name': 'sigma', 'min': sig_min, 'max': sig_max}]})
1660
+
1661
+ # Add the Bragg peaks
1662
+ hc = 1.e7 * physical_constants['Planck constant in eV/Hz'][0] \
1663
+ * physical_constants['speed of light in vacuum'][0]
1664
+ for i, (e_peak, ds) in enumerate(zip(e_bragg_init, ds_fit)):
1665
+ norm = 0.5*hc/ds
1666
+ expr = f'(({norm}/sin(0.5*tth))-c)/b'
1667
+ if quadratic_energy_calibration:
1668
+ expr = '(' + expr \
1669
+ + f')*(1.0-a*((({norm}/sin(0.5*tth))-c)/(b*b)))'
1670
+ models.append(
1671
+ {'model': 'gaussian', 'prefix': f'peak{i+1}_',
1672
+ 'parameters': [
1673
+ {'name': 'amplitude', 'min': min_value},
1674
+ {'name': 'center', 'expr': expr},
1675
+ {'name': 'sigma', 'min': sig_min, 'max': sig_max}]})
1676
+
1677
+ # Perform the fit
1678
+ fit = FitProcessor()
1679
+ result = fit.process(
1680
+ NXdata(NXfield(mca_data_fit, 'y'),
1681
+ NXfield(np.arange(detector.num_bins)[mca_mask], 'x')),
1682
+ {'parameters': parameters, 'models': models, 'method': 'trf'})
1683
+
1684
+ # Extract values of interest from the best values
1685
+ best_fit_uniform = result.best_fit
1686
+ residual_uniform = result.residual
1687
+ tth_fit = np.degrees(result.best_values['tth'])
1688
+ if quadratic_energy_calibration:
1689
+ a_fit = result.best_values['a']
1690
+ else:
1691
+ a_fit = 0.0
1692
+ b_fit = result.best_values['b']
1693
+ c_fit = result.best_values['c']
1694
+ peak_indices_fit = np.asarray(
1695
+ [result.best_values[f'xrf{i+1}_center'] for i in range(num_xrf)]
1696
+ + [result.best_values[f'peak{i+1}_center']
1697
+ for i in range(num_bragg)])
1698
+ peak_energies_fit = ((a_fit*peak_indices_fit + b_fit)
1699
+ * peak_indices_fit + c_fit)
1700
+ e_bragg_uniform = peak_energies_fit[num_xrf:]
1701
+ a_uniform = np.sqrt(c_1_fit) * abs(
1702
+ get_peak_locations(e_bragg_uniform, tth_fit))
1703
+ strains_uniform = np.log(
1704
+ (a_uniform
476
1705
  / calibration_config.material.lattice_parameters))
477
- unconstrained_strain = np.mean(unconstrained_strains)
478
- unconstrained_tth = tth * (1.0 + unconstrained_strain)
1706
+ strain_uniform = np.mean(strains_uniform)
479
1707
 
480
- # Update tth for the next iteration of tuning
481
- prev_tth = tth
482
- tth = unconstrained_tth
483
-
484
- # Stop tuning tth at this iteration if differences are
485
- # small enough
486
- if abs(tth - prev_tth) < calibration_config.tune_tth_tol:
487
- break
1708
+ elif calibration_method == 'direct_fit_peak_energies':
1709
+ # Third party modules
1710
+ from scipy.optimize import minimize
1711
+
1712
+ def cost_function(
1713
+ pars, quadratic_energy_calibration, ds_fit,
1714
+ indices_unconstrained, e_xrf):
1715
+ tth = pars[0]
1716
+ b = pars[1]
1717
+ c = pars[2]
1718
+ if quadratic_energy_calibration:
1719
+ a = pars[3]
1720
+ else:
1721
+ a = 0.0
1722
+ energies_unconstrained = (
1723
+ (a*indices_unconstrained + b) * indices_unconstrained + c)
1724
+ target_energies = np.concatenate(
1725
+ (e_xrf, get_peak_locations(ds_fit, tth)))
1726
+ return np.sqrt(np.sum(
1727
+ (energies_unconstrained-target_energies)**2))
1728
+
1729
+ # Get the initial free fit parameters
1730
+ a_init, b_init, c_init = detector.energy_calibration_coeffs
1731
+
1732
+ # Perform an unconstrained fit in terms of MCA bin index
1733
+ mca_bins_fit = np.arange(detector.num_bins)[mca_mask]
1734
+ centers = [index_nearest(mca_bin_energies, e_peak)
1735
+ for e_peak in np.concatenate((e_xrf, e_bragg_init))]
1736
+ models = []
1737
+ if detector.background is not None:
1738
+ if isinstance(detector.background, str):
1739
+ models.append(
1740
+ {'model': detector.background, 'prefix': 'bkgd_'})
1741
+ else:
1742
+ for model in detector.background:
1743
+ models.append({'model': model, 'prefix': f'{model}_'})
1744
+ models.append(
1745
+ {'model': 'multipeak', 'centers': centers,
1746
+ 'centers_range': centers_range,
1747
+ 'fwhm_min': fwhm_min, 'fwhm_max': fwhm_max})
1748
+ fit = FitProcessor()
1749
+ result = fit.process(
1750
+ NXdata(NXfield(mca_data_fit, 'y'),
1751
+ NXfield(mca_bins_fit, 'x')),
1752
+ {'models': models, 'method': 'trf'})
1753
+
1754
+ # Extract the peak properties from the fit
1755
+ indices_unconstrained = np.asarray(
1756
+ [result.best_values[f'peak{i+1}_center']
1757
+ for i in range(num_xrf+num_bragg)])
1758
+
1759
+ # Perform a peak center fit using the theoretical values
1760
+ # for the fluorescense peaks and Bragg's law for the Bragg
1761
+ # peaks for given material properties and a freely
1762
+ # adjustable 2&theta angle and MCA energy axis calibration
1763
+ pars_init = [tth_init, b_init, c_init]
1764
+ if quadratic_energy_calibration:
1765
+ pars_init.append(a_init)
1766
+ # For testing: hardwired limits:
1767
+ if True:
1768
+ bounds = [
1769
+ (0.9*tth_init, 1.1*tth_init),
1770
+ (0.1*b_init, 10.*b_init),
1771
+ (0.1*c_init, 10.*c_init)]
1772
+ if quadratic_energy_calibration:
1773
+ if a_init:
1774
+ bounds.append((0.1*a_init, 10.0*a_init))
1775
+ else:
1776
+ bounds.append((None, None))
1777
+ else:
1778
+ bounds = None
1779
+ result = minimize(
1780
+ cost_function, pars_init,
1781
+ args=(
1782
+ quadratic_energy_calibration, ds_fit,
1783
+ indices_unconstrained, e_xrf),
1784
+ method='Nelder-Mead', bounds=bounds)
1785
+
1786
+ # Extract values of interest from the best values
1787
+ best_fit_uniform = None
1788
+ residual_uniform = None
1789
+ tth_fit = float(result['x'][0])
1790
+ b_fit = float(result['x'][1])
1791
+ c_fit = float(result['x'][2])
1792
+ if quadratic_energy_calibration:
1793
+ a_fit = float(result['x'][3])
1794
+ else:
1795
+ a_fit = 0.0
1796
+ e_bragg_fit = get_peak_locations(ds_fit, tth_fit)
1797
+ peak_energies_fit = [
1798
+ (a_fit*i + b_fit) * i + c_fit
1799
+ for i in indices_unconstrained[:num_xrf]] \
1800
+ + list(e_bragg_fit)
1801
+
1802
+ fit_uniform = None
1803
+ residual_uniform = None
1804
+ e_bragg_uniform = e_bragg_fit
1805
+ strain_uniform = None
1806
+
1807
+ elif calibration_method == 'direct_fit_combined':
1808
+ # Third party modules
1809
+ from scipy.optimize import minimize
1810
+
1811
+ def gaussian(x, a, b, c, amp, sig, e_peak):
1812
+ sig2 = 2.*sig**2
1813
+ norm = sig*np.sqrt(2.0*np.pi)
1814
+ cen = (e_peak-c) * (1.0 - a * (e_peak-c) / b**2) / b
1815
+ return amp*np.exp(-(x-cen)**2/sig2)/norm
1816
+
1817
+ def cost_function_combined(
1818
+ pars, x, y, quadratic_energy_calibration, ds_fit,
1819
+ indices_unconstrained, e_xrf):
1820
+ tth = pars[0]
1821
+ b = pars[1]
1822
+ c = pars[2]
1823
+ amplitudes = pars[3::2]
1824
+ sigmas = pars[4::2]
1825
+ if quadratic_energy_calibration:
1826
+ a = pars[-1]
1827
+ else:
1828
+ a = 0.0
1829
+ energies_unconstrained = (
1830
+ (a*indices_unconstrained + b) * indices_unconstrained + c)
1831
+ target_energies = np.concatenate(
1832
+ (e_xrf, get_peak_locations(ds_fit, tth)))
1833
+ y_fit = np.zeros((x.size))
1834
+ for i, e_peak in enumerate(target_energies):
1835
+ y_fit += gaussian(
1836
+ x, a, b, c, amplitudes[i], sigmas[i], e_peak)
1837
+ target_energies_error = np.sqrt(
1838
+ np.sum(
1839
+ (energies_unconstrained
1840
+ - np.asarray(target_energies))**2)
1841
+ / len(target_energies))
1842
+ residual_error = np.sqrt(
1843
+ np.sum((y-y_fit)**2)
1844
+ / (np.sum(y**2) * len(target_energies)))
1845
+ return target_energies_error+residual_error
1846
+
1847
+ # Get the initial free fit parameters
1848
+ a_init, b_init, c_init = detector.energy_calibration_coeffs
1849
+
1850
+ # Perform an unconstrained fit in terms of MCS bin index
1851
+ mca_bins_fit = np.arange(detector.num_bins)[mca_mask]
1852
+ centers = [index_nearest(mca_bin_energies, e_peak)
1853
+ for e_peak in np.concatenate((e_xrf, e_bragg_init))]
1854
+ models = []
1855
+ if detector.background is not None:
1856
+ if isinstance(detector.background, str):
1857
+ models.append(
1858
+ {'model': detector.background, 'prefix': 'bkgd_'})
1859
+ else:
1860
+ for model in detector.background:
1861
+ models.append({'model': model, 'prefix': f'{model}_'})
1862
+ models.append(
1863
+ {'model': 'multipeak', 'centers': centers,
1864
+ 'centers_range': centers_range,
1865
+ 'fwhm_min': fwhm_min, 'fwhm_max': fwhm_max})
1866
+ fit = FitProcessor()
1867
+ result = fit.process(
1868
+ NXdata(NXfield(mca_data_fit, 'y'),
1869
+ NXfield(mca_bins_fit, 'x')),
1870
+ {'models': models, 'method': 'trf'})
1871
+
1872
+ # Extract the peak properties from the fit
1873
+ num_peak = num_xrf+num_bragg
1874
+ indices_unconstrained = np.asarray(
1875
+ [result.best_values[f'peak{i+1}_center']
1876
+ for i in range(num_peak)])
1877
+ amplitudes_init = np.asarray(
1878
+ [result.best_values[f'peak{i+1}_amplitude']
1879
+ for i in range(num_peak)])
1880
+ sigmas_init = np.asarray(
1881
+ [result.best_values[f'peak{i+1}_sigma']
1882
+ for i in range(num_peak)])
488
1883
 
489
- # Fit line to expected / computed peak locations from the last
490
- # unconstrained fit.
491
- fit = Fit.fit_data(
492
- fit_E0, 'linear', x=unconstrained_fit_centers, nan_policy='omit')
493
- slope = fit.best_values['slope']
494
- intercept = fit.best_values['intercept']
1884
+ # Perform a peak center fit using the theoretical values
1885
+ # for the fluorescense peaks and Bragg's law for the Bragg
1886
+ # peaks for given material properties and a freely
1887
+ # adjustable 2&theta angle and MCA energy axis calibration
1888
+ norm = mca_data_fit.max()
1889
+ pars_init = [tth_init, b_init, c_init]
1890
+ for amp, sig in zip(amplitudes_init, sigmas_init):
1891
+ pars_init += [amp/norm, sig]
1892
+ if quadratic_energy_calibration:
1893
+ pars_init += [a_init]
1894
+ # For testing: hardwired limits:
1895
+ if True:
1896
+ bounds = [
1897
+ (0.9*tth_init, 1.1*tth_init),
1898
+ (0.1*b_init, 10.*b_init),
1899
+ (0.1*c_init, 10.*c_init)]
1900
+ for amp, sig in zip(amplitudes_init, sigmas_init):
1901
+ bounds += [
1902
+ (0.9*amp/norm, 1.1*amp/norm), (0.9*sig, 1.1*sig)]
1903
+ if quadratic_energy_calibration:
1904
+ if a_init:
1905
+ bounds += [(0.1*a_init, 10.*a_init)]
1906
+ else:
1907
+ bounds += [(None, None)]
1908
+ else:
1909
+ bounds = None
1910
+ result = minimize(
1911
+ cost_function_combined, pars_init,
1912
+ args=(
1913
+ mca_bins_fit, mca_data_fit/norm,
1914
+ quadratic_energy_calibration, ds_fit,
1915
+ indices_unconstrained, e_xrf),
1916
+ method='Nelder-Mead', bounds=bounds)
1917
+
1918
+ # Extract values of interest from the best values
1919
+ tth_fit = float(result['x'][0])
1920
+ b_fit = float(result['x'][1])
1921
+ c_fit = float(result['x'][2])
1922
+ amplitudes_fit = norm * result['x'][3::2]
1923
+ sigmas_fit = result['x'][4::2]
1924
+ if quadratic_energy_calibration:
1925
+ a_fit = float(result['x'][-1])
1926
+ else:
1927
+ a_fit = 0.0
1928
+ e_bragg_fit = get_peak_locations(ds_fit, tth_fit)
1929
+ peak_energies_fit = [
1930
+ (a_fit*i + b_fit) * i + c_fit
1931
+ for i in indices_unconstrained[:num_xrf]] \
1932
+ + list(e_bragg_fit)
1933
+
1934
+ best_fit_uniform = np.zeros((mca_bins_fit.size))
1935
+ for i, e_peak in enumerate(peak_energies_fit):
1936
+ best_fit_uniform += gaussian(
1937
+ mca_bins_fit, a_fit, b_fit, c_fit, amplitudes_fit[i],
1938
+ sigmas_fit[i], e_peak)
1939
+ residual_uniform = mca_data_fit - best_fit_uniform
1940
+ e_bragg_uniform = e_bragg_fit
1941
+ strain_uniform = 0.0
1942
+
1943
+ elif calibration_method == 'iterate_tth':
1944
+
1945
+ tth_fit = tth_init
1946
+ e_bragg_fit = e_bragg_init
1947
+ mca_bin_energies_fit = mca_bin_energies[mca_mask]
1948
+ a_init, b_init, c_init = detector.energy_calibration_coeffs
1949
+ if isinstance(fwhm_min, (int, float)):
1950
+ fwhm_min = fwhm_min*b_init
1951
+ else:
1952
+ fwhm_min = None
1953
+ if isinstance(fwhm_max, (int, float)):
1954
+ fwhm_max = fwhm_max*b_init
1955
+ else:
1956
+ fwhm_max = None
1957
+ for iter_i in range(calibration_config.max_iter):
1958
+ self.logger.debug(f'Tuning tth: iteration no. {iter_i}, '
1959
+ f'starting value = {tth_fit} ')
1960
+
1961
+ # Construct the fit model
1962
+ models = []
1963
+ if detector.background is not None:
1964
+ if isinstance(detector.background, str):
1965
+ models.append(
1966
+ {'model': detector.background, 'prefix': 'bkgd_'})
1967
+ else:
1968
+ for model in detector.background:
1969
+ models.append(
1970
+ {'model': model, 'prefix': f'{model}_'})
1971
+ models.append(
1972
+ {'model': 'multipeak', 'centers': list(e_bragg_fit),
1973
+ 'fit_type': 'uniform',
1974
+ 'centers_range': centers_range*b_init,
1975
+ 'fwhm_min': fwhm_min, 'fwhm_max': fwhm_max})
1976
+
1977
+ # Perform the uniform
1978
+ fit = FitProcessor()
1979
+ uniform_fit = fit.process(
1980
+ NXdata(
1981
+ NXfield(mca_data_fit, 'y'),
1982
+ NXfield(mca_bin_energies_fit, 'x')),
1983
+ {'models': models, 'method': 'trf'})
1984
+
1985
+ # Extract values of interest from the best values for
1986
+ # the uniform fit parameters
1987
+ best_fit_uniform = uniform_fit.best_fit
1988
+ residual_uniform = uniform_fit.residual
1989
+ e_bragg_uniform = [
1990
+ uniform_fit.best_values[f'peak{i+1}_center']
1991
+ for i in range(num_bragg)]
1992
+ strain_uniform = -np.log(
1993
+ uniform_fit.best_values['scale_factor'])
1994
+
1995
+ # Next, perform the unconstrained fit
1996
+
1997
+ # Use the peak parameters from the uniform fit as
1998
+ # the initial guesses for peak locations in the
1999
+ # unconstrained fit
2000
+ models[-1]['fit_type'] = 'unconstrained'
2001
+ unconstrained_fit = fit.process(
2002
+ uniform_fit, {'models': models, 'method': 'trf'})
2003
+
2004
+ # Extract values of interest from the best values for
2005
+ # the unconstrained fit parameters
2006
+ best_fit_unconstrained = unconstrained_fit.best_fit
2007
+ residual_unconstrained = unconstrained_fit.residual
2008
+ e_bragg_unconstrained = np.array(
2009
+ [unconstrained_fit.best_values[f'peak{i+1}_center']
2010
+ for i in range(num_bragg)])
2011
+ a_unconstrained = np.sqrt(c_1_fit)*abs(get_peak_locations(
2012
+ e_bragg_unconstrained, tth_fit))
2013
+ strains_unconstrained = np.log(
2014
+ (a_unconstrained
2015
+ / calibration_config.material.lattice_parameters))
2016
+ strain_unconstrained = np.mean(strains_unconstrained)
2017
+ tth_unconstrained = tth_fit * (1.0 + strain_unconstrained)
2018
+
2019
+ # Update tth for the next iteration of tuning
2020
+ prev_tth = tth_fit
2021
+ tth_fit = float(tth_unconstrained)
2022
+
2023
+ # Update the peak energy locations for this iteration
2024
+ e_bragg_fit = get_peak_locations(ds_fit, tth_fit)
2025
+
2026
+ # Stop tuning tth at this iteration if differences are
2027
+ # small enough
2028
+ if abs(tth_fit - prev_tth) < calibration_config.tune_tth_tol:
2029
+ break
2030
+
2031
+ # Fit line to expected / computed peak locations from the
2032
+ # last unconstrained fit.
2033
+ if quadratic_energy_calibration:
2034
+ fit = FitProcessor()
2035
+ result = fit.process(
2036
+ NXdata(
2037
+ NXfield(e_bragg_fit, 'y'),
2038
+ NXfield(e_bragg_unconstrained, 'x')),
2039
+ {'models': [{'model': 'quadratic'}]})
2040
+ a = result.best_values['a']
2041
+ b = result.best_values['b']
2042
+ c = result.best_values['c']
2043
+ else:
2044
+ fit = FitProcessor()
2045
+ result = fit.process(
2046
+ NXdata(
2047
+ NXfield(e_bragg_fit, 'y'),
2048
+ NXfield(e_bragg_unconstrained, 'x')),
2049
+ {'models': [{'model': 'linear'}]})
2050
+ a = 0.0
2051
+ b = result.best_values['slope']
2052
+ c = result.best_values['intercept']
2053
+ # The following assumes that a_init = 0
2054
+ if a_init:
2055
+ raise NotImplemented(
2056
+ f'A linear energy calibration is required at this time')
2057
+ a_fit = float(a*b_init**2)
2058
+ b_fit = float(2*a*b_init*c_init + b*b_init)
2059
+ c_fit = float(a*c_init**2 + b*c_init + c)
2060
+ peak_energies_fit = ((a*e_bragg_unconstrained + b)
2061
+ * e_bragg_unconstrained + c)
2062
+
2063
+ # Store the results in the detector object
2064
+ detector.tth_calibrated = float(tth_fit)
2065
+ detector.energy_calibration_coeffs = [
2066
+ float(a_fit), float(b_fit), float(c_fit)]
2067
+
2068
+ # Update the MCA channel energies with the newly calibrated
2069
+ # coefficients
2070
+ mca_bin_energies = detector.energies
495
2071
 
496
2072
  if interactive or save_figures:
497
2073
  # Third party modules
498
2074
  import matplotlib.pyplot as plt
499
2075
 
2076
+ # Update the peak energies and the MCA channel energies
2077
+ e_bragg_fit = get_peak_locations(ds_fit, tth_fit)
2078
+ mca_energies_fit = mca_bin_energies[mca_mask]
2079
+
2080
+ # Get an unconstrained fit
2081
+ if calibration_method != 'iterate_tth':
2082
+ if isinstance(fwhm_min, (int, float)):
2083
+ fwhm_min = fwhm_min*b_fit
2084
+ else:
2085
+ fwhm_min = None
2086
+ if isinstance(fwhm_max, (int, float)):
2087
+ fwhm_max = fwhm_max*b_fit
2088
+ else:
2089
+ fwhm_max = None
2090
+ models = []
2091
+ if detector.background is not None:
2092
+ if isinstance(detector.background, str):
2093
+ models.append(
2094
+ {'model': detector.background, 'prefix': 'bkgd_'})
2095
+ else:
2096
+ for model in detector.background:
2097
+ models.append(
2098
+ {'model': model, 'prefix': f'{model}_'})
2099
+ models.append(
2100
+ {'model': 'multipeak', 'centers': list(peak_energies_fit),
2101
+ 'centers_range': centers_range*b_fit,
2102
+ 'fwhm_min': fwhm_min, 'fwhm_max': fwhm_max})
2103
+ fit = FitProcessor()
2104
+ result = fit.process(
2105
+ NXdata(NXfield(mca_data_fit, 'y'),
2106
+ NXfield(mca_energies_fit, 'x')),
2107
+ {'models': models, 'method': 'trf'})
2108
+ best_fit_unconstrained = result.best_fit
2109
+ residual_unconstrained = result.residual
2110
+ e_bragg_unconstrained = np.sort(
2111
+ [result.best_values[f'peak{i+1}_center']
2112
+ for i in range(num_xrf, num_xrf+num_bragg)])
2113
+ a_unconstrained = np.sqrt(c_1_fit) * abs(
2114
+ get_peak_locations(e_bragg_unconstrained, tth_fit))
2115
+ strains_unconstrained = np.log(
2116
+ (a_unconstrained
2117
+ / calibration_config.material.lattice_parameters))
2118
+ strain_unconstrained = np.mean(strains_unconstrained)
2119
+
2120
+ # Create the figure
500
2121
  fig, axs = plt.subplots(2, 2, sharex='all', figsize=(11, 8.5))
2122
+ fig.suptitle(
2123
+ f'Detector {detector.detector_name} '
2124
+ r'2$\theta$ Calibration')
501
2125
 
502
- # Upper left axes: Input data & best fits
503
- axs[0,0].set_title('Ceria Calibration Fits')
2126
+ # Upper left axes: best fit with calibrated peak centers
2127
+ axs[0,0].set_title(r'2$\theta$ Calibration Fits')
504
2128
  axs[0,0].set_xlabel('Energy (keV)')
505
2129
  axs[0,0].set_ylabel('Intensity (a.u)')
506
- for i, hkl_E in enumerate(fit_E0):
507
- # KLS: annotate indicated HKLs w millier indices
508
- axs[0,0].axvline(hkl_E, color='k', linestyle='--')
509
- axs[0,0].text(hkl_E, 1, str(fit_hkls[i])[1:-1],
2130
+ for i, e_peak in enumerate(e_bragg_fit):
2131
+ axs[0,0].axvline(e_peak, c='k', ls='--')
2132
+ axs[0,0].text(e_peak, 1, str(hkls_fit[i])[1:-1],
510
2133
  ha='right', va='top', rotation=90,
511
2134
  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')
516
- #axs[0,0].plot(fit_mca_energies, MISSING?, label='least squares')
517
- axs[0,0].plot(fit_mca_energies, fit_mca_intensities,
518
- label='Flux-Corrected & Masked MCA Data')
2135
+ if flux_correct is None:
2136
+ axs[0,0].plot(
2137
+ mca_energies_fit, mca_data_fit, marker='.', c='C2', ms=3,
2138
+ ls='', label='MCA data')
2139
+ else:
2140
+ axs[0,0].plot(
2141
+ mca_energies_fit, mca_data_fit, marker='.', c='C2', ms=3,
2142
+ ls='', label='Flux-corrected MCA data')
2143
+ if calibration_method == 'iterate_tth':
2144
+ label_unconstrained = 'Unconstrained'
2145
+ else:
2146
+ if quadratic_energy_calibration:
2147
+ label_unconstrained = \
2148
+ 'Unconstrained fit using calibrated a, b, and c'
2149
+ else:
2150
+ label_unconstrained = \
2151
+ 'Unconstrained fit using calibrated b and c'
2152
+ if best_fit_uniform is None:
2153
+ axs[0,0].plot(
2154
+ mca_energies_fit, best_fit_unconstrained, c='C1',
2155
+ label=label_unconstrained)
2156
+ else:
2157
+ axs[0,0].plot(
2158
+ mca_energies_fit, best_fit_uniform, c='C0',
2159
+ label='Single strain')
2160
+ axs[0,0].plot(
2161
+ mca_energies_fit, best_fit_unconstrained, c='C1', ls='--',
2162
+ label=label_unconstrained)
519
2163
  axs[0,0].legend()
520
2164
 
521
- # Lower left axes: fit residuals
2165
+ # Lower left axes: fit residual
522
2166
  axs[1,0].set_title('Fit Residuals')
523
2167
  axs[1,0].set_xlabel('Energy (keV)')
524
2168
  axs[1,0].set_ylabel('Residual (a.u)')
525
- axs[1,0].plot(fit_mca_energies,
526
- uniform_fit.residual,
527
- label='Single Strain')
528
- axs[1,0].plot(fit_mca_energies,
529
- unconstrained_fit.residual,
530
- label='Unconstrained')
2169
+ if residual_uniform is None:
2170
+ axs[1,0].plot(
2171
+ mca_energies_fit, residual_unconstrained, c='C1',
2172
+ label=label_unconstrained)
2173
+ else:
2174
+ axs[1,0].plot(
2175
+ mca_energies_fit, residual_uniform, c='C0',
2176
+ label='Single strain')
2177
+ axs[1,0].plot(
2178
+ mca_energies_fit, residual_unconstrained, c='C1', ls='--',
2179
+ label=label_unconstrained)
531
2180
  axs[1,0].legend()
532
2181
 
533
2182
  # Upper right axes: E vs strain for each fit
534
2183
  axs[0,1].set_title('HKL Energy vs. Microstrain')
535
2184
  axs[0,1].set_xlabel('Energy (keV)')
536
2185
  axs[0,1].set_ylabel('Strain (\u03BC\u03B5)')
537
- axs[0,1].axhline(uniform_strain * 1e6,
538
- linestyle='--', label='Single Strain')
539
- axs[0,1].plot(fit_E0, unconstrained_strains * 1e6,
540
- color='C1', marker='s', label='Unconstrained')
541
- axs[0,1].axhline(unconstrained_strain * 1e6,
542
- color='C1', linestyle='--',
543
- label='Unconstrained: Unweighted Mean')
2186
+ if strain_uniform is not None:
2187
+ axs[0,1].axhline(strain_uniform * 1e6,
2188
+ ls='--', label='Single strain')
2189
+ axs[0,1].plot(e_bragg_fit, strains_unconstrained * 1e6,
2190
+ marker='o', mfc='none', c='C1',
2191
+ label='Unconstrained')
2192
+ axs[0,1].axhline(strain_unconstrained* 1e6,
2193
+ ls='--', c='C1',
2194
+ label='Unconstrained: unweighted mean')
544
2195
  axs[0,1].legend()
545
2196
 
546
- # Lower right axes: theoretical HKL E vs fit HKL E for
547
- # each fit
548
- axs[1,1].set_title('Theoretical vs. Fit HKL Energies')
2197
+ # Lower right axes: theoretical E vs fitted E for all peaks
2198
+ axs[1,1].set_title('Theoretical vs. Fitted Peak Energies')
549
2199
  axs[1,1].set_xlabel('Energy (keV)')
550
2200
  axs[1,1].set_ylabel('Energy (keV)')
551
- axs[1,1].plot(fit_E0, uniform_fit_centers,
552
- marker='o', label='Single Strain')
553
- axs[1,1].plot(fit_E0, unconstrained_fit_centers,
554
- linestyle='', marker='o', label='Unconstrained')
555
- axs[1,1].plot(slope * unconstrained_fit_centers + intercept,fit_E0,
556
- color='C1', label='Unconstrained: Linear Fit')
2201
+ if calibration_method == 'iterate_tth':
2202
+ e_fit = e_bragg_fit
2203
+ e_unconstrained = e_bragg_unconstrained
2204
+ if quadratic_energy_calibration:
2205
+ label = 'Unconstrained: quadratic fit'
2206
+ else:
2207
+ label = 'Unconstrained: linear fit'
2208
+ else:
2209
+ e_fit = np.concatenate((e_xrf, e_bragg_fit))
2210
+ e_unconstrained = np.concatenate(
2211
+ (e_xrf, e_bragg_unconstrained))
2212
+ if quadratic_energy_calibration:
2213
+ label = 'Quadratic fit'
2214
+ else:
2215
+ label = 'Linear fit'
2216
+ axs[1,1].plot(
2217
+ e_bragg_fit, e_bragg_uniform, marker='x', ls='',
2218
+ label='Single strain')
2219
+ axs[1,1].plot(
2220
+ e_fit, e_unconstrained, marker='o', mfc='none', ls='',
2221
+ label='Unconstrained')
2222
+ axs[1,1].plot(
2223
+ e_fit, peak_energies_fit, c='C1', label=label)
557
2224
  axs[1,1].legend()
2225
+ txt = 'Calibrated values:' \
2226
+ f'\nTakeoff angle:\n {tth_fit:.5f}$^\circ$'
2227
+ if quadratic_energy_calibration:
2228
+ txt += '\nQuadratic coefficient (a):' \
2229
+ f'\n {a_fit:.5e} $keV$/channel$^2$'
2230
+ txt += '\nLinear coefficient (b):' \
2231
+ f'\n {b_fit:.5f} $keV$/channel' \
2232
+ f'\nConstant offset (c):\n {c_fit:.5f}'
2233
+ axs[1,1].text(
2234
+ 0.98, 0.02, txt,
2235
+ ha='right', va='bottom', ma='left',
2236
+ transform=axs[1,1].transAxes,
2237
+ bbox=dict(boxstyle='round',
2238
+ ec=(1., 0.5, 0.5),
2239
+ fc=(1., 0.8, 0.8, 0.8)))
558
2240
 
559
2241
  fig.tight_layout()
560
2242
 
561
2243
  if save_figures:
562
- figfile = os.path.join(outputdir, 'ceria_calibration_fits.png')
2244
+ figfile = os.path.join(
2245
+ outputdir,
2246
+ f'{detector.detector_name}_tth_calibration_fits.png')
563
2247
  plt.savefig(figfile)
564
2248
  self.logger.info(f'Saved figure to {figfile}')
565
2249
  if interactive:
566
2250
  plt.show()
567
2251
 
568
- return float(tth), float(slope), float(intercept)
569
-
570
2252
 
571
2253
  class MCADataProcessor(Processor):
572
2254
  """A Processor to return data from an MCA, restuctured to
573
2255
  incorporate the shape & metadata associated with a map
574
2256
  configuration to which the MCA data belongs, and linearly
575
- transformed according to the results of a ceria calibration.
2257
+ transformed according to the results of a energy/tth calibration.
576
2258
  """
577
2259
 
578
2260
  def process(self,
579
2261
  data,
580
2262
  config=None,
581
2263
  save_figures=False,
582
- outputdir='.',
583
2264
  inputdir='.',
2265
+ outputdir='.',
584
2266
  interactive=False):
585
2267
  """Process configurations for a map and MCA detector(s), and
586
2268
  return the calibrated MCA data collected over the map.
587
2269
 
588
- :param data: Input map configuration and results of ceria
589
- calibration.
2270
+ :param data: Input map configuration and results of
2271
+ energy/tth calibration.
590
2272
  :type data: list[dict[str,object]]
591
2273
  :return: Calibrated and flux-corrected MCA data.
592
2274
  :rtype: nexusformat.nexus.NXentry
@@ -596,9 +2278,9 @@ class MCADataProcessor(Processor):
596
2278
  exit('Done Here')
597
2279
  map_config = self.get_config(
598
2280
  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)
2281
+ calibration_config = self.get_config(
2282
+ data, 'edd.models.MCATthCalibrationConfig', inputdir=inputdir)
2283
+ nxroot = self.get_nxroot(map_config, calibration_config)
602
2284
 
603
2285
  return nxroot
604
2286
 
@@ -613,7 +2295,7 @@ class MCADataProcessor(Processor):
613
2295
  :type map_config: CHAP.common.models.MapConfig.
614
2296
  :param calibration_config: The calibration configuration.
615
2297
  :type calibration_config:
616
- CHAP.edd.models.MCACeriaCalibrationConfig
2298
+ CHAP.edd.models.MCATthCalibrationConfig
617
2299
  :return: A map of the calibrated and flux-corrected MCA data.
618
2300
  :rtype: nexusformat.nexus.NXroot
619
2301
  """
@@ -673,6 +2355,87 @@ class MCADataProcessor(Processor):
673
2355
  return nxroot
674
2356
 
675
2357
 
2358
+ class MCACalibratedDataPlotter(Processor):
2359
+ """Convenience Processor for quickly visualizing calibrated MCA
2360
+ data from a single scan. Returns None!"""
2361
+ def process(self, data, spec_file, scan_number, scan_step_index=None,
2362
+ material=None, save_figures=False, interactive=False,
2363
+ outputdir='.'):
2364
+ """Show a maplotlib figure of the MCA data fom the scan
2365
+ provided on a calibrated energy axis. If `scan_step_index` is
2366
+ None, a plot of the sum of all spectra across the whole scan
2367
+ will be shown.
2368
+
2369
+ :param data: PipelineData containing an MCA calibration.
2370
+ :type data: list[PipelineData]
2371
+ :param spec_file: SPEC file containing scan of interest.
2372
+ :type spec_file: str
2373
+ :param scan_number: Scan number of interest.
2374
+ :type scan_number: int
2375
+ :param scan_step_index: Scan step index of interest, defaults to None.
2376
+ :type scan_step_index: int, optional
2377
+ :param material: Material parameters to plot HKLs for.
2378
+ :type material: dict
2379
+ :param save_figures: Save .pngs of plots for checking inputs &
2380
+ outputs of this Processor.
2381
+ :type save_figures: bool
2382
+ :param interactive: Allows for user interactions.
2383
+ :type interactive: bool
2384
+ :param outputdir: Directory to which any output figures will
2385
+ be saved.
2386
+ :type outputdir: str
2387
+ :returns: None
2388
+ :rtype: None
2389
+ """
2390
+ # Third party modules
2391
+ import matplotlib.pyplot as plt
2392
+
2393
+ # Local modules
2394
+ from CHAP.utils.scanparsers import SMBMCAScanParser as ScanParser
2395
+
2396
+ if material is not None:
2397
+ self.logger.warning('Plotting HKL lines is not supported yet.')
2398
+
2399
+ if scan_step_index is not None:
2400
+ if not isinstance(scan_step_index, int):
2401
+ try:
2402
+ scan_step_index = int(scan_step_index)
2403
+ except:
2404
+ msg = 'scan_step_index must be an int'
2405
+ self.logger.error(msg)
2406
+ raise TypeError(msg)
2407
+
2408
+ calibration_config = self.get_config(
2409
+ data, 'edd.models.MCATthCalibrationConfig')
2410
+ scanparser = ScanParser(spec_file, scan_number)
2411
+
2412
+ fig, ax = plt.subplots(1, 1, figsize=(11, 8.5))
2413
+ title = f'{scanparser.scan_title} MCA Data'
2414
+ if scan_step_index is None:
2415
+ title += ' (sum of all spectra in the scan)'
2416
+ ax.set_title(title)
2417
+ ax.set_xlabel('Calibrated energy (keV)')
2418
+ ax.set_ylabel('Intenstiy (a.u)')
2419
+ for detector in calibration_config.detectors:
2420
+ if scan_step_index is None:
2421
+ mca_data = np.sum(
2422
+ scanparser.get_all_detector_data(detector.detector_name),
2423
+ axis=0)
2424
+ else:
2425
+ mca_data = scanparser.get_detector_data(
2426
+ detector.detector_name, scan_step_index=scan_step_index)
2427
+ ax.plot(detector.energies, mca_data,
2428
+ label=f'Detector {detector.detector_name}')
2429
+ ax.legend()
2430
+ if interactive:
2431
+ plt.show()
2432
+ if save_figures:
2433
+ fig.savefig(os.path.join(
2434
+ outputdir, f'mca_data_{scanparser.scan_title}'))
2435
+ plt.close()
2436
+ return None
2437
+
2438
+
676
2439
  class StrainAnalysisProcessor(Processor):
677
2440
  """Processor that takes a map of MCA data and returns a map of
678
2441
  sample strains
@@ -680,20 +2443,26 @@ class StrainAnalysisProcessor(Processor):
680
2443
  def process(self,
681
2444
  data,
682
2445
  config=None,
2446
+ find_peaks=False,
683
2447
  save_figures=False,
684
- outputdir='.',
685
2448
  inputdir='.',
2449
+ outputdir='.',
686
2450
  interactive=False):
687
2451
  """Return strain analysis maps & associated metadata in an NXprocess.
688
2452
 
689
2453
  :param data: Input data containing configurations for a map,
690
- completed ceria calibration, and parameters for strain
2454
+ completed energy/tth calibration, and parameters for strain
691
2455
  analysis
692
2456
  :type data: list[PipelineData]
693
2457
  :param config: Initialization parameters for an instance of
694
2458
  CHAP.edd.models.StrainAnalysisConfig, defaults to
695
2459
  None.
696
2460
  :type config: dict, optional
2461
+ :param find_peaks: Exclude peaks where the average spectrum
2462
+ is below the `rel_height_cutoff` (in the detector
2463
+ configuration) cutoff relative to the maximum value of the
2464
+ average spectrum, defaults to `False`.
2465
+ :type find_peaks: bool, optional
697
2466
  :param save_figures: Save .pngs of plots for checking inputs &
698
2467
  outputs of this Processor, defaults to False.
699
2468
  :type save_figures: bool, optional
@@ -716,8 +2485,8 @@ class StrainAnalysisProcessor(Processor):
716
2485
 
717
2486
  """
718
2487
  # Get required configuration models from input data
719
- ceria_calibration_config = self.get_config(
720
- data, 'edd.models.MCACeriaCalibrationConfig', inputdir=inputdir)
2488
+ calibration_config = self.get_config(
2489
+ data, 'edd.models.MCATthCalibrationConfig', inputdir=inputdir)
721
2490
  try:
722
2491
  strain_analysis_config = self.get_config(
723
2492
  data, 'edd.models.StrainAnalysisConfig', inputdir=inputdir)
@@ -735,8 +2504,9 @@ class StrainAnalysisProcessor(Processor):
735
2504
 
736
2505
  nxroot = self.get_nxroot(
737
2506
  strain_analysis_config.map_config,
738
- ceria_calibration_config,
2507
+ calibration_config,
739
2508
  strain_analysis_config,
2509
+ find_peaks=find_peaks,
740
2510
  save_figures=save_figures,
741
2511
  outputdir=outputdir,
742
2512
  interactive=interactive)
@@ -746,8 +2516,9 @@ class StrainAnalysisProcessor(Processor):
746
2516
 
747
2517
  def get_nxroot(self,
748
2518
  map_config,
749
- ceria_calibration_config,
2519
+ calibration_config,
750
2520
  strain_analysis_config,
2521
+ find_peaks=False,
751
2522
  save_figures=False,
752
2523
  outputdir='.',
753
2524
  interactive=False):
@@ -756,13 +2527,18 @@ class StrainAnalysisProcessor(Processor):
756
2527
 
757
2528
  :param map_config: The map configuration.
758
2529
  :type map_config: CHAP.common.models.map.MapConfig
759
- :param ceria_calibration_config: The calibration configuration.
760
- :type ceria_calibration_config:
761
- 'CHAP.edd.models.MCACeriaCalibrationConfig'
2530
+ :param calibration_config: The calibration configuration.
2531
+ :type calibration_config:
2532
+ 'CHAP.edd.models.MCATthCalibrationConfig'
762
2533
  :param strain_analysis_config: Strain analysis processing
763
2534
  configuration.
764
2535
  :type strain_analysis_config:
765
2536
  CHAP.edd.models.StrainAnalysisConfig
2537
+ :param find_peaks: Exclude peaks where the average spectrum
2538
+ is below the `rel_height_cutoff` (in the detector
2539
+ configuration) cutoff relative to the maximum value of the
2540
+ average spectrum, defaults to `False`.
2541
+ :type find_peaks: bool, optional
766
2542
  :param save_figures: Save .pngs of plots for checking inputs &
767
2543
  outputs of this Processor, defaults to False.
768
2544
  :type save_figures: bool, optional
@@ -791,39 +2567,55 @@ class StrainAnalysisProcessor(Processor):
791
2567
  from CHAP.edd.utils import (
792
2568
  get_peak_locations,
793
2569
  get_unique_hkls_ds,
2570
+ get_spectra_fits
794
2571
  )
795
- from CHAP.utils.fit import FitMap
2572
+ if interactive or save_figures:
2573
+ from CHAP.edd.utils import (
2574
+ select_material_params,
2575
+ select_mask_and_hkls,
2576
+ )
796
2577
 
797
- def linkdims(nxgroup, field_dims=[]):
2578
+ def linkdims(nxgroup, field_dims=[], oversampling_axis={}):
798
2579
  if isinstance(field_dims, dict):
799
2580
  field_dims = [field_dims]
800
2581
  if map_config.map_type == 'structured':
801
2582
  axes = deepcopy(map_config.dims)
802
2583
  for dims in field_dims:
803
2584
  axes.append(dims['axes'])
804
- nxgroup.attrs['axes'] = axes
805
2585
  else:
806
2586
  axes = ['map_index']
807
2587
  for dims in field_dims:
808
2588
  axes.append(dims['axes'])
809
- nxgroup.attrs['axes'] = axes
810
2589
  nxgroup.attrs[f'map_index_indices'] = 0
811
2590
  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']
2591
+ if dim in oversampling_axis:
2592
+ bin_name = dim.replace('fly_', 'bin_')
2593
+ axes[axes.index(dim)] = bin_name
2594
+ nxgroup[bin_name] = NXfield(
2595
+ value=oversampling_axis[dim],
2596
+ units=nxentry.data[dim].units,
2597
+ attrs={
2598
+ 'long_name':
2599
+ f'oversampled {nxentry.data[dim].long_name}',
2600
+ 'data_type': nxentry.data[dim].data_type,
2601
+ 'local_name':
2602
+ f'oversampled {nxentry.data[dim].local_name}'})
2603
+ else:
2604
+ nxgroup.makelink(nxentry.data[dim])
2605
+ if f'{dim}_indices' in nxentry.data.attrs:
2606
+ nxgroup.attrs[f'{dim}_indices'] = \
2607
+ nxentry.data.attrs[f'{dim}_indices']
2608
+ nxgroup.attrs['axes'] = axes
816
2609
  for dims in field_dims:
817
2610
  nxgroup.attrs[f'{dims["axes"]}_indices'] = dims['index']
818
2611
 
819
- if len(strain_analysis_config.detectors) != 1:
820
- raise RuntimeError('Multiple detectors not tested')
821
- for detector in strain_analysis_config.detectors:
822
- calibration = [
823
- d for d in ceria_calibration_config.detectors \
824
- if d.detector_name == detector.detector_name][0]
825
- detector.add_calibration(calibration)
2612
+ if not interactive and not strain_analysis_config.materials:
2613
+ raise ValueError(
2614
+ 'No material provided. Provide a material in the '
2615
+ 'StrainAnalysis Configuration, or re-run the pipeline with '
2616
+ 'the --interactive flag.')
826
2617
 
2618
+ # Create the NXroot object
827
2619
  nxroot = NXroot()
828
2620
  nxroot[map_config.title] = MapProcessor.get_nxentry(map_config)
829
2621
  nxentry = nxroot[map_config.title]
@@ -837,84 +2629,174 @@ class StrainAnalysisProcessor(Processor):
837
2629
  nxdata = nxprocess.data
838
2630
  linkdims(nxdata)
839
2631
 
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)
2632
+ # Collect the raw MCA data
2633
+ self.logger.debug(f'Reading data ...')
847
2634
  mca_data = strain_analysis_config.mca_data()
2635
+ self.logger.debug(f'... done')
2636
+ self.logger.debug(f'mca_data.shape: {mca_data.shape}')
2637
+ if mca_data.ndim == 2:
2638
+ mca_data_summed = mca_data
2639
+ else:
2640
+ mca_data_summed = np.mean(
2641
+ mca_data, axis=tuple(np.arange(1, mca_data.ndim-1)))
2642
+ effective_map_shape = mca_data.shape[1:-1]
2643
+ self.logger.debug(f'mca_data_summed.shape: {mca_data_summed.shape}')
2644
+ self.logger.debug(f'effective_map_shape: {effective_map_shape}')
2645
+
2646
+ # Check for oversampling axis and create the binned coordinates
2647
+ oversampling_axis = {}
2648
+ if (map_config.attrs.get('scan_type') == 4
2649
+ and strain_analysis_config.sum_fly_axes):
2650
+ # Local modules
2651
+ from CHAP.utils.general import rolling_average
2652
+
2653
+ fly_axis = map_config.attrs.get('fly_axis_labels')[0]
2654
+ oversampling = strain_analysis_config.oversampling
2655
+ oversampling_axis[fly_axis] = rolling_average(
2656
+ nxdata[fly_axis].nxdata,
2657
+ start=oversampling.get('start', 0),
2658
+ end=oversampling.get('end'),
2659
+ width=oversampling.get('width'),
2660
+ stride=oversampling.get('stride'),
2661
+ num=oversampling.get('num'),
2662
+ mode=oversampling.get('mode', 'valid'))
2663
+
2664
+ # Loop over the detectors to adjust the material properties
2665
+ # and the mask and HKLs used in the strain analysis
2666
+ baselines = []
2667
+ for i, detector in enumerate(strain_analysis_config.detectors):
848
2668
 
849
- # Select interactive params / save figures
850
- if interactive or save_figures:
851
- # Third party modules
852
- import matplotlib.pyplot as plt
2669
+ # Get and add the calibration info to the detector
2670
+ calibration = [
2671
+ d for d in calibration_config.detectors \
2672
+ if d.detector_name == detector.detector_name][0]
2673
+ detector.add_calibration(calibration)
853
2674
 
854
- # Local modules
855
- from CHAP.edd.utils import (
856
- select_material_params,
857
- select_mask_and_hkls,
858
- )
2675
+ # Get the MCA bin energies
2676
+ mca_bin_energies = detector.energies
859
2677
 
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()
2678
+ # Blank out data below 25 keV as well as the last bin
2679
+ energy_mask = np.where(mca_bin_energies >= 25.0, 1, 0)
2680
+ energy_mask[-1] = 0
881
2681
 
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)
898
- detector.include_bin_ranges = include_bin_ranges
899
- detector.hkl_indices = hkl_indices
2682
+ # Subtract the baseline
2683
+ if detector.baseline:
2684
+ # Local modules
2685
+ from CHAP.edd.models import BaselineConfig
2686
+ from CHAP.common.processor import ConstructBaseline
2687
+
2688
+ if isinstance(detector.baseline, bool):
2689
+ detector.baseline = BaselineConfig()
900
2690
  if save_figures:
901
- fig.savefig(os.path.join(
2691
+ filename = os.path.join(
902
2692
  outputdir,
903
2693
  f'{detector.detector_name}_strainanalysis_'
904
- 'fit_mask_hkls.png'))
905
- plt.close()
906
- else:
907
- # ASK: can we assume same hkl_tth_tol and tth_max for
908
- # every detector in this part?
2694
+ 'baseline.png')
2695
+ else:
2696
+ filename = None
2697
+ baseline, baseline_config = \
2698
+ ConstructBaseline.construct_baseline(
2699
+ mca_data_summed[i], mask=energy_mask,
2700
+ tol=detector.baseline.tol, lam=detector.baseline.lam,
2701
+ max_iter=detector.baseline.max_iter,
2702
+ title=
2703
+ f'Baseline for detector {detector.detector_name}',
2704
+ xlabel='Energy (keV)', ylabel='Intensity (counts)',
2705
+ interactive=interactive, filename=filename)
2706
+
2707
+ mca_data_summed[i] -= baseline
2708
+ baselines.append(baseline)
2709
+ detector.baseline.lam = baseline_config['lambda']
2710
+ detector.baseline.attrs['num_iter'] = \
2711
+ baseline_config['num_iter']
2712
+ detector.baseline.attrs['error'] = baseline_config['error']
2713
+
2714
+ # Interactively adjust the material properties based on the
2715
+ # first detector calibration information and/or save figure
2716
+ # ASK: extend to multiple detectors?
2717
+ if not i and (interactive or save_figures):
2718
+
2719
+ tth = detector.tth_calibrated
2720
+ if save_figures:
2721
+ filename = os.path.join(
2722
+ outputdir,
2723
+ f'{detector.detector_name}_strainanalysis_'
2724
+ 'material_config.png')
2725
+ else:
2726
+ filename = None
2727
+ strain_analysis_config.materials = select_material_params(
2728
+ mca_bin_energies, mca_data_summed[i]*energy_mask, tth,
2729
+ preselected_materials=strain_analysis_config.materials,
2730
+ label='Sum of all spectra in the map',
2731
+ interactive=interactive, filename=filename)
2732
+ self.logger.debug(
2733
+ f'materials: {strain_analysis_config.materials}')
2734
+
2735
+ # Mask during calibration
2736
+ calibration_bin_ranges = calibration.include_bin_ranges
2737
+
909
2738
  # 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)
2739
+ # analysis materials
912
2740
  hkls, ds = get_unique_hkls_ds(
913
2741
  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)
2742
+ tth_tol=detector.hkl_tth_tol,
2743
+ tth_max=detector.tth_max)
916
2744
 
2745
+ # Interactively adjust the mask and HKLs used in the
2746
+ # strain analysis
2747
+ if save_figures:
2748
+ filename = os.path.join(
2749
+ outputdir,
2750
+ f'{detector.detector_name}_strainanalysis_'
2751
+ 'fit_mask_hkls.png')
2752
+ else:
2753
+ filename = None
2754
+ include_bin_ranges, hkl_indices = \
2755
+ select_mask_and_hkls(
2756
+ mca_bin_energies, mca_data_summed[i]*energy_mask,
2757
+ hkls, ds, detector.tth_calibrated,
2758
+ preselected_bin_ranges=detector.include_bin_ranges,
2759
+ preselected_hkl_indices=detector.hkl_indices,
2760
+ detector_name=detector.detector_name,
2761
+ ref_map=mca_data[i]*energy_mask,
2762
+ calibration_bin_ranges=calibration_bin_ranges,
2763
+ label='Sum of all spectra in the map',
2764
+ interactive=interactive, filename=filename)
2765
+ detector.include_energy_ranges = \
2766
+ detector.get_include_energy_ranges(include_bin_ranges)
2767
+ detector.hkl_indices = hkl_indices
2768
+ self.logger.debug(
2769
+ f'include_energy_ranges for detector {detector.detector_name}:'
2770
+ f' {detector.include_energy_ranges}')
2771
+ self.logger.debug(
2772
+ f'hkl_indices for detector {detector.detector_name}:'
2773
+ f' {detector.hkl_indices}')
2774
+ if not detector.include_energy_ranges:
2775
+ raise ValueError(
2776
+ 'No value provided for include_energy_ranges. '
2777
+ 'Provide them in the MCA Tth Calibration Configuration, '
2778
+ 'or re-run the pipeline with the --interactive flag.')
2779
+ if not detector.hkl_indices:
2780
+ raise ValueError(
2781
+ 'No value provided for hkl_indices. Provide them in '
2782
+ 'the detector\'s MCA Tth Calibration Configuration, or'
2783
+ ' re-run the pipeline with the --interactive flag.')
2784
+
2785
+ # Loop over the detectors to perform the strain analysis
917
2786
  for i, detector in enumerate(strain_analysis_config.detectors):
2787
+
2788
+ self.logger.info(f'Analysing detector {detector.detector_name}')
2789
+
2790
+ # Get the MCA bin energies
2791
+ mca_bin_energies = detector.energies
2792
+
2793
+ # Get the unique HKLs and lattice spacings for the strain
2794
+ # analysis materials
2795
+ hkls, ds = get_unique_hkls_ds(
2796
+ strain_analysis_config.materials,
2797
+ tth_tol=detector.hkl_tth_tol,
2798
+ tth_max=detector.tth_max)
2799
+
918
2800
  # Setup NXdata group
919
2801
  self.logger.debug(
920
2802
  f'Setting up NXdata group for {detector.detector_name}')
@@ -925,31 +2807,43 @@ class StrainAnalysisProcessor(Processor):
925
2807
  nxdetector.data = NXdata()
926
2808
  det_nxdata = nxdetector.data
927
2809
  linkdims(
928
- det_nxdata, {'axes': 'energy', 'index': len(map_config.shape)})
2810
+ det_nxdata,
2811
+ {'axes': 'energy', 'index': len(effective_map_shape)},
2812
+ oversampling_axis=oversampling_axis)
929
2813
  mask = detector.mca_mask()
930
- energies = mca_bin_energies[i][mask]
931
- det_nxdata.energy = NXfield(value=energies,
932
- attrs={'units': 'keV'})
2814
+ energies = mca_bin_energies[mask]
2815
+ det_nxdata.energy = NXfield(value=energies, attrs={'units': 'keV'})
933
2816
  det_nxdata.intensity = NXfield(
934
- dtype='uint16',
935
- shape=(*map_config.shape, len(energies)),
2817
+ dtype=np.float64,
2818
+ shape=(*effective_map_shape, len(energies)),
936
2819
  attrs={'units': 'counts'})
937
2820
  det_nxdata.tth = NXfield(
938
- dtype='float64',
939
- shape=map_config.shape,
2821
+ dtype=np.float64,
2822
+ shape=effective_map_shape,
940
2823
  attrs={'units':'degrees', 'long_name': '2\u03B8 (degrees)'}
941
2824
  )
942
- det_nxdata.microstrain = NXfield(
943
- dtype='float64',
944
- shape=map_config.shape,
945
- attrs={'long_name': 'Strain (\u03BC\u03B5)'})
2825
+ det_nxdata.uniform_microstrain = NXfield(
2826
+ dtype=np.float64,
2827
+ shape=effective_map_shape,
2828
+ attrs={'long_name':
2829
+ 'Strain from uniform fit(\u03BC\u03B5)'})
2830
+ det_nxdata.unconstrained_microstrain = NXfield(
2831
+ dtype=np.float64,
2832
+ shape=effective_map_shape,
2833
+ attrs={'long_name':
2834
+ 'Strain from unconstrained fit(\u03BC\u03B5)'})
946
2835
 
947
2836
  # Gather detector data
948
2837
  self.logger.debug(
949
2838
  f'Gathering detector data for {detector.detector_name}')
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]
2839
+ for map_index in np.ndindex(effective_map_shape):
2840
+ if baselines:
2841
+ det_nxdata.intensity[map_index] = \
2842
+ (mca_data[i][map_index]-baselines[i]).astype(
2843
+ np.float64)[mask]
2844
+ else:
2845
+ det_nxdata.intensity[map_index] = \
2846
+ mca_data[i][map_index].astype(np.float64)[mask]
953
2847
  det_nxdata.summed_intensity = det_nxdata.intensity.sum(axis=-1)
954
2848
 
955
2849
  # Perform strain analysis
@@ -958,52 +2852,69 @@ class StrainAnalysisProcessor(Processor):
958
2852
 
959
2853
  # Get the HKLs and lattice spacings that will be used for
960
2854
  # 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])
2855
+ hkls_fit = np.asarray([hkls[i] for i in detector.hkl_indices])
2856
+ ds_fit = np.asarray([ds[i] for i in detector.hkl_indices])
963
2857
  peak_locations = get_peak_locations(
964
- 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)]
2858
+ ds_fit, detector.tth_calibrated)
2859
+
2860
+ # Find peaks
2861
+ if not find_peaks or detector.rel_height_cutoff is None:
2862
+ use_peaks = np.ones((peak_locations.size)).astype(bool)
2863
+ else:
2864
+ # Third party modules
2865
+ from scipy.signal import find_peaks as find_peaks_scipy
2866
+
2867
+ # Local modules
2868
+ from CHAP.utils.general import index_nearest
2869
+
2870
+ peaks = find_peaks_scipy(
2871
+ mca_data_summed[i],
2872
+ height=(detector.rel_height_cutoff
2873
+ * mca_data_summed[i][mask].max()),
2874
+ width=5)
2875
+ heights = peaks[1]['peak_heights']
2876
+ widths = peaks[1]['widths']
2877
+ centers = [mca_bin_energies[v] for v in peaks[0]]
2878
+ use_peaks = np.zeros((peak_locations.size)).astype(bool)
2879
+ # RV Potentially use peak_heights/widths as initial
2880
+ # values in fit?
2881
+ # peak_heights = np.zeros((peak_locations.size))
2882
+ # peak_widths = np.zeros((peak_locations.size))
2883
+ delta = mca_bin_energies[1]-mca_bin_energies[0]
2884
+ for height, width, center in zip(heights, widths, centers):
2885
+ for n, loc in enumerate(peak_locations):
2886
+ # RV Hardwired range now, use detector.centers_range?
2887
+ if center-width*delta < loc < center+width*delta:
2888
+ use_peaks[n] = True
2889
+ # peak_heights[n] = height
2890
+ # peak_widths[n] = width*delta
2891
+ break
2892
+
2893
+ if any(use_peaks):
2894
+ self.logger.debug(
2895
+ f'Using peaks with centers at {peak_locations[use_peaks]}')
2896
+ else:
2897
+ self.logger.warning(
2898
+ 'No matching peaks with heights above the threshold, '
2899
+ f'skipping the fit for detector {detector.detector_name}')
2900
+ continue
2901
+
2902
+ # Perform the fit
2903
+ self.logger.debug(f'Fitting {detector.detector_name} ...')
2904
+ (uniform_fit_centers, uniform_fit_centers_errors,
2905
+ uniform_fit_amplitudes, uniform_fit_amplitudes_errors,
2906
+ uniform_fit_sigmas, uniform_fit_sigmas_errors,
2907
+ uniform_best_fit, uniform_residuals,
2908
+ uniform_redchi, uniform_success,
2909
+ unconstrained_fit_centers, unconstrained_fit_centers_errors,
2910
+ unconstrained_fit_amplitudes, unconstrained_fit_amplitudes_errors,
2911
+ unconstrained_fit_sigmas, unconstrained_fit_sigmas_errors,
2912
+ unconstrained_best_fit, unconstrained_residuals,
2913
+ unconstrained_redchi, unconstrained_success) = \
2914
+ get_spectra_fits(
2915
+ det_nxdata.intensity.nxdata, energies,
2916
+ peak_locations[use_peaks], detector)
2917
+ self.logger.debug(f'... done')
1007
2918
 
1008
2919
  # Add uniform fit results to the NeXus structure
1009
2920
  nxdetector.uniform_fit = NXcollection()
@@ -1013,24 +2924,26 @@ class StrainAnalysisProcessor(Processor):
1013
2924
  fit_nxgroup.results = NXdata()
1014
2925
  fit_nxdata = fit_nxgroup.results
1015
2926
  linkdims(
1016
- fit_nxdata, {'axes': 'energy', 'index': len(map_config.shape)})
2927
+ fit_nxdata,
2928
+ {'axes': 'energy', 'index': len(map_config.shape)},
2929
+ oversampling_axis=oversampling_axis)
1017
2930
  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
2931
+ fit_nxdata.best_fit = uniform_best_fit
2932
+ fit_nxdata.residuals = uniform_residuals
2933
+ fit_nxdata.redchi = uniform_redchi
2934
+ fit_nxdata.success = uniform_success
1022
2935
 
1023
2936
  # Peak-by-peak results
1024
2937
  # fit_nxgroup.fit_hkl_centers = NXdata()
1025
2938
  # fit_nxdata = fit_nxgroup.fit_hkl_centers
1026
2939
  # linkdims(fit_nxdata)
1027
2940
  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):
2941
+ amplitudes_fit, amplitudes_error, sigmas_fit,
2942
+ sigmas_error) in zip(
2943
+ hkls_fit, peak_locations,
2944
+ uniform_fit_centers, uniform_fit_centers_errors,
2945
+ uniform_fit_amplitudes, uniform_fit_amplitudes_errors,
2946
+ uniform_fit_sigmas, uniform_fit_sigmas_errors):
1034
2947
  hkl_name = '_'.join(str(hkl)[1:-1].split(' '))
1035
2948
  fit_nxgroup[hkl_name] = NXparameters()
1036
2949
  # Report initial HKL peak centers
@@ -1064,89 +2977,67 @@ class StrainAnalysisProcessor(Processor):
1064
2977
  value=sigmas_error)
1065
2978
  fit_nxgroup[hkl_name].sigmas.attrs['signal'] = 'values'
1066
2979
 
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
2980
  if interactive or save_figures:
1101
2981
  # Third party modules
1102
2982
  import matplotlib.animation as animation
2983
+ import matplotlib.pyplot as plt
1103
2984
 
1104
2985
  if save_figures:
1105
2986
  path = os.path.join(
1106
- outputdir, f'{detector.detector_name}_strainanalysis_'
1107
- 'unconstrained_fits')
2987
+ outputdir,
2988
+ f'{detector.detector_name}_strainanalysis_'
2989
+ 'unconstrained_fits')
1108
2990
  if not os.path.isdir(path):
1109
2991
  os.mkdir(path)
1110
2992
 
1111
2993
  def animate(i):
1112
- map_index = np.unravel_index(i, map_config.shape)
2994
+ map_index = np.unravel_index(i, effective_map_shape)
2995
+ norm = det_nxdata.intensity.nxdata[map_index].max()
1113
2996
  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()))
2997
+ det_nxdata.intensity.nxdata[map_index] / norm)
2998
+ best_fit.set_ydata(
2999
+ unconstrained_best_fit[map_index] / norm)
3000
+ index.set_text('\n'.join(
3001
+ [f'norm = {int(norm)}'] +
3002
+ ['relative norm = '
3003
+ f'{(norm / det_nxdata.intensity.max()):.5f}'] +
3004
+ [f'{k}[{i}] = {v}'
3005
+ for k, v in map_config.get_coords(map_index).items()]))
1121
3006
  if save_figures:
1122
- plt.savefig(os.path.join(path, f'frame_{i}.png'))
1123
- #return intensity, best_fit, residual, index
3007
+ plt.savefig(os.path.join(
3008
+ path, f'frame_{str(i).zfill(num_digit)}.png'))
1124
3009
  return intensity, best_fit, index
1125
3010
 
1126
3011
  fig, ax = plt.subplots()
1127
- map_index = np.unravel_index(0, map_config.shape)
3012
+ effective_map_shape
3013
+ map_index = np.unravel_index(0, effective_map_shape)
1128
3014
  data_normalized = (
1129
3015
  det_nxdata.intensity.nxdata[map_index]
1130
3016
  / det_nxdata.intensity.nxdata[map_index].max())
1131
3017
  intensity, = ax.plot(
1132
3018
  energies, data_normalized, 'b.', label='data')
1133
- fit_normalized = (fit.best_fit[map_index]
1134
- / fit.best_fit[map_index].max())
3019
+ if unconstrained_best_fit[map_index].max():
3020
+ fit_normalized = (
3021
+ unconstrained_best_fit[map_index]
3022
+ / unconstrained_best_fit[map_index].max())
3023
+ else:
3024
+ fit_normalized = unconstrained_best_fit[map_index]
1135
3025
  best_fit, = ax.plot(
1136
3026
  energies, fit_normalized, 'k-', label='fit')
1137
3027
  # residual, = ax.plot(
1138
- # energies, fit.residual[map_index], 'r-',
3028
+ # energies, unconstrained_residuals[map_index], 'r-',
1139
3029
  # label='residual')
1140
3030
  ax.set(
1141
3031
  title='Unconstrained fits',
1142
3032
  xlabel='Energy (keV)',
1143
- ylabel='Normalized Intensity (-)')
3033
+ ylabel='Normalized intensity (-)')
1144
3034
  ax.legend(loc='upper right')
1145
3035
  index = ax.text(
1146
3036
  0.05, 0.95, '', transform=ax.transAxes, va='top')
1147
3037
 
1148
- num_frames = int(det_nxdata.intensity.nxdata.size
3038
+ num_frame = int(det_nxdata.intensity.nxdata.size
1149
3039
  / det_nxdata.intensity.nxdata.shape[-1])
3040
+ num_digit = len(str(num_frame))
1150
3041
  if not save_figures:
1151
3042
  ani = animation.FuncAnimation(
1152
3043
  fig, animate,
@@ -1154,16 +3045,18 @@ class StrainAnalysisProcessor(Processor):
1154
3045
  / det_nxdata.intensity.nxdata.shape[-1]),
1155
3046
  interval=1000, blit=True, repeat=False)
1156
3047
  else:
1157
- for i in range(num_frames):
3048
+ for i in range(num_frame):
1158
3049
  animate(i)
1159
3050
 
1160
3051
  plt.close()
1161
3052
  plt.subplots_adjust(top=1, bottom=0, left=0, right=1)
1162
3053
 
1163
3054
  frames = []
1164
- for i in range(num_frames):
3055
+ for i in range(num_frame):
1165
3056
  frame = plt.imread(
1166
- os.path.join(path, f'frame_{i}.png'))
3057
+ os.path.join(
3058
+ path,
3059
+ f'frame_{str(i).zfill(num_digit)}.png'))
1167
3060
  im = plt.imshow(frame, animated=True)
1168
3061
  if not i:
1169
3062
  plt.imshow(frame)
@@ -1180,18 +3073,25 @@ class StrainAnalysisProcessor(Processor):
1180
3073
  path = os.path.join(
1181
3074
  outputdir,
1182
3075
  f'{detector.detector_name}_strainanalysis_'
1183
- 'unconstrained_fits.mp4')
3076
+ 'unconstrained_fits.gif')
1184
3077
  ani.save(path)
1185
3078
  plt.close()
1186
3079
 
1187
- tth_map = detector.get_tth_map(map_config)
3080
+ tth_map = detector.get_tth_map(effective_map_shape)
1188
3081
  det_nxdata.tth.nxdata = tth_map
1189
3082
  nominal_centers = np.asarray(
1190
- [get_peak_locations(d0, tth_map) for d0 in fit_ds])
3083
+ [get_peak_locations(d0, tth_map)
3084
+ for d0, use_peak in zip(ds_fit, use_peaks) if use_peak])
3085
+ uniform_strains = np.log(
3086
+ nominal_centers / uniform_fit_centers)
3087
+ uniform_strain = np.mean(uniform_strains, axis=0)
3088
+ det_nxdata.uniform_microstrain.nxdata = uniform_strain * 1e6
3089
+
1191
3090
  unconstrained_strains = np.log(
1192
3091
  nominal_centers / unconstrained_fit_centers)
1193
3092
  unconstrained_strain = np.mean(unconstrained_strains, axis=0)
1194
- det_nxdata.microstrain.nxdata = unconstrained_strain * 1e6
3093
+ det_nxdata.unconstrained_microstrain.nxdata = \
3094
+ unconstrained_strain * 1e6
1195
3095
 
1196
3096
  # Add unconstrained fit results to the NeXus structure
1197
3097
  nxdetector.unconstrained_fit = NXcollection()
@@ -1201,12 +3101,14 @@ class StrainAnalysisProcessor(Processor):
1201
3101
  fit_nxgroup.results = NXdata()
1202
3102
  fit_nxdata = fit_nxgroup.results
1203
3103
  linkdims(
1204
- fit_nxdata, {'axes': 'energy', 'index': len(map_config.shape)})
3104
+ fit_nxdata,
3105
+ {'axes': 'energy', 'index': len(map_config.shape)},
3106
+ oversampling_axis=oversampling_axis)
1205
3107
  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
3108
+ fit_nxdata.best_fit= unconstrained_best_fit
3109
+ fit_nxdata.residuals = unconstrained_residuals
3110
+ fit_nxdata.redchi = unconstrained_redchi
3111
+ fit_nxdata.success = unconstrained_success
1210
3112
 
1211
3113
  # Peak-by-peak results
1212
3114
  fit_nxgroup.fit_hkl_centers = NXdata()
@@ -1215,7 +3117,7 @@ class StrainAnalysisProcessor(Processor):
1215
3117
  for (hkl, center_guesses, centers_fit, centers_error,
1216
3118
  amplitudes_fit, amplitudes_error, sigmas_fit,
1217
3119
  sigmas_error) in zip(
1218
- fit_hkls, uniform_fit_centers,
3120
+ hkls_fit, uniform_fit_centers,
1219
3121
  unconstrained_fit_centers,
1220
3122
  unconstrained_fit_centers_errors,
1221
3123
  unconstrained_fit_amplitudes,
@@ -1261,6 +3163,25 @@ class StrainAnalysisProcessor(Processor):
1261
3163
  return nxroot
1262
3164
 
1263
3165
 
3166
+ class CreateStrainAnalysisConfigProcessor(Processor):
3167
+ """Processor that takes a basics stain analysis config file
3168
+ (the old style configuration without the map_config) and the output
3169
+ of EddMapReader and returns the old style stain analysis
3170
+ configuration.
3171
+ """
3172
+ def process(self, data, inputdir='.'):
3173
+ # Local modules
3174
+ from CHAP.common.models.map import MapConfig
3175
+
3176
+ map_config = self.get_config(
3177
+ data, 'common.models.map.MapConfig', inputdir=inputdir)
3178
+ config = self.get_config(
3179
+ data, 'edd.models.StrainAnalysisConfig', inputdir=inputdir)
3180
+ config.map_config = map_config
3181
+
3182
+ return config
3183
+
3184
+
1264
3185
  if __name__ == '__main__':
1265
3186
  # Local modules
1266
3187
  from CHAP.processor import main