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/simulation.py
ADDED
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## MAIN SIMULATION ENGINE
|
|
4
|
+
## (simulation.py)
|
|
5
|
+
##
|
|
6
|
+
## This module contains the simulation class that handles
|
|
7
|
+
## the blocks and connections and the timestepping methods.
|
|
8
|
+
##
|
|
9
|
+
## Milan Rother 2024
|
|
10
|
+
##
|
|
11
|
+
#########################################################################################
|
|
12
|
+
|
|
13
|
+
# IMPORTS ===============================================================================
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
from .utils.funcs import path_length_dfs
|
|
19
|
+
from .utils.progresstracker import ProgressTracker
|
|
20
|
+
from .solvers import SSPRK22
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# MAIN SIMULATION CLASS =================================================================
|
|
24
|
+
|
|
25
|
+
class Simulation:
|
|
26
|
+
"""
|
|
27
|
+
Class that performs transient analysis of the dynamical system, defined by the
|
|
28
|
+
blocks and connecions. It manages all the blocks and connections and the timestep update.
|
|
29
|
+
|
|
30
|
+
The global system equation is evaluated by fixed point iteration, so the information from
|
|
31
|
+
each timestep gets distributed within the entire system and is available for all blocks at
|
|
32
|
+
all times.
|
|
33
|
+
|
|
34
|
+
The minimum number of fixed-point iterations 'iterations_min' is set to 'None' by default
|
|
35
|
+
and then the length of the longest internal signal path (with passthrough) is used as the
|
|
36
|
+
estimate for minimum number of iterations needed for the information to reach all instant
|
|
37
|
+
time blocks in each timestep. Dont change this unless you know that the actual path is
|
|
38
|
+
shorter or something similar that prohibits instant time information flow.
|
|
39
|
+
|
|
40
|
+
Convergence check for the fixed-point iteration loop with 'tolerance_fpi' is based on
|
|
41
|
+
max absolute error (max-norm) to previous iteration and should not be touched.
|
|
42
|
+
|
|
43
|
+
Multiple numerical integrators are implemented in the 'pathsim.solvers' module.
|
|
44
|
+
The default solver is a fixed timestep 2nd order Strong Stability Preserving Runge Kutta
|
|
45
|
+
(SSPRK22) method which is quite fast and has ok accuracy, especially if you are forced to
|
|
46
|
+
take small steps to cover the behaviour of forcing functions. Adaptive timestepping and
|
|
47
|
+
implicit integrators are also available.
|
|
48
|
+
|
|
49
|
+
INPUTS:
|
|
50
|
+
blocks : (list of 'Block' objects) blocks that make up the system
|
|
51
|
+
connections : (list of 'Connection' objects) connections that connect the blocks
|
|
52
|
+
dt : (float) transient simulation timestep
|
|
53
|
+
dt_min : (float) lower bound for timestep, default '0.0'
|
|
54
|
+
dt_max : (float) upper bound for timestep, default 'None'
|
|
55
|
+
Solver : ('Solver' class) solver for numerical integration from pathsim.solvers
|
|
56
|
+
tolerance_fpi : (float) absolute tolerance for convergence of fixed-point iterations
|
|
57
|
+
tolerance_lte : (float) absolute tolerance for local truncation error (integrator error controller)
|
|
58
|
+
iterations_min : (int) minimum number of fixed-point iterations for system function evaluation
|
|
59
|
+
iterations_max : (int) maximum allowed number of fixed-point iterations for system function evaluation
|
|
60
|
+
log : (bool, string) flag to enable logging (alternatively a path can be specified)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self,
|
|
64
|
+
blocks=None,
|
|
65
|
+
connections=None,
|
|
66
|
+
dt=0.01,
|
|
67
|
+
dt_min=0.0,
|
|
68
|
+
dt_max=None,
|
|
69
|
+
Solver=SSPRK22,
|
|
70
|
+
tolerance_fpi=1e-12,
|
|
71
|
+
tolerance_lte=1e-8,
|
|
72
|
+
iterations_min=None,
|
|
73
|
+
iterations_max=200,
|
|
74
|
+
log=True):
|
|
75
|
+
|
|
76
|
+
#system definition
|
|
77
|
+
self.blocks = [] if blocks is None else blocks
|
|
78
|
+
self.connections = [] if connections is None else connections
|
|
79
|
+
|
|
80
|
+
#simulation timestep and bounds
|
|
81
|
+
self.dt = dt
|
|
82
|
+
self.dt_min = dt_min
|
|
83
|
+
self.dt_max = dt_max
|
|
84
|
+
|
|
85
|
+
#numerical integrator to be used (class definition)
|
|
86
|
+
self.Solver = Solver
|
|
87
|
+
|
|
88
|
+
#numerical integrator instance -> initialized later
|
|
89
|
+
self.engine = None
|
|
90
|
+
|
|
91
|
+
#error tolerances
|
|
92
|
+
self.tolerance_fpi = tolerance_fpi
|
|
93
|
+
self.tolerance_lte = tolerance_lte
|
|
94
|
+
|
|
95
|
+
#iterations for fixed-point loop
|
|
96
|
+
self.iterations_min = iterations_min
|
|
97
|
+
self.iterations_max = iterations_max
|
|
98
|
+
|
|
99
|
+
#enable logging flag
|
|
100
|
+
self.log = log
|
|
101
|
+
|
|
102
|
+
#initial simulation time
|
|
103
|
+
self.time = 0.0
|
|
104
|
+
|
|
105
|
+
#setup everything
|
|
106
|
+
self._setup()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def __str__(self):
|
|
110
|
+
return "\n".join([str(block) for block in self.blocks])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# simulation setup ------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def _setup(self):
|
|
116
|
+
"""
|
|
117
|
+
Initialize the logger, check the connections for validity, initialize
|
|
118
|
+
the numerical integrators within the dynamical blocks and compute
|
|
119
|
+
the internal path length of the system.
|
|
120
|
+
|
|
121
|
+
This is very lightweight.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
#initialize logging for logging mode
|
|
125
|
+
self._initialize_logger()
|
|
126
|
+
|
|
127
|
+
#check if connections are valid
|
|
128
|
+
self._check_connections()
|
|
129
|
+
|
|
130
|
+
#set numerical integration solver to all blocks
|
|
131
|
+
self._set_solver()
|
|
132
|
+
|
|
133
|
+
#compute the length of the longest path in the system
|
|
134
|
+
self._estimate_path_length()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# logger methods --------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def _initialize_logger(self):
|
|
140
|
+
"""
|
|
141
|
+
setup and configure logging
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
#initialize the logger
|
|
145
|
+
self.logger = logging.Logger("PathSim_Simulation_Logger")
|
|
146
|
+
|
|
147
|
+
#check if logging is selected
|
|
148
|
+
if self.log:
|
|
149
|
+
#if a filename for logging is specified
|
|
150
|
+
filename = self.log if isinstance(self.log, str) else None
|
|
151
|
+
handler = logging.FileHandler(filename) if filename else logging.StreamHandler()
|
|
152
|
+
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
153
|
+
handler.setFormatter(formatter)
|
|
154
|
+
|
|
155
|
+
self.logger.addHandler(handler)
|
|
156
|
+
self.logger.setLevel(logging.INFO)
|
|
157
|
+
|
|
158
|
+
self._logger_info("LOGGING enabled")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _logger_info(self, message):
|
|
162
|
+
if self.log: self.logger.info(message)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _logger_error(self, message):
|
|
166
|
+
if self.log: self.logger.error(message)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _logger_warning(self, message):
|
|
170
|
+
if self.log: self.logger.warning(message)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# adding blocks and connections -----------------------------------------------
|
|
174
|
+
|
|
175
|
+
def add_block(self, block):
|
|
176
|
+
"""
|
|
177
|
+
Adds a new block to an existing 'Simulation' instance and initializes the solver.
|
|
178
|
+
|
|
179
|
+
INPUTS:
|
|
180
|
+
block : ('Block' instance) block to add to the simulation
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
#check if block already in block list
|
|
184
|
+
if block in self.blocks:
|
|
185
|
+
raise ValueError(f"block {block} already part of simulation")
|
|
186
|
+
|
|
187
|
+
#initialize numerical integrator of block
|
|
188
|
+
block.set_solver(self.Solver, self.tolerance_lte)
|
|
189
|
+
|
|
190
|
+
#add block to global blocklist
|
|
191
|
+
self.blocks.append(block)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def add_connection(self, connection):
|
|
195
|
+
"""
|
|
196
|
+
Adds a new connection to an existing 'Simulation' instance.
|
|
197
|
+
|
|
198
|
+
INPUTS:
|
|
199
|
+
connection : ('Connection' instance) connection to add to the simulation
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
#check if connection already in block list
|
|
203
|
+
if connection in self.connections:
|
|
204
|
+
raise ValueError(f"connection {connection} already part of simulation")
|
|
205
|
+
|
|
206
|
+
#check if connection overwrites existing connections
|
|
207
|
+
for conn in self.connections:
|
|
208
|
+
if connection.overwrites(conn):
|
|
209
|
+
_msg = f"connection {connection} overwrites {conn}"
|
|
210
|
+
self._logger_error(_msg)
|
|
211
|
+
raise ValueError(_msg)
|
|
212
|
+
|
|
213
|
+
#add connection to global connection list
|
|
214
|
+
self.connections.append(connection)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# topological checks ----------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
def _check_connections(self):
|
|
220
|
+
"""
|
|
221
|
+
Check if connections are valid and if there is no input port that recieves
|
|
222
|
+
multiple outputs and could be overwritten unintentionally.
|
|
223
|
+
|
|
224
|
+
If multiple outputs are assigned to the same input, a warning is displayed
|
|
225
|
+
in the logging and the target port index is incremented by one.
|
|
226
|
+
This is more convenient, because with this, the input ports for multi input blocks
|
|
227
|
+
where the port assignment doesnt matter (such as 'Multiplier' and 'Adder') dont
|
|
228
|
+
have to be specified explicitly.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
#iterate connections and check if they are valid
|
|
232
|
+
for i, conn_1 in enumerate(self.connections):
|
|
233
|
+
|
|
234
|
+
#check if connections overwrite each other and raise exception
|
|
235
|
+
for conn_2 in self.connections[(i+1):]:
|
|
236
|
+
if conn_1.overwrites(conn_2):
|
|
237
|
+
_msg = f"connection {conn_1} overwrites {conn_2}"
|
|
238
|
+
self._logger_error(_msg)
|
|
239
|
+
raise ValueError(_msg)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _estimate_path_length(self):
|
|
243
|
+
"""
|
|
244
|
+
Perform recursive depth first search to compute the length of the
|
|
245
|
+
longest signal path over instant time blocks, information can travel
|
|
246
|
+
within a single timestep.
|
|
247
|
+
|
|
248
|
+
The depth first search leverates the '__len__' method of the blocks
|
|
249
|
+
for contribution of each block to the total signal path.
|
|
250
|
+
This enables 'Subsystem' blocks to propagate their internal length upward.
|
|
251
|
+
|
|
252
|
+
The result 'max_path_length' can be used as a an estimate for the
|
|
253
|
+
minimum number of fixed-point iterations in the '_update' method in
|
|
254
|
+
the main simulation loop.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
#iterate all possible starting blocks (nodes of directed graph)
|
|
258
|
+
max_path_length = 0
|
|
259
|
+
for block in self.blocks:
|
|
260
|
+
|
|
261
|
+
#recursively compute the longest path via depth first search
|
|
262
|
+
path_length = path_length_dfs(self.connections, block)
|
|
263
|
+
if path_length > max_path_length:
|
|
264
|
+
max_path_length = path_length
|
|
265
|
+
|
|
266
|
+
#set 'iterations_min' for fixed-point loop if not provided globally
|
|
267
|
+
if self.iterations_min is None:
|
|
268
|
+
self.iterations_min = max(1, max_path_length)
|
|
269
|
+
|
|
270
|
+
#logging message, using path length as minimum iterations
|
|
271
|
+
self._logger_info(f"PATH LENGTH ESTIMATE {max_path_length}, 'iterations_min' set to {self.iterations_min}")
|
|
272
|
+
|
|
273
|
+
else:
|
|
274
|
+
#logging message
|
|
275
|
+
self._logger_info(f"PATH LENGTH ESTIMATE {max_path_length}")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# solver management -----------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
def _set_solver(self, Solver=None, tolerance_lte=None):
|
|
281
|
+
"""
|
|
282
|
+
Initialize all blocks with solver for numerical integration
|
|
283
|
+
and tolerance for local truncation error 'tolerance_lte'.
|
|
284
|
+
|
|
285
|
+
If blocks already have solvers, change the numerical integrator
|
|
286
|
+
to the 'Solver' class.
|
|
287
|
+
|
|
288
|
+
INPUTS:
|
|
289
|
+
Solver : ('Solver' class) numerical solver definition
|
|
290
|
+
tolerance_lte : (float) tolerance for local truncation error
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
#update global solver class
|
|
294
|
+
if Solver is not None:
|
|
295
|
+
self.Solver = Solver
|
|
296
|
+
|
|
297
|
+
#update tolerance for local truncation error
|
|
298
|
+
if tolerance_lte is not None:
|
|
299
|
+
self.tolerance_lte = tolerance_lte
|
|
300
|
+
|
|
301
|
+
#initialize dummy engine to get solver attributes
|
|
302
|
+
self.engine = self.Solver()
|
|
303
|
+
|
|
304
|
+
#flag for adaptive and explicit solver selection
|
|
305
|
+
self.is_adaptive = self.engine.is_adaptive
|
|
306
|
+
self.is_explicit = self.engine.is_explicit
|
|
307
|
+
|
|
308
|
+
#iterate all blocks and set integration engines
|
|
309
|
+
for block in self.blocks:
|
|
310
|
+
block.set_solver(self.Solver, self.tolerance_lte)
|
|
311
|
+
|
|
312
|
+
#logging message
|
|
313
|
+
self._logger_info(f"SOLVER {self.engine} adaptive={self.is_adaptive} implicit={not self.is_explicit}")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# resetting -------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
def reset(self):
|
|
319
|
+
"""
|
|
320
|
+
Reset the blocks to their initial state and the global time of
|
|
321
|
+
the simulation. For recording blocks such as 'Scope', their recorded
|
|
322
|
+
data is also reset.
|
|
323
|
+
|
|
324
|
+
Afterwards the system function os evaluated with '_update' to update
|
|
325
|
+
the block inputs and outputs.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
self._logger_info("RESET")
|
|
329
|
+
|
|
330
|
+
#reset simulation time
|
|
331
|
+
self.time = 0.0
|
|
332
|
+
|
|
333
|
+
#reset blocks to initial state
|
|
334
|
+
for block in self.blocks:
|
|
335
|
+
block.reset()
|
|
336
|
+
|
|
337
|
+
#evaluate system function
|
|
338
|
+
self._update(0.0)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# timestepping ----------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
def _revert(self):
|
|
344
|
+
"""
|
|
345
|
+
Revert simulation state to previous timestep for adaptive solvers
|
|
346
|
+
when local truncation error is too large and timestep has to be
|
|
347
|
+
retaken with smaller timestep.
|
|
348
|
+
"""
|
|
349
|
+
for block in self.blocks:
|
|
350
|
+
block.revert()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _sample(self, t):
|
|
354
|
+
"""
|
|
355
|
+
Sample data from blocks that implement the 'sample' method such
|
|
356
|
+
as 'Scope', 'Delay' and the blocks that sample from a random
|
|
357
|
+
distribution at a given time 't'.
|
|
358
|
+
|
|
359
|
+
INPUTS:
|
|
360
|
+
t : (float) time where to sample
|
|
361
|
+
"""
|
|
362
|
+
for block in self.blocks:
|
|
363
|
+
block.sample(t)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _update(self, t):
|
|
367
|
+
"""
|
|
368
|
+
Fixed-point iterations to resolve algebraic loops and distribute
|
|
369
|
+
information within the system.
|
|
370
|
+
|
|
371
|
+
Effectively evaluates the right hand side function of the global
|
|
372
|
+
system ODE/DAE
|
|
373
|
+
|
|
374
|
+
dx/dt = f(x, t) <- this one (ODE system function)
|
|
375
|
+
0 = g(x, t) <- and this one (algebraic constraints)
|
|
376
|
+
|
|
377
|
+
by converging the whole system to a fixed-point at a given point
|
|
378
|
+
in time 't'.
|
|
379
|
+
|
|
380
|
+
If no algebraic loops are present in the system, it usually converges
|
|
381
|
+
already after 'iterations_min' as long as the path length has been
|
|
382
|
+
used as an estimate for the minimum number of iterations.
|
|
383
|
+
|
|
384
|
+
INPUTS:
|
|
385
|
+
t : (float) evaluation time of the system function
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
#perform minimum number of fixed-point iterations without error checking
|
|
389
|
+
for _iteration in range(self.iterations_min):
|
|
390
|
+
|
|
391
|
+
#update connenctions (data transfer)
|
|
392
|
+
for connection in self.connections:
|
|
393
|
+
connection.update()
|
|
394
|
+
|
|
395
|
+
#update all blocks
|
|
396
|
+
for block in self.blocks:
|
|
397
|
+
block.update(t)
|
|
398
|
+
|
|
399
|
+
#perform fixed-point iterations until convergence with error checking
|
|
400
|
+
for iteration in range(self.iterations_min, self.iterations_max):
|
|
401
|
+
|
|
402
|
+
#update connenctions (data transfer)
|
|
403
|
+
for connection in self.connections:
|
|
404
|
+
connection.update()
|
|
405
|
+
|
|
406
|
+
#update instant time blocks
|
|
407
|
+
max_error = 0.0
|
|
408
|
+
for block in self.blocks:
|
|
409
|
+
error = block.update(t)
|
|
410
|
+
if error > max_error:
|
|
411
|
+
max_error = error
|
|
412
|
+
|
|
413
|
+
#return number of iterations if converged
|
|
414
|
+
if max_error <= self.tolerance_fpi:
|
|
415
|
+
return iteration+1
|
|
416
|
+
|
|
417
|
+
#not converged
|
|
418
|
+
_msg = f"fixed-point loop in '_update' not converged, iter={iteration+1}, err={max_error}"
|
|
419
|
+
self._logger_error(_msg)
|
|
420
|
+
raise RuntimeError(_msg)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _solve(self, t, dt):
|
|
424
|
+
"""
|
|
425
|
+
For implicit solvers, this method implements the solving step
|
|
426
|
+
of the implicit update equation.
|
|
427
|
+
|
|
428
|
+
It already involves the evaluation of the system equation with
|
|
429
|
+
the '_update' method within the loop.
|
|
430
|
+
|
|
431
|
+
This also tracks the evolution of the solution as an estimate
|
|
432
|
+
for the convergence via the max residual norm of the fixed point
|
|
433
|
+
equation of the previous solution.
|
|
434
|
+
|
|
435
|
+
INPUTS:
|
|
436
|
+
t : (float) evaluation time of dynamical timestepping
|
|
437
|
+
dt : (float) timestep
|
|
438
|
+
|
|
439
|
+
RETURNS:
|
|
440
|
+
success : (bool) indicator if the timestep was successful
|
|
441
|
+
total_evaluations : (int) total number of system evaluations
|
|
442
|
+
total_solver_iterations : (int) total number of implicit solver iterations
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
#total evaluations of system equation
|
|
446
|
+
total_evaluations = 0
|
|
447
|
+
|
|
448
|
+
#perform fixed-point iterations to solve implicit update equation
|
|
449
|
+
for iteration in range(self.iterations_max):
|
|
450
|
+
|
|
451
|
+
#evaluate system equation (this is a fixed point loop)
|
|
452
|
+
total_evaluations += self._update(t)
|
|
453
|
+
|
|
454
|
+
#advance solution of implicit solver
|
|
455
|
+
max_error = 0.0
|
|
456
|
+
for block in self.blocks:
|
|
457
|
+
error = block.solve(t, dt)
|
|
458
|
+
if error > max_error:
|
|
459
|
+
max_error = error
|
|
460
|
+
|
|
461
|
+
#check for convergence (only error)
|
|
462
|
+
if max_error <= self.tolerance_fpi:
|
|
463
|
+
return True, total_evaluations, iteration + 1
|
|
464
|
+
|
|
465
|
+
#not converged in 'self.iterations_max' steps
|
|
466
|
+
return False, total_evaluations, iteration + 1
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _step(self, t, dt):
|
|
470
|
+
"""
|
|
471
|
+
Performs the 'step' method for dynamical blocks with internal
|
|
472
|
+
states that have a numerical integration engine.
|
|
473
|
+
Collects the local truncation error estimates and the timestep
|
|
474
|
+
rescale factor from the error controllers of the internal
|
|
475
|
+
intergation engines if they provide an error estimate
|
|
476
|
+
(for example embedded Runge-Kutta methods).
|
|
477
|
+
|
|
478
|
+
NOTE:
|
|
479
|
+
Not to be confused with the global 'step' method, the '_step'
|
|
480
|
+
method executes the intermediate timesteps in multistage solvers
|
|
481
|
+
such as Runge-Kutta methods.
|
|
482
|
+
|
|
483
|
+
INPUTS:
|
|
484
|
+
t : (float) evaluation time of dynamical timestepping
|
|
485
|
+
dt : (float) timestep
|
|
486
|
+
|
|
487
|
+
RETURNS:
|
|
488
|
+
success : (bool) indicator if the timestep was successful
|
|
489
|
+
max_error : (float) maximum local truncation error from integration
|
|
490
|
+
scale : (float) rescale factor for timestep
|
|
491
|
+
"""
|
|
492
|
+
|
|
493
|
+
#initial timestep rescale and error estimate
|
|
494
|
+
success, max_error, scale = True, 0.0, 1.0
|
|
495
|
+
|
|
496
|
+
#step blocks and get error estimates if available
|
|
497
|
+
for block in self.blocks:
|
|
498
|
+
ss, error, scl = block.step(t, dt)
|
|
499
|
+
if not ss: success = False
|
|
500
|
+
if error > max_error:
|
|
501
|
+
max_error, scale = error, scl
|
|
502
|
+
|
|
503
|
+
return success, max_error, scale
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def step(self, dt=None, adaptive=False):
|
|
507
|
+
"""
|
|
508
|
+
Advances the simulation by one timestep 'dt'.
|
|
509
|
+
|
|
510
|
+
If the 'adaptive' flag is set to 'True' and the selected solver
|
|
511
|
+
supports adaptive timestepping ('self.is_adaptive'), and the
|
|
512
|
+
local truncation error or the solver error exceeds the tolerance
|
|
513
|
+
'tolerance_lte', simulation state is reverted ('revert') to the
|
|
514
|
+
state before the 'step' method was called.
|
|
515
|
+
|
|
516
|
+
INPUTS:
|
|
517
|
+
dt : (float) timestep
|
|
518
|
+
adaptive : (bool) use adaptive timestepping (if available)
|
|
519
|
+
|
|
520
|
+
RETURNS:
|
|
521
|
+
success : (bool) indicator if the timestep was successful
|
|
522
|
+
max_error : (float) maximum local truncation error from integration
|
|
523
|
+
scale : (float) rescale factor for timestep
|
|
524
|
+
total_evaluations : (int) total number of system evaluations
|
|
525
|
+
total_solver_iterations : (int) total number of implicit solver iterations
|
|
526
|
+
"""
|
|
527
|
+
|
|
528
|
+
#default global timestep as local timestep
|
|
529
|
+
if dt is None:
|
|
530
|
+
dt = self.dt
|
|
531
|
+
|
|
532
|
+
#buffer internal states
|
|
533
|
+
for block in self.blocks:
|
|
534
|
+
block.buffer()
|
|
535
|
+
|
|
536
|
+
#total function evaluations of system equation
|
|
537
|
+
total_evaluations = 0
|
|
538
|
+
|
|
539
|
+
#total number of implicit solver iterations
|
|
540
|
+
total_solver_iterations = 0
|
|
541
|
+
|
|
542
|
+
#iterate explicit solver stages with evaluation time (generator)
|
|
543
|
+
for time in self.engine.stages(self.time, dt):
|
|
544
|
+
|
|
545
|
+
#explicit or implicit solver stepping loop
|
|
546
|
+
if self.is_explicit:
|
|
547
|
+
|
|
548
|
+
#evaluate system equation by fixed-point iteration
|
|
549
|
+
evaluations = self._update(time)
|
|
550
|
+
iterations_sol = 0
|
|
551
|
+
|
|
552
|
+
else:
|
|
553
|
+
|
|
554
|
+
#solve implicit update equation and get iteration count
|
|
555
|
+
success, evaluations, iterations_sol = self._solve(time, dt)
|
|
556
|
+
|
|
557
|
+
#if solver did not converge -> quit early (adaptive only)
|
|
558
|
+
if adaptive and not success:
|
|
559
|
+
error, scale = 0.0, 0.5
|
|
560
|
+
break
|
|
561
|
+
|
|
562
|
+
#count iterations and function evaluations
|
|
563
|
+
total_evaluations += evaluations
|
|
564
|
+
total_solver_iterations += iterations_sol
|
|
565
|
+
|
|
566
|
+
#timestep for dynamical blocks (with internal states)
|
|
567
|
+
success, error, scale = self._step(time, dt)
|
|
568
|
+
|
|
569
|
+
#if step not successful and adaptive -> quit early
|
|
570
|
+
if adaptive and not success:
|
|
571
|
+
self._revert()
|
|
572
|
+
return success, error, scale, total_evaluations, total_solver_iterations
|
|
573
|
+
|
|
574
|
+
#increment global time and continue simulation
|
|
575
|
+
self.time += dt
|
|
576
|
+
|
|
577
|
+
#evaluate system equation before recording state
|
|
578
|
+
total_evaluations += self._update(self.time)
|
|
579
|
+
|
|
580
|
+
#sample data after successful timestep
|
|
581
|
+
self._sample(self.time)
|
|
582
|
+
|
|
583
|
+
#max local truncation error, timestep rescale, successful step
|
|
584
|
+
return success, error, scale, total_evaluations, total_solver_iterations
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def run(self, duration=10, reset=True):
|
|
588
|
+
"""
|
|
589
|
+
Perform multiple simulation timesteps for a given 'duration' in seconds.
|
|
590
|
+
|
|
591
|
+
INPUTS:
|
|
592
|
+
duration : (float) simulation time in seconds [s]
|
|
593
|
+
reset : (bool) reset the simulation before running
|
|
594
|
+
|
|
595
|
+
RETURN:
|
|
596
|
+
steps : (int) total number of simulation timesteps
|
|
597
|
+
total_evaluations : (int) total number of system evaluations
|
|
598
|
+
total_solver_iterations : (int) total number of implicit solver iterations
|
|
599
|
+
"""
|
|
600
|
+
|
|
601
|
+
#reset the simulation before running it
|
|
602
|
+
if reset:
|
|
603
|
+
self.reset()
|
|
604
|
+
|
|
605
|
+
#log message solver selection
|
|
606
|
+
self._logger_info(f"RUN duration={duration}")
|
|
607
|
+
|
|
608
|
+
#simulation start and end time
|
|
609
|
+
start_time = self.time
|
|
610
|
+
end_time = self.time + duration
|
|
611
|
+
|
|
612
|
+
#effective timestep for duration
|
|
613
|
+
_dt = self.dt
|
|
614
|
+
|
|
615
|
+
#initialize progress tracker
|
|
616
|
+
tracker = ProgressTracker(logger=self.logger, log_interval=10)
|
|
617
|
+
|
|
618
|
+
#count the number of function evaluations and solver iterations
|
|
619
|
+
total_evaluations = 0
|
|
620
|
+
total_solver_iterations = 0
|
|
621
|
+
|
|
622
|
+
#initial system function evaluation
|
|
623
|
+
total_evaluations += self._update(self.time)
|
|
624
|
+
|
|
625
|
+
#sampling states and inputs at 'self.time == starting_time'
|
|
626
|
+
self._sample(self.time)
|
|
627
|
+
|
|
628
|
+
#iterate progress tracker generator until 'progress >= 1.0' is reached
|
|
629
|
+
for _ in tracker:
|
|
630
|
+
|
|
631
|
+
#rescale effective timestep if in danger of overshooting 'end_time'
|
|
632
|
+
if self.time + _dt > end_time:
|
|
633
|
+
_dt = end_time - self.time
|
|
634
|
+
|
|
635
|
+
#advance the simulation by one (effective) timestep '_dt'
|
|
636
|
+
success, error, scale, evaluations, solver_iterations = self.step(_dt, self.is_adaptive)
|
|
637
|
+
|
|
638
|
+
#update evaluation and iteration counters
|
|
639
|
+
total_evaluations += evaluations
|
|
640
|
+
total_solver_iterations += solver_iterations
|
|
641
|
+
|
|
642
|
+
#rescale the timestep for error control if adaptive solver
|
|
643
|
+
if self.is_adaptive:
|
|
644
|
+
|
|
645
|
+
#apply bounds to timestep
|
|
646
|
+
_dt = np.clip(scale*_dt, self.dt_min, self.dt_max)
|
|
647
|
+
|
|
648
|
+
#calculate progress and update progress tracker
|
|
649
|
+
progress = (self.time - start_time)/duration
|
|
650
|
+
tracker.check(progress, success)
|
|
651
|
+
|
|
652
|
+
return tracker.steps, total_evaluations, total_solver_iterations
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .euler import *
|
|
2
|
+
from .bdf import *
|
|
3
|
+
|
|
4
|
+
from .dirk2 import *
|
|
5
|
+
from .dirk3 import *
|
|
6
|
+
|
|
7
|
+
from .esdirk4 import *
|
|
8
|
+
|
|
9
|
+
from .esdirk32 import *
|
|
10
|
+
from .esdirk43 import *
|
|
11
|
+
from .esdirk54 import *
|
|
12
|
+
from .esdirk85 import *
|
|
13
|
+
|
|
14
|
+
from .ssprk22 import *
|
|
15
|
+
from .ssprk33 import *
|
|
16
|
+
from .ssprk34 import *
|
|
17
|
+
from .rk4 import *
|
|
18
|
+
|
|
19
|
+
from .rkbs32 import *
|
|
20
|
+
from .rkf45 import *
|
|
21
|
+
from .rkck54 import *
|
|
22
|
+
from .rkdp54 import *
|
|
23
|
+
from .rkv65 import *
|
|
24
|
+
from .rkf78 import *
|
|
25
|
+
from .rkdp87 import *
|