Simulating a Control-Bounded ADC

This example shows how to simulate the interactions between an analog system and a digital control while the former is excited by an analog signal.

 8 import matplotlib.pyplot as plt
 9 import cbadc
10 import numpy as np

The Analog System

The chain of integrators ADC.

First, we have to decide on an analog system. For this tutorial, we will commit to a chain-of-integrators ADC, see cbadc.analog_system.ChainOfIntegrators, as our analog system.

26 # We fix the number of analog states.
27 N = 6
28 # Set the amplification factor.
29 beta = 6250.
30 rho = - 1e-2
31 kappa = - 1.0
32 # In this example, each nodes amplification and local feedback will be set
33 # identically.
34 betaVec = beta * np.ones(N)
35 rhoVec = betaVec * rho
36 kappaVec = kappa * beta * np.eye(N)
37
38 # Instantiate a chain-of-integrators analog system.
39 analog_system = cbadc.analog_system.ChainOfIntegrators(betaVec, rhoVec, kappaVec)
40 # print the analog system such that we can very it being correctly initalized.
41 print(analog_system)

Out:

The analog system is parameterized as:
A =
[[ -62.5    0.     0.     0.     0.     0. ]
 [6250.   -62.5    0.     0.     0.     0. ]
 [   0.  6250.   -62.5    0.     0.     0. ]
 [   0.     0.  6250.   -62.5    0.     0. ]
 [   0.     0.     0.  6250.   -62.5    0. ]
 [   0.     0.     0.     0.  6250.   -62.5]],
B =
[[6250.]
 [   0.]
 [   0.]
 [   0.]
 [   0.]
 [   0.]],
CT =
[[1. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0.]
 [0. 0. 1. 0. 0. 0.]
 [0. 0. 0. 1. 0. 0.]
 [0. 0. 0. 0. 1. 0.]
 [0. 0. 0. 0. 0. 1.]],
Gamma =
[[-6250.    -0.    -0.    -0.    -0.    -0.]
 [   -0. -6250.    -0.    -0.    -0.    -0.]
 [   -0.    -0. -6250.    -0.    -0.    -0.]
 [   -0.    -0.    -0. -6250.    -0.    -0.]
 [   -0.    -0.    -0.    -0. -6250.    -0.]
 [   -0.    -0.    -0.    -0.    -0. -6250.]],
Gamma_tildeT =
[[1. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0.]
 [0. 0. 1. 0. 0. 0.]
 [0. 0. 0. 1. 0. 0.]
 [0. 0. 0. 0. 1. 0.]
 [0. 0. 0. 0. 0. 1.]], and D=[[0.]
 [0.]
 [0.]
 [0.]
 [0.]
 [0.]]

The Digital Control

In addition to the analog system, our simulation will require us to specify a digital control. For this tutorial, we will use cbadc.digital_control.DigitalControl.

51 # Set the time period which determines how often the digital control updates.
52 T = 1.0/(2 * beta)
53 # Set the number of digital controls to be same as analog states.
54 M = N
55 # Initialize the digital control.
56 digital_control = cbadc.digital_control.DigitalControl(T, M)
57 # print the digital control to verify proper initialization.
58 print(digital_control)

Out:

The Digital Control is parameterized as:
T = 8e-05,
M = 6, and next update at
t = 8e-05

The Analog Signal

The final and third component of the simulation is an analog signal. For this tutorial, we will choose a cbadc.analog_signal.Sinusodial. Again, this is one of several possible choices.

70 # Set the peak amplitude.
71 amplitude = 0.5
72 # Choose the sinusodial frequency via an oversampling ratio (OSR).
73 OSR = 1 << 9
74 frequency = 1.0 / (T * OSR)
75
76 # We also specify a phase an offset these are hovewer optional.
77 phase = np.pi / 3
78 offset = 0.0
79
80 # Instantiate the analog signal
81 analog_signal = cbadc.analog_signal.Sinusodial(amplitude, frequency, phase, offset)
82 # print to ensure correct parametrization.
83 print(analog_signal)

Out:

Sinusodial parameterized as:
amplitude = 0.5,

        frequency = 24.414062499999996,
phase = 1.0471975511965976,
        and
offset = 0.0

Simulating

Next, we set up the simulator. Here we use the cbadc.simulator.StateSpaceSimulator for simulating the involved differential equations as outlined in cbadc.analog_system.AnalogSystem.

 95 # Simulate for 2^18 control cycles.
 96 end_time = T * (1 << 18)
 97
 98 # Instantiate the simulator.
 99 simulator = cbadc.simulator.StateSpaceSimulator(analog_system, digital_control, [
100                                 analog_signal], t_stop=end_time)
101 # Depending on your analog system the step above might take some time to
102 # compute as it involves precomputing solutions to initial value problems.
103
104 # Let's print the first 20 control decisions.
105 index = 0
106 for s in simulator:
107     if (index > 19):
108         break
109     print(f"step:{index} -> s:{np.array(s)}")
110     index += 1
111
112 # To verify the simulation parametrization we can
113 print(simulator)

