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.
Files changed (73) hide show
  1. eagleplot/Home.py +362 -0
  2. eagleplot/__init__.py +19 -0
  3. eagleplot/annotations.py +215 -0
  4. eagleplot/assets/logo.ico +0 -0
  5. eagleplot/assets/logo.png +0 -0
  6. eagleplot/assets/nccu-horiz-logo.png +0 -0
  7. eagleplot/assets/nccu-wings.png +0 -0
  8. eagleplot/batch.py +446 -0
  9. eagleplot/cli.py +719 -0
  10. eagleplot/data_tools.py +849 -0
  11. eagleplot/example-data/Artificial/bimodal_distribution.csv +501 -0
  12. eagleplot/example-data/Artificial/correlation_matrix.npy +0 -0
  13. eagleplot/example-data/Artificial/gaussian_surface.npy +0 -0
  14. eagleplot/example-data/Artificial/grouped_categorical.csv +241 -0
  15. eagleplot/example-data/Artificial/ir_spectrum.csv +1802 -0
  16. eagleplot/example-data/Artificial/molecular_properties.csv +121 -0
  17. eagleplot/example-data/Artificial/normal_distribution.csv +401 -0
  18. eagleplot/example-data/Artificial/reaction_kinetics.csv +121 -0
  19. eagleplot/example-data/Artificial/scatter_correlation.csv +201 -0
  20. eagleplot/example-data/Artificial/sine_cosine_waves.csv +301 -0
  21. eagleplot/gallery/box_plot.png +0 -0
  22. eagleplot/gallery/contour_plot.png +0 -0
  23. eagleplot/gallery/distribution_plot.png +0 -0
  24. eagleplot/gallery/heatmap.png +0 -0
  25. eagleplot/gallery/histogram.png +0 -0
  26. eagleplot/gallery/interactive_3d_surface.png +0 -0
  27. eagleplot/gallery/interactive_heatmap.png +0 -0
  28. eagleplot/gallery/interactive_histogram.png +0 -0
  29. eagleplot/gallery/interactive_line.png +0 -0
  30. eagleplot/gallery/interactive_scatter.png +0 -0
  31. eagleplot/gallery/line_plot.png +0 -0
  32. eagleplot/gallery/manifest.json +196 -0
  33. eagleplot/gallery/regression_plot.png +0 -0
  34. eagleplot/gallery/scatter_plot.png +0 -0
  35. eagleplot/io.py +1008 -0
  36. eagleplot/math.py +1210 -0
  37. eagleplot/molecular/__init__.py +21 -0
  38. eagleplot/molecular/atom_data.py +287 -0
  39. eagleplot/molecular/parsers.py +747 -0
  40. eagleplot/nist.py +241 -0
  41. eagleplot/pages/10_Molecular_Viz.py +225 -0
  42. eagleplot/pages/11_Batch_Analysis.py +325 -0
  43. eagleplot/pages/12_Help.py +199 -0
  44. eagleplot/pages/13_Settings.py +388 -0
  45. eagleplot/pages/1_Data_Upload.py +521 -0
  46. eagleplot/pages/2_Quick_Plot.py +1693 -0
  47. eagleplot/pages/3_Analysis_Tools.py +2510 -0
  48. eagleplot/pages/4_Multi_Plot_Dashboard.py +791 -0
  49. eagleplot/pages/5_Advanced_Plotting.py +691 -0
  50. eagleplot/pages/6_Export_Results.py +442 -0
  51. eagleplot/pages/7_Gallery.py +201 -0
  52. eagleplot/pages/8_Data_Tools.py +434 -0
  53. eagleplot/pages/9_Spectroscopy.py +821 -0
  54. eagleplot/pages/__init__.py +8 -0
  55. eagleplot/peaks.py +513 -0
  56. eagleplot/plotting.py +2630 -0
  57. eagleplot/plugin_loader.py +208 -0
  58. eagleplot/report.py +448 -0
  59. eagleplot/signal.py +494 -0
  60. eagleplot/spectroscopy.py +830 -0
  61. eagleplot/utils/__init__.py +76 -0
  62. eagleplot/utils/access.py +77 -0
  63. eagleplot/utils/data_preview.py +352 -0
  64. eagleplot/utils/plot_config.py +2721 -0
  65. eagleplot/utils/session_state.py +535 -0
  66. eagleplot/utils/theming.py +312 -0
  67. eagleplot/utils/user_settings.py +294 -0
  68. eagleplot-3.0.0.dist-info/METADATA +304 -0
  69. eagleplot-3.0.0.dist-info/RECORD +73 -0
  70. eagleplot-3.0.0.dist-info/WHEEL +5 -0
  71. eagleplot-3.0.0.dist-info/entry_points.txt +2 -0
  72. eagleplot-3.0.0.dist-info/licenses/LICENSE +21 -0
  73. 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)