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

25 statements  

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

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

2add elliptic 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.EllipticFiltersMixin, 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_elliptic(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/003_example_elliptic_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 EllipticFiltersMixin(FiltersMixin): 

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

55 

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

57 

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

59 

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

61 

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

63 def filters_elliptic( 

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 pb_ripple: float = 0.1, 

72 sb_ripple: float = 100.0, 

73 ) -> Self: 

74 """Runs an elliptic filter on the signal, refer to :func:`scipy.signal.ellip` for more information. 

75 

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

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

78 

79 :param sampling_frequency: Sampling frequency in Hertz. 

80 :param order: Order of the filter. 

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

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

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

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

85 :param pb_ripple: Pass band ripple, defaults to ``0.1``. 

86 :param sb_ripple: Stop band ripple, defaults to ``100.0``. 

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

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

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

90 # pylint: disable=too-many-arguments 

91 

92 # Figure out filter mode from the cutoff frequencies. 

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

94 cutoff, ftype = lp_cutoff, "lowpass" 

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

96 cutoff, ftype = hp_cutoff, "highpass" 

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

98 cutoff, ftype = bp_cutoff, "bandpass" 

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

100 cutoff, ftype = bs_cutoff, "bandstop" 

101 else: 

102 raise FiltersError("Invalid cutoff frequency combination given to elliptic filter.") 

103 

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

105 sos = scipy.signal.ellip( 

106 N=order, 

107 rp=pb_ripple, 

108 rs=sb_ripple, 

109 Wn=cutoff, 

110 btype=ftype, 

111 analog=False, 

112 output="sos", 

113 fs=sampling_frequency, 

114 ) 

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

116 raise FiltersError("Invalid elliptic filter configuration given.") 

117 

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

119 filtered_signal = deepcopy(self) 

120 

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

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

123 

124 return filtered_signal