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
« 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:
4.. code-block:: python
6 import signal_edges.signal as ses
8 class ExampleSignal(ses.filters.EllipticFiltersMixin, ses.Signal):
9 pass
11An example of its usage using :class:`.VoltageSignal` is described below:
13.. code-block:: python
15 import numpy as np
16 import signal_edges.signal as ses
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)
29 # Plot original signal and filtered signal.
30 signal.filters_plot("signal.png", filtered_signal)
32This code snippet generates the following plot:
34.. figure:: ../../.assets/img/003_example_elliptic_signal.png
35 :width: 600
36 :align: center
38 The generated signal and filtered signal in the code snippet."""
40try:
41 from typing import Self
42except ImportError:
43 from typing_extensions import Self
45from copy import deepcopy
47import scipy.signal
49from ...exceptions import FiltersError
50from .filters import FiltersMixin
53class EllipticFiltersMixin(FiltersMixin):
54 """Mixin derived from :class:`.FiltersMixin` for :class:`.Signal` that implements elliptic filters."""
56 # pylint: disable=too-few-public-methods
58 ## Private API #####################################################################################################
60 ## Protected API ###################################################################################################
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.
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.
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
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.")
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.")
118 # Create a copy of self, and update the vertical values with the new filtered values.
119 filtered_signal = deepcopy(self)
121 # Use zero phase filter for filtering, creating the same number of samples.
122 setattr(filtered_signal, "_vv", scipy.signal.sosfiltfilt(sos, self._vv))
124 return filtered_signal