Coverage for src/signal_edges/signal/filters/butterworth.py: 49%

25 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-04-21 11:16 +0000

1"""The :class:`.ButterworthFiltersMixin` implements a mixin that can be added to :class:`.Signal` derived classes to 

2add Butterworth filters functionality, as shown in the code snippet below: 

3 

4.. code-block:: python 

5 

6 import signal_edges.signal as ses 

7 

8 class ExampleSignal(ses.filters.ButterworthFiltersMixin, ses.Signal): 

9 pass 

10  

11An example of its usage using :class:`.VoltageSignal` is described below: 

12 

13.. code-block:: python 

14 

15 import numpy as np 

16 import signal_edges.signal as ses 

17 

18 # Create timestamps for the signal, and calculate the sampling frequency. 

19 signal_timestamps = np.linspace(start=0, stop=160, num=160, endpoint=False) 

20 sampling_frequency = 1 / (signal_timestamps[1] - signal_timestamps[0]) 

21 # Create voltages for the signal, and add some noise to them. 

22 signal_voltages = np.asarray([0, 0, 0, 0, 5, 5, 5, 5, 5, 5] * (160 // 10)) + \\ 

23 np.random.normal(0, 0.1, 160) 

24 # Create signal. 

25 signal = ses.VoltageSignal(signal_timestamps, signal_voltages, "s", "V") 

26 # Create the filtered signal. 

27 filtered_signal = signal.filters_butterworth(sampling_frequency, 2, lp_cutoff=sampling_frequency / 2.5) 

28  

29 # Plot original signal and filtered signal. 

30 signal.filters_plot("signal.png", filtered_signal) 

31 

32This code snippet generates the following plot: 

33 

34.. figure:: ../../.assets/img/002_example_butterworth_signal.png 

35 :width: 600 

36 :align: center 

37  

38 The generated signal and filtered signal in the code snippet.""" 

39 

40try: 

41 from typing import Self 

42except ImportError: 

43 from typing_extensions import Self 

44 

45from copy import deepcopy 

46 

47import scipy.signal 

48 

49from ...exceptions import FiltersError 

50from .filters import FiltersMixin 

51 

52 

53class ButterworthFiltersMixin(FiltersMixin): 

54 """Mixin derived from :class:`.FiltersMixin` for :class:`.Signal` that implements Butterworth filters.""" 

55 

56 # pylint: disable=too-few-public-methods 

57 

58 ## Private API ##################################################################################################### 

59 

60 ## Protected API ################################################################################################### 

61 

62 ## Public API ###################################################################################################### 

63 def filters_butterworth( 

64 self, 

65 sampling_frequency: float, 

66 order: int, 

67 lp_cutoff: float | None = None, 

68 hp_cutoff: float | None = None, 

69 bp_cutoff: tuple[float, float] | None = None, 

70 bs_cutoff: tuple[float, float] | None = None, 

71 ) -> Self: 

72 """Runs a Butterworth filter on the signal, refer to :func:`scipy.signal.butter` for more information. 

73 

74 One of ``lp_cutoff``, ``hp_cutoff``, ``bp_cutoff`` or ``bs_cutoff`` must be specified. If none or more than one 

75 are specified, an error is raised as it won't be possible to determine the type of filter to apply. 

76 

77 :param sampling_frequency: Sampling frequency in Hertz. 

78 :param order: Order of the filter. 

79 :param lp_cutoff: Use a lowpass filter with this cutoff frequency in Hertz, defaults to ``None``. 

80 :param hp_cutoff: Use a highpass filter with this cutoff frequency in Hertz, defaults to ``None``. 

81 :param bp_cutoff: Use a bandpass filter with this pair of cutoff frequencies in Hertz, defaults to ``None``. 

82 :param bs_cutoff: Use a bandstop filter with this pair of cutoff frequencies in Hertz, defaults to ``None``. 

83 :raise FiltersError: Could not determine the type of filter to use. 

84 :raise FiltersError: Could not create underlying Second Order Sections for filter. 

85 :return: A new instance of the class with the filtered signal.""" 

86 # pylint: disable=too-many-arguments 

87 

88 # Figure out filter mode from the cutoff frequencies. 

89 if all([lp_cutoff is not None, hp_cutoff is None, bp_cutoff is None, bs_cutoff is None]): 89 ↛ 91line 89 didn't jump to line 91, because the condition on line 89 was never false

90 cutoff, ftype = lp_cutoff, "lowpass" 

91 elif all([lp_cutoff is None, hp_cutoff is not None, bp_cutoff is None, bs_cutoff is None]): 

92 cutoff, ftype = hp_cutoff, "highpass" 

93 elif all([lp_cutoff is None, hp_cutoff is None, bp_cutoff is not None, bs_cutoff is None]): 

94 cutoff, ftype = bp_cutoff, "bandpass" 

95 elif all([lp_cutoff is None, hp_cutoff is None, bp_cutoff is None, bs_cutoff is not None]): 

96 cutoff, ftype = bs_cutoff, "bandstop" 

97 else: 

98 raise FiltersError("Invalid cutoff frequency combination given to Butterworth filter.") 

99 

100 # Create second order sections and check they are valid. 

101 sos = scipy.signal.butter( 

102 N=order, 

103 Wn=cutoff, 

104 btype=ftype, 

105 analog=False, 

106 output="sos", 

107 fs=sampling_frequency, 

108 ) 

109 if sos is None: 109 ↛ 110line 109 didn't jump to line 110, because the condition on line 109 was never true

110 raise FiltersError("Invalid Butterworth filter configuration given.") 

111 

112 # Create a copy of self, and update the vertical values with the new filtered values. 

113 filtered_signal = deepcopy(self) 

114 

115 # Use zero phase filter for filtering, creating the same number of samples. 

116 setattr(filtered_signal, "_vv", scipy.signal.sosfiltfilt(sos, self._vv)) 

117 

118 return filtered_signal