fosanalysis
A framework to evaluate distributed fiber optic sensor data
Loading...
Searching...
No Matches
filtering.py
Go to the documentation of this file.
2r"""
3Contains class definitions for filtering algorithms.
4Those can be leveraged to deal with noise, e.g.\ by smoothing neighboring data points.
5
6\author Bertram Richter
7\date 2022
8"""
9
10from abc import abstractmethod
11import copy
12
13import numpy as np
14
15from fosanalysis import utils
16from . import base
17
19 r"""
20 Abstract base class for filter classes.
21 These filters will modify the values, but not the shape of the arrays.
22
23 To reduce/avoid boundary effects, generally crop the data after smoothing.
24 """
25
27 r"""
28 A filter to limit the entries.
29 The result \f$y\f$ will only contain all entries for which \f$y_i \in [x_{\mathrm{min}},\: x_{\mathrm{max}}]\f$ holds.
30 Values, that exceed the limits, will be truncated at the according limit using the equation
31 \f[y_i = \min\left(\max\left(x_i,\: x_{\mathrm{min}}\right),\: x_{\mathrm{max}}\right)\f].
32 """
33 def __init__(self,
34 minimum: float = None,
35 maximum: float = None,
36 *args, **kwargs):
37 r"""
38 Construct an instance of the class.
39 \param minimum \copydoc minimum
40 \param maximum \copydoc maximum
41 \param *args Additional positional arguments, will be passed to the superconstructor.
42 \param **kwargs Additional keyword arguments, will be passed to the superconstructor.
43 """
44 super().__init__(*args, **kwargs)
45
48 self.minimum = minimum
49
52 self.maximum = maximum
53 def run(self,
54 x: np.array,
55 y: np.array,
56 z: np.array,
57 make_copy: bool = True,
58 timespace: str = "2d",
59 minimum: float = None,
60 maximum: float = None,
61 *args, **kwargs) -> tuple:
62 r"""
63 Limit the entries in the given list to the specified range.
64 Returns a list, which conforms to \f$\mathrm{minimum} \leq x \leq \mathrm{maximum} \forall x \in X\f$.
65 Entries, which exceed the given range are cropped to it.
66 \copydetails preprocessing.base.Task.run()
67 \param minimum \copydoc minimum
68 \param maximum \copydoc maximum
69 """
70 return super().run(x, y, z,
71 timespace=timespace,
72 make_copy=make_copy,
73 minimum=minimum,
74 maximum=maximum,
75 *args, **kwargs)
76 def _run_1d(self,
77 x: np.array,
78 z: np.array,
79 *args, **kwargs) -> tuple:
80 return x, self._limit(z, *args, **kwargs)
81 def _run_2d(self,
82 x: np.array,
83 y: np.array,
84 z: np.array,
85 *args, **kwargs) -> tuple:
86 return x, y, self._limit(z, *args, **kwargs)
87 def _limit(self,
88 z: np.array,
89 minimum: float = None,
90 maximum: float = None,
91 *args, **kwargs) -> np.array:
92 r"""
93 Limit the values of the array.
94 \param z Array with data to be limited.
95 \param minimum \copydoc minimum
96 \param maximum \copydoc maximum
97 \param *args Additional positional arguments, ignored.
98 \param **kwargs Additional keyword arguments, ignored.
99 """
100 minimum = minimum if minimum is not None else self.minimum
101 maximum = maximum if maximum is not None else self.maximum
102 if minimum is not None:
103 z = np.maximum(z, minimum)
104 if maximum is not None:
105 z = np.minimum(z, maximum)
106 return z
107
109 r"""
110 Abstract base class for filter classes, based on sliding windows.
111 The sliding windows are generated by \ref utils.windows.sliding().
112 To each of those sliding windows, \ref method is applied.
113 The result is written to the central pixel of the window.
114 To reduce/avoid boundary effects, genrally crop the data after smoothing.
115 """
116 def __init__(self,
117 radius: int,
118 method: str,
119 pad_mode: str = None,
120 *args, **kwargs):
121 r"""
122 Construct an instance of the class.
123 As this is an abstract class, it may not be instantiated directly itself.
124 \param radius \copydoc radius
125 \param method \copydoc method
126 \param pad_mode \copydoc pad_mode
127 \param *args Additional positional arguments, will be passed to the superconstructor.
128 \param **kwargs Additional keyword arguments, will be passed to the superconstructor.
129 """
130 super().__init__(*args, **kwargs)
131
139 self.radius = radius
140
153 self.method = method
154
157 self.pad_mode = pad_mode
158 def run(self,
159 x: np.array,
160 y: np.array,
161 z: np.array,
162 timespace: str = None,
163 make_copy: bool = True,
164 radius: int = None,
165 method: str = None,
166 *args, **kwargs) -> tuple:
167 r"""
168 The given data is filtered with a sliding window.
169 \copydetails SlidingFilter
170 \copydetails preprocessing.base.Task.run()
171 \param radius \copydoc radius Defaults to \ref radius.
172 \param method \copydoc method
173 """
174 return super().run(x, y, z,
175 timespace=timespace,
176 make_copy=make_copy,
177 radius=radius,
178 method=method,
179 *args, **kwargs)
180 def _run_1d(self,
181 x: np.array,
182 z: np.array,
183 *args, **kwargs) -> tuple:
184 return x, self._slide(z, *args, **kwargs)
185 def _run_2d(self,
186 x: np.array,
187 y: np.array,
188 z: np.array,
189 *args, **kwargs) -> tuple:
190 return x, y, self._slide(z, *args, **kwargs)
191 def _slide(self,
192 z: np.array,
193 radius = None,
194 method: str = None,
195 *args, **kwargs) -> np.array:
196 r"""
197 Move the window over the input array and apply \ref method on it.
198 The central pixel of the window \f$x_{i}\f$ is assigned the value
199 \f[
200 x_{i} \gets \mathrm{op}(x_{j,\:\ldots,\:k}) \text{ with } j = i -r \text{ and } k = i + r.
201 \f]
202 \param z Array of strain data.
203 \param radius \copydoc radius Defaults to \ref radius.
204 \param method \copydoc method
205 \param *args Additional positional arguments, will be ignored.
206 \param **kwargs Additional keyword arguments, will be ignored.
207 \return Returns an array with the same shape as `z`.
208 """
209 radius = radius if radius is not None else self.radius
210 method = method if method is not None else self.method
211 method_function = method if callable(method) else getattr(np, method)
212 if radius == 0:
213 return z
214 return utils.windows.sliding_window_function(z, radius, method_function)
215
217 r"""
218 The Cluster filter is an iterative smoothing algorithm guaranteed to converge \cite Lou_2020_ApplicationofClustering.
219 The one-dimensional signal to filter consists of abscissa data \f$\mathbf{x}\f$ and according ordinate data (measured values) \f$\mathbf{z}\f$.
220 For the \f$k\f$th entry (pixel), consisting of its location \f$x_{k}\f$ and its original value \f$z_{k}\f$, a value is estimated iteratively.
221 The pixel value estimate \f$z^{(t+1)}_{k}\f$ for the next iteration step \f$t+1\f$ is determined by (see \ref _new_z_t()):
222 \f[
223 z^{(t+1)}_{k} = \frac{
224 \sum_{i} z_{i} w_{i} \exp\left(- \beta^{(t)} \left(z_{i} - z^{(t)}_{k}\right)^{2}\right)
225 }{
226 \sum_{i} w_{i} \exp\left(- \beta^{(t)} \left(z_{i} - z^{(t)}_{k}\right)^{2}\right)
227 }.
228 \f]
229 Here, the \f$i\f$th pixels position is \f$x_{i}\f$, its value is \f$z_{i}\f$ and \f$w_{i}\f$ is its weight.
230 The weight indicates the influence on the currently optimized pixel at position \f$x_{k}\f$
231 and drops exponentially with the distance for the current pixel (see \ref _get_weights())
232 \f[
233 w_{i} = \exp\left(-\alpha ||x_{i} - x_{k}||^{2}\right)
234 \f]
235 with \f$|| x_{i} - x_{k} ||^{2}\f$ being the squared Euclidian norm.
236 The main parameter for the filter is \f$\alpha\f$, which controls the weight falloff and hence, the filter's scale.
237 It can be calculated from ttarget weight and distance with \ref estimate_alpha().
238 The locality parameter \f$\beta\f$ based on the local variance is estimated to (see \ref _get_beta()):
239 \f[
240 \beta^{(t)} = \frac{
241 \sum_{i} w_{i}
242 }{
243 2 \sum_{i} \left(z_{i}-z^{(t)_{k}}\right)^{2} w_{i}
244 }.
245 \f]
246 The initial guess \f$z^{(0)}\f$ is estimated by (see \ref _initial_z):
247 \f[
248 z^{(0)} = \frac{
249 \sum_{i} z_{i} w_{i}
250 }{
251 \sum_{i} w_{i}
252 }.
253 \f]
254 After each iteration, the estimate change is
255 \f[
256 \Delta z^{(t)}_{k} = |z^{(t-1)} - z^{(t)}_{k}|
257 \f]
258 calculated and the iteration is stopped if this change falls below the predefined threshold \f$\Delta z_{\mathrm{tol}}\f$:
259 \f[
260 \Delta z^{(t)}_{k} \leq \Delta z_{\mathrm{tol}}.
261 \f]
262 This process is repeated for all pixels.
263 """
264 def __init__(self,
265 alpha: float,
266 tolerance: float = 0.1,
267 fill: bool = False,
268 *args, **kwargs):
269 r"""
270 Construct a Cluster object.
271 \param alpha \copybrief alpha \copydetails alpha For more, see \ref alpha.
272 \param tolerance \copybrief tolerance \copydetails tolerance.
273 \param fill \copybrief fill \copydetails fill.
274 \param *args Additional positional arguments, will be passed to the superconstructor.
275 \param **kwargs Additional keyword arguments, will be passed to the superconstructor.
276 """
277 super().__init__(*args, **kwargs)
278
286 self.alpha = alpha
287
291 self.tolerance = tolerance
292
294 self.fill = fill
295 @property
296 def alpha(self):
297 return self._alpha
298 @alpha.setter
299 def alpha(self, alpha):
300 assert alpha >= 0, "The scaling value alpha needs to be non-negative!"
301 self._alpha = alpha
302 def _run_1d(self,
303 x: np.array,
304 z: np.array,
305 *args, **kwargs) -> tuple:
306 r"""
307 Carry out the filtering on one-dimensional data.
308
309 \copydetails fosanalysis.preprocessing.base.Task._run_1d()
310 """
311 z_filtered = copy.deepcopy(z)
312 z_zero = copy.deepcopy(z)
313 nan_array = np.logical_not(np.isfinite(z_zero))
314 z_zero[nan_array] = 0
315 iterator = np.nditer(z_zero, flags=["multi_index"])
316 for z_orig in iterator:
317 pixel = iterator.multi_index
318 if self.fill or not nan_array[pixel]:
319 weights_array = self._get_weights(pixel, x)
320 weights_array[nan_array] = 0
321 z_t = self._initial_z(z_zero, weights_array)
322 improvement = np.inf
323 while abs(improvement) > self.tolerance:
324 z_t_new = self._new_z_t(z_zero, weights_array, z_t)
325 improvement = z_t_new - z_t
326 z_t = z_t_new
327 z_filtered[pixel] = z_t
328 return x, z_filtered
329 def _run_2d(self,
330 x: np.array,
331 y: np.array,
332 z: np.array,
333 *args, **kwargs) -> tuple:
334 r"""
335 Cluster has no true 2D operation mode.
336 Set \ref timespace to `"1d_space"`!
337 """
338 raise NotImplementedError("Cluster does not support true 2D operation. Try `timepace='1d_space'` instead.")
339 def _get_weights(self,
340 pixel: int,
341 x_array: np.array,
342 ) -> np.array:
343 r"""
344 Calculate the array of weights for the current position.
345 The weight \f$w_i\f$ of the \f$i\f$th element on the current
346 element is calculated as
347 \f[
348 w_{i} = \exp\left(-\alpha || x_{i} - x ||^{2}\right).
349 \f]
350 \param pixel Position (index) of the current datapoint to estimate.
351 \param x_array Array of abscissa data.
352 """
353 position = x_array[pixel]
354 dist = np.square(x_array - position)
355 return np.exp(-self.alpha * dist)
356 def _get_beta(self,
357 z_dist_array: np.array,
358 weights_array: np.array,
359 ) -> float:
360 r"""
361 Calculate the locality parameter \f$\beta\f$.
362 The locality parameter \f$\beta\f$ is affected on the local variance.
363 It is calculated as
364 \f[
365 \beta^{(t)} = \frac{
366 \sum_{i} w_{i}
367 }{
368 2 \sum_{i} \left(z_{i}-z^{(t)_{k}}\right)^{2} w_{i}
369 }.
370 \f]
371 \param z_dist_array 1D-array distance matrix for the current pixel.
372 \param weights_array 1D-array containing the weights.
373 """
374 return 0.5 * np.sum(weights_array)/np.sum(weights_array * z_dist_array)
375 def _initial_z(self,
376 z_array: np.array,
377 weights_array:np.array,
378 ) -> float:
379 r"""
380 Guess the the initial estimate.
381 The initial estimate \f$z^{(0)}\f$ is calculated by
382 \f[
383 z^{(0)} = \frac{\sum_{i} z_{i} w_{i}}{\sum_{i} w_{i}}.
384 \f]
385 \param z_array 1D-array of the original ordniate data.
386 \param weights_array 1D-array containing the weights.
387 """
388 return np.sum(weights_array * z_array)/np.sum(weights_array)
389 def _new_z_t(self,
390 z_array: np.array,
391 weights_array: np.array,
392 z_t: float,
393 ) -> float:
394 r"""
395 Calculate the next iteration estimate.
396 The next estimate \f$z^{(t+1)}_{k}\f$ is calculated as
397 \f[
398 z^{(t+1)}_{k} = \frac{
399 \sum_{i} z_{i} w_{i} \exp\left(- \beta^{(t)} \left(z_{i} - z^{(t)}_{k}\right)^{2}\right)
400 }{
401 \sum_{i} w_{i} \exp\left(- \beta^{(t)} \left(z_{i} - z^{(t)}_{k}\right)^{2}\right)
402 }.
403 \f]
404 Here, the \f$i\f$th pixels position is \f$x_{i}\f$, its value is \f$z_{i}\f$ and \f$w_{i}\f$ is its weight.
405 The weight indicates the influence on the currently optimized pixel at position \f$x_{k}\f$
406 and drops exponentially with the distance for the current pixel.
407 \param z_array 1D-array of the original ordniate data.
408 \param weights_array 1D-array containing the weights.
409 \param z_t Estimate of the previous iteration step for the current pixel.
410 """
411 z_dist_array = np.square(z_array - z_t)
412 beta = self._get_beta(z_dist_array, weights_array)
413 weighted = weights_array * np.exp(-beta * z_dist_array)
414 numerator = np.sum(z_array * weighted)
415 denominator = np.sum(weighted)
416 return numerator/denominator
418 weight: float,
419 length: float,
420 ):
421 r"""
422 Calculate the weight falloff parameter \f$\alpha\f$, see \ref alpha.
423 It is calculated as
424 \f[
425 \alpha = -\frac{\ln w}{l^2}
426 \f]
427 \param weight Target weight \f$w\f$.
428 \param length Target distance \f$l\f$.
429 """
430 assert weight > 0, "weight and length must be greater than 0!"
431 assert length > 0, "weight and length must be greater than 0!"
432 return -np.log(weight)/(np.square(length))
Abstract base class for preprocessing task classes.
Definition base.py:56
The Cluster filter is an iterative smoothing algorithm guaranteed to converge lou_2020_applicationofc...
Definition filtering.py:216
float _initial_z(self, np.array z_array, np.array weights_array)
Guess the the initial estimate.
Definition filtering.py:378
tuple _run_2d(self, np.array x, np.array y, np.array z, *args, **kwargs)
Cluster has no true 2D operation mode.
Definition filtering.py:333
float _new_z_t(self, np.array z_array, np.array weights_array, float z_t)
Definition filtering.py:393
fill
Switch, whether missing data should be interpolated.
Definition filtering.py:294
tuple _run_1d(self, np.array x, np.array z, *args, **kwargs)
Carry out the filtering on one-dimensional data.
Definition filtering.py:305
tolerance
Stopping criterion for the iterative process.
Definition filtering.py:291
alpha
Falloff parameter for the weight function.
Definition filtering.py:286
__init__(self, float alpha, float tolerance=0.1, bool fill=False, *args, **kwargs)
Construct a Cluster object.
Definition filtering.py:268
float _get_beta(self, np.array z_dist_array, np.array weights_array)
Definition filtering.py:359
np.array _get_weights(self, int pixel, np.array x_array)
Calculate the array of weights for the current position.
Definition filtering.py:342
estimate_alpha(self, float weight, float length)
Calculate the weight falloff parameter , see alpha.
Definition filtering.py:420
Abstract base class for filter classes.
Definition filtering.py:18
A filter to limit the entries.
Definition filtering.py:26
tuple _run_2d(self, np.array x, np.array y, np.array z, *args, **kwargs)
Native two-dimensional operation implementation.
Definition filtering.py:85
maximum
Maximal value, which will be included in the result.
Definition filtering.py:52
np.array _limit(self, np.array z, float minimum=None, float maximum=None, *args, **kwargs)
Limit the values of the array.
Definition filtering.py:91
__init__(self, float minimum=None, float maximum=None, *args, **kwargs)
Construct an instance of the class.
Definition filtering.py:36
tuple run(self, np.array x, np.array y, np.array z, bool make_copy=True, str timespace="2d", float minimum=None, float maximum=None, *args, **kwargs)
Limit the entries in the given list to the specified range.
Definition filtering.py:61
minimum
Minimal value, which will be included in the result.
Definition filtering.py:48
tuple _run_1d(self, np.array x, np.array z, *args, **kwargs)
Reimplementations describe a one-dimensional operation.
Definition filtering.py:79
Abstract base class for filter classes, based on sliding windows.
Definition filtering.py:108
tuple run(self, np.array x, np.array y, np.array z, str timespace=None, bool make_copy=True, int radius=None, str method=None, *args, **kwargs)
The given data is filtered with a sliding window.
Definition filtering.py:166
__init__(self, int radius, str method, str pad_mode=None, *args, **kwargs)
Construct an instance of the class.
Definition filtering.py:120
tuple _run_1d(self, np.array x, np.array z, *args, **kwargs)
Reimplementations describe a one-dimensional operation.
Definition filtering.py:183
np.array _slide(self, np.array z, radius=None, str method=None, *args, **kwargs)
Move the window over the input array and apply method on it.
Definition filtering.py:195
tuple _run_2d(self, np.array x, np.array y, np.array z, *args, **kwargs)
Native two-dimensional operation implementation.
Definition filtering.py:189
pad_mode
Mode for padding the edges of the result array.
Definition filtering.py:157
radius
Smoothing radius for the data, number of entries of data to each side to be taken into account.
Definition filtering.py:139
method
Specify, how the data is smoothed.
Definition filtering.py:153