1+ """
2+ ALine3 electron beam size evaluator for optimization.
3+
4+ This module provides an evaluator for optimizing electron beam size on the ALine3
5+ diagnostic camera (UC_ALineEBeam3). The evaluator computes beam size metrics using
6+ full-width half-maximum (FWHM) measurements in both x and y directions, combining
7+ them into a single objective function suitable for automated optimization.
8+
9+ The evaluator integrates with the GEECS scan analysis framework to process beam
10+ profile images and extract quantitative beam size measurements with proper spatial
11+ calibration.
12+
13+ Classes
14+ -------
15+ ALine3SizeEval
16+ Evaluator for ALine3 electron beam size optimization.
17+
18+ Examples
19+ --------
20+ Basic usage in optimization:
21+
22+ >>> evaluator = ALine3SizeEval(
23+ ... device_requirements=device_reqs,
24+ ... scan_data_manager=sdm,
25+ ... data_logger=logger
26+ ... )
27+ >>> result = evaluator.get_value(input_parameters)
28+ >>> beam_size_metric = result['f']
29+
30+ Notes
31+ -----
32+ This evaluator requires the UC_ALineEBeam3 camera device and uses a spatial
33+ calibration of 24.4 μm/pixel for converting pixel measurements to physical units.
34+ The objective function combines x and y FWHM measurements as a sum of squares.
35+ """
36+
137from __future__ import annotations
238
3- import logging
439from typing import TYPE_CHECKING , Dict , Optional
40+
541if TYPE_CHECKING :
642 from geecs_scanner .data_acquisition .scan_data_manager import ScanDataManager
743 from geecs_scanner .data_acquisition .data_logger import DataLogger
844
9- import numpy as np
1045
1146from geecs_scanner .optimization .base_evaluator import BaseEvaluator
1247
13- from scan_analysis .base import ScanAnalyzerInfo
14- from scan_analysis .execute_scan_analysis import analyze_scan , instantiate_scan_analyzer
1548from scan_analysis .analyzers .common .array2D_scan_analysis import Array2DScanAnalyzer
1649from image_analysis .utils import read_imaq_image
1750from image_analysis .offline_analyzers .Undulator .EBeamProfile import EBeamProfileAnalyzer
18- from geecs_data_utils import ScanTag , ScanData
51+
1952
2053class ALine3SizeEval (BaseEvaluator ):
21- def __init__ (self , device_requirements = None ,
22- scan_data_manager : Optional [ScanDataManager ] = None ,
23- data_logger : Optional [DataLogger ] = None ):
54+ """
55+ Evaluator for ALine3 electron beam size optimization.
56+
57+ This evaluator computes electron beam size metrics from the UC_ALineEBeam3
58+ diagnostic camera using FWHM measurements. It integrates with the GEECS scan
59+ analysis framework to process beam profile images and extract quantitative
60+ measurements suitable for automated optimization.
61+
62+ The evaluator uses an Array2DScanAnalyzer with an EBeamProfileAnalyzer to
63+ process images and compute beam size statistics. The objective function
64+ combines x and y FWHM measurements with spatial calibration to produce
65+ a single metric for optimization.
2466
25- required_keys = {}
67+ Parameters
68+ ----------
69+ device_requirements : dict, optional
70+ Dictionary specifying required devices and variables. If None,
71+ defaults will be used for UC_ALineEBeam3 camera.
72+ scan_data_manager : ScanDataManager, optional
73+ Manager for accessing scan data and file paths.
74+ data_logger : DataLogger, optional
75+ Logger instance for accessing shot data and bin information.
76+
77+ Attributes
78+ ----------
79+ dev_name : str
80+ Name of the camera device ('UC_ALineEBeam3').
81+ scan_analyzer : Array2DScanAnalyzer
82+ Analyzer instance for processing beam profile images.
83+ output_key : str
84+ Key name for the optimization objective ('f').
85+ objective_tag : str
86+ Tag for logging objective values.
87+
88+ Methods
89+ -------
90+ evaluate_all_shots(shot_entries)
91+ Evaluate beam size for all shots in a data bin.
92+ objective_fn(x, y)
93+ Static method computing objective function from FWHM values.
94+
95+ Examples
96+ --------
97+ >>> evaluator = ALine3SizeEval(
98+ ... scan_data_manager=sdm,
99+ ... data_logger=logger
100+ ... )
101+ >>> result = evaluator.get_value({"focus_strength": 120.5})
102+ >>> beam_size_metric = result['f']
103+
104+ Notes
105+ -----
106+ - Requires UC_ALineEBeam3 camera device to be available
107+ - Uses spatial calibration of 24.4 μm/pixel
108+ - Objective function is sum of squares: (x_fwhm * cal)² + (y_fwhm * cal)²
109+ - Configured for live analysis during optimization scans
110+ """
111+
112+ def __init__ (
113+ self ,
114+ device_requirements = None ,
115+ scan_data_manager : Optional [ScanDataManager ] = None ,
116+ data_logger : Optional [DataLogger ] = None ,
117+ ):
118+ required_keys = {}
26119
27120 super ().__init__ (
28- device_requirements = device_requirements ,
29- required_keys = required_keys ,
30- scan_data_manager = scan_data_manager ,
31- data_logger = data_logger
121+ device_requirements = device_requirements ,
122+ required_keys = required_keys ,
123+ scan_data_manager = scan_data_manager ,
124+ data_logger = data_logger ,
32125 )
33126
34- self .dev_name = ' UC_ALineEBeam3'
35- config_dict = {' camera_name' : self .dev_name }
127+ self .dev_name = " UC_ALineEBeam3"
128+ config_dict = {" camera_name" : self .dev_name }
36129 self .scan_analyzer = Array2DScanAnalyzer (
37- device_name = self .dev_name ,
38- image_analyzer = EBeamProfileAnalyzer (** config_dict )
39- )
130+ device_name = self .dev_name ,
131+ image_analyzer = EBeamProfileAnalyzer (** config_dict ),
132+ )
40133
41134 # use live_analysis option for the scan_analyzer so that it knows not to try to load
42135 # data from an sFile already written to disk (which doesn't happen until the end of scan)
43136 self .scan_analyzer .live_analysis = True
44137 self .scan_analyzer .use_colon_scan_param = False # default is file-style
45138
46- self .output_key = 'f' # string name of optimization function defined in config, don't change
47- self .objective_tag : str = 'PlaceHolder' # string to append to logged objective value
139+ self .output_key = (
140+ "f" # string name of optimization function defined in config, don't change
141+ )
142+ self .objective_tag : str = (
143+ "PlaceHolder" # string to append to logged objective value
144+ )
48145
49- def evaluate_all_shots (self , shot_entries : list [dict ]) -> list [ float ] :
146+ def evaluate_all_shots (self , shot_entries : list [dict ]) -> float :
50147 """
51- Evaluate the objective function for a list of shots .
148+ Evaluate beam size objective function for all shots in current bin .
52149
53- Args:
54- shot_entries (list[dict]): List of shot_entry dicts as produced by _gather_shot_entries.
150+ Processes all shots in the current data bin to compute an aggregate
151+ beam size metric. Uses the scan analyzer to create an averaged image
152+ from all shots, then analyzes the averaged image to extract FWHM
153+ measurements and compute the objective function.
55154
56- Returns:
57- list[float]: List of scalar objective values, one per shot.
58- """
155+ Parameters
156+ ----------
157+ shot_entries : list of dict
158+ List of shot entry dictionaries as produced by _gather_shot_entries.
159+ Each dictionary contains shot number, scalars, and image paths.
160+
161+ Returns
162+ -------
163+ float
164+ Computed objective value combining x and y FWHM measurements
165+ with spatial calibration.
59166
167+ Notes
168+ -----
169+ This method:
170+ 1. Sets auxiliary data for the scan analyzer
171+ 2. Runs analysis to create averaged image
172+ 3. Loads the averaged image from disk
173+ 4. Analyzes the averaged image for beam profile
174+ 5. Extracts FWHM measurements
175+ 6. Computes objective function
176+ 7. Logs results for each shot
177+
178+ The objective function is: (x_fwhm * cal)² + (y_fwhm * cal)²
179+ where cal = 24.4e-3 mm/pixel.
180+
181+ Examples
182+ --------
183+ >>> shot_entries = evaluator._gather_shot_entries(...)
184+ >>> objective_value = evaluator.evaluate_all_shots(shot_entries)
185+ >>> print(f"Beam size metric: {objective_value:.3f}")
186+ """
60187 # set the 'aux' data manually to isolate the current bin to get analyzed by the ScanAnalyzer
61188 self .scan_analyzer .auxiliary_data = self .current_data_bin
62189 self .scan_analyzer .run_analysis (scan_tag = self .scan_tag )
63190
64191 # grab the path to the saved average image from the scan analyzer and load
65- avg_image_path = self .scan_analyzer .saved_avg_image_paths [self .bin_number ]
192+ avg_image_path = self .scan_analyzer .saved_avg_image_paths [self .bin_number ]
66193 avg_image = read_imaq_image (avg_image_path )
67194
68195 # run standalone analyis using the image_analyzer, passing the argument that preprocessing
69196 # has already been done, e.g. ROI, background etc.
70- result = self .scan_analyzer .image_analyzer .analyze_image (avg_image ,
71- auxiliary_data = {"preprocessed" :True })
197+ result = self .scan_analyzer .image_analyzer .analyze_image (
198+ avg_image , auxiliary_data = {"preprocessed" : True }
199+ )
72200
73201 # extract the scalar results returned by the image analyzer
74- scalar_results = result [' analyzer_return_dictionary' ]
202+ scalar_results = result [" analyzer_return_dictionary" ]
75203
76204 # define keys to extract values to use for the objective function
77- x_key = f' { self .dev_name } _x_fwhm'
78- y_key = f' { self .dev_name } _y_fwhm'
205+ x_key = f" { self .dev_name } _x_fwhm"
206+ y_key = f" { self .dev_name } _y_fwhm"
79207
80- objective_value = self .objective_fn ( x = scalar_results [x_key ],
81- y = scalar_results [y_key ])
208+ objective_value = self .objective_fn (
209+ x = scalar_results [x_key ], y = scalar_results [y_key ]
210+ )
82211
83212 for shot_entry in shot_entries :
84- self .log_objective_result (shot_num = shot_entry ['shot_number' ],scalar_value = objective_value )
213+ self .log_objective_result (
214+ shot_num = shot_entry ["shot_number" ], scalar_value = objective_value
215+ )
85216
86217 return objective_value
87218
88219 @staticmethod
89220 def objective_fn (x , y ):
90- calibration = 24.4e-3 # spatial calibration in um/pixel
91- return (x * calibration )** 2 + (y * calibration )** 2
221+ """
222+ Compute beam size objective function from FWHM measurements.
223+
224+ Combines x and y full-width half-maximum measurements with spatial
225+ calibration to produce a single objective metric for optimization.
226+ The function computes the sum of squares of the calibrated FWHM values.
227+
228+ Parameters
229+ ----------
230+ x : float
231+ X-direction FWHM measurement in pixels.
232+ y : float
233+ Y-direction FWHM measurement in pixels.
234+
235+ Returns
236+ -------
237+ float
238+ Objective function value: (x*cal)² + (y*cal)² where
239+ cal = 24.4e-3 mm/pixel is the spatial calibration.
240+
241+ Examples
242+ --------
243+ >>> x_fwhm = 50.2 # pixels
244+ >>> y_fwhm = 45.8 # pixels
245+ >>> objective = ALine3SizeEval.objective_fn(x_fwhm, y_fwhm)
246+ >>> print(f"Beam size metric: {objective:.6f}")
247+
248+ Notes
249+ -----
250+ The spatial calibration of 24.4 μm/pixel converts pixel measurements
251+ to physical units. The sum of squares formulation treats x and y
252+ contributions equally and provides a single metric that decreases
253+ as the beam becomes more focused.
254+ """
255+ calibration = 24.4e-3 # spatial calibration in um/pixel
256+ return (x * calibration ) ** 2 + (y * calibration ) ** 2
92257
93258 def _get_value (self , input_data : Dict ) -> Dict :
259+ """
260+ Evaluate beam size objective for current optimization step.
261+
262+ This is the main evaluation method called by the optimization framework.
263+ It gathers shot data from the current bin, processes the beam profile
264+ images, and returns the computed objective function value.
265+
266+ Parameters
267+ ----------
268+ input_data : dict
269+ Dictionary of input parameter values for the current optimization
270+ step. Not directly used by this evaluator as it operates on
271+ acquired image data.
94272
95- shot_entries = self ._gather_shot_entries ( shot_numbers = self .current_shot_numbers ,
96- scalar_variables = self .required_keys ,
97- non_scalar_variables = ['UC_ALineEBeam3' ])
273+ Returns
274+ -------
275+ dict
276+ Dictionary containing the objective function result with key 'f'.
277+
278+ Examples
279+ --------
280+ >>> input_params = {"focus_strength": 120.5, "beam_energy": 150.0}
281+ >>> result = evaluator._get_value(input_params)
282+ >>> print(result)
283+ {'f': 0.00234}
284+
285+ Notes
286+ -----
287+ This method:
288+ 1. Gathers shot entries for the current data bin
289+ 2. Processes all shots to compute beam size metric
290+ 3. Returns result in format expected by optimization framework
291+
292+ The 'f' key in the returned dictionary corresponds to the objective
293+ function name defined in the optimization configuration.
294+ """
295+ shot_entries = self ._gather_shot_entries (
296+ shot_numbers = self .current_shot_numbers ,
297+ scalar_variables = self .required_keys ,
298+ non_scalar_variables = ["UC_ALineEBeam3" ],
299+ )
98300
99301 result = self .evaluate_all_shots (shot_entries )
100302
101- return {self .output_key : result }
303+ return {self .output_key : result }
0 commit comments