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.
- pathsim/__init__.py +3 -0
- pathsim/blocks/__init__.py +14 -0
- pathsim/blocks/_block.py +209 -0
- pathsim/blocks/adder.py +30 -0
- pathsim/blocks/amplifier.py +34 -0
- pathsim/blocks/delay.py +70 -0
- pathsim/blocks/differentiator.py +70 -0
- pathsim/blocks/function.py +82 -0
- pathsim/blocks/integrator.py +66 -0
- pathsim/blocks/lti.py +155 -0
- pathsim/blocks/multiplier.py +30 -0
- pathsim/blocks/ode.py +86 -0
- pathsim/blocks/rf/__init__.py +4 -0
- pathsim/blocks/rf/filters.py +169 -0
- pathsim/blocks/rf/noise.py +218 -0
- pathsim/blocks/rf/sources.py +163 -0
- pathsim/blocks/rf/wienerhammerstein.py +338 -0
- pathsim/blocks/rng.py +57 -0
- pathsim/blocks/scope.py +224 -0
- pathsim/blocks/sources.py +71 -0
- pathsim/blocks/spectrum.py +316 -0
- pathsim/connection.py +112 -0
- pathsim/simulation.py +652 -0
- pathsim/solvers/__init__.py +25 -0
- pathsim/solvers/_solver.py +403 -0
- pathsim/solvers/bdf.py +240 -0
- pathsim/solvers/dirk2.py +101 -0
- pathsim/solvers/dirk3.py +86 -0
- pathsim/solvers/esdirk32.py +131 -0
- pathsim/solvers/esdirk4.py +99 -0
- pathsim/solvers/esdirk43.py +139 -0
- pathsim/solvers/esdirk54.py +141 -0
- pathsim/solvers/esdirk85.py +200 -0
- pathsim/solvers/euler.py +81 -0
- pathsim/solvers/rk4.py +61 -0
- pathsim/solvers/rkbs32.py +101 -0
- pathsim/solvers/rkck54.py +108 -0
- pathsim/solvers/rkdp54.py +111 -0
- pathsim/solvers/rkdp87.py +116 -0
- pathsim/solvers/rkf45.py +102 -0
- pathsim/solvers/rkf78.py +111 -0
- pathsim/solvers/rkv65.py +103 -0
- pathsim/solvers/ssprk22.py +62 -0
- pathsim/solvers/ssprk33.py +65 -0
- pathsim/solvers/ssprk34.py +74 -0
- pathsim/subsystem.py +267 -0
- pathsim/utils/__init__.py +0 -0
- pathsim/utils/adaptivebuffer.py +87 -0
- pathsim/utils/anderson.py +180 -0
- pathsim/utils/funcs.py +205 -0
- pathsim/utils/gilbert.py +110 -0
- pathsim/utils/progresstracker.py +90 -0
- pathsim/utils/realtimeplotter.py +230 -0
- pathsim/utils/statespacerealizations.py +116 -0
- pathsim/utils/waveforms.py +36 -0
- pathsim-0.2.0.dist-info/LICENSE.txt +21 -0
- pathsim-0.2.0.dist-info/METADATA +149 -0
- pathsim-0.2.0.dist-info/RECORD +109 -0
- pathsim-0.2.0.dist-info/WHEEL +5 -0
- pathsim-0.2.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/blocks/__init__.py +0 -0
- tests/blocks/test_adder.py +85 -0
- tests/blocks/test_amplifier.py +66 -0
- tests/blocks/test_block.py +138 -0
- tests/blocks/test_delay.py +122 -0
- tests/blocks/test_differentiator.py +102 -0
- tests/blocks/test_function.py +165 -0
- tests/blocks/test_integrator.py +92 -0
- tests/blocks/test_lti.py +162 -0
- tests/blocks/test_multiplier.py +87 -0
- tests/blocks/test_ode.py +125 -0
- tests/blocks/test_rng.py +109 -0
- tests/blocks/test_scope.py +196 -0
- tests/blocks/test_sources.py +119 -0
- tests/blocks/test_spectrum.py +119 -0
- tests/solvers/__init__.py +0 -0
- tests/solvers/test_bdf.py +364 -0
- tests/solvers/test_dirk2.py +138 -0
- tests/solvers/test_dirk3.py +137 -0
- tests/solvers/test_esdirk32.py +158 -0
- tests/solvers/test_esdirk4.py +138 -0
- tests/solvers/test_esdirk43.py +158 -0
- tests/solvers/test_esdirk54.py +160 -0
- tests/solvers/test_esdirk85.py +157 -0
- tests/solvers/test_euler.py +223 -0
- tests/solvers/test_rk4.py +138 -0
- tests/solvers/test_rkbs32.py +159 -0
- tests/solvers/test_rkck54.py +157 -0
- tests/solvers/test_rkdp54.py +159 -0
- tests/solvers/test_rkdp87.py +157 -0
- tests/solvers/test_rkf45.py +159 -0
- tests/solvers/test_rkf78.py +160 -0
- tests/solvers/test_rkv65.py +160 -0
- tests/solvers/test_solver.py +119 -0
- tests/solvers/test_ssprk22.py +136 -0
- tests/solvers/test_ssprk33.py +136 -0
- tests/solvers/test_ssprk34.py +136 -0
- tests/test_connection.py +176 -0
- tests/test_simulation.py +271 -0
- tests/test_subsystem.py +182 -0
- tests/utils/__init__.py +0 -0
- tests/utils/test_adaptivebuffer.py +111 -0
- tests/utils/test_anderson.py +142 -0
- tests/utils/test_funcs.py +143 -0
- tests/utils/test_gilbert.py +108 -0
- tests/utils/test_progresstracker.py +144 -0
- tests/utils/test_realtimeplotter.py +122 -0
- tests/utils/test_statespacerealizations.py +107 -0
pathsim/blocks/scope.py
ADDED
|
@@ -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
|