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/models.py
CHANGED
|
@@ -12,12 +12,14 @@ import numpy as np
|
|
|
12
12
|
from hexrd.material import Material
|
|
13
13
|
from pydantic import (
|
|
14
14
|
BaseModel,
|
|
15
|
+
DirectoryPath,
|
|
16
|
+
FilePath,
|
|
17
|
+
PrivateAttr,
|
|
18
|
+
StrictBool,
|
|
15
19
|
confloat,
|
|
16
20
|
conint,
|
|
17
21
|
conlist,
|
|
18
22
|
constr,
|
|
19
|
-
DirectoryPath,
|
|
20
|
-
FilePath,
|
|
21
23
|
root_validator,
|
|
22
24
|
validator,
|
|
23
25
|
)
|
|
@@ -29,6 +31,103 @@ from CHAP.utils.parfile import ParFile
|
|
|
29
31
|
from CHAP.utils.scanparsers import SMBMCAScanParser as ScanParser
|
|
30
32
|
|
|
31
33
|
|
|
34
|
+
# Baseline configuration class
|
|
35
|
+
|
|
36
|
+
class BaselineConfig(BaseModel):
|
|
37
|
+
"""Baseline model configuration
|
|
38
|
+
|
|
39
|
+
:ivar tol: The convergence tolerence, defaults to `1.e-6`.
|
|
40
|
+
:type tol: float, optional
|
|
41
|
+
:ivar lam: The &lambda (smoothness) parameter (the balance
|
|
42
|
+
between the residual of the data and the baseline and the
|
|
43
|
+
smoothness of the baseline). The suggested range is between
|
|
44
|
+
100 and 10^8, defaults to `10^6`.
|
|
45
|
+
:type lam: float, optional
|
|
46
|
+
:ivar max_iter: The maximum number of iterations,
|
|
47
|
+
defaults to `100`.
|
|
48
|
+
:type max_iter: int, optional
|
|
49
|
+
"""
|
|
50
|
+
tol: confloat(gt=0, allow_inf_nan=False) = 1.e-6
|
|
51
|
+
lam: confloat(gt=0, allow_inf_nan=False) = 1.e6
|
|
52
|
+
max_iter: conint(gt=0) = 100
|
|
53
|
+
attrs: Optional[dict] = {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Material configuration classes
|
|
57
|
+
|
|
58
|
+
class MaterialConfig(BaseModel):
|
|
59
|
+
"""Model for parameters to characterize a sample material.
|
|
60
|
+
|
|
61
|
+
:ivar material_name: Sample material name.
|
|
62
|
+
:type material_name: str, optional
|
|
63
|
+
:ivar lattice_parameters: Lattice spacing(s) in angstroms.
|
|
64
|
+
:type lattice_parameters: float, list[float], optional
|
|
65
|
+
:ivar sgnum: Space group of the material.
|
|
66
|
+
:type sgnum: int, optional
|
|
67
|
+
"""
|
|
68
|
+
material_name: Optional[constr(strip_whitespace=True, min_length=1)]
|
|
69
|
+
lattice_parameters: Optional[Union[
|
|
70
|
+
confloat(gt=0),
|
|
71
|
+
conlist(item_type=confloat(gt=0), min_items=1, max_items=6)]]
|
|
72
|
+
sgnum: Optional[conint(ge=0)]
|
|
73
|
+
|
|
74
|
+
_material: Optional[Material]
|
|
75
|
+
|
|
76
|
+
class Config:
|
|
77
|
+
underscore_attrs_are_private = False
|
|
78
|
+
|
|
79
|
+
@root_validator
|
|
80
|
+
def validate_material(cls, values):
|
|
81
|
+
"""Create and validate the private attribute _material.
|
|
82
|
+
|
|
83
|
+
:param values: Dictionary of previously validated field values.
|
|
84
|
+
:type values: dict
|
|
85
|
+
:return: The validated list of `values`.
|
|
86
|
+
:rtype: dict
|
|
87
|
+
"""
|
|
88
|
+
# Local modules
|
|
89
|
+
from CHAP.edd.utils import make_material
|
|
90
|
+
|
|
91
|
+
values['_material'] = make_material(values.get('material_name'),
|
|
92
|
+
values.get('sgnum'),
|
|
93
|
+
values.get('lattice_parameters'))
|
|
94
|
+
return values
|
|
95
|
+
|
|
96
|
+
def unique_hkls_ds(self, tth_tol=0.15, tth_max=90.0):
|
|
97
|
+
"""Get a list of unique HKLs and their lattice spacings.
|
|
98
|
+
|
|
99
|
+
:param tth_tol: Minimum resolvable difference in 2&theta
|
|
100
|
+
between two unique HKL peaks, defaults to `0.15`.
|
|
101
|
+
:type tth_tol: float, optional
|
|
102
|
+
:param tth_max: Detector rotation about hutch x axis,
|
|
103
|
+
defaults to `90.0`.
|
|
104
|
+
:type tth_max: float, optional
|
|
105
|
+
:return: Unique HKLs and their lattice spacings in angstroms.
|
|
106
|
+
:rtype: np.ndarray, np.ndarray
|
|
107
|
+
"""
|
|
108
|
+
# Local modules
|
|
109
|
+
from CHAP.edd.utils import get_unique_hkls_ds
|
|
110
|
+
|
|
111
|
+
return get_unique_hkls_ds([self._material])
|
|
112
|
+
|
|
113
|
+
def dict(self, *args, **kwargs):
|
|
114
|
+
"""Return a representation of this configuration in a
|
|
115
|
+
dictionary that is suitable for dumping to a YAML file.
|
|
116
|
+
|
|
117
|
+
:return: Dictionary representation of the configuration.
|
|
118
|
+
:rtype: dict
|
|
119
|
+
"""
|
|
120
|
+
d = super().dict(*args, **kwargs)
|
|
121
|
+
for k,v in d.items():
|
|
122
|
+
if isinstance(v, PosixPath):
|
|
123
|
+
d[k] = str(v)
|
|
124
|
+
if '_material' in d:
|
|
125
|
+
del d['_material']
|
|
126
|
+
return d
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Detector configuration classes
|
|
130
|
+
|
|
32
131
|
class MCAElementConfig(BaseModel):
|
|
33
132
|
"""Class representing metadata required to configure a single MCA
|
|
34
133
|
detector element.
|
|
@@ -38,43 +137,391 @@ class MCAElementConfig(BaseModel):
|
|
|
38
137
|
:type detector_name: str
|
|
39
138
|
:ivar num_bins: Number of MCA channels.
|
|
40
139
|
:type num_bins: int, optional
|
|
41
|
-
:ivar include_bin_ranges: List of MCA channel index ranges whose
|
|
42
|
-
data should be included after applying a mask (the bounds are
|
|
43
|
-
inclusive), defaults to `[]`
|
|
44
|
-
:type include_bin_ranges: list[[int, int]], optional
|
|
45
140
|
"""
|
|
46
141
|
detector_name: constr(strip_whitespace=True, min_length=1) = 'mca1'
|
|
47
142
|
num_bins: Optional[conint(gt=0)]
|
|
48
|
-
|
|
143
|
+
|
|
144
|
+
def dict(self, *args, **kwargs):
|
|
145
|
+
"""Return a representation of this configuration in a
|
|
146
|
+
dictionary that is suitable for dumping to a YAML file.
|
|
147
|
+
|
|
148
|
+
:return: Dictionary representation of the configuration.
|
|
149
|
+
:rtype: dict
|
|
150
|
+
"""
|
|
151
|
+
d = super().dict(*args, **kwargs)
|
|
152
|
+
return d
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class MCAElementCalibrationConfig(MCAElementConfig):
|
|
156
|
+
"""Class representing metadata required to calibrate a single MCA
|
|
157
|
+
detector element.
|
|
158
|
+
|
|
159
|
+
:ivar tth_max: Detector rotation about lab frame x axis,
|
|
160
|
+
defaults to `90`.
|
|
161
|
+
:type tth_max: float, optional
|
|
162
|
+
:ivar hkl_tth_tol: Minimum resolvable difference in 2&theta between
|
|
163
|
+
two unique Bragg peaks, defaults to `0.15`.
|
|
164
|
+
:type hkl_tth_tol: float, optional
|
|
165
|
+
:ivar energy_calibration_coeffs: Detector channel index to energy
|
|
166
|
+
polynomial conversion coefficients ([a, b, c] with
|
|
167
|
+
E_i = a*i^2 + b*i + c), defaults to `[0, 0, 1]`.
|
|
168
|
+
:type energy_calibration_coeffs:
|
|
169
|
+
list[float, float, float], optional
|
|
170
|
+
:ivar background: Background model for peak fitting.
|
|
171
|
+
:type background: str, list[str], optional
|
|
172
|
+
:ivar baseline: Automated baseline subtraction configuration,
|
|
173
|
+
defaults to `False`.
|
|
174
|
+
:type baseline: Union(bool, BaselineConfig), optional
|
|
175
|
+
:ivar tth_initial_guess: Initial guess for 2&theta,
|
|
176
|
+
defaults to `5.0`.
|
|
177
|
+
:type tth_initial_guess: float, optional
|
|
178
|
+
:ivar tth_calibrated: Calibrated value for 2&theta.
|
|
179
|
+
:type tth_calibrated: float, optional
|
|
180
|
+
:ivar include_energy_ranges: List of MCA channel energy ranges
|
|
181
|
+
in keV whose data should be included after applying a mask
|
|
182
|
+
(bounds are inclusive), defaults to `[[50, 150]]`
|
|
183
|
+
:type include_energy_ranges: list[[float, float]], optional
|
|
184
|
+
"""
|
|
185
|
+
tth_max: confloat(gt=0, allow_inf_nan=False) = 90.0
|
|
186
|
+
hkl_tth_tol: confloat(gt=0, allow_inf_nan=False) = 0.15
|
|
187
|
+
energy_calibration_coeffs: conlist(
|
|
188
|
+
min_items=3, max_items=3,
|
|
189
|
+
item_type=confloat(allow_inf_nan=False)) = [0, 0, 1]
|
|
190
|
+
background: Optional[Union[str, list]]
|
|
191
|
+
baseline: Optional[Union[bool, BaselineConfig]] = False
|
|
192
|
+
tth_initial_guess: confloat(gt=0, le=tth_max, allow_inf_nan=False) = 5.0
|
|
193
|
+
tth_calibrated: Optional[confloat(gt=0, allow_inf_nan=False)]
|
|
194
|
+
include_energy_ranges: conlist(
|
|
49
195
|
min_items=1,
|
|
50
196
|
item_type=conlist(
|
|
51
|
-
item_type=
|
|
197
|
+
item_type=confloat(ge=25),
|
|
52
198
|
min_items=2,
|
|
53
|
-
max_items=2)) = []
|
|
199
|
+
max_items=2)) = [[50, 150]]
|
|
200
|
+
|
|
201
|
+
_hkl_indices: list = PrivateAttr()
|
|
54
202
|
|
|
55
|
-
@validator('
|
|
56
|
-
def
|
|
57
|
-
"""Ensure that no
|
|
203
|
+
@validator('include_energy_ranges', each_item=True)
|
|
204
|
+
def validate_include_energy_range(cls, value, values):
|
|
205
|
+
"""Ensure that no energy ranges are outside the boundary of the
|
|
58
206
|
detector.
|
|
59
207
|
|
|
60
|
-
:param value: Field value to validate (`
|
|
208
|
+
:param value: Field value to validate (`include_energy_ranges`).
|
|
61
209
|
:type values: dict
|
|
62
210
|
:param values: Dictionary of previously validated field values.
|
|
63
211
|
:type values: dict
|
|
64
|
-
:return: The validated value of `
|
|
212
|
+
:return: The validated value of `include_energy_ranges`.
|
|
65
213
|
:rtype: dict
|
|
66
214
|
"""
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
215
|
+
value.sort()
|
|
216
|
+
n_max = values.get('num_bins')
|
|
217
|
+
if n_max is not None:
|
|
218
|
+
n_max -= 1
|
|
219
|
+
a, b, c = values.get('energy_calibration_coeffs')
|
|
220
|
+
e_max = (a*n_max + b)*n_max +c
|
|
221
|
+
if value[0] < c or value[1] > e_max:
|
|
222
|
+
newvalue = [float(max(value[0], c)),
|
|
223
|
+
float(min(value[1], e_max))]
|
|
224
|
+
print(
|
|
225
|
+
f'WARNING: include_energy_range out of range'
|
|
226
|
+
f' ({value}): adjusted to {newvalue}')
|
|
227
|
+
value = newvalue
|
|
73
228
|
return value
|
|
74
229
|
|
|
230
|
+
@property
|
|
231
|
+
def energies(self):
|
|
232
|
+
"""Return calibrated bin energies."""
|
|
233
|
+
a, b, c = self.energy_calibration_coeffs
|
|
234
|
+
channels = np.arange(self.num_bins)
|
|
235
|
+
return (a*channels + b)*channels + c
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def include_bin_ranges(self):
|
|
239
|
+
"""Return the value of `include_energy_ranges` represented in
|
|
240
|
+
terms of channel indices instead of channel energies.
|
|
241
|
+
"""
|
|
242
|
+
from CHAP.utils.general import (
|
|
243
|
+
index_nearest_down,
|
|
244
|
+
index_nearest_up,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
include_bin_ranges = []
|
|
248
|
+
energies = self.energies
|
|
249
|
+
for e_min, e_max in self.include_energy_ranges:
|
|
250
|
+
include_bin_ranges.append(
|
|
251
|
+
[index_nearest_down(energies, e_min),
|
|
252
|
+
index_nearest_up(energies, e_max)])
|
|
253
|
+
return include_bin_ranges
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def hkl_indices(self):
|
|
257
|
+
"""Return the hkl_indices consistent with the selected energy
|
|
258
|
+
ranges (include_energy_ranges).
|
|
259
|
+
"""
|
|
260
|
+
if hasattr(self, '_hkl_indices'):
|
|
261
|
+
return self._hkl_indices
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
def get_include_energy_ranges(self, include_bin_ranges):
|
|
265
|
+
"""Given a list of channel index ranges, return the
|
|
266
|
+
corresponding list of channel energy ranges.
|
|
267
|
+
|
|
268
|
+
:param include_bin_ranges: A list of channel bin ranges to convert to
|
|
269
|
+
energy ranges.
|
|
270
|
+
:type include_bin_ranges: list[list[int]]
|
|
271
|
+
:returns: Energy ranges
|
|
272
|
+
:rtype: list[list[float]]
|
|
273
|
+
"""
|
|
274
|
+
energies = self.energies
|
|
275
|
+
return [[float(energies[i]) for i in range_]
|
|
276
|
+
for range_ in include_bin_ranges]
|
|
277
|
+
|
|
278
|
+
def mca_mask(self):
|
|
279
|
+
"""Get a boolean mask array to use on this MCA element's data.
|
|
280
|
+
Note that the bounds of self.include_energy_ranges are inclusive.
|
|
281
|
+
|
|
282
|
+
:return: Boolean mask array.
|
|
283
|
+
:rtype: numpy.ndarray
|
|
284
|
+
"""
|
|
285
|
+
mask = np.asarray([False] * self.num_bins)
|
|
286
|
+
bin_indices = np.arange(self.num_bins)
|
|
287
|
+
for min_, max_ in self.include_bin_ranges:
|
|
288
|
+
mask = np.logical_or(
|
|
289
|
+
mask, np.logical_and(bin_indices >= min_, bin_indices <= max_))
|
|
290
|
+
return mask
|
|
291
|
+
|
|
292
|
+
def set_hkl_indices(self, hkl_indices):
|
|
293
|
+
"""Set the private attribute `hkl_indices`."""
|
|
294
|
+
self._hkl_indices = hkl_indices
|
|
295
|
+
|
|
296
|
+
#RV need def dict?
|
|
297
|
+
# d['include_energy_ranges'] = [
|
|
298
|
+
# [float(energy) for energy in d['include_energy_ranges'][i]]
|
|
299
|
+
# for i in range(len(d['include_energy_ranges']))]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class MCAElementDiffractionVolumeLengthConfig(MCAElementConfig):
|
|
303
|
+
"""Class representing metadata required to perform a diffraction
|
|
304
|
+
volume length measurement for a single MCA detector element.
|
|
305
|
+
|
|
306
|
+
:ivar include_bin_ranges: List of MCA channel index ranges
|
|
307
|
+
whose data is included in the measurement.
|
|
308
|
+
:type include_bin_ranges: list[[int, int]], optional
|
|
309
|
+
:ivar measurement_mode: Placeholder for recording whether the
|
|
310
|
+
measured DVL value was obtained through the automated
|
|
311
|
+
calculation or a manual selection, defaults to `'auto'`.
|
|
312
|
+
:type measurement_mode: Literal['manual', 'auto'], optional
|
|
313
|
+
:ivar sigma_to_dvl_factor: The DVL is obtained by fitting a reduced
|
|
314
|
+
form of the MCA detector data. `sigma_to_dvl_factor` is a
|
|
315
|
+
scalar value that converts the standard deviation of the
|
|
316
|
+
gaussian fit to the measured DVL, defaults to `3.5`.
|
|
317
|
+
:type sigma_to_dvl_factor: Literal[3.5, 2.0, 4.0], optional
|
|
318
|
+
:ivar dvl_measured: Placeholder for the measured diffraction
|
|
319
|
+
volume length before writing the data to file.
|
|
320
|
+
:type dvl_measured: float, optional
|
|
321
|
+
:ivar fit_amplitude: Placeholder for amplitude of the gaussian fit.
|
|
322
|
+
:type fit_amplitude: float, optional
|
|
323
|
+
:ivar fit_center: Placeholder for center of the gaussian fit.
|
|
324
|
+
:type fit_center: float, optional
|
|
325
|
+
:ivar fit_sigma: Placeholder for sigma of the gaussian fit.
|
|
326
|
+
:type fit_sigma: float, optional
|
|
327
|
+
"""
|
|
328
|
+
include_bin_ranges: Optional[
|
|
329
|
+
conlist(
|
|
330
|
+
min_items=1,
|
|
331
|
+
item_type=conlist(
|
|
332
|
+
item_type=conint(ge=0),
|
|
333
|
+
min_items=2,
|
|
334
|
+
max_items=2))]
|
|
335
|
+
measurement_mode: Optional[Literal['manual', 'auto']] = 'auto'
|
|
336
|
+
sigma_to_dvl_factor: Optional[Literal[3.5, 2.0, 4.0]] = 3.5
|
|
337
|
+
dvl_measured: Optional[confloat(gt=0)] = None
|
|
338
|
+
fit_amplitude: Optional[float] = None
|
|
339
|
+
fit_center: Optional[float] = None
|
|
340
|
+
fit_sigma: Optional[float] = None
|
|
341
|
+
|
|
342
|
+
def mca_mask(self):
|
|
343
|
+
"""Get a boolean mask array to use on this MCA element's data.
|
|
344
|
+
Note that the bounds of self.include_energy_ranges are inclusive.
|
|
345
|
+
|
|
346
|
+
:return: Boolean mask array.
|
|
347
|
+
:rtype: numpy.ndarray
|
|
348
|
+
"""
|
|
349
|
+
mask = np.asarray([False] * self.num_bins)
|
|
350
|
+
bin_indices = np.arange(self.num_bins)
|
|
351
|
+
for min_, max_ in self.include_bin_ranges:
|
|
352
|
+
mask = np.logical_or(
|
|
353
|
+
mask, np.logical_and(bin_indices >= min_, bin_indices <= max_))
|
|
354
|
+
return mask
|
|
355
|
+
|
|
356
|
+
def dict(self, *args, **kwargs):
|
|
357
|
+
"""Return a representation of this configuration in a
|
|
358
|
+
dictionary that is suitable for dumping to a YAML file.
|
|
359
|
+
Exclude `sigma_to_dvl_factor` from the dict representation if
|
|
360
|
+
`measurement_mode` is `'manual'`.
|
|
361
|
+
|
|
362
|
+
:return: Dictionary representation of the configuration.
|
|
363
|
+
:rtype: dict
|
|
364
|
+
"""
|
|
365
|
+
d = super().dict(*args, **kwargs)
|
|
366
|
+
if self.measurement_mode == 'manual':
|
|
367
|
+
del d['sigma_to_dvl_factor']
|
|
368
|
+
for param in ('amplitude', 'center', 'sigma'):
|
|
369
|
+
d[f'fit_{param}'] = float(d[f'fit_{param}'])
|
|
370
|
+
return d
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class MCAElementStrainAnalysisConfig(MCAElementConfig):
|
|
374
|
+
"""Class representing metadata required to perform a strain
|
|
375
|
+
analysis fitting for a single MCA detector element.
|
|
376
|
+
|
|
377
|
+
:param tth_max: Detector rotation about hutch x axis, defaults
|
|
378
|
+
to `90.0`.
|
|
379
|
+
:type tth_max: float, optional
|
|
380
|
+
:ivar hkl_tth_tol: Minimum resolvable difference in 2&theta between
|
|
381
|
+
two unique HKL peaks, defaults to `0.15`.
|
|
382
|
+
:type hkl_tth_tol: float, optional
|
|
383
|
+
:ivar hkl_indices: List of unique HKL indices to fit peaks for in
|
|
384
|
+
the calibration routine, defaults to `[]`.
|
|
385
|
+
:type hkl_indices: list[int], optional
|
|
386
|
+
:ivar background: Background model for peak fitting.
|
|
387
|
+
:type background: str, list[str], optional
|
|
388
|
+
:ivar baseline: Automated baseline subtraction configuration,
|
|
389
|
+
defaults to `False`.
|
|
390
|
+
:type baseline: Union(bool, BaselineConfig), optional
|
|
391
|
+
:ivar num_proc: Number of processors used for peak fitting.
|
|
392
|
+
:type num_proc: int, optional
|
|
393
|
+
:ivar peak_models: Peak model for peak fitting,
|
|
394
|
+
defaults to `'gaussian'`.
|
|
395
|
+
:type peak_models: Literal['gaussian', 'lorentzian']],
|
|
396
|
+
list[Literal['gaussian', 'lorentzian']]], optional
|
|
397
|
+
:ivar fwhm_min: Minimum FWHM for peak fitting, defaults to `0.25`.
|
|
398
|
+
:type fwhm_min: float, optional
|
|
399
|
+
:ivar fwhm_max: Maximum FWHM for peak fitting, defaults to `2.0`.
|
|
400
|
+
:type fwhm_max: float, optional
|
|
401
|
+
:ivar centers_range: Peak centers range for peak fitting.
|
|
402
|
+
The allowed range the peak centers will be the initial
|
|
403
|
+
values ± `centers_range`. Defaults to `2.0`.
|
|
404
|
+
:type centers_range: float, optional
|
|
405
|
+
:ivar rel_height_cutoff: Relative peak height cutoff for
|
|
406
|
+
peak fitting (any peak with a height smaller than
|
|
407
|
+
`rel_height_cutoff` times the maximum height of all peaks
|
|
408
|
+
gets removed from the fit model), defaults to `None`.
|
|
409
|
+
:type rel_height_cutoff: float, optional
|
|
410
|
+
:ivar tth_calibrated: Calibrated value for 2&theta.
|
|
411
|
+
:type tth_calibrated: float, optional
|
|
412
|
+
:ivar energy_calibration_coeffs: Detector channel index to energy
|
|
413
|
+
polynomial conversion coefficients ([a, b, c] with
|
|
414
|
+
E_i = a*i^2 + b*i + c), defaults to `[0, 0, 1]`.
|
|
415
|
+
:type energy_calibration_coeffs:
|
|
416
|
+
list[float, float, float], optional
|
|
417
|
+
:ivar calibration_bin_ranges: List of MCA channel index ranges
|
|
418
|
+
whose data is included in the calibration.
|
|
419
|
+
:type calibration_bin_ranges: list[[int, int]], optional
|
|
420
|
+
:ivar tth_file: Path to the file with the 2&theta map.
|
|
421
|
+
:type tth_file: FilePath, optional
|
|
422
|
+
:ivar tth_map: Map of the 2&theta values.
|
|
423
|
+
:type tth_map: np.ndarray, optional
|
|
424
|
+
:ivar include_energy_ranges: List of MCA channel energy ranges
|
|
425
|
+
in keV whose data should be included after applying a mask
|
|
426
|
+
(bounds are inclusive), defaults to `[[50, 150]]`
|
|
427
|
+
:type include_energy_ranges: list[[float, float]], optional
|
|
428
|
+
"""
|
|
429
|
+
tth_max: confloat(gt=0, allow_inf_nan=False) = 90.0
|
|
430
|
+
hkl_tth_tol: confloat(gt=0, allow_inf_nan=False) = 0.15
|
|
431
|
+
hkl_indices: Optional[conlist(item_type=conint(ge=0))] = []
|
|
432
|
+
background: Optional[Union[str, list]]
|
|
433
|
+
baseline: Optional[Union[bool, BaselineConfig]] = False
|
|
434
|
+
num_proc: Optional[conint(gt=0)] = os.cpu_count()
|
|
435
|
+
peak_models: Union[
|
|
436
|
+
conlist(item_type=Literal['gaussian', 'lorentzian'], min_items=1),
|
|
437
|
+
Literal['gaussian', 'lorentzian']] = 'gaussian'
|
|
438
|
+
fwhm_min: confloat(gt=0, allow_inf_nan=False) = 0.25
|
|
439
|
+
fwhm_max: confloat(gt=0, allow_inf_nan=False) = 2.0
|
|
440
|
+
centers_range: confloat(gt=0, allow_inf_nan=False) = 2.0
|
|
441
|
+
rel_height_cutoff: Optional[confloat(gt=0, lt=1.0, allow_inf_nan=False)]
|
|
442
|
+
|
|
443
|
+
tth_calibrated: Optional[confloat(gt=0, allow_inf_nan=False)]
|
|
444
|
+
energy_calibration_coeffs: conlist(
|
|
445
|
+
min_items=3, max_items=3,
|
|
446
|
+
item_type=confloat(allow_inf_nan=False)) = [0, 0, 1]
|
|
447
|
+
calibration_bin_ranges: Optional[
|
|
448
|
+
conlist(
|
|
449
|
+
min_items=1,
|
|
450
|
+
item_type=conlist(
|
|
451
|
+
item_type=conint(ge=0),
|
|
452
|
+
min_items=2,
|
|
453
|
+
max_items=2))]
|
|
454
|
+
tth_file: Optional[FilePath]
|
|
455
|
+
tth_map: Optional[np.ndarray] = None
|
|
456
|
+
include_energy_ranges: conlist(
|
|
457
|
+
min_items=1,
|
|
458
|
+
item_type=conlist(
|
|
459
|
+
item_type=confloat(ge=25),
|
|
460
|
+
min_items=2,
|
|
461
|
+
max_items=2)) = [[50, 150]]
|
|
462
|
+
|
|
463
|
+
#RV lots of overlap with MCAElementCalibrationConfig (only missing
|
|
464
|
+
# tth_initial_guess)
|
|
465
|
+
# Should we derive from MCAElementCalibrationConfig in some way
|
|
466
|
+
# or make a MCAElementEnergyCalibrationConfig with what's shared
|
|
467
|
+
# and derive MCAElementCalibrationConfig from this as well with
|
|
468
|
+
# the unique fields tth_initial_guess added?
|
|
469
|
+
# Revisit when we redo the detectors
|
|
470
|
+
|
|
471
|
+
@validator('hkl_indices', pre=True)
|
|
472
|
+
def validate_hkl_indices(cls, hkl_indices):
|
|
473
|
+
if isinstance(hkl_indices, str):
|
|
474
|
+
# Local modules
|
|
475
|
+
from CHAP.utils.general import string_to_list
|
|
476
|
+
|
|
477
|
+
hkl_indices = string_to_list(hkl_indices)
|
|
478
|
+
return sorted(hkl_indices)
|
|
479
|
+
|
|
480
|
+
class Config:
|
|
481
|
+
arbitrary_types_allowed = True
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def energies(self):
|
|
485
|
+
"""Return calibrated bin energies."""
|
|
486
|
+
a, b, c = self.energy_calibration_coeffs
|
|
487
|
+
channels = np.arange(self.num_bins)
|
|
488
|
+
return (a*channels + b)*channels + c
|
|
489
|
+
|
|
490
|
+
@property
|
|
491
|
+
def include_bin_ranges(self):
|
|
492
|
+
"""Return the value of `include_energy_ranges` represented in
|
|
493
|
+
terms of channel indices instead of channel energies.
|
|
494
|
+
"""
|
|
495
|
+
from CHAP.utils.general import (
|
|
496
|
+
index_nearest_down,
|
|
497
|
+
index_nearest_up,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
include_bin_ranges = []
|
|
501
|
+
energies = self.energies
|
|
502
|
+
for e_min, e_max in self.include_energy_ranges:
|
|
503
|
+
include_bin_ranges.append(
|
|
504
|
+
[index_nearest_down(energies, e_min),
|
|
505
|
+
index_nearest_up(energies, e_max)])
|
|
506
|
+
return include_bin_ranges
|
|
507
|
+
|
|
508
|
+
def get_include_energy_ranges(self, include_bin_ranges):
|
|
509
|
+
"""Given a list of channel index ranges, return the
|
|
510
|
+
corresponding list of channel energy ranges.
|
|
511
|
+
|
|
512
|
+
:param include_bin_ranges: A list of channel bin ranges to convert to
|
|
513
|
+
energy ranges.
|
|
514
|
+
:type include_bin_ranges: list[list[int]]
|
|
515
|
+
:returns: Energy ranges
|
|
516
|
+
:rtype: list[list[float]]
|
|
517
|
+
"""
|
|
518
|
+
energies = self.energies
|
|
519
|
+
return [[float(energies[i]) for i in range_]
|
|
520
|
+
for range_ in include_bin_ranges]
|
|
521
|
+
|
|
75
522
|
def mca_mask(self):
|
|
76
523
|
"""Get a boolean mask array to use on this MCA element's data.
|
|
77
|
-
Note that the bounds of self.
|
|
524
|
+
Note that the bounds of self.include_energy_ranges are inclusive.
|
|
78
525
|
|
|
79
526
|
:return: Boolean mask array.
|
|
80
527
|
:rtype: numpy.ndarray
|
|
@@ -86,6 +533,38 @@ class MCAElementConfig(BaseModel):
|
|
|
86
533
|
mask, np.logical_and(bin_indices >= min_, bin_indices <= max_))
|
|
87
534
|
return mask
|
|
88
535
|
|
|
536
|
+
def add_calibration(self, calibration):
|
|
537
|
+
"""Finalize values for some fields using a completed
|
|
538
|
+
MCAElementCalibrationConfig that corresponds to the same
|
|
539
|
+
detector.
|
|
540
|
+
|
|
541
|
+
:param calibration: Existing calibration configuration to use
|
|
542
|
+
by MCAElementStrainAnalysisConfig.
|
|
543
|
+
:type calibration: MCAElementCalibrationConfig
|
|
544
|
+
:return: None
|
|
545
|
+
"""
|
|
546
|
+
add_fields = [
|
|
547
|
+
'tth_calibrated', 'energy_calibration_coeffs', 'num_bins']
|
|
548
|
+
for field in add_fields:
|
|
549
|
+
setattr(self, field, getattr(calibration, field))
|
|
550
|
+
self.calibration_bin_ranges = calibration.include_bin_ranges
|
|
551
|
+
|
|
552
|
+
def get_tth_map(self, map_shape):
|
|
553
|
+
"""Return the map of 2&theta values to use -- may vary at each
|
|
554
|
+
point in the map.
|
|
555
|
+
|
|
556
|
+
:param map_shape: The shape of the suplied 2&theta map.
|
|
557
|
+
:return: Map of 2&theta values.
|
|
558
|
+
:rtype: np.ndarray
|
|
559
|
+
"""
|
|
560
|
+
if getattr(self, 'tth_map', None) is not None:
|
|
561
|
+
if self.tth_map.shape != map_shape:
|
|
562
|
+
raise ValueError(
|
|
563
|
+
'Invalid "tth_map" field shape '
|
|
564
|
+
f'{self.tth_map.shape} (expected {map_shape})')
|
|
565
|
+
return self.tth_map
|
|
566
|
+
return np.full(map_shape, self.tth_calibrated)
|
|
567
|
+
|
|
89
568
|
def dict(self, *args, **kwargs):
|
|
90
569
|
"""Return a representation of this configuration in a
|
|
91
570
|
dictionary that is suitable for dumping to a YAML file.
|
|
@@ -94,12 +573,16 @@ class MCAElementConfig(BaseModel):
|
|
|
94
573
|
:rtype: dict
|
|
95
574
|
"""
|
|
96
575
|
d = super().dict(*args, **kwargs)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
576
|
+
for k,v in d.items():
|
|
577
|
+
if isinstance(v, PosixPath):
|
|
578
|
+
d[k] = str(v)
|
|
579
|
+
if isinstance(v, np.ndarray):
|
|
580
|
+
d[k] = v.tolist()
|
|
100
581
|
return d
|
|
101
582
|
|
|
102
583
|
|
|
584
|
+
# Processor configuration classes
|
|
585
|
+
|
|
103
586
|
class MCAScanDataConfig(BaseModel):
|
|
104
587
|
"""Class representing metadata required to locate raw MCA data for
|
|
105
588
|
a single scan and construct a mask for it.
|
|
@@ -172,7 +655,7 @@ class MCAScanDataConfig(BaseModel):
|
|
|
172
655
|
def validate_detectors(cls, values):
|
|
173
656
|
"""Fill in values for _scanparser / _parfile (if applicable).
|
|
174
657
|
Fill in each detector's num_bins field, if needed.
|
|
175
|
-
Check each detector's
|
|
658
|
+
Check each detector's include_energy_ranges field against the
|
|
176
659
|
flux file, if available.
|
|
177
660
|
|
|
178
661
|
:param values: Dictionary of previously validated field values.
|
|
@@ -196,262 +679,96 @@ class MCAScanDataConfig(BaseModel):
|
|
|
196
679
|
values['_parfile'].good_scan_numbers()[0])
|
|
197
680
|
for detector in detectors:
|
|
198
681
|
if detector.num_bins is None:
|
|
199
|
-
try:
|
|
200
|
-
detector.num_bins = values['_scanparser']\
|
|
201
|
-
.get_detector_num_bins(detector.detector_name)
|
|
202
|
-
except Exception as e:
|
|
203
|
-
raise ValueError('No value found for num_bins') from e
|
|
204
|
-
if flux_file is not None:
|
|
205
|
-
# System modules
|
|
206
|
-
from copy import deepcopy
|
|
207
|
-
|
|
208
|
-
# Local modules
|
|
209
|
-
from CHAP.utils.general import (
|
|
210
|
-
index_nearest_down,
|
|
211
|
-
index_nearest_upp,
|
|
212
|
-
)
|
|
213
|
-
flux = np.loadtxt(flux_file)
|
|
214
|
-
flux_file_energies = flux[:,0]/1.e3
|
|
215
|
-
energy_range = (flux_file_energies.min(), flux_file_energies.max())
|
|
216
|
-
for detector in detectors:
|
|
217
|
-
mca_bin_energies = np.linspace(
|
|
218
|
-
0, detector.max_energy_kev, detector.num_bins)
|
|
219
|
-
e_min = index_nearest_upp(mca_bin_energies, energy_range[0])
|
|
220
|
-
e_max = index_nearest_down(mca_bin_energies, energy_range[1])
|
|
221
|
-
for i, (min_, max_) in enumerate(
|
|
222
|
-
deepcopy(detector.include_bin_ranges)):
|
|
223
|
-
if min_ < e_min or max_ > e_max:
|
|
224
|
-
bin_range = [max(min_, e_min), min(max_, e_max)]
|
|
225
|
-
print(f'WARNING: include_bin_ranges[{i}] out of range '
|
|
226
|
-
f'({detector.include_bin_ranges[i]}): adjusted '
|
|
227
|
-
f'to {bin_range}')
|
|
228
|
-
detector.include_bin_ranges[i] = bin_range
|
|
229
|
-
|
|
230
|
-
return values
|
|
231
|
-
|
|
232
|
-
@property
|
|
233
|
-
def scanparser(self):
|
|
234
|
-
"""Return the scanparser."""
|
|
235
|
-
try:
|
|
236
|
-
scanparser = self._scanparser
|
|
237
|
-
except:
|
|
238
|
-
scanparser = ScanParser(self.spec_file, self.scan_number)
|
|
239
|
-
self._scanparser = scanparser
|
|
240
|
-
return scanparser
|
|
241
|
-
|
|
242
|
-
def mca_data(self, detector_config, scan_step_index=None):
|
|
243
|
-
"""Get the array of MCA data collected by the scan.
|
|
244
|
-
|
|
245
|
-
:param detector_config: Detector for which data is returned.
|
|
246
|
-
:type detector_config: MCAElementConfig
|
|
247
|
-
:param scan_step_index: Only return the MCA spectrum for the
|
|
248
|
-
given scan step index, defaults to `None`, which returns
|
|
249
|
-
all the available MCA spectra.
|
|
250
|
-
:type scan_step_index: int, optional
|
|
251
|
-
:return: The current detectors's MCA data.
|
|
252
|
-
:rtype: np.ndarray
|
|
253
|
-
"""
|
|
254
|
-
detector_name = detector_config.detector_name
|
|
255
|
-
if self._parfile is not None:
|
|
256
|
-
if scan_step_index is None:
|
|
257
|
-
data = np.asarray(
|
|
258
|
-
[ScanParser(self._parfile.spec_file, scan_number)\
|
|
259
|
-
.get_all_detector_data(detector_name)[0] \
|
|
260
|
-
for scan_number in self._parfile.good_scan_numbers()])
|
|
261
|
-
else:
|
|
262
|
-
data = ScanParser(
|
|
263
|
-
self._parfile.spec_file,
|
|
264
|
-
self._parfile.good_scan_numbers()[scan_step_index])\
|
|
265
|
-
.get_all_detector_data(detector_name)
|
|
266
|
-
else:
|
|
267
|
-
if scan_step_index is None:
|
|
268
|
-
data = self.scanparser.get_all_detector_data(
|
|
269
|
-
detector_name)
|
|
270
|
-
else:
|
|
271
|
-
data = self.scanparser.get_detector_data(
|
|
272
|
-
detector_config.detector_name, self.scan_step_index)
|
|
273
|
-
return data
|
|
274
|
-
|
|
275
|
-
def dict(self, *args, **kwargs):
|
|
276
|
-
"""Return a representation of this configuration in a
|
|
277
|
-
dictionary that is suitable for dumping to a YAML file.
|
|
278
|
-
|
|
279
|
-
:return: Dictionary representation of the configuration.
|
|
280
|
-
:rtype: dict
|
|
281
|
-
"""
|
|
282
|
-
d = super().dict(*args, **kwargs)
|
|
283
|
-
for k,v in d.items():
|
|
284
|
-
if isinstance(v, PosixPath):
|
|
285
|
-
d[k] = str(v)
|
|
286
|
-
if d.get('_parfile') is None:
|
|
287
|
-
del d['par_file']
|
|
288
|
-
del d['scan_column']
|
|
289
|
-
else:
|
|
290
|
-
del d['spec_file']
|
|
291
|
-
del d['scan_number']
|
|
292
|
-
for k in ('_scanparser', '_parfile', 'inputdir'):
|
|
293
|
-
if k in d:
|
|
294
|
-
del d[k]
|
|
295
|
-
return d
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
class MaterialConfig(BaseModel):
|
|
299
|
-
"""Model for parameters to characterize a sample material.
|
|
300
|
-
|
|
301
|
-
:ivar material_name: Sample material name.
|
|
302
|
-
:type material_name: str, optional
|
|
303
|
-
:ivar lattice_parameters: Lattice spacing(s) in angstroms.
|
|
304
|
-
:type lattice_parameters: float, list[float], optional
|
|
305
|
-
:ivar sgnum: Space group of the material.
|
|
306
|
-
:type sgnum: int, optional
|
|
307
|
-
"""
|
|
308
|
-
material_name: Optional[constr(strip_whitespace=True, min_length=1)]
|
|
309
|
-
lattice_parameters: Optional[Union[
|
|
310
|
-
confloat(gt=0),
|
|
311
|
-
conlist(item_type=confloat(gt=0), min_items=1, max_items=6)]]
|
|
312
|
-
sgnum: Optional[conint(ge=0)]
|
|
313
|
-
|
|
314
|
-
_material: Optional[Material]
|
|
315
|
-
|
|
316
|
-
class Config:
|
|
317
|
-
underscore_attrs_are_private = False
|
|
318
|
-
|
|
319
|
-
@root_validator
|
|
320
|
-
def validate_material(cls, values):
|
|
321
|
-
"""Create and validate the private attribute _material.
|
|
322
|
-
|
|
323
|
-
:param values: Dictionary of previously validated field values.
|
|
324
|
-
:type values: dict
|
|
325
|
-
:return: The validated list of `values`.
|
|
326
|
-
:rtype: dict
|
|
327
|
-
"""
|
|
328
|
-
# Local modules
|
|
329
|
-
from CHAP.edd.utils import make_material
|
|
330
|
-
|
|
331
|
-
values['_material'] = make_material(values.get('material_name'),
|
|
332
|
-
values.get('sgnum'),
|
|
333
|
-
values.get('lattice_parameters'))
|
|
334
|
-
return values
|
|
335
|
-
|
|
336
|
-
def unique_hkls_ds(self, tth_tol=0.15, tth_max=90.0):
|
|
337
|
-
"""Get a list of unique HKLs and their lattice spacings.
|
|
338
|
-
|
|
339
|
-
:param tth_tol: Minimum resolvable difference in 2&theta
|
|
340
|
-
between two unique HKL peaks, defaults to `0.15`.
|
|
341
|
-
:type tth_tol: float, optional
|
|
342
|
-
:param tth_max: Detector rotation about hutch x axis,
|
|
343
|
-
defaults to `90.0`.
|
|
344
|
-
:type tth_max: float, optional
|
|
345
|
-
:return: Unique HKLs and their lattice spacings in angstroms.
|
|
346
|
-
:rtype: np.ndarray, np.ndarray
|
|
347
|
-
"""
|
|
348
|
-
# Local modules
|
|
349
|
-
from CHAP.edd.utils import get_unique_hkls_ds
|
|
350
|
-
|
|
351
|
-
return get_unique_hkls_ds([self._material])
|
|
352
|
-
|
|
353
|
-
def dict(self, *args, **kwargs):
|
|
354
|
-
"""Return a representation of this configuration in a
|
|
355
|
-
dictionary that is suitable for dumping to a YAML file.
|
|
356
|
-
|
|
357
|
-
:return: Dictionary representation of the configuration.
|
|
358
|
-
:rtype: dict
|
|
359
|
-
"""
|
|
360
|
-
d = super().dict(*args, **kwargs)
|
|
361
|
-
for k,v in d.items():
|
|
362
|
-
if isinstance(v, PosixPath):
|
|
363
|
-
d[k] = str(v)
|
|
364
|
-
if '_material' in d:
|
|
365
|
-
del d['_material']
|
|
366
|
-
return d
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
class MCAElementCalibrationConfig(MCAElementConfig):
|
|
370
|
-
"""Class representing metadata required to calibrate a single MCA
|
|
371
|
-
detector element.
|
|
682
|
+
try:
|
|
683
|
+
detector.num_bins = values['_scanparser']\
|
|
684
|
+
.get_detector_num_bins(detector.detector_name)
|
|
685
|
+
except Exception as e:
|
|
686
|
+
raise ValueError('No value found for num_bins') from e
|
|
687
|
+
if flux_file is not None:
|
|
688
|
+
# System modules
|
|
689
|
+
from copy import deepcopy
|
|
372
690
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
energy correction linear slope, defaults to `1.0`.
|
|
389
|
-
:type slope_initial_guess: float, optional
|
|
390
|
-
:ivar intercept_initial_guess: Initial guess for detector channel
|
|
391
|
-
energy correction y-intercept, defaults to `0.0`.
|
|
392
|
-
:type intercept_initial_guess: float, optional
|
|
393
|
-
:ivar tth_calibrated: Calibrated value for 2&theta.
|
|
394
|
-
:type tth_calibrated: float, optional
|
|
395
|
-
:ivar slope_calibrated: Calibrated value for detector channel
|
|
396
|
-
energy correction linear slope.
|
|
397
|
-
:type slope_calibrated: float, optional
|
|
398
|
-
:ivar intercept_calibrated: Calibrated value for detector channel
|
|
399
|
-
energy correction y-intercept.
|
|
400
|
-
:type intercept_calibrated: float, optional
|
|
401
|
-
"""
|
|
402
|
-
max_energy_kev: confloat(gt=0)
|
|
403
|
-
tth_max: confloat(gt=0, allow_inf_nan=False) = 90.0
|
|
404
|
-
hkl_tth_tol: confloat(gt=0, allow_inf_nan=False) = 0.15
|
|
405
|
-
hkl_indices: Optional[conlist(item_type=conint(ge=0), min_items=1)] = []
|
|
406
|
-
tth_initial_guess: confloat(gt=0, le=tth_max, allow_inf_nan=False) = 5.0
|
|
407
|
-
slope_initial_guess: float = 1.0
|
|
408
|
-
intercept_initial_guess: float = 0.0
|
|
409
|
-
tth_calibrated: Optional[confloat(gt=0, allow_inf_nan=False)]
|
|
410
|
-
slope_calibrated: Optional[confloat(allow_inf_nan=False)]
|
|
411
|
-
intercept_calibrated: Optional[confloat(allow_inf_nan=False)]
|
|
691
|
+
flux = np.loadtxt(flux_file)
|
|
692
|
+
flux_file_energies = flux[:,0]/1.e3
|
|
693
|
+
flux_e_min = flux_file_energies.min()
|
|
694
|
+
flux_e_max = flux_file_energies.max()
|
|
695
|
+
for detector in detectors:
|
|
696
|
+
for i, (det_e_min, det_e_max) in enumerate(
|
|
697
|
+
deepcopy(detector.include_energy_ranges)):
|
|
698
|
+
if det_e_min < flux_e_min or det_e_max > flux_e_max:
|
|
699
|
+
energy_range = [float(max(det_e_min, flux_e_min)),
|
|
700
|
+
float(min(det_e_max, flux_e_max))]
|
|
701
|
+
print(
|
|
702
|
+
f'WARNING: include_energy_ranges[{i}] out of range'
|
|
703
|
+
f' ({detector.include_energy_ranges[i]}): adjusted'
|
|
704
|
+
f' to {energy_range}')
|
|
705
|
+
detector.include_energy_ranges[i] = energy_range
|
|
412
706
|
|
|
413
|
-
|
|
414
|
-
def validate_hkl_indices(cls, hkl_indices):
|
|
415
|
-
if isinstance(hkl_indices, str):
|
|
416
|
-
# Local modules
|
|
417
|
-
from CHAP.utils.general import string_to_list
|
|
707
|
+
return values
|
|
418
708
|
|
|
419
|
-
|
|
420
|
-
|
|
709
|
+
@property
|
|
710
|
+
def scanparser(self):
|
|
711
|
+
"""Return the scanparser."""
|
|
712
|
+
try:
|
|
713
|
+
scanparser = self._scanparser
|
|
714
|
+
except:
|
|
715
|
+
scanparser = ScanParser(self.spec_file, self.scan_number)
|
|
716
|
+
self._scanparser = scanparser
|
|
717
|
+
return scanparser
|
|
421
718
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
volume length measurement for a single MCA detector element.
|
|
719
|
+
def mca_data(self, detector_config, scan_step_index=None):
|
|
720
|
+
"""Get the array of MCA data collected by the scan.
|
|
425
721
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
722
|
+
:param detector_config: Detector for which data is returned.
|
|
723
|
+
:type detector_config: MCAElementConfig
|
|
724
|
+
:param scan_step_index: Only return the MCA spectrum for the
|
|
725
|
+
given scan step index, defaults to `None`, which returns
|
|
726
|
+
all the available MCA spectra.
|
|
727
|
+
:type scan_step_index: int, optional
|
|
728
|
+
:return: The current detectors's MCA data.
|
|
729
|
+
:rtype: np.ndarray
|
|
730
|
+
"""
|
|
731
|
+
detector_name = detector_config.detector_name
|
|
732
|
+
if self._parfile is not None:
|
|
733
|
+
if scan_step_index is None:
|
|
734
|
+
data = np.asarray(
|
|
735
|
+
[ScanParser(self._parfile.spec_file, scan_number)\
|
|
736
|
+
.get_all_detector_data(detector_name)[0] \
|
|
737
|
+
for scan_number in self._parfile.good_scan_numbers()])
|
|
738
|
+
else:
|
|
739
|
+
data = ScanParser(
|
|
740
|
+
self._parfile.spec_file,
|
|
741
|
+
self._parfile.good_scan_numbers()[scan_step_index])\
|
|
742
|
+
.get_all_detector_data(detector_name)
|
|
743
|
+
else:
|
|
744
|
+
if scan_step_index is None:
|
|
745
|
+
data = self.scanparser.get_all_detector_data(
|
|
746
|
+
detector_name)
|
|
747
|
+
else:
|
|
748
|
+
data = self.scanparser.get_detector_data(
|
|
749
|
+
detector_config.detector_name, scan_step_index)
|
|
750
|
+
return data
|
|
442
751
|
|
|
443
752
|
def dict(self, *args, **kwargs):
|
|
444
753
|
"""Return a representation of this configuration in a
|
|
445
754
|
dictionary that is suitable for dumping to a YAML file.
|
|
446
|
-
Exclude `sigma_to_dvl_factor` from the dict representation if
|
|
447
|
-
`measurement_mode` is `'manual'`.
|
|
448
755
|
|
|
449
756
|
:return: Dictionary representation of the configuration.
|
|
450
757
|
:rtype: dict
|
|
451
758
|
"""
|
|
452
759
|
d = super().dict(*args, **kwargs)
|
|
453
|
-
|
|
454
|
-
|
|
760
|
+
for k,v in d.items():
|
|
761
|
+
if isinstance(v, PosixPath):
|
|
762
|
+
d[k] = str(v)
|
|
763
|
+
if d.get('_parfile') is None:
|
|
764
|
+
del d['par_file']
|
|
765
|
+
del d['scan_column']
|
|
766
|
+
else:
|
|
767
|
+
del d['spec_file']
|
|
768
|
+
del d['scan_number']
|
|
769
|
+
for k in ('_scanparser', '_parfile', 'inputdir'):
|
|
770
|
+
if k in d:
|
|
771
|
+
del d[k]
|
|
455
772
|
return d
|
|
456
773
|
|
|
457
774
|
|
|
@@ -460,10 +777,15 @@ class DiffractionVolumeLengthConfig(MCAScanDataConfig):
|
|
|
460
777
|
volume length calculation for an EDD setup using a steel-foil
|
|
461
778
|
raster scan.
|
|
462
779
|
|
|
780
|
+
:ivar sample_thickness: Thickness of scanned foil sample. Quantity
|
|
781
|
+
must be provided in the same units as the values of the
|
|
782
|
+
scanning motor.
|
|
783
|
+
:type sample_thickness: float
|
|
463
784
|
:ivar detectors: Individual detector element DVL
|
|
464
785
|
measurement configurations
|
|
465
786
|
:type detectors: list[MCAElementDiffractionVolumeLengthConfig]
|
|
466
787
|
"""
|
|
788
|
+
sample_thickness: float
|
|
467
789
|
detectors: conlist(min_items=1,
|
|
468
790
|
item_type=MCAElementDiffractionVolumeLengthConfig)
|
|
469
791
|
|
|
@@ -481,70 +803,55 @@ class DiffractionVolumeLengthConfig(MCAScanDataConfig):
|
|
|
481
803
|
scan_numbers=self._parfile.good_scan_numbers())
|
|
482
804
|
return self.scanparser.spec_scan_motor_vals[0]
|
|
483
805
|
|
|
484
|
-
@property
|
|
485
|
-
def scanned_dim_lbl(self):
|
|
486
|
-
"""Return a label for plot axes corresponding to the scanned
|
|
487
|
-
dimension.
|
|
488
|
-
|
|
489
|
-
:return: Name of scanned motor.
|
|
490
|
-
:rtype: str
|
|
491
|
-
"""
|
|
492
|
-
if self._parfile is not None:
|
|
493
|
-
return self.scan_column
|
|
494
|
-
return self.scanparser.spec_scan_motor_mnes[0]
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
class CeriaConfig(MaterialConfig):
|
|
498
|
-
"""Model for the sample material used in calibrations.
|
|
499
|
-
|
|
500
|
-
:ivar material_name: Calibration material name,
|
|
501
|
-
defaults to `'CeO2'`.
|
|
502
|
-
:type material_name: str, optional
|
|
503
|
-
:ivar lattice_parameters: Lattice spacing(s) for the calibration
|
|
504
|
-
material in angstroms, defaults to `5.41153`.
|
|
505
|
-
:type lattice_parameters: float, list[float], optional
|
|
506
|
-
:ivar sgnum: Space group of the calibration material,
|
|
507
|
-
defaults to `225`.
|
|
508
|
-
:type sgnum: int, optional
|
|
509
|
-
"""
|
|
510
|
-
#RV Name suggests it's always Ceria, why have material_name?
|
|
511
|
-
material_name: constr(strip_whitespace=True, min_length=1) = 'CeO2'
|
|
512
|
-
lattice_parameters: confloat(gt=0) = 5.41153
|
|
513
|
-
sgnum: Optional[conint(ge=0)] = 225
|
|
514
|
-
|
|
515
806
|
|
|
516
|
-
class
|
|
807
|
+
class MCAEnergyCalibrationConfig(MCAScanDataConfig):
|
|
517
808
|
"""
|
|
518
|
-
Class representing metadata required to perform
|
|
519
|
-
for an MCA detector.
|
|
809
|
+
Class representing metadata required to perform an energy
|
|
810
|
+
calibration for an MCA detector.
|
|
520
811
|
|
|
521
|
-
:ivar
|
|
812
|
+
:ivar scan_step_indices: Optional scan step indices to use for the
|
|
522
813
|
calibration. If not specified, the calibration will be
|
|
523
814
|
performed on the average of all MCA spectra for the scan.
|
|
524
|
-
:type
|
|
525
|
-
:ivar flux_file: File name of the csv flux file containing station
|
|
526
|
-
beam energy in eV (column 0) versus flux (column 1).
|
|
527
|
-
:type flux_file: str
|
|
528
|
-
:ivar material: Material configuration for Ceria.
|
|
529
|
-
:type material: CeriaConfig
|
|
815
|
+
:type scan_step_indices: list[int], optional
|
|
530
816
|
:ivar detectors: List of individual MCA detector element
|
|
531
817
|
calibration configurations.
|
|
532
818
|
:type detectors: list[MCAElementCalibrationConfig]
|
|
533
|
-
:ivar
|
|
534
|
-
|
|
535
|
-
:type
|
|
536
|
-
:ivar
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
819
|
+
:ivar flux_file: File name of the csv flux file containing station
|
|
820
|
+
beam energy in eV (column 0) versus flux (column 1).
|
|
821
|
+
:type flux_file: str, optional
|
|
822
|
+
:ivar material: Material configuration for the calibration,
|
|
823
|
+
defaults to `Ceria`.
|
|
824
|
+
:type material: MaterialConfig, optional
|
|
825
|
+
:ivar peak_energies: Theoretical locations of peaks in keV to use
|
|
826
|
+
for calibrating the MCA channel energies. It is _strongly_
|
|
827
|
+
recommended to use fluorescence peaks for the energy
|
|
828
|
+
calibration.
|
|
829
|
+
:type peak_energies: list[float]
|
|
830
|
+
:ivar max_peak_index: Index of the peak in `peak_energies`
|
|
831
|
+
with the highest amplitude.
|
|
832
|
+
:type max_peak_index: int
|
|
833
|
+
:ivar fit_index_ranges: Explicit ranges of uncalibrated MCA
|
|
834
|
+
channel index ranges to include during energy calibration
|
|
835
|
+
when the given peaks are fitted to the provied MCA spectrum.
|
|
836
|
+
Use this parameter or select it interactively by running a
|
|
837
|
+
pipeline with `config.interactive: True`.
|
|
838
|
+
:type fit_index_ranges: list[[int, int]], optional
|
|
839
|
+
|
|
541
840
|
"""
|
|
542
|
-
|
|
543
|
-
material: CeriaConfig = CeriaConfig()
|
|
841
|
+
scan_step_indices: Optional[conlist(min_items=1, item_type=conint(ge=0))]
|
|
544
842
|
detectors: conlist(min_items=1, item_type=MCAElementCalibrationConfig)
|
|
545
|
-
flux_file: FilePath
|
|
546
|
-
|
|
547
|
-
|
|
843
|
+
flux_file: Optional[FilePath]
|
|
844
|
+
material: Optional[MaterialConfig] = MaterialConfig(
|
|
845
|
+
material_name='CeO2', lattice_parameters=5.41153, sgnum=225)
|
|
846
|
+
peak_energies: conlist(item_type=confloat(gt=0), min_items=2)
|
|
847
|
+
max_peak_index: conint(ge=0)
|
|
848
|
+
fit_index_ranges: Optional[
|
|
849
|
+
conlist(
|
|
850
|
+
min_items=1,
|
|
851
|
+
item_type=conlist(
|
|
852
|
+
item_type=conint(ge=0),
|
|
853
|
+
min_items=2,
|
|
854
|
+
max_items=2))]
|
|
548
855
|
|
|
549
856
|
@root_validator(pre=True)
|
|
550
857
|
def validate_config(cls, values):
|
|
@@ -559,18 +866,61 @@ class MCACeriaCalibrationConfig(MCAScanDataConfig):
|
|
|
559
866
|
inputdir = values.get('inputdir')
|
|
560
867
|
if inputdir is not None:
|
|
561
868
|
flux_file = values.get('flux_file')
|
|
562
|
-
if not os.path.isabs(flux_file):
|
|
869
|
+
if flux_file is not None and not os.path.isabs(flux_file):
|
|
563
870
|
values['flux_file'] = os.path.join(inputdir, flux_file)
|
|
564
871
|
|
|
565
872
|
return values
|
|
566
873
|
|
|
567
|
-
@
|
|
874
|
+
@validator('scan_step_indices', pre=True, always=True)
|
|
875
|
+
def validate_scan_step_indices(cls, scan_step_indices, values):
|
|
876
|
+
"""Validate the specified list of scan numbers.
|
|
877
|
+
|
|
878
|
+
:ivar scan_step_indices: Optional scan step indices to use for the
|
|
879
|
+
calibration. If not specified, the calibration will be
|
|
880
|
+
performed on the average of all MCA spectra for the scan.
|
|
881
|
+
:type scan_step_indices: list[int], optional
|
|
882
|
+
:param values: Dictionary of validated class field values.
|
|
883
|
+
:type values: dict
|
|
884
|
+
:raises ValueError: If a specified scan number is not found in
|
|
885
|
+
the SPEC file.
|
|
886
|
+
:return: List of step indices.
|
|
887
|
+
:rtype: list of int
|
|
888
|
+
"""
|
|
889
|
+
if isinstance(scan_step_indices, str):
|
|
890
|
+
# Local modules
|
|
891
|
+
from CHAP.utils.general import string_to_list
|
|
892
|
+
|
|
893
|
+
scan_step_indices = string_to_list(
|
|
894
|
+
scan_step_indices, raise_error=True)
|
|
895
|
+
return scan_step_indices
|
|
896
|
+
|
|
897
|
+
@validator('max_peak_index')
|
|
898
|
+
def validate_max_peak_index(cls, max_peak_index, values):
|
|
899
|
+
"""Validate the specified index of the XRF peak with the
|
|
900
|
+
highest amplitude.
|
|
901
|
+
|
|
902
|
+
:ivar max_peak_index: The index of the XRF peak with the
|
|
903
|
+
highest amplitude.
|
|
904
|
+
:type max_peak_index: int
|
|
905
|
+
:param values: Dictionary of validated class field values.
|
|
906
|
+
:type values: dict
|
|
907
|
+
:raises ValueError: Invalid max_peak_index.
|
|
908
|
+
:return: The validated value of `max_peak_index`.
|
|
909
|
+
:rtype: int
|
|
910
|
+
"""
|
|
911
|
+
peak_energies = values.get('peak_energies')
|
|
912
|
+
if not 0 <= max_peak_index < len(peak_energies):
|
|
913
|
+
raise ValueError('max_peak_index out of bounds')
|
|
914
|
+
return max_peak_index
|
|
915
|
+
|
|
568
916
|
def flux_file_energy_range(self):
|
|
569
917
|
"""Get the energy range in the flux corection file.
|
|
570
918
|
|
|
571
919
|
:return: The energy range in the flux corection file.
|
|
572
920
|
:rtype: tuple(float, float)
|
|
573
921
|
"""
|
|
922
|
+
if self.flux_file is None:
|
|
923
|
+
return None
|
|
574
924
|
flux = np.loadtxt(self.flux_file)
|
|
575
925
|
energies = flux[:,0]/1.e3
|
|
576
926
|
return energies.min(), energies.max()
|
|
@@ -583,15 +933,21 @@ class MCACeriaCalibrationConfig(MCAScanDataConfig):
|
|
|
583
933
|
:return: The current detectors's MCA data.
|
|
584
934
|
:rtype: np.ndarray
|
|
585
935
|
"""
|
|
586
|
-
if self.
|
|
936
|
+
if self.scan_step_indices is None:
|
|
587
937
|
data = super().mca_data(detector_config)
|
|
588
938
|
if self.scanparser.spec_scan_npts > 1:
|
|
589
|
-
data = np.average(data, axis=
|
|
939
|
+
data = np.average(data, axis=0)
|
|
590
940
|
else:
|
|
591
941
|
data = data[0]
|
|
942
|
+
elif len(self.scan_step_indices) == 1:
|
|
943
|
+
data = super().mca_data(
|
|
944
|
+
detector_config, scan_step_index=self.scan_step_indices[0])
|
|
592
945
|
else:
|
|
593
|
-
data =
|
|
594
|
-
|
|
946
|
+
data = []
|
|
947
|
+
for scan_step_index in self.scan_step_indices:
|
|
948
|
+
data.append(super().mca_data(
|
|
949
|
+
detector_config, scan_step_index=scan_step_index))
|
|
950
|
+
data = np.average(data, axis=0)
|
|
595
951
|
return data
|
|
596
952
|
|
|
597
953
|
def flux_correction_interpolation_function(self):
|
|
@@ -602,7 +958,8 @@ class MCACeriaCalibrationConfig(MCAScanDataConfig):
|
|
|
602
958
|
:return: Energy flux correction interpolation function.
|
|
603
959
|
:rtype: scipy.interpolate._polyint._Interpolator1D
|
|
604
960
|
"""
|
|
605
|
-
|
|
961
|
+
if self.flux_file is None:
|
|
962
|
+
return None
|
|
606
963
|
flux = np.loadtxt(self.flux_file)
|
|
607
964
|
energies = flux[:,0]/1.e3
|
|
608
965
|
relative_intensities = flux[:,1]/np.max(flux[:,1])
|
|
@@ -610,121 +967,43 @@ class MCACeriaCalibrationConfig(MCAScanDataConfig):
|
|
|
610
967
|
return interpolation_function
|
|
611
968
|
|
|
612
969
|
|
|
613
|
-
class
|
|
614
|
-
"""Class representing metadata required to perform a strain
|
|
615
|
-
analysis fitting for a single MCA detector element.
|
|
616
|
-
|
|
617
|
-
:ivar max_energy_kev: Maximum channel energy of the MCA in keV.
|
|
618
|
-
:type max_energy_kev: float, optional
|
|
619
|
-
:ivar num_bins: Number of MCA channels.
|
|
620
|
-
:type num_bins: int, optional
|
|
621
|
-
:param tth_max: Detector rotation about hutch x axis, defaults
|
|
622
|
-
to `90.0`.
|
|
623
|
-
:type tth_max: float, optional
|
|
624
|
-
:ivar hkl_tth_tol: Minimum resolvable difference in 2&theta between
|
|
625
|
-
two unique HKL peaks, defaults to `0.15`.
|
|
626
|
-
:type hkl_tth_tol: float, optional
|
|
627
|
-
:ivar hkl_indices: List of unique HKL indices to fit peaks for in
|
|
628
|
-
the calibration routine, defaults to `[]`.
|
|
629
|
-
:type hkl_indices: list[int], optional
|
|
630
|
-
:ivar background: Background model for peak fitting.
|
|
631
|
-
:type background: str, list[str], optional
|
|
632
|
-
:ivar peak_models: Peak model for peak fitting,
|
|
633
|
-
defaults to `'gaussian'`.
|
|
634
|
-
:type peak_models: Literal['gaussian', 'lorentzian']],
|
|
635
|
-
list[Literal['gaussian', 'lorentzian']]], optional
|
|
636
|
-
:ivar fwhm_min: Minimum FWHM for peak fitting, defaults to `1.0`.
|
|
637
|
-
:type fwhm_min: float, optional
|
|
638
|
-
:ivar fwhm_max: Maximum FWHM for peak fitting, defaults to `5.0`.
|
|
639
|
-
:type fwhm_max: float, optional
|
|
640
|
-
:ivar rel_amplitude_cutoff: Relative peak amplitude cutoff for
|
|
641
|
-
peak fitting (any peak with an amplitude smaller than
|
|
642
|
-
`rel_amplitude_cutoff` times the sum of all peak amplitudes
|
|
643
|
-
gets removed from the fit model), defaults to `None`.
|
|
644
|
-
:type rel_amplitude_cutoff: float, optional
|
|
645
|
-
:ivar tth_calibrated: Calibrated value for 2&theta.
|
|
646
|
-
:type tth_calibrated: float, optional
|
|
647
|
-
:ivar slope_calibrated: Calibrated value for detector channel.
|
|
648
|
-
energy correction linear slope
|
|
649
|
-
:type slope_calibrated: float, optional
|
|
650
|
-
:ivar intercept_calibrated: Calibrated value for detector channel
|
|
651
|
-
energy correction y-intercept.
|
|
652
|
-
:type intercept_calibrated: float, optional
|
|
970
|
+
class MCATthCalibrationConfig(MCAEnergyCalibrationConfig):
|
|
653
971
|
"""
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
tth_max: confloat(gt=0, allow_inf_nan=False) = 90.0
|
|
657
|
-
hkl_tth_tol: confloat(gt=0, allow_inf_nan=False) = 0.15
|
|
658
|
-
hkl_indices: Optional[conlist(item_type=conint(ge=0), min_items=1)] = []
|
|
659
|
-
background: Optional[str]
|
|
660
|
-
peak_models: Union[
|
|
661
|
-
conlist(item_type=Literal['gaussian', 'lorentzian'], min_items=1),
|
|
662
|
-
Literal['gaussian', 'lorentzian']] = 'gaussian'
|
|
663
|
-
fwhm_min: confloat(gt=0, allow_inf_nan=False) = 1.0
|
|
664
|
-
fwhm_max: confloat(gt=0, allow_inf_nan=False) = 5.0
|
|
665
|
-
rel_amplitude_cutoff: Optional[confloat(gt=0, lt=1.0, allow_inf_nan=False)]
|
|
666
|
-
|
|
667
|
-
tth_calibrated: Optional[confloat(gt=0, allow_inf_nan=False)]
|
|
668
|
-
slope_calibrated: Optional[confloat(allow_inf_nan=False)]
|
|
669
|
-
intercept_calibrated: Optional[confloat(allow_inf_nan=False)]
|
|
670
|
-
tth_file: Optional[FilePath]
|
|
671
|
-
tth_map: Optional[np.ndarray] = None
|
|
672
|
-
|
|
673
|
-
@validator('hkl_indices', pre=True)
|
|
674
|
-
def validate_hkl_indices(cls, hkl_indices):
|
|
675
|
-
if isinstance(hkl_indices, str):
|
|
676
|
-
# Local modules
|
|
677
|
-
from CHAP.utils.general import string_to_list
|
|
678
|
-
|
|
679
|
-
hkl_indices = string_to_list(hkl_indices)
|
|
680
|
-
return sorted(hkl_indices)
|
|
681
|
-
|
|
682
|
-
class Config:
|
|
683
|
-
arbitrary_types_allowed = True
|
|
684
|
-
|
|
685
|
-
def add_calibration(self, calibration):
|
|
686
|
-
"""Finalize values for some fields using a completed
|
|
687
|
-
MCAElementCalibrationConfig that corresponds to the same
|
|
688
|
-
detector.
|
|
689
|
-
|
|
690
|
-
:param calibration: Existing calibration configuration to use
|
|
691
|
-
by MCAElementStrainAnalysisConfig.
|
|
692
|
-
:type calibration: MCAElementCalibrationConfig
|
|
693
|
-
:return: None
|
|
694
|
-
"""
|
|
695
|
-
add_fields = ['tth_calibrated', 'slope_calibrated',
|
|
696
|
-
'intercept_calibrated', 'num_bins', 'max_energy_kev']
|
|
697
|
-
for field in add_fields:
|
|
698
|
-
setattr(self, field, getattr(calibration, field))
|
|
699
|
-
|
|
700
|
-
def get_tth_map(self, map_config):
|
|
701
|
-
"""Return a map of 2&theta values to use -- may vary at each
|
|
702
|
-
point in the map.
|
|
972
|
+
Class representing metadata required to perform a tth calibration
|
|
973
|
+
for an MCA detector.
|
|
703
974
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
975
|
+
:ivar calibration_method: Type of calibration method,
|
|
976
|
+
defaults to `'direct_fit_residual'`.
|
|
977
|
+
:type calibration_method:
|
|
978
|
+
Literal['direct_fit_residual', 'iterate_tth'], optional
|
|
979
|
+
:ivar max_iter: Maximum number of iterations of the calibration
|
|
980
|
+
routine (only used for `'iterate_tth'`), defaults to `10`.
|
|
981
|
+
:type max_iter: int, optional
|
|
982
|
+
:ivar tune_tth_tol: Cutoff error for tuning 2&theta (only used for
|
|
983
|
+
`'iterate_tth'`). Stop iterating the calibration routine after
|
|
984
|
+
an iteration produces a change in the tuned value of 2&theta
|
|
985
|
+
that is smaller than this cutoff, defaults to `1e-8`.
|
|
986
|
+
:ivar tune_tth_tol: float, optional
|
|
987
|
+
"""
|
|
988
|
+
calibration_method: Optional[Literal[
|
|
989
|
+
'direct_fit_residual',
|
|
990
|
+
'direct_fit_peak_energies',
|
|
991
|
+
'direct_fit_combined',
|
|
992
|
+
'iterate_tth']] = 'iterate_tth'
|
|
993
|
+
max_iter: conint(gt=0) = 10
|
|
994
|
+
tune_tth_tol: confloat(ge=0) = 1e-8
|
|
713
995
|
|
|
714
|
-
def
|
|
715
|
-
"""
|
|
716
|
-
dictionary that is suitable for dumping to a YAML file.
|
|
996
|
+
def flux_file_energy_range(self):
|
|
997
|
+
"""Get the energy range in the flux corection file.
|
|
717
998
|
|
|
718
|
-
:return:
|
|
719
|
-
:rtype:
|
|
999
|
+
:return: The energy range in the flux corection file.
|
|
1000
|
+
:rtype: tuple(float, float)
|
|
720
1001
|
"""
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
d[k] = v.tolist()
|
|
727
|
-
return d
|
|
1002
|
+
if self.flux_file is None:
|
|
1003
|
+
return None
|
|
1004
|
+
flux = np.loadtxt(self.flux_file)
|
|
1005
|
+
energies = flux[:,0]/1.e3
|
|
1006
|
+
return energies.min(), energies.max()
|
|
728
1007
|
|
|
729
1008
|
|
|
730
1009
|
class StrainAnalysisConfig(BaseModel):
|
|
@@ -739,6 +1018,8 @@ class StrainAnalysisConfig(BaseModel):
|
|
|
739
1018
|
:type map_config: CHAP.common.models.map.MapConfig, optional
|
|
740
1019
|
:ivar par_file: Path to the par file associated with the scan.
|
|
741
1020
|
:type par_file: str, optional
|
|
1021
|
+
:ivar dataset_id: Integer ID of the SMB-style EDD dataset.
|
|
1022
|
+
:type dataset_id: int, optional
|
|
742
1023
|
:ivar par_dims: List of independent dimensions.
|
|
743
1024
|
:type par_dims: list[dict[str,str]], optional
|
|
744
1025
|
:ivar other_dims: List of other column names from `par_file`.
|
|
@@ -746,17 +1027,26 @@ class StrainAnalysisConfig(BaseModel):
|
|
|
746
1027
|
:ivar detectors: List of individual detector element strain
|
|
747
1028
|
analysis configurations
|
|
748
1029
|
:type detectors: list[MCAElementStrainAnalysisConfig]
|
|
749
|
-
:ivar
|
|
750
|
-
:type
|
|
1030
|
+
:ivar materials: Sample material configurations.
|
|
1031
|
+
:type materials: list[MaterialConfig]
|
|
1032
|
+
:ivar flux_file: File name of the csv flux file containing station
|
|
1033
|
+
beam energy in eV (column 0) versus flux (column 1).
|
|
1034
|
+
:type flux_file: str, optional
|
|
1035
|
+
:ivar sum_axes: Whether to sum over the fly axis or not
|
|
1036
|
+
for EDD scan types not 0, defaults to `True`.
|
|
1037
|
+
:type sum_axes: bool, optional
|
|
751
1038
|
"""
|
|
752
1039
|
inputdir: Optional[DirectoryPath]
|
|
753
1040
|
map_config: Optional[MapConfig]
|
|
754
1041
|
par_file: Optional[FilePath]
|
|
1042
|
+
dataset_id: Optional[int]
|
|
755
1043
|
par_dims: Optional[list[dict[str,str]]]
|
|
756
1044
|
other_dims: Optional[list[dict[str,str]]]
|
|
757
1045
|
detectors: conlist(min_items=1, item_type=MCAElementStrainAnalysisConfig)
|
|
758
1046
|
materials: list[MaterialConfig]
|
|
759
|
-
flux_file: FilePath
|
|
1047
|
+
flux_file: Optional[FilePath]
|
|
1048
|
+
sum_axes: Optional[list[str]]
|
|
1049
|
+
oversampling: Optional[dict] = {'num': 10}
|
|
760
1050
|
|
|
761
1051
|
_parfile: Optional[ParFile]
|
|
762
1052
|
|
|
@@ -774,18 +1064,25 @@ class StrainAnalysisConfig(BaseModel):
|
|
|
774
1064
|
inputdir = values.get('inputdir')
|
|
775
1065
|
flux_file = values.get('flux_file')
|
|
776
1066
|
par_file = values.get('par_file')
|
|
777
|
-
if inputdir is not None and not
|
|
1067
|
+
if (inputdir is not None and flux_file is not None
|
|
1068
|
+
and not os.path.isabs(flux_file)):
|
|
778
1069
|
values['flux_file'] = os.path.join(inputdir, flux_file)
|
|
779
1070
|
if par_file is not None:
|
|
780
1071
|
if inputdir is not None and not os.path.isabs(par_file):
|
|
781
1072
|
values['par_file'] = os.path.join(inputdir, par_file)
|
|
782
|
-
if '
|
|
1073
|
+
if 'dataset_id' in values:
|
|
1074
|
+
from CHAP.edd import EddMapReader
|
|
1075
|
+
values['_parfile'] = ParFile(values['par_file'])
|
|
1076
|
+
values['map_config'] = EddMapReader().read(
|
|
1077
|
+
values['par_file'], values['dataset_id'])
|
|
1078
|
+
elif 'par_dims' in values:
|
|
1079
|
+
values['_parfile'] = ParFile(values['par_file'])
|
|
1080
|
+
values['map_config'] = values['_parfile'].get_map(
|
|
1081
|
+
'EDD', 'id1a3', values['par_dims'],
|
|
1082
|
+
other_dims=values.get('other_dims', []))
|
|
1083
|
+
else:
|
|
783
1084
|
raise ValueError(
|
|
784
|
-
'par_dims is required when using par_file')
|
|
785
|
-
values['_parfile'] = ParFile(values['par_file'])
|
|
786
|
-
values['map_config'] = values['_parfile'].get_map(
|
|
787
|
-
'EDD', 'id1a3', values['par_dims'],
|
|
788
|
-
other_dims=values.get('other_dims', []))
|
|
1085
|
+
'dataset_id or par_dims is required when using par_file')
|
|
789
1086
|
map_config = values.get('map_config')
|
|
790
1087
|
if isinstance(map_config, dict):
|
|
791
1088
|
for i, scans in enumerate(map_config.get('spec_scans')):
|
|
@@ -825,6 +1122,68 @@ class StrainAnalysisConfig(BaseModel):
|
|
|
825
1122
|
+ f'{detector.tth_file}') from e
|
|
826
1123
|
return detector
|
|
827
1124
|
|
|
1125
|
+
@validator('sum_axes', always=True)
|
|
1126
|
+
def validate_sum_axes(cls, value, values):
|
|
1127
|
+
"""Validate the sum_axes field.
|
|
1128
|
+
|
|
1129
|
+
:param value: Field value to validate (`sum_axes`).
|
|
1130
|
+
:type value: bool
|
|
1131
|
+
:param values: Dictionary of validated class field values.
|
|
1132
|
+
:type values: dict
|
|
1133
|
+
:return: The validated value for sum_axes.
|
|
1134
|
+
:rtype: bool
|
|
1135
|
+
"""
|
|
1136
|
+
if value is None:
|
|
1137
|
+
map_config = values.get('map_config')
|
|
1138
|
+
if map_config is not None:
|
|
1139
|
+
if map_config.attrs['scan_type'] < 3:
|
|
1140
|
+
value = value
|
|
1141
|
+
else:
|
|
1142
|
+
value = map_config.attrs.get('fly_axis_labels', [])
|
|
1143
|
+
return value
|
|
1144
|
+
|
|
1145
|
+
@validator('oversampling', always=True)
|
|
1146
|
+
def validate_oversampling(cls, value, values):
|
|
1147
|
+
"""Validate the oversampling field.
|
|
1148
|
+
|
|
1149
|
+
:param value: Field value to validate (`oversampling`).
|
|
1150
|
+
:type value: bool
|
|
1151
|
+
:param values: Dictionary of validated class field values.
|
|
1152
|
+
:type values: dict
|
|
1153
|
+
:return: The validated value for oversampling.
|
|
1154
|
+
:rtype: bool
|
|
1155
|
+
"""
|
|
1156
|
+
# Local modules
|
|
1157
|
+
from CHAP.utils.general import is_int
|
|
1158
|
+
|
|
1159
|
+
map_config = values.get('map_config')
|
|
1160
|
+
if map_config is None or map_config.attrs['scan_type'] < 3:
|
|
1161
|
+
return None
|
|
1162
|
+
if value is None:
|
|
1163
|
+
return {'num': 10}
|
|
1164
|
+
if 'start' in value and not is_int(value['start'], ge=0):
|
|
1165
|
+
raise ValueError('Invalid "start" parameter in "oversampling" '
|
|
1166
|
+
f'field ({value["start"]})')
|
|
1167
|
+
if 'end' in value and not is_int(value['end'], gt=0):
|
|
1168
|
+
raise ValueError('Invalid "end" parameter in "oversampling" '
|
|
1169
|
+
f'field ({value["end"]})')
|
|
1170
|
+
if 'width' in value and not is_int(value['width'], gt=0):
|
|
1171
|
+
raise ValueError('Invalid "width" parameter in "oversampling" '
|
|
1172
|
+
f'field ({value["width"]})')
|
|
1173
|
+
if 'stride' in value and not is_int(value['stride'], gt=0):
|
|
1174
|
+
raise ValueError('Invalid "stride" parameter in "oversampling" '
|
|
1175
|
+
f'field ({value["stride"]})')
|
|
1176
|
+
if 'num' in value and not is_int(value['num'], gt=0):
|
|
1177
|
+
raise ValueError('Invalid "num" parameter in "oversampling" '
|
|
1178
|
+
f'field ({value["num"]})')
|
|
1179
|
+
if 'mode' in value and 'mode' not in ('valid', 'full'):
|
|
1180
|
+
raise ValueError('Invalid "mode" parameter in "oversampling" '
|
|
1181
|
+
f'field ({value["mode"]})')
|
|
1182
|
+
if not ('width' in value or 'stride' in value or 'num' in value):
|
|
1183
|
+
raise ValueError('Invalid input parameters, specify at least one '
|
|
1184
|
+
'of "width", "stride" or "num"')
|
|
1185
|
+
return value
|
|
1186
|
+
|
|
828
1187
|
def mca_data(self, detector=None, map_index=None):
|
|
829
1188
|
"""Get MCA data for a single or multiple detector elements.
|
|
830
1189
|
|
|
@@ -842,7 +1201,8 @@ class StrainAnalysisConfig(BaseModel):
|
|
|
842
1201
|
if detector is None:
|
|
843
1202
|
mca_data = []
|
|
844
1203
|
for detector_config in self.detectors:
|
|
845
|
-
mca_data.append(
|
|
1204
|
+
mca_data.append(
|
|
1205
|
+
self.mca_data(detector_config, map_index))
|
|
846
1206
|
return np.asarray(mca_data)
|
|
847
1207
|
else:
|
|
848
1208
|
if isinstance(detector, int):
|
|
@@ -854,8 +1214,65 @@ class StrainAnalysisConfig(BaseModel):
|
|
|
854
1214
|
if map_index is None:
|
|
855
1215
|
mca_data = []
|
|
856
1216
|
for map_index in np.ndindex(self.map_config.shape):
|
|
857
|
-
mca_data.append(self.mca_data(
|
|
858
|
-
|
|
1217
|
+
mca_data.append(self.mca_data(
|
|
1218
|
+
detector_config, map_index))
|
|
1219
|
+
mca_data = np.reshape(
|
|
1220
|
+
mca_data, (*self.map_config.shape, len(mca_data[0])))
|
|
1221
|
+
if self.sum_axes:
|
|
1222
|
+
scan_type = self.map_config.attrs['scan_type']
|
|
1223
|
+
if self.map_config.map_type == 'structured':
|
|
1224
|
+
sum_axis_indices = []
|
|
1225
|
+
for axis in self.sum_axes:
|
|
1226
|
+
sum_axis_indices.append(
|
|
1227
|
+
self.map_config.dims.index(axis))
|
|
1228
|
+
mca_data = np.sum(
|
|
1229
|
+
mca_data, tuple(sorted(sum_axis_indices)))
|
|
1230
|
+
if scan_type == 4:
|
|
1231
|
+
raise NotImplementedError(
|
|
1232
|
+
'Oversampling scan types not tested yet.')
|
|
1233
|
+
from CHAP.edd.utils import get_rolling_sum_spectra
|
|
1234
|
+
mca_data = get_rolling_sum_spectra(
|
|
1235
|
+
mca_data,
|
|
1236
|
+
self.map_config.dims.index(fly_axis_labels[0]),
|
|
1237
|
+
self.oversampling.get('start', 0),
|
|
1238
|
+
self.oversampling.get('end'),
|
|
1239
|
+
self.oversampling.get('width'),
|
|
1240
|
+
self.oversampling.get('stride'),
|
|
1241
|
+
self.oversampling.get('num'),
|
|
1242
|
+
self.oversampling.get('mode', 'valid'))
|
|
1243
|
+
elif scan_type not in (0, 1, 2, 3, 5):
|
|
1244
|
+
raise ValueError(
|
|
1245
|
+
f'scan_type {scan_type} not implemented yet '
|
|
1246
|
+
'in StrainAnalysisConfig.mca_data()')
|
|
1247
|
+
else:
|
|
1248
|
+
# Perform summing along axes of an unstructured map
|
|
1249
|
+
map_dims = self.map_config.dims
|
|
1250
|
+
map_coords = self.map_config.coords
|
|
1251
|
+
map_length = len(map_coords[map_dims[0]])
|
|
1252
|
+
for sum_axis in self.sum_axes:
|
|
1253
|
+
axis_index = map_dims.index(sum_axis)
|
|
1254
|
+
sum_map_indices = {}
|
|
1255
|
+
for i in range(map_length):
|
|
1256
|
+
coord = tuple(
|
|
1257
|
+
v[i] for k, v in map_coords.items() \
|
|
1258
|
+
if k != sum_axis)
|
|
1259
|
+
if coord not in sum_map_indices:
|
|
1260
|
+
sum_map_indices[coord] = []
|
|
1261
|
+
sum_map_indices[coord].append(i)
|
|
1262
|
+
map_dims = (*map_dims[:axis_index],
|
|
1263
|
+
*map_dims[axis_index + 1:])
|
|
1264
|
+
sum_indices_list = sum_map_indices.values()
|
|
1265
|
+
map_coords = {
|
|
1266
|
+
dim: [map_coords[dim][sum_indices[0]] \
|
|
1267
|
+
for sum_indices in sum_indices_list] \
|
|
1268
|
+
for dim in map_dims}
|
|
1269
|
+
map_length = len(map_coords[map_dims[0]])
|
|
1270
|
+
mca_data = np.asarray(
|
|
1271
|
+
[np.sum(mca_data[sum_indices], axis=0) \
|
|
1272
|
+
for sum_indices in sum_indices_list])
|
|
1273
|
+
return mca_data
|
|
1274
|
+
else:
|
|
1275
|
+
return np.asarray(mca_data)
|
|
859
1276
|
else:
|
|
860
1277
|
return self.map_config.get_detector_data(
|
|
861
1278
|
detector_config.detector_name, map_index)
|