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/batch.py
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""Batch processing utilities for EaglePlot.
|
|
2
|
+
|
|
3
|
+
Provides helpers for scanning directories, loading multiple files in bulk,
|
|
4
|
+
and running common analyses (statistics, curve fitting, peak detection) across
|
|
5
|
+
entire datasets in one call.
|
|
6
|
+
|
|
7
|
+
No Streamlit dependency — safe to import from CLI or notebooks.
|
|
8
|
+
|
|
9
|
+
Public API
|
|
10
|
+
----------
|
|
11
|
+
scan_directory(folder_path, extensions=None, pattern=None) -> list[Path]
|
|
12
|
+
Return sorted paths matching the given criteria.
|
|
13
|
+
batch_load_files(file_paths, on_error='skip') -> dict
|
|
14
|
+
Load many files; returns datasets, errors, and metadata.
|
|
15
|
+
batch_statistics(datasets, columns=None) -> pd.DataFrame
|
|
16
|
+
Descriptive statistics across all DataFrame datasets.
|
|
17
|
+
batch_curve_fit(datasets, x_col, y_col, fit_type='linear', degree=2) -> pd.DataFrame
|
|
18
|
+
Curve fit one x/y column pair across all DataFrame datasets.
|
|
19
|
+
batch_peak_analysis(datasets, y_col, x_col=None, ...) -> pd.DataFrame
|
|
20
|
+
Find peaks in y_col across all DataFrame datasets.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import fnmatch
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
import pandas as pd
|
|
31
|
+
|
|
32
|
+
from eagleplot.io import load_data
|
|
33
|
+
from eagleplot.math import (
|
|
34
|
+
calculate_statistics,
|
|
35
|
+
fit_exponential,
|
|
36
|
+
fit_linear,
|
|
37
|
+
fit_polynomial,
|
|
38
|
+
)
|
|
39
|
+
from eagleplot.peaks import compute_fwhm, find_peaks
|
|
40
|
+
|
|
41
|
+
# ── Constants ─────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
_SUPPORTED_EXTENSIONS: set[str] = {
|
|
44
|
+
".pkl",
|
|
45
|
+
".npy",
|
|
46
|
+
".npz",
|
|
47
|
+
".csv",
|
|
48
|
+
".xlsx",
|
|
49
|
+
".xls",
|
|
50
|
+
".tsv",
|
|
51
|
+
".json",
|
|
52
|
+
".parquet",
|
|
53
|
+
".jdx",
|
|
54
|
+
".dx",
|
|
55
|
+
".h5",
|
|
56
|
+
".hdf5",
|
|
57
|
+
".nc",
|
|
58
|
+
".cdf",
|
|
59
|
+
".spc",
|
|
60
|
+
".asc",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _normalise_extensions(extensions: list[str] | None) -> set[str] | None:
|
|
67
|
+
"""Return a set of lower-case dot-prefixed extensions, or None for 'all'."""
|
|
68
|
+
if extensions is None:
|
|
69
|
+
return None
|
|
70
|
+
normalised = set()
|
|
71
|
+
for ext in extensions:
|
|
72
|
+
ext = ext.strip().lower()
|
|
73
|
+
if not ext.startswith("."):
|
|
74
|
+
ext = "." + ext
|
|
75
|
+
normalised.add(ext)
|
|
76
|
+
return normalised
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _file_metadata(path: Path, data: Any) -> dict:
|
|
80
|
+
"""Return a metadata dict for a single loaded file."""
|
|
81
|
+
meta: dict[str, Any] = {
|
|
82
|
+
"source": str(path),
|
|
83
|
+
"size_bytes": path.stat().st_size if path.exists() else 0,
|
|
84
|
+
"suffix": path.suffix.lower(),
|
|
85
|
+
}
|
|
86
|
+
if isinstance(data, pd.DataFrame):
|
|
87
|
+
meta["shape"] = data.shape
|
|
88
|
+
meta["columns"] = list(data.columns)
|
|
89
|
+
elif isinstance(data, np.ndarray):
|
|
90
|
+
meta["shape"] = data.shape
|
|
91
|
+
meta["dtype"] = str(data.dtype)
|
|
92
|
+
return meta
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ── Public API ────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def scan_directory(
|
|
99
|
+
folder_path: str | Path,
|
|
100
|
+
extensions: list[str] | None = None,
|
|
101
|
+
pattern: str | None = None,
|
|
102
|
+
) -> list[Path]:
|
|
103
|
+
"""Scan a directory and return sorted matching file paths.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
folder_path: Path to the directory to scan.
|
|
107
|
+
extensions: List of file extensions to include, e.g. ``['csv', 'xlsx']``.
|
|
108
|
+
Leading dots are optional. Pass ``None`` (default) to accept all
|
|
109
|
+
EaglePlot-supported formats.
|
|
110
|
+
pattern: Optional ``fnmatch``-style filename filter, e.g.
|
|
111
|
+
``'sample_*.csv'``. Applied after the extension filter.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Sorted list of ``Path`` objects for every matching file.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
NotADirectoryError: If ``folder_path`` is not a directory.
|
|
118
|
+
"""
|
|
119
|
+
folder = Path(folder_path)
|
|
120
|
+
if not folder.is_dir():
|
|
121
|
+
raise NotADirectoryError(f"Not a directory: {folder}")
|
|
122
|
+
|
|
123
|
+
allowed_exts = _normalise_extensions(extensions)
|
|
124
|
+
if allowed_exts is None:
|
|
125
|
+
allowed_exts = _SUPPORTED_EXTENSIONS
|
|
126
|
+
|
|
127
|
+
results: list[Path] = []
|
|
128
|
+
for p in folder.iterdir():
|
|
129
|
+
if not p.is_file():
|
|
130
|
+
continue
|
|
131
|
+
if p.suffix.lower() not in allowed_exts:
|
|
132
|
+
continue
|
|
133
|
+
if pattern is not None and not fnmatch.fnmatch(p.name, pattern):
|
|
134
|
+
continue
|
|
135
|
+
results.append(p)
|
|
136
|
+
|
|
137
|
+
return sorted(results)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def batch_load_files(
|
|
141
|
+
file_paths: list[str | Path],
|
|
142
|
+
on_error: str = "skip",
|
|
143
|
+
) -> dict:
|
|
144
|
+
"""Load multiple files and return a structured result dict.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
file_paths: Iterable of file paths to load.
|
|
148
|
+
on_error: What to do when a file fails to load.
|
|
149
|
+
``'skip'`` silently records the error and continues;
|
|
150
|
+
``'raise'`` re-raises the exception immediately.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
A dict with three keys:
|
|
154
|
+
|
|
155
|
+
- ``'datasets'``: ``{name: data}`` — successfully loaded objects.
|
|
156
|
+
- ``'errors'``: ``{name: error_message}`` — files that failed.
|
|
157
|
+
- ``'metadata'``: ``{name: {...}}`` — per-file metadata (source path,
|
|
158
|
+
size_bytes, suffix; plus shape/columns for DataFrames and
|
|
159
|
+
shape/dtype for ndarrays).
|
|
160
|
+
|
|
161
|
+
The *name* key is the bare filename (e.g. ``'data.csv'``).
|
|
162
|
+
"""
|
|
163
|
+
if on_error not in ("skip", "raise"):
|
|
164
|
+
raise ValueError(f"on_error must be 'skip' or 'raise', got {on_error!r}")
|
|
165
|
+
|
|
166
|
+
datasets: dict[str, Any] = {}
|
|
167
|
+
errors: dict[str, str] = {}
|
|
168
|
+
metadata: dict[str, dict] = {}
|
|
169
|
+
|
|
170
|
+
for raw_path in file_paths:
|
|
171
|
+
path = Path(raw_path)
|
|
172
|
+
name = path.name
|
|
173
|
+
try:
|
|
174
|
+
data = load_data(str(path))
|
|
175
|
+
datasets[name] = data
|
|
176
|
+
metadata[name] = _file_metadata(path, data)
|
|
177
|
+
except Exception as exc: # noqa: BLE001
|
|
178
|
+
if on_error == "raise":
|
|
179
|
+
raise
|
|
180
|
+
errors[name] = str(exc)
|
|
181
|
+
|
|
182
|
+
return {"datasets": datasets, "errors": errors, "metadata": metadata}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def batch_statistics(
|
|
186
|
+
datasets: dict[str, Any],
|
|
187
|
+
columns: list[str] | None = None,
|
|
188
|
+
) -> pd.DataFrame:
|
|
189
|
+
"""Compute descriptive statistics across multiple datasets.
|
|
190
|
+
|
|
191
|
+
Only ``pd.DataFrame`` datasets are processed; others are silently skipped.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
datasets: Mapping of dataset name to loaded data object.
|
|
195
|
+
columns: Column names to analyse. Pass ``None`` (default) to use all
|
|
196
|
+
numeric columns in each DataFrame.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
A ``pd.DataFrame`` with one row per (dataset, column) combination and
|
|
200
|
+
columns: ``dataset, column, n, mean, median, std, min, max, q1, q3,
|
|
201
|
+
iqr, range``. Returns an empty DataFrame with those columns if there
|
|
202
|
+
is nothing to compute.
|
|
203
|
+
"""
|
|
204
|
+
_COLS = [
|
|
205
|
+
"dataset",
|
|
206
|
+
"column",
|
|
207
|
+
"n",
|
|
208
|
+
"mean",
|
|
209
|
+
"median",
|
|
210
|
+
"std",
|
|
211
|
+
"min",
|
|
212
|
+
"max",
|
|
213
|
+
"q1",
|
|
214
|
+
"q3",
|
|
215
|
+
"iqr",
|
|
216
|
+
"range",
|
|
217
|
+
]
|
|
218
|
+
rows: list[dict] = []
|
|
219
|
+
|
|
220
|
+
for name, data in datasets.items():
|
|
221
|
+
if not isinstance(data, pd.DataFrame):
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
df: pd.DataFrame = data
|
|
225
|
+
if columns is not None:
|
|
226
|
+
target_cols = [c for c in columns if c in df.columns]
|
|
227
|
+
else:
|
|
228
|
+
target_cols = list(df.select_dtypes(include="number").columns)
|
|
229
|
+
|
|
230
|
+
for col in target_cols:
|
|
231
|
+
series = df[col].dropna()
|
|
232
|
+
if series.empty:
|
|
233
|
+
continue
|
|
234
|
+
arr = series.to_numpy(dtype=float)
|
|
235
|
+
stats = calculate_statistics(arr)
|
|
236
|
+
rows.append(
|
|
237
|
+
{
|
|
238
|
+
"dataset": name,
|
|
239
|
+
"column": col,
|
|
240
|
+
"n": int(len(arr)),
|
|
241
|
+
"mean": stats["mean"],
|
|
242
|
+
"median": stats["median"],
|
|
243
|
+
"std": stats["std"],
|
|
244
|
+
"min": stats["min"],
|
|
245
|
+
"max": stats["max"],
|
|
246
|
+
"q1": stats["q1"],
|
|
247
|
+
"q3": stats["q3"],
|
|
248
|
+
"iqr": stats["iqr"],
|
|
249
|
+
"range": stats["range"],
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if not rows:
|
|
254
|
+
return pd.DataFrame(columns=_COLS)
|
|
255
|
+
return pd.DataFrame(rows, columns=_COLS)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def batch_curve_fit(
|
|
259
|
+
datasets: dict[str, Any],
|
|
260
|
+
x_col: str,
|
|
261
|
+
y_col: str,
|
|
262
|
+
fit_type: str = "linear",
|
|
263
|
+
degree: int = 2,
|
|
264
|
+
) -> pd.DataFrame:
|
|
265
|
+
"""Fit a curve to x_col vs y_col across multiple datasets.
|
|
266
|
+
|
|
267
|
+
Only ``pd.DataFrame`` datasets that contain both ``x_col`` and ``y_col``
|
|
268
|
+
are processed; others are silently skipped.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
datasets: Mapping of dataset name to loaded data object.
|
|
272
|
+
x_col: Name of the independent-variable column.
|
|
273
|
+
y_col: Name of the dependent-variable column.
|
|
274
|
+
fit_type: One of ``'linear'``, ``'polynomial'``, or
|
|
275
|
+
``'exponential'``.
|
|
276
|
+
degree: Polynomial degree; only used when ``fit_type='polynomial'``.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
A ``pd.DataFrame`` with one row per dataset. Columns depend on
|
|
280
|
+
``fit_type``:
|
|
281
|
+
|
|
282
|
+
- *linear*: ``dataset, slope, intercept, r_squared, p_value, error``
|
|
283
|
+
- *polynomial*: ``dataset, degree, r_squared, coeff_0 … coeff_N, error``
|
|
284
|
+
- *exponential*: ``dataset, a, b, c, r_squared, error``
|
|
285
|
+
|
|
286
|
+
The ``error`` column is ``None`` on success, or an error message string
|
|
287
|
+
on failure. Returns an empty ``pd.DataFrame`` if nothing could be
|
|
288
|
+
computed.
|
|
289
|
+
"""
|
|
290
|
+
rows: list[dict] = []
|
|
291
|
+
|
|
292
|
+
for name, data in datasets.items():
|
|
293
|
+
if not isinstance(data, pd.DataFrame):
|
|
294
|
+
continue
|
|
295
|
+
df: pd.DataFrame = data
|
|
296
|
+
if x_col not in df.columns or y_col not in df.columns:
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
sub = df[[x_col, y_col]].dropna()
|
|
300
|
+
x = sub[x_col].to_numpy(dtype=float)
|
|
301
|
+
y = sub[y_col].to_numpy(dtype=float)
|
|
302
|
+
|
|
303
|
+
row: dict[str, Any] = {"dataset": name, "error": None}
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
if fit_type == "linear":
|
|
307
|
+
result = fit_linear(x, y)
|
|
308
|
+
row.update(
|
|
309
|
+
{
|
|
310
|
+
"slope": result["slope"],
|
|
311
|
+
"intercept": result["intercept"],
|
|
312
|
+
"r_squared": result["r_squared"],
|
|
313
|
+
"p_value": result["p_value"],
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
elif fit_type == "polynomial":
|
|
317
|
+
result = fit_polynomial(x, y, degree)
|
|
318
|
+
row["degree"] = degree
|
|
319
|
+
row["r_squared"] = result["r_squared"]
|
|
320
|
+
for i, coeff in enumerate(result["coefficients"]): # type: ignore[var-annotated,arg-type]
|
|
321
|
+
row[f"coeff_{i}"] = coeff
|
|
322
|
+
elif fit_type == "exponential":
|
|
323
|
+
result = fit_exponential(x, y)
|
|
324
|
+
row.update(
|
|
325
|
+
{
|
|
326
|
+
"a": result["a"],
|
|
327
|
+
"b": result["b"],
|
|
328
|
+
"c": result["c"],
|
|
329
|
+
"r_squared": result["r_squared"],
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
else:
|
|
333
|
+
row["error"] = f"Unknown fit_type: {fit_type!r}"
|
|
334
|
+
except Exception as exc: # noqa: BLE001
|
|
335
|
+
row["error"] = str(exc)
|
|
336
|
+
|
|
337
|
+
rows.append(row)
|
|
338
|
+
|
|
339
|
+
if not rows:
|
|
340
|
+
return pd.DataFrame()
|
|
341
|
+
return pd.DataFrame(rows)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def batch_peak_analysis(
|
|
345
|
+
datasets: dict[str, Any],
|
|
346
|
+
y_col: str,
|
|
347
|
+
x_col: str | None = None,
|
|
348
|
+
height: float | None = None,
|
|
349
|
+
prominence: float | None = None,
|
|
350
|
+
distance: int | None = None,
|
|
351
|
+
) -> pd.DataFrame:
|
|
352
|
+
"""Find peaks in y_col across multiple datasets.
|
|
353
|
+
|
|
354
|
+
Only ``pd.DataFrame`` datasets that contain ``y_col`` are processed;
|
|
355
|
+
others are silently skipped. If ``x_col`` is not given, integer row
|
|
356
|
+
indices are used as the x axis.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
datasets: Mapping of dataset name to loaded data object.
|
|
360
|
+
y_col: Name of the column containing the signal.
|
|
361
|
+
x_col: Name of the x-axis column. Defaults to integer indices.
|
|
362
|
+
height: Minimum absolute peak height passed to ``find_peaks``.
|
|
363
|
+
prominence: Minimum peak prominence passed to ``find_peaks``.
|
|
364
|
+
distance: Minimum sample distance between peaks passed to
|
|
365
|
+
``find_peaks``.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
A ``pd.DataFrame`` with columns: ``dataset, n_peaks,
|
|
369
|
+
peak_positions, mean_height, max_height, mean_fwhm, error``.
|
|
370
|
+
|
|
371
|
+
- ``peak_positions``: comma-separated string of x values.
|
|
372
|
+
- ``mean_fwhm``: ``NaN`` if FWHM could not be computed.
|
|
373
|
+
- ``error``: ``None`` on success, or an error message string.
|
|
374
|
+
|
|
375
|
+
Returns an empty ``pd.DataFrame`` with those columns if nothing could
|
|
376
|
+
be computed.
|
|
377
|
+
"""
|
|
378
|
+
_COLS = [
|
|
379
|
+
"dataset",
|
|
380
|
+
"n_peaks",
|
|
381
|
+
"peak_positions",
|
|
382
|
+
"mean_height",
|
|
383
|
+
"max_height",
|
|
384
|
+
"mean_fwhm",
|
|
385
|
+
"error",
|
|
386
|
+
]
|
|
387
|
+
rows: list[dict] = []
|
|
388
|
+
|
|
389
|
+
for name, data in datasets.items():
|
|
390
|
+
if not isinstance(data, pd.DataFrame):
|
|
391
|
+
continue
|
|
392
|
+
df: pd.DataFrame = data
|
|
393
|
+
if y_col not in df.columns:
|
|
394
|
+
continue
|
|
395
|
+
|
|
396
|
+
row: dict[str, Any] = {
|
|
397
|
+
"dataset": name,
|
|
398
|
+
"n_peaks": 0,
|
|
399
|
+
"peak_positions": "",
|
|
400
|
+
"mean_height": float("nan"),
|
|
401
|
+
"max_height": float("nan"),
|
|
402
|
+
"mean_fwhm": float("nan"),
|
|
403
|
+
"error": None,
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
y = df[y_col].to_numpy(dtype=float)
|
|
408
|
+
if x_col is not None and x_col in df.columns:
|
|
409
|
+
x = df[x_col].to_numpy(dtype=float)
|
|
410
|
+
else:
|
|
411
|
+
x = np.arange(len(y), dtype=float)
|
|
412
|
+
|
|
413
|
+
peaks_dict = find_peaks(
|
|
414
|
+
y,
|
|
415
|
+
x=x,
|
|
416
|
+
height=height,
|
|
417
|
+
prominence=prominence,
|
|
418
|
+
distance=distance,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
n = peaks_dict["n_peaks"]
|
|
422
|
+
row["n_peaks"] = n
|
|
423
|
+
|
|
424
|
+
if n > 0:
|
|
425
|
+
positions = peaks_dict["positions"]
|
|
426
|
+
heights = peaks_dict["heights"]
|
|
427
|
+
row["peak_positions"] = ", ".join(f"{p:.6g}" for p in positions)
|
|
428
|
+
row["mean_height"] = float(np.mean(heights))
|
|
429
|
+
row["max_height"] = float(np.max(heights))
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
fwhm_dict = compute_fwhm(y, x, peaks_dict)
|
|
433
|
+
fwhm_vals = fwhm_dict["fwhm"]
|
|
434
|
+
if len(fwhm_vals) > 0:
|
|
435
|
+
row["mean_fwhm"] = float(np.mean(fwhm_vals))
|
|
436
|
+
except Exception: # noqa: BLE001
|
|
437
|
+
pass # mean_fwhm stays NaN
|
|
438
|
+
|
|
439
|
+
except Exception as exc: # noqa: BLE001
|
|
440
|
+
row["error"] = str(exc)
|
|
441
|
+
|
|
442
|
+
rows.append(row)
|
|
443
|
+
|
|
444
|
+
if not rows:
|
|
445
|
+
return pd.DataFrame(columns=_COLS)
|
|
446
|
+
return pd.DataFrame(rows, columns=_COLS)
|