44Automated SDD Detector Calibration
55
66This script automates the collection and fitting of X-ray spectra from experimental standards
7- to generate an SDD calibration file for AutoEMXsp .
7+ to generate an SDD calibration file for AutoEMX .
88
99Requirements:
10- - Proper instrument calibration files and instrument driver for the selected microscope
10+ - Proper instrument calibration files and instrument driver for the selected microscope
1111
1212Typical Usage:
1313 - Edit the 'std_list' list to define your standards.
1616 - ref_el: Element in the composition whose peak should be taken as reference to calibrate the SDD.
1717 - ref_peak: Characteristic X-ray to take as reference to calibrate the SDD (e.g., Ka1, La1, Ma1, Mz1).
1818 - pos: Position of standard in the microscope stage.
19- - sample_type: 'bulk' or 'powder'. Check autoemx.tools.config_classes .SampleConfig for updated support.
19+ - sample_type: 'bulk' or 'powder'. Check autoemx.config.runtime_configs .SampleConfig for updated support.
2020 - is_manual_meas: Set to True to manually select spots to measure.
2121
2222 - Suggestions:
2323 - Preferably use bulk standards. Powder standards are also acceptable if bulk standards are not available.
2424 - Choose peaks above 2 keV, well-distanced between themselves.
25+ - Exactly two standards are required (two-point energy calibration).
26+
27+ - Set reuse_session to None to acquire new spectra, or to an existing session folder
28+ name under SDD calibrations/ to skip acquisition and reuse prior ledgers.
2529
2630 - Run the script to collect and fit spectra, with automated generation of SDD calibration file.
27-
31+
2832Potential improvements:
29- - At the moment the script runs the spectral fitting twice, which is inefficient, but not really important given
30- that this code is not run very often.
33+ - At the moment the script may run spectral fitting twice when fit_during_collection is True
34+ and centers are still re-read from the ledger, which is inefficient but acceptable given
35+ that this code is not run very often.
3136
3237Created on Fri Aug 20 09:34:34 2025
3338
3439@author: Andrea
3540"""
3641import os
3742from datetime import datetime
38- import pandas as pd
43+
44+ import numpy as np
3945
4046from autoemx .runners .batch_acquire_experimental_stds import batch_acquire_experimental_stds
4147from autoemx .runners .batch_fit_spectra import batch_fit_spectra
4248from autoemx .data .Xray_lines import get_el_xray_lines
49+ from autoemx .config .ledger_io import load_sample_ledger
50+ from autoemx .config .runtime_configs import SampleConfig
4351import autoemx .calibrations as calibs
4452import autoemx .utils .constants as cnst
4553from autoemx .utils import print_double_separator
6169now_formatted = now .strftime ("%Y%m%d_%Hh%Mm" )
6270output_filename_suffix = f'_{ now_formatted } _50kcnts'
6371
72+ # Set to an existing session folder name under SDD calibrations/ to skip acquisition
73+ # (e.g. '20260521_09h22m'). None acquires new spectra into a timestamped session folder.
74+ reuse_session = None
75+
6476# =============================================================================
6577# Sample Definitions - Add two pure elements to measure
6678# =============================================================================
67- Cu_center = (37.863 ,38.195 )
79+ Cu_center = (37.863 , 38.195 )
6880std_list = [
69- {'ID' : 'Cu' ,'formula' : 'Cu' , 'ref_el' : 'Cu' , 'ref_peak' : 'Ka1' , 'pos' : Cu_center , 'sample_type' : 'bulk' , 'is_manual_meas' : False },
70- {'ID' : 'Al' ,'formula' : 'Al' , 'ref_el' : 'Al' , 'ref_peak' : 'Ka1' , 'pos' : tuple (a + b for a , b in zip (Cu_center , (5 , 0 ))), 'sample_type' : 'bulk' , 'is_manual_meas' : False }, # CAlibration standard center
81+ {'ID' : 'Cu' , 'formula' : 'Cu' , 'ref_el' : 'Cu' , 'ref_peak' : 'Ka1' , 'pos' : Cu_center , 'sample_type' : 'bulk' , 'is_manual_meas' : False },
82+ {'ID' : 'Al' , 'formula' : 'Al' , 'ref_el' : 'Al' , 'ref_peak' : 'Ka1' , 'pos' : tuple (a + b for a , b in zip (Cu_center , (5 , 0 ))), 'sample_type' : 'bulk' , 'is_manual_meas' : False },
7183]
7284
85+ if len (std_list ) != 2 :
86+ raise ValueError ("SDD energy calibration requires exactly two standards in std_list." )
87+
7388# =============================================================================
7489# Acquisition Options and Sample description
75- working_distance = 5.5 # mm
90+ working_distance = 5.5 # mm
7691is_auto_substrate_detection = False
7792
78- fit_during_collection = False
93+ is_manual_meas = False # Default navigation mode; per-sample 'is_manual_meas' overrides this.
94+
95+ fit_during_collection = False
7996update_std_library = False
8097
8198sample_substrate_type = 'None'
86103beam_energy = 15 # keV
87104
88105auto_adjust_brightness_contrast = True
89- contrast = None # 4.3877 # Used if auto_adjust_brightness_contrast = False
90- brightness = None # 0.4504 # Used if auto_adjust_brightness_contrast = False
91- saved_images_extension = 'png' # Default lightweight output. Set 'tif' for higher-resolution/larger files.
92- save_raw_images = False # Default saves only the annotated image. Set True to also save raw images.
106+ contrast = None # 4.3877 # Used if auto_adjust_brightness_contrast = False
107+ brightness = None # 0.4504 # Used if auto_adjust_brightness_contrast = False
108+ saved_images_extension = 'png' # Default lightweight output. Set 'tif' for higher-resolution/larger files.
109+ save_raw_images = False # Default saves only the annotated image. Set True to also save raw images.
93110
94111n_target_spectra = 5
95112max_n_spectra = 10
105122# =============================================================================
106123powder_meas_cfg_kwargs = dict (
107124 par_selection_mode = 'auto' ,
108- is_known_powder_mixture_meas = True ,
125+ is_known_powder_mixture_meas = True ,
126+ img_shift_tracking = True ,
109127 max_n_par_per_frame = 30 ,
110128 max_spectra_per_par = 3 ,
111129 max_area_par = 10000.0 ,
114132 xsp_spots_distance_um = 1.0 ,
115133 par_brightness_thresh = 100 ,
116134 par_xy_spots_thresh = 100 ,
117- par_feature_selection = 'peaks' ,
118- par_spot_spacing = 'random'
135+ par_feature_selection = 'peaks' ,
136+ par_spot_spacing = 'random' ,
119137)
120138
121139# =============================================================================
122140# Bulk options
123141# =============================================================================
124142bulk_meas_cfg_kwargs = dict (
125- grid_spot_spacing_um = 10.0 , # µm
126- min_xsp_spots_distance_um = 2.5 , # µm
127- randomize_frames = False ,
128- exclude_sample_margin = False
143+ grid_spot_spacing_um = 10.0 , # µm
144+ min_xsp_spots_distance_um = 2.5 , # µm
145+ image_frame_width_um = None , # µm
146+ randomize_frames = False ,
147+ exclude_sample_margin = False ,
129148)
130149
131150# =============================================================================
132151# Options for experimental standard collection
133152# =============================================================================
134153exp_stds_meas_cfg_kwargs = dict (
135- min_acceptable_PB_ratio = 10 ,
136- quant_flags_accepted = [0 ],
137- use_for_mean_PB_calc = False
154+ min_acceptable_PB_ratio = 10 ,
155+ quant_flags_accepted = [0 ],
156+ els_to_use_for_mean_PB_calc = ["none" ],
157+ generate_separate_std_dict = False ,
138158)
139159
140160# =============================================================================
141161# Run
142162# =============================================================================
143163# Load microscope calibrations for this instrument and mode
144164calibs .load_microscope_calibrations (microscope_ID , measurement_mode , load_detector_channel_params = True )
145- eds_calibration_path = os .path .join (calibs .calibration_files_dir , cnst .SDD_CALIBS_MEAS_DIR , now_formatted )
146-
147- # # --- Acquire and save spectra
148- # analyzers = batch_acquire_experimental_stds(
149- # stds=std_list,
150- # microscope_ID=microscope_ID,
151- # microscope_type=microscope_type,
152- # measurement_type=measurement_type,
153- # measurement_mode=measurement_mode,
154- # sample_halfwidth=sample_halfwidth,
155- # sample_substrate_type=sample_substrate_type,
156- # sample_substrate_shape=sample_substrate_shape,
157- # working_distance = working_distance,
158- # beam_energy=beam_energy,
159- # spectrum_lims=spectrum_lims,
160- # use_instrument_background=use_instrument_background,
161- # min_bckgrnd_cnts=min_bckgrnd_cnts,
162- # fit_during_collection= fit_during_collection,
163- # update_std_library = update_std_library,
164- # is_auto_substrate_detection=is_auto_substrate_detection,
165- # auto_adjust_brightness_contrast=auto_adjust_brightness_contrast,
166- # contrast=contrast,
167- # brightness=brightness,
168- # saved_images_extension=saved_images_extension,
169- # save_raw_images=save_raw_images,
170- # min_n_spectra=n_target_spectra,
171- # max_n_spectra=max_n_spectra,
172- # target_Xsp_counts=target_Xsp_counts,
173- # max_XSp_acquisition_time=max_XSp_acquisition_time,
174- # els_substrate=els_substrate,
175- # powder_meas_cfg_kwargs=powder_meas_cfg_kwargs,
176- # bulk_meas_cfg_kwargs=bulk_meas_cfg_kwargs,
177- # exp_stds_meas_cfg_kwargs=exp_stds_meas_cfg_kwargs,
178- # output_filename_suffix=output_filename_suffix,
179- # development_mode=False,
180- # verbose=True,
181- # exp_std_dir = eds_calibration_path
182- # )
183-
184- # std_paths = [an.sample_result_dir for an in analyzers]
185- std_paths = [
186- '/Users/Andrea_1/Desktop/Work/Codes/Repositories/AutoEMXSp/autoemx/calibrations/PhenomXL/Detector_channel_params_calibs/SDD calibrations/20260521_09h22m/Cu' ,
187- '/Users/Andrea_1/Desktop/Work/Codes/Repositories/AutoEMXSp/autoemx/calibrations/PhenomXL/Detector_channel_params_calibs/SDD calibrations/20260521_09h22m/Al'
188- ]
189165
190- # --- Ensure fit results exist in ledgers, launch fit if missing
191- from autoemx .config .ledger_io import load_sample_ledger
192- import numpy as np
166+ session_name = reuse_session if reuse_session else now_formatted
167+ eds_calibration_path = os .path .join (
168+ calibs .calibration_files_dir ,
169+ cnst .SDD_CALIBS_MEAS_DIR ,
170+ session_name ,
171+ )
172+
173+ if reuse_session :
174+ print_double_separator ()
175+ print (f"Reusing existing SDD calibration session: { eds_calibration_path } " )
176+ std_paths = [os .path .join (eds_calibration_path , std ['ID' ]) for std in std_list ]
177+ missing = [path for path in std_paths if not os .path .isdir (path )]
178+ if missing :
179+ raise FileNotFoundError (
180+ "reuse_session is set but the following standard folders are missing:\n "
181+ + "\n " .join (missing )
182+ )
183+ else :
184+ # --- Acquire and save spectra
185+ analyzers = batch_acquire_experimental_stds (
186+ stds = std_list ,
187+ microscope_ID = microscope_ID ,
188+ microscope_type = microscope_type ,
189+ measurement_type = measurement_type ,
190+ measurement_mode = measurement_mode ,
191+ sample_halfwidth = sample_halfwidth ,
192+ sample_substrate_type = sample_substrate_type ,
193+ sample_substrate_shape = sample_substrate_shape ,
194+ working_distance = working_distance ,
195+ beam_energy = beam_energy ,
196+ spectrum_lims = spectrum_lims ,
197+ use_instrument_background = use_instrument_background ,
198+ min_bckgrnd_cnts = min_bckgrnd_cnts ,
199+ is_manual_meas = is_manual_meas ,
200+ fit_during_collection = fit_during_collection ,
201+ update_std_library = update_std_library ,
202+ is_auto_substrate_detection = is_auto_substrate_detection ,
203+ auto_adjust_brightness_contrast = auto_adjust_brightness_contrast ,
204+ contrast = contrast ,
205+ brightness = brightness ,
206+ saved_images_extension = saved_images_extension ,
207+ save_raw_images = save_raw_images ,
208+ min_n_spectra = n_target_spectra ,
209+ max_n_spectra = max_n_spectra ,
210+ target_Xsp_counts = target_Xsp_counts ,
211+ max_XSp_acquisition_time = max_XSp_acquisition_time ,
212+ els_substrate = els_substrate ,
213+ powder_meas_cfg_kwargs = powder_meas_cfg_kwargs ,
214+ bulk_meas_cfg_kwargs = bulk_meas_cfg_kwargs ,
215+ exp_stds_meas_cfg_kwargs = exp_stds_meas_cfg_kwargs ,
216+ output_filename_suffix = output_filename_suffix ,
217+ development_mode = False ,
218+ verbose = True ,
219+ exp_std_dir = eds_calibration_path ,
220+ )
221+ if any (an is None for an in analyzers ):
222+ failed = [std ['ID' ] for std , an in zip (std_list , analyzers ) if an is None ]
223+ raise RuntimeError (f"Acquisition failed for standard(s): { failed } " )
224+ std_paths = [an .sample_result_dir for an in analyzers ]
225+
193226
194227def ledger_has_fit_results (ledger , ref_peak ):
195228 for spectrum in getattr (ledger , 'spectra' , []):
@@ -199,37 +232,42 @@ def ledger_has_fit_results(ledger, ref_peak):
199232 return True
200233 return False
201234
235+
202236extracted_par_vals = {}
203- sample_IDs = [std_d ['ID' ] for std_d in std_list ]
204237fit_params_vals_to_extract = [f"{ std_d ['ref_el' ]} _{ std_d ['ref_peak' ]} _center" for std_d in std_list ]
205238
206239for std , sample_path in zip (std_list , std_paths ):
207240 sample_ID = std ['ID' ]
208241 ref_peak = f"{ std ['ref_el' ]} _{ std ['ref_peak' ]} "
242+ is_particle = std ['sample_type' ] in SampleConfig .POWDER_SAMPLES_TYPES
209243 ledger_path = os .path .join (sample_path , cnst .LEDGER_FILENAME + cnst .LEDGER_FILEEXT )
244+ if not os .path .exists (ledger_path ):
245+ raise FileNotFoundError (f"Ledger not found for standard '{ sample_ID } ': { ledger_path } " )
210246 ledger = load_sample_ledger (ledger_path )
211247 if not ledger_has_fit_results (ledger , ref_peak ):
212248 batch_fit_spectra (
213249 [sample_ID ],
214250 'all' ,
215251 is_standard = True ,
216252 fit_params_vals_to_extract = fit_params_vals_to_extract ,
253+ spectrum_lims = spectrum_lims ,
217254 samples_path = eds_calibration_path ,
218- use_instrument_background = False ,
255+ use_instrument_background = use_instrument_background ,
219256 plot_signal = False ,
220257 zoom_plot = False ,
221258 line_to_plot = '' ,
222259 els_substrate = els_substrate ,
223260 fit_tol = 1e-4 ,
224- is_particle = True ,
261+ is_particle = is_particle ,
225262 max_undetectable_w_fr = 0 ,
226263 force_single_iteration = False ,
227264 interrupt_fits_bad_spectra = False ,
228265 print_results = False ,
229266 quant_verbose = True ,
230- fitting_verbose = False
267+ fitting_verbose = False ,
231268 )
232269 ledger = load_sample_ledger (ledger_path )
270+
233271 # Collect fit parameter values for this sample, only if quant_flag == 0
234272 centers = []
235273 for spectrum in getattr (ledger , 'spectra' , []):
@@ -264,13 +302,16 @@ def ledger_has_fit_results(ledger, ref_peak):
264302 raise RuntimeError (f"No valid fitted center values found for standard '{ std ['ID' ]} ' ({ param_name } )" )
265303 measured_means [param_name ] = meas_mean
266304
267- # Assign to calibration variables
268- x_measured_en = measured_means ["Cu_Ka1_center" ]
269- y_measured_en = measured_means ["Al_Ka1_center" ]
305+ # Two-point calibration from std_list order (first = x, second = y)
306+ std_x , std_y = std_list
307+ x_param = f"{ std_x ['ref_el' ]} _{ std_x ['ref_peak' ]} _center"
308+ y_param = f"{ std_y ['ref_el' ]} _{ std_y ['ref_peak' ]} _center"
309+ x_measured_en = measured_means [x_param ]
310+ y_measured_en = measured_means [y_param ]
270311
271312# Theoretical energies
272- x_th_en = get_el_xray_lines ("Cu" )[ "Ka1" ]["energy (keV)" ]
273- y_th_en = get_el_xray_lines ("Al" )[ "Ka1" ]["energy (keV)" ]
313+ x_th_en = get_el_xray_lines (std_x [ 'ref_el' ])[ std_x [ 'ref_peak' ] ]["energy (keV)" ]
314+ y_th_en = get_el_xray_lines (std_y [ 'ref_el' ])[ std_y [ 'ref_peak' ] ]["energy (keV)" ]
274315
275316# Calculate new calibration
276317i_x = (x_measured_en - current_energy_zero ) / current_bin_width
@@ -282,14 +323,11 @@ def ledger_has_fit_results(ledger, ref_peak):
282323new_scale = (Dx - Dy + x_measured_en - y_measured_en ) / (i_x - i_y )
283324new_offset = Dy + y_measured_en - i_y * new_scale
284325
285- print_double_separator
326+ print_double_separator ()
286327print (f"Current scale: { current_bin_width :.6f} " )
287328print (f"Current offset: { current_energy_zero :.6f} " )
288329print (f"New scale: { new_scale :.6f} " )
289330print (f"New offset: { new_offset :.6f} " )
290331
291332# Add calibration file
292333calibs .update_detector_channel_params (measurement_mode , new_offset , new_scale )
293-
294-
295-
0 commit comments