A flexible and modular Python toolkit for multi-peak curve fitting, parameter management, visualization, and area analysis using scipy.optimize.curve_fit.
I had to dig into scipy too many times over the years without using it on a daily basis. I decided that I needed a more handy peak-fitting tool (based on scipy) that would speed up my workflow. This project is designed as a modular research-grade fitting system, not a black-box tool. Each component can be reused independently in scientific workflows.
This package is designed to handle arbitrary numbers of peaks with different models (Gaussian, Voigt, Asymmetric, Skewed), including support for fixed parameters (e.g., fixed peak centers μ).
🔹 Automatic peak detection — Intelligently finds peaks and estimates initial parameters
🔹 Multi-peak fitting with arbitrary peak count
🔹 Multiple peak models:
- Gaussian
- Voigt
- Asymmetric
- Skewed
🔹 Support for fixed parameters (e.g., μ fixed per peak)
🔹 Automatic parameter flattening/unflattening
🔹 Intelligent initial guess generation from data
🔹 Flexible bounds handling
🔹 Full peak decomposition after fitting
🔹 Area integration for peaks and total signal
🔹 Clean visualization with residuals and RMSE
Install from source:
git clone https://github.com/your-username/ReadyToFit.git
cd ReadyToFit
pip install .
Or install directly from GitHub:
pip install git+https://github.com/your-username/ReadyToFit.git
Dependencies: numpy, scipy, matplotlib (automatically installed).
Each peak is defined as a dictionary:
peaks = [
{"model": "gauss"},
{"model": "voigt", "mu": 50}, # fixed center at x=50
{"model": "asym"}
]The system automatically:
- Detects peaks in data using
scipy.signal.find_peaks - Estimates initial parameters (amplitude, position, width) from data
- Builds the composite model (sum of all peaks)
- Flattens parameters for optimization
- Handles fixed parameters (removed from optimization, reinstated after fit)
- Reconstructs fitted peaks
The Problem: Multi-peak fitting requires good initial guesses (p0). Bad guesses lead to poor convergence or fitting the wrong peaks.
The Solution: ReadyToFit includes intelligent automatic peak detection:
-
Peak Detection (
detect_peaks()):- Finds local maxima using local gradient analysis
- Filters by prominence (relative height above baseline)
- Estimates peak positions, amplitudes, and widths
-
Parameter Estimation (
estimate_initial_parameters()):- Converts detected peaks into initial parameter guesses
- Estimates widths from Full-Width-Half-Maximum (FWHM)
- Respects user-defined fixed parameters (not overridden)
-
Automatic p0:
- When
fit_model()is called withoutp0, it auto-generates it - Much more robust than naive guesses (e.g., same μ for all peaks)
- Can be overridden by providing manual
p0if needed
- When
Example:
from readytofit import detect_peaks
# Detect peaks manually (optional — fit_model() does this automatically)
detected = detect_peaks(x, y, n_peaks=3)
# Returns: [{"mu": 25.5, "A": 4.8, "sigma": 5.1}, ...]The simplest approach: ReadyToFit automatically detects peaks and estimates initial parameters.
from readytofit import fit_model
import numpy as np
import matplotlib.pyplot as plt
# Your noisy multi-peak data
x = np.array([...])
y = np.array([...])
# Define peak structure (models only — parameters auto-detected)
peaks = [
{"model": "gauss"},
{"model": "gauss"},
{"model": "voigt"}
]
# Fit — p0 and initial parameters are auto-generated!
result = fit_model(x, y, peaks)
print(f"RMSE: {result['rmse']:.4f}")
print("Fitted parameters per peak:")
for i, peak_params in enumerate(result["params"]):
print(f" Peak {i+1}: {peak_params}")How auto-detection works:
- Peak finding: Uses
scipy.signal.find_peaksto locate local maxima - Parameter estimation: Estimates amplitude (A), center (μ), and width (σ) from data
- Smart initial guess: Provides excellent starting point for optimization
- Respects fixed parameters: If you specify
"mu": 50, it's locked and not overridden
Lock specific parameter values (e.g., peak center):
peaks = [
{"model": "gauss"}, # All parameters free
{"model": "gauss", "mu": 50}, # Center fixed at x=50
{"model": "voigt", "mu": 80} # Center fixed at x=80
]
result = fit_model(x, y, peaks)
# View results (fixed parameters are preserved in result["params"])
assert result["params"][1]["mu"] == 50 # mu is locked
assert result["params"][1]["A"] is not None # A and sigma are fittedOverride automatic detection with manual p0:
# Manual initial guesses (if you know better than auto-detection!)
p0 = [
{"A": 100, "mu": 25, "sigma": 5},
{"A": 80, "mu": 75, "sigma": 6}
]
result = fit_model(x, y, peaks, p0=p0)from readytofit import plot_fit_result
fig, ax = plt.subplots(figsize=(10, 6))
plot_fit_result(x, y, result, show_residual=True, show_rmse=True, fig=fig, ax=ax)
fig.suptitle("Multi-Peak Fitting Results")
plt.savefig("fit_result.png")
plt.close()Example output:
from readytofit import evaluate_peak_areas
areas = evaluate_peak_areas(x, result)
print(f"Total signal area: {areas['total']:.2f}")
print(f"Individual peak areas: {areas['peaks']}")Detect peaks without fitting:
from readytofit import detect_peaks
detected = detect_peaks(x, y, n_peaks=3)
for i, peak in enumerate(detected):
print(f"Peak {i+1}:")
print(f" Center (μ): {peak['mu']:.2f}")
print(f" Amplitude (A): {peak['A']:.3f}")
print(f" Width (σ): {peak['sigma']:.3f}")The fit_model() function returns a comprehensive results dictionary:
result = {
# Parameters
"popt": [...], # Optimized parameters (flat, free only)
"params": [ # Structured parameters per peak
{"A": 5.0, "mu": 30.0, "sigma": 5.1},
{"A": 3.0, "mu": 70.0, "sigma": 3.0}
],
# Fitted curves
"total_fit": [...], # Full reconstructed signal
"peak_fits": [...], # Individual peaks
# Diagnostics
"residual": [...], # y - total_fit
"rmse": 0.1006, # Root mean square error
# Metadata
"param_names": [...], # Names of free parameters
"param_slices": [...], # Index mapping per peak
"model_function": callable, # Composite model function
"p0": [...], # Initial guess used
"bounds": (lower, upper) # Bounds used in optimization
}Access results:
# Check fit quality
print(f"RMSE: {result['rmse']:.4f}")
# Get all parameters (including fixed ones)
for i, peak in enumerate(result["params"]):
print(f"Peak {i}: {peak}")
# Get fitted curve and residuals
y_fitted = result["total_fit"]
residuals = result["residual"]
# Get individual peak contributions
for i, peak_curve in enumerate(result["peak_fits"]):
plt.plot(x, peak_curve, label=f"Peak {i}")| Model | Parameters | Use Case |
|---|---|---|
| gauss | A, μ, σ | Symmetric peaks (most common) |
| voigt | A, μ, σ, γ | Symmetric peaks with natural broadening |
| asym | A, μ, σL, σR, γ | Asymmetric peaks (different left/right widths) |
| skew | A, μ, σ, γ, α | Peaks with asymmetric tails |
Legend:
- A: Amplitude (peak height above baseline)
- μ: Center position (can be fixed:
{"model": "gauss", "mu": 50}) - σ: Standard deviation (width parameter)
- σL, σR: Left/right widths for asymmetric model
- γ: Lorentz width (natural line broadening)
- α: Skew parameter (asymmetry control)
Fixed parameters are removed from optimization. Example:
{"model": "gauss", "mu": 50}
# → Only A and sigma are fitted; mu is locked at 50🔹 Automatic parameter handling
No manual indexing required — parameters are flattened internally and fixed parameters are properly managed.
🔹 Robust fitting pipeline
Handles missing p0, partial bounds, and invalid input gracefully with intelligent fallbacks.
🔹 Full decomposition
Inspect total fit, individual peaks, residuals, and peak areas independently.
🔹 Peak detection
detect_peaks() and estimate_initial_parameters() enable automatic initial guess generation.
fit_model(x, y, peaks, p0=None, bounds=None, debug=False)
Main fitting function. Automatically detects peaks and estimates p0 if not provided.
plot_fit_result(x, y, result, show_residual=True, show_rmse=True, fig=None, ax=None)
Plot raw data, total fit, individual peaks, residuals, and RMSE.
-
detect_peaks(x, y, n_peaks=None, height_threshold=0.1, prominence_threshold=None, distance=None)
Automatically detect peaks. Returns list of peak dictionaries with estimated parameters. -
estimate_initial_parameters(x, y, peaks)
Estimate initial parameters from data. Respects fixed parameters in peak definitions.
-
evaluate_peak_areas(x, result)
Compute areas of total fit and individual peaks using trapezoidal rule. -
area_integration(y, x=None)
Low-level numerical integration utility.
numpy
scipy
matplotlib
To add new functions you need the following changes:
- Insert the function definition into functions.py
- Add dedicated section in get_model() inside functions.py
- Add dedicated entry inside the PARAM_ORDER dictionary inside parameters.py
- Add dedicated section in generate_default_p0() inside parameters.py
- Add the new peak name into peak_types variable inside test.py in the peaks import section
