ChessAnalysisPipeline 0.0.17.dev3__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.
Files changed (70) hide show
  1. CHAP/TaskManager.py +216 -0
  2. CHAP/__init__.py +27 -0
  3. CHAP/common/__init__.py +57 -0
  4. CHAP/common/models/__init__.py +8 -0
  5. CHAP/common/models/common.py +124 -0
  6. CHAP/common/models/integration.py +659 -0
  7. CHAP/common/models/map.py +1291 -0
  8. CHAP/common/processor.py +2869 -0
  9. CHAP/common/reader.py +658 -0
  10. CHAP/common/utils.py +110 -0
  11. CHAP/common/writer.py +730 -0
  12. CHAP/edd/__init__.py +23 -0
  13. CHAP/edd/models.py +876 -0
  14. CHAP/edd/processor.py +3069 -0
  15. CHAP/edd/reader.py +1023 -0
  16. CHAP/edd/select_material_params_gui.py +348 -0
  17. CHAP/edd/utils.py +1572 -0
  18. CHAP/edd/writer.py +26 -0
  19. CHAP/foxden/__init__.py +19 -0
  20. CHAP/foxden/models.py +71 -0
  21. CHAP/foxden/processor.py +124 -0
  22. CHAP/foxden/reader.py +224 -0
  23. CHAP/foxden/utils.py +80 -0
  24. CHAP/foxden/writer.py +168 -0
  25. CHAP/giwaxs/__init__.py +11 -0
  26. CHAP/giwaxs/models.py +491 -0
  27. CHAP/giwaxs/processor.py +776 -0
  28. CHAP/giwaxs/reader.py +8 -0
  29. CHAP/giwaxs/writer.py +8 -0
  30. CHAP/inference/__init__.py +7 -0
  31. CHAP/inference/processor.py +69 -0
  32. CHAP/inference/reader.py +8 -0
  33. CHAP/inference/writer.py +8 -0
  34. CHAP/models.py +227 -0
  35. CHAP/pipeline.py +479 -0
  36. CHAP/processor.py +125 -0
  37. CHAP/reader.py +124 -0
  38. CHAP/runner.py +277 -0
  39. CHAP/saxswaxs/__init__.py +7 -0
  40. CHAP/saxswaxs/processor.py +8 -0
  41. CHAP/saxswaxs/reader.py +8 -0
  42. CHAP/saxswaxs/writer.py +8 -0
  43. CHAP/server.py +125 -0
  44. CHAP/sin2psi/__init__.py +7 -0
  45. CHAP/sin2psi/processor.py +8 -0
  46. CHAP/sin2psi/reader.py +8 -0
  47. CHAP/sin2psi/writer.py +8 -0
  48. CHAP/tomo/__init__.py +15 -0
  49. CHAP/tomo/models.py +210 -0
  50. CHAP/tomo/processor.py +3862 -0
  51. CHAP/tomo/reader.py +9 -0
  52. CHAP/tomo/writer.py +59 -0
  53. CHAP/utils/__init__.py +6 -0
  54. CHAP/utils/converters.py +188 -0
  55. CHAP/utils/fit.py +2947 -0
  56. CHAP/utils/general.py +2655 -0
  57. CHAP/utils/material.py +274 -0
  58. CHAP/utils/models.py +595 -0
  59. CHAP/utils/parfile.py +224 -0
  60. CHAP/writer.py +122 -0
  61. MLaaS/__init__.py +0 -0
  62. MLaaS/ktrain.py +205 -0
  63. MLaaS/mnist_img.py +83 -0
  64. MLaaS/tfaas_client.py +371 -0
  65. chessanalysispipeline-0.0.17.dev3.dist-info/LICENSE +60 -0
  66. chessanalysispipeline-0.0.17.dev3.dist-info/METADATA +29 -0
  67. chessanalysispipeline-0.0.17.dev3.dist-info/RECORD +70 -0
  68. chessanalysispipeline-0.0.17.dev3.dist-info/WHEEL +5 -0
  69. chessanalysispipeline-0.0.17.dev3.dist-info/entry_points.txt +2 -0
  70. chessanalysispipeline-0.0.17.dev3.dist-info/top_level.txt +2 -0
