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

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: 

3 

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. 

9 

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.  

12 

13An example of its usage is shown in the following code snippet: 

14 

15.. code-block:: python 

16 

17 import numpy as np 

18 import signal_edges.signal as ses 

19  

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") 

26 

27 # Plot signal to file. 

28 signal.signal_plot("signal.png") 

29 

30Which creates the following signal: 

31 

32.. figure:: ../.assets/img/000_example_signal.png 

33 :width: 600 

34 :align: center 

35  

36 The generated signal in the code snippet.""" 

37 

38try: 

39 from typing import Self 

40except ImportError: 

41 from typing_extensions import Self 

42 

43import logging 

44from abc import ABC 

45 

46import numpy as np 

47import numpy.typing as npt 

48 

49from .. import plotter as sep 

50from ..definitions import get_logger 

51from ..exceptions import SignalError 

52 

53 

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. 

57 

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. 

62 

63 .. note:: 

64 

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.""" 

68 

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

70 

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. 

82 

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 

89 

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() 

102 

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`. 

108 

109 :meta public: 

110 :return: The logger.""" 

111 return self.__logger 

112 

113 @property 

114 def _hv(self) -> npt.NDArray[np.float_]: 

115 """Getter for the values of the horizontal axis. 

116 

117 :meta public: 

118 :return: A `1xN` array with the values of the horizontal axis.""" 

119 return self.__hv 

120 

121 @_hv.setter 

122 def _hv(self, new_hv: npt.NDArray[np.float_]) -> None: 

123 """Setter for the values of the horizontal axis. 

124 

125 .. note:: 

126 

127 When setting the values directly, :meth:`.Signal._validate_values` can be used to validate them. 

128 

129 :meta public: 

130 :param new_hv: A `1xN` array with the new values of the horizontal axis.""" 

131 self.__hv = new_hv 

132 

133 @property 

134 def _vv(self) -> npt.NDArray[np.float_]: 

135 """Getter for the values of the vertical axis. 

136 

137 :meta public: 

138 :return: A `1xN` array with the values of the vertical axis.""" 

139 return self.__vv 

140 

141 @_vv.setter 

142 def _vv(self, new_vv: npt.NDArray[np.float_]) -> None: 

143 """Setter for the values of the vertical axis. 

144 

145 .. note:: 

146 

147 When setting the values directly, :meth:`.Signal._validate_values` can be used to validate them. 

148 

149 :meta public: 

150 :param new_vv: A `1xN` array with the new values of the vertical axis.""" 

151 self.__vv = new_vv 

152 

153 @property 

154 def _hunits(self) -> sep.Units: 

155 """Getter for the units of the values of the horizontal axis. 

156 

157 :meta public: 

158 :return: The units.""" 

159 return self.__hunits 

160 

161 @_hunits.setter 

162 def _hunits(self, new_hunits: sep.Units) -> None: 

163 """Setter for the units of the values of the horizontal axis. 

164 

165 :meta public: 

166 :param new_hunits: The new units for the values of the horizontal axis.""" 

167 self.__hunits = new_hunits 

168 

169 @property 

170 def _vunits(self) -> sep.Units: 

171 """Getter for the units of the values of the vertical axis. 

172 

173 :meta public: 

174 :return: The units.""" 

175 return self.__vunits 

176 

177 @_vunits.setter 

178 def _vunits(self, new_vunits: sep.Units) -> None: 

179 """Setter for the units of the values of the vertical axis. 

180 

181 :meta public: 

182 :param new_vunits: The new units for the values of the vertical axis.""" 

183 self.__vunits = new_vunits 

184 

185 def _validate_values(self) -> "Signal": 

186 """Validates the values of the horizontal axis and the vertical axis. 

187 

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.") 

205 

206 return self 

207 

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. 

219 

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 

228 

229 # Create plotter. 

230 plotter = sep.Plotter(sep.Mode.LINEAR, rows=1, columns=1) 

231 

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]) 

235 

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) 

239 

240 # Create plot. 

241 plotter.plot(path, *args, **kwargs) 

242 

243 return self