1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
|
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
Spectrum Analysis
=================
This example demonstrates advanced signal processing for spectroscopy analysis
using a paracetamol spectrum. It shows how to apply noise reduction, region
of interest selection, peak fitting, and detrending techniques available
in Sigima. Each step builds upon the previous one to create a comprehensive
analysis workflow.
Usage:
python paracetamol_example.py
The script demonstrates spectroscopy data processing workflows commonly used
in analytical chemistry and materials science applications.
"""
# %% Importing necessary modules
import numpy as np
import sigima.objects
import sigima.proc.signal
from sigima.tests import vistools
from sigima.tests.data import create_paracetamol_signal
from sigima.tools.signal import fitting, peakdetection
# Constants
XLABEL_ANGLE = "Angle"
YLABEL_INTENSITY = "Intensity"
# %%
# Load test signal and initial visualization
# -----------------------------------------------------------
# We load a sample paracetamol spectrum included in the Sigima test data.
# This spectrum contains characteristic absorption peaks that we will
# analyze using various signal processing techniques.
# Load the paracetamol signal from test data
sig = create_paracetamol_signal()
x_orig, y_orig = sig.xydata
print("✓ Paracetamol spectrum loaded successfully!")
print(f"Signal contains {len(x_orig)} data points")
print(f"Energy range: {x_orig.min():.1f} to {x_orig.max():.1f} eV")
print(f"Intensity range: {y_orig.min():.1f} to {y_orig.max():.1f}")
# Visualize the original spectrum
vistools.view_curves(sig, title="Paracetamol Spectrum - Original")
# %%
# Apply Wiener filter for noise reduction
# --------------------------------------------
# The signal is quite clean. Anyway, to illustrate the filtering capabilities
# of Sigima, we apply a Wiener filter to reduce any residual noise while
# preserving the spectral features.
sig_filt = sigima.proc.signal.wiener(sig)
print("\n✓ Wiener filter applied!")
print("The Wiener filter provides optimal noise reduction for signals")
print("with known statistical properties.")
# Compare original and filtered signals
vistools.view_curves(
[sig, sig_filt], title="Paracetamol Spectrum - Original vs Wiener Filtered"
)
# %%
# Region of Interest (ROI) selection
# -----------------------------------------------------------
# We fistly focus our analysis on one of the peaks of interest. To to that,
# we define a region of interest (ROI) around the feature we want to analyze.
# Define ROI around the peak
roi_bounds = [35.5, 41.3] # Energy range in eV
sig_filt.roi = sigima.objects.create_signal_roi(roi_bounds)
print(f"\n✓ ROI defined from {roi_bounds[0]} to {roi_bounds[1]} eV")
print("This focuses analysis on the primary absorption feature")
# Visualize the signal with ROI
vistools.view_curves(sig_filt, title="Paracetamol Spectrum - Filtered with ROI")
# %%
# Gaussian fit on the peak
# -----------------------------
# We can now fit the peak within the selected ROI using a
# Gaussian model. This provides quantitative parameters such as peak position,
# amplitude, and width.
# Perform Gaussian fit on the ROI-selected data
fit = sigima.proc.signal.gaussian_fit(sig_filt)
print("\n✓ Gaussian fit completed!")
print("This characterizes the main absorption peak with parameters:")
print("- Peak position (energy)")
print("- Peak amplitude (intensity)")
print("- Peak width (FWHM)")
# Visualize the signal with Gaussian fit
vistools.view_curves(
[sig_filt, fit], title="Paracetamol Spectrum - ROI with Gaussian Fit"
)
# %%
# Linear detrending
# -----------------------------------------------------------
# After fitting the main peak, we may want to remove any baseline drift
# present in the entire spectrum.
#
# The detrending function of Sigima performs a linear fit on the whole signal,
# including the peaks. In our signal peaks take a large part of the signal itself,
# witch is enough for signals where the peak are symmetrically distributed around the
# center, with more or less the same amplitude. This is not the case here, and we cannot
# expect this function to work well. It is however an interesting example to illustrate
# how Sigima functions can be combined to perform a more advanced analysis.
# %%
# In order to visualize the limitation cited above, we apply the detrending
# function directly on the filtered signal. It's important to remembre that we setted
# a ROI on the signal to focus the analysis on the main peak. We need to remove
# this ROI constraint to perform the detrending on the full signal.
# Remove ROI constraint for full signal detrending
sig_filt.roi = None
# Apply linear detrending to remove baseline drift
detrended_signal = sigima.proc.signal.detrending(sig_filt, method="linear")
print("\n✓ Linear detrending applied!")
# Compare filtered and detrended signals
vistools.view_curves(
[sig_filt, detrended_signal], title="Paracetamol Spectrum - Filtered vs Detrended"
)
# %%
# The comparison shows, as expected, that the detrending function does not work well on
# this signal.
# This, as explained before, is due to the alogirithm used, which performs a linear fit
# on the whole signal, including the peaks. This effect is clearly visible on the plot:
# the peaks on the left, that are higher than the ones on the right, starts after the
# detrending at an intensity value lower than the ones on the right, and all peaks has
# a baseline under the zero.
#
#
# %%
# Improved detrending with peak exclusion
# -----------------------------------------------------------
# An idea to overcome the limitation of the detrending function is suggested from the
# behavior of the detrended signal: we already identified the problem, which is that the
# linear fit is not performed on the baseline only, but also on the peaks.
#
# To perform a better detrending, we can first thus detect the peaks and then perform
# a linear fit only on the non-peak regions. We reasonably expect this approach to
# provide a more accurate baseline estimation and a better detrended signal.
# %%
# Automatic peak detection
# -------------------------------------
# We can use the peak detection function of Sigima to automatically identify
# the peaks in the spectrum. This function analyzes the signal and returns
# the indices of the detected peaks.
# Identify peaks in the detrended signal
peak_indices = peakdetection.peak_indices(sig_filt.y)
print("\n✓ Peak detection completed!")
print(f"Found {len(peak_indices)} potential peaks in the spectrum")
# Print detected peak positions
x_data = sig_filt.x
for i, peak_idx in enumerate(peak_indices):
energy = x_data[peak_idx]
intensity = sig_filt.y[peak_idx]
print(f" Peak {i + 1}: Energy = {energy:.2f} eV, Intensity = {intensity:.1f}")
# %%
# Multi-Gaussian fitting
# -----------------------------------------------------------
# We can now fit multiple Gaussian functions to the detected peaks. This allows us to
# characterize each peak individually and obtain parameters such as position,
# amplitude, and width for each peak.
# Perform multi-Gaussian fit using detected peaks
fitted_y, params = fitting.multigaussian_fit(
sig_filt.x, sig_filt.y, peak_indices=peak_indices.tolist()
)
# Create fitted signal object for visualization
fitted_signal = sig_filt.copy()
fitted_signal.y = fitted_y
fitted_signal.title = "Multi-Gaussian Fit"
print("\n✓ Multi-Gaussian fitting completed!")
print("Each detected peak is fitted with individual Gaussian functions")
# Visualize the final fitting result
vistools.view_curves(
[sig_filt, fitted_signal],
title="Paracetamol Spectrum - Detrended with Multi-Gaussian Fit",
)
# %%
# Defining ROI outside peaks for better detrending
# -----------------------------------------------------------
# To improve the detrending, we define ROIs that exclude the detected peaks.
# This allows us to fit the baseline only on the non-peak regions.
# Extract peak parameters from the multi-Gaussian fit
# Each peak has 3 parameters: amplitude, center, sigma
num_peaks = len(peak_indices)
peak_params = []
peaks_roi_bounds = np.zeros((num_peaks, 2))
for i in range(num_peaks):
# Extract parameters for each Gaussian (amplitude, center, sigma)
amplitude = params[f"amp_{i + 1}"]
center = params[f"x0_{i + 1}"]
sigma = params[f"sigma_{i + 1}"]
peak_params.append([amplitude, center, sigma])
# Define exclusion zone as center ± 2*sigma
exclusion_start = center - 2 * abs(sigma)
exclusion_end = center + 2 * abs(sigma)
peaks_roi_bounds[i] = [exclusion_start, exclusion_end]
print(f"Peak {i + 1}: Center = {center:.2f} eV, Sigma = {sigma:.3f}")
print(f" Exclusion zone: [{exclusion_start:.2f}, {exclusion_end:.2f}] eV")
# Create ROIs including detected peaks
roi = sigima.objects.create_signal_roi(peaks_roi_bounds)
# invert ROIs to exclude peaks
sig_filt.roi = roi.inverted(sig_filt.x.min(), sig_filt.x.max())
# Visualize the signal with new ROI
vistools.view_curves(
sig_filt, title="Paracetamol Spectrum - Filtered with Peak Exclusion ROIs"
)
# %%
# We can now perform a linear fit on the signal using the defined ROIs to exclude
# the peaks. This will provide a more accurate baseline estimation.
fitted_signal = fit = sigima.proc.signal.linear_fit(sig_filt)
# %%
# Finally, we can subtract the extended linear fit from the original filtered signal
# to obtain a better detrended signal. We can see that the baseline is now properly
# estimated, leading to a more accurate detrended signal.
better_detrended_signal = sigima.proc.signal.difference(sig_filt, fitted_signal)
better_detrended_signal.title = "Improved Detrended Signal"
print("\n✓ Improved detrending applied!")
# Compare filtered and better detrended signals
vistools.view_curves(
[sig_filt, better_detrended_signal],
title="Paracetamol Spectrum - Filtered vs Improved Detrended",
show_roi=False,
)
# %%
# To go futher...
# -----------------------------------------------------------
# In the last step we really improved the detrending of our signal. Anyway, several
# pics on the left of the signal are still not detected and the dentrending can be
# futher improved. We suggest you to experiment by tuning the parameters of the peak
# detection function and put your hands on the source code of this function to better
# understand how it works.
#
|