Source code for autoemx.config.runtime_configs

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AutoEMX configuration dataclasses.

Created on Mon Jul 28 10:33:35 2025

@author: Andrea

This module provides configuration dataclasses for all stages of an automated X-ray spectroscopy workflow,
including microscope setup, sample and substrate definition, measurement and acquisition settings,
spectrum fitting, quantification and filtering, powder measurement, and plotting.

Configurations:

- MicroscopeConfig: Settings for microscope hardware, calibration, and imaging parameters.
- SampleConfig: Defines the sample’s composition/type and spatial properties.
- SampleSubstrateConfig: Specifies the substrate composition and geometry supporting the sample.
- MeasurementConfig: Controls measurement type, beam parameters, and acquisition settings.
- QuantificationOptionsConfig: Runtime options for spectral fitting and quantification.
- PowderMeasurementConfig: Settings for analyzing powder samples and particle selection.
- BulkMeasurementConfig: Settings for analyzing non-powder samples.
- PlotConfig: Options for saving, displaying, and customizing plots.

Each dataclass includes attribute documentation and input validation.
"""
import numpy as np
import multiprocessing
from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple

from pydantic import BaseModel, ConfigDict, Field, model_validator

from pymatgen.core.periodic_table import Element # type: ignore
from pymatgen.core import Composition # type: ignore

import autoemx.utils.constants as cnst
import autoemx.config.defaults as dflt
import autoemx.core.em_runtime.particle_segmentation_models as par_seg_models

[docs] class MicroscopeConfig(BaseModel): """ Configuration for the microscope hardware. Attributes: ID (str): Identifier for the microscope, defining instrument calibrations at ./XSp_calibs/Microscopes/ID. type (str): Type of microscope. Allowed: 'SEM' (implemented), 'STEM' (not implemented). is_auto_BC (bool): If True, brightness/contrast are set automatically. brightness (Optional[float]): Manual brightness value; required if is_auto_BC is False. contrast (Optional[float]): Manual contrast value; required if is_auto_BC is False. energy_zero (Optional[float]): Set from detector calibration files during spectral collection. bin_width (Optional[float]): Set from detector calibration files during spectral collection. Notes: - If `is_auto_BC` is False, both `brightness` and `contrast` must be provided. - STEM mode is not implemented and will raise NotImplementedError. - The microscope ID must correspond to a folder at ./XSp_calibs/Microscopes/ID containing all necessary calibration files. """ ID: str = dflt.microscope_ID type: str = dflt.microscope_type detector_type: str = dflt.detector_type is_auto_BC: bool = True brightness: Optional[float] = None contrast: Optional[float] = None energy_zero: Optional[float] = None bin_width: Optional[float] = None ALLOWED_TYPES: ClassVar[Tuple[str, ...]] = ("SEM", "STEM") ALLOWED_DETECTOR_TYPES: ClassVar[Tuple[str, ...]] = ("BSD",) model_config = ConfigDict(extra="forbid") @model_validator(mode="after") def _validate(self) -> "MicroscopeConfig": import os from pathlib import Path parent_dir = str(Path(__file__).resolve().parent.parent) calib_path = os.path.join(parent_dir, cnst.CALIBS_DIR, self.ID) if not os.path.isdir(calib_path): raise FileNotFoundError( f"Calibration folder for microscope ID '{self.ID}' not found at '{calib_path}'.\n" "Please add all necessary calibration files and ensure the folder is named with the same ID." ) if self.type not in self.ALLOWED_TYPES: raise ValueError(f"Microscope type must be one of {self.ALLOWED_TYPES}, got '{self.type}'.") if self.type == "STEM": raise NotImplementedError("STEM mode is not implemented yet.") if self.detector_type not in self.ALLOWED_DETECTOR_TYPES: raise ValueError(f"Detector type must be one of {self.ALLOWED_DETECTOR_TYPES}, got '{self.detector_type}'.") if self.is_auto_BC: # Do not persist manual BC values when auto BC is enabled. self.brightness = None self.contrast = None else: if self.brightness is None or self.contrast is None: raise ValueError( "If is_auto_BC is False, both brightness and contrast must be provided." ) return self
[docs] class SampleConfig(BaseModel): """ Configuration for the sample. Attributes: elements (List[str]): List of elemental symbols (e.g., ['Fe', 'O']). type (str): Sample type. Allowed types: - powder: Expects particles, and uses geometrical correction factors during spectral fits. - powder_continuous: Expects a quasi-continuous powder mixture, sampled in a grid (NO PARTICLE DETECTION APPLIED). Applies geometrical correction factors during spectral fits. - bulk: Expects a flat, continuous surface, sampled in a grid. Does not apply geometrical factors. - bulk_rough: Expects a continuous surface, sampled in a grid. Applies geometrical factors. - film: NOT IMPLEMENTED YET w_frs (Dict[str,float]): Dict of elemental mass fractions to be kept fixed (e.g., {'Fe': 0.4, 'O': 0.6}). Normally not used center_pos (Tuple[float, float]): (x, y) center position of the sample on the stage, in mm. half_width_mm (float): Half-width of the sample in millimeters. Notes: - Only 'powder' and 'bulk' type are implemented. 'film' will raise NotImplementedError. - Element symbols are validated. An error is raised if any symbol is unrecognized. """ elements: List[str] type: str = cnst.S_POWDER_SAMPLE_TYPE w_frs: Optional[Dict[str, float]] = None center_pos: Tuple[float, float] = (0.0, 0.0) # in mm half_width_mm: float = 2.9 # in mm ALLOWED_TYPES: ClassVar[Tuple[str, ...]] = ( cnst.S_POWDER_SAMPLE_TYPE, cnst.S_POWDER_CONTINUOUS_SAMPLE_TYPE, cnst.S_BULK_SAMPLE_TYPE, cnst.S_BULK_ROUGH_SAMPLE_TYPE, cnst.S_FILM_SAMPLE_TYPE, ) POWDER_SAMPLES_TYPES: ClassVar[List[str]] = [cnst.S_POWDER_SAMPLE_TYPE, cnst.S_POWDER_CONTINUOUS_SAMPLE_TYPE] is_powder_sample: bool = False # overwritten based on type ROUGH_SURFACE_TYPES: ClassVar[List[str]] = POWDER_SAMPLES_TYPES + [cnst.S_BULK_ROUGH_SAMPLE_TYPE] is_surface_rough: bool = False # overwritten based on type GRID_ACQUISITION_TYPES: ClassVar[Tuple[str, ...]] = ( cnst.S_BULK_SAMPLE_TYPE, cnst.S_POWDER_CONTINUOUS_SAMPLE_TYPE, cnst.S_BULK_ROUGH_SAMPLE_TYPE, cnst.S_FILM_SAMPLE_TYPE, ) is_grid_acquisition: bool = False # overwritten based on type PARTICLE_ACQUISITION_TYPES: ClassVar[str] = cnst.S_POWDER_SAMPLE_TYPE is_particle_acquisition: bool = False # overwritten based on type model_config = ConfigDict(extra="forbid") @model_validator(mode="after") def _validate(self) -> "SampleConfig": if self.type not in self.ALLOWED_TYPES: raise ValueError(f"Sample type must be one of {self.ALLOWED_TYPES}, got '{self.type}'.") if self.type in (cnst.S_FILM_SAMPLE_TYPE): raise NotImplementedError(f"Sample type '{self.type}' is not implemented yet.") for symbol in self.elements: try: Element(symbol) except Exception: raise ValueError(f"Element symbol '{symbol}' is not a recognized element.") self.is_powder_sample = self.type in self.POWDER_SAMPLES_TYPES self.is_surface_rough = self.type in self.ROUGH_SURFACE_TYPES self.is_grid_acquisition = self.type in self.GRID_ACQUISITION_TYPES self.is_particle_acquisition = self.type in self.PARTICLE_ACQUISITION_TYPES return self
[docs] class SampleSubstrateConfig(BaseModel): """ Configuration for the sample substrate. Attributes: elements (List[str]): List of element symbols present in the sample substrate. type (str): Type of the sample substrate. Allowed values: 'Ctape'. shape (str): Shape of the sample substrate. Allowed values: 'circle', 'rectangle'. auto_detection (bool): Whether to attempt automatic detection of substrate. (implemented only for type = Ctape & shape = 'circle') stub_w_mm (float): Lateral dimension of substrate holder in mm, used for determining image size for auto_detection. Notes: - Element symbols are validated. An error is raised if any symbol is unrecognized. """ elements: List[str] = Field(default_factory=lambda: ['C', 'O', 'Al']) type: str = cnst.CTAPE_SUBSTRATE_TYPE shape: str = cnst.CIRCLE_SUBSTRATE_SHAPE auto_detection: bool = True stub_w_mm: float = 12 ALLOWED_TYPES: ClassVar[Tuple[str, ...]] = (cnst.CTAPE_SUBSTRATE_TYPE, cnst.NONE_SUBSTRATE_TYPE) ALLOWED_SHAPES: ClassVar[Tuple[str, ...]] = (cnst.CIRCLE_SUBSTRATE_SHAPE, cnst.SQUARE_SUBSTRATE_SHAPE) ALLOWED_AUTO_DETECTION_TYPES: ClassVar[str] = cnst.CTAPE_SUBSTRATE_TYPE model_config = ConfigDict(extra="forbid") @model_validator(mode="after") def _validate(self) -> "SampleSubstrateConfig": if self.type not in self.ALLOWED_TYPES: raise ValueError(f"SampleSubstrate type must be one of {self.ALLOWED_TYPES}") if self.shape not in self.ALLOWED_SHAPES: raise ValueError(f"SampleSubstrate shape must be one of {self.ALLOWED_SHAPES}") if self.auto_detection and self.type != cnst.CTAPE_SUBSTRATE_TYPE: raise NotImplementedError(f"auto_detection is only implemented for types {self.ALLOWED_AUTO_DETECTION_TYPES}.") for symbol in self.elements: try: Element(symbol) except Exception: raise ValueError(f"Element symbol '{symbol}' is not a recognized element.") return self
[docs] class MeasurementConfig(BaseModel): """ Configuration for the measurement/acquisition session. Attributes: type (str): Measurement type. Allowed: 'EDS' (implemented), 'WDS' (not implemented). mode (str): Measurement mode (e.g., 'point'). Defines set of measurement parameters (i.e., beam current), determining detector calibration parameters working_distance (Optional[float]): Working distance to use for current measurement, in mm. Takes it from EM_driver if left unspecified. working_distance_tolerance (Optional[float]): Defines maximum accepted deviation of working distance from its typical value, in mm. Used to prevent gross mistakes from EM autofocus. Default: 1 mm. beam_energy_keV (float): Electron beam energy in keV. beam_current (Optional[float]): Beam current; must be provided at initialization or via detector channel calibration file. emergence_angle (Optional[float]): Emergence angle; updated from microscope driver file if not provided. is_manual_navigation (bool): If True, instrument navigation is performed manually. max_acquisition_time (float): Maximum X-ray spectral acquisition time in seconds. target_acquisition_counts (int): Target number of counts for acquisition of X-ray spectrum. min_n_spectra (int): Minimum number of spectra to acquire. max_n_spectra (int): Maximum number of spectra to acquire. powder_meas_cfg (Optional[PowderMeasurementConfig]): Powder acquisition settings. bulk_meas_cfg (Optional[BulkMeasurementConfig]): Bulk/grid acquisition settings. exp_stds_cfg (Optional[ExpStandardsConfig]): Experimental standards settings. saved_images_extension (str): Extension used when saving SEM frame images. save_raw_images (bool): Whether to save the non-annotated SEM image. """ type: str = dflt.measurement_type mode: str = dflt.measurement_mode working_distance: Optional[float] = None # mm working_distance_tolerance: float = 1 # mm beam_energy_keV: float = 15.0 # in keV beam_current: Optional[float] = None emergence_angle: Optional[float] = None is_manual_navigation: bool = False max_acquisition_time: float = 30.0 # seconds target_acquisition_counts: int = 50000 min_n_spectra: int = 30 max_n_spectra: int = 100 powder_meas_cfg: Optional["PowderMeasurementConfig"] = Field(default_factory=lambda: PowderMeasurementConfig()) bulk_meas_cfg: Optional["BulkMeasurementConfig"] = Field(default_factory=lambda: BulkMeasurementConfig()) exp_stds_cfg: Optional["ExpStandardsConfig"] = None saved_images_extension: str = dflt.saved_images_extension save_raw_images: bool = dflt.save_raw_images ALLOWED_IMAGE_EXTENSIONS: ClassVar[Tuple[str, ...]] = ( "tif", "tiff", "png", "jpg", "jpeg", "webp", "bmp", ) PARTICLE_STATS_MEAS_TYPE_KEY: ClassVar[str] = "particle_stats" ALLOWED_TYPES: ClassVar[Tuple[str, ...]] = ("EDS", "WDS", "particle_stats") model_config = ConfigDict(extra="forbid") @model_validator(mode="after") def _validate(self) -> "MeasurementConfig": if self.type not in self.ALLOWED_TYPES: raise ValueError(f"Measurement type must be one of {self.ALLOWED_TYPES}, got '{self.type}'.") if self.type == "WDS": raise NotImplementedError("WDS measurement type is not implemented yet.") ext = str(self.saved_images_extension).strip().lower().lstrip(".") if ext not in self.ALLOWED_IMAGE_EXTENSIONS: raise ValueError( f"saved_images_extension must be one of {self.ALLOWED_IMAGE_EXTENSIONS}, got '{self.saved_images_extension}'." ) self.saved_images_extension = ext return self
[docs] class QuantificationOptionsConfig(BaseModel): """ Configuration for X-ray spectrum fitting and quantification. Attributes: method (str): Method to use for quantification. Currently only accepts 'PB' spectrum_lims (Tuple[float, float]): Lower and upper spectral index limits. fit_tolerance (float): lmfit tolerance for fit convergence use_instrument_background (bool): Whether to use the instrument background in the fit (Default: False). If False, AutoEMX computes the background while fitting. use_project_specific_std_dict (bool): If True, tries to load the dictionary of reference standards from the project folder. If not found, uses the default file "EDS_Stds_{beamenergy}keV.json" at XSp_calibs/Microscopes/your_microscope. """ method: str = dflt.quantification_method spectrum_lims: Tuple[float, float] = dflt.spectrum_lims fit_tolerance: float = 1e-4 use_instrument_background: bool = dflt.use_instrument_background use_project_specific_std_dict: bool = False ALLOWED_METHODS: ClassVar[List[str]] = ['PB'] model_config = ConfigDict(extra="forbid") @model_validator(mode="before") @classmethod def _normalize_legacy_fields(cls, data: Any) -> Any: if isinstance(data, dict): cleaned = dict(data) cleaned.pop("interrupt_fits_bad_spectra", None) cleaned.pop("min_bckgrnd_cnts", None) cleaned.pop("is_particle", None) cleaned.pop("beam_energy_keV", None) cleaned.pop("emergence_angle", None) cleaned.pop("det_ch_offset", None) cleaned.pop("det_ch_width", None) return cleaned return data @model_validator(mode="after") def _validate(self) -> "QuantificationOptionsConfig": if self.method not in self.ALLOWED_METHODS: raise ValueError( f"Quantification method must be one of {self.ALLOWED_METHODS}, got '{self.method}'." "Currently no other method is implemented." ) return self
[docs] class PowderMeasurementConfig(BaseModel): """ Configuration for powder measurement. Attributes: par_selection_mode (str): 'auto' for automatic particle navigation, 'manual' to prompt user to center each particle (default: 'auto'). is_known_powder_mixture_meas (bool): Whether sample is a known binary mixture of powders. Used to characterize precursor extent of intermixing (Default = False). img_shift_tracking (bool): Whether to use image shift tracking during acquisition (Default = True). par_search_frame_width_um (float, optional): Frame width used when searching for particles, in um. Default: min(20*max_par_radius, 500 um) max_n_par_per_frame (int): Maximum number of particles analyzed in a single frame. Used to ensure spatial representation of the analyzed sample. max_spectra_per_par (int): Maximum number of spot X-ray spectra collected in a single particle. Limiting this ensures more particles are analyzed. max_area_par (float): Maximum area (in µm²) for a particle to be considered. min_area_par (float): Minimum area (in µm²) for a particle to be considered. par_mask_margin (float): Margin (in µm) from particle edge where X-ray spectra should not be collected. xsp_spots_distance_um (float): Min distance between X-ray spectrum acquisition points par_segmentation_model (str) : Model to use for particle segmentation. Default: "threshold_bright" par_brightness_thresh (int): Intensity threshold in 8-bit image that defines a particle over a dark background. par_xy_spots_thresh (int): Intensity threshold in 8-bit image that defines bright (i.e., thickest) regions in particles. X-ray spectra are acquired only from these regions. Particle pixel intensities are scaled to 8-bit prior threhsolding, i.e., darkest pixel will be set to 0, and brightest to 255. par_feature_selection (str): 'random' for random selection of points within bright regions, 'peaks' for brightest peak spots (default: 'random'). par_spot_spacing (str): 'random' for unbiased spot selecton, 'maximized' for maximized spot spacing over particle (default: 'random'). par_spot_selection_mode (str): 'auto' for built-in spot selection, 'callback' to supply spots via xsp_spot_selector (default: 'auto'). """ DEFAULT_PAR_SEGMENTATION_MODEL: ClassVar[str] = "threshold_bright" AVAILABLE_SPOT_SELECTION_MODES: ClassVar[Tuple[str, ...]] = ('auto', 'callback') AVAILABLE_PAR_SELECTION: ClassVar[Tuple[str, ...]] = ('auto', 'manual') par_selection_mode: Literal['auto', 'manual'] = 'auto' is_known_powder_mixture_meas: bool = False img_shift_tracking: bool = True par_search_frame_width_um: Optional[float] = None max_n_par_per_frame: int = 30 max_spectra_per_par: int = 3 max_area_par: float = 300.0 # µm² min_area_par: float = 10.0 # µm² par_mask_margin: float = 1.0 # µm xsp_spots_distance_um: float = 1.0 # µm par_spot_selection_mode: Literal['auto', 'callback'] = 'auto' par_segmentation_model: str = DEFAULT_PAR_SEGMENTATION_MODEL # "threshold_bright" par_brightness_thresh: int = 100 # in 8-bit image par_xy_spots_thresh: int = 100 par_feature_selection: str = 'random' par_spot_spacing: str = 'random' AVAILABLE_PAR_SEGMENTATION_MODELS: ClassVar[List[str]] = ["threshold_bright"] + par_seg_models.AVAILABLE_SEGMENTATION_MODELS AVAILABLE_FEATURE_SELECTION: ClassVar[Tuple[str, ...]] = ('random', 'peaks') AVAILABLE_SPOT_SPACING_SELECTION: ClassVar[Tuple[str, ...]] = ('random', 'maximized') model_config = ConfigDict(extra="forbid") @model_validator(mode="before") @classmethod def _normalize_legacy_fields(cls, data: Any) -> Any: if isinstance(data, dict): cleaned = dict(data) legacy_manual = cleaned.pop("is_manual_particle_selection", None) if legacy_manual is not None and "par_selection_mode" not in cleaned: cleaned["par_selection_mode"] = "manual" if legacy_manual else "auto" return cleaned return data @model_validator(mode="after") def _validate(self) -> "PowderMeasurementConfig": if self.par_segmentation_model not in self.AVAILABLE_PAR_SEGMENTATION_MODELS: raise ValueError( f'Value of "par_segmentation_model" set to {self.par_segmentation_model} is invalid. ' f'Must be one of {self.AVAILABLE_PAR_SEGMENTATION_MODELS}.' ) if self.par_feature_selection not in self.AVAILABLE_FEATURE_SELECTION: raise ValueError( f'Value of "par_feature_selection" set to {self.par_feature_selection} is invalid. ' f'Must be one of {self.AVAILABLE_FEATURE_SELECTION}.' ) if self.par_spot_spacing not in self.AVAILABLE_SPOT_SPACING_SELECTION: raise ValueError( f'Value of "par_spot_spacing" set to {self.par_spot_spacing} is invalid. ' f'Must be one of {self.AVAILABLE_SPOT_SPACING_SELECTION}.' ) if self.par_spot_selection_mode not in self.AVAILABLE_SPOT_SELECTION_MODES: raise ValueError( f'Value of "par_spot_selection_mode" set to {self.par_spot_selection_mode} is invalid. ' f'Must be one of {self.AVAILABLE_SPOT_SELECTION_MODES}.' ) if self.par_selection_mode not in self.AVAILABLE_PAR_SELECTION: raise ValueError( f'Value of "par_selection_mode" set to {self.par_selection_mode} is invalid. ' f'Must be one of {self.AVAILABLE_PAR_SELECTION}.' ) if self.min_area_par < 0 or self.max_area_par < 0: raise ValueError("Particle area thresholds must be non-negative.") if self.max_area_par < self.min_area_par: raise ValueError("max_area_par must be greater than or equal to min_area_par.") if self.max_n_par_per_frame <= 0: raise ValueError("max_n_par_per_frame must be positive.") if self.max_spectra_per_par <= 0: raise ValueError("max_spectra_per_par must be positive.") if self.par_mask_margin < 0: raise ValueError("par_mask_margin must be non-negative.") if not (0 <= self.par_brightness_thresh <= 255): raise ValueError("par_brightness_thresh must be in 0..255.") if not (0 <= self.par_xy_spots_thresh <= 255): raise ValueError("par_xy_spots_thresh must be in 0..255.") if self.par_search_frame_width_um is None: max_par_radius = np.sqrt(self.max_area_par / np.pi) # in µm self.par_search_frame_width_um = min(20 * max_par_radius, 500.0) # µm return self
[docs] class BulkMeasurementConfig(BaseModel): """ Configuration for characterization or bulk-like samples. Attributes ---------- grid_spot_spacing_um : float Distance between grid points to measure, in micrometers (µm). min_xsp_spots_distance_um : float Offset distance for acquisition spot grid if the original grid does not contain enough spots to measure the required number of spectra, in micrometers (µm). image_frame_width_um : float, optional Width of the image frame in micrometers (µm). If not specified, defaults to 10 × grid_spot_spacing_um. randomize_frames : bool Whether to randomize the order of spectra acquisition in the constructed grid. exclude_sample_margin : bool Whether to exclude the margin of the sample (useful if contaminated). """ grid_spot_spacing_um: float = 100.0 # µm min_xsp_spots_distance_um: float = 5.0 # µm image_frame_width_um: Optional[float] = None # µm randomize_frames: bool = False exclude_sample_margin: bool = False model_config = ConfigDict(extra="forbid") @model_validator(mode="after") def _validate(self) -> "BulkMeasurementConfig": if not (self.grid_spot_spacing_um > 0): raise ValueError("grid_spot_spacing_um must be positive.") if not (self.min_xsp_spots_distance_um > 0): raise ValueError("min_xsp_spots_distance_um must be positive.") if self.min_xsp_spots_distance_um > self.grid_spot_spacing_um: raise ValueError("min_xsp_spots_distance_um should not exceed grid_spot_spacing_um.") if self.image_frame_width_um is None: self.image_frame_width_um = 10 * self.grid_spot_spacing_um return self
[docs] class ExpStandardsConfig(BaseModel): """ Configuration for the collection of experimental standards. Attributes: is_exp_std_measurement (bool): Whether the configuration corresponds to the measurement of an experimental standard (Default = False) If True, a valid `formula` must be provided and weight fractions will be automatically calculated. formula (str): Chemical formula of the experimental standard. Required if `is_exp_std_measurement` is True. Must be parseable by `pymatgen.core.Composition`. els_to_use_for_mean_PB_calc (Optional[List[str]]): List of element symbols (e.g., ["Fe", "O"]) to use for mean PB calculation. Special values: ["all"] or ["All"]: use all elements/lines as standards (default) None, [], ["none"], or ["None"]: use no elements/lines generate_separate_std_dict (bool): Whether the acquired reference standard values are added to the current reference dictionary. If True, copies the current standard dictionary to the project folder and updates it. If such .json file is already present in the project folder, then it updates it. This is generally used when measuring the extent of powder precursor intermixing (i.e., powder_meas_cfg_kwargs["is_known_powder_mixture_meas"] = True). min_acceptable_PB_ratio (float): Minimum PB ratio required for a peak to be accepted as a standard. in cnts/cnts*keV^-1 (Default = 10). quant_flags_accepted (List[int]): List of quantification flags considered acceptable. Other spectra are filtered out before clustering. Quantification flags indicate whether the quantification or the fit of each spectrum is likely to be affected by large errors: - 0 : Quantification is ok, although it may be affected by large analytical error. - -1 : As above, but quantification did not converge within 30 steps. - 1 : Error during EDS acquisition. No fit executed. 2 : Total counts < 90% of target counts, likely due to wrong segmentation. Fit interrupted if interrupt_fits_bad_spectra=True. - 3 : Too little low-energy signal, causing poor quantification in that region. Fit interrupted if interrupt_fits_bad_spectra=True. - 4 : Poor fit. Fit interrupted if interrupt_fits_bad_spectra=True. - 5 : High analytical error (>50%), possibly due to missing element or other major error. Fit interrupted if interrupt_fits_bad_spectra=True. - 6 : Excessive X-ray absorption. Fit interrupted if interrupt_fits_bad_spectra=True. - 7 : Excessive contamination from substrate. - 8 : Too few background counts below reference peak, likely leading to large quantification errors. - 9 : Unknown fitting error. - 10 : (Only for measurement of experimental standards) Reference peak missing. w_frs (Optional[Dict[str, float]]): Dictionary of element symbols and their corresponding weight fractions (computed via pymatgen) if `is_exp_std_measurement` is True and `formula` is valid; otherwise None. Raises: ValueError: If `is_exp_std_measurement` is True but `formula` is missing or invalid. """ is_exp_std_measurement: bool = False formula: str = '' els_to_use_for_mean_PB_calc: Optional[List[str]] = ["all"] generate_separate_std_dict: bool = False min_acceptable_PB_ratio: float = 10 quant_flags_accepted: List[int] = Field(default_factory=lambda: [0]) w_frs: Optional[Dict[str, float]] = None model_config = ConfigDict(extra="forbid") @model_validator(mode="before") @classmethod def _normalize_legacy_els_to_use(cls, data: Any) -> Any: # Legacy compatibility: convert bool to list or None if isinstance(data, dict): val = data.get("use_for_mean_PB_calc", None) if isinstance(val, bool): if val: data["els_to_use_for_mean_PB_calc"] = ["all"] else: data["els_to_use_for_mean_PB_calc"] = None # Remove legacy key if present data.pop("use_for_mean_PB_calc", None) return data @model_validator(mode="after") def _validate(self) -> "ExpStandardsConfig": # Normalize els_to_use_for_mean_PB_calc v = self.els_to_use_for_mean_PB_calc if v is None or v == [] or v == ["none"] or v == ["None"]: self.els_to_use_for_mean_PB_calc = None elif v == ["all"] or v == ["All"]: self.els_to_use_for_mean_PB_calc = ["all"] elif not isinstance(v, list): raise ValueError("els_to_use_for_mean_PB_calc must be a list of element symbols, ['all'], or None.") # Validate elements if not special values if self.els_to_use_for_mean_PB_calc not in (None, ["all"]): for el in self.els_to_use_for_mean_PB_calc: try: Element(el) except Exception: raise ValueError(f"Element symbol '{el}' in els_to_use_for_mean_PB_calc is not a recognized element.") if self.is_exp_std_measurement: if not self.formula: raise ValueError("Formula must be provided when is_exp_std_measurement is True.") try: comp = Composition(self.formula) try: self.w_frs = {el: float(w) for el, w in comp.as_weight_dict.items()} # type: ignore except Exception: import warnings with warnings.catch_warnings(): warnings.simplefilter("ignore", category=FutureWarning) self.w_frs = {el: float(w) for el, w in comp.to_weight_dict.items()} except Exception as e: raise ValueError(f"Invalid chemical formula '{self.formula}': {e}") return self
[docs] class PlotConfig(BaseModel): """ Configuration for plotting. Attributes: show_unused_comps_clust (bool): Whether to plot unused data points in clustering plot. els_excluded_clust_plot (List[str]): Elements to exclude in cluster plot when more than 3 elements are present. show_legend_clustering bool : Whether to show the legend in the clustering plot. Default: True save_plots (bool): Whether to save plots to disk. show_plots (bool): Whether to display plots interactively. use_custom_plots (bool): Whether to use custom plotting routines. custom_plot_file (Optional[str]): Path to a user-editable custom plotting file. """ show_unused_comps_clust: bool = True els_excluded_clust_plot: List[str] = Field(default_factory=list) show_legend_clustering: bool = True save_plots: bool = True show_plots: bool = False use_custom_plots: bool = False custom_plot_file: Optional[str] = None model_config = ConfigDict(extra="forbid")
# Dictionary of all config models. Loaded for data import config_classes_dict = { cnst.SAMPLE_CFG_KEY: SampleConfig, cnst.MICROSCOPE_CFG_KEY: MicroscopeConfig, cnst.MEASUREMENT_CFG_KEY: MeasurementConfig, cnst.SAMPLESUBSTRATE_CFG_KEY: SampleSubstrateConfig, cnst.QUANTIFICATION_CFG_KEY: QuantificationOptionsConfig, cnst.PLOT_CFG_KEY: PlotConfig, cnst.POWDER_MEASUREMENT_CFG_KEY: PowderMeasurementConfig, cnst.BULK_MEASUREMENT_CFG_KEY: BulkMeasurementConfig, cnst.EXP_STD_MEASUREMENT_CFG_KEY: ExpStandardsConfig, }