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