ezmsg-sigproc 2.1.0__py3-none-any.whl → 2.3.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.
@@ -0,0 +1,119 @@
1
+ import functools
2
+ import typing
3
+
4
+ import numpy as np
5
+ import numpy.typing as npt
6
+ import scipy.signal
7
+
8
+ from .filter import (
9
+ FilterBaseSettings,
10
+ FilterByDesignTransformer,
11
+ BACoeffs,
12
+ BaseFilterByDesignTransformerUnit,
13
+ )
14
+
15
+
16
+ class FIRFilterSettings(FilterBaseSettings):
17
+ """Settings for :obj:`FIRFilter`. See scipy.signal.firwin for more details"""
18
+
19
+ # axis and coef_type are inherited from FilterBaseSettings
20
+
21
+ order: int = 0
22
+ """
23
+ Filter order/number of taps
24
+ """
25
+
26
+ cutoff: float | npt.ArrayLike | None = None
27
+ """
28
+ Cutoff frequency of filter (expressed in the same units as fs) OR an array of cutoff frequencies
29
+ (that is, band edges). In the former case, as a float, the cutoff frequency should correspond with
30
+ the half-amplitude point, where the attenuation will be -6dB. In the latter case, the frequencies in
31
+ cutoff should be positive and monotonically increasing between 0 and fs/2. The values 0 and fs/2 must
32
+ not be included in cutoff.
33
+ """
34
+
35
+ width: float | None = None
36
+ """
37
+ If width is not None, then assume it is the approximate width of the transition region (expressed in
38
+ the same units as fs) for use in Kaiser FIR filter design. In this case, the window argument is ignored.
39
+ """
40
+
41
+ window: str | None = "hamming"
42
+ """
43
+ Desired window to use. See scipy.signal.get_window for a list of windows and required parameters.
44
+ """
45
+
46
+ pass_zero: bool | str = True
47
+ """
48
+ If True, the gain at the frequency 0 (i.e., the “DC gain”) is 1. If False, the DC gain is 0. Can also
49
+ be a string argument for the desired filter type (equivalent to btype in IIR design functions).
50
+ {‘lowpass’, ‘highpass’, ‘bandpass’, ‘bandstop’}
51
+ """
52
+
53
+ scale: bool = True
54
+ """
55
+ Set to True to scale the coefficients so that the frequency response is exactly unity at a certain
56
+ frequency. That frequency is either:
57
+ * 0 (DC) if the first passband starts at 0 (i.e. pass_zero is True)
58
+ * fs/2 (the Nyquist frequency) if the first passband ends at fs/2
59
+ (i.e the filter is a single band highpass filter);
60
+ center of first passband otherwise
61
+ """
62
+
63
+ wn_hz: bool = True
64
+ """
65
+ Set False if provided Wn are normalized from 0 to 1, where 1 is the Nyquist frequency
66
+ """
67
+
68
+
69
+ def firwin_design_fun(
70
+ fs: float,
71
+ order: int = 0,
72
+ cutoff: float | npt.ArrayLike | None = None,
73
+ width: float | None = None,
74
+ window: str | None = "hamming",
75
+ pass_zero: bool | str = True,
76
+ scale: bool = True,
77
+ wn_hz: bool = True,
78
+ ) -> BACoeffs | None:
79
+ """
80
+ Design an `order`th-order FIR filter and return the filter coefficients.
81
+ See :obj:`FIRFilterSettings` for argument description.
82
+
83
+ Returns:
84
+ The filter taps as designed by firwin
85
+ """
86
+ if order > 0:
87
+ taps = scipy.signal.firwin(
88
+ numtaps=order,
89
+ cutoff=cutoff,
90
+ width=width,
91
+ window=window,
92
+ pass_zero=pass_zero,
93
+ scale=scale,
94
+ fs=fs if wn_hz else None,
95
+ )
96
+ return (taps, np.array([1.0]))
97
+ return None
98
+
99
+
100
+ class FIRFilterTransformer(FilterByDesignTransformer[FIRFilterSettings, BACoeffs]):
101
+ def get_design_function(
102
+ self,
103
+ ) -> typing.Callable[[float], BACoeffs | None]:
104
+ return functools.partial(
105
+ firwin_design_fun,
106
+ order=self.settings.order,
107
+ cutoff=self.settings.cutoff,
108
+ width=self.settings.width,
109
+ window=self.settings.window,
110
+ pass_zero=self.settings.pass_zero,
111
+ scale=self.settings.scale,
112
+ wn_hz=self.settings.wn_hz,
113
+ )
114
+
115
+
116
+ class FIRFilter(
117
+ BaseFilterByDesignTransformerUnit[FIRFilterSettings, FIRFilterTransformer]
118
+ ):
119
+ SETTINGS = FIRFilterSettings
@@ -0,0 +1,93 @@
1
+ from typing import Callable
2
+ import warnings
3
+
4
+ import numpy as np
5
+
6
+ from .filter import (
7
+ FilterBaseSettings,
8
+ BACoeffs,
9
+ FilterByDesignTransformer,
10
+ BaseFilterByDesignTransformerUnit,
11
+ )
12
+
13
+
14
+ class GaussianSmoothingSettings(FilterBaseSettings):
15
+ sigma: float | None = 1.0
16
+ """
17
+ sigma : float
18
+ Standard deviation of the Gaussian kernel.
19
+ """
20
+
21
+ width: int | None = 4
22
+ """
23
+ width : int
24
+ Number of standard deviations covered by the kernel window if kernel_size is not provided.
25
+ """
26
+
27
+ kernel_size: int | None = None
28
+ """
29
+ kernel_size : int | None
30
+ Length of the kernel in samples. If provided, overrides automatic calculation.
31
+ """
32
+
33
+
34
+ def gaussian_smoothing_filter_design(
35
+ sigma: float = 1.0,
36
+ width: int = 4,
37
+ kernel_size: int | None = None,
38
+ ) -> BACoeffs | None:
39
+ # Parameter checks
40
+ if sigma <= 0:
41
+ raise ValueError(f"sigma must be positive. Received: {sigma}")
42
+
43
+ if width <= 0:
44
+ raise ValueError(f"width must be positive. Received: {width}")
45
+
46
+ if kernel_size is not None:
47
+ if kernel_size < 1:
48
+ raise ValueError(f"kernel_size must be >= 1. Received: {kernel_size}")
49
+ else:
50
+ kernel_size = int(2 * width * sigma + 1)
51
+
52
+ # Warn if kernel_size is smaller than recommended but don't fail
53
+ expected_kernel_size = int(2 * width * sigma + 1)
54
+ if kernel_size < expected_kernel_size:
55
+ ## TODO: Either add a warning or determine appropriate kernel size and raise an error
56
+ warnings.warn(
57
+ f"Provided kernel_size {kernel_size} is smaller than recommended "
58
+ f"size {expected_kernel_size} for sigma={sigma} and width={width}. "
59
+ "The kernel may be truncated."
60
+ )
61
+
62
+ from scipy.signal.windows import gaussian
63
+
64
+ b = gaussian(kernel_size, std=sigma)
65
+ b /= np.sum(b) # Ensure normalization
66
+ a = np.array([1.0])
67
+
68
+ return b, a
69
+
70
+
71
+ class GaussianSmoothingFilterTransformer(
72
+ FilterByDesignTransformer[GaussianSmoothingSettings, BACoeffs]
73
+ ):
74
+ def get_design_function(
75
+ self,
76
+ ) -> Callable[[float], BACoeffs]:
77
+ # Create a wrapper function that ignores fs parameter since gaussian smoothing doesn't need it
78
+ def design_wrapper(fs: float) -> BACoeffs:
79
+ return gaussian_smoothing_filter_design(
80
+ sigma=self.settings.sigma,
81
+ width=self.settings.width,
82
+ kernel_size=self.settings.kernel_size,
83
+ )
84
+
85
+ return design_wrapper
86
+
87
+
88
+ class GaussianSmoothingFilter(
89
+ BaseFilterByDesignTransformerUnit[
90
+ GaussianSmoothingSettings, GaussianSmoothingFilterTransformer
91
+ ]
92
+ ):
93
+ SETTINGS = GaussianSmoothingSettings
@@ -0,0 +1,110 @@
1
+ import functools
2
+ import typing
3
+
4
+ import numpy as np
5
+ import numpy.typing as npt
6
+ import scipy.signal
7
+
8
+ from .filter import (
9
+ FilterBaseSettings,
10
+ FilterByDesignTransformer,
11
+ BACoeffs,
12
+ BaseFilterByDesignTransformerUnit,
13
+ )
14
+
15
+
16
+ class KaiserFilterSettings(FilterBaseSettings):
17
+ """Settings for :obj:`KaiserFilter`"""
18
+
19
+ # axis and coef_type are inherited from FilterBaseSettings
20
+
21
+ cutoff: float | npt.ArrayLike | None = None
22
+ """
23
+ Cutoff frequency of filter (expressed in the same units as fs) OR an array of cutoff frequencies
24
+ (that is, band edges). In the former case, as a float, the cutoff frequency should correspond with
25
+ the half-amplitude point, where the attenuation will be -6dB. In the latter case, the frequencies in
26
+ cutoff should be positive and monotonically increasing between 0 and fs/2. The values 0 and fs/2 must
27
+ not be included in cutoff.
28
+ """
29
+
30
+ ripple: float | None = None
31
+ """
32
+ Upper bound for the deviation (in dB) of the magnitude of the filter's frequency response from that of
33
+ the desired filter (not including frequencies in any transition intervals).
34
+ See scipy.signal.kaiserord for more information.
35
+ """
36
+
37
+ width: float | None = None
38
+ """
39
+ If width is not None, then assume it is the approximate width of the transition region (expressed in
40
+ the same units as fs) for use in Kaiser FIR filter design.
41
+ See scipy.signal.kaiserord for more information.
42
+ """
43
+
44
+ pass_zero: bool | str = True
45
+ """
46
+ If True, the gain at the frequency 0 (i.e., the “DC gain”) is 1. If False, the DC gain is 0. Can also
47
+ be a string argument for the desired filter type (equivalent to btype in IIR design functions).
48
+ {‘lowpass’, ‘highpass’, ‘bandpass’, ‘bandstop’}
49
+ """
50
+
51
+ wn_hz: bool = True
52
+ """
53
+ Set False if cutoff and width are normalized from 0 to 1, where 1 is the Nyquist frequency
54
+ """
55
+
56
+
57
+ def kaiser_design_fun(
58
+ fs: float,
59
+ cutoff: float | npt.ArrayLike | None = None,
60
+ ripple: float | None = None,
61
+ width: float | None = None,
62
+ pass_zero: bool | str = True,
63
+ wn_hz: bool = True,
64
+ ) -> BACoeffs | None:
65
+ """
66
+ Design an `order`th-order FIR Kaiser filter and return the filter coefficients.
67
+ See :obj:`FIRFilterSettings` for argument description.
68
+
69
+ Returns:
70
+ The filter taps as designed by firwin
71
+ """
72
+ if ripple is None or width is None or cutoff is None:
73
+ return None
74
+
75
+ width = width / (0.5 * fs) if wn_hz else width
76
+ n_taps, beta = scipy.signal.kaiserord(ripple, width)
77
+ if n_taps % 2 == 0:
78
+ n_taps += 1
79
+ taps = scipy.signal.firwin(
80
+ numtaps=n_taps,
81
+ cutoff=cutoff,
82
+ window=("kaiser", beta), # type: ignore
83
+ pass_zero=pass_zero, # type: ignore
84
+ scale=False,
85
+ fs=fs if wn_hz else None,
86
+ )
87
+
88
+ return (taps, np.array([1.0]))
89
+
90
+
91
+ class KaiserFilterTransformer(
92
+ FilterByDesignTransformer[KaiserFilterSettings, BACoeffs]
93
+ ):
94
+ def get_design_function(
95
+ self,
96
+ ) -> typing.Callable[[float], BACoeffs | None]:
97
+ return functools.partial(
98
+ kaiser_design_fun,
99
+ cutoff=self.settings.cutoff,
100
+ ripple=self.settings.ripple,
101
+ width=self.settings.width,
102
+ pass_zero=self.settings.pass_zero,
103
+ wn_hz=self.settings.wn_hz,
104
+ )
105
+
106
+
107
+ class KaiserFilter(
108
+ BaseFilterByDesignTransformerUnit[KaiserFilterSettings, KaiserFilterTransformer]
109
+ ):
110
+ SETTINGS = KaiserFilterSettings