Source code for autoemx.core.em_runtime.microscope_controller

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Microscope hardware control module for automated electron microscopy operations.

This module provides low-level control of microscope hardware including beam settings,
focus adjustment, brightness/contrast, and stage movement.

Classes
-------
MicroscopeController
    Handles microscope hardware initialization and adjustment.

Created on 2026
@author: Andrea
"""
import time
import warnings
from typing import Optional

from autoemx.utils.helper import EMError, print_single_separator
from autoemx import microscope_drivers as EM_driver

from autoemx._logging import get_logger
logger = get_logger(__name__)
import numpy as np


[docs] class MicroscopeController: """ Low-level microscope hardware controller. Handles initialization of SEM, beam settings, focus, brightness/contrast adjustment, and stage movement. Parameters ---------- EM_driver_obj : module The EM_driver module for hardware communication. microscope_cfg : MicroscopeConfig Microscope configuration. measurement_cfg : MeasurementConfig Measurement/acquisition configuration. init_wd : float Initial working distance in mm. im_width : int Image width in pixels. im_height : int Image height in pixels. verbose : bool If True, print progress information. """ def __init__(self, EM_driver_obj, microscope_cfg, measurement_cfg, init_wd, im_width, im_height, verbose=True): self.EM_driver = EM_driver_obj self.microscope_cfg = microscope_cfg self.measurement_cfg = measurement_cfg self.init_wd = init_wd self.im_width = im_width self.im_height = im_height self.verbose = verbose # Working distance bounds self._min_wd = init_wd - measurement_cfg.working_distance_tolerance self._max_wd = init_wd + measurement_cfg.working_distance_tolerance # Refresh time for BCF adjustments self.refresh_time = 1 # seconds # Track last adjustment time self._last_EM_adjustment_time = 0.0
[docs] def initialise_SEM(self, center_pos: Optional[tuple] = None) -> None: """ Activate and configure the Scanning Electron Microscope (SEM). This method performs the following steps: 1. Wakes up the SEM if necessary. 2. Switches to SEM mode. 3. Sets the electron detector mode to backscattered electrons (BSD). 4. Sets the beam voltage and current according to measurement configuration. 5. Moves the stage to the sample center, if provided. 6. Sets working distance. 7. Adjusts focus, brightness, and contrast. Parameters ---------- center_pos : tuple of float, optional (x, y) stage position of the sample center, in mm. Focus, brightness and contrast are adjusted there. If None, the stage is not moved. Raises ------ EMError If an error occurs during SEM activation or configuration. """ if self.verbose: print_single_separator("▶️ Activating SEM, and setting up...") try: # Wake up SEM if necessary self.EM_driver.activate() # Switch to SEM mode self.EM_driver.to_SEM() # Set detector type to BSD (Backscattered electron detector) self.EM_driver.set_electron_detector_mode(self.microscope_cfg.detector_type) # Set beam voltage (high tension) for EDS collection if self.measurement_cfg.beam_energy_keV: self.EM_driver.set_high_tension(self.measurement_cfg.beam_energy_keV) else: warnings.warn( "No acceleration voltage provided via measurement_cfg.beam_energy_keV. " "Using current microscope configurations", UserWarning ) # Set beam current for EDS collection if self.measurement_cfg.beam_current: self.EM_driver.set_beam_current(self.measurement_cfg.beam_current) else: warnings.warn( "No beam current provided via measurement_cfg.beam_current. " "Using current microscope configurations", UserWarning ) # Move to the sample, so that focus, brightness and contrast are adjusted on it if center_pos is not None: self.move_to_pos(center_pos) # Set working distance (needed for reliable autofocus) self.EM_driver.adjust_focus(self.init_wd) # Adjust focus, brightness, and contrast if self.verbose: print_single_separator("🔬 Adjusting contrast, brightness, and focus.") self.adjust_BCF() if self.verbose: logger.info("✅ SEM initialisation completed.") except KeyError: raise except Exception as e: raise EMError(f"Error during SEM activation: {e}") from e
[docs] def adjust_BCF(self) -> float: """ Adjust brightness, contrast, and focus (BCF). If automatic brightness and contrast adjustment is enabled, calls the automatic adjustment method. Otherwise, sets brightness and contrast to fixed values then performs autofocus. Returns ------- float Timestamp of adjustment completion. Raises ------ EMError If an error occurs during adjustment. """ try: if self.microscope_cfg.is_auto_BC: adj_time = self._autoadjust_BCF() else: self._set_frame_BC() adj_time = self._auto_focus() time.sleep(0.5) self._last_EM_adjustment_time = adj_time return adj_time except Exception as e: raise EMError(f"Failed to adjust brightness, contrast, and focus: {e}") from e
def _set_frame_BC(self) -> None: """ Set frame brightness and contrast to fixed values. Uses values from self.microscope_cfg.brightness and self.microscope_cfg.contrast. Raises ------ EMError If the EM driver fails to set brightness or contrast. """ try: self.EM_driver.set_brightness(self.microscope_cfg.brightness) self.EM_driver.set_contrast(self.microscope_cfg.contrast) except Exception as e: raise EMError(f"Failed to set brightness/contrast: {e}") from e def _autoadjust_BCF(self) -> float: """ Automatically adjust brightness, contrast, and focus. Returns ------- float Timestamp of adjustment completion. Raises ------ EMError If an error occurs during adjustment. """ try: self.EM_driver.auto_contrast_brightness() self._auto_focus() return time.time() except Exception as e: raise EMError(f"Failed to auto-adjust brightness/contrast/focus: {e}") from e def _auto_focus(self) -> float: """ Automatically adjust focus within allowed bounds. Calls the EM driver's autofocus method and ensures the resulting working distance is within allowed limits. Returns ------- float Timestamp of adjustment completion. Raises ------ EMError If an error occurs during autofocus. """ try: wd = self.EM_driver.auto_focus() # If WD is out of allowed bounds, clip and readjust if not (self._min_wd < wd < self._max_wd): logger.warning(f"⚠️ Working distance of {wd:.1f} mm obtained through autofocus was out of accepted limits.") wd = float(np.clip(wd, self._min_wd, self._max_wd)) logger.info(f"✅ WD was set to {wd:.1f} mm") self.EM_driver.adjust_focus(wd) return time.time() except Exception as e: raise EMError(f"Failed to auto-focus EM: {e}") from e
[docs] def set_frame_width(self, frame_width: float) -> None: """ Set the frame width at the microscope. Parameters ---------- frame_width : float The desired frame width in millimeters. Raises ------ EMError If an error occurs during frame width setting. """ try: self.EM_driver.set_frame_width(frame_width) except Exception as e: raise EMError(f"Failed to set frame width: {e}") from e
[docs] def move_to_pos(self, pos: tuple) -> None: """ Move the EM stage to the specified (x, y) position. Parameters ---------- pos : tuple of float Target (x, y) coordinates. Raises ------ EMError If an error occurs during stage movement. """ try: x, y = pos self.EM_driver.move_to(x, y) except Exception as e: raise EMError(f"Failed to move to desired position: {e}") from e
[docs] @staticmethod def standby() -> None: """Put microscope in standby mode.""" EM_driver.standby()