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