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.
Files changed (50) hide show
  1. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/PKG-INFO +1 -1
  2. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/pyproject.toml +1 -1
  3. funcnodes_span-1.0.3/src/funcnodes_span/smoothing.py +258 -0
  4. funcnodes_span-1.0.3/tests/test_smoothing.py +46 -0
  5. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/uv.lock +1 -1
  6. funcnodes_span-1.0.2/src/funcnodes_span/smoothing.py +0 -212
  7. funcnodes_span-1.0.2/tests/test_smoothing.py +0 -25
  8. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.flake8 +0 -0
  9. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/config.json +0 -0
  10. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/config.json.bu +0 -0
  11. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_819786f45cc247a7b086ce71d030df11/main.py +0 -0
  12. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_819786f45cc247a7b086ce71d030df11/nodespace.json +0 -0
  13. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_819786f45cc247a7b086ce71d030df11/pyproject.toml +0 -0
  14. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_819786f45cc247a7b086ce71d030df11/uv.lock +0 -0
  15. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_819786f45cc247a7b086ce71d030df11.json +0 -0
  16. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_819786f45cc247a7b086ce71d030df11.p +0 -0
  17. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_demoworker/nodespace.json +0 -0
  18. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.funcnodes/workers/worker_demoworker.json +0 -0
  19. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.github/actions/install_package/action.yml +0 -0
  20. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.github/workflows/py_test.yml +0 -0
  21. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.github/workflows/version_publish_main.yml +0 -0
  22. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.gitignore +0 -0
  23. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.pre-commit-config.yaml +0 -0
  24. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/.vscode/settings.json +0 -0
  25. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/CHANGELOG.md +0 -0
  26. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/LICENSE +0 -0
  27. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/MANIFEST.in +0 -0
  28. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/README.md +0 -0
  29. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/THIRD_PARTY_NOTICES.md +0 -0
  30. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/pytest.ini +0 -0
  31. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/reports/mypy-baseline.txt +0 -0
  32. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/__init__.py +0 -0
  33. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/_baseline.py +0 -0
  34. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/_curves.py +0 -0
  35. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/_smoothing.py +0 -0
  36. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/baseline.py +0 -0
  37. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/curves.py +0 -0
  38. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/fitting.py +0 -0
  39. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/normalization.py +0 -0
  40. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/peak_analysis.py +0 -0
  41. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/src/funcnodes_span/peaks.py +0 -0
  42. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/__init__.py +0 -0
  43. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/all_nodes_test_base.py +0 -0
  44. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/test_all_nodes.py +0 -0
  45. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/test_baseline.py +0 -0
  46. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/test_curves.py +0 -0
  47. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/test_fit.py +0 -0
  48. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/test_normalization.py +0 -0
  49. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/test_peak_analysis.py +0 -0
  50. {funcnodes_span-1.0.2 → funcnodes_span-1.0.3}/tests/test_peaks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: funcnodes-span
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: SPectral ANalysis (SPAN) for funcnodes
5
5
  Project-URL: homepage, https://github.com/Linkdlab/funcnodes_span
6
6
  Project-URL: source, https://github.com/Linkdlab/funcnodes_span
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "funcnodes-span"
3
- version = "1.0.2"
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)
@@ -688,7 +688,7 @@ wheels = [
688
688
 
689
689
  [[package]]
690
690
  name = "funcnodes-span"
691
- version = "1.0.2"
691
+ version = "1.0.3"
692
692
  source = { editable = "." }
693
693
  dependencies = [
694
694
  { name = "funcnodes" },
@@ -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