Basic Analysis Tutorial
This tutorial introduces the core QEM workflow using the ImageFitting class and Model classes for analyzing atomic-resolution STEM images.
Learning Objectives
By the end of this tutorial, you will:
Understand QEM’s core ImageFitting class
Use different Model types (Gaussian, Lorentzian, Voigt)
Load and visualize STEM data
Find atomic column positions automatically
Fit models to experimental data
Analyze and interpret results
Core QEM Components
- ImageFitting Class
The main analysis engine that handles: - Peak detection - Parameter initialization - Model fitting and optimization - Results extraction and visualization
- Model Classes
Mathematical models for atomic peaks: -
GaussianModel
: Standard 2D Gaussian peaks -LorentzianModel
: Lorentzian peak shapes -VoigtModel
: Convolution of Gaussian and Lorentzian
Prerequisites
QEM installed
Basic Python/NumPy knowledge
Sample STEM image (or use provided example)
Step 1: Import Libraries and Load Data
import numpy as np
import matplotlib.pyplot as plt
from qem.image_fitting import ImageFitting
from qem import io
# Load example STO data
image, metadata = io.load_example_data('STO')
dx = metadata.get('pixel_size', 0.01) # Angstroms per pixel
print(f"Image shape: {image.shape}")
print(f"Pixel size: {dx} Å")
Step 2: Visualize the Raw Data
plt.figure(figsize=(10, 8))
plt.imshow(image, cmap='gray')
plt.colorbar(label='Intensity')
plt.title('Raw STEM Image')
plt.xlabel('x (pixels)')
plt.ylabel('y (pixels)')
plt.show()
Step 3: Initialize ImageFitting
# Create ImageFitting instance
fitter = ImageFitting(
image=image,
dx=dx,
model_type="gaussian" # Can be 'gaussian', 'lorentzian', or 'voigt'
)
print(f"Using backend: {fitter.backend}")
print(f"Image size: {fitter.image.shape}")
Step 4: Find Atomic Column Positions
# Automatic peak finding
coordinates = fitter.find_peaks(
min_distance=8, # Minimum distance between peaks (pixels)
threshold_abs=0.3, # Absolute intensity threshold
threshold_rel=0.1 # Relative threshold (fraction of max intensity)
)
print(f"Found {len(coordinates)} atomic columns")
# Visualize detected peaks
plt.figure(figsize=(10, 8))
plt.imshow(image, cmap='gray')
plt.scatter(coordinates[:, 1], coordinates[:, 0],
c='red', s=30, marker='+', linewidth=2)
plt.title(f'Detected Atomic Columns ({len(coordinates)} peaks)')
plt.colorbar()
plt.show()
Step 5: Initialize Model Parameters
# Set coordinates and initialize parameters
fitter.coordinates = coordinates
# Initialize parameters with reasonable starting values
params = fitter.init_params(
atom_size=2.0, # Initial Gaussian width in pixels
intensity_guess=1.0, # Initial intensity
background=None # Auto-estimate background
)
print("Initial parameters set")
print(f"Number of atomic columns: {fitter.num_coordinates}")
Step 6: Perform the Fit
# Fit the model
result = fitter.fit(
max_iterations=200, # Maximum optimization steps
learning_rate=0.01, # Optimization step size
tolerance=1e-6, # Convergence criterion
verbose=True # Show progress
)
print(f"\nFitting completed!")
print(f"Iterations: {result['iterations']}")
print(f"Final loss: {result['final_loss']:.8f}")
print(f"Converged: {result['converged']}")
Step 7: Analyze Results
# Get fitted parameters
fitted_positions = fitter.get_positions()
fitted_intensities = fitter.get_intensities()
fitted_widths = fitter.get_widths()
print("Summary of fitted parameters:")
print(f"Position precision: {np.std(fitted_positions, axis=0)} pixels")
print(f"Intensity range: {np.min(fitted_intensities):.3f} - {np.max(fitted_intensities):.3f}")
print(f"Width range: {np.min(fitted_widths):.3f} - {np.max(fitted_widths):.3f} pixels")
Step 8: Visualize Results
# Generate model prediction
prediction = fitter.predict()
residuals = image - prediction
# Create comprehensive plot
fig, axes = plt.subplots(2, 3, figsize=(18, 12))
# Original image
im1 = axes[0, 0].imshow(image, cmap='gray')
axes[0, 0].set_title('Original Image')
axes[0, 0].scatter(fitted_positions[:, 1], fitted_positions[:, 0],
c='red', s=20, marker='+')
plt.colorbar(im1, ax=axes[0, 0])
# Model prediction
im2 = axes[0, 1].imshow(prediction, cmap='gray')
axes[0, 1].set_title('Model Fit')
plt.colorbar(im2, ax=axes[0, 1])
# Residuals
im3 = axes[0, 2].imshow(residuals, cmap='RdBu_r')
axes[0, 2].set_title('Residuals (Data - Model)')
plt.colorbar(im3, ax=axes[0, 2])
# Intensity histogram
axes[1, 0].hist(fitted_intensities, bins=20, alpha=0.7)
axes[1, 0].set_xlabel('Fitted Intensity')
axes[1, 0].set_ylabel('Count')
axes[1, 0].set_title('Intensity Distribution')
# Width histogram
axes[1, 1].hist(fitted_widths, bins=20, alpha=0.7)
axes[1, 1].set_xlabel('Fitted Width (pixels)')
axes[1, 1].set_ylabel('Count')
axes[1, 1].set_title('Width Distribution')
# Convergence plot
if 'loss_history' in result:
axes[1, 2].plot(result['loss_history'])
axes[1, 2].set_xlabel('Iteration')
axes[1, 2].set_ylabel('Loss')
axes[1, 2].set_title('Convergence')
axes[1, 2].set_yscale('log')
plt.tight_layout()
plt.show()
Step 9: Quality Assessment
# Calculate quality metrics
r_squared = fitter.calculate_r_squared()
rmse = fitter.calculate_rmse()
print(f"\nFit Quality Metrics:")
print(f"R² (coefficient of determination): {r_squared:.4f}")
print(f"RMSE (root mean square error): {rmse:.6f}")
# Residual statistics
residual_std = np.std(residuals)
residual_mean = np.mean(residuals)
print(f"Residual mean: {residual_mean:.6f}")
print(f"Residual std: {residual_std:.6f}")
Step 10: Save Results
# Save fitted parameters
results_dict = {
'positions': fitted_positions,
'intensities': fitted_intensities,
'widths': fitted_widths,
'model_prediction': prediction,
'residuals': residuals,
'fit_metrics': {
'r_squared': r_squared,
'rmse': rmse,
'iterations': result['iterations'],
'final_loss': result['final_loss']
}
}
# Save to file (optional)
# np.save('analysis_results.npy', results_dict)
print("Analysis complete!")
Advanced Tips
Improving Peak Detection:
# Use preprocessing for better peak detection
from qem.processing import butterworth_filter
filtered_image = butterworth_filter(image, high_cutoff=0.8)
fitter_filtered = ImageFitting(filtered_image, dx, "gaussian")
coordinates_improved = fitter_filtered.find_peaks(min_distance=8)
Manual Peak Selection:
# Interactive peak selection (in Jupyter)
from qem.gui_classes import InteractivePlot
interactive = InteractivePlot(image)
manual_coordinates = interactive.get_coordinates()
Different Model Types:
# Try different fitting models
for model_type in ['gaussian', 'lorentzian', 'voigt']:
fitter_test = ImageFitting(image, dx, model_type)
fitter_test.coordinates = coordinates
fitter_test.init_params(atom_size=2.0)
result_test = fitter_test.fit(max_iterations=100)
print(f"{model_type}: R² = {fitter_test.calculate_r_squared():.4f}")
Next Steps
Try advanced_fitting for optimization techniques
Learn about multi_element analysis
Explore strain_analysis for displacement mapping
Common Issues
Poor Peak Detection:
- Adjust threshold_abs
and threshold_rel
parameters
- Try image preprocessing (filtering, denoising)
- Check image contrast and quality
Fitting Not Converging:
- Increase max_iterations
- Adjust learning_rate
(try 0.001 to 0.1)
- Check initial parameter estimates
- Ensure adequate peak separation
Memory Issues: - Use smaller image regions for testing - Consider using JAX backend for large images - Process in batches for very large datasets