"""`Pydantic <https://github.com/pydantic/pydantic>`__ model
configuration classes unique to the the EDD workflow.
"""
# System modules
from copy import deepcopy
import os
import typing
from typing import (
Literal,
Optional,
Union,
)
# Third party modules
import numpy as np
#from hexrd.material import Material
from CHAP.utils.material import Material
from pydantic import (
Field,
FilePath,
PrivateAttr,
confloat,
conint,
conlist,
constr,
field_validator,
model_validator,
)
from scipy.interpolate import interp1d
from typing_extensions import Annotated
# Local modules
from CHAP.models import CHAPBaseModel
from CHAP.common.models.map import Detector
from CHAP.utils.models import Multipeak
#from CHAP.utils.parfile import ParFile
# Baseline configuration class
[docs]
class BaselineConfig(CHAPBaseModel):
"""Baseline model configuration class.
:ivar attrs: Additional baseline model configuration attributes.
:vartype attrs: dict, optional
:ivar lam: &lambda (smoothness) parameter (the balance between the
residual of the data and the baseline and the smoothness of the
baseline). The suggested range is between 100 and 10^8,
defaults to `10^6`.
:vartype lam: float, optional
:ivar max_iter: Maximum number of iterations,
defaults to `100`.
:vartype max_iter: int, optional
:ivar tol: Convergence tolerence, defaults to `1.e-6`.
:vartype tol: float, optional
"""
attrs: Optional[dict] = {}
lam: confloat(gt=0, allow_inf_nan=False) = 1.e6
max_iter: conint(gt=0) = 100
tol: confloat(gt=0, allow_inf_nan=False) = 1.e-6
# Fit configuration class
[docs]
class FitConfig(CHAPBaseModel):
"""Fit parameters configuration class for peak fitting.
:ivar background: Background model for peak fitting, defaults
to `constant`.
:vartype background: str, list[str], optional
:ivar baseline: Automated baseline subtraction configuration,
defaults to `False`.
:vartype baseline: bool or BaselineConfig, optional
:ivar centers_range: Peak centers range for peak fitting.
The allowed range for the peak centers will be the initial
values ± `centers_range` (in MCA channels for calibration
or keV for strain analysis). Defaults to `20` for calibration
and `2.0` for strain analysis.
:vartype centers_range: float, optional
:ivar energy_mask_ranges: MCA energy mask ranges in keV for
selecting the data to be included after applying a mask (bounds
are inclusive). Specify either energy_mask_ranges or
mask_ranges, not both.
:vartype energy_mask_ranges: list[[float, float]], optional
:ivar fwhm_min: Minimum FWHM for peak fitting (in MCA channels
for calibration or keV for strain analysis). Defaults to `3`
for calibration and `0.25` for strain analysis.
:vartype fwhm_min: float, optional
:ivar fwhm_max: Maximum FWHM for peak fitting (in MCA channels
for calibration or keV for strain analysis). Defaults to `25`
for calibration and `2.0` for strain analysis.
:vartype fwhm_max: float, optional
:ivar mask_ranges: MCA channel bin ranges for selecting the data
to be included in the energy calibration after applying a mask
(bounds are inclusive). Specify for energy calibration only.
:vartype mask_ranges: list[[int, int]], optional
:ivar backgroundpeaks: Additional background peaks (their
associated fit parameters in units of keV).
:vartype backgroundpeaks: Multipeak, optional
"""
background: Optional[conlist(item_type=constr(
strict=True, strip_whitespace=True, to_lower=True))] = ['constant']
baseline: Optional[Union[bool, BaselineConfig]] = None
centers_range: Optional[confloat(gt=0, allow_inf_nan=False)] = 20
energy_mask_ranges: Optional[conlist(
min_length=1,
item_type=conlist(
min_length=2,
max_length=2,
item_type=confloat(allow_inf_nan=False)))] = None
fwhm_min: Optional[confloat(gt=0, allow_inf_nan=False)] = 3
fwhm_max: Optional[confloat(gt=0, allow_inf_nan=False)] = 25
mask_ranges: Optional[conlist(
min_length=1,
item_type=conlist(
min_length=2,
max_length=2,
item_type=conint(ge=0)))] = None
backgroundpeaks: Optional[Multipeak] = None
[docs]
@field_validator('background', mode='before')
@classmethod
def validate_background(cls, background):
"""Validate the background model.
:param background: Background model for peak fitting.
:type background: str or list[str], optional
:return: Validated background models.
:rtype: list[str]
"""
if background is None:
return background
if isinstance(background, str):
return [background]
return sorted(background)
[docs]
@field_validator('baseline', mode='before')
@classmethod
def validate_baseline(cls, baseline):
"""Validate the baseline configuration.
:param baseline: Automated baseline subtraction configuration.
:type baseline: BaselineConfig, optional
:return: Validated baseline subtraction configuration.
:rtype: BaselineConfig or None
"""
if isinstance(baseline, bool) and baseline:
return BaselineConfig()
return baseline
[docs]
@field_validator('energy_mask_ranges', mode='before')
@classmethod
def validate_energy_mask_ranges(cls, energy_mask_ranges):
"""Validate the mask ranges for selecting the data to include.
:param energy_mask_ranges: MCA energy mask ranges in keV for
selecting the data to be included after applying a mask
(bounds are inclusive).
:type energy_mask_ranges: list[[float, float]], optional
:return: Validated energy mask ranges.
:rtype: list[[float, float]]
"""
if energy_mask_ranges:
return sorted([sorted(v) for v in energy_mask_ranges])
return energy_mask_ranges
[docs]
@field_validator('mask_ranges', mode='before')
@classmethod
def validate_mask_ranges(cls, mask_ranges):
"""Validate the mask ranges for selecting the data to include.
:param mask_ranges: MCA channel bin ranges for selecting the
data to be included after applying a mask (bounds are
inclusive).
:type mask_ranges: list[[int, int]], optional
:return: Validated mask ranges.
:rtype: list[[int, int]]
"""
if mask_ranges:
return sorted([sorted(v) for v in mask_ranges])
return mask_ranges
# Material configuration class
[docs]
class MaterialConfig(CHAPBaseModel):
"""Sample material parameters configuration class.
:ivar material_name: Sample material name.
:vartype material_name: str, optional
:ivar lattice_parameters: Lattice spacing(s) in angstroms.
:vartype lattice_parameters: float, list[float], optional
:ivar sgnum: Space group of the material.
:vartype sgnum: int, optional
:ivar dmin: Minimum d-spacing for selecting the available HKLs,
defaults to 0.35.
:vartype dmin: float, optional
"""
#RV FIX create a getter for lattice_parameters that always returns a list?
material_name: Optional[constr(strip_whitespace=True, min_length=1)] = None
lattice_parameters: Optional[Union[
confloat(gt=0, allow_inf_nan=False),
conlist(
min_length=1, max_length=6,
item_type=confloat(gt=0, allow_inf_nan=False))]] = None
sgnum: Optional[conint(ge=0)] = None
dmin: Optional[confloat(gt=0, allow_inf_nan=False)] = 0.35
_material: Optional[Material]
[docs]
@model_validator(mode='after')
def validate_materialconfig_after(self):
"""Create and validate the private attribute _material.
:return: Validated configuration class.
:rtype: MaterialConfig
"""
# self._material = make_material(
# self.material_name, self.sgnum, self.lattice_parameters, self.dmin)
self._material = Material.make_material(
self.material_name, sgnum=self.sgnum,
lattice_parameters_angstroms=self.lattice_parameters)
# pos=['4a', '8c'])
#pos=[(0,0,0), (1/4, 1/4, 1/4), (3/4, 3/4, 3/4)])
self.lattice_parameters = list([
x.getVal('angstrom') if x.isLength() else x.getVal('radians')
for x in self._material._lparms])
return self
# Detector configuration classes
# Avoid Pydantic "Class not fully defined" in sphinx autodoc as a
# result of lazy importing by using in an _exclude pydantic instance
# variable
FitConfig.model_rebuild(_types_namespace=vars(typing))
[docs]
class MCADetectorCalibration(Detector, FitConfig):
"""Class representing the configuration for a single MCA detector
element to perform detector calibration.
:ivar energy_calibration_coeffs: Detector channel index to energy
polynomial conversion coefficients ([a, b, c] with
E_i = a*i^2 + b*i + c).
:vartype energy_calibration_coeffs:
list[float, float, float], optional
:ivar num_bins: Number of MCA channels.
:vartype num_bins: int, optional
:ivar tth_max: Detector rotation about lab frame x axis.
:vartype tth_max: float, optional
:ivar tth_tol: Minimum resolvable difference in 2&theta between
two unique Bragg peaks,
:vartype tth_tol: float, optional
:ivar tth_calibrated: Calibrated value for 2&theta.
:vartype tth_calibrated: float, optional
:ivar tth_initial_guess: Initial guess for 2&theta superseding
the global one in
:class:`~CHAP.edd.models.MCATthCalibrationConfig`.
:vartype tth_initial_guess: float, optional
"""
processor_type: Literal['calibration']
energy_calibration_coeffs: Optional[conlist(
min_length=3, max_length=3,
item_type=confloat(allow_inf_nan=False))] = None
num_bins: Optional[conint(gt=0)] = None
tth_max: Optional[confloat(gt=0, allow_inf_nan=False)] = None
tth_tol: Optional[confloat(gt=0, allow_inf_nan=False)] = None
tth_calibrated: Optional[confloat(gt=0, allow_inf_nan=False)] = None
tth_initial_guess: Optional[confloat(gt=0, allow_inf_nan=False)] = None
_energy_calibration_mask_ranges: conlist(
min_length=1,
item_type=conlist(
min_length=2,
max_length=2,
item_type=conint(ge=0))) = PrivateAttr()
_hkl_indices: list = PrivateAttr()
# def add_calibration(self, calibration):
# """Finalize values for some fields using a calibration
# MCADetectorCalibration corresponding to the same detector.
#
# :param calibration: Existing calibration configuration.
# :type calibration: MCADetectorCalibration
# """
# raise RuntimeError('To do')
# for field in ['energy_calibration_coeffs', 'num_bins',
# '_energy_calibration_mask_ranges']:
# setattr(self, field, deepcopy(getattr(calibration, field)))
# if self.tth_calibrated is not None:
# self.logger.warning(
# 'Ignoring tth_calibrated in calibration configuration')
# self.tth_calibrated = None
@property
def energies(self):
"""Return the calibrated bin energies.
:type: numpy.ndarray
"""
a, b, c = tuple(self.energy_calibration_coeffs)
channel_bins = np.arange(self.num_bins)
return (a*channel_bins + b)*channel_bins + c
@property
def hkl_indices(self):
"""Return the HKL indices consistent with the selected energy
ranges (include_energy_ranges).
:type: list
"""
if hasattr(self, '_hkl_indices'):
return self._hkl_indices
return []
@hkl_indices.setter
def hkl_indices(self, hkl_indices):
"""Set the HKL indices.
:param hkl_indices: HKL indices.
:type: list
"""
self._hkl_indices = hkl_indices
[docs]
def convert_mask_ranges(self, mask_ranges):
"""Given a list of mask ranges in channel bins, set the
corresponding list of channel energy mask ranges.
:param mask_ranges: Mask ranges to convert to energy mask
ranges.
:type mask_ranges: list[[int, int]]
"""
energies = self.energies
self.energy_mask_ranges = [
[float(energies[i]) for i in range_]
for range_ in sorted([sorted(v) for v in mask_ranges])]
[docs]
def get_mask_ranges(self):
"""Return the list of mask ranges if set or convert the
energy mask ranges from channel energies to channel indices
and return those.
:type: list[[float, float]]
"""
if self.mask_ranges:
return self.mask_ranges
if self.energy_mask_ranges is None:
return None
# Local modules
from CHAP.utils.general import (
index_nearest_down,
index_nearest_up,
)
mask_ranges = []
energies = self.energies
for e_min, e_max in self.energy_mask_ranges:
mask_ranges.append(
[index_nearest_down(energies, e_min),
index_nearest_up(energies, e_max)])
return mask_ranges
[docs]
def mca_mask(self):
"""Get a boolean mask array to use on this MCA element's data.
Note that the bounds of the mask ranges are inclusive.
:return: Boolean mask array.
:rtype: numpy.ndarray
"""
mask = np.asarray([False] * self.num_bins)
mask_ranges = self.get_mask_ranges()
channel_bins = np.arange(self.num_bins, dtype=np.int32)
for (min_, max_) in mask_ranges:
mask = np.logical_or(
mask,
np.logical_and(channel_bins >= min_, channel_bins <= max_))
return mask
[docs]
def set_energy_calibration_mask_ranges(self):
"""Set the value of the private attribite
`_energy_calibration_mask_ranges` to value of `mask_ranges`.
"""
self._energy_calibration_mask_ranges = deepcopy(self.mask_ranges)
[docs]
class MCADetectorDiffractionVolumeLength(MCADetectorCalibration):
"""Class representing the configuration for a single MCA detector
element to perform a diffraction volume length measurement.
:ivar dvl: Measured diffraction volume length.
:vartype dvl: float, optional
:ivar fit_amplitude: Amplitude of the Gaussian fit.
:vartype fit_amplitude: float, optional
:ivar fit_center: Center of the Gaussian fit.
:vartype fit_center: float, optional
:ivar fit_sigma: Sigma of the Gaussian fit.
:vartype fit_sigma: float, optional
"""
processor_type: Literal['diffractionvolumelength']
dvl: Optional[confloat(gt=0, allow_inf_nan=False)] = None
fit_amplitude: Optional[float] = None
fit_center: Optional[float] = None
fit_sigma: Optional[float] = None
[docs]
class MCADetectorStrainAnalysis(MCADetectorCalibration):
"""Class representing the configuration to perform a strain
analysis.
:ivar centers_range: Peak centers range for peak fitting.
The allowed range for the peak centers will be the initial
values ± `centers_range` (in keV), defaults to `2.0`.
:vartype centers_range: float, optional
:ivar fwhm_min: Minimum FWHM for peak fitting (in keV),
defaults to `0.25`.
:vartype fwhm_min: float, optional
:ivar fwhm_max: Maximum FWHM for peak fitting (in keV),
defaults to `2.0`.
:vartype fwhm_max: float, optional
:ivar peak_models: Peak model(s) for peak fitting,
defaults to `'gaussian'`.
:vartype peak_models: Literal['gaussian', 'lorentzian', 'pvoigt']],
list[Literal['gaussian', 'lorentzian', 'pvoigt']]], optional
:ivar rel_height_cutoff: Relative peak height cutoff for
peak fitting (any peak with a height smaller than
`rel_height_cutoff` times the maximum height of all peaks
gets removed from the fit model), defaults to `None`.
:vartype rel_height_cutoff: float, optional
:ivar tth_map: Map of the 2&theta values.
:vartype tth_map: numpy.ndarray, optional
"""
#:ivar tth_file: Path to the file with the 2&theta map.
#:vartype tth_file: FilePath, optional
centers_range: Optional[confloat(gt=0, allow_inf_nan=False)] = 2
fwhm_min: Optional[confloat(gt=0, allow_inf_nan=False)] = 0.25
fwhm_max: Optional[confloat(gt=0, allow_inf_nan=False)] = 2.0
processor_type: Literal['strainanalysis']
peak_models: Union[
conlist(
min_length=1,
item_type=Literal['gaussian', 'lorentzian', 'pvoigt']),
Literal['gaussian', 'lorentzian', 'pvoigt']] = 'gaussian'
rel_height_cutoff: Optional[
confloat(gt=0, lt=1.0, allow_inf_nan=False)] = None
# tth_file: Optional[FilePath] = None
tth_map: Optional[np.ndarray] = None
_calibration_energy_mask_ranges: conlist(
min_length=1,
item_type=conlist(
min_length=2,
max_length=2,
item_type=confloat(allow_inf_nan=False))) = PrivateAttr()
[docs]
@field_validator('peak_models')
@classmethod
def validate_peak_models(cls, peak_models):
"""Validate the specified peak_models.
:param peak_models: Peak model(s) for peak fitting.
:type peak_models:
Literal['gaussian', 'lorentzian', 'pvoigt']] or
list[Literal['gaussian', 'lorentzian', 'pvoigt']]],
optional
:type peak_models:
:return: Validated peak_models
:rtype: Literal['gaussian', 'lorentzian', 'pvoigt']] or
list[Literal['gaussian', 'lorentzian', 'pvoigt']]]
"""
if isinstance(peak_models, list):
raise NotImplementedError(
'Multiple peak models not yet implemented')
return peak_models
[docs]
def add_calibration(self, calibration):
"""Transfer certain 2&theta calibration parameters for use by
:class:`~CHAP.edd.processor.LatticeParameterRefinementProcessor`
or :class:`~CHAP.edd.processor.StrainAnalysisProcessor`.
:param calibration: Existing calibration configuration.
:type calibration: MCADetectorCalibration
"""
for field in ['energy_calibration_coeffs', 'num_bins',
'tth_calibrated']:
setattr(self, field, deepcopy(getattr(calibration, field)))
if self.energy_mask_ranges is None:
self.energy_mask_ranges = deepcopy(calibration.energy_mask_ranges)
self._calibration_energy_mask_ranges = deepcopy(
calibration.energy_mask_ranges)
[docs]
def get_calibration_mask_ranges(self):
"""Return the MCA channel bin ranges for the data used during
the 2&theta calibration.
:type: list[[int, int]]
"""
if not hasattr(self, '_calibration_energy_mask_ranges'):
return None
# Local modules
from CHAP.utils.general import (
index_nearest_down,
index_nearest_up,
)
energy_mask_ranges = []
energies = self.energies
for e_min, e_max in self._calibration_energy_mask_ranges:
energy_mask_ranges.append(
[index_nearest_down(energies, e_min),
index_nearest_up(energies, e_max)])
return energy_mask_ranges
[docs]
def get_tth_map(self, map_shape):
"""Return the map of 2&theta values to use -- may vary at each
point in the map.
:param map_shape: Shape of the suplied 2&theta map.
:type map_shape: tuple
:return: Map of 2&theta values.
:rtype: numpy.ndarray
"""
if getattr(self, 'tth_map', None) is not None:
if self.tth_map.shape != map_shape:
raise ValueError(
'Invalid "tth_map" field shape '
f'{self.tth_map.shape} (expected {map_shape})')
return self.tth_map
return np.full(map_shape, self.tth_calibrated)
MCADetector = Annotated[
Union[
MCADetectorCalibration,
MCADetectorDiffractionVolumeLength,
MCADetectorStrainAnalysis],
Field(discriminator='processor_type')
]
[docs]
class MCADetectorConfig(FitConfig):
"""Class representing metadata required to configure a full MCA
detector.
:ivar detectors: Individual MCA detector elements.
:vartype detectors: list[MCADetector], optional
"""
processor_type: Literal[
'calibration', 'diffractionvolumelength', 'strainanalysis']
detectors: Optional[conlist(min_length=1, item_type=MCADetector)] = []
_exclude = set(vars(FitConfig()).keys())
[docs]
@model_validator(mode='before')
@classmethod
def validate_mcadetectorconfig_before(cls, data):
"""Validate the `MCADetectorConfig` class attributes.
:param data:
`Pydantic <https://github.com/pydantic/pydantic>`__
validator data object.
:type data: dict
:return: Currently validated class attributes.
:rtype: dict
"""
if isinstance(data, dict):
processor_type = data.get('processor_type').lower()
if 'detectors' in data:
detectors = data.pop('detectors')
for d in detectors:
d['processor_type'] = processor_type
attrs = d.pop('attrs', {})
if 'default_fields' in attrs:
attrs.pop('default_fields')
if attrs:
d['attrs'] = attrs
data['detectors'] = detectors
return data
[docs]
@model_validator(mode='after')
def validate_mcadetectorconfig_after(self):
"""Validate and update the detectors.
:return: Validated detectors.
:rtype: MCADetectorConfig
"""
if self.detectors:
self.update_detectors()
return self
[docs]
def update_detectors(self):
"""Update individual detector parameters with any non-default
values from the global detector configuration.
"""
for k, v in self:
if k in self.model_fields_set:
for d in self.detectors:
if hasattr(d, k):
setattr(d, k, deepcopy(v))
# Processor configuration classes
[docs]
class DiffractionVolumeLengthConfig(FitConfig):
"""Configuration for the differential volume length processor
:class:`~CHAP.edd.processor.DiffractionVolumeLengthProcessor`
for an EDD setup using a steel-foil raster scan.
:ivar max_energy_kev: Maximum channel energy of the MCA in
keV, defaults to `200.0`.
:vartype max_energy_kev: float, optional
:ivar measurement_mode: Placeholder for recording whether the
measured DVL value was obtained through the automated
calculation or a manual selection, defaults to `'auto'`.
:vartype measurement_mode: Literal['manual', 'auto'], optional
:ivar sample_thickness: Thickness of scanned foil sample. Quantity
must be provided in the same units as the values of the
scanning motor.
:vartype sample_thickness: float
:ivar sigma_to_dvl_factor: The DVL is obtained by fitting a reduced
form of the MCA detector data. `sigma_to_dvl_factor` is a
scalar value that converts the standard deviation of the
gaussian fit to the measured DVL, defaults to `3.5`.
:vartype sigma_to_dvl_factor: Literal[2.0, 3.5, 4.0], optional
"""
max_energy_kev: Optional[confloat(gt=0, allow_inf_nan=False)] = 200.0
measurement_mode: Optional[Literal['manual', 'auto']] = 'auto'
sample_thickness: Optional[confloat(gt=0, allow_inf_nan=False)] = None
sigma_to_dvl_factor: Optional[Literal[2.0, 3.5, 4.0]] = 3.5
_exclude = set(vars(FitConfig()).keys())
[docs]
@model_validator(mode='after')
def validate_diffractionvolumelengthconfig_after(self):
"""Update the configuration with costum defaults after the
normal native pydantic validation.
:return: Updated DVL configuration class.
:rtype: DiffractionVolumeLengthConfig
"""
if self.measurement_mode == 'manual':
self._exclude |= {'sigma_to_dvl_factor'}
return self
# Avoid Pydantic "Class not fully defined" in sphinx autodoc as a
# result of lazy importing by using MaterialConfig within a default
# value of a pydantic instance variable
MaterialConfig.model_rebuild(_types_namespace=vars(typing))
[docs]
class MCACalibrationConfig(CHAPBaseModel):
"""Base class configuration for energy and 2&theta calibration
processors.
:ivar flux_file: File name of the csv flux file containing station
beam energy in eV (column 0) versus flux (column 1).
:vartype flux_file: str, optional
:ivar materials: Material configurations for the calibration,
defaults to [`Ceria`].
:vartype materials: list[MaterialConfig], optional
:ivar peak_energies: Theoretical locations of the fluorescence
peaks in keV to use for calibrating the MCA channel energies.
:vartype peak_energies: list[float], optional for energy calibration
:ivar scan_step_indices: Optional scan step indices to use for the
calibration. If not specified, the calibration will be
performed on the average of all MCA spectra for the scan.
:vartype scan_step_indices: int, str, list[int], optional
.. note::
Fluorescence data:
https://physics.nist.gov/PhysRefData/XrayTrans/Html/search.html
"""
flux_file: Optional[FilePath] = None
materials: Optional[conlist(item_type=MaterialConfig)] = [MaterialConfig(
material_name='CeO2', lattice_parameters=5.41153, sgnum=225)]
peak_energies: Optional[conlist(
min_length=2, item_type=confloat(gt=0, allow_inf_nan=False))] = [
34.279, 34.720, 39.258, 40.233]
scan_step_indices: Optional[
conlist(min_length=1, item_type=conint(ge=0))] = None
[docs]
@model_validator(mode='before')
@classmethod
def validate_mcacalibrationconfig_before(cls, data):
"""Ensure that a valid configuration was provided and finalize
flux_file filepath.
:param data:
`Pydantic <https://github.com/pydantic/pydantic>`__
validator data object.
:type data: dict
:return: Currently validated class attributes.
:rtype: dict
"""
if isinstance(data, dict):
inputdir = data.get('inputdir')
if inputdir is not None:
flux_file = data.get('flux_file')
if flux_file is not None and not os.path.isabs(flux_file):
data['flux_file'] = os.path.join(inputdir, flux_file)
return data
[docs]
@field_validator('scan_step_indices', mode='before')
@classmethod
def validate_scan_step_indices(cls, scan_step_indices):
"""Validate the specified list of scan numbers.
:param scan_step_indices: Optional scan step indices to use for
the calibration. If not specified, the calibration will be
performed on the average of all MCA spectra for the scan.
:type scan_step_indices: int or str or list[int], optional
:raises ValueError: Invalid experiment type.
:return: Validated scan step indices.
:rtype: list[int]
"""
if isinstance(scan_step_indices, int):
scan_step_indices = [scan_step_indices]
elif isinstance(scan_step_indices, str):
# Local modules
from CHAP.utils.general import string_to_list
scan_step_indices = string_to_list(scan_step_indices)
return scan_step_indices
[docs]
def flux_file_energy_range(self):
"""Get the energy range in the flux correction file.
:type: tuple(float, float)
"""
if self.flux_file is None:
return None
flux = np.loadtxt(self.flux_file)
energies = flux[:,0]/1.e3
return energies.min(), energies.max()
[docs]
def flux_correction_interpolation_function(self):
"""Get an interpolation function to correct MCA data for the
relative energy flux of the incident beam.
:type: scipy.interpolate._polyint._Interpolator1D
"""
if self.flux_file is None:
return None
flux = np.loadtxt(self.flux_file)
energies = flux[:,0]/1.e3
relative_intensities = flux[:,1]/np.max(flux[:,1])
interpolation_function = interp1d(energies, relative_intensities)
return interpolation_function
[docs]
class MCAEnergyCalibrationConfig(MCACalibrationConfig):
"""Configuration for the energy calibration processor
:class:`~CHAP.edd.processor.MCAEnergyCalibrationProcessor`.
:ivar max_energy_kev: Maximum channel energy of the MCA in
keV, defaults to `200.0`.
:vartype max_energy_kev: float, optional
:ivar max_peak_index: Index of the peak in `peak_energies`
with the highest amplitude, defaults to `1` (the second peak)
for CeO2 calibration. Required for any other materials.
:vartype max_peak_index: int, optional
"""
max_energy_kev: Optional[confloat(gt=0, allow_inf_nan=False)] = 200.0
max_peak_index: Optional[conint(ge=0)] = None
[docs]
@model_validator(mode='before')
@classmethod
def validate_mcaenergycalibrationconfig_before(cls, data):
"""Validate the `MCAEnergyCalibrationConfig` class attributes.
:param data:
`Pydantic <https://github.com/pydantic/pydantic>`__
validator data object.
:type data: dict
:return: Currently validated class attributes.
:rtype: dict
"""
if isinstance(data, dict):
detectors = data.pop('detectors', None)
if detectors is not None:
data['detector_config'] = {'detectors': detectors}
return data
[docs]
@model_validator(mode='after')
def validate_mcaenergycalibrationconfig_after(self):
"""Validate the detector (energy) mask ranges and update any
detector configuration parameters not superseded by their
individual values.
:return: Validated energy calibration configuration class.
:rtype: MCAEnergyCalibrationConfig
"""
if self.peak_energies is None:
raise ValueError('peak_energies is required')
if (self.max_peak_index is not None
and not 0 <= self.max_peak_index < len(self.peak_energies)):
raise ValueError('max_peak_index out of bounds')
return self
[docs]
@field_validator('max_peak_index', mode='before')
@classmethod
def validate_max_peak_index(cls, max_peak_index, info):
"""Validate max_peak_index.
:param max_peak_index: Index of the peak in `peak_energies`
with the highest amplitude, defaults to `1` (the second
peak) for CeO2 calibration. Required for any other
materials.
:type max_peak_index: int, optional
:param info: Model parameter validation information.
:type info: pydantic.ValidationInfo
:return: Validated max_peak_index.
:rtype: int
"""
if max_peak_index is None:
materials = info.data.get('materials', [])
if len(materials) != 1 or materials[0].material_name != 'CeO2':
raise ValueError('max_peak_index is required unless the '
'calibration material is CeO2')
max_peak_index = 1
return max_peak_index
[docs]
class MCATthCalibrationConfig(MCACalibrationConfig):
"""Configuration for the 2&theta calibration and the reduced data
processors, :class:`~CHAP.edd.processor.MCATthCalibrationProcessor`
and :class:`~CHAP.edd.processor.ReducedDataProcessor`,
respectively.
:ivar calibration_method: Type of calibration method,
defaults to `'direct_fit_bragg'`.
:vartype calibration_method:
Literal['direct_fit_bragg', 'direct_fit_tth_ecc'], optional
:ivar quadratic_energy_calibration: Adds a quadratic term to
the detector channel index to energy conversion, defaults
to `False` (linear only).
:vartype quadratic_energy_calibration: bool, optional
:ivar tth_initial_guess: Initial guess for 2&theta.
:vartype tth_initial_guess: float, optional
"""
calibration_method: Optional[Literal[
'direct_fit_bragg', 'direct_fit_tth_ecc']] = 'direct_fit_bragg'
quadratic_energy_calibration: Optional[bool] = False
tth_initial_guess: Optional[
confloat(gt=0, allow_inf_nan=False)] = Field(None, exclude=True)
[docs]
def flux_file_energy_range(self):
"""Get the energy range in the flux corection file.
:type: tuple(float, float)
"""
if self.flux_file is None:
return None
flux = np.loadtxt(self.flux_file)
energies = flux[:,0]/1.e3
return energies.min(), energies.max()
[docs]
class StrainAnalysisConfig(MCACalibrationConfig):
"""Configuration for the lattice parameter refinement and strain
analysis processors,
:class:`~CHAP.edd.processor.LatticeParameterRefinementProcessor`
and :class:`~CHAP.edd.processor.StrainAnalysisProcessor`,
respectively.
:ivar find_peak_cutoff: Use scipy.signal.find_peaks to exclude
peaks for all spectra for a given detector and user specified
mask. A particular HKL peak is removed from the set of HKLs,
when its mean peak height is below `find_peak_cutoff` times
the maximum mean intensity for that detector. Defaults to `0`
in which case this step is ignored.
:vartype find_peak_cutoff: float, optional
:ivar num_proc: Number of processors to be used by the strain
analysis peak fitting routine.
:vartype num_proc: int
:ivar rel_height_cutoff: Used to excluded peaks based on the
`find_peak` parameter as well as for peak fitting exclusion
of the individual detector spectra (see the strain detector
configuration
:class:`~CHAP.edd.models.MCADetectorStrainAnalysis`).
Defaults to `None`.
:vartype rel_height_cutoff: float, optional
:ivar skip_animation: Skip the animation and plotting of
the strain analysis fits, defaults to `False`.
:vartype skip_animation: bool, optional
:ivar sum_axes: Whether to sum over the fly axis or not
for EDD scan types not 0, defaults to `True`.
:vartype sum_axes: bool or list[str], optional
"""
#:ivar oversampling: FIX
#:vartype oversampling: FIX
find_peak_cutoff: Optional[confloat(ge=0.0, allow_inf_nan=False)] = 0.0
num_proc: Optional[conint(gt=0)] = max(1, os.cpu_count()//4)
#oversampling: dict = {'num': 10}
rel_height_cutoff: Optional[
confloat(gt=0.0, lt=1.0, allow_inf_nan=False)] = None
skip_animation: Optional[bool] = False
sum_axes: Optional[
Union[bool, conlist(min_length=1, item_type=str)]] = True
# FIX tth_file/tth_map not updated
# @field_validator('detectors')
# @classmethod
# def validate_detectors(cls, detectors, info):
# """Validate detector element tth_file field. It may only be
# used if StrainAnalysisConfig used par_file.
# """
# for detector in detectors:
# tth_file = detector.tth_file
# if tth_file is not None:
# if not info.data.get('par_file'):
# raise ValueError(
# 'variable tth angles may only be used with a '
# 'StrainAnalysisConfig that uses par_file.')
# else:
# try:
# detector.tth_map = ParFile(
# info.data['par_file']).map_values(
# info.data['map_config'],
# np.loadtxt(tth_file))
# except Exception as e:
# raise ValueError(
# 'Could not get map of tth angles from '
# f'{tth_file}') from e
# return detectors
# @field_validator('oversampling')
# @classmethod
# def validate_oversampling(cls, oversampling, info):
# """Validate the oversampling field.
#
# :param oversampling: Value of `oversampling` to validate.
# :type oversampling: dict
# :param info: Model parameter validation information.
# :type info: pydantic.ValidationInfo
# :return: Validated oversampling value.
# :rtype: bool
# """
# # Local modules
# from CHAP.utils.general import is_int
#
# raise ValueError('oversampling not updated yet')
# map_config = info.data.get('map_config')
# if map_config is None or map_config.attrs['scan_type'] < 3:
# return None
# if oversampling is None:
# return {'num': 10}
# if 'start' in oversampling and not is_int(oversampling['start'], ge=0):
# raise ValueError('Invalid "start" parameter in "oversampling" '
# f'field ({oversampling["start"]})')
# if 'end' in oversampling and not is_int(oversampling['end'], gt=0):
# raise ValueError('Invalid "end" parameter in "oversampling" '
# f'field ({oversampling["end"]})')
# if 'width' in oversampling and not is_int(oversampling['width'], gt=0):
# raise ValueError('Invalid "width" parameter in "oversampling" '
# f'field ({oversampling["width"]})')
# if ('stride' in oversampling
# and not is_int(oversampling['stride'], gt=0)):
# raise ValueError('Invalid "stride" parameter in "oversampling" '
# f'field ({oversampling["stride"]})')
# if 'num' in oversampling and not is_int(oversampling['num'], gt=0):
# raise ValueError('Invalid "num" parameter in "oversampling" '
# f'field ({oversampling["num"]})')
# if 'mode' in oversampling and 'mode' not in ('valid', 'full'):
# raise ValueError('Invalid "mode" parameter in "oversampling" '
# f'field ({oversampling["mode"]})')
# if not ('width' in oversampling or 'stride' in oversampling
# or 'num' in oversampling):
# raise ValueError('Invalid input parameters, specify at least one '
# 'of "width", "stride" or "num"')
# return oversampling