Out:

step:0 -> s:[0 0 0 0 0 0]
step:1 -> s:[ True  True  True  True  True  True]
step:2 -> s:[False False False False False False]
step:3 -> s:[ True  True False False False False]
step:4 -> s:[ True False  True  True  True  True]
step:5 -> s:[ True  True  True False False False]
step:6 -> s:[False  True False  True  True False]
step:7 -> s:[ True False  True False False  True]
step:8 -> s:[ True  True False  True  True False]
step:9 -> s:[False False  True False False  True]
step:10 -> s:[ True  True False  True  True  True]
step:11 -> s:[ True  True  True  True  True False]
step:12 -> s:[ True  True  True False False  True]
step:13 -> s:[False False False  True  True False]
step:14 -> s:[ True  True  True False False False]
step:15 -> s:[ True  True False  True  True  True]
step:16 -> s:[ True False  True False False False]
step:17 -> s:[False  True False  True  True  True]
step:18 -> s:[ True False  True False False False]
step:19 -> s:[ True  True False  True  True  True]
t = 0.00168, (current simulator time)
Ts = 8e-05,
t_stop = 20.97152,
rtol = 1e-12,
atol = 1e-12, and
max_step = 0.0008

Tracking the Analog State Vector

Clearly the output type of the generator simulator above is the sequence of control signals s[k]. Sometimes we are interested in also monitoring the internal states of analog system during simulation.

To this end we use the cbadc.simulator.StateSpaceSimulator.state_vector() and an cbadc.simulator.extended_simulation_result().

Note that the cbadc.simulator.extended_simulation_result() is defined like this

def extended_simulation_result(simulator):
    for control_signal in simulator:
        analog_state = simulator.state_vector()
        yield {
            'control_signal': np.array(control_signal),
            'analog_state': np.array(analog_state)
        }

So, in essence, we are creating a new generator from the old with an extended output.

Note

The convenience function extended_simulation_result is one of many such convenience functions found in the cbadc.simulator module.

We can achieve this by appending yet another generator to the control signal stream as:

150 # Repeating the steps above we now get for the following
151 # ten control cycles.
152
153 ext_simulator = cbadc.simulator.extended_simulation_result(simulator)
154 for res in ext_simulator:
155     if (index > 29):
156         break
157     print(
158         f"step:{index} -> s:{res['control_signal']}, x:{res['analog_state']}")
159     index += 1

Out:

step:20 -> s:[False False False  True  True  True], x:[ 0.54823676  0.11670772  0.06484886 -0.46198389 -0.49102059 -0.40805816]
step:21 -> s:[ True  True  True False False False], x:[ 0.28852725 -0.17409673 -0.44326189 -0.04946166 -0.10665263 -0.06475268]
step:22 -> s:[ True False False False False False], x:[0.03084446 0.4050305  0.12088599 0.35734476 0.45783109 0.51372668]
step:23 -> s:[ True  True  True  True  True  True], x:[-0.22485823 -0.14425853 -0.30835322 -0.17870188  0.01002193  0.13999125]
step:24 -> s:[False False False False  True  True], x:[ 0.51887684  0.42881669  0.24768117  0.29413072 -0.47129112 -0.48446692]
step:25 -> s:[ True  True  True  True False False], x:[ 0.2620199   0.12253093 -0.10960284 -0.16543741  0.0691172  -0.07384136]
step:26 -> s:[ True  True False False  True False], x:[ 0.00702877 -0.30986756  0.34809793  0.40279176 -0.38008514  0.33569678]
step:27 -> s:[ True False  True  True False  True], x:[-0.24614276  0.13067341 -0.19163271 -0.06832894  0.21496668 -0.19582836]
step:28 -> s:[False  True False False  True False], x:[ 0.4999634  -0.30514404  0.24888015  0.45427751 -0.19748233  0.2971895 ]
step:29 -> s:[ True False  True  True False  True], x:[ 0.24531795  0.38086046 -0.2266544  -0.05565045  0.41130859 -0.13881065]

Saving to File

In general, simulating the analog system and digital control interaction is a computationally much more intense procedure compared to the digital estimation step. This is one reason, and there are more, why you would want to store the intermediate control signal sequence to a file.

For this purpose use the cbadc.utilities.control_signal_2_byte_stream() and cbadc.utilities.write_byte_stream_to_file() functions.

178 # Instantiate a new simulator and control.
179 simulator = cbadc.simulator.StateSpaceSimulator(analog_system, digital_control, [
180                                 analog_signal], t_stop=end_time)
181
182 # Construct byte stream.
183 byte_stream = cbadc.utilities.control_signal_2_byte_stream(simulator, M)
184
185
186 def print_next_10_bytes(stream):
187     global index
188     for byte in stream:
189         if (index < 40):
190             print(f"{index} -> {byte}")
191             index += 1
192         yield byte
193
194
195 cbadc.utilities.write_byte_stream_to_file("sinusodial_simulation.adcs",
196                           print_next_10_bytes(byte_stream))

Out:

