Source code for autoemx.config.schema_models.detector_model

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Detector model of a microscope: the physical and empirical parameters used to model its X-ray spectra.

The values of each microscope are saved in ``autoemx/calibrations/<microscope_ID>/detector_model.json``, next to its
detector efficiency (detector_efficiency.msa). The functional forms (sigmoids for the peak shape, modified Duncumb
model for the background) are defined here; the file holds their coefficients.

To add a microscope, add a folder with these two files, named with its microscope ID, and its driver in
autoemx/microscope_drivers. The driver is the file named as the microscope ID, unless ``driver`` names another one:
instruments of the same model, each with its own calibrated detector model (e.g. 'PhenomXL_LabB'), share the driver
of that model (driver 'PhenomXL').

This model replaces the module XS_calibrations.py of each microscope (AutoEMX <= 1.4.1).

It centralizes all physical and empirical calibration data needed for accurate spectrum modeling, including detector
characteristics, peak shape, background continuum, and system-specific factors. The channel-to-energy calibration
(offset, bin width) is not part of it: it is saved separately in Detector_channel_params_calibs, since it is
recalibrated regularly (Run_SDD_Calibration.py).

Notes
-----
- The values must be customized for every new detector or microscope system: update all calibration values and
  empirical fits to match the specific hardware and calibration data of each instrument. Failure to do so will result
  in inaccurate quantification.
- All calibration parameters should be reviewed and updated regularly, and after the microscope system is serviced.
- If additional EDS modes are added, every mode-dependent parameter needs an entry for the new mode.
- All formulas and empirical fits are traceable to published literature or in-house calibration data:
    - Goldstein et al., "Scanning Electron Microscopy and X-ray Microanalysis", 4th Ed.
    - N.W.M. Ritchie, Spectrum Simulation in DTSA-II, Microsc. Microanal. 15 (2009) 454–468.
      https://doi.org/10.1017/S1431927609990407
    - R. H. Redus, A. C. Huber (2015). Response Function of Silicon Drift Detectors for Low Energy X-rays. In Advances
      in X-ray Analysis (AXA), pp. 274–282. International Centre for Diffraction Data (ICDD).

Fine-tuning parameters
----------------------
weight_Ll_ref_Ka1, escape_peak_probability, pileup_peak_probability, gen_background_time_scaling_factor,
strobe_peak_int_factor, zero_strobe_peak_sigma:

- These values are empirical and not highly sensitive for most quantification tasks; reasonable estimates suffice.
- Calibration can be performed manually by observing the fitted values in a few high-quality spectra of EDS standards.
- To do this, allow the parameter(s) to vary freely in your lmfit model (set `vary=True`), fit the spectrum, and
  observe the optimized value reported by lmfit. If desired, repeat for several standard spectra to confirm
  consistency, and use the typical fitted results, or their average.
