ChessAnalysisPipeline 0.0.14__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/__init__.py +1 -1
- CHAP/common/__init__.py +9 -0
- CHAP/common/models/map.py +295 -55
- CHAP/common/processor.py +846 -10
- CHAP/common/reader.py +171 -0
- CHAP/common/writer.py +181 -18
- CHAP/edd/__init__.py +10 -3
- CHAP/edd/models.py +822 -451
- CHAP/edd/processor.py +2221 -756
- CHAP/edd/reader.py +672 -0
- CHAP/edd/utils.py +846 -292
- CHAP/foxden/__init__.py +6 -0
- CHAP/foxden/processor.py +42 -0
- CHAP/foxden/writer.py +65 -0
- CHAP/pipeline.py +1 -1
- CHAP/runner.py +4 -4
- CHAP/tomo/models.py +7 -5
- CHAP/tomo/processor.py +118 -39
- CHAP/utils/__init__.py +1 -0
- CHAP/utils/fit.py +1292 -1315
- CHAP/utils/general.py +393 -53
- CHAP/utils/models.py +567 -0
- CHAP/utils/scanparsers.py +141 -28
- ChessAnalysisPipeline-0.0.15.dist-info/LICENSE +60 -0
- {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.15.dist-info}/METADATA +1 -1
- {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.15.dist-info}/RECORD +29 -25
- {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.15.dist-info}/WHEEL +1 -1
- ChessAnalysisPipeline-0.0.14.dist-info/LICENSE +0 -21
- {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.15.dist-info}/entry_points.txt +0 -0
- {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.15.dist-info}/top_level.txt +0 -0
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
|
|
@@ -128,40 +130,43 @@ class DiffractionVolumeLengthProcessor(Processor):
|
|
|
128
130
|
# Get raw MCA data from raster scan
|
|
129
131
|
mca_data = dvl_config.mca_data(detector)
|
|
130
132
|
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
135
139
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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(
|
|
140
151
|
np.sum(mca_data, axis=0),
|
|
141
|
-
x=np.
|
|
152
|
+
x=np.arange(detector.num_bins, dtype=np.int16),
|
|
142
153
|
label='Sum of MCA spectra over all scan points',
|
|
143
154
|
preselected_index_ranges=detector.include_bin_ranges,
|
|
144
155
|
title='Click and drag to select data range to include when '
|
|
145
156
|
'measuring diffraction volume length',
|
|
146
|
-
xlabel='Uncalibrated
|
|
157
|
+
xlabel='Uncalibrated energy (keV)',
|
|
147
158
|
ylabel='MCA intensity (counts)',
|
|
148
159
|
min_num_index_ranges=1,
|
|
149
|
-
interactive=interactive)
|
|
150
|
-
detector.include_energy_ranges = detector.get_energy_ranges(
|
|
151
|
-
include_bin_ranges)
|
|
160
|
+
interactive=interactive, filename=filename)
|
|
152
161
|
self.logger.debug(
|
|
153
|
-
'Mask selected. Including detector
|
|
154
|
-
+ str(detector.
|
|
155
|
-
|
|
156
|
-
fig.savefig(os.path.join(
|
|
157
|
-
outputdir, f'{detector.detector_name}_dvl_mask.png'))
|
|
158
|
-
plt.close()
|
|
159
|
-
if not detector.include_energy_ranges:
|
|
162
|
+
'Mask selected. Including detector bin ranges: '
|
|
163
|
+
+ str(detector.include_bin_ranges))
|
|
164
|
+
if not detector.include_bin_ranges:
|
|
160
165
|
raise ValueError(
|
|
161
|
-
'No value provided for
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
'No value provided for include_bin_ranges. '
|
|
167
|
+
'Provide them in the Diffraction Volume Length '
|
|
168
|
+
'Measurement Configuration, or re-run the pipeline '
|
|
169
|
+
'with the --interactive flag.')
|
|
165
170
|
|
|
166
171
|
# Reduce the raw MCA data in 3 ways:
|
|
167
172
|
# 1) sum of intensities in all detector bins
|
|
@@ -195,17 +200,18 @@ class DiffractionVolumeLengthProcessor(Processor):
|
|
|
195
200
|
detector.fit_sigma = fit.best_values['sigma']
|
|
196
201
|
if detector.measurement_mode == 'manual':
|
|
197
202
|
if interactive:
|
|
198
|
-
_,
|
|
203
|
+
_, dvl_bounds = select_mask_1d(
|
|
199
204
|
masked_sum, x=x,
|
|
200
205
|
label='Total (masked & normalized)',
|
|
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=('Beam
|
|
210
|
+
xlabel=('Beam direction (offset from scan "center")'),
|
|
206
211
|
ylabel='MCA intensity (normalized)',
|
|
207
212
|
min_num_index_ranges=1,
|
|
208
|
-
max_num_index_ranges=1
|
|
213
|
+
max_num_index_ranges=1,
|
|
214
|
+
interactive=interactive)
|
|
209
215
|
dvl_bounds = dvl_bounds[0]
|
|
210
216
|
dvl = abs(x[dvl_bounds[1]] - x[dvl_bounds[0]])
|
|
211
217
|
else:
|
|
@@ -220,7 +226,7 @@ class DiffractionVolumeLengthProcessor(Processor):
|
|
|
220
226
|
|
|
221
227
|
fig, ax = plt.subplots()
|
|
222
228
|
ax.set_title(f'Diffraction Volume ({detector.detector_name})')
|
|
223
|
-
ax.set_xlabel('Beam
|
|
229
|
+
ax.set_xlabel('Beam direction (offset from scan "center")')
|
|
224
230
|
ax.set_ylabel('MCA intensity (normalized)')
|
|
225
231
|
ax.plot(x, masked_sum, label='total (masked & normalized)')
|
|
226
232
|
ax.plot(x, fit.best_fit, label='gaussian fit (to total)')
|
|
@@ -263,8 +269,20 @@ class LatticeParameterRefinementProcessor(Processor):
|
|
|
263
269
|
"""Given a strain analysis configuration, return a copy
|
|
264
270
|
contining refined values for the materials' lattice
|
|
265
271
|
parameters."""
|
|
266
|
-
|
|
267
|
-
|
|
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)
|
|
268
286
|
try:
|
|
269
287
|
strain_analysis_config = self.get_config(
|
|
270
288
|
data, 'edd.models.StrainAnalysisConfig', inputdir=inputdir)
|
|
@@ -285,32 +303,84 @@ class LatticeParameterRefinementProcessor(Processor):
|
|
|
285
303
|
self.logger.error('Not implemented for multiple materials')
|
|
286
304
|
raise NotImplementedError(msg)
|
|
287
305
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
self.logger.debug(f'
|
|
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())
|
|
292
335
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
296
356
|
|
|
297
357
|
def refine_lattice_parameters(
|
|
298
|
-
self, strain_analysis_config,
|
|
299
|
-
|
|
358
|
+
self, strain_analysis_config, calibration_config, detector,
|
|
359
|
+
mca_data, mca_data_summed, nxsubentry, interactive, save_figures,
|
|
360
|
+
outputdir):
|
|
300
361
|
"""Return refined values for the lattice parameters of the
|
|
301
362
|
materials indicated in `strain_analysis_config`. Method: given
|
|
302
|
-
a scan of a material, fit the peaks of each MCA
|
|
303
|
-
|
|
304
|
-
lattice parameters that would produce them.
|
|
305
|
-
|
|
306
|
-
all spectra.
|
|
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.
|
|
307
368
|
|
|
308
369
|
:param strain_analysis_config: Strain analysis configuration
|
|
309
|
-
:type strain_analysis_config:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
:
|
|
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
|
|
314
384
|
:param interactive: Boolean to indicate whether interactive
|
|
315
385
|
matplotlib figures should be presented
|
|
316
386
|
:type interactive: bool
|
|
@@ -324,31 +394,202 @@ class LatticeParameterRefinementProcessor(Processor):
|
|
|
324
394
|
`strain_analysis_config`
|
|
325
395
|
:rtype: list[numpy.ndarray]
|
|
326
396
|
"""
|
|
327
|
-
|
|
328
|
-
from
|
|
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
|
|
329
405
|
|
|
330
|
-
|
|
331
|
-
|
|
406
|
+
# Local modules
|
|
407
|
+
from CHAP.edd.utils import (
|
|
408
|
+
get_peak_locations,
|
|
409
|
+
get_spectra_fits,
|
|
410
|
+
get_unique_hkls_ds,
|
|
411
|
+
)
|
|
332
412
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
|
336
481
|
hkls, ds = get_unique_hkls_ds(
|
|
337
482
|
strain_analysis_config.materials,
|
|
338
483
|
tth_tol=detector.hkl_tth_tol,
|
|
339
484
|
tth_max=detector.tth_max)
|
|
340
485
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
self.select_fit_mask_hkls(
|
|
348
|
-
strain_analysis_config, detector_i, mca_data, mca_bin_energies,
|
|
349
|
-
hkls, ds,
|
|
350
|
-
interactive, save_figures, outputdir)
|
|
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
|
+
)
|
|
351
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
|
|
352
593
|
(uniform_fit_centers, uniform_fit_centers_errors,
|
|
353
594
|
uniform_fit_amplitudes, uniform_fit_amplitudes_errors,
|
|
354
595
|
uniform_fit_sigmas, uniform_fit_sigmas_errors,
|
|
@@ -359,261 +600,734 @@ class LatticeParameterRefinementProcessor(Processor):
|
|
|
359
600
|
unconstrained_fit_sigmas, unconstrained_fit_sigmas_errors,
|
|
360
601
|
unconstrained_best_fit, unconstrained_residuals,
|
|
361
602
|
unconstrained_redchi, unconstrained_success) = \
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
365
640
|
|
|
366
641
|
# Get the interplanar spacings measured for each fit HKL peak
|
|
367
|
-
# at every point in the map
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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())
|
|
385
675
|
|
|
386
|
-
|
|
387
|
-
|
|
676
|
+
if interactive or save_figures:
|
|
677
|
+
# Third party modules
|
|
678
|
+
import matplotlib.pyplot as plt
|
|
388
679
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
:type strain_analysis_config: CHAP.edd.models.StrainAnalysisConfig
|
|
414
|
-
:param ceria_calibration_config: Configuration of a completed
|
|
415
|
-
ceria calibration containing a list of detector swith the
|
|
416
|
-
same names as those in `strain_analysis_config`
|
|
417
|
-
:returns: None"""
|
|
418
|
-
for detector in strain_analysis_config.detectors:
|
|
419
|
-
calibration = [
|
|
420
|
-
d for d in ceria_calibration_config.detectors \
|
|
421
|
-
if d.detector_name == detector.detector_name][0]
|
|
422
|
-
detector.add_calibration(calibration)
|
|
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()
|
|
423
704
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
mca_data, mca_bin_energies, hkls, ds,
|
|
427
|
-
interactive, save_figures, outputdir):
|
|
428
|
-
"""Select the maks & HKLs to use for fitting for each
|
|
429
|
-
detector. Update `strain_analysis_config` with new values for
|
|
430
|
-
`hkl_indices` and `include_bin_ranges` if needed.
|
|
705
|
+
return [
|
|
706
|
+
a_uniform, a_uniform, a_uniform, 90., 90., 90.]
|
|
431
707
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
|
459
782
|
"""
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
fig, include_bin_ranges, hkl_indices = \
|
|
468
|
-
select_mask_and_hkls(
|
|
469
|
-
mca_bin_energies[detector_i],
|
|
470
|
-
np.sum(mca_data[detector_i], axis=0),
|
|
471
|
-
hkls, ds,
|
|
472
|
-
detector.tth_calibrated,
|
|
473
|
-
detector.include_bin_ranges, detector.hkl_indices,
|
|
474
|
-
detector.detector_name, mca_data[detector_i],
|
|
475
|
-
calibration_bin_ranges=detector.calibration_bin_ranges,
|
|
476
|
-
label='Sum of all spectra in the map',
|
|
477
|
-
interactive=interactive)
|
|
478
|
-
detector.include_energy_ranges = detector.get_energy_ranges(
|
|
479
|
-
include_bin_ranges)
|
|
480
|
-
detector.hkl_indices = hkl_indices
|
|
481
|
-
if save_figures:
|
|
482
|
-
fig.savefig(os.path.join(
|
|
483
|
-
outputdir,
|
|
484
|
-
f'{detector.detector_name}_strainanalysis_'
|
|
485
|
-
'fit_mask_hkls.png'))
|
|
486
|
-
plt.close()
|
|
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
|
+
)
|
|
487
790
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
|
494
802
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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.
|
|
511
880
|
:type save_figures: bool
|
|
512
|
-
:param
|
|
513
|
-
|
|
881
|
+
:param interactive: Allows for user interactions.
|
|
882
|
+
:type interactive: bool
|
|
883
|
+
:param outputdir: Directory to which any output figures will
|
|
884
|
+
be saved.
|
|
514
885
|
:type outputdir: str
|
|
515
|
-
:returns:
|
|
886
|
+
:returns: Slope and intercept for linearly correcting the
|
|
887
|
+
detector's MCA channels to bin energies.
|
|
888
|
+
:rtype: tuple[float, float]
|
|
516
889
|
"""
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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)
|
|
527
953
|
self.logger.debug(
|
|
528
|
-
|
|
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)
|
|
529
961
|
if save_figures:
|
|
530
|
-
|
|
531
|
-
strain_analysis_config.detectors[detector_i].detector_name
|
|
532
|
-
fig.savefig(os.path.join(
|
|
962
|
+
filename = os.path.join(
|
|
533
963
|
outputdir,
|
|
534
|
-
f'{detector_name}
|
|
535
|
-
|
|
536
|
-
|
|
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'})
|
|
537
993
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
|
543
1014
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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)
|
|
592
1239
|
|
|
1240
|
+
plt.show()
|
|
593
1241
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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.
|
|
598
1267
|
"""
|
|
599
1268
|
|
|
600
1269
|
def process(self,
|
|
601
1270
|
data,
|
|
602
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,
|
|
603
1281
|
save_figures=False,
|
|
604
1282
|
inputdir='.',
|
|
605
1283
|
outputdir='.',
|
|
606
1284
|
interactive=False):
|
|
607
|
-
"""Return
|
|
608
|
-
|
|
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.
|
|
609
1288
|
|
|
610
1289
|
:param data: Input configuration for the raw data & tuning
|
|
611
1290
|
procedure.
|
|
612
1291
|
:type data: list[PipelineData]
|
|
613
1292
|
:param config: Initialization parameters for an instance of
|
|
614
|
-
CHAP.edd.models.
|
|
1293
|
+
CHAP.edd.models.MCATthCalibrationConfig, defaults to
|
|
615
1294
|
None.
|
|
616
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 ± `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
|
|
617
1331
|
:param save_figures: Save .pngs of plots for checking inputs &
|
|
618
1332
|
outputs of this Processor, defaults to False.
|
|
619
1333
|
:type save_figures: bool, optional
|
|
@@ -632,38 +1346,81 @@ class MCACeriaCalibrationProcessor(Processor):
|
|
|
632
1346
|
2&theta and the linear correction parameters added.
|
|
633
1347
|
:rtype: dict[str,float]
|
|
634
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
|
+
|
|
635
1356
|
try:
|
|
636
1357
|
calibration_config = self.get_config(
|
|
637
|
-
data, 'edd.models.
|
|
1358
|
+
data, 'edd.models.MCATthCalibrationConfig',
|
|
1359
|
+
calibration_method=calibration_method,
|
|
638
1360
|
inputdir=inputdir)
|
|
639
1361
|
except Exception as data_exc:
|
|
640
1362
|
self.logger.info('No valid calibration config in input pipeline '
|
|
641
1363
|
'data, using config parameter instead.')
|
|
642
1364
|
try:
|
|
643
1365
|
# Local modules
|
|
644
|
-
from CHAP.edd.models import
|
|
1366
|
+
from CHAP.edd.models import MCATthCalibrationConfig
|
|
645
1367
|
|
|
646
|
-
calibration_config =
|
|
647
|
-
**config,
|
|
1368
|
+
calibration_config = MCATthCalibrationConfig(
|
|
1369
|
+
**config, calibration_method=calibration_method,
|
|
1370
|
+
inputdir=inputdir)
|
|
1371
|
+
except Exception as dict_exc:
|
|
1372
|
+
raise RuntimeError from dict_exc
|
|
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)
|
|
648
1392
|
except Exception as dict_exc:
|
|
649
1393
|
raise RuntimeError from dict_exc
|
|
650
1394
|
|
|
1395
|
+
self.logger.debug(f'In process: save_figures = {save_figures}; '
|
|
1396
|
+
f'interactive = {interactive}')
|
|
1397
|
+
|
|
651
1398
|
for detector in calibration_config.detectors:
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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)
|
|
658
1411
|
|
|
659
1412
|
return calibration_config.dict()
|
|
660
1413
|
|
|
661
1414
|
def calibrate(self,
|
|
662
1415
|
calibration_config,
|
|
663
1416
|
detector,
|
|
1417
|
+
quadratic_energy_calibration=False,
|
|
1418
|
+
centers_range=20,
|
|
1419
|
+
fwhm_min=None,
|
|
1420
|
+
fwhm_max=None,
|
|
664
1421
|
save_figures=False,
|
|
665
|
-
|
|
666
|
-
|
|
1422
|
+
interactive=False,
|
|
1423
|
+
outputdir='.'):
|
|
667
1424
|
"""Iteratively calibrate 2&theta by fitting selected peaks of
|
|
668
1425
|
an MCA spectrum until the computed strain is sufficiently
|
|
669
1426
|
small. Use the fitted peak locations to determine linear
|
|
@@ -672,27 +1429,56 @@ class MCACeriaCalibrationProcessor(Processor):
|
|
|
672
1429
|
:param calibration_config: Object configuring the CeO2
|
|
673
1430
|
calibration procedure for an MCA detector.
|
|
674
1431
|
:type calibration_config:
|
|
675
|
-
CHAP.edd.models.
|
|
1432
|
+
CHAP.edd.models.MCATthCalibrationConfig
|
|
676
1433
|
:param detector: A single MCA detector element configuration.
|
|
677
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 ± `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
|
|
678
1450
|
:param save_figures: Save .pngs of plots for checking inputs &
|
|
679
1451
|
outputs of this Processor, defaults to False.
|
|
680
1452
|
:type save_figures: bool, optional
|
|
681
|
-
:param outputdir: Directory to which any output figures will
|
|
682
|
-
be saved, defaults to '.'.
|
|
683
|
-
:type outputdir: str, optional
|
|
684
1453
|
:param interactive: Allows for user interactions, defaults to
|
|
685
1454
|
False.
|
|
686
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
|
|
687
1459
|
:raises ValueError: No value provided for included bin ranges
|
|
688
1460
|
or the fitted HKLs for the MCA detector element.
|
|
689
|
-
:return: Calibrated values of 2&theta and the linear correction
|
|
690
|
-
parameters for MCA channel energies: tth, slope, intercept.
|
|
691
|
-
:rtype: float, float, float
|
|
692
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
|
+
|
|
693
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
|
+
)
|
|
694
1475
|
from CHAP.edd.utils import get_peak_locations
|
|
695
|
-
from CHAP.utils.fit import
|
|
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
|
|
696
1482
|
|
|
697
1483
|
# Get the unique HKLs and lattice spacings for the calibration
|
|
698
1484
|
# material
|
|
@@ -700,219 +1486,752 @@ class MCACeriaCalibrationProcessor(Processor):
|
|
|
700
1486
|
tth_tol=detector.hkl_tth_tol, tth_max=detector.tth_max)
|
|
701
1487
|
|
|
702
1488
|
# Collect raw MCA data of interest
|
|
703
|
-
mca_bin_energies =
|
|
704
|
-
0, detector.max_energy_kev, detector.num_bins)
|
|
1489
|
+
mca_bin_energies = detector.energies
|
|
705
1490
|
mca_data = calibration_config.mca_data(detector)
|
|
706
|
-
if interactive or save_figures:
|
|
707
|
-
# Third party modules
|
|
708
|
-
import matplotlib.pyplot as plt
|
|
709
1491
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
)
|
|
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
|
|
715
1496
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
if save_figures:
|
|
721
|
-
fig.savefig(os.path.join(
|
|
722
|
-
outputdir,
|
|
723
|
-
f'{detector.detector_name}_calibration_'
|
|
724
|
-
'tth_initial_guess.png'))
|
|
725
|
-
plt.close()
|
|
1497
|
+
# Subtract the baseline
|
|
1498
|
+
if detector.baseline:
|
|
1499
|
+
# Local modules
|
|
1500
|
+
from CHAP.common.processor import ConstructBaseline
|
|
726
1501
|
|
|
727
|
-
# Select mask & HKLs for fitting
|
|
728
|
-
fig, include_bin_ranges, hkl_indices = select_mask_and_hkls(
|
|
729
|
-
mca_bin_energies, mca_data, hkls, ds,
|
|
730
|
-
detector.tth_initial_guess, detector.include_bin_ranges,
|
|
731
|
-
detector.hkl_indices, detector.detector_name,
|
|
732
|
-
flux_energy_range=calibration_config.flux_file_energy_range,
|
|
733
|
-
label='MCA data',
|
|
734
|
-
interactive=interactive)
|
|
735
|
-
detector.include_energy_ranges = detector.get_energy_ranges(
|
|
736
|
-
include_bin_ranges)
|
|
737
|
-
detector.hkl_indices = hkl_indices
|
|
738
1502
|
if save_figures:
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
|
743
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)
|
|
744
1559
|
self.logger.debug(
|
|
745
1560
|
f'include_energy_ranges = {detector.include_energy_ranges}')
|
|
1561
|
+
self.logger.debug(
|
|
1562
|
+
f'hkl_indices = {detector.hkl_indices}')
|
|
746
1563
|
if not detector.include_energy_ranges:
|
|
747
1564
|
raise ValueError(
|
|
748
1565
|
'No value provided for include_energy_ranges. '
|
|
749
|
-
'Provide them in the MCA
|
|
1566
|
+
'Provide them in the MCA Tth Calibration Configuration '
|
|
750
1567
|
'or re-run the pipeline with the --interactive flag.')
|
|
751
1568
|
if not detector.hkl_indices:
|
|
752
1569
|
raise ValueError(
|
|
753
|
-
'
|
|
754
|
-
'
|
|
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 '
|
|
755
1573
|
're-run the pipeline with the --interactive flag.')
|
|
756
1574
|
mca_mask = detector.mca_mask()
|
|
757
|
-
|
|
758
|
-
fit_mca_intensities = mca_data[mca_mask]
|
|
1575
|
+
mca_data_fit = mca_data[mca_mask]
|
|
759
1576
|
|
|
760
1577
|
# Correct raw MCA data for variable flux at different energies
|
|
761
1578
|
flux_correct = \
|
|
762
1579
|
calibration_config.flux_correction_interpolation_function()
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
#
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
#
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
#
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
(
|
|
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
|
|
826
1705
|
/ calibration_config.material.lattice_parameters))
|
|
827
|
-
|
|
828
|
-
unconstrained_tth = tth * (1.0 + unconstrained_strain)
|
|
829
|
-
|
|
830
|
-
# Update tth for the next iteration of tuning
|
|
831
|
-
prev_tth = tth
|
|
832
|
-
tth = unconstrained_tth
|
|
1706
|
+
strain_uniform = np.mean(strains_uniform)
|
|
833
1707
|
|
|
834
|
-
|
|
835
|
-
#
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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)])
|
|
1883
|
+
|
|
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
|
|
845
2071
|
|
|
846
2072
|
if interactive or save_figures:
|
|
847
2073
|
# Third party modules
|
|
848
2074
|
import matplotlib.pyplot as plt
|
|
849
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
|
|
850
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')
|
|
851
2125
|
|
|
852
|
-
# Upper left axes:
|
|
853
|
-
axs[0,0].set_title('
|
|
2126
|
+
# Upper left axes: best fit with calibrated peak centers
|
|
2127
|
+
axs[0,0].set_title(r'2$\theta$ Calibration Fits')
|
|
854
2128
|
axs[0,0].set_xlabel('Energy (keV)')
|
|
855
2129
|
axs[0,0].set_ylabel('Intensity (a.u)')
|
|
856
|
-
for i,
|
|
857
|
-
|
|
858
|
-
axs[0,0].
|
|
859
|
-
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],
|
|
860
2133
|
ha='right', va='top', rotation=90,
|
|
861
2134
|
transform=axs[0,0].get_xaxis_transform())
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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)
|
|
869
2163
|
axs[0,0].legend()
|
|
870
2164
|
|
|
871
|
-
# Lower left axes: fit
|
|
2165
|
+
# Lower left axes: fit residual
|
|
872
2166
|
axs[1,0].set_title('Fit Residuals')
|
|
873
2167
|
axs[1,0].set_xlabel('Energy (keV)')
|
|
874
2168
|
axs[1,0].set_ylabel('Residual (a.u)')
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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)
|
|
881
2180
|
axs[1,0].legend()
|
|
882
2181
|
|
|
883
2182
|
# Upper right axes: E vs strain for each fit
|
|
884
2183
|
axs[0,1].set_title('HKL Energy vs. Microstrain')
|
|
885
2184
|
axs[0,1].set_xlabel('Energy (keV)')
|
|
886
2185
|
axs[0,1].set_ylabel('Strain (\u03BC\u03B5)')
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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')
|
|
894
2195
|
axs[0,1].legend()
|
|
895
2196
|
|
|
896
|
-
# Lower right axes: theoretical
|
|
897
|
-
|
|
898
|
-
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')
|
|
899
2199
|
axs[1,1].set_xlabel('Energy (keV)')
|
|
900
2200
|
axs[1,1].set_ylabel('Energy (keV)')
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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)
|
|
907
2224
|
axs[1,1].legend()
|
|
908
|
-
|
|
909
|
-
|
|
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}'
|
|
910
2233
|
axs[1,1].text(
|
|
911
|
-
0.98, 0.02,
|
|
912
|
-
'Calibrated Values:\n\n'
|
|
913
|
-
+ f'Takeoff Angle:\n {tth:.5f}$^\circ$\n\n'
|
|
914
|
-
+ f'Slope:\n {slope:.5f}\n\n'
|
|
915
|
-
+ f'Intercept:\n {intercept:.5f} $keV$',
|
|
2234
|
+
0.98, 0.02, txt,
|
|
916
2235
|
ha='right', va='bottom', ma='left',
|
|
917
2236
|
transform=axs[1,1].transAxes,
|
|
918
2237
|
bbox=dict(boxstyle='round',
|
|
@@ -922,155 +2241,20 @@ class MCACeriaCalibrationProcessor(Processor):
|
|
|
922
2241
|
fig.tight_layout()
|
|
923
2242
|
|
|
924
2243
|
if save_figures:
|
|
925
|
-
figfile = os.path.join(
|
|
2244
|
+
figfile = os.path.join(
|
|
2245
|
+
outputdir,
|
|
2246
|
+
f'{detector.detector_name}_tth_calibration_fits.png')
|
|
926
2247
|
plt.savefig(figfile)
|
|
927
2248
|
self.logger.info(f'Saved figure to {figfile}')
|
|
928
2249
|
if interactive:
|
|
929
2250
|
plt.show()
|
|
930
2251
|
|
|
931
|
-
return float(tth), float(slope), float(intercept)
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
class MCAEnergyCalibrationProcessor(Processor):
|
|
935
|
-
"""Processor to return parameters for linearly transforming MCA
|
|
936
|
-
channel indices to energies (in keV). Procedure: provide a
|
|
937
|
-
spectrum from the MCA element to be calibrated and the theoretical
|
|
938
|
-
location of at least one peak present in that spectrum (peak
|
|
939
|
-
locations must be given in keV). It is strongly recommended to use
|
|
940
|
-
the location of fluorescence peaks whenever possible, _not_
|
|
941
|
-
diffraction peaks, as this Processor does not account for
|
|
942
|
-
2&theta."""
|
|
943
|
-
def process(self,
|
|
944
|
-
data,
|
|
945
|
-
max_energy,
|
|
946
|
-
peak_energies,
|
|
947
|
-
peak_initial_guesses=None,
|
|
948
|
-
peak_center_fit_delta=2.0,
|
|
949
|
-
fit_ranges=None,
|
|
950
|
-
save_figures=False,
|
|
951
|
-
interactive=False,
|
|
952
|
-
outputdir='.'):
|
|
953
|
-
"""Fit the specified peaks in the MCA spectrum provided. Using
|
|
954
|
-
the difference between the provided peak locations and the fit
|
|
955
|
-
centers of those peaks, compute linear correction parameters
|
|
956
|
-
to convert MCA channel indices to energies in keV. Return
|
|
957
|
-
those parameters as a dictionary.
|
|
958
|
-
|
|
959
|
-
:param data: An MCA spectrum
|
|
960
|
-
:type data: PipelineData
|
|
961
|
-
:param max_energy: The (uncalibrated) maximum energy measured
|
|
962
|
-
by the MCA spectrum provided.
|
|
963
|
-
:type max_energy: float
|
|
964
|
-
:param peak_energies: Theoretical locations of peaks to use
|
|
965
|
-
for calibrating the MCA channel energies. It is _strongly_
|
|
966
|
-
recommended to use fluorescence peaks.
|
|
967
|
-
:type peak_energies: list[float]
|
|
968
|
-
:param peak_initial_guesses: A list of values to use for the
|
|
969
|
-
initial guesses for peak locations when performing the fit
|
|
970
|
-
of the spectrum. Providing good values to this parameter
|
|
971
|
-
can greatly improve the quality of the spectrum fit when
|
|
972
|
-
the uncalibrated detector channel energies are too far off
|
|
973
|
-
to use the values in `peak_energies` for the initial
|
|
974
|
-
guesses for peak centers. Defaults to None.
|
|
975
|
-
:type peak_inital_guesses: Optional[list[float]]
|
|
976
|
-
:param peak_center_fit_delta: Set boundaries on the fit peak
|
|
977
|
-
centers when performing the fit. The min/max possible
|
|
978
|
-
values for the peak centers will be the values provided in
|
|
979
|
-
`peak_energies` (or `peak_initial_guesses`, if used) ±
|
|
980
|
-
`peak_center_fit_delta`. Defaults to 2.0.
|
|
981
|
-
:type peak_center_fit_delta: float
|
|
982
|
-
:param fit_ranges: Explicit ranges of MCA channel indices
|
|
983
|
-
(_not_ energies) to include when performing a fit of the
|
|
984
|
-
given peaks to the provied MCA spectrum. Use this
|
|
985
|
-
parameter or select it interactively by running a pipeline
|
|
986
|
-
with `config.interactive: True`. Defaults to []
|
|
987
|
-
:type fit_ranges: Optional[list[tuple[int, int]]]
|
|
988
|
-
:param save_figures: Save .pngs of plots for checking inputs &
|
|
989
|
-
outputs of this Processor, defaults to False.
|
|
990
|
-
:type save_figures: bool, optional
|
|
991
|
-
:param interactive: Allows for user interactions, defaults to
|
|
992
|
-
False.
|
|
993
|
-
:type interactive: bool, optional
|
|
994
|
-
:param outputdir: Directory to which any output figures will
|
|
995
|
-
be saved, defaults to '.'.
|
|
996
|
-
:type outputdir: str, optional
|
|
997
|
-
:returns: Dictionary containing linear energy correction
|
|
998
|
-
parameters for the MCA element
|
|
999
|
-
:rtype: dict[str, float]
|
|
1000
|
-
"""
|
|
1001
|
-
# Validate arguments: fit_ranges & interactive
|
|
1002
|
-
if not (fit_ranges or interactive):
|
|
1003
|
-
self.logger.exception(
|
|
1004
|
-
RuntimeError(
|
|
1005
|
-
'If `fit_ranges` is not explicitly provided, '
|
|
1006
|
-
+ self.__class__.__name__
|
|
1007
|
-
+ ' must be run with `interactive=True`.'))
|
|
1008
|
-
# Validate arguments: peak_energies & peak_initial_guesses
|
|
1009
|
-
if peak_initial_guesses is None:
|
|
1010
|
-
peak_initial_guesses = peak_energies
|
|
1011
|
-
else:
|
|
1012
|
-
from CHAP.utils.general import is_num_series
|
|
1013
|
-
is_num_series(peak_initial_guesses, raise_error=True)
|
|
1014
|
-
if len(peak_initial_guesses) != len(peak_energies):
|
|
1015
|
-
self.logger.exception(
|
|
1016
|
-
ValueError(
|
|
1017
|
-
'peak_initial_guesses must have the same number of '
|
|
1018
|
-
+ 'values as peak_energies'))
|
|
1019
|
-
|
|
1020
|
-
import matplotlib.pyplot as plt
|
|
1021
|
-
import numpy as np
|
|
1022
|
-
from CHAP.utils.fit import Fit
|
|
1023
|
-
from CHAP.utils.general import select_mask_1d
|
|
1024
|
-
|
|
1025
|
-
spectrum = self.unwrap_pipelinedata(data)[0]
|
|
1026
|
-
num_bins = len(spectrum)
|
|
1027
|
-
uncalibrated_energies = np.linspace(0, max_energy, num_bins)
|
|
1028
|
-
|
|
1029
|
-
fig, mask, fit_ranges = select_mask_1d(
|
|
1030
|
-
spectrum, x=uncalibrated_energies,
|
|
1031
|
-
preselected_index_ranges=fit_ranges,
|
|
1032
|
-
xlabel='Uncalibrated Energy', ylabel='Intensity',
|
|
1033
|
-
min_num_index_ranges=1, interactive=interactive)
|
|
1034
|
-
if save_figures:
|
|
1035
|
-
fig.savefig(os.path.join(
|
|
1036
|
-
outputdir, 'mca_energy_calibration_mask.png'))
|
|
1037
|
-
plt.close()
|
|
1038
|
-
self.logger.debug(f'Selected index ranges to fit: {fit_ranges}')
|
|
1039
|
-
|
|
1040
|
-
spectrum_fit = Fit(spectrum[mask], x=uncalibrated_energies[mask])
|
|
1041
|
-
for i, (peak_energy, initial_guess) in enumerate(
|
|
1042
|
-
zip(peak_energies, peak_initial_guesses)):
|
|
1043
|
-
spectrum_fit.add_model(
|
|
1044
|
-
'gaussian', prefix=f'peak{i+1}_', parameters=(
|
|
1045
|
-
{'name': 'amplitude', 'min': 0.0},
|
|
1046
|
-
{'name': 'center', 'value': initial_guess,
|
|
1047
|
-
'min': initial_guess - peak_center_fit_delta,
|
|
1048
|
-
'max': initial_guess + peak_center_fit_delta}
|
|
1049
|
-
))
|
|
1050
|
-
self.logger.debug('Fitting spectrum')
|
|
1051
|
-
spectrum_fit.fit()
|
|
1052
|
-
fit_peak_energies = [
|
|
1053
|
-
spectrum_fit.best_values[f'peak{i+1}_center']
|
|
1054
|
-
for i in range(len(peak_energies))]
|
|
1055
|
-
self.logger.debug(f'Fit peak centers: {fit_peak_energies}')
|
|
1056
|
-
|
|
1057
|
-
energy_fit = Fit.fit_data(
|
|
1058
|
-
peak_energies, 'linear', x=fit_peak_energies, nan_policy='omit')
|
|
1059
|
-
slope = energy_fit.best_values['slope']
|
|
1060
|
-
intercept = energy_fit.best_values['intercept']
|
|
1061
|
-
|
|
1062
|
-
# Rescale slope so results are a linear correction from
|
|
1063
|
-
# channel indices -> calibrated energies, not uncalibrated
|
|
1064
|
-
# energies -> calibrated energies
|
|
1065
|
-
slope = (max_energy / num_bins) * slope
|
|
1066
|
-
return({'slope': slope, 'intercept': intercept})
|
|
1067
|
-
|
|
1068
2252
|
|
|
1069
2253
|
class MCADataProcessor(Processor):
|
|
1070
2254
|
"""A Processor to return data from an MCA, restuctured to
|
|
1071
2255
|
incorporate the shape & metadata associated with a map
|
|
1072
2256
|
configuration to which the MCA data belongs, and linearly
|
|
1073
|
-
transformed according to the results of a
|
|
2257
|
+
transformed according to the results of a energy/tth calibration.
|
|
1074
2258
|
"""
|
|
1075
2259
|
|
|
1076
2260
|
def process(self,
|
|
@@ -1083,8 +2267,8 @@ class MCADataProcessor(Processor):
|
|
|
1083
2267
|
"""Process configurations for a map and MCA detector(s), and
|
|
1084
2268
|
return the calibrated MCA data collected over the map.
|
|
1085
2269
|
|
|
1086
|
-
:param data: Input map configuration and results of
|
|
1087
|
-
calibration.
|
|
2270
|
+
:param data: Input map configuration and results of
|
|
2271
|
+
energy/tth calibration.
|
|
1088
2272
|
:type data: list[dict[str,object]]
|
|
1089
2273
|
:return: Calibrated and flux-corrected MCA data.
|
|
1090
2274
|
:rtype: nexusformat.nexus.NXentry
|
|
@@ -1094,9 +2278,9 @@ class MCADataProcessor(Processor):
|
|
|
1094
2278
|
exit('Done Here')
|
|
1095
2279
|
map_config = self.get_config(
|
|
1096
2280
|
data, 'common.models.map.MapConfig', inputdir=inputdir)
|
|
1097
|
-
|
|
1098
|
-
data, 'edd.models.
|
|
1099
|
-
nxroot = self.get_nxroot(map_config,
|
|
2281
|
+
calibration_config = self.get_config(
|
|
2282
|
+
data, 'edd.models.MCATthCalibrationConfig', inputdir=inputdir)
|
|
2283
|
+
nxroot = self.get_nxroot(map_config, calibration_config)
|
|
1100
2284
|
|
|
1101
2285
|
return nxroot
|
|
1102
2286
|
|
|
@@ -1111,7 +2295,7 @@ class MCADataProcessor(Processor):
|
|
|
1111
2295
|
:type map_config: CHAP.common.models.MapConfig.
|
|
1112
2296
|
:param calibration_config: The calibration configuration.
|
|
1113
2297
|
:type calibration_config:
|
|
1114
|
-
CHAP.edd.models.
|
|
2298
|
+
CHAP.edd.models.MCATthCalibrationConfig
|
|
1115
2299
|
:return: A map of the calibrated and flux-corrected MCA data.
|
|
1116
2300
|
:rtype: nexusformat.nexus.NXroot
|
|
1117
2301
|
"""
|
|
@@ -1171,6 +2355,87 @@ class MCADataProcessor(Processor):
|
|
|
1171
2355
|
return nxroot
|
|
1172
2356
|
|
|
1173
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
|
+
|
|
1174
2439
|
class StrainAnalysisProcessor(Processor):
|
|
1175
2440
|
"""Processor that takes a map of MCA data and returns a map of
|
|
1176
2441
|
sample strains
|
|
@@ -1178,6 +2443,7 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1178
2443
|
def process(self,
|
|
1179
2444
|
data,
|
|
1180
2445
|
config=None,
|
|
2446
|
+
find_peaks=False,
|
|
1181
2447
|
save_figures=False,
|
|
1182
2448
|
inputdir='.',
|
|
1183
2449
|
outputdir='.',
|
|
@@ -1185,13 +2451,18 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1185
2451
|
"""Return strain analysis maps & associated metadata in an NXprocess.
|
|
1186
2452
|
|
|
1187
2453
|
:param data: Input data containing configurations for a map,
|
|
1188
|
-
completed
|
|
2454
|
+
completed energy/tth calibration, and parameters for strain
|
|
1189
2455
|
analysis
|
|
1190
2456
|
:type data: list[PipelineData]
|
|
1191
2457
|
:param config: Initialization parameters for an instance of
|
|
1192
2458
|
CHAP.edd.models.StrainAnalysisConfig, defaults to
|
|
1193
2459
|
None.
|
|
1194
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
|
|
1195
2466
|
:param save_figures: Save .pngs of plots for checking inputs &
|
|
1196
2467
|
outputs of this Processor, defaults to False.
|
|
1197
2468
|
:type save_figures: bool, optional
|
|
@@ -1214,8 +2485,8 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1214
2485
|
|
|
1215
2486
|
"""
|
|
1216
2487
|
# Get required configuration models from input data
|
|
1217
|
-
|
|
1218
|
-
data, 'edd.models.
|
|
2488
|
+
calibration_config = self.get_config(
|
|
2489
|
+
data, 'edd.models.MCATthCalibrationConfig', inputdir=inputdir)
|
|
1219
2490
|
try:
|
|
1220
2491
|
strain_analysis_config = self.get_config(
|
|
1221
2492
|
data, 'edd.models.StrainAnalysisConfig', inputdir=inputdir)
|
|
@@ -1233,8 +2504,9 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1233
2504
|
|
|
1234
2505
|
nxroot = self.get_nxroot(
|
|
1235
2506
|
strain_analysis_config.map_config,
|
|
1236
|
-
|
|
2507
|
+
calibration_config,
|
|
1237
2508
|
strain_analysis_config,
|
|
2509
|
+
find_peaks=find_peaks,
|
|
1238
2510
|
save_figures=save_figures,
|
|
1239
2511
|
outputdir=outputdir,
|
|
1240
2512
|
interactive=interactive)
|
|
@@ -1244,8 +2516,9 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1244
2516
|
|
|
1245
2517
|
def get_nxroot(self,
|
|
1246
2518
|
map_config,
|
|
1247
|
-
|
|
2519
|
+
calibration_config,
|
|
1248
2520
|
strain_analysis_config,
|
|
2521
|
+
find_peaks=False,
|
|
1249
2522
|
save_figures=False,
|
|
1250
2523
|
outputdir='.',
|
|
1251
2524
|
interactive=False):
|
|
@@ -1254,13 +2527,18 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1254
2527
|
|
|
1255
2528
|
:param map_config: The map configuration.
|
|
1256
2529
|
:type map_config: CHAP.common.models.map.MapConfig
|
|
1257
|
-
:param
|
|
1258
|
-
:type
|
|
1259
|
-
'CHAP.edd.models.
|
|
2530
|
+
:param calibration_config: The calibration configuration.
|
|
2531
|
+
:type calibration_config:
|
|
2532
|
+
'CHAP.edd.models.MCATthCalibrationConfig'
|
|
1260
2533
|
:param strain_analysis_config: Strain analysis processing
|
|
1261
2534
|
configuration.
|
|
1262
2535
|
:type strain_analysis_config:
|
|
1263
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
|
|
1264
2542
|
:param save_figures: Save .pngs of plots for checking inputs &
|
|
1265
2543
|
outputs of this Processor, defaults to False.
|
|
1266
2544
|
:type save_figures: bool, optional
|
|
@@ -1291,37 +2569,53 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1291
2569
|
get_unique_hkls_ds,
|
|
1292
2570
|
get_spectra_fits
|
|
1293
2571
|
)
|
|
2572
|
+
if interactive or save_figures:
|
|
2573
|
+
from CHAP.edd.utils import (
|
|
2574
|
+
select_material_params,
|
|
2575
|
+
select_mask_and_hkls,
|
|
2576
|
+
)
|
|
1294
2577
|
|
|
1295
|
-
def linkdims(nxgroup, field_dims=[]):
|
|
2578
|
+
def linkdims(nxgroup, field_dims=[], oversampling_axis={}):
|
|
1296
2579
|
if isinstance(field_dims, dict):
|
|
1297
2580
|
field_dims = [field_dims]
|
|
1298
2581
|
if map_config.map_type == 'structured':
|
|
1299
2582
|
axes = deepcopy(map_config.dims)
|
|
1300
2583
|
for dims in field_dims:
|
|
1301
2584
|
axes.append(dims['axes'])
|
|
1302
|
-
nxgroup.attrs['axes'] = axes
|
|
1303
2585
|
else:
|
|
1304
2586
|
axes = ['map_index']
|
|
1305
2587
|
for dims in field_dims:
|
|
1306
2588
|
axes.append(dims['axes'])
|
|
1307
|
-
nxgroup.attrs['axes'] = axes
|
|
1308
2589
|
nxgroup.attrs[f'map_index_indices'] = 0
|
|
1309
2590
|
for dim in map_config.dims:
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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
|
|
1314
2609
|
for dims in field_dims:
|
|
1315
2610
|
nxgroup.attrs[f'{dims["axes"]}_indices'] = dims['index']
|
|
1316
2611
|
|
|
1317
|
-
if
|
|
1318
|
-
raise
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
if d.detector_name == detector.detector_name][0]
|
|
1323
|
-
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.')
|
|
1324
2617
|
|
|
2618
|
+
# Create the NXroot object
|
|
1325
2619
|
nxroot = NXroot()
|
|
1326
2620
|
nxroot[map_config.title] = MapProcessor.get_nxentry(map_config)
|
|
1327
2621
|
nxentry = nxroot[map_config.title]
|
|
@@ -1335,98 +2629,174 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1335
2629
|
nxdata = nxprocess.data
|
|
1336
2630
|
linkdims(nxdata)
|
|
1337
2631
|
|
|
1338
|
-
# Collect raw MCA data
|
|
1339
|
-
|
|
1340
|
-
for i, detector in enumerate(strain_analysis_config.detectors):
|
|
1341
|
-
mca_bin_energies.append(
|
|
1342
|
-
detector.slope_calibrated
|
|
1343
|
-
* np.linspace(0, detector.max_energy_kev, detector.num_bins)
|
|
1344
|
-
+ detector.intercept_calibrated)
|
|
2632
|
+
# Collect the raw MCA data
|
|
2633
|
+
self.logger.debug(f'Reading data ...')
|
|
1345
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):
|
|
1346
2668
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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)
|
|
1351
2674
|
|
|
1352
|
-
#
|
|
1353
|
-
|
|
1354
|
-
select_material_params,
|
|
1355
|
-
select_mask_and_hkls,
|
|
1356
|
-
)
|
|
2675
|
+
# Get the MCA bin energies
|
|
2676
|
+
mca_bin_energies = detector.energies
|
|
1357
2677
|
|
|
1358
|
-
#
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
for detector in ceria_calibration_config.detectors:
|
|
1362
|
-
# calibration_mask = detector.mca_mask()
|
|
1363
|
-
calibration_bin_ranges = detector.include_bin_ranges
|
|
1364
|
-
|
|
1365
|
-
tth = strain_analysis_config.detectors[0].tth_calibrated
|
|
1366
|
-
fig, strain_analysis_config.materials = select_material_params(
|
|
1367
|
-
mca_bin_energies[0], np.sum(mca_data, axis=1)[0], tth,
|
|
1368
|
-
materials=strain_analysis_config.materials,
|
|
1369
|
-
label='Sum of all spectra in the map',
|
|
1370
|
-
interactive=interactive)
|
|
1371
|
-
self.logger.debug(
|
|
1372
|
-
f'materials: {strain_analysis_config.materials}')
|
|
1373
|
-
if save_figures:
|
|
1374
|
-
fig.savefig(os.path.join(
|
|
1375
|
-
outputdir,
|
|
1376
|
-
f'{detector.detector_name}_strainanalysis_'
|
|
1377
|
-
'material_config.png'))
|
|
1378
|
-
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
|
|
1379
2681
|
|
|
1380
|
-
#
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
select_mask_and_hkls(
|
|
1389
|
-
mca_bin_energies[i],
|
|
1390
|
-
np.sum(mca_data[i], axis=0),
|
|
1391
|
-
hkls, ds,
|
|
1392
|
-
detector.tth_calibrated,
|
|
1393
|
-
detector.include_bin_ranges, detector.hkl_indices,
|
|
1394
|
-
detector.detector_name, mca_data[i],
|
|
1395
|
-
# calibration_mask=calibration_mask,
|
|
1396
|
-
calibration_bin_ranges=calibration_bin_ranges,
|
|
1397
|
-
label='Sum of all spectra in the map',
|
|
1398
|
-
interactive=interactive)
|
|
1399
|
-
detector.include_energy_ranges = detector.get_energy_ranges(
|
|
1400
|
-
include_bin_ranges)
|
|
1401
|
-
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()
|
|
1402
2690
|
if save_figures:
|
|
1403
|
-
|
|
2691
|
+
filename = os.path.join(
|
|
1404
2692
|
outputdir,
|
|
1405
2693
|
f'{detector.detector_name}_strainanalysis_'
|
|
1406
|
-
'
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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
|
+
|
|
1411
2738
|
# Get the unique HKLs and lattice spacings for the strain
|
|
1412
|
-
# analysis materials
|
|
1413
|
-
# same for each detector)
|
|
2739
|
+
# analysis materials
|
|
1414
2740
|
hkls, ds = get_unique_hkls_ds(
|
|
1415
2741
|
strain_analysis_config.materials,
|
|
1416
|
-
tth_tol=
|
|
1417
|
-
tth_max=
|
|
2742
|
+
tth_tol=detector.hkl_tth_tol,
|
|
2743
|
+
tth_max=detector.tth_max)
|
|
1418
2744
|
|
|
1419
|
-
|
|
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}')
|
|
1420
2774
|
if not detector.include_energy_ranges:
|
|
1421
2775
|
raise ValueError(
|
|
1422
2776
|
'No value provided for include_energy_ranges. '
|
|
1423
|
-
'Provide them in the MCA
|
|
2777
|
+
'Provide them in the MCA Tth Calibration Configuration, '
|
|
1424
2778
|
'or re-run the pipeline with the --interactive flag.')
|
|
1425
2779
|
if not detector.hkl_indices:
|
|
1426
2780
|
raise ValueError(
|
|
1427
2781
|
'No value provided for hkl_indices. Provide them in '
|
|
1428
|
-
'the detector\'s MCA
|
|
2782
|
+
'the detector\'s MCA Tth Calibration Configuration, or'
|
|
1429
2783
|
' re-run the pipeline with the --interactive flag.')
|
|
2784
|
+
|
|
2785
|
+
# Loop over the detectors to perform the strain analysis
|
|
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
|
+
|
|
1430
2800
|
# Setup NXdata group
|
|
1431
2801
|
self.logger.debug(
|
|
1432
2802
|
f'Setting up NXdata group for {detector.detector_name}')
|
|
@@ -1437,31 +2807,43 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1437
2807
|
nxdetector.data = NXdata()
|
|
1438
2808
|
det_nxdata = nxdetector.data
|
|
1439
2809
|
linkdims(
|
|
1440
|
-
det_nxdata,
|
|
2810
|
+
det_nxdata,
|
|
2811
|
+
{'axes': 'energy', 'index': len(effective_map_shape)},
|
|
2812
|
+
oversampling_axis=oversampling_axis)
|
|
1441
2813
|
mask = detector.mca_mask()
|
|
1442
|
-
energies = mca_bin_energies[
|
|
1443
|
-
det_nxdata.energy = NXfield(value=energies,
|
|
1444
|
-
attrs={'units': 'keV'})
|
|
2814
|
+
energies = mca_bin_energies[mask]
|
|
2815
|
+
det_nxdata.energy = NXfield(value=energies, attrs={'units': 'keV'})
|
|
1445
2816
|
det_nxdata.intensity = NXfield(
|
|
1446
|
-
dtype=
|
|
1447
|
-
shape=(*
|
|
2817
|
+
dtype=np.float64,
|
|
2818
|
+
shape=(*effective_map_shape, len(energies)),
|
|
1448
2819
|
attrs={'units': 'counts'})
|
|
1449
2820
|
det_nxdata.tth = NXfield(
|
|
1450
|
-
dtype=
|
|
1451
|
-
shape=
|
|
2821
|
+
dtype=np.float64,
|
|
2822
|
+
shape=effective_map_shape,
|
|
1452
2823
|
attrs={'units':'degrees', 'long_name': '2\u03B8 (degrees)'}
|
|
1453
2824
|
)
|
|
1454
|
-
det_nxdata.
|
|
1455
|
-
dtype=
|
|
1456
|
-
shape=
|
|
1457
|
-
attrs={'long_name':
|
|
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)'})
|
|
1458
2835
|
|
|
1459
2836
|
# Gather detector data
|
|
1460
2837
|
self.logger.debug(
|
|
1461
2838
|
f'Gathering detector data for {detector.detector_name}')
|
|
1462
|
-
for
|
|
1463
|
-
|
|
1464
|
-
|
|
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]
|
|
1465
2847
|
det_nxdata.summed_intensity = det_nxdata.intensity.sum(axis=-1)
|
|
1466
2848
|
|
|
1467
2849
|
# Perform strain analysis
|
|
@@ -1470,11 +2852,55 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1470
2852
|
|
|
1471
2853
|
# Get the HKLs and lattice spacings that will be used for
|
|
1472
2854
|
# fitting
|
|
1473
|
-
|
|
1474
|
-
|
|
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])
|
|
1475
2857
|
peak_locations = get_peak_locations(
|
|
1476
|
-
|
|
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
|
|
1477
2901
|
|
|
2902
|
+
# Perform the fit
|
|
2903
|
+
self.logger.debug(f'Fitting {detector.detector_name} ...')
|
|
1478
2904
|
(uniform_fit_centers, uniform_fit_centers_errors,
|
|
1479
2905
|
uniform_fit_amplitudes, uniform_fit_amplitudes_errors,
|
|
1480
2906
|
uniform_fit_sigmas, uniform_fit_sigmas_errors,
|
|
@@ -1487,7 +2913,8 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1487
2913
|
unconstrained_redchi, unconstrained_success) = \
|
|
1488
2914
|
get_spectra_fits(
|
|
1489
2915
|
det_nxdata.intensity.nxdata, energies,
|
|
1490
|
-
peak_locations, detector)
|
|
2916
|
+
peak_locations[use_peaks], detector)
|
|
2917
|
+
self.logger.debug(f'... done')
|
|
1491
2918
|
|
|
1492
2919
|
# Add uniform fit results to the NeXus structure
|
|
1493
2920
|
nxdetector.uniform_fit = NXcollection()
|
|
@@ -1497,9 +2924,11 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1497
2924
|
fit_nxgroup.results = NXdata()
|
|
1498
2925
|
fit_nxdata = fit_nxgroup.results
|
|
1499
2926
|
linkdims(
|
|
1500
|
-
fit_nxdata,
|
|
2927
|
+
fit_nxdata,
|
|
2928
|
+
{'axes': 'energy', 'index': len(map_config.shape)},
|
|
2929
|
+
oversampling_axis=oversampling_axis)
|
|
1501
2930
|
fit_nxdata.makelink(det_nxdata.energy)
|
|
1502
|
-
fit_nxdata.best_fit= uniform_best_fit
|
|
2931
|
+
fit_nxdata.best_fit = uniform_best_fit
|
|
1503
2932
|
fit_nxdata.residuals = uniform_residuals
|
|
1504
2933
|
fit_nxdata.redchi = uniform_redchi
|
|
1505
2934
|
fit_nxdata.success = uniform_success
|
|
@@ -1509,12 +2938,12 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1509
2938
|
# fit_nxdata = fit_nxgroup.fit_hkl_centers
|
|
1510
2939
|
# linkdims(fit_nxdata)
|
|
1511
2940
|
for (hkl, center_guess, centers_fit, centers_error,
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
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):
|
|
1518
2947
|
hkl_name = '_'.join(str(hkl)[1:-1].split(' '))
|
|
1519
2948
|
fit_nxgroup[hkl_name] = NXparameters()
|
|
1520
2949
|
# Report initial HKL peak centers
|
|
@@ -1551,40 +2980,48 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1551
2980
|
if interactive or save_figures:
|
|
1552
2981
|
# Third party modules
|
|
1553
2982
|
import matplotlib.animation as animation
|
|
2983
|
+
import matplotlib.pyplot as plt
|
|
1554
2984
|
|
|
1555
2985
|
if save_figures:
|
|
1556
2986
|
path = os.path.join(
|
|
1557
|
-
outputdir,
|
|
1558
|
-
'
|
|
2987
|
+
outputdir,
|
|
2988
|
+
f'{detector.detector_name}_strainanalysis_'
|
|
2989
|
+
'unconstrained_fits')
|
|
1559
2990
|
if not os.path.isdir(path):
|
|
1560
2991
|
os.mkdir(path)
|
|
1561
2992
|
|
|
1562
2993
|
def animate(i):
|
|
1563
|
-
map_index = np.unravel_index(i,
|
|
2994
|
+
map_index = np.unravel_index(i, effective_map_shape)
|
|
2995
|
+
norm = det_nxdata.intensity.nxdata[map_index].max()
|
|
1564
2996
|
intensity.set_ydata(
|
|
1565
|
-
det_nxdata.intensity.nxdata[map_index]
|
|
1566
|
-
/ det_nxdata.intensity.nxdata[map_index].max())
|
|
2997
|
+
det_nxdata.intensity.nxdata[map_index] / norm)
|
|
1567
2998
|
best_fit.set_ydata(
|
|
1568
|
-
unconstrained_best_fit[map_index]
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
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()]))
|
|
1573
3006
|
if save_figures:
|
|
1574
3007
|
plt.savefig(os.path.join(
|
|
1575
3008
|
path, f'frame_{str(i).zfill(num_digit)}.png'))
|
|
1576
|
-
#return intensity, best_fit, residual, index
|
|
1577
3009
|
return intensity, best_fit, index
|
|
1578
3010
|
|
|
1579
3011
|
fig, ax = plt.subplots()
|
|
1580
|
-
|
|
3012
|
+
effective_map_shape
|
|
3013
|
+
map_index = np.unravel_index(0, effective_map_shape)
|
|
1581
3014
|
data_normalized = (
|
|
1582
3015
|
det_nxdata.intensity.nxdata[map_index]
|
|
1583
3016
|
/ det_nxdata.intensity.nxdata[map_index].max())
|
|
1584
3017
|
intensity, = ax.plot(
|
|
1585
3018
|
energies, data_normalized, 'b.', label='data')
|
|
1586
|
-
|
|
1587
|
-
|
|
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]
|
|
1588
3025
|
best_fit, = ax.plot(
|
|
1589
3026
|
energies, fit_normalized, 'k-', label='fit')
|
|
1590
3027
|
# residual, = ax.plot(
|
|
@@ -1593,7 +3030,7 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1593
3030
|
ax.set(
|
|
1594
3031
|
title='Unconstrained fits',
|
|
1595
3032
|
xlabel='Energy (keV)',
|
|
1596
|
-
ylabel='Normalized
|
|
3033
|
+
ylabel='Normalized intensity (-)')
|
|
1597
3034
|
ax.legend(loc='upper right')
|
|
1598
3035
|
index = ax.text(
|
|
1599
3036
|
0.05, 0.95, '', transform=ax.transAxes, va='top')
|
|
@@ -1636,18 +3073,25 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1636
3073
|
path = os.path.join(
|
|
1637
3074
|
outputdir,
|
|
1638
3075
|
f'{detector.detector_name}_strainanalysis_'
|
|
1639
|
-
|
|
3076
|
+
'unconstrained_fits.gif')
|
|
1640
3077
|
ani.save(path)
|
|
1641
3078
|
plt.close()
|
|
1642
3079
|
|
|
1643
|
-
tth_map = detector.get_tth_map(
|
|
3080
|
+
tth_map = detector.get_tth_map(effective_map_shape)
|
|
1644
3081
|
det_nxdata.tth.nxdata = tth_map
|
|
1645
3082
|
nominal_centers = np.asarray(
|
|
1646
|
-
[get_peak_locations(d0, tth_map)
|
|
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
|
+
|
|
1647
3090
|
unconstrained_strains = np.log(
|
|
1648
3091
|
nominal_centers / unconstrained_fit_centers)
|
|
1649
3092
|
unconstrained_strain = np.mean(unconstrained_strains, axis=0)
|
|
1650
|
-
det_nxdata.
|
|
3093
|
+
det_nxdata.unconstrained_microstrain.nxdata = \
|
|
3094
|
+
unconstrained_strain * 1e6
|
|
1651
3095
|
|
|
1652
3096
|
# Add unconstrained fit results to the NeXus structure
|
|
1653
3097
|
nxdetector.unconstrained_fit = NXcollection()
|
|
@@ -1657,7 +3101,9 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1657
3101
|
fit_nxgroup.results = NXdata()
|
|
1658
3102
|
fit_nxdata = fit_nxgroup.results
|
|
1659
3103
|
linkdims(
|
|
1660
|
-
fit_nxdata,
|
|
3104
|
+
fit_nxdata,
|
|
3105
|
+
{'axes': 'energy', 'index': len(map_config.shape)},
|
|
3106
|
+
oversampling_axis=oversampling_axis)
|
|
1661
3107
|
fit_nxdata.makelink(det_nxdata.energy)
|
|
1662
3108
|
fit_nxdata.best_fit= unconstrained_best_fit
|
|
1663
3109
|
fit_nxdata.residuals = unconstrained_residuals
|
|
@@ -1671,7 +3117,7 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1671
3117
|
for (hkl, center_guesses, centers_fit, centers_error,
|
|
1672
3118
|
amplitudes_fit, amplitudes_error, sigmas_fit,
|
|
1673
3119
|
sigmas_error) in zip(
|
|
1674
|
-
|
|
3120
|
+
hkls_fit, uniform_fit_centers,
|
|
1675
3121
|
unconstrained_fit_centers,
|
|
1676
3122
|
unconstrained_fit_centers_errors,
|
|
1677
3123
|
unconstrained_fit_amplitudes,
|
|
@@ -1717,6 +3163,25 @@ class StrainAnalysisProcessor(Processor):
|
|
|
1717
3163
|
return nxroot
|
|
1718
3164
|
|
|
1719
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
|
+
|
|
1720
3185
|
if __name__ == '__main__':
|
|
1721
3186
|
# Local modules
|
|
1722
3187
|
from CHAP.processor import main
|