pathsim 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. pathsim/__init__.py +3 -0
  2. pathsim/blocks/__init__.py +14 -0
  3. pathsim/blocks/_block.py +209 -0
  4. pathsim/blocks/adder.py +30 -0
  5. pathsim/blocks/amplifier.py +34 -0
  6. pathsim/blocks/delay.py +70 -0
  7. pathsim/blocks/differentiator.py +70 -0
  8. pathsim/blocks/function.py +82 -0
  9. pathsim/blocks/integrator.py +66 -0
  10. pathsim/blocks/lti.py +155 -0
  11. pathsim/blocks/multiplier.py +30 -0
  12. pathsim/blocks/ode.py +86 -0
  13. pathsim/blocks/rf/__init__.py +4 -0
  14. pathsim/blocks/rf/filters.py +169 -0
  15. pathsim/blocks/rf/noise.py +218 -0
  16. pathsim/blocks/rf/sources.py +163 -0
  17. pathsim/blocks/rf/wienerhammerstein.py +338 -0
  18. pathsim/blocks/rng.py +57 -0
  19. pathsim/blocks/scope.py +224 -0
  20. pathsim/blocks/sources.py +71 -0
  21. pathsim/blocks/spectrum.py +316 -0
  22. pathsim/connection.py +112 -0
  23. pathsim/simulation.py +652 -0
  24. pathsim/solvers/__init__.py +25 -0
  25. pathsim/solvers/_solver.py +403 -0
  26. pathsim/solvers/bdf.py +240 -0
  27. pathsim/solvers/dirk2.py +101 -0
  28. pathsim/solvers/dirk3.py +86 -0
  29. pathsim/solvers/esdirk32.py +131 -0
  30. pathsim/solvers/esdirk4.py +99 -0
  31. pathsim/solvers/esdirk43.py +139 -0
  32. pathsim/solvers/esdirk54.py +141 -0
  33. pathsim/solvers/esdirk85.py +200 -0
  34. pathsim/solvers/euler.py +81 -0
  35. pathsim/solvers/rk4.py +61 -0
  36. pathsim/solvers/rkbs32.py +101 -0
  37. pathsim/solvers/rkck54.py +108 -0
  38. pathsim/solvers/rkdp54.py +111 -0
  39. pathsim/solvers/rkdp87.py +116 -0
  40. pathsim/solvers/rkf45.py +102 -0
  41. pathsim/solvers/rkf78.py +111 -0
  42. pathsim/solvers/rkv65.py +103 -0
  43. pathsim/solvers/ssprk22.py +62 -0
  44. pathsim/solvers/ssprk33.py +65 -0
  45. pathsim/solvers/ssprk34.py +74 -0
  46. pathsim/subsystem.py +267 -0
  47. pathsim/utils/__init__.py +0 -0
  48. pathsim/utils/adaptivebuffer.py +87 -0
  49. pathsim/utils/anderson.py +180 -0
  50. pathsim/utils/funcs.py +205 -0
  51. pathsim/utils/gilbert.py +110 -0
  52. pathsim/utils/progresstracker.py +90 -0
  53. pathsim/utils/realtimeplotter.py +230 -0
  54. pathsim/utils/statespacerealizations.py +116 -0
  55. pathsim/utils/waveforms.py +36 -0
  56. pathsim-0.2.0.dist-info/LICENSE.txt +21 -0
  57. pathsim-0.2.0.dist-info/METADATA +149 -0
  58. pathsim-0.2.0.dist-info/RECORD +109 -0
  59. pathsim-0.2.0.dist-info/WHEEL +5 -0
  60. pathsim-0.2.0.dist-info/top_level.txt +2 -0
  61. tests/__init__.py +0 -0
  62. tests/blocks/__init__.py +0 -0
  63. tests/blocks/test_adder.py +85 -0
  64. tests/blocks/test_amplifier.py +66 -0
  65. tests/blocks/test_block.py +138 -0
  66. tests/blocks/test_delay.py +122 -0
  67. tests/blocks/test_differentiator.py +102 -0
  68. tests/blocks/test_function.py +165 -0
  69. tests/blocks/test_integrator.py +92 -0
  70. tests/blocks/test_lti.py +162 -0
  71. tests/blocks/test_multiplier.py +87 -0
  72. tests/blocks/test_ode.py +125 -0
  73. tests/blocks/test_rng.py +109 -0
  74. tests/blocks/test_scope.py +196 -0
  75. tests/blocks/test_sources.py +119 -0
  76. tests/blocks/test_spectrum.py +119 -0
  77. tests/solvers/__init__.py +0 -0
  78. tests/solvers/test_bdf.py +364 -0
  79. tests/solvers/test_dirk2.py +138 -0
  80. tests/solvers/test_dirk3.py +137 -0
  81. tests/solvers/test_esdirk32.py +158 -0
  82. tests/solvers/test_esdirk4.py +138 -0
  83. tests/solvers/test_esdirk43.py +158 -0
  84. tests/solvers/test_esdirk54.py +160 -0
  85. tests/solvers/test_esdirk85.py +157 -0
  86. tests/solvers/test_euler.py +223 -0
  87. tests/solvers/test_rk4.py +138 -0
  88. tests/solvers/test_rkbs32.py +159 -0
  89. tests/solvers/test_rkck54.py +157 -0
  90. tests/solvers/test_rkdp54.py +159 -0
  91. tests/solvers/test_rkdp87.py +157 -0
  92. tests/solvers/test_rkf45.py +159 -0
  93. tests/solvers/test_rkf78.py +160 -0
  94. tests/solvers/test_rkv65.py +160 -0
  95. tests/solvers/test_solver.py +119 -0
  96. tests/solvers/test_ssprk22.py +136 -0
  97. tests/solvers/test_ssprk33.py +136 -0
  98. tests/solvers/test_ssprk34.py +136 -0
  99. tests/test_connection.py +176 -0
  100. tests/test_simulation.py +271 -0
  101. tests/test_subsystem.py +182 -0
  102. tests/utils/__init__.py +0 -0
  103. tests/utils/test_adaptivebuffer.py +111 -0
  104. tests/utils/test_anderson.py +142 -0
  105. tests/utils/test_funcs.py +143 -0
  106. tests/utils/test_gilbert.py +108 -0
  107. tests/utils/test_progresstracker.py +144 -0
  108. tests/utils/test_realtimeplotter.py +122 -0
  109. tests/utils/test_statespacerealizations.py +107 -0
