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
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 *