funcnodes-span 1.0.2__tar.gz → 1.0.3__tar.gz
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.
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/PKG-INFO +1 -1
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/pyproject.toml +1 -1
- funcnodes_span-1.0.3/src/funcnodes_span/smoothing.py +258 -0
- funcnodes_span-1.0.3/tests/test_smoothing.py +46 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/uv.lock +1 -1
- funcnodes_span-1.0.2/src/funcnodes_span/smoothing.py +0 -212
- funcnodes_span-1.0.2/tests/test_smoothing.py +0 -25
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.flake8 +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/config.json +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/config.json.bu +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_819786f45cc247a7b086ce71d030df11/main.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_819786f45cc247a7b086ce71d030df11/nodespace.json +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_819786f45cc247a7b086ce71d030df11/pyproject.toml +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_819786f45cc247a7b086ce71d030df11/uv.lock +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_819786f45cc247a7b086ce71d030df11.json +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_819786f45cc247a7b086ce71d030df11.p +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_demoworker/nodespace.json +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_demoworker.json +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.github/actions/install_package/action.yml +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.github/workflows/py_test.yml +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.github/workflows/version_publish_main.yml +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.gitignore +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.pre-commit-config.yaml +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.vscode/settings.json +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/CHANGELOG.md +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/LICENSE +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/MANIFEST.in +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/README.md +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/THIRD_PARTY_NOTICES.md +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/pytest.ini +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/reports/mypy-baseline.txt +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/__init__.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/_baseline.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/_curves.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/_smoothing.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/baseline.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/curves.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/fitting.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/normalization.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/peak_analysis.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/peaks.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/__init__.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/all_nodes_test_base.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/test_all_nodes.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/test_baseline.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/test_curves.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/test_fit.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/test_normalization.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/test_peak_analysis.py +0 -0
- {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/test_peaks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "funcnodes-span"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.3"
|
|
4
4
|
description = "SPectral ANalysis (SPAN) for funcnodes"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",]
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
from typing import Dict, Callable, Union
|
|
2
|
+
from funcnodes import NodeDecorator, Shelf
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from scipy.signal import savgol_filter, medfilt
|
|
6
|
+
from scipy.ndimage import gaussian_filter1d, median_filter
|
|
7
|
+
import warnings
|
|
8
|
+
import funcnodes as fn
|
|
9
|
+
|
|
10
|
+
warnings.filterwarnings("ignore")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SmoothMode(fn.DataEnum):
|
|
14
|
+
SAVITZKY_GOLAY = "savgol"
|
|
15
|
+
GAUSSIAN = "gaussian"
|
|
16
|
+
MOVING_AVERAGE = "ma"
|
|
17
|
+
EXPONENTIAL_MOVING_AVERAGE = "ema"
|
|
18
|
+
MEDIAN = "median"
|
|
19
|
+
SPIKE_REMOVE = "remove_spike"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def smooth_savgol(x: np.ndarray, window: int) -> np.ndarray:
|
|
23
|
+
return savgol_filter(x, window, 2)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def smooth_gaussian(x: np.ndarray, window: int) -> np.ndarray:
|
|
27
|
+
return gaussian_filter1d(x, window)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def smooth_ma(x: np.ndarray, window: int) -> np.ndarray:
|
|
31
|
+
if x.ndim > 1:
|
|
32
|
+
n, m = x.shape
|
|
33
|
+
result = np.zeros((n, m))
|
|
34
|
+
for i in range(n):
|
|
35
|
+
result[i, :] = np.convolve(x[i, :], np.ones(window) / window, mode="same")
|
|
36
|
+
return result
|
|
37
|
+
else:
|
|
38
|
+
return np.convolve(x, np.ones(window) / window, mode="same")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def smooth_ema(x: np.ndarray, window: int) -> np.ndarray:
|
|
42
|
+
if x.ndim > 1:
|
|
43
|
+
n, m = x.shape
|
|
44
|
+
result = np.zeros((n, m))
|
|
45
|
+
for i in range(n):
|
|
46
|
+
result[i, :] = pd.Series(x[i, :]).ewm(span=window).mean().values
|
|
47
|
+
return result
|
|
48
|
+
else:
|
|
49
|
+
return pd.Series(x).ewm(span=window).mean().values
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def smooth_median(x: np.ndarray, window: int) -> np.ndarray:
|
|
53
|
+
return medfilt(x, window)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _remove_spikes_1d(y: np.ndarray, threshold: float, window: int) -> np.ndarray:
|
|
57
|
+
"""
|
|
58
|
+
Whitaker & Hayes (2018) modified-z-score cosmic spike removal.
|
|
59
|
+
|
|
60
|
+
1. Compute first differences: delta_i = y_i - y_{i-1}
|
|
61
|
+
2. Modified z-score on delta: z_i = 0.6745 * (delta_i - median(delta)) / MAD(delta)
|
|
62
|
+
3. Flag points where |z_i| > threshold as spikes
|
|
63
|
+
4. Replace flagged points with the median of a local window of
|
|
64
|
+
neighbouring NON-spike values.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
y : 1D spectrum
|
|
69
|
+
threshold : modified z-score cutoff (typically 5-10; lower = more aggressive)
|
|
70
|
+
window : half-width of the window used to compute the replacement value
|
|
71
|
+
"""
|
|
72
|
+
y = np.asarray(y, dtype=float)
|
|
73
|
+
n = len(y)
|
|
74
|
+
|
|
75
|
+
delta = np.diff(y)
|
|
76
|
+
median_delta = np.median(delta)
|
|
77
|
+
mad = np.median(np.abs(delta - median_delta))
|
|
78
|
+
if mad == 0:
|
|
79
|
+
return y.copy() # nothing to do — perfectly smooth diff
|
|
80
|
+
|
|
81
|
+
z = 0.6745 * (delta - median_delta) / mad
|
|
82
|
+
# a spike at index i produces a large |z| at delta[i-1] (jump up)
|
|
83
|
+
# AND a large |z| at delta[i] (jump back down) -> flag both endpoints
|
|
84
|
+
spike_mask = np.zeros(n, dtype=bool)
|
|
85
|
+
flagged = np.where(np.abs(z) > threshold)[0]
|
|
86
|
+
for idx in flagged:
|
|
87
|
+
spike_mask[idx] = True # point before the jump
|
|
88
|
+
spike_mask[idx + 1] = True # point after the jump
|
|
89
|
+
|
|
90
|
+
cleaned = y.copy()
|
|
91
|
+
spike_positions = np.where(spike_mask)[0]
|
|
92
|
+
for idx in spike_positions:
|
|
93
|
+
lo = max(0, idx - window)
|
|
94
|
+
hi = min(n, idx + window + 1)
|
|
95
|
+
local = y[lo:hi]
|
|
96
|
+
local_mask = ~spike_mask[lo:hi]
|
|
97
|
+
if local_mask.sum() == 0:
|
|
98
|
+
continue # entire window is spikes — leave as-is (rare, widen window)
|
|
99
|
+
cleaned[idx] = np.median(local[local_mask])
|
|
100
|
+
|
|
101
|
+
return cleaned
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def smooth_remove_cosmic_spikes(x: np.ndarray, window: int = 5) -> np.ndarray:
|
|
105
|
+
"""
|
|
106
|
+
Remove cosmic-ray spikes via residual deviation from a robust local median.
|
|
107
|
+
|
|
108
|
+
A robust self-estimate of each spectrum is computed using a median filter
|
|
109
|
+
of width ``window``. The residual (input - smoothed) is then converted to
|
|
110
|
+
a modified z-score (Iglewicz & Hoaglin, using the 0.6745 MAD scaling
|
|
111
|
+
factor), and points with |z| > 3.5 — the standard outlier cutoff for the
|
|
112
|
+
modified z-score — are replaced by their corresponding median-filtered
|
|
113
|
+
value.
|
|
114
|
+
|
|
115
|
+
This is threshold-free in the sense that the spike cutoff (3.5) is a
|
|
116
|
+
fixed, statistically-derived constant rather than a user-tuned parameter;
|
|
117
|
+
the only remaining parameter is the median-filter window size, which
|
|
118
|
+
controls how much real spectral structure is treated as "local trend"
|
|
119
|
+
versus "anomaly."
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
x : np.ndarray
|
|
124
|
+
Input spectrum/spectra.
|
|
125
|
+
- 1D, shape ``(n_points,)``: a single spectrum.
|
|
126
|
+
- 2D, shape ``(n_spectra, n_points)``: a batch of spectra. The median
|
|
127
|
+
filter is applied along the last axis only (``size=(1, window)``),
|
|
128
|
+
so each spectrum is processed independently — a spike in one row
|
|
129
|
+
cannot affect the smoothing of another.
|
|
130
|
+
window : int, default=5
|
|
131
|
+
Width of the median filter used to compute the robust local
|
|
132
|
+
self-estimate. Must be odd and >= 3. Larger windows treat broader
|
|
133
|
+
features as "trend" (more tolerant of wide real peaks but slower to
|
|
134
|
+
react to genuine sharp features); smaller windows are more sensitive
|
|
135
|
+
but risk flagging steep real peak edges as spikes.
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
np.ndarray
|
|
140
|
+
Array of the same shape as ``x``, with spike points replaced by their
|
|
141
|
+
local median-filtered value. Rows (in the 2D case) or the whole
|
|
142
|
+
array (in the 1D case) with zero residual MAD — i.e. perfectly flat
|
|
143
|
+
signals — are returned unchanged, since no statistically meaningful
|
|
144
|
+
z-score can be computed.
|
|
145
|
+
|
|
146
|
+
Raises
|
|
147
|
+
------
|
|
148
|
+
ValueError
|
|
149
|
+
If ``x`` is not 1D or 2D, or if ``window`` is not odd and >= 3.
|
|
150
|
+
|
|
151
|
+
Notes
|
|
152
|
+
-----
|
|
153
|
+
- This method operates on a single spectrum/acquisition. If repeated
|
|
154
|
+
acquisitions of the same sample are available, a median-across-scans
|
|
155
|
+
approach is strictly more robust (a cosmic ray is non-reproducible
|
|
156
|
+
across acquisitions) and should be preferred when feasible.
|
|
157
|
+
- The 3.5 cutoff is a standard convention for the modified z-score, not
|
|
158
|
+
an arbitrary default; it is not currently exposed as a parameter.
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
"""
|
|
162
|
+
x = np.asarray(x, dtype=float)
|
|
163
|
+
|
|
164
|
+
if x.ndim == 1:
|
|
165
|
+
size = window
|
|
166
|
+
elif x.ndim == 2:
|
|
167
|
+
size = (1, window)
|
|
168
|
+
else:
|
|
169
|
+
raise ValueError(f"Expected 1D or 2D input, got shape {x.shape}")
|
|
170
|
+
|
|
171
|
+
if window < 3 or window % 2 == 0:
|
|
172
|
+
raise ValueError(f"window must be odd and >= 3, got {window}")
|
|
173
|
+
|
|
174
|
+
smooth = median_filter(x, size=size)
|
|
175
|
+
resid = x - smooth
|
|
176
|
+
if x.ndim == 1:
|
|
177
|
+
mad = np.median(np.abs(resid - np.median(resid)))
|
|
178
|
+
if mad == 0:
|
|
179
|
+
return x.copy()
|
|
180
|
+
z = 0.6745 * (resid - np.median(resid)) / mad
|
|
181
|
+
spike_mask = np.abs(z) > 3.5 # statistical, not arbitrary, cutoff
|
|
182
|
+
cleaned = x.copy()
|
|
183
|
+
cleaned[spike_mask] = smooth[spike_mask]
|
|
184
|
+
return cleaned
|
|
185
|
+
|
|
186
|
+
# 2D: per-row median/MAD, vectorized
|
|
187
|
+
med = np.median(resid, axis=1, keepdims=True)
|
|
188
|
+
mad = np.median(np.abs(resid - med), axis=1, keepdims=True)
|
|
189
|
+
|
|
190
|
+
cleaned = x.copy()
|
|
191
|
+
safe_mad = np.where(
|
|
192
|
+
mad == 0, 1.0, mad
|
|
193
|
+
) # avoid div-by-zero; rows with mad==0 get no spikes flagged
|
|
194
|
+
z = 0.6745 * (resid - med) / safe_mad
|
|
195
|
+
spike_mask = (np.abs(z) > 3.5) & (mad != 0)
|
|
196
|
+
cleaned[spike_mask] = smooth[spike_mask]
|
|
197
|
+
return cleaned
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
_SMOOTHING_MAPPER: Dict[str, Callable[[np.ndarray, int], np.ndarray]] = {
|
|
201
|
+
SmoothMode.SAVITZKY_GOLAY.value: smooth_savgol,
|
|
202
|
+
SmoothMode.GAUSSIAN.value: smooth_gaussian,
|
|
203
|
+
SmoothMode.MOVING_AVERAGE.value: smooth_ma,
|
|
204
|
+
SmoothMode.EXPONENTIAL_MOVING_AVERAGE.value: smooth_ema,
|
|
205
|
+
SmoothMode.MEDIAN.value: smooth_median,
|
|
206
|
+
SmoothMode.SPIKE_REMOVE.value: smooth_remove_cosmic_spikes,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@NodeDecorator(
|
|
211
|
+
"span.basics.smooth",
|
|
212
|
+
name="Smoothing",
|
|
213
|
+
outputs=[{"name": "smoothed"}],
|
|
214
|
+
)
|
|
215
|
+
def _smooth(
|
|
216
|
+
y: np.ndarray,
|
|
217
|
+
mode: SmoothMode = SmoothMode.SAVITZKY_GOLAY,
|
|
218
|
+
window: Union[float, int] = 5,
|
|
219
|
+
x: np.ndarray = None,
|
|
220
|
+
) -> np.ndarray:
|
|
221
|
+
# """
|
|
222
|
+
# Apply different smoothing techniques to the input array.
|
|
223
|
+
# the window is the number of points to consider for the smoothing.
|
|
224
|
+
# If x is provided, the window is in x units and is converted to points using the median x difference.
|
|
225
|
+
|
|
226
|
+
# Args:
|
|
227
|
+
# y (np.ndarray): The input array to be smoothed.
|
|
228
|
+
# mode (SmoothMode): The smoothing mode to apply. Defaults to SmoothMode.SAVITZKY_GOLAY.
|
|
229
|
+
# window (int): The window size for the smoothing function. Defaults to 5.
|
|
230
|
+
# x (np.ndarray): The x values of the input array. Defaults to None.
|
|
231
|
+
|
|
232
|
+
# Returns:
|
|
233
|
+
# np.ndarray: The smoothed array.
|
|
234
|
+
|
|
235
|
+
# Raises:
|
|
236
|
+
# ValueError: If an unsupported smoothing mode is provided.
|
|
237
|
+
# """
|
|
238
|
+
mode = SmoothMode.v(mode)
|
|
239
|
+
|
|
240
|
+
if mode not in _SMOOTHING_MAPPER.keys():
|
|
241
|
+
raise ValueError(f"Unsupported smoothing mode: {mode}")
|
|
242
|
+
y = np.asarray(y)
|
|
243
|
+
if x is not None:
|
|
244
|
+
x = np.asarray(x)
|
|
245
|
+
med_xdiff = np.nanmedian(np.diff(x))
|
|
246
|
+
window = window / med_xdiff
|
|
247
|
+
window = int(window)
|
|
248
|
+
if window == 0:
|
|
249
|
+
return y.copy()
|
|
250
|
+
return _SMOOTHING_MAPPER[mode](y, window)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
SMOOTH_NODE_SHELF = Shelf(
|
|
254
|
+
nodes=[_smooth],
|
|
255
|
+
subshelves=[],
|
|
256
|
+
name="Smoothing",
|
|
257
|
+
description="Smoothing of the spectra",
|
|
258
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import funcnodes as fn
|
|
2
|
+
import unittest
|
|
3
|
+
import numpy as np
|
|
4
|
+
from funcnodes_span.smoothing import _smooth, SmoothMode
|
|
5
|
+
from scipy.datasets import electrocardiogram
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# x = electrocardiogram()[2000:4000]
|
|
9
|
+
class TestSmoothing(unittest.IsolatedAsyncioTestCase):
|
|
10
|
+
async def test_default(self):
|
|
11
|
+
smooth: fn.Node = _smooth()
|
|
12
|
+
smooth.inputs["y"].value = electrocardiogram()[2000:4000]
|
|
13
|
+
self.assertIsInstance(smooth, fn.Node)
|
|
14
|
+
await smooth
|
|
15
|
+
out = smooth.outputs["smoothed"]
|
|
16
|
+
self.assertIsInstance(out.value, np.ndarray)
|
|
17
|
+
|
|
18
|
+
async def test_non_default_mode(self):
|
|
19
|
+
smooth: fn.Node = _smooth()
|
|
20
|
+
smooth.inputs["y"].value = electrocardiogram()[2000:4000]
|
|
21
|
+
smooth.inputs["mode"].value = SmoothMode.MOVING_AVERAGE
|
|
22
|
+
self.assertIsInstance(smooth, fn.Node)
|
|
23
|
+
await smooth
|
|
24
|
+
out = smooth.outputs["smoothed"]
|
|
25
|
+
self.assertIsInstance(out.value, np.ndarray)
|
|
26
|
+
|
|
27
|
+
async def test_1d_preserves_shape_and_finiteness(self):
|
|
28
|
+
smooth: fn.Node = _smooth()
|
|
29
|
+
smooth.inputs["y"].value = electrocardiogram()[2000:4000]
|
|
30
|
+
smooth.inputs["mode"].value = SmoothMode.SPIKE_REMOVE
|
|
31
|
+
self.assertIsInstance(smooth, fn.Node)
|
|
32
|
+
await smooth
|
|
33
|
+
out = smooth.outputs["smoothed"]
|
|
34
|
+
self.assertIsInstance(out.value, np.ndarray)
|
|
35
|
+
|
|
36
|
+
async def test_no_spikes_minimal_change(self):
|
|
37
|
+
smooth: fn.Node = _smooth()
|
|
38
|
+
smooth.inputs["y"].value = electrocardiogram()[2000:4000]
|
|
39
|
+
smooth.inputs["mode"].value = SmoothMode.SPIKE_REMOVE
|
|
40
|
+
self.assertIsInstance(smooth, fn.Node)
|
|
41
|
+
await smooth
|
|
42
|
+
out = smooth.outputs["smoothed"].value
|
|
43
|
+
diff = np.abs(out - smooth.inputs["y"].value)
|
|
44
|
+
frac_changed = np.mean(diff > 1e-10)
|
|
45
|
+
self.assertIsInstance(diff, np.ndarray)
|
|
46
|
+
self.assertLess(frac_changed, 0.05)
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
from typing import Dict, Callable, Union
|
|
2
|
-
from funcnodes import NodeDecorator, Shelf
|
|
3
|
-
import numpy as np
|
|
4
|
-
import pandas as pd
|
|
5
|
-
from scipy.signal import savgol_filter, medfilt
|
|
6
|
-
from scipy.ndimage import gaussian_filter1d
|
|
7
|
-
import warnings
|
|
8
|
-
import funcnodes as fn
|
|
9
|
-
|
|
10
|
-
warnings.filterwarnings("ignore")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class SmoothMode(fn.DataEnum):
|
|
14
|
-
SAVITZKY_GOLAY = "savgol"
|
|
15
|
-
GAUSSIAN = "gaussian"
|
|
16
|
-
MOVING_AVERAGE = "ma"
|
|
17
|
-
EXPONENTIAL_MOVING_AVERAGE = "ema"
|
|
18
|
-
MEDIAN = "median"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def smooth_savgol(x: np.ndarray, window: int) -> np.ndarray:
|
|
22
|
-
return savgol_filter(x, window, 2)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def smooth_gaussian(x: np.ndarray, window: int) -> np.ndarray:
|
|
26
|
-
return gaussian_filter1d(x, window)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def smooth_ma(x: np.ndarray, window: int) -> np.ndarray:
|
|
30
|
-
if x.ndim > 1:
|
|
31
|
-
n, m = x.shape
|
|
32
|
-
result = np.zeros((n, m))
|
|
33
|
-
for i in range(n):
|
|
34
|
-
result[i, :] = np.convolve(x[i, :], np.ones(window) / window, mode="same")
|
|
35
|
-
return result
|
|
36
|
-
else:
|
|
37
|
-
return np.convolve(x, np.ones(window) / window, mode="same")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def smooth_ema(x: np.ndarray, window: int) -> np.ndarray:
|
|
41
|
-
if x.ndim > 1:
|
|
42
|
-
n, m = x.shape
|
|
43
|
-
result = np.zeros((n, m))
|
|
44
|
-
for i in range(n):
|
|
45
|
-
result[i, :] = pd.Series(x[i, :]).ewm(span=window).mean().values
|
|
46
|
-
return result
|
|
47
|
-
else:
|
|
48
|
-
return pd.Series(x).ewm(span=window).mean().values
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def smooth_median(x: np.ndarray, window: int) -> np.ndarray:
|
|
52
|
-
return medfilt(x, window)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
_SMOOTHING_MAPPER: Dict[str, Callable[[np.ndarray, int], np.ndarray]] = {
|
|
56
|
-
SmoothMode.SAVITZKY_GOLAY.value: smooth_savgol,
|
|
57
|
-
SmoothMode.GAUSSIAN.value: smooth_gaussian,
|
|
58
|
-
SmoothMode.MOVING_AVERAGE.value: smooth_ma,
|
|
59
|
-
SmoothMode.EXPONENTIAL_MOVING_AVERAGE.value: smooth_ema,
|
|
60
|
-
SmoothMode.MEDIAN.value: smooth_median,
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@NodeDecorator(
|
|
65
|
-
"span.basics.smooth",
|
|
66
|
-
name="Smoothing",
|
|
67
|
-
outputs=[{"name": "smoothed"}],
|
|
68
|
-
)
|
|
69
|
-
def _smooth(
|
|
70
|
-
y: np.ndarray,
|
|
71
|
-
mode: SmoothMode = SmoothMode.SAVITZKY_GOLAY,
|
|
72
|
-
window: Union[float, int] = 5,
|
|
73
|
-
x: np.ndarray = None,
|
|
74
|
-
) -> np.ndarray:
|
|
75
|
-
# """
|
|
76
|
-
# Apply different smoothing techniques to the input array.
|
|
77
|
-
# the window is the number of points to consider for the smoothing.
|
|
78
|
-
# If x is provided, the window is in x units and is converted to points using the median x difference.
|
|
79
|
-
|
|
80
|
-
# Args:
|
|
81
|
-
# y (np.ndarray): The input array to be smoothed.
|
|
82
|
-
# mode (SmoothMode): The smoothing mode to apply. Defaults to SmoothMode.SAVITZKY_GOLAY.
|
|
83
|
-
# window (int): The window size for the smoothing function. Defaults to 5.
|
|
84
|
-
# x (np.ndarray): The x values of the input array. Defaults to None.
|
|
85
|
-
|
|
86
|
-
# Returns:
|
|
87
|
-
# np.ndarray: The smoothed array.
|
|
88
|
-
|
|
89
|
-
# Raises:
|
|
90
|
-
# ValueError: If an unsupported smoothing mode is provided.
|
|
91
|
-
# """
|
|
92
|
-
mode = SmoothMode.v(mode)
|
|
93
|
-
|
|
94
|
-
if mode not in _SMOOTHING_MAPPER.keys():
|
|
95
|
-
raise ValueError(f"Unsupported smoothing mode: {mode}")
|
|
96
|
-
y = np.asarray(y)
|
|
97
|
-
if x is not None:
|
|
98
|
-
x = np.asarray(x)
|
|
99
|
-
med_xdiff = np.nanmedian(np.diff(x))
|
|
100
|
-
window = window / med_xdiff
|
|
101
|
-
window = int(window)
|
|
102
|
-
if window == 0:
|
|
103
|
-
return y.copy()
|
|
104
|
-
return _SMOOTHING_MAPPER[mode](y, window)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# @NodeDecorator("span.basics.smooth.savgol", name="Savgol")
|
|
108
|
-
# def _smooth_savgol(array: np.array, window: int = 5) -> np.array:
|
|
109
|
-
# """
|
|
110
|
-
# Apply Savitzky-Golay smoothing to the input array.
|
|
111
|
-
|
|
112
|
-
# Args:
|
|
113
|
-
# window (int): Parameter passed to scipy.signal.savgol_filter function
|
|
114
|
-
|
|
115
|
-
# Returns:
|
|
116
|
-
# array_o (np.array): Smoothed array
|
|
117
|
-
# """
|
|
118
|
-
|
|
119
|
-
# array_o = savgol_filter(array, window, 2)
|
|
120
|
-
|
|
121
|
-
# return array_o
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
# @NodeDecorator("span.basics.smooth.gaussian", name="Gaussian")
|
|
125
|
-
# def _smooth_gaussian(array: np.array, window: int = 5) -> np.array:
|
|
126
|
-
# """
|
|
127
|
-
# Apply Gaussians moothing to the input array.
|
|
128
|
-
|
|
129
|
-
# Args:
|
|
130
|
-
# window (int): Parameter passed to scipy.signal.savgol_filter function
|
|
131
|
-
|
|
132
|
-
# Returns:
|
|
133
|
-
# array_o (np.array): Smoothed array
|
|
134
|
-
# """
|
|
135
|
-
|
|
136
|
-
# array_o = gaussian_filter1d(array, window)
|
|
137
|
-
|
|
138
|
-
# return array_o
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
# @NodeDecorator("span.basics.smooth.ma", name="Moving average")
|
|
142
|
-
# def _smooth_ma(array: np.array, window: int = 5) -> np.array:
|
|
143
|
-
# """
|
|
144
|
-
# Apply Moving Average moothing to the input array.
|
|
145
|
-
|
|
146
|
-
# Args:
|
|
147
|
-
# window (int): Parameter passed to scipy.signal.savgol_filter function
|
|
148
|
-
|
|
149
|
-
# Returns:
|
|
150
|
-
# array_o (np.array): Smoothed array
|
|
151
|
-
# """
|
|
152
|
-
|
|
153
|
-
# if array.ndim > 1:
|
|
154
|
-
# n, m = array.shape
|
|
155
|
-
# array_o = np.zeros((n, m))
|
|
156
|
-
# for i in range(n):
|
|
157
|
-
# array_o[i, :] = np.convolve(
|
|
158
|
-
# array[i, :], np.ones(window) / window, mode="same"
|
|
159
|
-
# )
|
|
160
|
-
# else:
|
|
161
|
-
# array_o = np.convolve(array, np.ones(window) / window, mode="same")
|
|
162
|
-
|
|
163
|
-
# return array_o
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
# @NodeDecorator(
|
|
167
|
-
# "span.basics.smooth.ema", name="Exponential moving average"
|
|
168
|
-
# )
|
|
169
|
-
# def _smooth_ema(array: np.array, window: int = 5) -> np.array:
|
|
170
|
-
# """
|
|
171
|
-
# Apply Exponential Moving Average moothing to the input array.
|
|
172
|
-
|
|
173
|
-
# Args:
|
|
174
|
-
# window (int): Parameter passed to scipy.signal.savgol_filter function
|
|
175
|
-
|
|
176
|
-
# Returns:
|
|
177
|
-
# array_o (np.array): Smoothed array
|
|
178
|
-
# """
|
|
179
|
-
# if array.ndim > 1:
|
|
180
|
-
# n, m = array.shape
|
|
181
|
-
# array_o = np.zeros((n, m))
|
|
182
|
-
# for i in range(n):
|
|
183
|
-
# array_o[i, :] = pd.Series(array[i, :]).ewm(span=window).mean().values
|
|
184
|
-
# else:
|
|
185
|
-
# array_o = pd.Series(array).ewm(span=window).mean().values
|
|
186
|
-
|
|
187
|
-
# return array_o
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
# @NodeDecorator("span.basics.smooth.median", name="Median")
|
|
191
|
-
# def _smooth_median(array: np.array, window: int = 5) -> np.array:
|
|
192
|
-
# """
|
|
193
|
-
# Apply Median smoothing to the input array.
|
|
194
|
-
|
|
195
|
-
# Args:
|
|
196
|
-
# window (int): Parameter passed to scipy.signal.savgol_filter function
|
|
197
|
-
|
|
198
|
-
# Returns:
|
|
199
|
-
# array_o (np.array): Smoothed array
|
|
200
|
-
# """
|
|
201
|
-
|
|
202
|
-
# array_o = medfilt(array, window)
|
|
203
|
-
|
|
204
|
-
# return array_o
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
SMOOTH_NODE_SHELF = Shelf(
|
|
208
|
-
nodes=[_smooth],
|
|
209
|
-
subshelves=[],
|
|
210
|
-
name="Smoothing",
|
|
211
|
-
description="Smoothing of the spectra",
|
|
212
|
-
)
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import funcnodes as fn
|
|
2
|
-
import unittest
|
|
3
|
-
import numpy as np
|
|
4
|
-
from funcnodes_span.smoothing import _smooth, SmoothMode
|
|
5
|
-
from scipy.datasets import electrocardiogram
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
# x = electrocardiogram()[2000:4000]
|
|
9
|
-
class TestSmoothing(unittest.IsolatedAsyncioTestCase):
|
|
10
|
-
async def test_default(self):
|
|
11
|
-
smooth: fn.Node = _smooth()
|
|
12
|
-
smooth.inputs["y"].value = electrocardiogram()[2000:4000]
|
|
13
|
-
self.assertIsInstance(smooth, fn.Node)
|
|
14
|
-
await smooth
|
|
15
|
-
out = smooth.outputs["smoothed"]
|
|
16
|
-
self.assertIsInstance(out.value, np.ndarray)
|
|
17
|
-
|
|
18
|
-
async def test_non_default_mode(self):
|
|
19
|
-
norm: fn.Node = _smooth()
|
|
20
|
-
norm.inputs["y"].value = electrocardiogram()[2000:4000]
|
|
21
|
-
norm.inputs["mode"].value = SmoothMode.MOVING_AVERAGE
|
|
22
|
-
self.assertIsInstance(norm, fn.Node)
|
|
23
|
-
await norm
|
|
24
|
-
out = norm.outputs["smoothed"]
|
|
25
|
-
self.assertIsInstance(out.value, np.ndarray)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_demoworker/nodespace.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|