CHAP/edd/models.py ADDED
@@ -0,0 +1,876 @@
1
+ """EDD Pydantic model classes."""
2
+
3
+ # System modules
4
+ from copy import deepcopy
5
+ import os
6
+ from typing import (
7
+ Dict,
8
+ Literal,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ # Third party modules
14
+ import numpy as np
15
+ from hexrd.material import Material
16
+ #from CHAP.utils.material import Material
17
+ from pydantic import (
18
+ Field,
19
+ FilePath,
20
+ PrivateAttr,
21
+ confloat,
22
+ conint,
23
+ conlist,
24
+ constr,
25
+ field_validator,
26
+ model_validator,
27
+ )
28
+ from scipy.interpolate import interp1d
29
+ from typing_extensions import Annotated
30
+
31
+ # Local modules
32
+ from CHAP.models import CHAPBaseModel
33
+ from CHAP.common.models.map import Detector
34
+ from CHAP.utils.models import Multipeak
35
+ #from CHAP.utils.parfile import ParFile
36
+
37
+
38
+ # Baseline configuration class
39
+
40
+ class BaselineConfig(CHAPBaseModel):
41
+ """Baseline model configuration class.
42
+
43
+ :ivar lam: The &lambda (smoothness) parameter (the balance
44
+ between the residual of the data and the baseline and the
45
+ smoothness of the baseline). The suggested range is between
46
+ 100 and 10^8, defaults to `10^6`.
47
+ :type lam: float, optional
48
+ :ivar max_iter: The maximum number of iterations,
49
+ defaults to `100`.
50
+ :type max_iter: int, optional
51
+ :ivar tol: The convergence tolerence, defaults to `1.e-6`.
52
+ :type tol: float, optional
53
+ """
54
+ attrs: Optional[dict] = {}
55
+ lam: confloat(gt=0, allow_inf_nan=False) = 1.e6
56
+ max_iter: conint(gt=0) = 100
57
+ tol: confloat(gt=0, allow_inf_nan=False) = 1.e-6
58
+
59
+
60
+ # Fit configuration class
61
+
62
+ class FitConfig(CHAPBaseModel):
63
+ """Fit parameters configuration class for peak fitting.
64
+
65
+ :ivar background: Background model for peak fitting, defaults
66
+ to `constant`.
67
+ :type background: str, list[str], optional
68
+ :ivar baseline: Automated baseline subtraction configuration,
69
+ defaults to `False`.
70
+ :type baseline: Union(bool, BaselineConfig), optional
71
+ :ivar centers_range: Peak centers range for peak fitting.
72
+ The allowed range for the peak centers will be the initial
73
+ values ± `centers_range` (in MCA channels for calibration
74
+ or keV for strain analysis). Defaults to `20` for calibration
75
+ and `2.0` for strain analysis.
76
+ :type centers_range: float, optional
77
+ :ivar energy_mask_ranges: List of MCA energy mask ranges in keV
78
+ for selecting the data to be included after applying a mask
79
+ (bounds are inclusive). Specify either energy_mask_ranges or
80
+ mask_ranges, not both.
81
+ :type energy_mask_ranges: list[[float, float]], optional
82
+ :ivar fwhm_min: Minimum FWHM for peak fitting (in MCA channels
83
+ for calibration or keV for strain analysis). Defaults to `3`
84
+ for calibration and `0.25` for strain analysis.
85
+ :type fwhm_min: float, optional
86
+ :ivar fwhm_max: Maximum FWHM for peak fitting (in MCA channels
87
+ for calibration or keV for strain analysis). Defaults to `25`
88
+ for calibration and `2.0` for strain analysis.
89
+ :type fwhm_max: float, optional
90
+ :ivar mask_ranges: List of MCA channel bin ranges for selecting
91
+ the data to be included in the energy calibration after
92
+ applying a mask (bounds are inclusive). Specify for
93
+ energy calibration only.
94
+ :type mask_ranges: list[[int, int]], optional
95
+ :ivar backgroundpeaks: Additional background peaks (their
96
+ associated fit parameters in units of keV).
97
+ :type backgroundpeaks: CHAP.utils.models.Multipeak, optional
98
+ """
99
+ background: Optional[conlist(item_type=constr(
100
+ strict=True, strip_whitespace=True, to_lower=True))] = ['constant']
101
+ baseline: Optional[Union[bool, BaselineConfig]] = None
102
+ centers_range: Optional[confloat(gt=0, allow_inf_nan=False)] = 20
103
+ energy_mask_ranges: Optional[conlist(
104
+ min_length=1,
105
+ item_type=conlist(
106
+ min_length=2,
107
+ max_length=2,
108
+ item_type=confloat(allow_inf_nan=False)))] = None
109
+ fwhm_min: Optional[confloat(gt=0, allow_inf_nan=False)] = 3
110
+ fwhm_max: Optional[confloat(gt=0, allow_inf_nan=False)] = 25
111
+ mask_ranges: Optional[conlist(
112
+ min_length=1,
113
+ item_type=conlist(
114
+ min_length=2,
115
+ max_length=2,
116
+ item_type=conint(ge=0)))] = None
117
+ backgroundpeaks: Optional[Multipeak] = None
118
+
119
+ @field_validator('background', mode='before')
120
+ @classmethod
121
+ def validate_background(cls, background):
122
+ """Validate the background model.
123
+
124
+ :ivar background: Background model for peak fitting.
125
+ :type background: str, list[str], optional
126
+ :return: List of validated background models.
127
+ :rtype: list[str]
128
+ """
129
+ if background is None:
130
+ return background
131
+ if isinstance(background, str):
132
+ return [background]
133
+ return sorted(background)
134
+
135
+ @field_validator('baseline', mode='before')
136
+ @classmethod
137
+ def validate_baseline(cls, baseline):
138
+ """Validate the baseline configuration.
139
+
140
+ :ivar baseline: Automated baseline subtraction configuration.
141
+ :type baseline: Union(bool, BaselineConfig), optional
142
+ :return: Validated baseline subtraction configuration.
143
+ :rtype: bool, BaselineConfig
144
+ """
145
+ if isinstance(baseline, bool) and baseline:
146
+ return BaselineConfig()
147
+ return baseline
148
+
149
+ @field_validator('energy_mask_ranges', mode='before')
150
+ @classmethod
151
+ def validate_energy_mask_ranges(cls, energy_mask_ranges):
152
+ """Validate the mask ranges for selecting the data to include.
153
+
154
+ :ivar energy_mask_ranges: List of MCA energy mask ranges in keV
155
+ for selecting the data to be included after applying a mask
156
+ (bounds are inclusive).
157
+ :type energy_mask_ranges: list[[float, float]], optional
158
+ :return: Validated energy mask ranges.
159
+ :rtype: list[[float, float]]
160
+ """
161
+ if energy_mask_ranges:
162
+ return sorted([sorted(v) for v in energy_mask_ranges])
163
+ return energy_mask_ranges
164
+
165
+ @field_validator('mask_ranges', mode='before')
166
+ @classmethod
167
+ def validate_mask_ranges(cls, mask_ranges):
168
+ """Validate the mask ranges for selecting the data to include.
169
+
170
+ :ivar mask_ranges: List of MCA channel bin ranges for selecting
171
+ the data to be included after applying a mask
172
+ (bounds are inclusive).
173
+ :type mask_ranges: list[[int, int]], optional
174
+ :return: Validated mask ranges.
175
+ :rtype: list[[int, int]]
176
+ """
177
+ if mask_ranges:
178
+ return sorted([sorted(v) for v in mask_ranges])
179
+ return mask_ranges
180
+
181
+
182
+ # Material configuration class
183
+
184
+ class MaterialConfig(CHAPBaseModel):
185
+ """Sample material parameters configuration class.
186
+
187
+ :ivar material_name: Sample material name.
188
+ :type material_name: str, optional
189
+ :ivar lattice_parameters: Lattice spacing(s) in angstroms.
190
+ :type lattice_parameters: float, list[float], optional
191
+ :ivar sgnum: Space group of the material.
192
+ :type sgnum: int, optional
193
+ """
194
+ #RV FIX create a getter for lattice_parameters that always returns a list?
195
+ material_name: Optional[constr(strip_whitespace=True, min_length=1)] = None
196
+ lattice_parameters: Optional[Union[
197
+ confloat(gt=0, allow_inf_nan=False),
198
+ conlist(
199
+ min_length=1, max_length=6,
200
+ item_type=confloat(gt=0, allow_inf_nan=False))]] = None
201
+ sgnum: Optional[conint(ge=0)] = None
202
+
203
+ _material: Optional[Material]
204
+
205
+ @model_validator(mode='after')
206
+ def validate_materialconfig_after(self):
207
+ """Create and validate the private attribute _material.
208
+
209
+ :return: The validated list of class properties.
210
+ :rtype: MaterialConfig
211
+ """
212
+ # Local modules
213
+ from CHAP.edd.utils import make_material
214
+ # from CHAP.utils.material import Material
215
+
216
+ self._material = make_material(
217
+ self.material_name, self.sgnum, self.lattice_parameters)
218
+ # self._material = Material.make_material(
219
+ # self.material_name, sgnum=self.sgnum,
220
+ # lattice_parameters_angstroms=self.lattice_parameters,
221
+ # pos=['4a', '8c'])
222
+ #pos=[(0,0,0), (1/4, 1/4, 1/4), (3/4, 3/4, 3/4)])
223
+ self.lattice_parameters = list([
224
+ x.getVal('angstrom') if x.isLength() else x.getVal('radians')
225
+ for x in self._material._lparms])
226
+ return self
227
+
228
+
229
+ # Detector configuration classes
230
+
231
+ class MCADetectorCalibration(Detector, FitConfig):
232
+ """Class representing metadata required to configure a single MCA
233
+ detector element to perform detector calibration.
234
+
235
+ :ivar energy_calibration_coeffs: Detector channel index to energy
236
+ polynomial conversion coefficients ([a, b, c] with
237
+ E_i = a*i^2 + b*i + c).
238
+ :type energy_calibration_coeffs:
239
+ list[float, float, float], optional
240
+ :ivar num_bins: Number of MCA channels.
241
+ :type num_bins: int, optional
242
+ :ivar tth_max: Detector rotation about lab frame x axis.
243
+ :type tth_max: float, optional
244
+ :ivar tth_tol: Minimum resolvable difference in 2&theta between
245
+ two unique Bragg peaks,
246
+ :type tth_tol: float, optional
247
+ :ivar tth_calibrated: Calibrated value for 2&theta.
248
+ :type tth_calibrated: float, optional
249
+ :ivar tth_initial_guess: Initial guess for 2&theta superseding
250
+ the global one in MCATthCalibrationConfig.
251
+ :type tth_initial_guess: float, optional
252
+ """
253
+ processor_type: Literal['calibration']
254
+ energy_calibration_coeffs: Optional[conlist(
255
+ min_length=3, max_length=3,
256
+ item_type=confloat(allow_inf_nan=False))] = None
257
+ num_bins: Optional[conint(gt=0)] = None
258
+ tth_max: Optional[confloat(gt=0, allow_inf_nan=False)] = None
259
+ tth_tol: Optional[confloat(gt=0, allow_inf_nan=False)] = None
260
+ tth_calibrated: Optional[confloat(gt=0, allow_inf_nan=False)] = None
261
+ tth_initial_guess: Optional[confloat(gt=0, allow_inf_nan=False)] = None
262
+
263
+ _energy_calibration_mask_ranges: conlist(
264
+ min_length=1,
265
+ item_type=conlist(
266
+ min_length=2,
267
+ max_length=2,
268
+ item_type=conint(ge=0))) = PrivateAttr()
269
+ _hkl_indices: list = PrivateAttr()
270
+
271
+ # def add_calibration(self, calibration):
272
+ # """Finalize values for some fields using a calibration
273
+ # MCADetectorCalibration corresponding to the same detector.
274
+ #
275
+ # :param calibration: Existing calibration configuration.
276
+ # :type calibration: MCADetectorCalibration
277
+ # """
278
+ # raise RuntimeError('To do')
279
+ # for field in ['energy_calibration_coeffs', 'num_bins',
280
+ # '_energy_calibration_mask_ranges']:
281
+ # setattr(self, field, deepcopy(getattr(calibration, field)))
282
+ # if self.tth_calibrated is not None:
283
+ # self.logger.warning(
284
+ # 'Ignoring tth_calibrated in calibration configuration')
285
+ # self.tth_calibrated = None
286
+
287
+ @property
288
+ def energies(self):
289
+ """Return calibrated bin energies."""
290
+ a, b, c = tuple(self.energy_calibration_coeffs)
291
+ channel_bins = np.arange(self.num_bins)
292
+ return (a*channel_bins + b)*channel_bins + c
293
+
294
+ @property
295
+ def hkl_indices(self):
296
+ """Return the hkl_indices consistent with the selected energy
297
+ ranges (include_energy_ranges).
298
+ """
299
+ if hasattr(self, '_hkl_indices'):
300
+ return self._hkl_indices
301
+ return []
302
+
303
+ @hkl_indices.setter
304
+ def hkl_indices(self, value):
305
+ """Set the private attribute `hkl_indices`."""
306
+ self._hkl_indices = value
307
+
308
+ def convert_mask_ranges(self, mask_ranges):
309
+ """Given a list of mask ranges in channel bins, set the
310
+ corresponding list of channel energy mask ranges.
311
+
312
+ :param mask_ranges: A list of mask ranges to
313
+ convert to energy mask ranges.
314
+ :type mask_ranges: list[[int,int]]
315
+ """
316
+ energies = self.energies
317
+ self.energy_mask_ranges = [
318
+ [float(energies[i]) for i in range_]
319
+ for range_ in sorted([sorted(v) for v in mask_ranges])]
320
+
321
+ def get_mask_ranges(self):
322
+ """Return the value of `mask_ranges` if set or convert the
323
+ `energy_mask_ranges` from channel energies to channel indices.
324
+ """
325
+ if self.mask_ranges:
326
+ return self.mask_ranges
327
+ if self.energy_mask_ranges is None:
328
+ return None
329
+
330
+ # Local modules
331
+ from CHAP.utils.general import (
332
+ index_nearest_down,
333
+ index_nearest_up,
334
+ )
335
+
336
+ mask_ranges = []
337
+ energies = self.energies
338
+ for e_min, e_max in self.energy_mask_ranges:
339
+ mask_ranges.append(
340
+ [index_nearest_down(energies, e_min),
341
+ index_nearest_up(energies, e_max)])
342
+ return mask_ranges
343
+
344
+ def mca_mask(self):
345
+ """Get a boolean mask array to use on this MCA element's data.
346
+ Note that the bounds of the mask ranges are inclusive.
347
+
348
+ :return: Boolean mask array.
349
+ :rtype: numpy.ndarray
350
+ """
351
+ mask = np.asarray([False] * self.num_bins)
352
+ mask_ranges = self.get_mask_ranges()
353
+ channel_bins = np.arange(self.num_bins, dtype=np.int32)
354
+ for (min_, max_) in mask_ranges:
355
+ mask = np.logical_or(
356
+ mask,
357
+ np.logical_and(channel_bins >= min_, channel_bins <= max_))
358
+ return mask
359
+
360
+ def set_energy_calibration_mask_ranges(self):
361
+ self._energy_calibration_mask_ranges = deepcopy(self.mask_ranges)
362
+
363
+
364
+ class MCADetectorDiffractionVolumeLength(MCADetectorCalibration):
365
+ """Class representing metadata required to perform a diffraction
366
+ volume length measurement for a single MCA detector element.
367
+
368
+ :ivar dvl: Measured diffraction volume length.
369
+ :type dvl: float, optional
370
+ :ivar fit_amplitude: Amplitude of the Gaussian fit.
371
+ :type fit_amplitude: float, optional
372
+ :ivar fit_center: Center of the Gaussian fit.
373
+ :type fit_center: float, optional
374
+ :ivar fit_sigma: Sigma of the Gaussian fit.
375
+ :type fit_sigma: float, optional
376
+ """
377
+ processor_type: Literal['diffractionvolumelength']
378
+ dvl: Optional[confloat(gt=0, allow_inf_nan=False)] = None
379
+ fit_amplitude: Optional[float] = None
380
+ fit_center: Optional[float] = None
381
+ fit_sigma: Optional[float] = None
382
+
383
+
384
+ class MCADetectorStrainAnalysis(MCADetectorCalibration):
385
+ """Class representing metadata required to perform a strain
386
+ analysis.
387
+
388
+ :ivar centers_range: Peak centers range for peak fitting.
389
+ The allowed range for the peak centers will be the initial
390
+ values &pm; `centers_range` (in keV), defaults to `2.0`.
391
+ :type centers_range: float, optional
392
+ :ivar fwhm_min: Minimum FWHM for peak fitting (in keV),
393
+ defaults to `0.25`.
394
+ :type fwhm_min: float, optional
395
+ :ivar fwhm_max: Maximum FWHM for peak fitting (in keV),
396
+ defaults to `2.0`.
397
+ :ivar peak_models: Peak model for peak fitting,
398
+ defaults to `'gaussian'`.
399
+ :type peak_models: Literal['gaussian', 'lorentzian']],
400
+ list[Literal['gaussian', 'lorentzian']]], optional
401
+ :ivar rel_height_cutoff: Relative peak height cutoff for
402
+ peak fitting (any peak with a height smaller than
403
+ `rel_height_cutoff` times the maximum height of all peaks
404
+ gets removed from the fit model), defaults to `None`.
405
+ :type rel_height_cutoff: float, optional
406
+ :ivar tth_file: Path to the file with the 2&theta map.
407
+ :type tth_file: FilePath, optional
408
+ :ivar tth_map: Map of the 2&theta values.
409
+ :type tth_map: numpy.ndarray, optional
410
+ """
411
+ centers_range: Optional[confloat(gt=0, allow_inf_nan=False)] = 2
412
+ fwhm_min: Optional[confloat(gt=0, allow_inf_nan=False)] = 0.25
413
+ fwhm_max: Optional[confloat(gt=0, allow_inf_nan=False)] = 2.0
414
+ processor_type: Literal['strainanalysis']
415
+ peak_models: Union[
416
+ conlist(min_length=1, item_type=Literal['gaussian', 'lorentzian']),
417
+ Literal['gaussian', 'lorentzian']] = 'gaussian'
418
+ rel_height_cutoff: Optional[
419
+ confloat(gt=0, lt=1.0, allow_inf_nan=False)] = None
420
+ # tth_file: Optional[FilePath] = None
421
+ # tth_map: Optional[np.ndarray] = None
422
+
423
+ _calibration_energy_mask_ranges: conlist(
424
+ min_length=1,
425
+ item_type=conlist(
426
+ min_length=2,
427
+ max_length=2,
428
+ item_type=confloat(allow_inf_nan=False))) = PrivateAttr()
429
+
430
+ def add_calibration(self, calibration):
431
+ """Finalize values for some fields using a tth calibration
432
+ MCADetectorStrainAnalysis corresponding to the same detector.
433
+
434
+ :param calibration: Existing calibration configuration to use
435
+ by MCAElementStrainAnalysisConfig.
436
+ :type calibration: MCADetectorStrainAnalysis
437
+ """
438
+ for field in ['energy_calibration_coeffs', 'num_bins',
439
+ 'tth_calibrated']:
440
+ setattr(self, field, deepcopy(getattr(calibration, field)))
441
+ if self.energy_mask_ranges is None:
442
+ self.energy_mask_ranges = deepcopy(calibration.energy_mask_ranges)
443
+ self._calibration_energy_mask_ranges = deepcopy(
444
+ calibration.energy_mask_ranges)
445
+
446
+ def get_calibration_mask_ranges(self):
447
+ """Return the `_calibration_energy_mask_ranges` converted from
448
+ channel energies to channel indices.
449
+ """
450
+ if not hasattr(self, '_calibration_energy_mask_ranges'):
451
+ return None
452
+
453
+ # Local modules
454
+ from CHAP.utils.general import (
455
+ index_nearest_down,
456
+ index_nearest_up,
457
+ )
458
+
459
+ energy_mask_ranges = []
460
+ energies = self.energies
461
+ for e_min, e_max in self._calibration_energy_mask_ranges:
462
+ energy_mask_ranges.append(
463
+ [index_nearest_down(energies, e_min),
464
+ index_nearest_up(energies, e_max)])
465
+ return energy_mask_ranges
466
+
467
+ def get_tth_map(self, map_shape):
468
+ """Return the map of 2&theta values to use -- may vary at each
469
+ point in the map.
470
+
471
+ :param map_shape: The shape of the suplied 2&theta map.
472
+ :return: Map of 2&theta values.
473
+ :rtype: numpy.ndarray
474
+ """
475
+ if getattr(self, 'tth_map', None) is not None:
476
+ if self.tth_map.shape != map_shape:
477
+ raise ValueError(
478
+ 'Invalid "tth_map" field shape '
479
+ f'{self.tth_map.shape} (expected {map_shape})')
480
+ return self.tth_map
481
+ return np.full(map_shape, self.tth_calibrated)
482
+
483
+
484
+ MCADetector = Annotated[
485
+ Union[
486
+ MCADetectorCalibration,
487
+ MCADetectorDiffractionVolumeLength,
488
+ MCADetectorStrainAnalysis],
489
+ Field(discriminator='processor_type')
490
+ ]
491
+
492
+
493
+ class MCADetectorConfig(FitConfig):
494
+ """Class representing metadata required to configure a full MCA
495
+ detector.
496
+
497
+ :ivar detectors: List of individual MCA detector elements.
498
+ :type detectors: list[MCADetector], optional
499
+ """
500
+ processor_type: Literal[
501
+ 'calibration', 'diffractionvolumelength', 'strainanalysis']
502
+ detectors: Optional[conlist(min_length=1, item_type=MCADetector)] = []
503
+
504
+ _exclude = set(vars(FitConfig()).keys())
505
+
506
+ @model_validator(mode='before')
507
+ @classmethod
508
+ def validate_mcadetectorconfig_before(cls, data):
509
+ if isinstance(data, dict):
510
+ processor_type = data.get('processor_type').lower()
511
+ if 'detectors' in data:
512
+ detectors = data.pop('detectors')
513
+ for d in detectors:
514
+ d['processor_type'] = processor_type
515
+ attrs = d.pop('attrs', {})
516
+ if 'default_fields' in attrs:
517
+ attrs.pop('default_fields')
518
+ if attrs:
519
+ d['attrs'] = attrs
520
+ data['detectors'] = detectors
521
+ return data
522
+
523
+ @model_validator(mode='after')
524
+ def validate_mcadetectorconfig_after(self):
525
+ if self.detectors:
526
+ self.update_detectors()
527
+ return self
528
+
529
+ def update_detectors(self):
530
+ """Update individual detector parameters with any non-default
531
+ values from the global detector configuration.
532
+ """
533
+ for k, v in self:
534
+ if k in self.model_fields_set:
535
+ for d in self.detectors:
536
+ if hasattr(d, k):
537
+ setattr(d, k, deepcopy(v))
538
+
539
+
540
+ # Processor configuration classes
541
+
542
+ class DiffractionVolumeLengthConfig(FitConfig):
543
+ """Class representing metadata required to perform a diffraction
544
+ volume length calculation for an EDD setup using a steel-foil
545
+ raster scan.
546
+
547
+ :ivar max_energy_kev: Maximum channel energy of the MCA in
548
+ keV, defaults to `200.0`.
549
+ :type max_energy_kev: float, optional
550
+ :ivar measurement_mode: Placeholder for recording whether the
551
+ measured DVL value was obtained through the automated
552
+ calculation or a manual selection, defaults to `'auto'`.
553
+ :type measurement_mode: Literal['manual', 'auto'], optional
554
+ :ivar sample_thickness: Thickness of scanned foil sample. Quantity
555
+ must be provided in the same units as the values of the
556
+ scanning motor.
557
+ :type sample_thickness: float
558
+ :ivar sigma_to_dvl_factor: The DVL is obtained by fitting a reduced
559
+ form of the MCA detector data. `sigma_to_dvl_factor` is a
560
+ scalar value that converts the standard deviation of the
561
+ gaussian fit to the measured DVL, defaults to `3.5`.
562
+ :type sigma_to_dvl_factor: Literal[2.0, 3.5, 4.0], optional
563
+ """
564
+ max_energy_kev: Optional[confloat(gt=0, allow_inf_nan=False)] = 200.0
565
+ measurement_mode: Optional[Literal['manual', 'auto']] = 'auto'
566
+ sample_thickness: Optional[confloat(gt=0, allow_inf_nan=False)] = None
567
+ sigma_to_dvl_factor: Optional[Literal[2.0, 3.5, 4.0]] = 3.5
568
+
569
+ _exclude = set(vars(FitConfig()).keys())
570
+
571
+ @model_validator(mode='after')
572
+ def validate_diffractionvolumelengthconfig_after(self):
573
+ """Update the configuration with costum defaults after the
574
+ normal native pydantic validation.
575
+
576
+ :return: Updated energy calibration configuration class.
577
+ :rtype: DiffractionVolumeLengthConfig
578
+ """
579
+ if self.measurement_mode == 'manual':
580
+ self._exclude |= {'sigma_to_dvl_factor'}
581
+ return self
582
+
583
+
584
+ class MCACalibrationConfig(CHAPBaseModel):
585
+ """Base class representing metadata required to perform an energy
586
+ or 2&theta calibration of an MCA detector.
587
+
588
+ :ivar flux_file: File name of the csv flux file containing station
589
+ beam energy in eV (column 0) versus flux (column 1).
590
+ :type flux_file: str, optional
591
+ :ivar materials: Material configurations for the calibration,
592
+ defaults to [`Ceria`].
593
+ :type materials: list[MaterialConfig], optional
594
+ :ivar peak_energies: Theoretical locations of the fluorescence
595
+ peaks in keV to use for calibrating the MCA channel energies.
596
+ :type peak_energies: list[float], optional for energy calibration
597
+ :ivar scan_step_indices: Optional scan step indices to use for the
598
+ calibration. If not specified, the calibration will be
599
+ performed on the average of all MCA spectra for the scan.
600
+ :type scan_step_indices: int, str, list[int], optional
601
+
602
+ Note: Fluorescence data:
603
+ https://physics.nist.gov/PhysRefData/XrayTrans/Html/search.html
604
+ """
605
+ flux_file: Optional[FilePath] = None
606
+ materials: Optional[conlist(item_type=MaterialConfig)] = [MaterialConfig(
607
+ material_name='CeO2', lattice_parameters=5.41153, sgnum=225)]
608
+ peak_energies: Optional[conlist(
609
+ min_length=2, item_type=confloat(gt=0, allow_inf_nan=False))] = [
610
+ 34.279, 34.720, 39.258, 40.233]
611
+ scan_step_indices: Optional[Annotated[conlist(
612
+ min_length=1, item_type=conint(ge=0)),
613
+ Field(validate_default=True)]] = None
614
+
615
+ @model_validator(mode='before')
616
+ @classmethod
617
+ def validate_mcacalibrationconfig_before(cls, data):
618
+ """Ensure that a valid configuration was provided and finalize
619
+ flux_file filepath.
620
+
621
+ :param data: Pydantic validator data object.
622
+ :type data: MCACalibrationConfig,
623
+ pydantic_core._pydantic_core.ValidationInfo
624
+ :return: The currently validated list of class properties.
625
+ :rtype: dict
626
+ """
627
+ if isinstance(data, dict):
628
+ inputdir = data.get('inputdir')
629
+ if inputdir is not None:
630
+ flux_file = data.get('flux_file')
631
+ if flux_file is not None and not os.path.isabs(flux_file):
632
+ data['flux_file'] = os.path.join(inputdir, flux_file)
633
+ return data
634
+
635
+ @field_validator('scan_step_indices', mode='before')
636
+ @classmethod
637
+ def validate_scan_step_indices(cls, scan_step_indices):
638
+ """Validate the specified list of scan numbers.
639
+
640
+ :ivar scan_step_indices: Optional scan step indices to use for
641
+ the calibration. If not specified, the calibration will be
642
+ performed on the average of all MCA spectra for the scan.
643
+ :type scan_step_indices: int, str, list[int], optional
644
+ :raises ValueError: Invalid experiment type.
645
+ :return: List of step indices.
646
+ :rtype: list[int]
647
+ """
648
+ if isinstance(scan_step_indices, int):
649
+ scan_step_indices = [scan_step_indices]
650
+ elif isinstance(scan_step_indices, str):
651
+ # Local modules
652
+ from CHAP.utils.general import string_to_list
653
+
654
+ scan_step_indices = string_to_list(scan_step_indices)
655
+ return scan_step_indices
656
+
657
+ def flux_file_energy_range(self):
658
+ """Get the energy range in the flux correction file.
659
+
660
+ :return: The energy range in the flux correction file.
661
+ :rtype: tuple(float, float)
662
+ """
663
+ if self.flux_file is None:
664
+ return None
665
+ flux = np.loadtxt(self.flux_file)
666
+ energies = flux[:,0]/1.e3
667
+ return energies.min(), energies.max()
668
+
669
+ def flux_correction_interpolation_function(self):
670
+ """Get an interpolation function to correct MCA data for the
671
+ relative energy flux of the incident beam.
672
+
673
+ :return: Energy flux correction interpolation function.
674
+ :rtype: scipy.interpolate._polyint._Interpolator1D
675
+ """
676
+ if self.flux_file is None:
677
+ return None
678
+ flux = np.loadtxt(self.flux_file)
679
+ energies = flux[:,0]/1.e3
680
+ relative_intensities = flux[:,1]/np.max(flux[:,1])
681
+ interpolation_function = interp1d(energies, relative_intensities)
682
+ return interpolation_function
683
+
684
+
685
+ class MCAEnergyCalibrationConfig(MCACalibrationConfig):
686
+ """Base class representing metadata required to perform an energy
687
+ calibration of an MCA detector.
688
+
689
+ :ivar max_energy_kev: Maximum channel energy of the MCA in
690
+ keV, defaults to `200.0`.
691
+ :type max_energy_kev: float, optional
692
+ :ivar max_peak_index: Index of the peak in `peak_energies`
693
+ with the highest amplitude, defaults to `1` (the second peak)
694
+ for CeO2 calibration. Required for any other materials.
695
+ :type max_peak_index: int, optional
696
+ """
697
+ max_energy_kev: Optional[confloat(gt=0, allow_inf_nan=False)] = 200.0
698
+ max_peak_index: Optional[
699
+ Annotated[conint(ge=0), Field(validate_default=True)]] = None
700
+
701
+ @model_validator(mode='before')
702
+ @classmethod
703
+ def validate_mcaenergycalibrationconfig_before(cls, data):
704
+ if isinstance(data, dict):
705
+ detectors = data.pop('detectors', None)
706
+ if detectors is not None:
707
+ data['detector_config'] = {'detectors': detectors}
708
+ return data
709
+
710
+ @model_validator(mode='after')
711
+ def validate_mcaenergycalibrationconfig_after(self):
712
+ """Validate the detector (energy) mask ranges and update any
713
+ detector configuration parameters not superseded by their
714
+ individual values.
715
+
716
+ :return: Updated energy calibration configuration class.
717
+ :rtype: MCAEnergyCalibrationConfig
718
+ """
719
+ if self.peak_energies is None:
720
+ raise ValueError('peak_energies is required')
721
+ if not 0 <= self.max_peak_index < len(self.peak_energies):
722
+ raise ValueError('max_peak_index out of bounds')
723
+ return self
724
+
725
+ @field_validator('max_peak_index', mode='before')
726
+ @classmethod
727
+ def validate_max_peak_index(cls, max_peak_index, info):
728
+ if max_peak_index is None:
729
+ materials = info.data.get('materials', [])
730
+ if len(materials) != 1 or materials[0].material_name != 'CeO2':
731
+ raise ValueError('max_peak_index is required unless the '
732
+ 'calibration material is CeO2')
733
+ max_peak_index = 1
734
+ return max_peak_index
735
+
736
+ class MCATthCalibrationConfig(MCACalibrationConfig):
737
+ """Class representing metadata required to perform a 2&theta
738
+ calibration of an MCA detector.
739
+
740
+ :ivar calibration_method: Type of calibration method,
741
+ defaults to `'direct_fit_bragg'`.
742
+ :type calibration_method:
743
+ Literal['direct_fit_bragg', 'direct_fit_tth_ecc'], optional
744
+ :ivar detectors: List of individual MCA detector element
745
+ calibration configurations.
746
+ :ivar quadratic_energy_calibration: Adds a quadratic term to
747
+ the detector channel index to energy conversion, defaults
748
+ to `False` (linear only).
749
+ :type quadratic_energy_calibration: bool, optional
750
+ :ivar tth_initial_guess: Initial guess for 2&theta.
751
+ :type tth_initial_guess: float, optional
752
+ """
753
+ calibration_method: Optional[Literal[
754
+ 'direct_fit_bragg', 'direct_fit_tth_ecc']] = 'direct_fit_bragg'
755
+ quadratic_energy_calibration: Optional[bool] = False
756
+ tth_initial_guess: Optional[
757
+ confloat(gt=0, allow_inf_nan=False)] = Field(None, exclude=True)
758
+
759
+ def flux_file_energy_range(self):
760
+ """Get the energy range in the flux corection file.
761
+
762
+ :return: The energy range in the flux corection file.
763
+ :rtype: tuple(float, float)
764
+ """
765
+ if self.flux_file is None:
766
+ return None
767
+ flux = np.loadtxt(self.flux_file)
768
+ energies = flux[:,0]/1.e3
769
+ return energies.min(), energies.max()
770
+
771
+
772
+ class StrainAnalysisConfig(MCACalibrationConfig):
773
+ """Class representing input parameters required to perform a
774
+ strain analysis.
775
+
776
+ :ivar find_peaks: Exclude peaks where the average spectrum is
777
+ below the `rel_height_cutoff` cutoff relative to the
778
+ maximum value of the average spectrum, defaults to `True`.
779
+ :type find_peaks: bool, optional
780
+ :ivar oversampling: FIX
781
+ :type oversampling: FIX
782
+ :ivar rel_height_cutoff: Used to excluded peaks based on the
783
+ `find_peak` parameter as well as for peak fitting exclusion
784
+ of the individual detector spectra (see the strain detector
785
+ configuration `CHAP.edd.models.MCADetectorStrainAnalysis).
786
+ Defaults to `None`.
787
+ :type rel_height_cutoff: float, optional
788
+ :ivar skip_animation: Skip the animation and plotting of
789
+ the strain analysis fits, defaults to `False`.
790
+ :type skip_animation: bool, optional
791
+ :ivar sum_axes: Whether to sum over the fly axis or not
792
+ for EDD scan types not 0, defaults to `True`.
793
+ :type sum_axes: Union[bool, list[str]], optional
794
+ """
795
+ find_peaks: Optional[bool] = True
796
+ num_proc: Optional[conint(gt=0)] = max(1, os.cpu_count()//4)
797
+ oversampling: Optional[
798
+ Annotated[Dict, Field(validate_default=True)]] = {'num': 10}
799
+ rel_height_cutoff: Optional[
800
+ confloat(gt=0, lt=1.0, allow_inf_nan=False)] = None
801
+ skip_animation: Optional[bool] = False
802
+ sum_axes: Optional[
803
+ Union[bool, conlist(min_length=1, item_type=str)]] = True
804
+
805
+ # FIX tth_file/tth_map not updated
806
+ # @field_validator('detectors')
807
+ # @classmethod
808
+ # def validate_detectors(cls, detectors, info):
809
+ # """Validate detector element tth_file field. It may only be
810
+ # used if StrainAnalysisConfig used par_file.
811
+ # """
812
+ # for detector in detectors:
813
+ # tth_file = detector.tth_file
814
+ # if tth_file is not None:
815
+ # if not info.data.get('par_file'):
816
+ # raise ValueError(
817
+ # 'variable tth angles may only be used with a '
818
+ # 'StrainAnalysisConfig that uses par_file.')
819
+ # else:
820
+ # try:
821
+ # detector.tth_map = ParFile(
822
+ # info.data['par_file']).map_values(
823
+ # info.data['map_config'],
824
+ # np.loadtxt(tth_file))
825
+ # except Exception as e:
826
+ # raise ValueError(
827
+ # 'Could not get map of tth angles from '
828
+ # f'{tth_file}') from e
829
+ # return detectors
830
+
831
+ @field_validator('oversampling')
832
+ @classmethod
833
+ def validate_oversampling(cls, oversampling, info):
834
+ """Validate the oversampling field.
835
+
836
+ :param oversampling: The value of `oversampling` to validate.
837
+ :type oversampling: dict
838
+ :param info: Pydantic validator info object.
839
+ :type info: StrainAnalysisConfig,
840
+ pydantic_core._pydantic_core.ValidationInfo
841
+ :return: The validated value for oversampling.
842
+ :rtype: bool
843
+ """
844
+ # Local modules
845
+ from CHAP.utils.general import is_int
846
+
847
+ raise ValueError('oversampling not updated yet')
848
+ map_config = info.data.get('map_config')
849
+ if map_config is None or map_config.attrs['scan_type'] < 3:
850
+ return None
851
+ if oversampling is None:
852
+ return {'num': 10}
853
+ if 'start' in oversampling and not is_int(oversampling['start'], ge=0):
854
+ raise ValueError('Invalid "start" parameter in "oversampling" '
855
+ f'field ({oversampling["start"]})')
856
+ if 'end' in oversampling and not is_int(oversampling['end'], gt=0):
857
+ raise ValueError('Invalid "end" parameter in "oversampling" '
858
+ f'field ({oversampling["end"]})')
859
+ if 'width' in oversampling and not is_int(oversampling['width'], gt=0):
860
+ raise ValueError('Invalid "width" parameter in "oversampling" '
861
+ f'field ({oversampling["width"]})')
862
+ if ('stride' in oversampling
863
+ and not is_int(oversampling['stride'], gt=0)):
864
+ raise ValueError('Invalid "stride" parameter in "oversampling" '
865
+ f'field ({oversampling["stride"]})')
866
+ if 'num' in oversampling and not is_int(oversampling['num'], gt=0):
867
+ raise ValueError('Invalid "num" parameter in "oversampling" '
868
+ f'field ({oversampling["num"]})')
869
+ if 'mode' in oversampling and 'mode' not in ('valid', 'full'):
870
+ raise ValueError('Invalid "mode" parameter in "oversampling" '
871
+ f'field ({oversampling["mode"]})')
872
+ if not ('width' in oversampling or 'stride' in oversampling
873
+ or 'num' in oversampling):
874
+ raise ValueError('Invalid input parameters, specify at least one '
875
+ 'of "width", "stride" or "num"')
876
+ return oversampling