Basic usage: Array2DScanAnalyzer¶
Array2DScanAnalyzer wraps an ImageAnalyzer and runs it across every image in a scan. It handles file discovery, per-shot or per-bin processing, output rendering (montage of averaged images for parameter scans, GIF for noscans), and optional persistence of the rendered results into the scan's analysis/ tree.
The common path through this wrapper is Mode 2: declare the device + processing pipeline in a unified diagnostic YAML, then have the loader build everything for you. Mode 1 (in-code construction) exists for ad-hoc work where you want to override something at the notebook layer.
import logging
import time
import matplotlib.pyplot as plt
from IPython.display import Image, display
from geecs_data_utils import ScanPaths, ScanTag
from geecs_data_utils.config_roots import image_analysis_config
from image_analysis.config import load_diagnostic, load_camera_config
from scan_analysis.config import create_scan_analyzer
from scan_analysis.analyzers.common.array2D_scan_analysis import Array2DScanAnalyzer
from image_analysis.analyzers.beam_analyzer import BeamAnalyzer
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logging.getLogger("image_analysis").setLevel(logging.WARNING)
logging.getLogger("scan_analysis").setLevel(logging.WARNING)
logging.getLogger("geecs_data_utils").setLevel(logging.WARNING)
# Required for Mode 1's load_camera_config (Mode 2 uses
# scan_analysis_configs_path, resolved automatically).
image_analysis_config.set_base_dir(ScanPaths.paths_config.image_analysis_configs_path)
PosixPath('/Users/samuelbarber/Desktop/Code/Github_repos/GEECS-Plugins-Configs/scan_analysis_configs/analyzers')
Mode 2 — Load from a unified diagnostic YAML¶
Each device has one YAML in scan_analysis_configs/analyzers/<facility>/<stem>.yaml that declares:
- which
ImageAnalyzerclass to use (image_analyzer:) - how that analyzer should be configured (
image:— typed as aCameraConfigorLine1DConfig) - how the scan wrapper should be configured (
scan:— priority, save flags, file_tail, etc.)
load_diagnostic parses + validates the YAML; create_scan_analyzer instantiates the right wrapper class (Array2D vs Array1D, picked from the type of diag.image) populated with the inner ImageAnalyzer and the scan-side runtime config.
test_tag = ScanTag(year=2025, month=2, day=20, number=14, experiment="Undulator")
diag = load_diagnostic("Amp4Input")
scan_analyzer = create_scan_analyzer(diag)
t0 = time.monotonic()
result = scan_analyzer.run_analysis(scan_tag=test_tag)
print(f"execution time: {time.monotonic() - t0:.2f}s")
display(Image(filename=str(result[0]), width=500))
2026-06-01 12:36:05,519 - scan_analysis.base - WARNING - No parameter varied during the scan, setting noscan flag.
2026-06-01 12:37:55,023 - scan_analysis.base - WARNING - append_to_sfile: columns already exist in s-file: {'UC_Amp4_IR_Input_y_rms', 'UC_Amp4_IR_Input_y_fwhm', 'UC_Amp4_IR_Input_x_45_fwhm', 'UC_Amp4_IR_Input_y_45_fwhm', 'UC_Amp4_IR_Input_x_peak_location', 'UC_Amp4_IR_Input_image_total', 'UC_Amp4_IR_Input_x_45_rms', 'UC_Amp4_IR_Input_y_peak_location', 'UC_Amp4_IR_Input_y_CoM', 'UC_Amp4_IR_Input_y_45_peak_location', 'UC_Amp4_IR_Input_x_rms', 'UC_Amp4_IR_Input_image_peak_value', 'UC_Amp4_IR_Input_x_fwhm', 'UC_Amp4_IR_Input_y_45_rms', 'UC_Amp4_IR_Input_x_45_peak_location', 'UC_Amp4_IR_Input_x_45_CoM', 'UC_Amp4_IR_Input_y_45_CoM', 'UC_Amp4_IR_Input_x_CoM'} (will overwrite)
execution time: 111.66s
Inspecting per-shot results¶
After run_analysis, the wrapper retains a results dict keyed by shot number (per-shot mode) or bin number (per-bin mode). Each value is an ImageAnalyzerResult exposing processed_image, analyzer_return_dictionary, and any extra fields the analyzer returned.
shot = sorted(scan_analyzer.results.keys())[10]
array = scan_analyzer.results[shot].processed_image
plt.imshow(array, cmap="viridis", origin="lower", aspect="auto")
plt.title(f"Shot {shot}")
plt.colorbar()
plt.show()
Mode 1 — In-code construction (notebook tweaks, no YAML)¶
Mode 1 is the right escape hatch when you want to override behavior at the notebook layer: turn off image saving, swap in a different renderer, change the camera config's ROI on the fly, etc. The pattern is:
- Load the typed
CameraConfig(or build one in code). - Construct the
ImageAnalyzerwith it. - Construct the
Array2DScanAnalyzerwith explicit kwargs.
Below is the same workflow as Mode 2, but with flag_save_images=False (skip writing per-shot files to disk) and a renderer kwarg override.
cam_config = load_camera_config("Amp4Input")
# Notebook-time tweak — change the ROI without editing the YAML
cam_config.roi.x_max = 599
image_analyzer = BeamAnalyzer(camera_config=cam_config)
scan_analyzer_m1 = Array2DScanAnalyzer(
image_analyzer=image_analyzer,
device_name="UC_Amp4_IR_input",
flag_save_images=True,
renderer_kwargs={"colormap_mode": "sequential", "cmap": "magma"},
)
result = scan_analyzer_m1.run_analysis(scan_tag=test_tag)
display(Image(filename=str(result[0]), width=500))
Different analyzer classes — same wrapper¶
The wrapper is analyzer-agnostic. Swapping the image_analyzer field in the YAML is enough to pick a different processing pipeline (BeamAnalyzer, GrenouilleAnalyzer, MagSpecManualCalibAnalyzer, …). Below: the same Mode-2 call against the FROG Grenouille diagnostic.
frog_tag = ScanTag(year=2026, month=2, day=12, number=4, experiment="Undulator")
diag_frog = load_diagnostic("U_FROG")
scan_analyzer_frog = create_scan_analyzer(diag_frog)
result = scan_analyzer_frog.run_analysis(scan_tag=frog_tag)
display(Image(filename=str(result[0]), width=500))