Coverage for src/signal_edges/signal/signal.py: 76%
64 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 signal class, :class:`.Signal`, is the base class from which specialized signals can be created and to which
2mixins with additional functionality can be added, the current available mixins are the following:
4 - `Filter mixins`, :class:`.BesselFiltersMixin`, :class:`.ButterworthFiltersMixin`, :class:`.EllipticFiltersMixin`,
5 to filter the signal by different methods also providing the possibility to implement your own filters via the
6 :class:`.FiltersMixin` base class.
7 - `State levels mixin`, :class:`.StateLevelsMixin`, to calculate the logical state levels of the signal.
8 - `Edges mixin`, :class:`.EdgesMixin`, to obtain the edges of the signal.
10A detailed example of an specialized signal that ships with the package is the voltage signal, :class:`.VoltageSignal`,
11its implementation can be checked for details on how to use the base signal class.
13An example of its usage is shown in the following code snippet:
15.. code-block:: python
17 import numpy as np
18 import signal_edges.signal as ses
20 # Create timestamps in seconds for the signal, the horizontal axis.
21 signal_timestamps = np.linspace(start=0, stop=160, num=160, endpoint=False)
22 # Create voltages in seconds for the signal, the vertical axis.
23 signal_voltages = np.asarray([0, 0, 0, 0, 5, 5, 5, 5, 5, 5] * (160 // 10))
24 # Create signal with the previous values, and seconds and volts as units.
25 signal = ses.VoltageSignal(signal_timestamps, signal_voltages, "s", "V")
27 # Plot signal to file.
28 signal.signal_plot("signal.png")
30Which creates the following signal:
32.. figure:: ../.assets/img/000_example_signal.png
33 :width: 600
34 :align: center
36 The generated signal in the code snippet."""
38try:
39 from typing import Self
40except ImportError:
41 from typing_extensions import Self
43import logging
44from abc import ABC
46import numpy as np
47import numpy.typing as npt
49from .. import plotter as sep
50from ..definitions import get_logger
51from ..exceptions import SignalError
54class Signal(ABC):
55 """Base class for signal, meant to be derived to create specialized signals on which to add mixins from this
56 package with additional functionality.
58 A signal consists in two `1xN` arrays, where `N` is the number of values, one with the values for the
59 horizontal axis and another one with the values for the vertical axis. The number of values in each array must be
60 the same, and additionally the values of the horizontal axis must satisfy the requirement `x[n] < x[n+1]` for
61 all their values.
63 .. note::
65 For simplicity, the Numpy arrays provided to this class are copied internally, changes outside this classs
66 to the arrays provided will not reflect on the internal ones, use the relevant getters and setters to reflect
67 this changes on the internal arrays."""
69 # pylint: disable=too-few-public-methods
71 ## Private API #####################################################################################################
72 def __init__(
73 self,
74 hvalues: npt.NDArray[np.float_],
75 vvalues: npt.NDArray[np.float_],
76 *args,
77 hunits: sep.Units | None = None,
78 vunits: sep.Units | None = None,
79 **kwargs,
80 ) -> None:
81 """The constructor for the signal class.
83 :meta public:
84 :param hvalues: A `1xN` array with the values of the horizontal axis to copy for the signal.
85 :param vvalues: A `1xN` array with the values of the vertical axis to copy for the signal.
86 :param hunits: The units of the values of the horizontal axis for plots, defaults to no units.
87 :param vunits: The units of the values of the vertical axis for plots, defaults to no units."""
88 # pylint: disable=unused-argument
90 #: Logger.
91 self.__logger = get_logger()
92 #: Values of the horizontal axis for the signal, must satisfy ``x[n] < x[n+1]``.
93 self.__hv = np.array(hvalues, dtype=np.float_, copy=True, order="C")
94 #: Values of the vertical axis for the signal.
95 self.__vv = np.array(vvalues, dtype=np.float_, copy=True, order="C")
96 #: Units for the values on the horizontal axis.
97 self.__hunits = hunits if hunits is not None else sep.Units("N/A", "N/A", "N/A")
98 #: Units for the values on the vertical axis.
99 self.__vunits = vunits if vunits is not None else sep.Units("N/A", "N/A", "N/A")
100 # Validate values after initialization finished.
101 self._validate_values()
103 ## Protected API ###################################################################################################
104 @property
105 def _logger(self) -> logging.Logger:
106 """Getter for the logger for the signal, use this from the derived class to print information to the logger
107 of the package. For information on how to configure the package logger refer to :func:`.get_logger`.
109 :meta public:
110 :return: The logger."""
111 return self.__logger
113 @property
114 def _hv(self) -> npt.NDArray[np.float_]:
115 """Getter for the values of the horizontal axis.
117 :meta public:
118 :return: A `1xN` array with the values of the horizontal axis."""
119 return self.__hv
121 @_hv.setter
122 def _hv(self, new_hv: npt.NDArray[np.float_]) -> None:
123 """Setter for the values of the horizontal axis.
125 .. note::
127 When setting the values directly, :meth:`.Signal._validate_values` can be used to validate them.
129 :meta public:
130 :param new_hv: A `1xN` array with the new values of the horizontal axis."""
131 self.__hv = new_hv
133 @property
134 def _vv(self) -> npt.NDArray[np.float_]:
135 """Getter for the values of the vertical axis.
137 :meta public:
138 :return: A `1xN` array with the values of the vertical axis."""
139 return self.__vv
141 @_vv.setter
142 def _vv(self, new_vv: npt.NDArray[np.float_]) -> None:
143 """Setter for the values of the vertical axis.
145 .. note::
147 When setting the values directly, :meth:`.Signal._validate_values` can be used to validate them.
149 :meta public:
150 :param new_vv: A `1xN` array with the new values of the vertical axis."""
151 self.__vv = new_vv
153 @property
154 def _hunits(self) -> sep.Units:
155 """Getter for the units of the values of the horizontal axis.
157 :meta public:
158 :return: The units."""
159 return self.__hunits
161 @_hunits.setter
162 def _hunits(self, new_hunits: sep.Units) -> None:
163 """Setter for the units of the values of the horizontal axis.
165 :meta public:
166 :param new_hunits: The new units for the values of the horizontal axis."""
167 self.__hunits = new_hunits
169 @property
170 def _vunits(self) -> sep.Units:
171 """Getter for the units of the values of the vertical axis.
173 :meta public:
174 :return: The units."""
175 return self.__vunits
177 @_vunits.setter
178 def _vunits(self, new_vunits: sep.Units) -> None:
179 """Setter for the units of the values of the vertical axis.
181 :meta public:
182 :param new_vunits: The new units for the values of the vertical axis."""
183 self.__vunits = new_vunits
185 def _validate_values(self) -> "Signal":
186 """Validates the values of the horizontal axis and the vertical axis.
188 :raise SignalError: The horizontal or vertical axes values provided are not on the form `1xN`.
189 :raise SignalError: The number of horizontal or vertical axis values is zero.
190 :raise SignalError: The number of horizontal and vertical axis values is not the same.
191 :raise SignalError: The horizontal axis values do not satisfy the `x[n] < x[n+1]` requirement.
192 :return: Instance of the class."""
193 # Check that the arrays are of the form 1xN.
194 if any([len(self.__hv.shape) != 1, len(self.__vv.shape) != 1]): 194 ↛ 195line 194 didn't jump to line 195, because the condition on line 194 was never true
195 raise SignalError("The values of the horizontal or vertical axis are not of the form 1xN.")
196 # Ensure both axis have at least one value.
197 if any([len(self.__hv) == 0, len(self.__vv) == 0]): 197 ↛ 198line 197 didn't jump to line 198, because the condition on line 197 was never true
198 raise SignalError("The number of values in the horizontal or vertical axis can't be zero.")
199 # Ensure both axis have the same number of values.
200 if len(self.__hv) != len(self.__vv): 200 ↛ 201line 200 didn't jump to line 201, because the condition on line 200 was never true
201 raise SignalError("The number of values of the horizontal and vertical axis must be the same.")
202 # Ensure the values of the horizontal axis satisfy the x[n] < x[n+1] requirement.
203 if len(np.where(np.diff(self.__hv) <= 0)[0]) > 0: 203 ↛ 204line 203 didn't jump to line 204, because the condition on line 203 was never true
204 raise SignalError("The horizontal axis values given do not satisfy the x[n] < x[n+1] requirement.")
206 return self
208 ## Public API ######################################################################################################
209 def signal_plot(
210 self,
211 path: str,
212 *args,
213 begin: float | None = None,
214 end: float | None = None,
215 munits: float = 0,
216 **kwargs,
217 ) -> Self:
218 """Performs a plot of the signal.
220 :param path: The path where to store the plot, see :meth:`.Plotter.plot`.
221 :param args: Additional arguments to pass to the plotting function, see :meth:`.Plotter.plot`.
222 :param begin: The begin value of the horizontal axis where the plot starts, see :meth:`.Plotter.plot`.
223 :param end: The end value of the horizontal axis where the plot ends, see :meth:`.Plotter.plot`.
224 :param munits: Margin units for the plot, see :meth:`.Plotter.plot`.
225 :param kwargs: Additional keyword arguments to pass to the plotting function, see :meth:`.Plotter.plot`.
226 :return: Instance of the class."""
227 # pylint: disable=too-many-locals
229 # Create plotter.
230 plotter = sep.Plotter(sep.Mode.LINEAR, rows=1, columns=1)
232 # Adjust begin and end values if not provided.
233 begin = begin if begin is not None else float(self._hv[0])
234 end = end if end is not None else float(self._hv[-1])
236 # Create plot for the signal.
237 spl = sep.Subplot("Signal", self._hv, self._hunits, self._vv, self._vunits, begin, end, munits, "red")
238 plotter.add_plot(0, 0, spl)
240 # Create plot.
241 plotter.plot(path, *args, **kwargs)
243 return self