30 -> b'\x13'
31 -> b'\x13'
32 -> b'\x13'
33 -> b'\x13'
34 -> b'\x13'
35 -> b'\x13'
36 -> b'\x13'
37 -> b'\x13'
38 -> b'\x13'
39 -> b'\x13'

Evaluating the Analog State Vector in Between Control Signal Samples

If we wish to simulate the analog state vector trajectory between control updates, this can be achieved using the Ts parameter of the cbadc.simulator.StateSpaceSimulator. Technically you can scale \(T_s = T / \alpha\) for any positive number \(\alpha\). For such a scaling, the simulator will generate \(\alpha\) more control signals per unit of time. However, digital control is still restricted to only update the control signals at multiples of \(T\).

211 # Set sampling time three orders of magnitude smaller than the control period
212 Ts = T / 1000.0
213
214 # Simulate for 10000 control cycles.
215 size = 15000
216 end_time = size * Ts
217
218 # Initialize a new digital control.
219 new_digital_control = cbadc.digital_control.DigitalControl(T, M)
220
221 # Instantiate a new simulator with a sampling time.
222 simulator = cbadc.simulator.StateSpaceSimulator(analog_system, new_digital_control, [
223                                 analog_signal], t_stop=end_time, Ts=Ts)
224
225 # Create data containers to hold the resulting data.
226 time_vector = np.arange(size) * Ts / T
227 states = np.zeros((N, size))
228 control_signals = np.zeros((M, size), dtype=np.int8)
229
230 # Iterate through and store states and control_signals.
231 for index, res in enumerate(cbadc.simulator.extended_simulation_result(simulator)):
232     states[:, index] = res['analog_state']
233     control_signals[:, index] = res['control_signal']
234
235 # Plot all analog state evolutions.
236 plt.figure()
237 plt.title("Analog state vectors")
238 for index in range(N):
239     plt.plot(time_vector, states[index, :], label=f"$x_{index + 1}(t)$")
240 plt.grid(b=True, which='major', color='gray', alpha=0.6, lw=1.5)
241 plt.xlabel('$t/T$')
242 plt.xlim((0, 10))
243 plt.legend()
244
245 # reset figure size and plot individual results.
246 plt.rcParams['figure.figsize'] = [6.40, 6.40 * 2]
247 fig, ax = plt.subplots(N, 2)
248 for index in range(N):
249     color = next(ax[0, 0]._get_lines.prop_cycler)['color']
250     ax[index, 0].grid(b=True, which='major', color='gray', alpha=0.6, lw=1.5)
251     ax[index, 1].grid(b=True, which='major', color='gray', alpha=0.6, lw=1.5)
252     ax[index, 0].plot(time_vector, states[index, :], color=color)
253     ax[index, 1].plot(time_vector, control_signals[index, :],
254                       '--', color=color)
255     ax[index, 0].set_ylabel(f"$x_{index + 1}(t)$")
256     ax[index, 1].set_ylabel(f"$s_{index + 1}(t)$")
257     ax[index, 0].set_xlim((0, 15))
258     ax[index, 1].set_xlim((0, 15))
259     ax[index, 0].set_ylim((-1, 1))
260 fig.suptitle("Analog state and control contribution evolution")
261 ax[-1, 0].set_xlabel("$t / T$")
262 ax[-1, 1].set_xlabel("$t / T$")
263 fig.tight_layout()
  • Analog state vectors
  • Analog state and control contribution evolution

Analog State Statistics

As in the previous section, visualizing the analog state trajectory is a good way of identifying problems and possible errors. Another way of making sure that the analog states remain bounded is to estimate their corresponding densities (assuming i.i.d samples).

274 # Compute L_2 norm of analog state vector.
275 L_2_norm = np.linalg.norm(states, ord=2, axis=0)
276 # Similarly, compute L_infty (largest absolute value) of the analog state
277 # vector.
278 L_infty_norm = np.linalg.norm(states, ord=np.inf, axis=0)
279
280 # Estimate and plot densities using matplotlib tools.
281 bins = 150
282 plt.rcParams['figure.figsize'] = [6.40, 4.80]
283 fig, ax = plt.subplots(2, sharex=True)
284 ax[0].grid(b=True, which='major', color='gray', alpha=0.6, lw=1.5)
285 ax[1].grid(b=True, which='major', color='gray', alpha=0.6, lw=1.5)
286 ax[0].hist(L_2_norm, bins=bins, density=True)
287 ax[1].hist(L_infty_norm, bins=bins, density=True, color="orange")
288 plt.suptitle("Estimated probability densities")
289 ax[0].set_xlabel("$\|\mathbf{x}(t)\|_2$")
290 ax[1].set_xlabel("$\|\mathbf{x}(t)\|_\infty$")
291 ax[0].set_ylabel("$p ( \| \mathbf{x}(t) \|_2 ) $")
292 ax[1].set_ylabel("$p ( \| \mathbf{x}(t) \|_\infty )$")
293 fig.tight_layout()
Estimated probability densities

Total running time of the script: ( 11 minutes 39.103 seconds)

Gallery generated by Sphinx-Gallery