smoothiepy 0.0.1__py3-none-any.whl → 0.1.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.
smoothiepy/__init__.py CHANGED
@@ -1 +0,0 @@
1
- print("Easing with this one! [From smoothiepy]")
smoothiepy/core.py ADDED
@@ -0,0 +1,32 @@
1
+ """
2
+ Main module for the application.
3
+ """
4
+ from smoothiepy.smoother.builder import SmootherBuilder
5
+ from smoothiepy.filter.filter1d import ExponentialMovingAverageFilter1D
6
+
7
+ def main() -> None:
8
+ """
9
+ Main function to demonstrate and test the usage of the Signal Smoother.
10
+ """
11
+ smoother = (
12
+ SmootherBuilder()
13
+ .one_dimensional()
14
+ .continuous()
15
+ .attach_filter(ExponentialMovingAverageFilter1D(alpha=0.25))
16
+ .build()
17
+ )
18
+
19
+ print("Init finished")
20
+
21
+ smoother.add(40.0)
22
+ print(f"Smoothed value 1: {smoother.get()}")
23
+ smoother.add(60.0)
24
+ print(f"Smoothed value 2: {smoother.get()}")
25
+ smoother.add(100.0)
26
+ print(f"Smoothed value 3: {smoother.get()}")
27
+ smoother.add(3)
28
+ print(f"Smoothed value 4: {smoother.get()}")
29
+
30
+
31
+ if __name__ == "__main__":
32
+ main()
File without changes
@@ -0,0 +1,122 @@
1
+ """
2
+ Base module that defines the abstract base class for all signal filters.
3
+ """
4
+ import enum
5
+ from abc import ABC, abstractmethod
6
+ from collections import deque
7
+ import numpy as np
8
+
9
+
10
+ class Filter(ABC):
11
+ """
12
+ Abstract base class for all signal filters.
13
+ """
14
+
15
+
16
+ class Filter1D(Filter, ABC):
17
+ """
18
+ Abstract base class for all one-dimensional signal filters.
19
+ :param window_size: The size of the window for the filter. Must be a positive integer.
20
+ :type window_size: int
21
+ :ivar window_size: The size of the window for the filter.
22
+ :type window_size: int
23
+ :raises ValueError: If window_size is not a positive integer.
24
+ """
25
+ def __init__(self, window_size: int):
26
+ if window_size <= 0:
27
+ raise ValueError("window_size must be greater than 0")
28
+ self.window_size = window_size
29
+ self.buffer = deque(maxlen=window_size)
30
+ self.last_buffer_value: float | int = 0.0
31
+
32
+ def next(self, data: float | int) -> float | int:
33
+ """
34
+ Processes the next data point by adding it to the buffer
35
+ and calling the internal processing method.
36
+
37
+ :param data: The next data point to be processed.
38
+ :type data: float | int
39
+ :return: The filtered value.
40
+ :rtype: float | int
41
+ """
42
+ self.last_buffer_value = self.buffer[0] if self.buffer else 0.0
43
+ self.buffer.append(data)
44
+ return self._process_next(np.array(self.buffer))
45
+
46
+ @abstractmethod
47
+ def _process_next(self, buffer: np.array) -> float | int:
48
+ """
49
+ Processes the next data point using the current buffer data.
50
+
51
+ :param buffer: The current buffer data.
52
+ :type buffer: list[float | int]
53
+ :return: The processed value.
54
+ :rtype: float | int
55
+ """
56
+
57
+
58
+ class Filter2D(Filter, ABC):
59
+ """
60
+ Abstract base class for all two-dimensional signal filters.
61
+ This class is currently a placeholder and does not implement any methods.
62
+ It serves as a base for future two-dimensional filter implementations.
63
+ """
64
+ def __init__(self, window_size_x: int, window_size_y: int):
65
+ """
66
+ Initializes the 2D filter with specified window sizes.
67
+
68
+ :param window_size_x: The size of the window in the x-direction. Must be a positive integer.
69
+ :type window_size_x: int
70
+ :param window_size_y: The size of the window in the y-direction. Must be a positive integer.
71
+ :type window_size_y: int
72
+ """
73
+ if window_size_x <= 0 or window_size_y <= 0:
74
+ raise ValueError("window_size_x and window_size_y must be greater than 0")
75
+ self.window_size_x = window_size_x
76
+ self.window_size_y = window_size_y
77
+ self.buffer_x = deque(maxlen=window_size_x)
78
+ self.buffer_y = deque(maxlen=window_size_y)
79
+
80
+ def next(self, data_x: float | int, data_y: float | int) -> tuple[float | int, float | int]:
81
+ """
82
+ Processes the next data point in both x and y dimensions by adding
83
+ them to their respective buffers and calling the internal processing method.
84
+
85
+ :param data_x: The next data point in the x dimension.
86
+ :type data_x: float | int
87
+ :param data_y: The next data point in the y dimension.
88
+ :type data_y: float | int
89
+ :return: A tuple containing the filtered values for x and y dimensions.
90
+ :rtype: tuple[float | int, float | int]
91
+ """
92
+ self.buffer_x.append(data_x)
93
+ self.buffer_y.append(data_y)
94
+ return self._process_next(np.array(self.buffer_x), np.array(self.buffer_y))
95
+
96
+ @abstractmethod
97
+ def _process_next(self, buffer_x: np.array, buffer_y: np.array) \
98
+ -> tuple[float | int, float | int]:
99
+ """
100
+ Processes the next data points using the current buffer data in both x and y dimensions.
101
+
102
+ :param buffer_x: The current buffer data in the x dimension.
103
+ :type buffer_x: np.array
104
+ :param buffer_y: The current buffer data in the y dimension.
105
+ :type buffer_y: np.array
106
+ :return: A tuple containing the processed values for x and y dimensions.
107
+ :rtype: tuple[float | int, float | int]
108
+ """
109
+
110
+
111
+ class MovingAverageType(enum.Enum):
112
+ """
113
+ Enum for different types of moving averages.
114
+
115
+ This enum defines the types of moving averages that can be applied to signals.
116
+ """
117
+ SIMPLE = "simple"
118
+ WEIGHTED = "weighted"
119
+ GAUSSIAN = "gaussian"
120
+ MEDIAN = "median"
121
+ EXPONENTIAL = "exponential"
122
+ CUMULATIVE = "cumulative"
@@ -0,0 +1,324 @@
1
+ """
2
+ Contains the one-dimensional filters used for signal processing.
3
+ """
4
+ from abc import ABC, abstractmethod
5
+ import numpy as np
6
+ from typing_extensions import deprecated
7
+
8
+ from smoothiepy.filter.basefilter import MovingAverageType, Filter1D
9
+ from smoothiepy.smoother.builder import SmootherBuilder
10
+
11
+
12
+ # TODO versions for list data -> also account for future data
13
+
14
+ @deprecated("Filter has no use, why would you use it?")
15
+ class UselessFilter1D(Filter1D):
16
+ """
17
+ A filter that does not perform any filtering.
18
+ It simply returns the input data as is.
19
+ """
20
+ def __init__(self):
21
+ super().__init__(window_size=1)
22
+
23
+ def _process_next(self, buffer: np.array) -> float | int:
24
+ return buffer[0]
25
+
26
+
27
+ class OffsetFilter1D(Filter1D):
28
+ """
29
+ A filter that applies a constant offset to the input data.
30
+
31
+ :param offset: The constant value to be added to the input data.
32
+ :type offset: float | int
33
+ """
34
+ def __init__(self, offset: float | int):
35
+ super().__init__(window_size=1)
36
+ self.offset = offset
37
+
38
+ def _process_next(self, buffer: np.array) -> float | int:
39
+ return buffer[0] + self.offset
40
+
41
+
42
+ class KernelMovingAverageFilter1D(Filter1D, ABC):
43
+ """
44
+ KernelMovingAverageFilter1D is an abstract base class that implements a kernel-based moving
45
+ average filter in one dimension.
46
+
47
+ This class processes data using weights constructed for the filter,
48
+ providing a weighted average over a defined window size. The weights are
49
+ normalized automatically during processing.
50
+
51
+ :ivar weights: Weights for the kernel moving average filter, calculated by
52
+ the `_construct_weights` method.
53
+ :type weights: np.array
54
+ :ivar weights_sum: Precomputed sum of the weights, used for normalization in
55
+ the filtering process.
56
+ :type weights_sum: float
57
+ """
58
+ def __init__(self, window_size: int):
59
+ super().__init__(window_size)
60
+ self.weights = self._construct_weights()
61
+ self.weights_sum = self.weights.sum()
62
+
63
+ def _process_next(self, buffer: np.array) -> float | int:
64
+ if len(buffer) < self.window_size:
65
+ offset = self.window_size - len(buffer)
66
+ weighted_sum = np.sum(buffer * self.weights[offset:])
67
+ cur_weights_sum = self.weights[offset:].sum()
68
+ if cur_weights_sum == 0:
69
+ return 0.0
70
+ return weighted_sum / cur_weights_sum
71
+
72
+ weighted_sum = np.sum(buffer * self.weights)
73
+ return weighted_sum / self.weights_sum
74
+
75
+ @abstractmethod
76
+ def _construct_weights(self) -> np.array:
77
+ """
78
+ Constructs the weights for the kernel moving average filter.
79
+ This method should be implemented by subclasses to define
80
+ how the weights are calculated based on the window size.
81
+
82
+ The weights don't have to sum up to 1, they are normalized
83
+ during the processing step.
84
+
85
+ :return: An array of weights for the kernel moving average filter.
86
+ :rtype: np.array
87
+ """
88
+
89
+
90
+ class SimpleMovingAverageFilter1D(KernelMovingAverageFilter1D):
91
+ """
92
+ A filter that computes the average of the input data over a specified window size.
93
+ No weighting is applied, and the average is computed
94
+ as a simple arithmetic mean of the values in the buffer.
95
+
96
+ If not enough data points are available to fill the window,
97
+ it computes the average of the available data points.
98
+
99
+ :ivar window_size: The size of the window for averaging.
100
+ :type window_size: int
101
+ """
102
+ def _construct_weights(self) -> np.array:
103
+ return np.ones(self.window_size) / self.window_size
104
+
105
+ class WeightedMovingAverageFilter1D(KernelMovingAverageFilter1D):
106
+ """
107
+ A filter that computes a weighted average of the input data over a specified window size.
108
+ The weights are linearly decreasing from 1 to 0, applied to the most recent data points.
109
+
110
+ If not enough data points are available to fill the window,
111
+ it computes the weighted average of the available data points.
112
+
113
+ :ivar window_size: The size of the sliding window used for the filter.
114
+ :type window_size: int
115
+ """
116
+ def _construct_weights(self) -> np.array:
117
+ return np.linspace(1, 0, self.window_size)
118
+
119
+
120
+ class GaussianAverageFilter1D(KernelMovingAverageFilter1D):
121
+ """
122
+ Implements a Gaussian Average Filter for one-dimensional data.
123
+
124
+ Incorporating a Gaussian weighting function applied over a sliding window of data.
125
+ It is used for smoothing data by placing higher importance on values closer more
126
+ recent values of the window while progressively down-weighting values farther away.
127
+ The Gaussian distribution is controlled via the window size and standard deviation parameters.
128
+
129
+ If not enough data points are available to fill the window,
130
+ it computes the gaussian average of the available data points with
131
+ a trimmed gaussian filter.
132
+
133
+ The filter is only relying on previous data values in the buffer, not future values
134
+ which would result in a delay / offset in the output.
135
+
136
+ :param window_size: Size of the sliding window used for the filter.
137
+ :type window_size: int
138
+ :param std_dev: The standard deviation of the Gaussian distribution that determines the spread.
139
+ Must be a positive value.
140
+ :type std_dev: float
141
+ :raises ValueError: If std_dev is negative.
142
+ """
143
+ def __init__(self, window_size: int, std_dev: float = None):
144
+ if std_dev is None:
145
+ std_dev = window_size / 3
146
+ if std_dev <= 0:
147
+ raise ValueError("std_dev must be a positive value")
148
+ self.std_dev = std_dev
149
+ super().__init__(window_size)
150
+
151
+ def _construct_weights(self) -> np.array:
152
+ """
153
+ Constructs Gaussian weights based on the specified window size
154
+ and standard deviation.The weights are not centered around zero,
155
+ but rather they are computed from the window size down to zero.
156
+ The most recent data points are given more weight than older ones.
157
+
158
+ :return: Computed array of Gaussian weights with length equal to the window size
159
+ :rtype: np.array
160
+ """
161
+ lin_space = np.linspace(self.window_size, 0, self.window_size) \
162
+ if self.window_size % 2 == 0 else np.linspace(self.window_size, 0, self.window_size)
163
+ gaussian = np.exp(-0.5 * (lin_space / self.std_dev) ** 2)
164
+ return gaussian
165
+
166
+
167
+ class MedianAverageFilter1D(Filter1D):
168
+ def _process_next(self, buffer: np.array) -> float | int:
169
+ return np.median(buffer).astype(float)
170
+
171
+
172
+ class ExponentialMovingAverageFilter1D(Filter1D):
173
+ """
174
+ Implements a one-dimensional exponential moving average filter.
175
+
176
+ This filter applies exponential moving average weights
177
+ to the current and the previous filtered data point.
178
+ The smoothing factor (alpha) determines the weight given to the most recent data point
179
+ compared to the previous filtered value.
180
+
181
+ :ivar alpha: The smoothing factor. Must be between 0 and 1 (inclusive).
182
+ :type alpha: float
183
+ """
184
+ def __init__(self, alpha: float):
185
+ super().__init__(window_size=1)
186
+ if not 0 <= alpha <= 1:
187
+ raise ValueError("Alpha must be between 0 and 1")
188
+
189
+ self.alpha = alpha
190
+ self.__inverted_alpha = 1 - alpha
191
+ self.latest_filtered_value: float | int = 0.0
192
+
193
+ def _process_next(self, buffer: np.array) -> float | int:
194
+ if not self.latest_filtered_value:
195
+ self.latest_filtered_value = buffer[0]
196
+ return buffer[0]
197
+
198
+ self.latest_filtered_value = ((self.alpha * buffer[0])
199
+ + (self.__inverted_alpha * self.latest_filtered_value))
200
+ return self.latest_filtered_value
201
+
202
+
203
+ class CumulativeMovingAverageFilter1D(Filter1D):
204
+ """
205
+ Implements a one-dimensional cumulative moving average filter.
206
+
207
+ This filter computes the cumulative average of the input data points
208
+ as they are processed, updating the average with each new data point.
209
+
210
+ :ivar cumulative_average: The current cumulative average of the input data.
211
+ :type cumulative_average: float | int
212
+ """
213
+ def __init__(self):
214
+ super().__init__(window_size=1)
215
+ self.cumulative_average = 0.0
216
+ self.count = 0
217
+
218
+ def _process_next(self, buffer: np.array) -> float | int:
219
+ self.count += 1
220
+ self.cumulative_average += (buffer[0] - self.cumulative_average) / self.count
221
+ return self.cumulative_average
222
+
223
+
224
+ class FixationSmoothFilter1D(Filter1D):
225
+ """
226
+ A filter class to smooth fixation-like data in 1D. This is a type of deadband filter.
227
+
228
+ It uses a weighted averaging mechanism in conjunction with standard deviation-based
229
+ thresholding to determine whether to maintain or update the fixation value. The purpose
230
+ of this filter is to smooth out noise while preserving significant data trends.
231
+
232
+ The noise will be reduced by checking the standard deviation of the data in the buffer
233
+ and comparing the latest data value with the current fixation value. If both checks pass,
234
+ the current fixation value is returned. Otherwise, a new fixation value is computed
235
+ using a weighted average of the data in the buffer.
236
+
237
+ Note that this filter cuts out noise totally. It will not smooth out noise.
238
+
239
+ An example for the threshold would be calculated using the following formula:
240
+ ``{screen_width_px} * 0.004 + sqrt({window_size})``.
241
+ Where `screen_width_px` is the width of the screen in pixels and
242
+ `window_size` is the size of the sliding window used for the filter.
243
+ This could be used for eye-tracking data to smooth out noise when you fixate on a point
244
+ for a longer period of time.
245
+
246
+ :ivar window_size: The size of the sliding window for the filter.
247
+ :type window_size: int
248
+ :ivar threshold: The threshold value used for standard deviation and fixation value difference
249
+ checks.
250
+ :type threshold: float
251
+ :ivar fixation_value: The current fixation value being tracked by the filter.
252
+ :type fixation_value: float
253
+ :ivar average_weights: A numpy array of weights used for computing weighted averages
254
+ over the input data buffer.
255
+ :type average_weights: numpy.ndarray
256
+ """
257
+ def __init__(self, window_size: int, threshold: float):
258
+ super().__init__(window_size)
259
+ self.threshold = threshold
260
+ self.fixation_value = 0.0
261
+ self.average_weights = np.linspace(0.2, 1.0, window_size)
262
+
263
+ def _process_next(self, buffer: np.array) -> float | int:
264
+ std_dev = np.std(buffer)
265
+ latest_data_value = buffer[-1]
266
+
267
+ if (abs(std_dev) <= self.threshold
268
+ and abs(self.fixation_value - latest_data_value) <= self.threshold):
269
+ return self.fixation_value
270
+
271
+ if len(buffer) < self.window_size:
272
+ offset = self.window_size - len(buffer)
273
+ self.fixation_value = np.average(buffer, weights=self.average_weights[offset:])
274
+ else:
275
+ self.fixation_value = np.average(buffer, weights=self.average_weights)
276
+ return latest_data_value
277
+
278
+
279
+ class MultiPassMovingAverage1D(Filter1D):
280
+ """
281
+ This class implements a one-dimensional multi-pass moving average filter.
282
+
283
+ The MultiPassMovingAverage1D class applies a user-defined number of passes
284
+ over the data using the specified moving average filter type. It allows
285
+ different types of moving averages, such as simple, weighted, Gaussian,
286
+ and median.
287
+
288
+ :param window_size: The size of the sliding window for the filter.
289
+ :type window_size: int
290
+ :param num_passes: Number of passes to apply to the moving average filter.
291
+ :type num_passes: int
292
+ :param average_filter_type: The type of moving average filter to use.
293
+ :type average_filter_type: MovingAverageType
294
+ """
295
+ def __init__(self, window_size: int, num_passes: int,
296
+ average_filter_type: MovingAverageType = MovingAverageType.SIMPLE):
297
+ super().__init__(window_size=1)
298
+ if num_passes <= 0:
299
+ raise ValueError("num_passes must be greater than 0")
300
+
301
+ self.num_passes = num_passes
302
+ self.average_filter_type = average_filter_type
303
+
304
+ smoother = (
305
+ SmootherBuilder()
306
+ .one_dimensional()
307
+ .continuous()
308
+ )
309
+ for _ in range(num_passes):
310
+ if average_filter_type == MovingAverageType.SIMPLE:
311
+ smoother.attach_filter(SimpleMovingAverageFilter1D(window_size))
312
+ elif average_filter_type == MovingAverageType.WEIGHTED:
313
+ smoother.attach_filter(WeightedMovingAverageFilter1D(window_size))
314
+ elif average_filter_type == MovingAverageType.GAUSSIAN:
315
+ smoother.attach_filter(GaussianAverageFilter1D(window_size,
316
+ std_dev=window_size / 3))
317
+ elif average_filter_type == MovingAverageType.MEDIAN:
318
+ smoother.attach_filter(MedianAverageFilter1D(window_size))
319
+ else:
320
+ raise ValueError(f"Unsupported average filter type: {average_filter_type}")
321
+ self.smoother = smoother.build()
322
+
323
+ def _process_next(self, buffer: np.array) -> float | int:
324
+ return self.smoother.add_and_get(buffer[0])
@@ -0,0 +1,41 @@
1
+ """
2
+ Contains the two-dimensional filters used for signal processing.
3
+ """
4
+
5
+ import numpy as np
6
+
7
+ from smoothiepy.filter.basefilter import Filter2D
8
+
9
+
10
+ class OffsetFilter2D(Filter2D):
11
+ """
12
+ A 2D filter that applies a fixed offset to input data on both x and y axes.
13
+ Can be used in scenarios where a shift in data coordinates is required.
14
+
15
+ :param offset: Offset value to be applied to the x-axis data.
16
+ If `offset_y` is not provided, it will be set to the same value as `offset`.
17
+ :type offset: int | float
18
+ :param offset_y: Offset value to be applied to the y-axis data.
19
+ If not provided, it defaults to the same value as `offset`.
20
+ :type offset_y: int | float
21
+ """
22
+ def __init__(self, offset, offset_y = None):
23
+ if offset_y is None:
24
+ offset_y = offset
25
+ super().__init__(window_size_x=1, window_size_y=1)
26
+ self.offset_x = offset
27
+ self.offset_y = offset_y
28
+
29
+ def _process_next(self, buffer_x: np.array, buffer_y: np.array) \
30
+ -> tuple[float | int, float | int]:
31
+ """
32
+ Processes the next data point by applying an offset to the input buffers.
33
+
34
+ :param buffer_x: The current buffer data for the x-axis.
35
+ :type buffer_x: np.array
36
+ :param buffer_y: The current buffer data for the y-axis.
37
+ :type buffer_y: np.array
38
+ :return: A tuple containing the processed values for x and y axes.
39
+ :rtype: tuple[float | int, float | int]
40
+ """
41
+ return buffer_x[0] + self.offset_x, buffer_y[0] + self.offset_y