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
« 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:
4.. code-block:: python
6 import signal_edges.signal as ses
8 class ExampleSignal(ses.filters.BesselFiltersMixin, 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_bessel(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/001_example_bessel_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 BesselFiltersMixin(FiltersMixin):
54 """Mixin derived from :class:`.FiltersMixin` for :class:`.Signal` that implements Bessel filters."""
56 # pylint: disable=too-few-public-methods
58 ## Private API #####################################################################################################
60 ## Protected API ###################################################################################################
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.
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.
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
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.")
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.")
113 # Create a copy of self, and update the vertical values with the new filtered values.
114 filtered_signal = deepcopy(self)
116 # Use zero phase filter for filtering, creating the same number of samples.
117 setattr(filtered_signal, "_vv", scipy.signal.sosfiltfilt(sos, self._vv))
119 return filtered_signal