@@ -0,0 +1,224 @@
1
+ #########################################################################################
2
+ ##
3
+ ## SCOPE BLOCK (blocks/scope.py)
4
+ ##
5
+ ## This module defines a block for recording time domain data
6
+ ##
7
+ ## Milan Rother 2024
8
+ ##
9
+ #########################################################################################
10
+
11
+ # IMPORTS ===============================================================================
12
+
13
+ import csv
14
+
15
+ import numpy as np
16
+ import matplotlib.pyplot as plt
17
+
18
+ from ._block import Block
19
+ from ..utils.funcs import dict_to_array
20
+ from ..utils.realtimeplotter import RealtimePlotter
21
+
22
+
23
+
24
+ # BLOCKS FOR DATA RECORDING =============================================================
25
+
26
+ class Scope(Block):
27
+ """
28
+ Block for recording time domain data with variable sampling sampling rate.
29
+
30
+ A time threshold can be set by 'wait' to start recording data after the simulation
31
+ time is larger then the specified waiting time, i.e. 't - t_wait > 0'.
32
+ This is useful for recording data only after all the transients have settled.
33
+
34
+ INPUTS :
35
+ sampling_rate : (int or None) number of samples per second, default is every timestep
36
+ t_wait : (float) wait time before starting recording
37
+ labels : (list of strings) labels for the scope traces, and for the csv
38
+ """
39
+
40
+ def __init__(self, sampling_rate=None, t_wait=0.0, labels=[]):
41
+ super().__init__()
42
+
43
+ #time delay until start recording
44
+ self.t_wait = t_wait
45
+
46
+ #params for sampling
47
+ self.sampling_rate = sampling_rate
48
+
49
+ #labels for plotting and saving data
50
+ self.labels = labels
51
+
52
+ #set recording data and time
53
+ self.recording = {}
54
+
55
+
56
+ def __len__(self):
57
+ return 0
58
+
59
+
60
+ def reset(self):
61
+ #reset inputs
62
+ self.inputs = {k:0.0 for k in sorted(self.inputs.keys())}
63
+
64
+ #reset recording data and time
65
+ self.recording = {}
66
+
67
+
68
+ def read(self):
69
+ """
70
+ return the recorded time domain data and the
71
+ corresponding time for all input ports
72
+ """
73
+
74
+ #just return 'None' if no recording available
75
+ if not self.recording: return None, None
76
+
77
+ #reformat the data from the recording dict
78
+ time = np.array(list(self.recording.keys()))
79
+ data = np.array(list(self.recording.values())).T
80
+ return time, data
81
+
82
+
83
+ def sample(self, t):
84
+ """
85
+ Sample the data from all inputs, and overwrites existing timepoints,
86
+ since we use a dict for storing the recorded data.
87
+ """
88
+ if t >= self.t_wait:
89
+ if (self.sampling_rate is None or
90
+ t * self.sampling_rate > len(self.recording)):
91
+ self.recording[t] = dict_to_array(self.inputs)
92
+
93
+
94
+ def plot(self, *args, **kwargs):
95
+ """
96
+ Directly create a plot of the recorded data for quick visualization and debugging.
97
+ The 'fig' and 'ax' objects are accessible as attributes of the 'Scope' instance
98
+ from the outside for saving, or modification, etc.
99
+ """
100
+
101
+ #just return 'None' if no recording available
102
+ if not self.recording:
103
+ return None
104
+
105
+ #get data
106
+ time, data = self.read()
107
+
108
+ #initialize figure
109
+ self.fig, self.ax = plt.subplots(nrows=1, ncols=1, figsize=(8,4), tight_layout=True, dpi=120)
110
+
111
+ #custom colors
112
+ self.ax.set_prop_cycle(color=["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00"])
113
+
114
+ #plot the recorded data
115
+ for p, d in enumerate(data):
116
+ lb = self.labels[p] if p < len(self.labels) else f"port {p}"
117
+ self.ax.plot(time, d, *args, **kwargs, label=lb)
118
+
119
+ #legend labels from ports
120
+ self.ax.legend(fancybox=False)
121
+
122
+ #other plot settings
123
+ self.ax.set_xlabel("time [s]")
124
+ self.ax.grid()
125
+
126
+ # Legend picking functionality
127
+ lines = self.ax.get_lines() # Get the lines from the plot
128
+ leg = self.ax.get_legend() # Get the legend
129
+
130
+ # Map legend lines to original plot lines
131
+ lined = dict()
132
+ for legline, origline in zip(leg.get_lines(), lines):
133
+ # Enable picking within 5 points tolerance
134
+ legline.set_picker(5)
135
+ lined[legline] = origline
136
+
137
+ def on_pick(event):
138
+ legline = event.artist
139
+ origline = lined[legline]
140
+ visible = not origline.get_visible()
141
+ origline.set_visible(visible)
142
+ legline.set_alpha(1.0 if visible else 0.2)
143
+ # Redraw the figure
144
+ self.fig.canvas.draw()
145
+
146
+ #enable picking
147
+ self.fig.canvas.mpl_connect("pick_event", on_pick)
148
+
149
+ #show the plot without blocking following code
150
+ plt.show(block=False)
151
+
152
+
153
+ def save(self, path="scope.csv"):
154
+ """
155
+ Save the recording of the scope to a csv file.
156
+ """
157
+
158
+ #check path ending
159
+ if not path.lower().endswith(".csv"):
160
+ path += ".csv"
161
+
162
+ #get data
163
+ time, data = self.read()
164
+
165
+ #number of ports and labels
166
+ P, L = len(data), len(self.labels)
167
+
168
+ #make csv header
169
+ header = ["time [s]", *[self.labels[p] if p < L else f"port {p}" for p in range(P)]]
170
+
171
+ #write to csv file
172
+ with open(path, "w", newline="") as file:
173
+ wrt = csv.writer(file)
174
+
175
+ #write the header to csv file
176
+ wrt.writerow(header)
177
+
178
+ #write each sample to the csv file
179
+ for sample in zip(time, *data):
180
+ wrt.writerow(sample)
181
+
182
+
183
+
184
+
185
+ class RealtimeScope(Scope):
186
+ """
187
+ An extension of the 'Scope' block that also initializes a realtime plotter
188
+ that creates an interactive plotting window while the simulation is running.
189
+
190
+ Otherwise implements the same functionality as the regular 'Scope' block.
191
+
192
+ NOTE :
193
+ Due to the plotting being relatively expensive, including this block
194
+ slows down the simulation significantly but may still be valuable for
195
+ debugging and testing.
196
+
197
+ INPUTS :
198
+ sampling_rate : (int or None) number of samples per second, default is every timestep
199
+ t_wait : (float) wait time before starting recording
200
+ labels : (list of strings) labels for the scope traces, and for the csv
201
+ max_samples : (int or None) number of samples for realtime display, all per default
202
+ """
203
+
204
+ def __init__(self, sampling_rate=None, t_wait=0.0, labels=[], max_samples=None):
205
+ super().__init__(sampling_rate, t_wait, labels)
206
+
207
+ #initialize realtime plotter
208
+ self.plotter = RealtimePlotter(max_samples=max_samples,
209
+ update_interval=0.1,
210
+ labels=labels,
211
+ x_label="time [s]",
212
+ y_label="")
213
+
214
+
215
+ def sample(self, t):
216
+ """
217
+ Sample the data from all inputs, and overwrites existing timepoints,
218
+ since we use a dict for storing the recorded data.
219
+ """
220
+ if (self.sampling_rate is None or t * self.sampling_rate > len(self.recording)):
221
+ values = dict_to_array(self.inputs)
222
+ self.plotter.update(t, values)
223
+ if t >= self.t_wait:
224
+ self.recording[t] = values
@@ -0,0 +1,71 @@
1
+ #########################################################################################
2
+ ##
3
+ ## SOURCE BLOCKS (blocks/sources.py)
4
+ ##
5
+ ## This module defines blocks that serve purely as inputs / sources
6
+ ## for the simulation such as the generic 'Source' block
7
+ ##
8
+ ## Milan Rother 2024
9
+ ##
10
+ #########################################################################################
11
+
12
+ # IMPORTS ===============================================================================
13
+
14
+ import numpy as np
15
+
16
+ from ._block import Block
17
+
18
+ from ..utils.funcs import (
19
+ dict_to_array,
20
+ array_to_dict
21
+ )
22
+
23
+
24
+
25
+ # INPUT BLOCKS ==========================================================================
26
+
27
+ class Constant(Block):
28
+ """
29
+ produces a constant output signal (SISO)
30
+ (same as 'Source' with func=lambda t:value,
31
+ therefore one could argue that it is redundant)
32
+
33
+ INPUTS :
34
+ value : (float) constant defining block output
35
+ """
36
+
37
+ def __init__(self, value=1):
38
+ super().__init__()
39
+ self.value = value
40
+
41
+ #set output with value (DC)
42
+ self.outputs[0] = self.value
43
+
44
+
45
+ def reset(self):
46
+ pass
47
+
48
+
49
+ class Source(Block):
50
+ """
51
+ Generator, or source that produces an arbitrary time
52
+ dependent output, defined by the func (callable).
53
+
54
+ INPUTS :
55
+ func : (callable) function defining time dependent block output
56
+ """
57
+
58
+ def __init__(self, func=lambda t: 1):
59
+ super().__init__()
60
+
61
+ if not callable(func):
62
+ raise ValueError(f"'{func}' is not callable")
63
+
64
+ self.func = func
65
+
66
+
67
+ def update(self, t):
68
+ #set output with internal function definition at time (t)
69
+ self.outputs[0] = self.func(t)
70
+ return 0.0
71
+
@@ -0,0 +1,316 @@
1
+ #########################################################################################
2
+ ##
3
+ ## SPECTRUM ANALYZER BLOCK (blocks/spectrum.py)
4
+ ##
5
+ ## Milan Rother 2024
6
+ ##
7
+ #########################################################################################
8
+
9
+ # IMPORTS ===============================================================================
10
+
11
+ import csv
12
+
13
+ import numpy as np
14
+ import matplotlib.pyplot as plt
15
+
16
+ from ._block import Block
17
+ from ..utils.funcs import dict_to_array
18
+ from ..utils.realtimeplotter import RealtimePlotter
19
+
20
+
21
+
22
+ # BLOCKS FOR DATA RECORDING =============================================================
23
+
24
+ class Spectrum(Block):
25
+ """
26
+ Block for fourier spectrum analysis (basically a spectrum analyzer), computes
27
+ continuous time running fourier transform (RFT) of the incoming signal.
28
+
29
+ A time threshold can be set by 't_wait' to start recording data only after the
30
+ simulation time is larger then the specified waiting time, i.e. 't - t_wait > dt'.
31
+ This is useful for recording the steady state after all the transients have settled.
32
+
33
+ An exponential forgetting factor 'alpha' can be specified for realtime spectral
34
+ analysis. It biases the spectral components exponentially to the most recent signal
35
+ values by applying a single sided exponential window like this:
36
+
37
+ int_0^t x(tau) * exp(alpha*(t-tau)) * exp(-j*omega*tau) dtau
38
+
39
+ It is also known as the 'exponentially forgetting transform' (EFT) and a form of
40
+ short time fourier transform (STFT). It is implemented as a 1st order statespace model
41
+
42
+ dx/dt = - alpha * x + exp(-j*omega*t) * u
43
+
44
+ , where 'u' is the input signal and 'x' is the state variable that represents the
45
+ complex fourier coefficient to the frequency 'omega'. The ODE is integrated using the
46
+ numerical integration engine of the block.
47
+
48
+ NOTE :
49
+ This block is very slow! But it is valuable for long running simulations
50
+ with few evaluation frequencies, where just FFT'ing the time series data
51
+ wouldnt be efficient OR if only the evaluation at weirdly spaced frequencies
52
+ is required. Otherwise its more efficient to just do an FFT on the time
53
+ series recording.
54
+
55
+ INPUTS :
56
+ freq : (list or array) list of evaluation frequencies for RFT
57
+ t_wait : (float) t_wait time before starting RFT
58
+ alpha : (float) exponential forgetting factor for realtime spectrum
59
+ labels : (list of strings) labels for the inputs
60
+ """
61
+
62
+ def __init__(self, freq=[], t_wait=0.0, alpha=0.0, labels=[]):
63
+ super().__init__()
64
+
65
+ #time delay until start recording
66
+ self.t_wait = t_wait
67
+
68
+ #local integration time
69
+ self.time = 0.0
70
+
71
+ #forgetting factor
72
+ self.alpha = alpha
73
+
74
+ #labels for plotting and saving data
75
+ self.labels = labels
76
+
77
+ #frequency
78
+ self.freq = np.array(freq)
79
+ self.omega = 2.0 * np.pi * self.freq
80
+
81
+
82
+ def __len__(self):
83
+ return 0
84
+
85
+
86
+ def set_solver(self, Solver, tolerance_lte=1e-6):
87
+
88
+ if self.engine is None:
89
+
90
+ #initialize the numerical integration engine with kernel
91
+ def _f(x, u, t):
92
+ return np.kron(u, np.exp(-1j * self.omega * t))
93
+
94
+ def _f_decay(x, u, t):
95
+ return np.kron(u, np.exp(-1j * self.omega * t)) - self.alpha * x
96
+
97
+ #initialize depending on forgetting factor
98
+ if self.alpha == 0.0: self.engine = Solver(0.0, _f, None, tolerance_lte)
99
+ else: self.engine = Solver(0.0, _f_decay, None, tolerance_lte)
100
+
101
+ else:
102
+
103
+ #change solver if already initialized
104
+ self.engine = self.engine.change(Solver, tolerance_lte)
105
+
106
+
107
+ def reset(self):
108
+ #reset inputs
109
+ self.inputs = {k:0.0 for k in sorted(self.inputs.keys())}
110
+
111
+ #local integration time
112
+ self.time = 0.0
113
+
114
+ #reset numeric integration engine -> resets the spectrum
115
+ self.engine.reset()
116
+
117
+
118
+ def read(self):
119
+
120
+ #just return 'None' if no engine initialized
121
+ if self.engine is None:
122
+ return self.freq, np.zeros_like(self.freq)
123
+
124
+ #get state from engine
125
+ state = self.engine.get()
126
+
127
+ #catch case where state has not been updated
128
+ if np.all(state == self.engine.initial_value):
129
+ return self.freq, np.zeros_like(self.freq)
130
+
131
+ #reshape state into spectra
132
+ spec = np.reshape(state, (-1, len(self.freq)))
133
+
134
+ #rescale spectrum and return it
135
+ if self.alpha != 0.0:
136
+ return self.freq, spec * self.alpha / (1.0 - np.exp(-self.alpha*self.time))
137
+
138
+ #return spectrum from RFT
139
+ return self.freq, spec/self.time
140
+
141
+
142
+ def solve(self, t, dt):
143
+ #effective time for integration
144
+ _t = t - self.t_wait
145
+ if _t > dt:
146
+
147
+ #update local integtration time
148
+ self.time = _t
149
+
150
+ #advance solution of implicit update equation
151
+ return self.engine.solve(dict_to_array(self.inputs), _t, dt)
152
+
153
+ #no error
154
+ return 0.0
155
+
156
+
157
+ def step(self, t, dt):
158
+ #effective time for integration
159
+ _t = t - self.t_wait
160
+ if _t > dt:
161
+
162
+ #update local integtration time
163
+ self.time = _t
164
+
165
+ #compute update step with integration engine
166
+ return self.engine.step(dict_to_array(self.inputs), _t, dt)
167
+
168
+ #no error estimate
169
+ return True, 0.0, 1.0
170
+
171
+
172
+ def plot(self, *args, **kwargs):
173
+ """
174
+ Directly create a plot of the recorded data for visualization.
175
+ The 'fig' and 'ax' objects are accessible as attributes of the 'Spectrum' instance
176
+ from the outside for saving, or modification, etc.
177
+ """
178
+
179
+ #just return 'None' if no engine initialized
180
+ if self.engine is None:
181
+ return None
182
+
183
+ #get data
184
+ freq, data = self.read()
185
+
186
+ #initialize figure
187
+ self.fig, self.ax = plt.subplots(nrows=1, ncols=1, figsize=(8,4), tight_layout=True, dpi=120)
188
+
189
+ #custom colors
190
+ self.ax.set_prop_cycle(color=["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00"])
191
+
192
+ #plot magnitude in dB and add label
193
+ for p, d in enumerate(data):
194
+ lb = self.labels[p] if p < len(self.labels) else f"port {p}"
195
+ self.ax.plot(freq, abs(d), *args, **kwargs, label=lb)
196
+
197
+ #legend labels from ports
198
+ self.ax.legend(fancybox=False)
199
+
200
+ #other plot settings
201
+ self.ax.set_xlabel("freq [Hz]")
202
+ self.ax.set_ylabel("magnitude")
203
+ self.ax.grid()
204
+
205
+ # Legend picking functionality
206
+ lines = self.ax.get_lines() # Get the lines from the plot
207
+ leg = self.ax.get_legend() # Get the legend
208
+
209
+ # Map legend lines to original plot lines
210
+ lined = dict()
211
+ for legline, origline in zip(leg.get_lines(), lines):
212
+ # Enable picking within 5 points tolerance
213
+ legline.set_picker(5)
214
+ lined[legline] = origline
215
+
216
+ def on_pick(event):
217
+ legline = event.artist
218
+ origline = lined[legline]
219
+ visible = not origline.get_visible()
220
+ origline.set_visible(visible)
221
+ legline.set_alpha(1.0 if visible else 0.2)
222
+ # Redraw the figure
223
+ self.fig.canvas.draw()
224
+
225
+ #enable picking
226
+ self.fig.canvas.mpl_connect("pick_event", on_pick)
227
+
228
+ #show the plot without blocking following code
229
+ plt.show(block=False)
230
+
231
+
232
+ def save(self, path="spectrum.csv"):
233
+ """
234
+ save the recording of the spectrum to a csv file
235
+ """
236
+
237
+ #check path ending
238
+ if not path.lower().endswith(".csv"):
239
+ path += ".csv"
240
+
241
+ #get data
242
+ freq, data = self.read()
243
+
244
+ #number of ports and labels
245
+ P, L = len(data), len(self.labels)
246
+
247
+ #construct port labels
248
+ port_labels = [self.labels[p] if p < L else f"port {p}" for p in range(P)]
249
+
250
+ #make csv header
251
+ header = ["freq [Hz]"]
252
+ for l in port_labels:
253
+ header.extend([f"Re({l})", f"Im({l})"])
254
+
255
+ #write to csv file
256
+ with open(path, "w", newline="") as file:
257
+ wrt = csv.writer(file)
258
+
259
+ #write the header to csv file
260
+ wrt.writerow(header)
261
+
262
+ #write each sample to the csv file
263
+ for f, *dta in zip(freq, *data):
264
+ sample = [f]
265
+ for d in dta:
266
+ sample.extend([np.real(d), np.imag(d)])
267
+ wrt.writerow(sample)
268
+
269
+
270
+ class RealtimeSpectrum(Spectrum):
271
+
272
+ """
273
+ An extension of the 'Spectrum' block that also initializes a realtime plotter that
274
+ creates an interactive plotting window while the simulation is running.
275
+
276
+ Otherwise implements the same functionality as the regular 'Spectrum' block.
277
+
278
+ NOTE :
279
+ Due to the plotting being relatively expensive, including this block slows down
280
+ the simulation significantly but may still be valuable for debugging and testing.
281
+
282
+ INPUTS :
283
+ freq : (list or array) list of evaluation frequencies for RFT
284
+ t_wait : (float) t_wait time before starting RFT
285
+ alpha : (float) exponential forgetting factor for realtime spectrum
286
+ labels : (list of strings) labels for the inputs
287
+ """
288
+
289
+ def __init__(self, freq=[], t_wait=0.0, alpha=0.0, labels=[]):
290
+ super().__init__(freq, t_wait, alpha, labels)
291
+
292
+ #initialize realtime plotter
293
+ self.plotter = RealtimePlotter(update_interval=0.1,
294
+ labels=labels,
295
+ x_label="freq [Hz]",
296
+ y_label="magnitude")
297
+
298
+
299
+ def step(self, t, dt):
300
+ #effective time for integration
301
+ _t = t - self.t_wait
302
+ if _t > dt:
303
+
304
+ #update local integtration time
305
+ self.time = _t
306
+
307
+ if self.time > 2*dt:
308
+ #update realtime plotter
309
+ _, data = self.read()
310
+ self.plotter.update_all(self.freq, abs(data))
311
+
312
+ #compute update step with integration engine
313
+ return self.engine.step(dict_to_array(self.inputs), _t, dt)
314
+
315
+ #no error estimate
316
+ return True, 0.0, 1.0