SPECTRO_PEAKS
Overview
The SPECTRO_PEAKS function fits common spectroscopic peak models to experimental data, making it a versatile tool for analyzing spectral line shapes in applications such as chromatography, mass spectrometry, X-ray diffraction, and optical spectroscopy. The function uses non-linear least squares optimization via SciPy’s curve_fit to determine optimal model parameters.
Seven peak models are available, each suited for different physical phenomena:
- Gaussian Peak: Models peaks where broadening arises from random processes (e.g., Doppler broadening or instrumental resolution). The peak shape follows the normal distribution.
- Lorentzian Peak: Describes peaks from natural linewidth or lifetime broadening, characterized by broader tails than Gaussian profiles. Also known as the Cauchy distribution.
- Gaussian-Lorentzian Dual Peak: Combines both Gaussian and Lorentzian components at the same center position, useful when both broadening mechanisms contribute to a single peak.
- Pseudo-Voigt (Shared Width): A computationally efficient approximation to the true Voigt profile, expressed as a linear combination of Gaussian and Lorentzian profiles sharing a common width parameter.
- Pseudo-Voigt (Independent Widths): An extended pseudo-Voigt model allowing separate Gaussian (w_G) and Lorentzian (w_L) width parameters for greater flexibility.
- Voigt Profile (Convolution): The true Voigt profile computed as the convolution of Gaussian and Lorentzian distributions using the Faddeeva function. This is the most physically accurate model for spectral lines affected by both Doppler and natural broadening.
- Pearson Type VII: A flexible symmetric peak model that can interpolate between Gaussian (as the shape parameter \mu \to \infty) and Lorentzian (when \mu = 1) profiles.
- Inverse Polynomial Peak: Models asymmetric or complex peak shapes using a rational function with polynomial terms.
The Voigt profile is defined mathematically as:
V(x; \sigma, \gamma) = \int_{-\infty}^{\infty} G(x'; \sigma) \cdot L(x - x'; \gamma) \, dx' = \frac{\text{Re}[w(z)]}{\sigma\sqrt{2\pi}}
where w(z) is the Faddeeva function evaluated at z = (x + i\gamma) / (\sigma\sqrt{2}), \sigma is the Gaussian width, and \gamma is the Lorentzian width.
The function returns fitted parameter values along with standard errors derived from the covariance matrix, enabling uncertainty quantification for the estimated peak characteristics. For more details on the underlying optimization algorithm, see the SciPy curve_fit documentation.
This example function is provided as-is without any representation of accuracy.
Excel Usage
=SPECTRO_PEAKS(xdata, ydata, spectro_peaks_model)
xdata(list[list], required): The xdata valueydata(list[list], required): The ydata valuespectro_peaks_model(str, required): The spectro_peaks_model value
Returns (list[list]): 2D list [param_names, fitted_values, std_errors], or error string.
Examples
Example 1: Demo case 1
Inputs:
| spectro_peaks_model | xdata | ydata |
|---|---|---|
| gaussian_lorentzian_dual_peak | 0.01 | 0.18436343092326107 |
| 2.0075 | 3.2739956888036987 | |
| 4.005 | 0.13741205361162376 | |
| 6.0024999999999995 | 0.01 | |
| 8 | 0.037490212013517314 |
Excel formula:
=SPECTRO_PEAKS("gaussian_lorentzian_dual_peak", {0.01;2.0075;4.005;6.0024999999999995;8}, {0.18436343092326107;3.2739956888036987;0.13741205361162376;0.01;0.037490212013517314})
Expected output:
| y0 | xc | A1 | A2 | w1 | w2 |
|---|---|---|---|---|---|
| 0.06822 | 1.965 | 7.317 | -1.657 | 1.706 | 4.96 |
Example 2: Demo case 2
Inputs:
| spectro_peaks_model | xdata | ydata |
|---|---|---|
| lorentzian_peak | 0.1 | 2.7494029905519506 |
| 1.3250000000000002 | 1.0540770284615846 | |
| 2.5500000000000003 | 0.43012254911108183 | |
| 3.7750000000000004 | 0.27143948030788534 | |
| 5 | 0.10478140054086485 |
Excel formula:
=SPECTRO_PEAKS("lorentzian_peak", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {2.7494029905519506;1.0540770284615846;0.43012254911108183;0.27143948030788534;0.10478140054086485})
Expected output:
| A | x0 | w |
|---|---|---|
| 3.022 | -0.2667 | 1.164 |
| 0.3141 | 0.2236 | 0.06378 |
Example 3: Demo case 3
Inputs:
| spectro_peaks_model | xdata | ydata |
|---|---|---|
| pearson_type_vii_peak | 0.1 | 0.04913579064648669 |
| 1.3250000000000002 | 1.7688143080814422 | |
| 2.5500000000000003 | 0.650921022003965 | |
| 3.7750000000000004 | 0.01 | |
| 5 | 0.013963187192617028 |
Excel formula:
=SPECTRO_PEAKS("pearson_type_vii_peak", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.04913579064648669;1.7688143080814422;0.650921022003965;0.01;0.013963187192617028})
Expected output:
| xc | A | w | mu |
|---|---|---|---|
| 1.72 | 2.463 | 1.162 | 5.417 |
| 0.07173 | 0.7218 | 0.2927 | 7.334 |
Example 4: Demo case 4
Inputs:
| spectro_peaks_model | xdata | ydata |
|---|---|---|
| pseudo_voigt_shared_width | 0.01 | 0.7217206195343803 |
| 2.0075 | -1.596376566988804 | |
| 4.005 | 0.46671030759063314 | |
| 6.0024999999999995 | 0.09353111787881777 | |
| 8 | 0.07594589700449908 |
Excel formula:
=SPECTRO_PEAKS("pseudo_voigt_shared_width", {0.01;2.0075;4.005;6.0024999999999995;8}, {0.7217206195343803;-1.596376566988804;0.46671030759063314;0.09353111787881777;0.07594589700449908})
Expected output:
| y0 | xc | A | w | mu |
|---|---|---|---|---|
| -0.0786 | 8 | 0.001929 | 0.01905 | -1.935 |
Example 5: Demo case 5
Inputs:
| spectro_peaks_model | xdata | ydata |
|---|---|---|
| pseudo_voigt_independent_widths | 0.01 | 0.7217206195343803 |
| 2.0075 | -1.596376566988804 | |
| 4.005 | 0.46671030759063314 | |
| 6.0024999999999995 | 0.09353111787881777 | |
| 8 | 0.07594589700449908 |
Excel formula:
=SPECTRO_PEAKS("pseudo_voigt_independent_widths", {0.01;2.0075;4.005;6.0024999999999995;8}, {0.7217206195343803;-1.596376566988804;0.46671030759063314;0.09353111787881777;0.07594589700449908})
Expected output:
| y0 | xc | A | wG | wL | mu |
|---|---|---|---|---|---|
| -0.4466 | 5.719 | 31.19 | 0.2117 | 0.01549 | 29.75 |
Example 6: Demo case 6
Inputs:
| spectro_peaks_model | xdata | ydata |
|---|---|---|
| inverse_polynomial_peak | 0.5 | 0.27896815 |
| 1.3 | 0.7237742 | |
| 2.1 | 2.43656316 | |
| 2.9 | 2.8919391 | |
| 3.7 | 1.01852745 | |
| 4.5 | 0.32536436 |
Excel formula:
=SPECTRO_PEAKS("inverse_polynomial_peak", {0.5;1.3;2.1;2.9;3.7;4.5}, {0.27896815;0.7237742;2.43656316;2.8919391;1.01852745;0.32536436})
Expected output:
| y0 | xc | w | A | A1 | A2 | A3 |
|---|---|---|---|---|---|---|
| 0.3058 | 2.4997 | 0.5414 | 0.0335 | -0.5188 | 0.0317 | -0.0004 |
Example 7: Demo case 7
Inputs:
| spectro_peaks_model | xdata | ydata |
|---|---|---|
| voigt_profile_convolution | 0.1 | 1 |
| 1.3250000000000002 | 1 | |
| 2.5500000000000003 | 1 | |
| 3.7750000000000004 | 1 | |
| 5 | 1 |
Excel formula:
=SPECTRO_PEAKS("voigt_profile_convolution", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {1;1;1;1;1})
Expected output:
| y0 | xc | A | wG | wL |
|---|---|---|---|---|
| 1 | 0.1 | 0 | 0.8167 | 0.4083 |
Python Code
import numpy as np
from scipy.optimize import curve_fit as scipy_curve_fit
from scipy.special import wofz
import math
def spectro_peaks(xdata, ydata, spectro_peaks_model):
"""
Fits spectro_peaks models to data using scipy.optimize.curve_fit. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html for details.
See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html
This example function is provided as-is without any representation of accuracy.
Args:
xdata (list[list]): The xdata value
ydata (list[list]): The ydata value
spectro_peaks_model (str): The spectro_peaks_model value Valid options: Gaussian Lorentzian Dual Peak, Lorentzian Peak, Pearson Type Vii Peak, Pseudo Voigt Shared Width, Pseudo Voigt Independent Widths, Inverse Polynomial Peak, Voigt Profile Convolution.
Returns:
list[list]: 2D list [param_names, fitted_values, std_errors], or error string.
"""
def _validate_data(xdata, ydata):
"""Validate and convert both xdata and ydata to numpy arrays."""
for name, arg in [("xdata", xdata), ("ydata", ydata)]:
if not isinstance(arg, list) or len(arg) < 2:
raise ValueError(f"{name}: must be a 2D list with at least two rows")
vals = []
for i, row in enumerate(arg):
if not isinstance(row, list) or len(row) == 0:
raise ValueError(f"{name} row {i}: must be a non-empty list")
try:
vals.append(float(row[0]))
except Exception:
raise ValueError(f"{name} row {i}: non-numeric value")
if name == "xdata":
x_arr = np.asarray(vals, dtype=np.float64)
else:
y_arr = np.asarray(vals, dtype=np.float64)
if x_arr.shape[0] != y_arr.shape[0]:
raise ValueError("xdata and ydata must have the same number of rows")
return x_arr, y_arr
# Model definitions dictionary
models = {
'gaussian_lorentzian_dual_peak': {
'params': ['y0', 'xc', 'A1', 'A2', 'w1', 'w2'],
'model': lambda x, y0, xc, A1, A2, w1, w2: y0 + (A1 / (w1 * np.sqrt(np.pi / 2.0))) * np.exp(-2.0 * np.square((x - xc) / w1)) + (2.0 * A2 / np.pi) * (w2 / (4.0 * np.square(x - xc) + np.square(w2))),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 0.5), float(np.ptp(ya) if np.ptp(ya) else 0.5), float(max((np.max(xa) - np.min(xa)) / 6.0, 1e-3)), float(max((np.max(xa) - np.min(xa)) / 6.0, 1e-3))),
'bounds': ([-np.inf, -np.inf, -np.inf, -np.inf, 0.0, 0.0], np.inf),
},
'lorentzian_peak': {
'params': ['A', 'x0', 'w'],
'model': lambda x, A, x0, w: A * (w * w) / (np.power(x - x0, 2) + w * w),
'guess': lambda xa, ya: (float(max(ya)), float(np.median(xa)), 1.0),
'bounds': ([-np.inf, -np.inf, 0.0], np.inf),
},
'pearson_type_vii_peak': {
'params': ['xc', 'A', 'w', 'mu'],
'model': lambda x, xc, A, w, mu: A * np.power(1.0 + 4.0 * (np.power(2.0, 1.0 / mu) - 1.0) * np.square(x - xc) / np.square(w), -mu),
'guess': lambda xa, ya: (float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), float(max((np.max(xa) - np.min(xa)) / 4.0, 1e-3)), 1.5),
'bounds': ([-np.inf, 0.0, 0.0, 0.0], np.inf),
},
'pseudo_voigt_shared_width': {
'params': ['y0', 'xc', 'A', 'w', 'mu'],
'model': lambda x, y0, xc, A, w, mu: y0 + A * (mu * (2.0 / np.pi) * (w / (4.0 * np.square(x - xc) + np.square(w))) + (1.0 - mu) * (np.sqrt(4.0 * np.log(2.0)) / (np.sqrt(np.pi) * w)) * np.exp(-4.0 * np.log(2.0) * np.square(x - xc) / np.square(w))),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), float(max((np.max(xa) - np.min(xa)) / 4.0, 1e-3)), 0.5),
'bounds': ([-np.inf, -np.inf, -np.inf, 0.0, -np.inf], np.inf),
},
'pseudo_voigt_independent_widths': {
'params': ['y0', 'xc', 'A', 'wG', 'wL', 'mu'],
'model': lambda x, y0, xc, A, wG, wL, mu: y0 + A * (mu * (2.0 / np.pi) * (wL / (4.0 * np.square(x - xc) + np.square(wL))) + (1.0 - mu) * (np.sqrt(4.0 * np.log(2.0)) / (np.sqrt(np.pi) * wG)) * np.exp(-4.0 * np.log(2.0) * np.square(x - xc) / np.square(wG))),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(np.ptp(ya) if np.ptp(ya) else 1.0), float(max((np.max(xa) - np.min(xa)) / 4.0, 1e-3)), float(max((np.max(xa) - np.min(xa)) / 6.0, 1e-3)), 0.5),
'bounds': ([-np.inf, -np.inf, -np.inf, 0.0, 0.0, -np.inf], np.inf),
},
'inverse_polynomial_peak': {
'params': ['y0', 'xc', 'w', 'A', 'A1', 'A2', 'A3'],
'model': lambda x, y0, xc, w, A, A1, A2, A3: y0 + A / (1.0 + A1 * np.power(2.0 * (x - xc) / w, 2) + A2 * np.power(2.0 * (x - xc) / w, 4) + A3 * np.power(2.0 * (x - xc) / w, 6)),
'guess': lambda xa, ya: (float(np.min(ya)), float(np.median(xa)), float(max((np.max(xa) - np.min(xa)) / 6.0, 1e-3)), float(np.ptp(ya) if np.ptp(ya) else 1.0), 0.0, 0.0, 0.0),
'bounds': ([-np.inf, -np.inf, 0.0, -np.inf, -np.inf, -np.inf, -np.inf], np.inf),
},
'voigt_profile_convolution': {
'params': ['y0', 'xc', 'A', 'wG', 'wL'],
'model': lambda x, y0, xc, A, wG, wL: (np.asarray(y0, dtype=np.float64) + np.asarray(A, dtype=np.float64) * (np.real(wofz(((np.asarray(x, dtype=np.float64) - xc) + 1j * (wL / 2.0)) / ((wG / (2.0 * np.sqrt(2.0 * np.log(2.0)))) * np.sqrt(2.0)))) / ((wG / (2.0 * np.sqrt(2.0 * np.log(2.0)))) * np.sqrt(2.0 * np.pi)))),
'guess': lambda xa, ya: (float(np.min(ya)), float(xa[np.argmax(ya)]), max(0.0, float(np.trapz(ya - np.min(ya), xa))), float((np.max(xa) - np.min(xa)) / 6.0) or 1.0, float((np.max(xa) - np.min(xa)) / 12.0) or 0.5),
'bounds': ([-np.inf, -np.inf, -np.inf, 0.0, 0.0], np.inf),
}
}
# Validate model parameter
if spectro_peaks_model not in models:
return f"Invalid model: {str(spectro_peaks_model)}. Valid models are: {', '.join(models.keys())}"
model_info = models[spectro_peaks_model]
# Validate and convert input data
try:
x_arr, y_arr = _validate_data(xdata, ydata)
except ValueError as e:
return f"Invalid input: {e}"
# Perform curve fitting
try:
p0 = model_info['guess'](x_arr, y_arr)
bounds = model_info.get('bounds', (-np.inf, np.inf))
if bounds == (-np.inf, np.inf):
popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, maxfev=10000)
else:
popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, bounds=bounds, maxfev=10000)
fitted_vals = [float(v) for v in popt]
for v in fitted_vals:
if math.isnan(v) or math.isinf(v):
return "Fitting produced invalid numeric values (NaN or inf)."
except ValueError as e:
return f"Initial guess error: {e}"
except Exception as e:
return f"curve_fit error: {e}"
# Calculate standard errors
std_errors = None
try:
if pcov is not None and np.isfinite(pcov).all():
std_errors = [float(v) for v in np.sqrt(np.diag(pcov))]
except Exception:
pass
return [model_info['params'], fitted_vals, std_errors] if std_errors else [model_info['params'], fitted_vals]