Coverage for src/signal_edges/signal/generator/generator.py: 68%

70 statements  

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

1"""The signal generator, :class:`.SignalGenerator`, can be used to generate different kinds arrays suitable to create 

2signals for testing. A code snippet of its use along with :class:`.VoltageSignal` is shown below: 

3 

4.. code-block:: python 

5 

6 import signal_edges.signal as ses 

7 

8 # Create generator with initial values. 

9 generator = ses.generator.SignalGenerator(0, 0.001, 100, 0, 100) 

10 

11 # Build signal with multiple pulses. 

12 for _ in range(0, 4): 

13 generator.add_flat(10) 

14 generator.add_edge("falling", 0.0, 10) 

15 generator.add_flat(10) 

16 generator.add_edge("rising", 100.0, 10) 

17 

18 # Generate signal with some noise. 

19 (signal_x, signal_y) = generator.generate(noise=(0, 5)) 

20 signal = ses.VoltageSignal(signal_x, signal_y, "s", "V") 

21  

22 # Plot signal. 

23 signal.signal_plot("signal.png") 

24  

25This code snippet generates the following plot: 

26 

27.. figure:: ../../.assets/img/008_example_generated_signal.png 

28 :width: 600 

29 :align: center 

30  

31 The generated signal.""" 

32 

33from typing import Literal 

34 

35import numpy as np 

36import numpy.typing as npt 

37 

38from ...definitions import get_logger 

39from ...exceptions import SignalError 

40 

41 

42class SignalGenerator: 

43 """Generates arrays for the horizontal axis and vertical axis of :class:`.Signal` derived classes.""" 

44 

45 ## Private API ##################################################################################################### 

46 def __init__(self, hinit: float, hstep: float, vhigh: float, vlow: float, vinit: float) -> None: 

47 """Class constructor. 

48 

49 :param hinit: Initial value on the horizontal axis. 

50 :param hstep: Step between values of the horizontal axis, created automatically as values on the vertical axis 

51 are added. 

52 :param vhigh: The highest value on the vertical axis, used to calculate the full range of the signal. 

53 :param vlow: The lowest value on the vertical axis, used to calculate the full range of the signal. 

54 :param vinit: The initial value of the vertical axis, as a percentage of the full range, ``vhigh`` - ``vlow``. 

55 :raise SignalError: At least one of the parameters given is invalid.""" 

56 # pylint: disable=too-many-arguments 

57 

58 # Check for valid arguments. 

59 if any([vhigh <= vlow, hstep <= 0, vinit < 0, vinit > 100]): 59 ↛ 60line 59 didn't jump to line 60, because the condition on line 59 was never true

60 raise SignalError("The parameters passed to the signal generator constructor are invalid.") 

61 

62 #: Logger. 

63 self._logger = get_logger() 

64 #: The step between values of the signal in the horizontal axis. 

65 self._hstep = hstep 

66 #: The highest value of the signal in the vertical axis. 

67 self._vhigh = vhigh 

68 #: The lowest value of the signal in the vertical axis. 

69 self._vlow = vlow 

70 #: Full range of the signal, derived from t1he highest and lowest values on the vertical axis. 

71 self._vrange = np.absolute(self._vhigh - self._vlow) 

72 #: Accumulated values on the horizontal axis. 

73 self.__hv: npt.NDArray[np.float_] = np.full((1, 1), hinit)[0] 

74 #: Accumulated values on the vertical axis. 

75 self.__vv: npt.NDArray[np.float_] = np.full((1, 1), self._vvalue(vinit))[0] 

76 

77 ## Protected API ################################################################################################### 

78 def _vvalue(self, percentage: float) -> np.float_: 

79 """Calculates a value for the vertical axis from a percentage. 

80 

81 :param percentage: The percentage value of the full range. 

82 :raise SignalError: The target value must be a percentage value. 

83 :return: The absolute value.""" 

84 # Ensure that the target value is a percentage value. 

85 if any([percentage < 0, percentage > 100]): 85 ↛ 86line 85 didn't jump to line 86, because the condition on line 85 was never true

86 raise SignalError(f"The target percentage '{percentage}%' is not a value in the range zero to one hundred.") 

87 return self._vlow + self._vrange * (percentage / 100) 

88 

89 ## Public API ###################################################################################################### 

90 def add_flat(self, values: int) -> "SignalGenerator": 

91 """Adds a flat section, using the last value on the vertical axis as reference. 

92 

93 :param values: Number of values to add to both horizontal and vertical axes. 

94 :raise SignalError: The number of values must be larger than zero. 

95 :return: Instance of the class.""" 

96 # Ensure that the number of values to add is largen than zero. 

97 if values <= 0: 97 ↛ 98line 97 didn't jump to line 98, because the condition on line 97 was never true

98 raise SignalError(f"The number of values, '{values}', must be a positive number.") 

99 

100 # Get the initial for the beginning of the section. 

101 hval = self.__hv[-1] 

102 vval = self.__vv[-1] 

103 

104 # Generate the sections. 

105 hsect = np.linspace(hval, hval + self._hstep * values, num=values + 1, endpoint=True) 

106 vsect = np.full((1, values), vval)[0] 

107 

108 # Generate samples. 

109 self.__hv = np.concatenate([self.__hv, hsect[1:]]) 

