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
« 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:
4.. code-block:: python
6 import signal_edges.signal as ses
8 # Create generator with initial values.
9 generator = ses.generator.SignalGenerator(0, 0.001, 100, 0, 100)
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)
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")
22 # Plot signal.
23 signal.signal_plot("signal.png")
25This code snippet generates the following plot:
27.. figure:: ../../.assets/img/008_example_generated_signal.png
28 :width: 600
29 :align: center
31 The generated signal."""
33from typing import Literal
35import numpy as np
36import numpy.typing as npt
38from ...definitions import get_logger
39from ...exceptions import SignalError
42class SignalGenerator:
43 """Generates arrays for the horizontal axis and vertical axis of :class:`.Signal` derived classes."""
45 ## Private API #####################################################################################################
46 def __init__(self, hinit: float, hstep: float, vhigh: float, vlow: float, vinit: float) -> None:
47 """Class constructor.
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
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.")
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]
77 ## Protected API ###################################################################################################
78 def _vvalue(self, percentage: float) -> np.float_:
79 """Calculates a value for the vertical axis from a percentage.
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)
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.
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.")
100 # Get the initial for the beginning of the section.
101 hval = self.__hv[-1]
102 vval = self.__vv[-1]
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]
108 # Generate samples.
109 self.__hv = np.concatenate([self.__hv, hsect[1:]])
110 self.__vv = np.concatenate([self.__vv, vsect])
112 return self
114 def add_edge(self, edge_type: Literal["rising", "falling"], vtarget: float, values: int) -> "SignalGenerator":
115 """Adds a rising or falling edge.
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.")
132 # Update target value to absolute number.
133 vtarget_val = self._vvalue(vtarget)
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.")
142 # Get the initial for the beginning of the section.
143 hval = self.__hv[-1]
144 vval = self.__vv[-1]
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)
150 # Generate values.
151 self.__hv = np.concatenate([self.__hv, hsect[1:]])
152 self.__vv = np.concatenate([self.__vv, vsect[1:]])
154 return self
156 def repeat(self, count: int = 1) -> "SignalGenerator":
157 """Repeats the current pattern the specified number of times.
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.")
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)
171 # Generate the vertical section.
172 vsect = np.tile(self.__vv, count)
174 # Generate values.
175 self.__hv = np.concatenate([self.__hv, hsect[1:]])
176 self.__vv = np.concatenate([self.__vv, vsect])
178 return self
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.
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)
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)
203 return (hvalues, vvalues)
205 @property
206 def count(self) -> int:
207 """Number of values in the signal.
209 :return: Number of values currently in the signal."""
210 return len(self.__hv)