ChessAnalysisPipeline 0.0.11__py3-none-any.whl → 0.0.13__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 +2 -0
- CHAP/common/__init__.py +6 -2
- CHAP/common/models/map.py +217 -70
- CHAP/common/processor.py +249 -155
- CHAP/common/reader.py +175 -130
- CHAP/common/writer.py +150 -94
- CHAP/edd/models.py +458 -262
- CHAP/edd/processor.py +614 -354
- CHAP/edd/utils.py +746 -235
- CHAP/tomo/models.py +22 -18
- CHAP/tomo/processor.py +1215 -892
- CHAP/utils/fit.py +211 -127
- CHAP/utils/general.py +789 -610
- CHAP/utils/parfile.py +1 -9
- CHAP/utils/scanparsers.py +101 -52
- {ChessAnalysisPipeline-0.0.11.dist-info → ChessAnalysisPipeline-0.0.13.dist-info}/METADATA +1 -1
- {ChessAnalysisPipeline-0.0.11.dist-info → ChessAnalysisPipeline-0.0.13.dist-info}/RECORD +21 -21
- {ChessAnalysisPipeline-0.0.11.dist-info → ChessAnalysisPipeline-0.0.13.dist-info}/WHEEL +1 -1
- {ChessAnalysisPipeline-0.0.11.dist-info → ChessAnalysisPipeline-0.0.13.dist-info}/LICENSE +0 -0
- {ChessAnalysisPipeline-0.0.11.dist-info → ChessAnalysisPipeline-0.0.13.dist-info}/entry_points.txt +0 -0
- {ChessAnalysisPipeline-0.0.11.dist-info → ChessAnalysisPipeline-0.0.13.dist-info}/top_level.txt +0 -0
CHAP/edd/models.py
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
#
|
|
2
|
-
import numpy as np
|
|
1
|
+
# System modules
|
|
3
2
|
import os
|
|
4
3
|
from pathlib import PosixPath
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
from typing import (
|
|
5
|
+
Literal,
|
|
6
|
+
Optional,
|
|
7
|
+
Union,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
# Third party modules
|
|
11
|
+
import numpy as np
|
|
12
|
+
from hexrd.material import Material
|
|
13
|
+
from pydantic import (
|
|
14
|
+
BaseModel,
|
|
15
|
+
confloat,
|
|
16
|
+
conint,
|
|
17
|
+
conlist,
|
|
18
|
+
constr,
|
|
19
|
+
DirectoryPath,
|
|
20
|
+
FilePath,
|
|
21
|
+
root_validator,
|
|
22
|
+
validator,
|
|
23
|
+
)
|
|
14
24
|
from scipy.interpolate import interp1d
|
|
15
|
-
from typing import Literal, Optional, Union
|
|
16
25
|
|
|
17
|
-
#
|
|
26
|
+
# Local modules
|
|
18
27
|
from CHAP.common.models.map import MapConfig
|
|
19
|
-
from CHAP.utils.material import Material
|
|
20
28
|
from CHAP.utils.parfile import ParFile
|
|
21
29
|
from CHAP.utils.scanparsers import SMBMCAScanParser as ScanParser
|
|
22
30
|
|
|
@@ -25,50 +33,64 @@ class MCAElementConfig(BaseModel):
|
|
|
25
33
|
"""Class representing metadata required to configure a single MCA
|
|
26
34
|
detector element.
|
|
27
35
|
|
|
28
|
-
:ivar detector_name:
|
|
36
|
+
:ivar detector_name: Name of the MCA detector element in the scan,
|
|
37
|
+
defaults to `'mca1'`.
|
|
29
38
|
:type detector_name: str
|
|
30
|
-
:ivar num_bins:
|
|
31
|
-
:type num_bins: int
|
|
32
|
-
:ivar include_bin_ranges:
|
|
33
|
-
data should be included after applying a mask
|
|
34
|
-
|
|
39
|
+
:ivar num_bins: Number of MCA channels.
|
|
40
|
+
: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
|
|
35
45
|
"""
|
|
36
46
|
detector_name: constr(strip_whitespace=True, min_length=1) = 'mca1'
|
|
37
47
|
num_bins: Optional[conint(gt=0)]
|
|
38
|
-
include_bin_ranges:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
item_type=
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
max_items=2))] = None
|
|
48
|
+
include_bin_ranges: conlist(
|
|
49
|
+
min_items=1,
|
|
50
|
+
item_type=conlist(
|
|
51
|
+
item_type=conint(ge=0),
|
|
52
|
+
min_items=2,
|
|
53
|
+
max_items=2)) = []
|
|
45
54
|
|
|
46
55
|
@validator('include_bin_ranges', each_item=True)
|
|
47
56
|
def validate_include_bin_range(cls, value, values):
|
|
48
|
-
"""Ensure no bin ranges are outside the boundary of the
|
|
57
|
+
"""Ensure that no bin ranges are outside the boundary of the
|
|
58
|
+
detector.
|
|
59
|
+
|
|
60
|
+
:param value: Field value to validate (`include_bin_ranges`).
|
|
61
|
+
:type values: dict
|
|
62
|
+
:param values: Dictionary of previously validated field values.
|
|
63
|
+
:type values: dict
|
|
64
|
+
:return: The validated value of `include_bin_ranges`.
|
|
65
|
+
:rtype: dict
|
|
66
|
+
"""
|
|
49
67
|
num_bins = values.get('num_bins')
|
|
50
68
|
if num_bins is not None:
|
|
51
|
-
value[1] = min(value[1], num_bins)
|
|
69
|
+
value[1] = min(value[1], num_bins-1)
|
|
70
|
+
if value[0] >= value[1]:
|
|
71
|
+
raise ValueError('Invalid bin range in include_bin_ranges '
|
|
72
|
+
f'({value})')
|
|
52
73
|
return value
|
|
53
74
|
|
|
54
75
|
def mca_mask(self):
|
|
55
76
|
"""Get a boolean mask array to use on this MCA element's data.
|
|
77
|
+
Note that the bounds of self.include_bin_ranges are inclusive.
|
|
56
78
|
|
|
57
|
-
:return:
|
|
79
|
+
:return: Boolean mask array.
|
|
58
80
|
:rtype: numpy.ndarray
|
|
59
81
|
"""
|
|
60
82
|
mask = np.asarray([False] * self.num_bins)
|
|
61
83
|
bin_indices = np.arange(self.num_bins)
|
|
62
84
|
for min_, max_ in self.include_bin_ranges:
|
|
63
|
-
|
|
64
|
-
|
|
85
|
+
mask = np.logical_or(
|
|
86
|
+
mask, np.logical_and(bin_indices >= min_, bin_indices <= max_))
|
|
65
87
|
return mask
|
|
66
88
|
|
|
67
89
|
def dict(self, *args, **kwargs):
|
|
68
90
|
"""Return a representation of this configuration in a
|
|
69
91
|
dictionary that is suitable for dumping to a YAML file.
|
|
70
92
|
|
|
71
|
-
:return:
|
|
93
|
+
:return: Dictionary representation of the configuration.
|
|
72
94
|
:rtype: dict
|
|
73
95
|
"""
|
|
74
96
|
d = super().dict(*args, **kwargs)
|
|
@@ -82,22 +104,26 @@ class MCAScanDataConfig(BaseModel):
|
|
|
82
104
|
"""Class representing metadata required to locate raw MCA data for
|
|
83
105
|
a single scan and construct a mask for it.
|
|
84
106
|
|
|
85
|
-
:ivar
|
|
86
|
-
|
|
87
|
-
:
|
|
88
|
-
|
|
89
|
-
:
|
|
90
|
-
:ivar
|
|
91
|
-
|
|
92
|
-
:ivar
|
|
93
|
-
|
|
107
|
+
:ivar inputdir: Input directory, used only if any file in the
|
|
108
|
+
configuration is not an absolute path.
|
|
109
|
+
:type inputdir: str, optional
|
|
110
|
+
:ivar spec_file: Path to the SPEC file containing the scan.
|
|
111
|
+
:type spec_file: str, optional
|
|
112
|
+
:ivar scan_number: Number of the scan in `spec_file`.
|
|
113
|
+
:type scan_number: int, optional
|
|
114
|
+
:ivar par_file: Path to the par file associated with the scan.
|
|
115
|
+
:type par_file: str, optional
|
|
116
|
+
:ivar scan_column: Required column name in `par_file`.
|
|
117
|
+
:type scan_column: str, optional
|
|
118
|
+
:ivar detectors: List of MCA detector element metadata
|
|
119
|
+
configurations.
|
|
120
|
+
:type detectors: list[MCAElementConfig]
|
|
94
121
|
"""
|
|
95
122
|
inputdir: Optional[DirectoryPath]
|
|
96
123
|
spec_file: Optional[FilePath]
|
|
97
124
|
scan_number: Optional[conint(gt=0)]
|
|
98
125
|
par_file: Optional[FilePath]
|
|
99
|
-
scan_column: Optional[
|
|
100
|
-
|
|
126
|
+
scan_column: Optional[str]
|
|
101
127
|
detectors: conlist(min_items=1, item_type=MCAElementConfig)
|
|
102
128
|
|
|
103
129
|
_parfile: Optional[ParFile]
|
|
@@ -108,35 +134,30 @@ class MCAScanDataConfig(BaseModel):
|
|
|
108
134
|
|
|
109
135
|
@root_validator(pre=True)
|
|
110
136
|
def validate_scan(cls, values):
|
|
111
|
-
"""Finalize file paths for spec_file
|
|
137
|
+
"""Finalize file paths for spec_file and par_file.
|
|
112
138
|
|
|
113
|
-
:param values:
|
|
139
|
+
:param values: Dictionary of class field values.
|
|
114
140
|
:type values: dict
|
|
115
|
-
:
|
|
141
|
+
:raises ValueError: Invalid SPEC or par file.
|
|
142
|
+
:return: The validated list of `values`.
|
|
116
143
|
:rtype: dict
|
|
117
144
|
"""
|
|
118
145
|
inputdir = values.get('inputdir')
|
|
119
146
|
spec_file = values.get('spec_file')
|
|
120
147
|
par_file = values.get('par_file')
|
|
121
|
-
|
|
122
|
-
if flux_file:
|
|
123
|
-
if not os.path.isabs(flux_file):
|
|
124
|
-
values['flux_file'] = os.path.join(inputdir, flux_file)
|
|
125
|
-
if spec_file and par_file:
|
|
148
|
+
if spec_file is not None and par_file is not None:
|
|
126
149
|
raise ValueError('Use either spec_file or par_file, not both')
|
|
127
|
-
elif spec_file:
|
|
128
|
-
if inputdir is not None:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if not os.path.isabs(par_file):
|
|
134
|
-
values['par_file'] = os.path.join(inputdir, par_file)
|
|
150
|
+
elif spec_file is not None:
|
|
151
|
+
if inputdir is not None and not os.path.isabs(spec_file):
|
|
152
|
+
values['spec_file'] = os.path.join(inputdir, spec_file)
|
|
153
|
+
elif par_file is not None:
|
|
154
|
+
if inputdir is not None and not os.path.isabs(par_file):
|
|
155
|
+
values['par_file'] = os.path.join(inputdir, par_file)
|
|
135
156
|
if 'scan_column' not in values:
|
|
136
157
|
raise ValueError(
|
|
137
|
-
'
|
|
158
|
+
'scan_column is required when par_file is used')
|
|
138
159
|
if isinstance(values['scan_column'], str):
|
|
139
|
-
parfile = ParFile(
|
|
160
|
+
parfile = ParFile(par_file)
|
|
140
161
|
if values['scan_column'] not in parfile.column_names:
|
|
141
162
|
raise ValueError(
|
|
142
163
|
f'No column named {values["scan_column"]} in '
|
|
@@ -149,31 +170,68 @@ class MCAScanDataConfig(BaseModel):
|
|
|
149
170
|
|
|
150
171
|
@root_validator
|
|
151
172
|
def validate_detectors(cls, values):
|
|
152
|
-
"""Fill in values for _scanparser / _parfile (if
|
|
153
|
-
|
|
154
|
-
field
|
|
173
|
+
"""Fill in values for _scanparser / _parfile (if applicable).
|
|
174
|
+
Fill in each detector's num_bins field, if needed.
|
|
175
|
+
Check each detector's include_bin_ranges field against the
|
|
176
|
+
flux file, if available.
|
|
177
|
+
|
|
178
|
+
:param values: Dictionary of previously validated field values.
|
|
179
|
+
:type values: dict
|
|
180
|
+
:raises ValueError: Unable to obtain a value for num_bins.
|
|
181
|
+
:return: The validated list of `values`.
|
|
182
|
+
:rtype: dict
|
|
155
183
|
"""
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
184
|
+
spec_file = values.get('spec_file')
|
|
185
|
+
par_file = values.get('par_file')
|
|
186
|
+
detectors = values.get('detectors')
|
|
187
|
+
flux_file = values.get('flux_file')
|
|
188
|
+
if spec_file is not None:
|
|
189
|
+
values['_scanparser'] = ScanParser(
|
|
190
|
+
spec_file, values.get('scan_number'))
|
|
159
191
|
values['_parfile'] = None
|
|
160
|
-
elif
|
|
161
|
-
values['_parfile'] = ParFile(
|
|
192
|
+
elif par_file is not None:
|
|
193
|
+
values['_parfile'] = ParFile(par_file)
|
|
162
194
|
values['_scanparser'] = ScanParser(
|
|
163
195
|
values['_parfile'].spec_file,
|
|
164
196
|
values['_parfile'].good_scan_numbers()[0])
|
|
165
|
-
for detector in
|
|
197
|
+
for detector in detectors:
|
|
166
198
|
if detector.num_bins is None:
|
|
167
199
|
try:
|
|
168
200
|
detector.num_bins = values['_scanparser']\
|
|
169
201
|
.get_detector_num_bins(detector.detector_name)
|
|
170
|
-
except Exception as
|
|
171
|
-
raise ValueError('No value found for num_bins') from
|
|
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
|
|
172
229
|
|
|
173
230
|
return values
|
|
174
231
|
|
|
175
232
|
@property
|
|
176
233
|
def scanparser(self):
|
|
234
|
+
"""Return the scanparser."""
|
|
177
235
|
try:
|
|
178
236
|
scanparser = self._scanparser
|
|
179
237
|
except:
|
|
@@ -184,15 +242,18 @@ class MCAScanDataConfig(BaseModel):
|
|
|
184
242
|
def mca_data(self, detector_config, scan_step_index=None):
|
|
185
243
|
"""Get the array of MCA data collected by the scan.
|
|
186
244
|
|
|
187
|
-
:param detector_config:
|
|
245
|
+
:param detector_config: Detector for which data is returned.
|
|
188
246
|
:type detector_config: MCAElementConfig
|
|
189
|
-
:
|
|
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.
|
|
190
252
|
:rtype: np.ndarray
|
|
191
253
|
"""
|
|
192
254
|
detector_name = detector_config.detector_name
|
|
193
255
|
if self._parfile is not None:
|
|
194
256
|
if scan_step_index is None:
|
|
195
|
-
import numpy as np
|
|
196
257
|
data = np.asarray(
|
|
197
258
|
[ScanParser(self._parfile.spec_file, scan_number)\
|
|
198
259
|
.get_all_detector_data(detector_name)[0] \
|
|
@@ -215,7 +276,7 @@ class MCAScanDataConfig(BaseModel):
|
|
|
215
276
|
"""Return a representation of this configuration in a
|
|
216
277
|
dictionary that is suitable for dumping to a YAML file.
|
|
217
278
|
|
|
218
|
-
:return:
|
|
279
|
+
:return: Dictionary representation of the configuration.
|
|
219
280
|
:rtype: dict
|
|
220
281
|
"""
|
|
221
282
|
d = super().dict(*args, **kwargs)
|
|
@@ -235,14 +296,14 @@ class MCAScanDataConfig(BaseModel):
|
|
|
235
296
|
|
|
236
297
|
|
|
237
298
|
class MaterialConfig(BaseModel):
|
|
238
|
-
"""Model for parameters to characterize a sample material
|
|
239
|
-
|
|
240
|
-
:ivar
|
|
241
|
-
|
|
242
|
-
:ivar
|
|
243
|
-
|
|
244
|
-
:ivar
|
|
245
|
-
|
|
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
|
|
246
307
|
"""
|
|
247
308
|
material_name: Optional[constr(strip_whitespace=True, min_length=1)]
|
|
248
309
|
lattice_parameters: Optional[Union[
|
|
@@ -257,32 +318,43 @@ class MaterialConfig(BaseModel):
|
|
|
257
318
|
|
|
258
319
|
@root_validator
|
|
259
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
|
|
260
329
|
from CHAP.edd.utils import make_material
|
|
330
|
+
|
|
261
331
|
values['_material'] = make_material(values.get('material_name'),
|
|
262
332
|
values.get('sgnum'),
|
|
263
333
|
values.get('lattice_parameters'))
|
|
264
334
|
return values
|
|
265
335
|
|
|
266
|
-
def
|
|
267
|
-
"""Get a list of unique HKLs and their lattice spacings
|
|
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.
|
|
268
338
|
|
|
269
|
-
:param tth_tol:
|
|
339
|
+
:param tth_tol: Minimum resolvable difference in 2&theta
|
|
270
340
|
between two unique HKL peaks, defaults to `0.15`.
|
|
271
341
|
:type tth_tol: float, optional
|
|
272
|
-
:param tth_max:
|
|
273
|
-
to `90.0`.
|
|
342
|
+
:param tth_max: Detector rotation about hutch x axis,
|
|
343
|
+
defaults to `90.0`.
|
|
274
344
|
:type tth_max: float, optional
|
|
275
|
-
:return:
|
|
345
|
+
:return: Unique HKLs and their lattice spacings in angstroms.
|
|
276
346
|
:rtype: np.ndarray, np.ndarray
|
|
277
347
|
"""
|
|
348
|
+
# Local modules
|
|
278
349
|
from CHAP.edd.utils import get_unique_hkls_ds
|
|
350
|
+
|
|
279
351
|
return get_unique_hkls_ds([self._material])
|
|
280
352
|
|
|
281
353
|
def dict(self, *args, **kwargs):
|
|
282
354
|
"""Return a representation of this configuration in a
|
|
283
355
|
dictionary that is suitable for dumping to a YAML file.
|
|
284
356
|
|
|
285
|
-
:return:
|
|
357
|
+
:return: Dictionary representation of the configuration.
|
|
286
358
|
:rtype: dict
|
|
287
359
|
"""
|
|
288
360
|
d = super().dict(*args, **kwargs)
|
|
@@ -295,79 +367,87 @@ class MaterialConfig(BaseModel):
|
|
|
295
367
|
|
|
296
368
|
|
|
297
369
|
class MCAElementCalibrationConfig(MCAElementConfig):
|
|
298
|
-
"""Class representing metadata
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
:ivar max_energy_kev:
|
|
302
|
-
:
|
|
303
|
-
:ivar
|
|
304
|
-
|
|
305
|
-
:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
:ivar
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
:ivar
|
|
316
|
-
correction
|
|
370
|
+
"""Class representing metadata required to calibrate a single MCA
|
|
371
|
+
detector element.
|
|
372
|
+
|
|
373
|
+
:ivar max_energy_kev: Maximum channel energy of the MCA in keV.
|
|
374
|
+
:type max_energy_kev: float
|
|
375
|
+
:ivar tth_max: Detector rotation about lab frame x axis,
|
|
376
|
+
defaults to `90`.
|
|
377
|
+
:type tth_max: float, optional
|
|
378
|
+
:ivar hkl_tth_tol: Minimum resolvable difference in 2&theta between
|
|
379
|
+
two unique HKL peaks, defaults to `0.15`.
|
|
380
|
+
:type hkl_tth_tol: float, optional
|
|
381
|
+
:ivar hkl_indices: List of unique HKL indices to fit peaks for in
|
|
382
|
+
the calibration routine, defaults to `[]`.
|
|
383
|
+
:type hkl_indices: list[int], optional
|
|
384
|
+
:ivar tth_initial_guess: Initial guess for 2&theta,
|
|
385
|
+
defaults to `5.0`.
|
|
386
|
+
:type tth_initial_guess: float, optional
|
|
387
|
+
:ivar slope_initial_guess: Initial guess for the detector channel
|
|
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
|
|
317
401
|
"""
|
|
318
402
|
max_energy_kev: confloat(gt=0)
|
|
319
403
|
tth_max: confloat(gt=0, allow_inf_nan=False) = 90.0
|
|
320
404
|
hkl_tth_tol: confloat(gt=0, allow_inf_nan=False) = 0.15
|
|
321
|
-
|
|
322
|
-
tth_initial_guess: confloat(gt=0, le=tth_max, allow_inf_nan=False)
|
|
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
|
|
323
407
|
slope_initial_guess: float = 1.0
|
|
324
408
|
intercept_initial_guess: float = 0.0
|
|
325
409
|
tth_calibrated: Optional[confloat(gt=0, allow_inf_nan=False)]
|
|
326
410
|
slope_calibrated: Optional[confloat(allow_inf_nan=False)]
|
|
327
411
|
intercept_calibrated: Optional[confloat(allow_inf_nan=False)]
|
|
328
412
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
:rtype: np.ndarray, np.ndarray
|
|
335
|
-
"""
|
|
336
|
-
if not isinstance(materials, list):
|
|
337
|
-
materials = [materials]
|
|
338
|
-
from CHAP.edd.utils import get_unique_hkls_ds
|
|
339
|
-
unique_hkls, unique_ds = get_unique_hkls_ds(materials)
|
|
340
|
-
|
|
341
|
-
fit_hkls = np.array([unique_hkls[i] for i in self.fit_hkls])
|
|
342
|
-
fit_ds = np.array([unique_ds[i] for i in self.fit_hkls])
|
|
343
|
-
|
|
344
|
-
return fit_hkls, fit_ds
|
|
413
|
+
@validator('hkl_indices', pre=True)
|
|
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
|
|
345
418
|
|
|
419
|
+
hkl_indices = string_to_list(hkl_indices)
|
|
420
|
+
return sorted(hkl_indices)
|
|
346
421
|
|
|
347
422
|
class MCAElementDiffractionVolumeLengthConfig(MCAElementConfig):
|
|
348
|
-
"""Class representing
|
|
349
|
-
|
|
350
|
-
element.
|
|
423
|
+
"""Class representing metadata required to perform a diffraction
|
|
424
|
+
volume length measurement for a single MCA detector element.
|
|
351
425
|
|
|
352
|
-
:ivar measurement_mode:
|
|
426
|
+
:ivar measurement_mode: Placeholder for recording whether the
|
|
353
427
|
measured DVL value was obtained through the automated
|
|
354
|
-
calculation or a manual selection
|
|
355
|
-
:type measurement_mode: Literal['manual', 'auto']
|
|
356
|
-
:ivar sigma_to_dvl_factor:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
gaussian fit to the measured DVL.
|
|
360
|
-
:type sigma_to_dvl_factor:
|
|
361
|
-
:ivar dvl_measured:
|
|
362
|
-
volume length before writing data to file.
|
|
428
|
+
calculation or a manual selection, defaults to `'auto'`.
|
|
429
|
+
:type measurement_mode: Literal['manual', 'auto'], optional
|
|
430
|
+
:ivar sigma_to_dvl_factor: The DVL is obtained by fitting a reduced
|
|
431
|
+
form of the MCA detector data. `sigma_to_dvl_factor` is a
|
|
432
|
+
scalar value that converts the standard deviation of the
|
|
433
|
+
gaussian fit to the measured DVL, defaults to `3.5`.
|
|
434
|
+
:type sigma_to_dvl_factor: Literal[3.5, 2.0, 4.0], optional
|
|
435
|
+
:ivar dvl_measured: Placeholder for the measured diffraction
|
|
436
|
+
volume length before writing the data to file.
|
|
437
|
+
:type dvl_measured: float, optional
|
|
363
438
|
"""
|
|
364
439
|
measurement_mode: Optional[Literal['manual', 'auto']] = 'auto'
|
|
365
|
-
sigma_to_dvl_factor: Optional[Literal[3.5, 2
|
|
440
|
+
sigma_to_dvl_factor: Optional[Literal[3.5, 2.0, 4.0]] = 3.5
|
|
366
441
|
dvl_measured: Optional[confloat(gt=0)] = None
|
|
367
442
|
|
|
368
443
|
def dict(self, *args, **kwargs):
|
|
369
|
-
"""
|
|
370
|
-
|
|
444
|
+
"""Return a representation of this configuration in a
|
|
445
|
+
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
|
+
|
|
449
|
+
:return: Dictionary representation of the configuration.
|
|
450
|
+
:rtype: dict
|
|
371
451
|
"""
|
|
372
452
|
d = super().dict(*args, **kwargs)
|
|
373
453
|
if self.measurement_mode == 'manual':
|
|
@@ -380,7 +460,7 @@ class DiffractionVolumeLengthConfig(MCAScanDataConfig):
|
|
|
380
460
|
volume length calculation for an EDD setup using a steel-foil
|
|
381
461
|
raster scan.
|
|
382
462
|
|
|
383
|
-
:ivar detectors:
|
|
463
|
+
:ivar detectors: Individual detector element DVL
|
|
384
464
|
measurement configurations
|
|
385
465
|
:type detectors: list[MCAElementDiffractionVolumeLengthConfig]
|
|
386
466
|
"""
|
|
@@ -392,7 +472,7 @@ class DiffractionVolumeLengthConfig(MCAScanDataConfig):
|
|
|
392
472
|
"""Return the list of values visited by the scanning motor
|
|
393
473
|
over the course of the raster scan.
|
|
394
474
|
|
|
395
|
-
:return:
|
|
475
|
+
:return: List of scanned motor values
|
|
396
476
|
:rtype: np.ndarray
|
|
397
477
|
"""
|
|
398
478
|
if self._parfile is not None:
|
|
@@ -404,69 +484,103 @@ class DiffractionVolumeLengthConfig(MCAScanDataConfig):
|
|
|
404
484
|
@property
|
|
405
485
|
def scanned_dim_lbl(self):
|
|
406
486
|
"""Return a label for plot axes corresponding to the scanned
|
|
407
|
-
dimension
|
|
487
|
+
dimension.
|
|
408
488
|
|
|
489
|
+
:return: Name of scanned motor.
|
|
409
490
|
:rtype: str
|
|
410
491
|
"""
|
|
411
492
|
if self._parfile is not None:
|
|
412
493
|
return self.scan_column
|
|
413
494
|
return self.scanparser.spec_scan_motor_mnes[0]
|
|
414
495
|
|
|
415
|
-
class CeriaConfig(MaterialConfig):
|
|
416
|
-
"""Model for a Material representing CeO2 used in calibrations.
|
|
417
496
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
|
422
509
|
"""
|
|
510
|
+
#RV Name suggests it's always Ceria, why have material_name?
|
|
423
511
|
material_name: constr(strip_whitespace=True, min_length=1) = 'CeO2'
|
|
424
|
-
sgnum: Optional[conint(ge=0)] = 225
|
|
425
512
|
lattice_parameters: confloat(gt=0) = 5.41153
|
|
513
|
+
sgnum: Optional[conint(ge=0)] = 225
|
|
426
514
|
|
|
427
515
|
|
|
428
516
|
class MCACeriaCalibrationConfig(MCAScanDataConfig):
|
|
429
517
|
"""
|
|
430
|
-
Class representing metadata required to perform a Ceria calibration
|
|
431
|
-
MCA detector.
|
|
432
|
-
|
|
433
|
-
:ivar scan_step_index:
|
|
434
|
-
|
|
435
|
-
on the average of all MCA spectra for the scan.
|
|
436
|
-
|
|
437
|
-
:ivar flux_file:
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
:ivar material:
|
|
518
|
+
Class representing metadata required to perform a Ceria calibration
|
|
519
|
+
for an MCA detector.
|
|
520
|
+
|
|
521
|
+
:ivar scan_step_index: Optional scan step index to use for the
|
|
522
|
+
calibration. If not specified, the calibration will be
|
|
523
|
+
performed on the average of all MCA spectra for the scan.
|
|
524
|
+
:type scan_step_index: int, optional
|
|
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.
|
|
441
529
|
:type material: CeriaConfig
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
configurations
|
|
530
|
+
:ivar detectors: List of individual MCA detector element
|
|
531
|
+
calibration configurations.
|
|
445
532
|
:type detectors: list[MCAElementCalibrationConfig]
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
:ivar tune_tth_tol:
|
|
450
|
-
|
|
451
|
-
|
|
533
|
+
:ivar max_iter: Maximum number of iterations of the calibration
|
|
534
|
+
routine, defaults to `10`.
|
|
535
|
+
:type detectors: int, optional
|
|
536
|
+
:ivar tune_tth_tol: Cutoff error for tuning 2&theta. Stop iterating
|
|
537
|
+
the calibration routine after an iteration produces a change in
|
|
538
|
+
the tuned value of 2&theta that is smaller than this cutoff,
|
|
539
|
+
defaults to `1e-8`.
|
|
540
|
+
:ivar tune_tth_tol: float, optional
|
|
452
541
|
"""
|
|
453
542
|
scan_step_index: Optional[conint(ge=0)]
|
|
454
|
-
|
|
455
|
-
flux_file: FilePath
|
|
456
|
-
|
|
457
543
|
material: CeriaConfig = CeriaConfig()
|
|
458
|
-
|
|
459
544
|
detectors: conlist(min_items=1, item_type=MCAElementCalibrationConfig)
|
|
460
|
-
|
|
545
|
+
flux_file: FilePath
|
|
461
546
|
max_iter: conint(gt=0) = 10
|
|
462
547
|
tune_tth_tol: confloat(ge=0) = 1e-8
|
|
463
548
|
|
|
549
|
+
@root_validator(pre=True)
|
|
550
|
+
def validate_config(cls, values):
|
|
551
|
+
"""Ensure that a valid configuration was provided and finalize
|
|
552
|
+
flux_file filepath.
|
|
553
|
+
|
|
554
|
+
:param values: Dictionary of class field values.
|
|
555
|
+
:type values: dict
|
|
556
|
+
:return: The validated list of `values`.
|
|
557
|
+
:rtype: dict
|
|
558
|
+
"""
|
|
559
|
+
inputdir = values.get('inputdir')
|
|
560
|
+
if inputdir is not None:
|
|
561
|
+
flux_file = values.get('flux_file')
|
|
562
|
+
if not os.path.isabs(flux_file):
|
|
563
|
+
values['flux_file'] = os.path.join(inputdir, flux_file)
|
|
564
|
+
|
|
565
|
+
return values
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
def flux_file_energy_range(self):
|
|
569
|
+
"""Get the energy range in the flux corection file.
|
|
570
|
+
|
|
571
|
+
:return: The energy range in the flux corection file.
|
|
572
|
+
:rtype: tuple(float, float)
|
|
573
|
+
"""
|
|
574
|
+
flux = np.loadtxt(self.flux_file)
|
|
575
|
+
energies = flux[:,0]/1.e3
|
|
576
|
+
return energies.min(), energies.max()
|
|
577
|
+
|
|
464
578
|
def mca_data(self, detector_config):
|
|
465
|
-
"""Get the
|
|
579
|
+
"""Get the array of MCA data to use for calibration.
|
|
466
580
|
|
|
467
|
-
:param detector_config:
|
|
581
|
+
:param detector_config: Detector for which data is returned.
|
|
468
582
|
:type detector_config: MCAElementConfig
|
|
469
|
-
:return: MCA data
|
|
583
|
+
:return: The current detectors's MCA data.
|
|
470
584
|
:rtype: np.ndarray
|
|
471
585
|
"""
|
|
472
586
|
if self.scan_step_index is None:
|
|
@@ -482,10 +596,10 @@ class MCACeriaCalibrationConfig(MCAScanDataConfig):
|
|
|
482
596
|
|
|
483
597
|
def flux_correction_interpolation_function(self):
|
|
484
598
|
"""
|
|
485
|
-
Get an interpolation function to correct MCA data for
|
|
486
|
-
flux of the incident beam.
|
|
599
|
+
Get an interpolation function to correct MCA data for the
|
|
600
|
+
relative energy flux of the incident beam.
|
|
487
601
|
|
|
488
|
-
:return:
|
|
602
|
+
:return: Energy flux correction interpolation function.
|
|
489
603
|
:rtype: scipy.interpolate._polyint._Interpolator1D
|
|
490
604
|
"""
|
|
491
605
|
|
|
@@ -497,32 +611,85 @@ class MCACeriaCalibrationConfig(MCAScanDataConfig):
|
|
|
497
611
|
|
|
498
612
|
|
|
499
613
|
class MCAElementStrainAnalysisConfig(MCAElementConfig):
|
|
500
|
-
"""
|
|
501
|
-
for
|
|
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
|
|
502
653
|
"""
|
|
654
|
+
max_energy_kev: Optional[confloat(gt=0)]
|
|
655
|
+
num_bins: Optional[conint(gt=0)]
|
|
503
656
|
tth_max: confloat(gt=0, allow_inf_nan=False) = 90.0
|
|
504
657
|
hkl_tth_tol: confloat(gt=0, allow_inf_nan=False) = 0.15
|
|
505
|
-
|
|
658
|
+
hkl_indices: Optional[conlist(item_type=conint(ge=0), min_items=1)] = []
|
|
506
659
|
background: Optional[str]
|
|
507
660
|
peak_models: Union[
|
|
508
661
|
conlist(item_type=Literal['gaussian', 'lorentzian'], min_items=1),
|
|
509
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)]
|
|
510
666
|
|
|
511
|
-
tth_file: Optional[FilePath]
|
|
512
667
|
tth_calibrated: Optional[confloat(gt=0, allow_inf_nan=False)]
|
|
513
668
|
slope_calibrated: Optional[confloat(allow_inf_nan=False)]
|
|
514
669
|
intercept_calibrated: Optional[confloat(allow_inf_nan=False)]
|
|
515
|
-
|
|
516
|
-
|
|
670
|
+
tth_file: Optional[FilePath]
|
|
671
|
+
tth_map: Optional[np.ndarray] = None
|
|
517
672
|
|
|
518
|
-
|
|
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
|
|
519
684
|
|
|
520
685
|
def add_calibration(self, calibration):
|
|
521
686
|
"""Finalize values for some fields using a completed
|
|
522
687
|
MCAElementCalibrationConfig that corresponds to the same
|
|
523
688
|
detector.
|
|
524
689
|
|
|
525
|
-
:param calibration:
|
|
690
|
+
:param calibration: Existing calibration configuration to use
|
|
691
|
+
by MCAElementStrainAnalysisConfig.
|
|
692
|
+
:type calibration: MCAElementCalibrationConfig
|
|
526
693
|
:return: None
|
|
527
694
|
"""
|
|
528
695
|
add_fields = ['tth_calibrated', 'slope_calibrated',
|
|
@@ -530,71 +697,91 @@ class MCAElementStrainAnalysisConfig(MCAElementConfig):
|
|
|
530
697
|
for field in add_fields:
|
|
531
698
|
setattr(self, field, getattr(calibration, field))
|
|
532
699
|
|
|
533
|
-
def
|
|
534
|
-
"""
|
|
535
|
-
|
|
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.
|
|
536
703
|
|
|
537
|
-
:
|
|
538
|
-
|
|
704
|
+
:param map_config: The map configuration with which the
|
|
705
|
+
returned map of 2&theta values will be used.
|
|
706
|
+
:type map_config: CHAP.common.models.map.MapConfig
|
|
707
|
+
:return: Map of 2&theta values.
|
|
708
|
+
:rtype: np.ndarray
|
|
539
709
|
"""
|
|
540
|
-
if
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
unique_hkls, unique_ds = get_unique_hkls_ds(materials)
|
|
544
|
-
|
|
545
|
-
# unique_hkls, unique_ds = material.unique_ds(
|
|
546
|
-
# tth_tol=self.hkl_tth_tol, tth_max=self.tth_max)
|
|
547
|
-
|
|
548
|
-
fit_hkls = np.array([unique_hkls[i] for i in self.fit_hkls])
|
|
549
|
-
fit_ds = np.array([unique_ds[i] for i in self.fit_hkls])
|
|
550
|
-
|
|
551
|
-
return fit_hkls, fit_ds
|
|
710
|
+
if getattr(self, 'tth_map', None) is not None:
|
|
711
|
+
return self.tth_map
|
|
712
|
+
return np.full(map_config.shape, self.tth_calibrated)
|
|
552
713
|
|
|
553
|
-
def
|
|
554
|
-
"""Return a
|
|
555
|
-
|
|
714
|
+
def dict(self, *args, **kwargs):
|
|
715
|
+
"""Return a representation of this configuration in a
|
|
716
|
+
dictionary that is suitable for dumping to a YAML file.
|
|
556
717
|
|
|
557
|
-
:
|
|
558
|
-
|
|
559
|
-
:type map_config: MapConfig
|
|
560
|
-
:return: map of thh values
|
|
561
|
-
:rtype: np.ndarray
|
|
718
|
+
:return: Dictionary representation of the configuration.
|
|
719
|
+
:rtype: dict
|
|
562
720
|
"""
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
721
|
+
d = super().dict(*args, **kwargs)
|
|
722
|
+
for k,v in d.items():
|
|
723
|
+
if isinstance(v, PosixPath):
|
|
724
|
+
d[k] = str(v)
|
|
725
|
+
if isinstance(v, np.ndarray):
|
|
726
|
+
d[k] = v.tolist()
|
|
727
|
+
return d
|
|
566
728
|
|
|
567
729
|
|
|
568
730
|
class StrainAnalysisConfig(BaseModel):
|
|
569
|
-
"""
|
|
731
|
+
"""Class representing input parameters required to perform a
|
|
732
|
+
strain analysis.
|
|
733
|
+
|
|
734
|
+
:ivar inputdir: Input directory, used only if any file in the
|
|
735
|
+
configuration is not an absolute path.
|
|
736
|
+
:type inputdir: str, optional
|
|
737
|
+
:ivar map_config: The map configuration for the MCA data on which
|
|
738
|
+
the strain analysis is performed.
|
|
739
|
+
:type map_config: CHAP.common.models.map.MapConfig, optional
|
|
740
|
+
:ivar par_file: Path to the par file associated with the scan.
|
|
741
|
+
:type par_file: str, optional
|
|
742
|
+
:ivar par_dims: List of independent dimensions.
|
|
743
|
+
:type par_dims: list[dict[str,str]], optional
|
|
744
|
+
:ivar other_dims: List of other column names from `par_file`.
|
|
745
|
+
:type other_dims: list[dict[str,str]], optional
|
|
746
|
+
:ivar detectors: List of individual detector element strain
|
|
747
|
+
analysis configurations
|
|
748
|
+
:type detectors: list[MCAElementStrainAnalysisConfig]
|
|
749
|
+
:ivar material_name: Sample material configurations.
|
|
750
|
+
:type material_name: list[MaterialConfig]
|
|
751
|
+
"""
|
|
570
752
|
inputdir: Optional[DirectoryPath]
|
|
571
753
|
map_config: Optional[MapConfig]
|
|
572
754
|
par_file: Optional[FilePath]
|
|
573
755
|
par_dims: Optional[list[dict[str,str]]]
|
|
574
756
|
other_dims: Optional[list[dict[str,str]]]
|
|
575
|
-
flux_file: FilePath
|
|
576
757
|
detectors: conlist(min_items=1, item_type=MCAElementStrainAnalysisConfig)
|
|
577
758
|
materials: list[MaterialConfig]
|
|
759
|
+
flux_file: FilePath
|
|
578
760
|
|
|
579
761
|
_parfile: Optional[ParFile]
|
|
580
762
|
|
|
581
763
|
@root_validator(pre=True)
|
|
582
|
-
def
|
|
583
|
-
"""Ensure
|
|
584
|
-
|
|
764
|
+
def validate_config(cls, values):
|
|
765
|
+
"""Ensure that a valid configuration was provided and finalize
|
|
766
|
+
input filepaths.
|
|
767
|
+
|
|
768
|
+
:param values: Dictionary of class field values.
|
|
769
|
+
:type values: dict
|
|
770
|
+
:raises ValueError: Missing par_dims value.
|
|
771
|
+
:return: The validated list of `values`.
|
|
772
|
+
:rtype: dict
|
|
585
773
|
"""
|
|
586
774
|
inputdir = values.get('inputdir')
|
|
587
775
|
flux_file = values.get('flux_file')
|
|
588
776
|
par_file = values.get('par_file')
|
|
589
|
-
if flux_file:
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if not os.path.isabs(par_file):
|
|
777
|
+
if inputdir is not None and not os.path.isabs(flux_file):
|
|
778
|
+
values['flux_file'] = os.path.join(inputdir, flux_file)
|
|
779
|
+
if par_file is not None:
|
|
780
|
+
if inputdir is not None and not os.path.isabs(par_file):
|
|
594
781
|
values['par_file'] = os.path.join(inputdir, par_file)
|
|
595
782
|
if 'par_dims' not in values:
|
|
596
783
|
raise ValueError(
|
|
597
|
-
'
|
|
784
|
+
'par_dims is required when using par_file')
|
|
598
785
|
values['_parfile'] = ParFile(values['par_file'])
|
|
599
786
|
values['map_config'] = values['_parfile'].get_map(
|
|
600
787
|
'EDD', 'id1a3', values['par_dims'],
|
|
@@ -603,7 +790,7 @@ class StrainAnalysisConfig(BaseModel):
|
|
|
603
790
|
if isinstance(map_config, dict):
|
|
604
791
|
for i, scans in enumerate(map_config.get('spec_scans')):
|
|
605
792
|
spec_file = scans.get('spec_file')
|
|
606
|
-
if not os.path.isabs(spec_file):
|
|
793
|
+
if inputdir is not None and not os.path.isabs(spec_file):
|
|
607
794
|
values['map_config']['spec_scans'][i]['spec_file'] = \
|
|
608
795
|
os.path.join(inputdir, spec_file)
|
|
609
796
|
return values
|
|
@@ -630,7 +817,7 @@ class StrainAnalysisConfig(BaseModel):
|
|
|
630
817
|
+ 'StrainAnalysisConfig that uses par_file.')
|
|
631
818
|
else:
|
|
632
819
|
try:
|
|
633
|
-
detector.
|
|
820
|
+
detector.tth_map = ParFile(values['par_file']).map_values(
|
|
634
821
|
values['map_config'], np.loadtxt(detector.tth_file))
|
|
635
822
|
except Exception as e:
|
|
636
823
|
raise ValueError(
|
|
@@ -638,11 +825,46 @@ class StrainAnalysisConfig(BaseModel):
|
|
|
638
825
|
+ f'{detector.tth_file}') from e
|
|
639
826
|
return detector
|
|
640
827
|
|
|
828
|
+
def mca_data(self, detector=None, map_index=None):
|
|
829
|
+
"""Get MCA data for a single or multiple detector elements.
|
|
830
|
+
|
|
831
|
+
:param detector: Detector(s) for which data is returned,
|
|
832
|
+
defaults to `None`, which return MCA data for all
|
|
833
|
+
detector elements.
|
|
834
|
+
:type detector: Union[int, MCAElementStrainAnalysisConfig],
|
|
835
|
+
optional
|
|
836
|
+
:param map_index: Index of a single point in the map, defaults
|
|
837
|
+
to `None`, which returns MCA data for each point in the map.
|
|
838
|
+
:type map_index: tuple, optional
|
|
839
|
+
:return: A single MCA spectrum.
|
|
840
|
+
:rtype: np.ndarray
|
|
841
|
+
"""
|
|
842
|
+
if detector is None:
|
|
843
|
+
mca_data = []
|
|
844
|
+
for detector_config in self.detectors:
|
|
845
|
+
mca_data.append(self.mca_data(detector_config, map_index))
|
|
846
|
+
return np.asarray(mca_data)
|
|
847
|
+
else:
|
|
848
|
+
if isinstance(detector, int):
|
|
849
|
+
detector_config = self.detectors[detector]
|
|
850
|
+
else:
|
|
851
|
+
if not isinstance(detector, MCAElementStrainAnalysisConfig):
|
|
852
|
+
raise ValueError('Invalid parameter detector ({detector})')
|
|
853
|
+
detector_config = detector
|
|
854
|
+
if map_index is None:
|
|
855
|
+
mca_data = []
|
|
856
|
+
for map_index in np.ndindex(self.map_config.shape):
|
|
857
|
+
mca_data.append(self.mca_data(detector_config, map_index))
|
|
858
|
+
return np.asarray(mca_data)
|
|
859
|
+
else:
|
|
860
|
+
return self.map_config.get_detector_data(
|
|
861
|
+
detector_config.detector_name, map_index)
|
|
862
|
+
|
|
641
863
|
def dict(self, *args, **kwargs):
|
|
642
864
|
"""Return a representation of this configuration in a
|
|
643
865
|
dictionary that is suitable for dumping to a YAML file.
|
|
644
866
|
|
|
645
|
-
:return:
|
|
867
|
+
:return: Dictionary representation of the configuration.
|
|
646
868
|
:rtype: dict
|
|
647
869
|
"""
|
|
648
870
|
d = super().dict(*args, **kwargs)
|
|
@@ -652,29 +874,3 @@ class StrainAnalysisConfig(BaseModel):
|
|
|
652
874
|
if '_scanparser' in d:
|
|
653
875
|
del d['_scanparser']
|
|
654
876
|
return d
|
|
655
|
-
|
|
656
|
-
def mca_data(self, detector_config, map_index):
|
|
657
|
-
"""Get MCA data for a single detector element.
|
|
658
|
-
|
|
659
|
-
:param detector_config: the detector to get data for
|
|
660
|
-
:type detector_config: MCAElementStrainAnalysisConfig
|
|
661
|
-
:param map_index: index of a single point in the map
|
|
662
|
-
:type map_index: tuple
|
|
663
|
-
:return: one spectrum of MCA data
|
|
664
|
-
:rtype: np.ndarray
|
|
665
|
-
"""
|
|
666
|
-
map_coords = {dim: self.map_config.coords[dim][i]
|
|
667
|
-
for dim,i in zip(self.map_config.dims, map_index)}
|
|
668
|
-
for scans in self.map_config.spec_scans:
|
|
669
|
-
for scan_number in scans.scan_numbers:
|
|
670
|
-
scanparser = scans.get_scanparser(scan_number)
|
|
671
|
-
for scan_step_index in range(scanparser.spec_scan_npts):
|
|
672
|
-
_coords = {
|
|
673
|
-
dim.label:dim.get_value(
|
|
674
|
-
scans, scan_number, scan_step_index)
|
|
675
|
-
for dim in self.map_config.independent_dimensions}
|
|
676
|
-
if _coords == map_coords:
|
|
677
|
-
break
|
|
678
|
-
scanparser = scans.get_scanparser(scan_number)
|
|
679
|
-
return scanparser.get_detector_data(detector_config.detector_name,
|
|
680
|
-
scan_step_index)
|