110 self.__vv = np.concatenate([self.__vv, vsect]) 

111 

112 return self 

113 

114 def add_edge(self, edge_type: Literal["rising", "falling"], vtarget: float, values: int) -> "SignalGenerator": 

115 """Adds a rising or falling edge. 

116 

117 :param edge_type: The type of edge to add. 

118 :param values: Number of values to add to both horizontal and vertical axes. 

119 :param vtarget: The target value on the vertical axis at the end of the edge, as a percentage of the full range. 

120 :raise SignalError: The number of values must be larger than zero. 

121 :raise SignalError: The type of edge is not valid. 

122 :raise SignalError: The target value must be higher than the current value for a rising edge. 

123 :raise SignalError: The target value must be lower than the current value for a falling edge. 

124 :return: Instance of the class.""" 

125 # Ensure that the number of values to add is largen than zero. 

126 if values <= 0: 126 ↛ 127line 126 didn't jump to line 127, because the condition on line 126 was never true

127 raise SignalError(f"The number of values, {values}, must be a positive number.") 

128 # The type of edge is not recognized. 

129 if edge_type not in ("rising", "falling"): 129 ↛ 130line 129 didn't jump to line 130, because the condition on line 129 was never true

130 raise SignalError("The type of the edge provided is not valid.") 

131 

132 # Update target value to absolute number. 

133 vtarget_val = self._vvalue(vtarget) 

134 

135 # Ensure the target value is higher than the current value for rising edges. 

136 if edge_type == "rising" and vtarget_val <= self.__vv[-1]: 136 ↛ 137line 136 didn't jump to line 137, because the condition on line 136 was never true

137 raise SignalError("Target value is below the current value of the signal, can't add rising edge.") 

138 # Ensure the target value is lower than the current value for falling edges. 

139 if edge_type == "falling" and self.__vv[-1] <= vtarget_val: 139 ↛ 140line 139 didn't jump to line 140, because the condition on line 139 was never true

140 raise SignalError("Target value is above the current value of the signal, can't add falling edge.") 

141 

142 # Get the initial for the beginning of the section. 

143 hval = self.__hv[-1] 

144 vval = self.__vv[-1] 

145 

146 # Generate the sections. 

147 hsect = np.linspace(hval, hval + self._hstep * values, num=values + 1, endpoint=True) 

148 vsect = np.linspace(vval, vtarget_val, num=values + 1, endpoint=True) 

149 

150 # Generate values. 

151 self.__hv = np.concatenate([self.__hv, hsect[1:]]) 

152 self.__vv = np.concatenate([self.__vv, vsect[1:]]) 

153 

154 return self 

155 

156 def repeat(self, count: int = 1) -> "SignalGenerator": 

157 """Repeats the current pattern the specified number of times. 

158 

159 :param count: The number of times to repeat the signal. 

160 :raise SignalError: The number of times to repeat the signal not one or higher. 

161 :return: Instance of the class.""" 

162 # Ensure the number of times to repeat the signal is valid. 

163 if count < 1: 

164 raise SignalError(f"The number of times to repeat, {count}, the signal must be 1 or higher.") 

165 

166 # Generate the horizontal section. 

167 hval = self.__hv[-1] 

168 values = len(self.__hv) * count 

169 hsect = np.linspace(hval, hval + self._hstep * values, num=values + 1, endpoint=True) 

170 

171 # Generate the vertical section. 

172 vsect = np.tile(self.__vv, count) 

173 

174 # Generate values. 

175 self.__hv = np.concatenate([self.__hv, hsect[1:]]) 

176 self.__vv = np.concatenate([self.__vv, vsect]) 

177 

178 return self 

179 

180 def generate( 

181 self, noise: tuple[float, float] | None = None, hdecs: int | None = None, vdecs: int | None = None 

182 ) -> tuple[npt.NDArray[np.float_], npt.NDArray[np.float_]]: 

183 """Obtains the values for the generated signal, optionally adding Gaussian noise. 

184 

185 :param noise: Gaussian noise to add, `mean` and `stddev` respectively, defaults to no noise. 

186 :param hdecs: Number of decimals to round the horizontal axis values to, defaults to no rounding. 

187 :param vdecs: Number of decimals to round the vertical axis values to, defaults to no rounding. 

188 :return: The values of the horizontal axis and the values of the vertical axis.""" 

189 # Create copies of the arrays. 

190 hvalues = np.copy(self.__hv) 

191 vvalues = np.copy(self.__vv) 

192 

193 # Add noise if requested. 

194 if noise is not None: 

195 vvalues = vvalues + np.random.normal(noise[0], noise[1], len(vvalues)) 

196 # Round horizontal axis values if requested. 

197 if hdecs is not None: 197 ↛ 198line 197 didn't jump to line 198, because the condition on line 197 was never true

198 hvalues = np.round(hvalues, hdecs) 

199 # Round vertical axis values if requested. 

200 if vdecs is not None: 200 ↛ 201line 200 didn't jump to line 201, because the condition on line 200 was never true

201 vvalues = np.round(vvalues, vdecs) 

202 

203 return (hvalues, vvalues) 

204 

205 @property 

206 def count(self) -> int: 

207 """Number of values in the signal. 

208 

209 :return: Number of values currently in the signal.""" 

210 return len(self.__hv)