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

25 statements  

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

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

2add Bessel 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.BesselFiltersMixin, 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_bessel(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/001_example_bessel_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 BesselFiltersMixin(FiltersMixin): 

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

55 

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

57 

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

59 

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

61 

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

63 def filters_bessel( 

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 Bessel filter on the signal, refer to :func:`scipy.signal.bessel` 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 Bessel filter.") 

99 

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

101 sos = scipy.signal.bessel( 

102 N=order, 

103 Wn=cutoff, 

104 btype=ftype, 

105 analog=False, 

106 output="sos", 

107 fs=sampling_frequency, 

108 norm="mag", 

109 ) 

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

111 raise FiltersError("Invalid Bessel filter configuration given.") 

112 

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

114 filtered_signal = deepcopy(self) 

115 

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

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

118 

119 return filtered_signal