eagleplot 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eagleplot/Home.py +362 -0
- eagleplot/__init__.py +19 -0
- eagleplot/annotations.py +215 -0
- eagleplot/assets/logo.ico +0 -0
- eagleplot/assets/logo.png +0 -0
- eagleplot/assets/nccu-horiz-logo.png +0 -0
- eagleplot/assets/nccu-wings.png +0 -0
- eagleplot/batch.py +446 -0
- eagleplot/cli.py +719 -0
- eagleplot/data_tools.py +849 -0
- eagleplot/example-data/Artificial/bimodal_distribution.csv +501 -0
- eagleplot/example-data/Artificial/correlation_matrix.npy +0 -0
- eagleplot/example-data/Artificial/gaussian_surface.npy +0 -0
- eagleplot/example-data/Artificial/grouped_categorical.csv +241 -0
- eagleplot/example-data/Artificial/ir_spectrum.csv +1802 -0
- eagleplot/example-data/Artificial/molecular_properties.csv +121 -0
- eagleplot/example-data/Artificial/normal_distribution.csv +401 -0
- eagleplot/example-data/Artificial/reaction_kinetics.csv +121 -0
- eagleplot/example-data/Artificial/scatter_correlation.csv +201 -0
- eagleplot/example-data/Artificial/sine_cosine_waves.csv +301 -0
- eagleplot/gallery/box_plot.png +0 -0
- eagleplot/gallery/contour_plot.png +0 -0
- eagleplot/gallery/distribution_plot.png +0 -0
- eagleplot/gallery/heatmap.png +0 -0
- eagleplot/gallery/histogram.png +0 -0
- eagleplot/gallery/interactive_3d_surface.png +0 -0
- eagleplot/gallery/interactive_heatmap.png +0 -0
- eagleplot/gallery/interactive_histogram.png +0 -0
- eagleplot/gallery/interactive_line.png +0 -0
- eagleplot/gallery/interactive_scatter.png +0 -0
- eagleplot/gallery/line_plot.png +0 -0
- eagleplot/gallery/manifest.json +196 -0
- eagleplot/gallery/regression_plot.png +0 -0
- eagleplot/gallery/scatter_plot.png +0 -0
- eagleplot/io.py +1008 -0
- eagleplot/math.py +1210 -0
- eagleplot/molecular/__init__.py +21 -0
- eagleplot/molecular/atom_data.py +287 -0
- eagleplot/molecular/parsers.py +747 -0
- eagleplot/nist.py +241 -0
- eagleplot/pages/10_Molecular_Viz.py +225 -0
- eagleplot/pages/11_Batch_Analysis.py +325 -0
- eagleplot/pages/12_Help.py +199 -0
- eagleplot/pages/13_Settings.py +388 -0
- eagleplot/pages/1_Data_Upload.py +521 -0
- eagleplot/pages/2_Quick_Plot.py +1693 -0
- eagleplot/pages/3_Analysis_Tools.py +2510 -0
- eagleplot/pages/4_Multi_Plot_Dashboard.py +791 -0
- eagleplot/pages/5_Advanced_Plotting.py +691 -0
- eagleplot/pages/6_Export_Results.py +442 -0
- eagleplot/pages/7_Gallery.py +201 -0
- eagleplot/pages/8_Data_Tools.py +434 -0
- eagleplot/pages/9_Spectroscopy.py +821 -0
- eagleplot/pages/__init__.py +8 -0
- eagleplot/peaks.py +513 -0
- eagleplot/plotting.py +2630 -0
- eagleplot/plugin_loader.py +208 -0
- eagleplot/report.py +448 -0
- eagleplot/signal.py +494 -0
- eagleplot/spectroscopy.py +830 -0
- eagleplot/utils/__init__.py +76 -0
- eagleplot/utils/access.py +77 -0
- eagleplot/utils/data_preview.py +352 -0
- eagleplot/utils/plot_config.py +2721 -0
- eagleplot/utils/session_state.py +535 -0
- eagleplot/utils/theming.py +312 -0
- eagleplot/utils/user_settings.py +294 -0
- eagleplot-3.0.0.dist-info/METADATA +304 -0
- eagleplot-3.0.0.dist-info/RECORD +73 -0
- eagleplot-3.0.0.dist-info/WHEEL +5 -0
- eagleplot-3.0.0.dist-info/entry_points.txt +2 -0
- eagleplot-3.0.0.dist-info/licenses/LICENSE +21 -0
- eagleplot-3.0.0.dist-info/top_level.txt +1 -0
eagleplot/signal.py
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"""Signal Processing Module.
|
|
2
|
+
|
|
3
|
+
Smoothing, filtering (Butterworth IIR), frequency analysis (FFT),
|
|
4
|
+
derivatives, baseline correction, and interpolation for 1-D signals.
|
|
5
|
+
|
|
6
|
+
All functions accept and return NumPy arrays.
|
|
7
|
+
|
|
8
|
+
Functions
|
|
9
|
+
---------
|
|
10
|
+
Smoothing:
|
|
11
|
+
smooth_moving_average, smooth_savitzky_golay, smooth_gaussian
|
|
12
|
+
|
|
13
|
+
Filtering:
|
|
14
|
+
filter_lowpass, filter_highpass, filter_bandpass, filter_bandstop
|
|
15
|
+
|
|
16
|
+
Frequency domain:
|
|
17
|
+
fft, ifft, power_spectrum
|
|
18
|
+
|
|
19
|
+
Derivatives:
|
|
20
|
+
derivative
|
|
21
|
+
|
|
22
|
+
Baseline correction:
|
|
23
|
+
baseline_polynomial, baseline_rolling_ball, baseline_als
|
|
24
|
+
|
|
25
|
+
Interpolation:
|
|
26
|
+
interpolate
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from typing import Any, Dict, Optional, Tuple
|
|
32
|
+
|
|
33
|
+
import numpy as np
|
|
34
|
+
from scipy.fft import fft as _scipy_fft
|
|
35
|
+
from scipy.fft import fftfreq
|
|
36
|
+
from scipy.fft import ifft as _scipy_ifft
|
|
37
|
+
from scipy.interpolate import CubicSpline, interp1d
|
|
38
|
+
from scipy.ndimage import gaussian_filter1d, maximum_filter1d, minimum_filter1d
|
|
39
|
+
from scipy.signal import butter, savgol_filter, sosfiltfilt
|
|
40
|
+
from scipy.sparse import diags
|
|
41
|
+
from scipy.sparse.linalg import spsolve
|
|
42
|
+
|
|
43
|
+
# ─── Smoothing ────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def smooth_moving_average(y: np.ndarray, window: int) -> np.ndarray:
|
|
47
|
+
"""Smooth a signal using a centred rolling mean.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
y : np.ndarray
|
|
52
|
+
1-D input signal.
|
|
53
|
+
window : int
|
|
54
|
+
Number of points in the rolling window (must be ≥ 1).
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
np.ndarray
|
|
59
|
+
Smoothed signal (same length as *y*).
|
|
60
|
+
"""
|
|
61
|
+
import pandas as pd
|
|
62
|
+
|
|
63
|
+
y = np.asarray(y, dtype=float)
|
|
64
|
+
if window < 1:
|
|
65
|
+
raise ValueError("window must be ≥ 1")
|
|
66
|
+
return pd.Series(y).rolling(window=window, center=True, min_periods=1).mean().to_numpy()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def smooth_savitzky_golay(y: np.ndarray, window: int, poly_order: int) -> np.ndarray:
|
|
70
|
+
"""Smooth a signal using the Savitzky-Golay algorithm.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
y : np.ndarray
|
|
75
|
+
1-D input signal.
|
|
76
|
+
window : int
|
|
77
|
+
Window length (must be odd and > poly_order).
|
|
78
|
+
poly_order : int
|
|
79
|
+
Polynomial order for the local least-squares fit.
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
np.ndarray
|
|
84
|
+
Smoothed signal (same length as *y*).
|
|
85
|
+
"""
|
|
86
|
+
y = np.asarray(y, dtype=float)
|
|
87
|
+
if window % 2 == 0:
|
|
88
|
+
raise ValueError("window must be odd")
|
|
89
|
+
if poly_order >= window:
|
|
90
|
+
raise ValueError("poly_order must be less than window")
|
|
91
|
+
return savgol_filter(y, window_length=window, polyorder=poly_order)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def smooth_gaussian(y: np.ndarray, sigma: float) -> np.ndarray:
|
|
95
|
+
"""Smooth a signal by convolution with a Gaussian kernel.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
y : np.ndarray
|
|
100
|
+
1-D input signal.
|
|
101
|
+
sigma : float
|
|
102
|
+
Standard deviation of the Gaussian in index units.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
np.ndarray
|
|
107
|
+
Smoothed signal (same length as *y*).
|
|
108
|
+
"""
|
|
109
|
+
y = np.asarray(y, dtype=float)
|
|
110
|
+
if sigma <= 0:
|
|
111
|
+
raise ValueError("sigma must be > 0")
|
|
112
|
+
return gaussian_filter1d(y, sigma=sigma)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ─── Filtering ────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _butterworth(
|
|
119
|
+
y: np.ndarray,
|
|
120
|
+
btype: str,
|
|
121
|
+
cutoff: Any,
|
|
122
|
+
fs: float,
|
|
123
|
+
order: int = 4,
|
|
124
|
+
) -> np.ndarray:
|
|
125
|
+
"""Zero-phase Butterworth filter (second-order sections, sosfiltfilt)."""
|
|
126
|
+
y = np.asarray(y, dtype=float)
|
|
127
|
+
nyq = fs / 2.0
|
|
128
|
+
if isinstance(cutoff, (list, tuple)):
|
|
129
|
+
wn = [cutoff[0] / nyq, cutoff[1] / nyq]
|
|
130
|
+
else:
|
|
131
|
+
wn = cutoff / nyq
|
|
132
|
+
sos = butter(order, wn, btype=btype, output="sos")
|
|
133
|
+
return sosfiltfilt(sos, y)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def filter_lowpass(y: np.ndarray, cutoff: float, fs: float, order: int = 4) -> np.ndarray:
|
|
137
|
+
"""Apply a zero-phase Butterworth low-pass filter.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
y : np.ndarray
|
|
142
|
+
1-D input signal.
|
|
143
|
+
cutoff : float
|
|
144
|
+
Cut-off frequency in the same units as *fs*.
|
|
145
|
+
fs : float
|
|
146
|
+
Sampling frequency.
|
|
147
|
+
order : int
|
|
148
|
+
Filter order (default 4).
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
np.ndarray
|
|
153
|
+
Filtered signal.
|
|
154
|
+
"""
|
|
155
|
+
return _butterworth(y, "lowpass", cutoff, fs, order)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def filter_highpass(y: np.ndarray, cutoff: float, fs: float, order: int = 4) -> np.ndarray:
|
|
159
|
+
"""Apply a zero-phase Butterworth high-pass filter.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
y : np.ndarray
|
|
164
|
+
1-D input signal.
|
|
165
|
+
cutoff : float
|
|
166
|
+
Cut-off frequency in the same units as *fs*.
|
|
167
|
+
fs : float
|
|
168
|
+
Sampling frequency.
|
|
169
|
+
order : int
|
|
170
|
+
Filter order (default 4).
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
np.ndarray
|
|
175
|
+
Filtered signal.
|
|
176
|
+
"""
|
|
177
|
+
return _butterworth(y, "highpass", cutoff, fs, order)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def filter_bandpass(
|
|
181
|
+
y: np.ndarray, low: float, high: float, fs: float, order: int = 4
|
|
182
|
+
) -> np.ndarray:
|
|
183
|
+
"""Apply a zero-phase Butterworth band-pass filter.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
y : np.ndarray
|
|
188
|
+
1-D input signal.
|
|
189
|
+
low : float
|
|
190
|
+
Lower cut-off frequency.
|
|
191
|
+
high : float
|
|
192
|
+
Upper cut-off frequency.
|
|
193
|
+
fs : float
|
|
194
|
+
Sampling frequency.
|
|
195
|
+
order : int
|
|
196
|
+
Filter order (default 4).
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
np.ndarray
|
|
201
|
+
Filtered signal.
|
|
202
|
+
"""
|
|
203
|
+
return _butterworth(y, "bandpass", [low, high], fs, order)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def filter_bandstop(
|
|
207
|
+
y: np.ndarray, low: float, high: float, fs: float, order: int = 4
|
|
208
|
+
) -> np.ndarray:
|
|
209
|
+
"""Apply a zero-phase Butterworth band-stop (notch) filter.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
y : np.ndarray
|
|
214
|
+
1-D input signal.
|
|
215
|
+
low : float
|
|
216
|
+
Lower cut-off frequency.
|
|
217
|
+
high : float
|
|
218
|
+
Upper cut-off frequency.
|
|
219
|
+
fs : float
|
|
220
|
+
Sampling frequency.
|
|
221
|
+
order : int
|
|
222
|
+
Filter order (default 4).
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
np.ndarray
|
|
227
|
+
Filtered signal.
|
|
228
|
+
"""
|
|
229
|
+
return _butterworth(y, "bandstop", [low, high], fs, order)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ─── Frequency domain ─────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def fft(y: np.ndarray, dt: float = 1.0) -> Dict[str, Any]:
|
|
236
|
+
"""Compute the real-input FFT and return a summary dictionary.
|
|
237
|
+
|
|
238
|
+
Parameters
|
|
239
|
+
----------
|
|
240
|
+
y : np.ndarray
|
|
241
|
+
1-D real signal.
|
|
242
|
+
dt : float
|
|
243
|
+
Sample spacing (1 / sampling_frequency). Default 1.0.
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
dict
|
|
248
|
+
Keys:
|
|
249
|
+
|
|
250
|
+
- ``frequencies`` : one-sided frequency array
|
|
251
|
+
- ``amplitudes`` : one-sided amplitude array (two-sided correction applied)
|
|
252
|
+
- ``power`` : amplitudes²
|
|
253
|
+
- ``phases`` : phase angles (radians)
|
|
254
|
+
- ``spectrum`` : full complex spectrum (pass to :func:`ifft`)
|
|
255
|
+
- ``dt`` : sample spacing used
|
|
256
|
+
- ``n`` : number of input samples
|
|
257
|
+
"""
|
|
258
|
+
y = np.asarray(y, dtype=float)
|
|
259
|
+
n = len(y)
|
|
260
|
+
freqs = fftfreq(n, d=dt)
|
|
261
|
+
spectrum = _scipy_fft(y)
|
|
262
|
+
pos_mask = freqs >= 0
|
|
263
|
+
pos_freqs = freqs[pos_mask]
|
|
264
|
+
pos_spectrum = spectrum[pos_mask]
|
|
265
|
+
amplitudes = np.abs(pos_spectrum) * 2.0 / n
|
|
266
|
+
if n % 2 == 0:
|
|
267
|
+
amplitudes[-1] /= 2.0 # Nyquist bin is not doubled
|
|
268
|
+
phases = np.angle(pos_spectrum)
|
|
269
|
+
power = amplitudes**2
|
|
270
|
+
return {
|
|
271
|
+
"frequencies": pos_freqs,
|
|
272
|
+
"amplitudes": amplitudes,
|
|
273
|
+
"power": power,
|
|
274
|
+
"phases": phases,
|
|
275
|
+
"spectrum": spectrum,
|
|
276
|
+
"dt": dt,
|
|
277
|
+
"n": n,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def ifft(spectrum: np.ndarray) -> np.ndarray:
|
|
282
|
+
"""Reconstruct a real signal from a complex frequency spectrum.
|
|
283
|
+
|
|
284
|
+
Parameters
|
|
285
|
+
----------
|
|
286
|
+
spectrum : np.ndarray
|
|
287
|
+
Full complex spectrum as returned in ``fft(y)["spectrum"]``.
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
-------
|
|
291
|
+
np.ndarray
|
|
292
|
+
Reconstructed real signal.
|
|
293
|
+
"""
|
|
294
|
+
return np.real(_scipy_ifft(spectrum))
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def power_spectrum(y: np.ndarray, dt: float = 1.0) -> Dict[str, Any]:
|
|
298
|
+
"""Compute the one-sided power spectrum of a real signal.
|
|
299
|
+
|
|
300
|
+
Parameters
|
|
301
|
+
----------
|
|
302
|
+
y : np.ndarray
|
|
303
|
+
1-D real signal.
|
|
304
|
+
dt : float
|
|
305
|
+
Sample spacing. Default 1.0.
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
dict
|
|
310
|
+
Keys: ``frequencies``, ``power``.
|
|
311
|
+
"""
|
|
312
|
+
result = fft(y, dt=dt)
|
|
313
|
+
return {"frequencies": result["frequencies"], "power": result["power"]}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ─── Derivatives ──────────────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def derivative(
|
|
320
|
+
y: np.ndarray,
|
|
321
|
+
x: Optional[np.ndarray] = None,
|
|
322
|
+
order: int = 1,
|
|
323
|
+
) -> np.ndarray:
|
|
324
|
+
"""Compute a numerical derivative using numpy.gradient (central differences).
|
|
325
|
+
|
|
326
|
+
Parameters
|
|
327
|
+
----------
|
|
328
|
+
y : np.ndarray
|
|
329
|
+
1-D signal values.
|
|
330
|
+
x : np.ndarray, optional
|
|
331
|
+
Corresponding x-axis values. If ``None``, unit spacing is assumed.
|
|
332
|
+
order : int
|
|
333
|
+
Derivative order (1 or 2). Default 1.
|
|
334
|
+
|
|
335
|
+
Returns
|
|
336
|
+
-------
|
|
337
|
+
np.ndarray
|
|
338
|
+
d^order y / dx^order, same length as *y*.
|
|
339
|
+
"""
|
|
340
|
+
y = np.asarray(y, dtype=float)
|
|
341
|
+
if order not in (1, 2):
|
|
342
|
+
raise ValueError("order must be 1 or 2")
|
|
343
|
+
if x is not None:
|
|
344
|
+
x = np.asarray(x, dtype=float)
|
|
345
|
+
dy = np.gradient(y, x)
|
|
346
|
+
if order == 2:
|
|
347
|
+
dy = np.gradient(dy, x)
|
|
348
|
+
else:
|
|
349
|
+
dy = np.gradient(y)
|
|
350
|
+
if order == 2:
|
|
351
|
+
dy = np.gradient(dy)
|
|
352
|
+
return dy
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ─── Baseline correction ──────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def baseline_polynomial(
|
|
359
|
+
y: np.ndarray,
|
|
360
|
+
x: np.ndarray,
|
|
361
|
+
degree: int,
|
|
362
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
363
|
+
"""Fit a polynomial baseline and subtract it from the signal.
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
y : np.ndarray
|
|
368
|
+
Signal values.
|
|
369
|
+
x : np.ndarray
|
|
370
|
+
Corresponding x-axis values.
|
|
371
|
+
degree : int
|
|
372
|
+
Polynomial degree.
|
|
373
|
+
|
|
374
|
+
Returns
|
|
375
|
+
-------
|
|
376
|
+
(baseline, corrected) : tuple of np.ndarray
|
|
377
|
+
``baseline`` is the fitted polynomial; ``corrected = y - baseline``.
|
|
378
|
+
"""
|
|
379
|
+
y = np.asarray(y, dtype=float)
|
|
380
|
+
x = np.asarray(x, dtype=float)
|
|
381
|
+
coeffs = np.polyfit(x, y, degree)
|
|
382
|
+
baseline = np.polyval(coeffs, x)
|
|
383
|
+
return baseline, y - baseline
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def baseline_rolling_ball(
|
|
387
|
+
y: np.ndarray,
|
|
388
|
+
radius: int,
|
|
389
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
390
|
+
"""Estimate baseline using the rolling-ball (morphological opening) algorithm.
|
|
391
|
+
|
|
392
|
+
Implemented as 1-D morphological opening: erosion (minimum filter)
|
|
393
|
+
followed by dilation (maximum filter).
|
|
394
|
+
|
|
395
|
+
Parameters
|
|
396
|
+
----------
|
|
397
|
+
y : np.ndarray
|
|
398
|
+
Signal values.
|
|
399
|
+
radius : int
|
|
400
|
+
Ball radius in samples. Larger values → smoother baseline.
|
|
401
|
+
|
|
402
|
+
Returns
|
|
403
|
+
-------
|
|
404
|
+
(baseline, corrected) : tuple of np.ndarray
|
|
405
|
+
``baseline`` is the rolling-ball estimate; ``corrected = y - baseline``.
|
|
406
|
+
"""
|
|
407
|
+
y = np.asarray(y, dtype=float)
|
|
408
|
+
size = 2 * int(radius) + 1
|
|
409
|
+
eroded = minimum_filter1d(y, size=size)
|
|
410
|
+
baseline = maximum_filter1d(eroded, size=size)
|
|
411
|
+
return baseline, y - baseline
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def baseline_als(
|
|
415
|
+
y: np.ndarray,
|
|
416
|
+
lam: float = 1e5,
|
|
417
|
+
p: float = 0.01,
|
|
418
|
+
max_iter: int = 10,
|
|
419
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
420
|
+
"""Asymmetric least squares (ALS) baseline correction.
|
|
421
|
+
|
|
422
|
+
Fits a smooth baseline by penalising deviations asymmetrically — peaks
|
|
423
|
+
above the baseline are down-weighted to push the baseline toward the minima.
|
|
424
|
+
|
|
425
|
+
Parameters
|
|
426
|
+
----------
|
|
427
|
+
y : np.ndarray
|
|
428
|
+
Signal values.
|
|
429
|
+
lam : float
|
|
430
|
+
Smoothness parameter (λ). Larger → smoother baseline. Default 1e5.
|
|
431
|
+
p : float
|
|
432
|
+
Asymmetry parameter (fraction of weight assigned to points above the
|
|
433
|
+
baseline). Typically 0.001–0.1. Default 0.01.
|
|
434
|
+
max_iter : int
|
|
435
|
+
Maximum iterations. Default 10.
|
|
436
|
+
|
|
437
|
+
Returns
|
|
438
|
+
-------
|
|
439
|
+
(baseline, corrected) : tuple of np.ndarray
|
|
440
|
+
``baseline`` is the ALS estimate; ``corrected = y - baseline``.
|
|
441
|
+
|
|
442
|
+
References
|
|
443
|
+
----------
|
|
444
|
+
Eilers & Boelens (2005), *Baseline Correction with Asymmetric Least
|
|
445
|
+
Squares Smoothing*.
|
|
446
|
+
"""
|
|
447
|
+
y = np.asarray(y, dtype=float)
|
|
448
|
+
n = len(y)
|
|
449
|
+
D_sp = diags([1, -2, 1], [0, 1, 2], shape=(n - 2, n), dtype=float, format="csc")
|
|
450
|
+
DtD = lam * (D_sp.T @ D_sp)
|
|
451
|
+
w = np.ones(n)
|
|
452
|
+
z = y.copy()
|
|
453
|
+
for _ in range(max_iter):
|
|
454
|
+
W = diags(w, 0, dtype=float, format="csc")
|
|
455
|
+
z = spsolve(W + DtD, w * y)
|
|
456
|
+
w = p * (y > z) + (1 - p) * (y <= z)
|
|
457
|
+
return z, y - z
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
# ─── Interpolation ────────────────────────────────────────────────────────────
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def interpolate(
|
|
464
|
+
x: np.ndarray,
|
|
465
|
+
y: np.ndarray,
|
|
466
|
+
x_new: np.ndarray,
|
|
467
|
+
method: str = "cubic",
|
|
468
|
+
) -> np.ndarray:
|
|
469
|
+
"""Interpolate a signal to a new x-grid.
|
|
470
|
+
|
|
471
|
+
Parameters
|
|
472
|
+
----------
|
|
473
|
+
x : np.ndarray
|
|
474
|
+
Original x-axis values (must be monotonically increasing).
|
|
475
|
+
y : np.ndarray
|
|
476
|
+
Corresponding signal values.
|
|
477
|
+
x_new : np.ndarray
|
|
478
|
+
New x-axis values at which to evaluate the interpolant.
|
|
479
|
+
method : str
|
|
480
|
+
One of ``'linear'``, ``'quadratic'``, ``'cubic'``, ``'nearest'``,
|
|
481
|
+
``'previous'``. Default ``'cubic'``.
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
np.ndarray
|
|
486
|
+
Interpolated y values at *x_new*.
|
|
487
|
+
"""
|
|
488
|
+
x = np.asarray(x, dtype=float)
|
|
489
|
+
y = np.asarray(y, dtype=float)
|
|
490
|
+
x_new = np.asarray(x_new, dtype=float)
|
|
491
|
+
if method == "cubic":
|
|
492
|
+
return CubicSpline(x, y)(x_new)
|
|
493
|
+
f = interp1d(x, y, kind=method, fill_value="extrapolate")
|
|
494
|
+
return f(x_new)
|