- Precise calibration is not critical unless the parameter is set to an extreme or unphysical value.
"""

from __future__ import annotations

import hashlib
import json
from pathlib import Path
from typing import Dict, List, Optional, Tuple

import numpy as np
import sympy as sp
from pydantic import BaseModel, ConfigDict, Field, model_validator

DETECTOR_MODEL_SCHEMA_VERSION = 1


[docs] class Sigmoid(BaseModel): """scale * (offset + amplitude / (1 + exp(-steepness * (E - center))))""" model_config = ConfigDict(extra="forbid") offset: float amplitude: float center: float steepness: float scale: float = 1.0 def __call__(self, E): return (self.offset + self.amplitude / (1 + np.exp(-self.steepness * (E - self.center)))) * self.scale
[docs] class PeakShapeParams(BaseModel): """ Peak shape parameters for incomplete charge collection (ICC) low-energy tailing, as functions of the X-ray energy E (keV): gamma (tailing shape), f_tail (fraction of tailing), R_e and F_loss (ICC parameters), all dimensionless. Calibration Procedure --------------------- The energy dependence and values of these parameters should be calibrated empirically using high-count spectra from bulk standards, ideally pure elements, with the script Calibrate_Peak_Shape_Params.py: it refits the spectra with the chosen parameters (gamma, f_tail, R_e, F_loss) fitted freely for the chosen elements, fits the chosen sigmoid coefficients to their values as a function of energy, and prints the coefficients to update. Accurate calibration of ICC parameters improves modeling of low-energy tailing and the quantitative accuracy of EDS fits, especially for light elements. These parameters are system- and detector-specific, and should be recalibrated after hardware changes or detector servicing (Redus & Huber, 2015). """ model_config = ConfigDict(extra="forbid") gamma: Sigmoid f_tail: Sigmoid R_e: Sigmoid F_loss: Sigmoid
[docs] class DuncumbF(BaseModel): """1 - exp(-(Z + z_offset) / z_scale)""" model_config = ConfigDict(extra="forbid") z_offset: float z_scale: float
[docs] class BetaParams(BaseModel): """ sigmoid_offset + sigmoid_amplitude / (1 + exp(-sigmoid_steepness * (Z - sigmoid_center))) + gaussian_amplitude * exp(-(Z - gaussian_center)**2 / (2 * gaussian_sigma**2)) """ model_config = ConfigDict(extra="forbid") sigmoid_offset: float sigmoid_amplitude: float sigmoid_steepness: float sigmoid_center: float gaussian_amplitude: float gaussian_center: float gaussian_sigma: float
[docs] class BackgroundParams(BaseModel): """ Parameters of the modified Duncumb background model, as functions of the atomic number Z: P (quadratic polynomial, coefficients from the highest power), F, and beta. Calibration Procedure --------------------- The background model parameters (P, F, and beta) should be empirically calibrated using high-count spectra from bulk standards, ideally pure elements covering a range of atomic numbers Z, with the script Calibrate_Background_Params.py: it refits the spectra with the chosen parameters (P, F, beta) fitted freely, fits the chosen coefficients of their functions to their values as a function of Z, and prints the coefficients to update. One parameter can be calibrated at a time, or all together. Accurate calibration of the background model is essential for quantification, especially for light elements and low-count regions, since it is used for composition-dependent background corrections in the PB method. These parameters are system- and detector-specific, and should be recalibrated after hardware changes or detector servicing. """ model_config = ConfigDict(extra="forbid") duncumb_P: List[float] = Field(min_length=3, max_length=3) duncumb_F: DuncumbF beta: BetaParams
[docs] class DetectorModel(BaseModel): """Detector model of a microscope (see the module docstring).""" model_config = ConfigDict(extra="forbid") schema_version: int = DETECTOR_MODEL_SCHEMA_VERSION driver: Optional[str] = Field( default=None, description="Microscope driver (autoemx/microscope_drivers/<driver>.py). Default: the microscope ID.", ) # --- EDS detector physical characteristics Si_density: float = Field(description="Silicon density at detector operating temperature (235 K), in g/cm^3.") detector_ch_n: int = Field(description="Number of energy channels in the EDS detector.") emergence_angle: float = Field(description="X-ray emergence (take-off) angle, in degrees.") undetectable_els: List[str] = Field( description="Elements that are undetectable or poorly detected by this EDS system. Li and Be detectability " "depends on detector window and configuration." ) available_meas_modes: Tuple[str, ...] = Field( description="Valid EDS acquisition modes for this system. All mode-dependent parameters must have entries for " "each mode used." ) # --- ICC parameters for background fitting (Redus & Huber, 2015) R_e_background: float = Field( description="Incomplete charge collection (ICC) parameter for background fitting. Can be manually adjusted by " "observing the background intensity 'jump' at ~1.84 keV, the Si absorption edge in the detector " "response, in high-count spectra. The exact value is not critical unless extreme or unphysical." ) F_loss_background: float = Field(description="ICC parameter for background fitting (see R_e_background).") # --- Detector noise and peak width: sigma = conv_eff * sqrt(elec_noise**2 + E * F / conv_eff), E in keV F: float = Field( description="(Fixed) Fano factor (dimensionless), characterizing statistical fluctuations in charge " "generation (Ritchie, 2009)." ) elec_noise: float = Field( description="(Fitted) Electronic noise (electrons, rms), characterizing the detector's intrinsic noise. " "elec_noise and conv_eff are calibrated by fitting " "sigma = conv_eff * sqrt(elec_noise**2 + E * F / conv_eff) to the sigmas of characteristic peaks " "at various energies E (keV), fitted in high-count spectra of bulk standards, with " "Calibrate_Peak_Sigma_Params.py. Recalibrate after hardware changes or detector servicing." ) conv_eff: float = Field( description="(Fitted) Conversion efficiency: energy per electron-hole pair (keV), see elec_noise." ) # --- Fine-tuning parameters, by EDS mode (see the module docstring) weight_Ll_ref_Ka1: Dict[str, float] = Field( description="Estimated upper bound for the Ll/Ka1 intensity ratio (for 12 < Z < 20, no La line). Prevents " "overestimation of the Ll peak, e.g. for Al and Si. The fitted value may be lower." ) escape_peak_probability: Dict[str, float] = Field( description="Estimated upper bound for the escape peak probability (fraction). The fitted value may be lower." ) pileup_peak_probability: Dict[str, float] = Field( description="Estimated upper bound for the pileup peak probability (fraction). The fitted value may be lower." ) gen_background_time_scaling_factor: Dict[str, float] = Field( description="Scaling factor K in the modified Duncumb continuum model, yielding F=1 for bulk standards of high " "atomic number. Used to guess the initial value of the background scaling factor K, which is " "allowed to vary within a small interval during fitting." ) strobe_peak_int_factor: Dict[str, float] = Field( description="Multiplicative factor converting the total spectrum collection time to the initial guess of the " "strobe peak intensity, which is allowed to vary within a small interval during fitting." ) zero_strobe_peak_sigma: Dict[str, float] = Field( description="Standard deviation (sigma) of the Gaussian zero strobe peak. Fixed during fitting." ) # --- Calibrated functions, by EDS mode peak_shape: Dict[str, PeakShapeParams] background: Dict[str, BackgroundParams] @model_validator(mode="after") def _check_schema_version(self) -> "DetectorModel": if self.schema_version > DETECTOR_MODEL_SCHEMA_VERSION: raise ValueError( f"Detector model schema version {self.schema_version} is newer than the one supported by this " f"AutoEMX version ({DETECTOR_MODEL_SCHEMA_VERSION}): update AutoEMX." ) return self
[docs] @classmethod def from_json_file(cls, file_path: str | Path) -> "DetectorModel": with open(file_path, "r", encoding="utf-8") as handle: return cls.model_validate(json.load(handle))
[docs] def to_json_file(self, file_path: str | Path, indent: int = 2) -> None: with open(file_path, "w", encoding="utf-8") as handle: json.dump(self.model_dump(mode="json"), handle, indent=indent) handle.write("\n")
[docs] def fingerprint(self) -> str: """Short hash of all values of the detector model (not the driver), e.g. for caches of quantities computed from it.""" payload = json.dumps(self.model_dump(mode="json", exclude={"driver"}), sort_keys=True) return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:12]
[docs] def get_calibrated_peak_shape_params(self, E, meas_mode: str): """ Peak shape parameters for incomplete charge collection (ICC) low-energy tailing. Parameters ---------- E : float or ndarray X-ray energy in keV. meas_mode : str EDS mode, must be in available_meas_modes. Returns ------- tuple (gamma, f_tail, R_e, F_loss), see PeakShapeParams. """ params = self.peak_shape.get(meas_mode) if params is None: raise ValueError(f"Missing peak shape parameters for meas_mode '{meas_mode}'. " "Add them to 'peak_shape' in the detector model of the microscope.") return params.gamma(E), params.f_tail(E), params.R_e(E), params.F_loss(E)
[docs] def get_calibrated_background_params(self, meas_mode: str): """ Parameters of the modified Duncumb background model. Parameters ---------- meas_mode : str EDS mode, must be in available_meas_modes. Returns ------- tuple (gen_bckgrnd_Duncumb_P, gen_bckgrnd_Duncumb_F, beta_expr) - gen_bckgrnd_Duncumb_P: str, expression for the background P parameter as a function of Z - gen_bckgrnd_Duncumb_F: str, expression for the background F parameter as a function of Z - beta_expr: sympy expression for the beta parameter as a function of Z """ params = self.background.get(meas_mode) if params is None: raise ValueError(f"Missing background parameters for meas_mode '{meas_mode}'. " "Add them to 'background' in the detector model of the microscope.") Z = sp.symbols('Z') a2, a1, a0 = params.duncumb_P gen_bckgrnd_Duncumb_P = str(a2 * Z**2 + a1 * Z + a0) gen_bckgrnd_Duncumb_F = str((1 - sp.exp(-(Z + params.duncumb_F.z_offset) / params.duncumb_F.z_scale))) beta = params.beta sigmoid_part = beta.sigmoid_offset + beta.sigmoid_amplitude / ( 1 + sp.exp(-beta.sigmoid_steepness * (Z - beta.sigmoid_center))) gaussian_part = beta.gaussian_amplitude * sp.exp( -((Z - beta.gaussian_center) ** 2) / (2 * beta.gaussian_sigma ** 2)) beta_expr = sigmoid_part + gaussian_part return gen_bckgrnd_Duncumb_P, gen_bckgrnd_Duncumb_F, beta_expr