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/utils/funcs.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## UTILITY FUNCTIONS
|
|
4
|
+
## (utils/funcs.py)
|
|
5
|
+
##
|
|
6
|
+
## Milan Rother 2023/24
|
|
7
|
+
##
|
|
8
|
+
########################################################################################
|
|
9
|
+
|
|
10
|
+
# IMPORTS ==============================================================================
|
|
11
|
+
|
|
12
|
+
from time import perf_counter
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# HELPERS ==============================================================================
|
|
18
|
+
|
|
19
|
+
def timer(func):
|
|
20
|
+
"""
|
|
21
|
+
shows the execution time in milliseconds of the
|
|
22
|
+
function object passed for debugging purposes
|
|
23
|
+
"""
|
|
24
|
+
def wrap_func(*args, **kwargs):
|
|
25
|
+
t1 = perf_counter()
|
|
26
|
+
result = func(*args, **kwargs)
|
|
27
|
+
t2 = perf_counter()
|
|
28
|
+
print(f"Function '{func.__name__!r}' executed in {(t2 - t1)*1e3:.2f}ms")
|
|
29
|
+
return result
|
|
30
|
+
return wrap_func
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def dB(x):
|
|
34
|
+
"""
|
|
35
|
+
Compute clipped decibel value (for signals) where
|
|
36
|
+
the minimum value is '-360dB'.
|
|
37
|
+
"""
|
|
38
|
+
return 20.0*np.log10(np.clip(abs(x), 1e-18, None))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# HELPERS FOR SIMULATION ===============================================================
|
|
42
|
+
|
|
43
|
+
def dict_to_array(a):
|
|
44
|
+
return np.array([a[k] for k in sorted(a.keys())])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def array_to_dict(a):
|
|
48
|
+
if np.isscalar(a): return {0:a}
|
|
49
|
+
else: return dict(enumerate(a))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def rel_error(a, b):
|
|
53
|
+
"""
|
|
54
|
+
Computes the relative error between two scalars.
|
|
55
|
+
It is robust to one of them being zero and falls
|
|
56
|
+
back to the absolute error in this case.
|
|
57
|
+
|
|
58
|
+
NOTE :
|
|
59
|
+
this is actually faster then inlining the
|
|
60
|
+
branching into the return statement
|
|
61
|
+
"""
|
|
62
|
+
if a == 0.0: return abs(b)
|
|
63
|
+
else: return abs((a - b)/a)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def abs_error(a, b):
|
|
67
|
+
"""
|
|
68
|
+
Computes the absolute error between two scalars.
|
|
69
|
+
"""
|
|
70
|
+
return abs(a - b)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def max_error(a, b):
|
|
74
|
+
"""
|
|
75
|
+
Computes the maximum absolute error / deviation between two
|
|
76
|
+
iterables such as lists with numerical values. Returns a scalar
|
|
77
|
+
value representing the maximum deviation.
|
|
78
|
+
|
|
79
|
+
NOTE:
|
|
80
|
+
this is actually faster then 'max' over a list comprehension
|
|
81
|
+
"""
|
|
82
|
+
max_err = 0.0
|
|
83
|
+
for err in map(abs_error, a, b):
|
|
84
|
+
if err > max_err:
|
|
85
|
+
max_err = err
|
|
86
|
+
return max_err
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def max_rel_error(a, b):
|
|
90
|
+
"""
|
|
91
|
+
Computes the maximum relative error between two iterables
|
|
92
|
+
such as lists with numerical values. It is robust to one of
|
|
93
|
+
them being zero and falls back to the absolute error in this
|
|
94
|
+
case. It returns a scalar value representing the maximum
|
|
95
|
+
relative error.
|
|
96
|
+
|
|
97
|
+
NOTE:
|
|
98
|
+
this is actually faster then 'max' over a list comprehension
|
|
99
|
+
"""
|
|
100
|
+
max_err = 0.0
|
|
101
|
+
for err in map(rel_error, a, b):
|
|
102
|
+
if err > max_err:
|
|
103
|
+
max_err = err
|
|
104
|
+
return max_err
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def max_error_dicts(a, b):
|
|
108
|
+
"""
|
|
109
|
+
Computes the maximum absolute error between two dictionaries
|
|
110
|
+
with numerical values. It returns a scalar value representing
|
|
111
|
+
the maximum absolute error.
|
|
112
|
+
"""
|
|
113
|
+
return max_error(a.values(), b.values())
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def max_rel_error_dicts(a, b):
|
|
117
|
+
"""
|
|
118
|
+
Computes the maximum relative error between two dictionaries
|
|
119
|
+
with numerical values. It is robust to one of them being zero
|
|
120
|
+
and falls back to the absolute error in this case. It returns
|
|
121
|
+
a scalar value representing the maximum relative error.
|
|
122
|
+
"""
|
|
123
|
+
return max_rel_error(a.values(), b.values())
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# AUTOMATIC DIFFERENTIATION ============================================================
|
|
127
|
+
|
|
128
|
+
def numerical_jacobian(func, x, h=1e-8):
|
|
129
|
+
"""
|
|
130
|
+
Numerically computes the jacobian of the function 'func' by
|
|
131
|
+
central differences with the stepsize 'h' which is set to
|
|
132
|
+
a default value of 'h=1e-8' which is the point where the
|
|
133
|
+
truncation error of the central differences balances with
|
|
134
|
+
the machine accuracy of 64bit floating point numbers.
|
|
135
|
+
|
|
136
|
+
INPUTS :
|
|
137
|
+
func : (function object) function to compute jacobian for
|
|
138
|
+
x : (float or array) value for function at which the jacobian is evaluated
|
|
139
|
+
h : (float) step size for central differences
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
#catch scalar case (gradient)
|
|
143
|
+
if np.isscalar(x):
|
|
144
|
+
return 0.5 * (func(x+h) - func(x-h)) / h
|
|
145
|
+
|
|
146
|
+
#perturbation matrix and jacobian
|
|
147
|
+
e = np.eye(len(x)) * h
|
|
148
|
+
return 0.5 * np.array([func(x_p) - func(x_m) for x_p, x_m in zip(x+e, x-e)]).T / h
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def auto_jacobian(func):
|
|
152
|
+
"""
|
|
153
|
+
Wraps a function object such that it computes the jacobian
|
|
154
|
+
of the function with respect to the first argument.
|
|
155
|
+
|
|
156
|
+
This is intended to compute the jacobian 'jac(x, u, t)' of
|
|
157
|
+
the right hand side function 'func(x, u, t)' of numerical
|
|
158
|
+
integrators with respect to 'x'.
|
|
159
|
+
"""
|
|
160
|
+
def wrap_func(*args):
|
|
161
|
+
_x, *_args = args
|
|
162
|
+
return numerical_jacobian(lambda x: func(x, *_args), _x)
|
|
163
|
+
return wrap_func
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# PATH ESTIMATION ======================================================================
|
|
168
|
+
|
|
169
|
+
def path_length_dfs(connections, starting_block, visited=set()):
|
|
170
|
+
"""
|
|
171
|
+
Recursively compute the longest path (depth first search)
|
|
172
|
+
in a directed graph from a starting node / block.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
#node already visited -> break cycles
|
|
176
|
+
if starting_block in visited:
|
|
177
|
+
return 0
|
|
178
|
+
|
|
179
|
+
#block without instant time component -> break cycles
|
|
180
|
+
if not len(starting_block):
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
#add starting node to set of visited nodes
|
|
184
|
+
visited.add(starting_block)
|
|
185
|
+
|
|
186
|
+
#length of paths from the starting nodes
|
|
187
|
+
max_length = 0
|
|
188
|
+
|
|
189
|
+
#iterate connections and explore the path from the target node
|
|
190
|
+
for conn in connections:
|
|
191
|
+
|
|
192
|
+
#find connections from starting block
|
|
193
|
+
src, _ = conn.source
|
|
194
|
+
if src == starting_block:
|
|
195
|
+
|
|
196
|
+
#iterate connection target blocks
|
|
197
|
+
for trg, _ in conn.targets:
|
|
198
|
+
|
|
199
|
+
#recursively compute the new longest path
|
|
200
|
+
length = path_length_dfs(connections, trg, visited.copy())
|
|
201
|
+
if length > max_length: max_length = length
|
|
202
|
+
|
|
203
|
+
#add the contribution of the starting node to longest path
|
|
204
|
+
return max_length + len(starting_block)
|
|
205
|
+
|
pathsim/utils/gilbert.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## METHODS FOR STATESPACE REALIZATIONS
|
|
4
|
+
## (utils/gilbert.py)
|
|
5
|
+
##
|
|
6
|
+
## Milan Rother 2024
|
|
7
|
+
##
|
|
8
|
+
########################################################################################
|
|
9
|
+
|
|
10
|
+
# IMPORTS ==============================================================================
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# STATESPACE REALIZATION ===============================================================
|
|
16
|
+
|
|
17
|
+
def gilbert_realization(Poles=[], Residues=[], Const=0.0, tolerance=1e-9):
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
Build real valued statespace model from transfer function
|
|
21
|
+
in pole residue form by Gilberts method and an additional
|
|
22
|
+
similarity transformation to get fully real valued matrices.
|
|
23
|
+
|
|
24
|
+
pole residue form:
|
|
25
|
+
H(s) = Const + sum( Residues / (s - Poles) )
|
|
26
|
+
|
|
27
|
+
statespace form:
|
|
28
|
+
H(s) = C * (s*I - A)^-1 * B + D
|
|
29
|
+
|
|
30
|
+
NOTE :
|
|
31
|
+
The resulting system is identical to the so-called
|
|
32
|
+
'Modal Form' and is a minimal realization.
|
|
33
|
+
|
|
34
|
+
INPUTS :
|
|
35
|
+
Poles : (array) real and complex poles
|
|
36
|
+
Residues : (array) array of real and complex residue matrices
|
|
37
|
+
Const : (array) matrix for constant term
|
|
38
|
+
tolerance : (float) relative tolerance for checking real poles
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
#make arrays
|
|
42
|
+
Poles = np.atleast_1d(Poles)
|
|
43
|
+
Residues = np.atleast_1d(Residues)
|
|
44
|
+
|
|
45
|
+
#check validity of args
|
|
46
|
+
if not len(Poles) or not len(Residues):
|
|
47
|
+
raise ValueError("No 'Poles' and 'Residues' defined!")
|
|
48
|
+
|
|
49
|
+
if len(Poles) != len(Residues):
|
|
50
|
+
raise ValueError("Same number of 'Poles' and 'Residues' have to be given!")
|
|
51
|
+
|
|
52
|
+
#check shape of residues for MIMO, etc
|
|
53
|
+
if Residues.ndim == 1:
|
|
54
|
+
N, m, n = Residues.size, 1, 1
|
|
55
|
+
Residues = np.reshape(Residues, (N, m, n))
|
|
56
|
+
elif Residues.ndim == 2:
|
|
57
|
+
N, m, n = *Residues.shape, 1
|
|
58
|
+
Residues = np.reshape(Residues, (N, m, n))
|
|
59
|
+
elif Residues.ndim == 3:
|
|
60
|
+
N, m, n = Residues.shape
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(f"shape mismatch of 'Residues': Residues.shape={Residues.shape}")
|
|
63
|
+
|
|
64
|
+
#initialize companion matrix
|
|
65
|
+
a = np.zeros((N, N))
|
|
66
|
+
b = np.zeros(N)
|
|
67
|
+
|
|
68
|
+
#residues
|
|
69
|
+
C = np.ones((m, n*N))
|
|
70
|
+
|
|
71
|
+
#go through poles and handle conjugate pairs
|
|
72
|
+
_Poles, _Residues = [], []
|
|
73
|
+
for p, R in zip(Poles, Residues):
|
|
74
|
+
|
|
75
|
+
#real pole
|
|
76
|
+
if np.isreal(p) or abs(np.imag(p) / np.real(p)) < tolerance:
|
|
77
|
+
_Poles.append(p.real)
|
|
78
|
+
_Residues.append(R.real)
|
|
79
|
+
|
|
80
|
+
#complex conjugate pair
|
|
81
|
+
elif np.imag(p) > 0.0:
|
|
82
|
+
_Poles.extend([p, np.conj(p)])
|
|
83
|
+
_Residues.extend([R, np.conj(R)])
|
|
84
|
+
|
|
85
|
+
#build real companion matrix from the poles
|
|
86
|
+
p_old = 0.0
|
|
87
|
+
for k, (p, R) in enumerate(zip(_Poles, _Residues)):
|
|
88
|
+
|
|
89
|
+
#check if complex conjugate
|
|
90
|
+
is_cc = (p.imag != 0.0 and p == np.conj(p_old))
|
|
91
|
+
p_old = p
|
|
92
|
+
|
|
93
|
+
a[k,k] = np.real(p)
|
|
94
|
+
b[k] = 1.0
|
|
95
|
+
if is_cc:
|
|
96
|
+
a[k, k-1] = - np.imag(p)
|
|
97
|
+
a[k-1, k] = np.imag(p)
|
|
98
|
+
b[k] = 0.0
|
|
99
|
+
b[k-1] = 2.0
|
|
100
|
+
|
|
101
|
+
#iterate columns of residue
|
|
102
|
+
for i in range(n):
|
|
103
|
+
C[:,k+N*i] = np.imag(R[:,i]) if is_cc else np.real(R[:,i])
|
|
104
|
+
|
|
105
|
+
#build block diagonal
|
|
106
|
+
A = np.kron(np.eye(n, dtype=float), a)
|
|
107
|
+
B = np.kron(np.eye(n, dtype=float), b).T
|
|
108
|
+
D = Const
|
|
109
|
+
|
|
110
|
+
return A, B, C, D
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## PROGRESS TRACKER CLASS DEFINITION
|
|
4
|
+
## (utils/progresstracker.py)
|
|
5
|
+
##
|
|
6
|
+
## Milan Rother 2023/24
|
|
7
|
+
##
|
|
8
|
+
########################################################################################
|
|
9
|
+
|
|
10
|
+
# IMPORTS ==============================================================================
|
|
11
|
+
|
|
12
|
+
from time import perf_counter
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# HELPER CLASS =========================================================================
|
|
17
|
+
|
|
18
|
+
class ProgressTracker:
|
|
19
|
+
"""
|
|
20
|
+
Class that manages progress tracking by providing a generator
|
|
21
|
+
interface that runs until an external condition is satisfied.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, logger=None, log_interval=10):
|
|
25
|
+
|
|
26
|
+
#set logger
|
|
27
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
#generation condition
|
|
30
|
+
self.condition = True
|
|
31
|
+
|
|
32
|
+
#step counter
|
|
33
|
+
self.steps = 0
|
|
34
|
+
self.successful_steps = 0
|
|
35
|
+
|
|
36
|
+
#for progress display in percent
|
|
37
|
+
self.display_percentages = list(range(0, 101, log_interval))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def __iter__(self):
|
|
41
|
+
|
|
42
|
+
#starting progress tracker
|
|
43
|
+
if self.logger:
|
|
44
|
+
self.logger.info("STARTING progress tracker")
|
|
45
|
+
|
|
46
|
+
#computer time for performance estimate
|
|
47
|
+
starting_time = perf_counter()
|
|
48
|
+
|
|
49
|
+
#generate as long as 'self.condition' is 'True'
|
|
50
|
+
while self.condition:
|
|
51
|
+
yield
|
|
52
|
+
|
|
53
|
+
#compute tracker runtime
|
|
54
|
+
runtime = perf_counter() - starting_time
|
|
55
|
+
|
|
56
|
+
#log the runtime
|
|
57
|
+
if self.logger:
|
|
58
|
+
self.logger.info(f"FINISHED steps(total)={self.successful_steps}({self.steps}) runtime={runtime*1e3:.2f}ms")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def check(self, progress, success=False, msg=""):
|
|
62
|
+
"""
|
|
63
|
+
Update the progress of the generator.
|
|
64
|
+
|
|
65
|
+
This method needs to be called within the iteration loop
|
|
66
|
+
to update the looping condition and the internal tracking.
|
|
67
|
+
|
|
68
|
+
INPUTS :
|
|
69
|
+
progress : (float) progress number between 0 and 1
|
|
70
|
+
success : (bool) was the update step successful?
|
|
71
|
+
msg : (string) additional logging message
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
#compute progress in percent (round to integer)
|
|
75
|
+
percentage = int(100 * progress)
|
|
76
|
+
|
|
77
|
+
#count successful steps
|
|
78
|
+
self.successful_steps += int(success)
|
|
79
|
+
|
|
80
|
+
#count total steps
|
|
81
|
+
self.steps += 1
|
|
82
|
+
|
|
83
|
+
#generation condition is progress less then 1
|
|
84
|
+
self.condition = progress < 1.0
|
|
85
|
+
|
|
86
|
+
#check if percentage can be displayed
|
|
87
|
+
if percentage >= self.display_percentages[0]:
|
|
88
|
+
self.display_percentages.pop(0)
|
|
89
|
+
if self.logger:
|
|
90
|
+
self.logger.info(f"progress={percentage:.0f}%"+msg)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## REALTIME PLOTTER CLASS
|
|
4
|
+
## (utils/realtimeplotter.py)
|
|
5
|
+
##
|
|
6
|
+
## Milan Rother 2024
|
|
7
|
+
##
|
|
8
|
+
#########################################################################################
|
|
9
|
+
|
|
10
|
+
# IMPORTS ===============================================================================
|
|
11
|
+
|
|
12
|
+
import matplotlib.pyplot as plt
|
|
13
|
+
import matplotlib.style as mplstyle
|
|
14
|
+
mplstyle.use("fast")
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
import time
|
|
19
|
+
from collections import deque
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# PLOTTER CLASS =========================================================================
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RealtimePlotter:
|
|
26
|
+
"""
|
|
27
|
+
Class that manages a realtime plotting window that
|
|
28
|
+
can stream in x-y-data and update accordingly
|
|
29
|
+
|
|
30
|
+
INPUTS:
|
|
31
|
+
max_samples : (int) maximum number of samples to plot
|
|
32
|
+
update_interval : (float) time in seconds between refreshs
|
|
33
|
+
labels : (list of strings) labels for plot traces
|
|
34
|
+
x_label : (str) label for x-axis
|
|
35
|
+
y_label : (str) label for y-axis
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, max_samples=None, update_interval=1, labels=[], x_label="", y_label=""):
|
|
39
|
+
|
|
40
|
+
#plotter settings
|
|
41
|
+
self.max_samples = max_samples
|
|
42
|
+
self.update_interval = update_interval
|
|
43
|
+
self.labels = labels
|
|
44
|
+
self.x_label = x_label
|
|
45
|
+
self.y_label = y_label
|
|
46
|
+
|
|
47
|
+
#figure initialization
|
|
48
|
+
self.fig, self.ax = plt.subplots(nrows=1,
|
|
49
|
+
ncols=1,
|
|
50
|
+
figsize=(8,4),
|
|
51
|
+
tight_layout=True,
|
|
52
|
+
dpi=120)
|
|
53
|
+
|
|
54
|
+
#custom colors
|
|
55
|
+
self.ax.set_prop_cycle(color=["#e41a1c",
|
|
56
|
+
"#377eb8",
|
|
57
|
+
"#4daf4a",
|
|
58
|
+
"#984ea3",
|
|
59
|
+
"#ff7f00"])
|
|
60
|
+
|
|
61
|
+
#plot settings
|
|
62
|
+
self.ax.set_xlabel(self.x_label)
|
|
63
|
+
self.ax.set_ylabel(self.y_label)
|
|
64
|
+
self.ax.grid(True)
|
|
65
|
+
|
|
66
|
+
#data and lines (traces) for plotting
|
|
67
|
+
self.lines = []
|
|
68
|
+
self.data = []
|
|
69
|
+
|
|
70
|
+
#tracking update time
|
|
71
|
+
self.last_update = time.time()
|
|
72
|
+
|
|
73
|
+
#flag for running mode
|
|
74
|
+
self.is_running = True
|
|
75
|
+
|
|
76
|
+
# Connect the close event to the on_close method
|
|
77
|
+
self.fig.canvas.mpl_connect("close_event", self.on_close)
|
|
78
|
+
|
|
79
|
+
# Initialize legend
|
|
80
|
+
self.legend = None
|
|
81
|
+
self.lined = {}
|
|
82
|
+
|
|
83
|
+
#show the plotting window
|
|
84
|
+
self.show()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def update_all(self, x, y):
|
|
88
|
+
|
|
89
|
+
#not running? -> quit early
|
|
90
|
+
if not self.is_running:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
#no data yet? -> initialize lines
|
|
94
|
+
if not self.data:
|
|
95
|
+
|
|
96
|
+
#data initialization
|
|
97
|
+
for i, val in enumerate(y):
|
|
98
|
+
self.data.append({"x": [], "y": []})
|
|
99
|
+
|
|
100
|
+
#label selection and line (trace) initialization
|
|
101
|
+
label = self.labels[i] if i < len(self.labels) else f"port {i}"
|
|
102
|
+
line, = self.ax.plot([], [], lw=1.5, label=label)
|
|
103
|
+
self.lines.append(line)
|
|
104
|
+
|
|
105
|
+
# Create legend
|
|
106
|
+
self.legend = self.ax.legend(fancybox=False, ncols=int(np.ceil(len(y)/4)), loc="lower left")
|
|
107
|
+
self._setup_legend_picking()
|
|
108
|
+
|
|
109
|
+
#check if new update of plot is required
|
|
110
|
+
current_time = time.time()
|
|
111
|
+
if current_time - self.last_update > self.update_interval:
|
|
112
|
+
|
|
113
|
+
#replace the data
|
|
114
|
+
for i, val in enumerate(y):
|
|
115
|
+
self.data[i]["x"] = x
|
|
116
|
+
self.data[i]["y"] = val
|
|
117
|
+
|
|
118
|
+
self._update_plot()
|
|
119
|
+
self.last_update = current_time
|
|
120
|
+
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def update(self, x, y):
|
|
125
|
+
|
|
126
|
+
#not running? -> quit early
|
|
127
|
+
if not self.is_running:
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
#no data yet? -> initialize lines
|
|
131
|
+
if not self.data:
|
|
132
|
+
|
|
133
|
+
#vectorial data -> multiple traces
|
|
134
|
+
if np.isscalar(y):
|
|
135
|
+
|
|
136
|
+
#size of data
|
|
137
|
+
n = 1
|
|
138
|
+
|
|
139
|
+
#check if rolling window plot
|
|
140
|
+
if self.max_samples is None:
|
|
141
|
+
self.data.append({"x": [], "y": []})
|
|
142
|
+
else:
|
|
143
|
+
self.data.append({"x": deque(maxlen=self.max_samples),
|
|
144
|
+
"y": deque(maxlen=self.max_samples)})
|
|
145
|
+
|
|
146
|
+
#label selection and line (trace) initialization
|
|
147
|
+
label = self.labels[0] if self.labels else "port 0"
|
|
148
|
+
line, = self.ax.plot([], [], lw=1.5, label=label)
|
|
149
|
+
self.lines.append(line)
|
|
150
|
+
|
|
151
|
+
else:
|
|
152
|
+
|
|
153
|
+
#size of data
|
|
154
|
+
n = len(y)
|
|
155
|
+
|
|
156
|
+
for i in range(n):
|
|
157
|
+
|
|
158
|
+
#check if rolling window plot
|
|
159
|
+
if self.max_samples is None:
|
|
160
|
+
self.data.append({"x": [], "y": []})
|
|
161
|
+
else:
|
|
162
|
+
self.data.append({"x": deque(maxlen=self.max_samples),
|
|
163
|
+
"y": deque(maxlen=self.max_samples)})
|
|
164
|
+
|
|
165
|
+
#label selection and line (trace) initialization
|
|
166
|
+
label = self.labels[i] if i < len(self.labels) else f"port {i}"
|
|
167
|
+
line, = self.ax.plot([], [], lw=1.5, label=label)
|
|
168
|
+
self.lines.append(line)
|
|
169
|
+
|
|
170
|
+
# Create legend
|
|
171
|
+
self.legend = self.ax.legend(fancybox=False, ncols=int(np.ceil(n/4)), loc="lower left")
|
|
172
|
+
self._setup_legend_picking()
|
|
173
|
+
|
|
174
|
+
#add the data
|
|
175
|
+
if np.isscalar(y):
|
|
176
|
+
self.data[0]["x"].append(x)
|
|
177
|
+
self.data[0]["y"].append(y)
|
|
178
|
+
else:
|
|
179
|
+
for i, val in enumerate(y):
|
|
180
|
+
self.data[i]["x"].append(x)
|
|
181
|
+
self.data[i]["y"].append(val)
|
|
182
|
+
|
|
183
|
+
#check if new update of plot is required
|
|
184
|
+
current_time = time.time()
|
|
185
|
+
if current_time - self.last_update > self.update_interval:
|
|
186
|
+
self._update_plot()
|
|
187
|
+
self.last_update = current_time
|
|
188
|
+
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _update_plot(self):
|
|
193
|
+
|
|
194
|
+
#set the data to the lines (traces) of the plot
|
|
195
|
+
for i, line in enumerate(self.lines):
|
|
196
|
+
line.set_data(self.data[i]["x"], self.data[i]["y"])
|
|
197
|
+
|
|
198
|
+
#rescale the window
|
|
199
|
+
self.ax.relim()
|
|
200
|
+
self.ax.autoscale_view()
|
|
201
|
+
|
|
202
|
+
#redraw the figure
|
|
203
|
+
self.fig.canvas.draw()
|
|
204
|
+
self.fig.canvas.flush_events()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def show(self):
|
|
208
|
+
plt.show(block=False)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def on_close(self, event):
|
|
212
|
+
self.is_running = False
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _setup_legend_picking(self):
|
|
216
|
+
|
|
217
|
+
#setup the picking for the legend lines
|
|
218
|
+
for legline, origline in zip(self.legend.get_lines(), self.lines):
|
|
219
|
+
legline.set_picker(5) # 5 points tolerance
|
|
220
|
+
self.lined[legline] = origline
|
|
221
|
+
|
|
222
|
+
def on_pick(event):
|
|
223
|
+
legline = event.artist
|
|
224
|
+
origline = self.lined[legline]
|
|
225
|
+
visible = not origline.get_visible()
|
|
226
|
+
origline.set_visible(visible)
|
|
227
|
+
legline.set_alpha(1.0 if visible else 0.2)
|
|
228
|
+
self.fig.canvas.draw()
|
|
229
|
+
|
|
230
|
+
self.fig.canvas.mpl_connect("pick_event", on_pick)
|