import numpy as np
import matplotlib.pyplot as plt
from pyleem.analyzer import Analyzer
from scipy.ndimage import gaussian_filter
[docs]
def SEES_onset(profile):
"""Determine onset position of secondary electron emission.
Locates the steepest rise in the profile by finding maximum
derivative. Extrapolates linear onset back to zero intensity.
:param ndarray profile: 1D array of secondary electron intensity values.
:return: Peak index, slope, and onset position in pixels.
:rtype: tuple(int, float, float)
"""
profile_derivative = np.gradient(profile)
slope = np.max(profile_derivative)
pk_idx = np.argmax(profile_derivative)
onset_pos = pk_idx - profile[pk_idx] / slope
return pk_idx, slope, onset_pos
[docs]
class SEESBase(Analyzer):
"""Base class for SEES analyzer."""
[docs]
def get_processed_profile(self, index, sigma):
"""Return the processed profile."""
return gaussian_filter(self.get_profile(index), sigma=sigma)
[docs]
class SEESCalibration(SEESBase):
"""Config for SEES analyzer."""
save_keys = ("pixel_per_ev", "peak_shift")
[docs]
def analyze(self, sigma=10, pixel_per_ev=None, peak_shift=None):
"""Calibration of the SEES analyzer."""
onset_pos = []
start_voltages = np.array(
[self.get_metadata("Start Voltage", index)[0] for index in self.indices]
)
for index in self.indices:
profile = self.get_processed_profile(index, sigma)
onset_pos.append(SEES_onset(profile)[2])
onset_pos = np.array(onset_pos)
if pixel_per_ev is None:
pixel_per_ev = np.mean(np.diff(onset_pos) / -np.diff(start_voltages))
if peak_shift is None:
peak_shift = np.mean(start_voltages + onset_pos / pixel_per_ev)
return {"pixel_per_ev": pixel_per_ev, "peak_shift": peak_shift}
[docs]
class SEESAnalyzer(SEESBase):
"""Analyzer for secondary electron energy spectroscopy data.
Analyzes SEES profiles to determine surface potentials by measuring
secondary electron emission onset. Onset shifts with surface charging.
:param list readers: List of readers.
:param ROI roi: Region of interest for profile extraction.
:param float sigma: Gaussian filter sigma for smoothing.
:ivar int pk_idx: Index of steepest rise.
:ivar float slope: Maximum derivative.
:ivar float onset_pos: Extrapolated onset position in pixels.
:ivar float surface_potential: Measured surface potential in V.
"""
def __init__(self, readers, roi, pixel_per_ev, peak_shift, onset=0, sigma=10):
super().__init__(readers, roi=roi, onset=onset)
self.pixel_per_ev = pixel_per_ev
self.peak_shift = peak_shift
self.sigma = sigma
[docs]
def analyze_profile(self, index):
"""Analyze a profile."""
profile = self.get_processed_profile(index, self.sigma)
pk_idx, slope, onset_pos = SEES_onset(profile)
kinetic_energy = (self.get_pixel(index) - onset_pos) / self.pixel_per_ev
surface_potential = self.peak_shift - (
self.get_metadata("Start Voltage", index)[0] + onset_pos / self.pixel_per_ev
)
return {
"kinetic_energy": kinetic_energy,
"surface_potential": surface_potential,
"onset_pos": onset_pos,
"pk_idx": pk_idx,
"slope": slope,
}
[docs]
def plot_profile(self, index, ax=None, show_fit=False):
"""Plot profile with optional onset fit overlay.
:param matplotlib.axes.Axes ax: Matplotlib axes object.
:param bool show_fit: Whether to show onset fit line.
"""
result = self.analyze_profile(index)
profile = self.get_profile(index)
ax = ax or plt.gca()
ax.plot(result["kinetic_energy"], profile)
if show_fit:
ax.plot(
[0, result["kinetic_energy"][result["pk_idx"]]],
[0, profile[result["pk_idx"]]],
"--",
label="fit",
)
ax.legend(loc="center left", bbox_to_anchor=(1, 0.5))
ax.set_xlabel("Energy [eV]")
ax.set_ylabel("Intensity")
return ax