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
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## BASE NUMERICAL INTEGRATOR CLASS
|
|
4
|
+
## (solvers/_solver.py)
|
|
5
|
+
##
|
|
6
|
+
## (c) Milan Rother 2023/24
|
|
7
|
+
##
|
|
8
|
+
########################################################################################
|
|
9
|
+
|
|
10
|
+
# IMPORTS ==============================================================================
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from ..utils.anderson import (
|
|
15
|
+
AndersonAcceleration,
|
|
16
|
+
NewtonAndersonAcceleration
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# BASE SOLVER CLASS ====================================================================
|
|
20
|
+
|
|
21
|
+
class Solver:
|
|
22
|
+
"""
|
|
23
|
+
Base skeleton class for solver definition. Defines the basic solver methods and the metadata.
|
|
24
|
+
|
|
25
|
+
Specific solvers need to implement (some of) the base class methods defined here.
|
|
26
|
+
This depends on the type of solver (implicit/explicit, multistage, adaptive).
|
|
27
|
+
|
|
28
|
+
INPUTS :
|
|
29
|
+
initial_value : (float or array) initial condition / integration constant
|
|
30
|
+
func : (callable) function to integrate with state 'x', input 'u' and time 't' dependency
|
|
31
|
+
jac : (callable or None) jacobian of 'func' with respect to 'x', depending on 'x', 'u' and 't', if 'None', no jacobian is used
|
|
32
|
+
tolerance_lte : (float) absolute tolerance for local truncation error (for solvers with error estimate)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, initial_value=0, func=lambda x, u, t: u, jac=None, tolerance_lte=1e-6):
|
|
36
|
+
|
|
37
|
+
#set buffer, initial state and initial condition
|
|
38
|
+
self.x_0 = self.x = self.initial_value = initial_value
|
|
39
|
+
|
|
40
|
+
#right hand side function for integration
|
|
41
|
+
self.func = func
|
|
42
|
+
|
|
43
|
+
#jacobian of right hand side function
|
|
44
|
+
self.jac = jac
|
|
45
|
+
|
|
46
|
+
#tolerance for local truncation error (for adaptive solvers)
|
|
47
|
+
self.tolerance_lte = tolerance_lte
|
|
48
|
+
|
|
49
|
+
#flag to identify adaptive/fixed timestep solvers
|
|
50
|
+
self.is_adaptive = False
|
|
51
|
+
|
|
52
|
+
#current evaluation stage for multistage solvers
|
|
53
|
+
self.stage = 0
|
|
54
|
+
|
|
55
|
+
#intermediate evaluation times for multistage solvers as ratios between [t, t+dt]
|
|
56
|
+
self.eval_stages = [0.0]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def __str__(self):
|
|
60
|
+
return self.__class__.__name__
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def stages(self, t, dt):
|
|
64
|
+
"""
|
|
65
|
+
Generator that yields the intermediate evaluation
|
|
66
|
+
time during the timestep 't + ratio * dt'.
|
|
67
|
+
"""
|
|
68
|
+
for ratio in self.eval_stages:
|
|
69
|
+
yield t + ratio * dt
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get(self):
|
|
73
|
+
"""
|
|
74
|
+
Returns current internal state of the solver.
|
|
75
|
+
"""
|
|
76
|
+
return self.x
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def set(self, x):
|
|
80
|
+
"""
|
|
81
|
+
Sets the internal state of the integration engine.
|
|
82
|
+
This method is required for event based simulations,
|
|
83
|
+
and to handle discontinuities in state variables.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
#overwrite internal state with value
|
|
87
|
+
self.x = self.x_0 = x
|
|
88
|
+
|
|
89
|
+
#reset stage counter
|
|
90
|
+
self.stage = 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def reset(self):
|
|
94
|
+
""""
|
|
95
|
+
Resets integration engine to initial state.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
#overwrite state with initial value
|
|
99
|
+
self.x = self.x_0 = self.initial_value
|
|
100
|
+
|
|
101
|
+
#reset stage counter
|
|
102
|
+
self.stage = 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def buffer(self):
|
|
106
|
+
"""
|
|
107
|
+
Saves the current state to an internal state buffer, which is
|
|
108
|
+
especially relevant for multistage and implicit solvers.
|
|
109
|
+
"""
|
|
110
|
+
self.x_0 = self.x
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def change(self, Solver, tolerance_lte=None):
|
|
114
|
+
"""
|
|
115
|
+
Change the integration engine to a new type and initialize
|
|
116
|
+
with previous solver arguments so it can continue from where
|
|
117
|
+
the 'old' solver stopped.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
#check if new tolerance is defined
|
|
121
|
+
tol = self.tolerance_lte if tolerance_lte is None else tolerance_lte
|
|
122
|
+
|
|
123
|
+
#create new engine from self
|
|
124
|
+
engine = Solver(self.initial_value, self.func, self.jac, tol)
|
|
125
|
+
|
|
126
|
+
#set internal state of new engine from self
|
|
127
|
+
engine.set(self.get())
|
|
128
|
+
|
|
129
|
+
return engine
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# methods for adaptive timestep solvers --------------------------------------------
|
|
133
|
+
|
|
134
|
+
def error_controller(self):
|
|
135
|
+
"""
|
|
136
|
+
Returns the estimated local truncation error and scaling factor
|
|
137
|
+
for the timestep, only relevant for adaptive timestepping methods.
|
|
138
|
+
"""
|
|
139
|
+
return True, 0.0, 1.0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def revert(self):
|
|
143
|
+
"""
|
|
144
|
+
Revert integration engine to previous timestep, this is only relevant
|
|
145
|
+
for adaptive methods where the simulation timestep 'dt' is rescaled and
|
|
146
|
+
the engine step is recomputed with the smaller timestep.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
#reset internal state to previous state
|
|
150
|
+
self.x = self.x_0
|
|
151
|
+
|
|
152
|
+
#reset stage counter
|
|
153
|
+
self.stage = 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# methods for timestepping ---------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def step(self, u, t, dt):
|
|
159
|
+
"""
|
|
160
|
+
performs the explicit timestep for (t+dt) based
|
|
161
|
+
on the state and input at (t)
|
|
162
|
+
|
|
163
|
+
returns the local truncation error estimate and the
|
|
164
|
+
rescale factor for the timestep if the solver is adaptive.
|
|
165
|
+
"""
|
|
166
|
+
return True, 0.0, 1.0
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# EXTENDED BASE SOLVER CLASSES =========================================================
|
|
170
|
+
|
|
171
|
+
class ExplicitSolver(Solver):
|
|
172
|
+
"""
|
|
173
|
+
Base class for explicit solver definition.
|
|
174
|
+
|
|
175
|
+
INPUTS :
|
|
176
|
+
initial_value : (float or array) initial condition / integration constant
|
|
177
|
+
func : (callable) function to integrate with state (x), input (u) and time (t) dependency
|
|
178
|
+
jac : (callable or None) jacobian of 'func' with respect to 'x', depending on 'x', 'u' and 't', if 'None', no jacobian is used
|
|
179
|
+
tolerance_lte : (float) absolute tolerance for local truncation error (for solvers with error estimate)
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(self, initial_value=0, func=lambda x, u, t: u, jac=None, tolerance_lte=1e-6):
|
|
183
|
+
super().__init__(initial_value, func, jac, tolerance_lte)
|
|
184
|
+
|
|
185
|
+
#flag to identify implicit/explicit solvers
|
|
186
|
+
self.is_explicit = True
|
|
187
|
+
self.is_implicit = False
|
|
188
|
+
|
|
189
|
+
#intermediate evaluation times for multistage solvers as ratios between [t, t+dt]
|
|
190
|
+
self.eval_stages = [0.0]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# method for direct integration ----------------------------------------------------
|
|
194
|
+
|
|
195
|
+
def integrate_singlestep(self, time=0.0, dt=0.1):
|
|
196
|
+
"""
|
|
197
|
+
Directly integrate the function 'func' for a single timestep 'dt' with
|
|
198
|
+
explicit solvers. This method is primarily intended for testing purposes.
|
|
199
|
+
|
|
200
|
+
INPUTS :
|
|
201
|
+
time_start : (float) starting time for timestep
|
|
202
|
+
dt : (float) timestep
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
#buffer current state
|
|
206
|
+
self.buffer()
|
|
207
|
+
|
|
208
|
+
#iterate solver stages (explicit updates)
|
|
209
|
+
for t in self.stages(time, dt):
|
|
210
|
+
success, error, scale = self.step(0.0, t, dt)
|
|
211
|
+
|
|
212
|
+
return success, error, scale
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def integrate(self,
|
|
216
|
+
time_start=0.0,
|
|
217
|
+
time_end=1.0,
|
|
218
|
+
dt=0.1,
|
|
219
|
+
dt_min=0.0,
|
|
220
|
+
dt_max=None,
|
|
221
|
+
adaptive=True):
|
|
222
|
+
"""
|
|
223
|
+
Directly integrate the function 'func' from 'time_start' to 'time_end' with
|
|
224
|
+
timestep 'dt' for explicit solvers. This method is primarily intended for
|
|
225
|
+
testing purposes.
|
|
226
|
+
|
|
227
|
+
INPUTS :
|
|
228
|
+
time_start : (float) starting time for integration
|
|
229
|
+
time_end : (float) end time for integration
|
|
230
|
+
dt : (float) timestep or initial timestep for adaptive solvers
|
|
231
|
+
dt_min : (float) lower bound for timestep, default '0.0'
|
|
232
|
+
dt_max : (float) upper bound for timestep, default 'None'
|
|
233
|
+
adaptive : (bool) usa adaptive timestepping if available
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
#output lists with initial state
|
|
237
|
+
output_states = [self.x]
|
|
238
|
+
output_times = [time_start]
|
|
239
|
+
|
|
240
|
+
#integration starting time
|
|
241
|
+
time = time_start
|
|
242
|
+
|
|
243
|
+
#step until duration is reached
|
|
244
|
+
while time < time_end + dt:
|
|
245
|
+
|
|
246
|
+
#perform single timestep
|
|
247
|
+
success, error, scale = self.integrate_singlestep(time, dt)
|
|
248
|
+
|
|
249
|
+
#check if timestep was successful
|
|
250
|
+
if adaptive and not success:
|
|
251
|
+
self.revert()
|
|
252
|
+
else:
|
|
253
|
+
time += dt
|
|
254
|
+
output_states.append(self.x)
|
|
255
|
+
output_times.append(time)
|
|
256
|
+
|
|
257
|
+
#rescale and apply bounds to timestep
|
|
258
|
+
if adaptive:
|
|
259
|
+
if scale*dt < dt_min:
|
|
260
|
+
raise RuntimeError("Error control requires timestep smaller 'dt_min'!")
|
|
261
|
+
dt = np.clip(scale*dt, dt_min, dt_max)
|
|
262
|
+
|
|
263
|
+
#return the evaluation times and the states
|
|
264
|
+
return np.array(output_times), np.array(output_states)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class ImplicitSolver(Solver):
|
|
268
|
+
"""
|
|
269
|
+
Base class for implicit solver definition.
|
|
270
|
+
|
|
271
|
+
INPUTS :
|
|
272
|
+
INPUTS :
|
|
273
|
+
initial_value : (float or array) initial condition / integration constant
|
|
274
|
+
func : (callable) function to integrate with state 'x', input 'u' and time 't' dependency
|
|
275
|
+
jac : (callable or None) jacobian of 'func' with respect to 'x', depending on 'x', 'u' and 't', if 'None', no jacobian is used
|
|
276
|
+
tolerance_lte : (float) absolute tolerance for local truncation error (for solvers with error estimate)
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def __init__(self, initial_value=0, func=lambda x, u, t: u, jac=None, tolerance_lte=1e-6):
|
|
280
|
+
super().__init__(initial_value, func, jac, tolerance_lte)
|
|
281
|
+
|
|
282
|
+
#flag to identify implicit/explicit solvers
|
|
283
|
+
self.is_explicit = False
|
|
284
|
+
self.is_implicit = True
|
|
285
|
+
|
|
286
|
+
#intermediate evaluation times for multistage solvers as ratios between [t, t+dt]
|
|
287
|
+
self.eval_stages = [1.0]
|
|
288
|
+
|
|
289
|
+
#initialize anderson accelerator for solving the implicit update equation
|
|
290
|
+
self.acc = NewtonAndersonAcceleration(m=5, restart=False)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# methods for timestepping ---------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
def solve(self, u, t, dt):
|
|
296
|
+
"""
|
|
297
|
+
Advances the solution of the implicit update equation of the solver
|
|
298
|
+
with Anderson Acceleration and tracks the evolution of the solution
|
|
299
|
+
by providing the residual norm of the fixed-point solution.
|
|
300
|
+
"""
|
|
301
|
+
return 0.0
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# method for direct integration ----------------------------------------------------
|
|
305
|
+
|
|
306
|
+
def integrate_singlestep(self,
|
|
307
|
+
time=0.0,
|
|
308
|
+
dt=0.1,
|
|
309
|
+
tolerance_fpi=1e-12,
|
|
310
|
+
max_iterations=5000):
|
|
311
|
+
"""
|
|
312
|
+
Directly integrate the function 'func' for a single timestep 'dt' with
|
|
313
|
+
implicit solvers. This method is primarily intended for testing purposes.
|
|
314
|
+
|
|
315
|
+
INPUTS :
|
|
316
|
+
time_start : (float) starting time for timestep
|
|
317
|
+
dt : (float) timestep
|
|
318
|
+
tolerance_fpi : (float) tolerance for fixed-point solver
|
|
319
|
+
max_iterations : (int) maximum number of fixed-point solver iterations
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
#buffer current state
|
|
323
|
+
self.buffer()
|
|
324
|
+
|
|
325
|
+
#flag for solver success
|
|
326
|
+
success_sol = True
|
|
327
|
+
|
|
328
|
+
#iterate solver stages (implicit updates)
|
|
329
|
+
for t in self.stages(time, dt):
|
|
330
|
+
|
|
331
|
+
#iteratively solve implicit update equation
|
|
332
|
+
for _ in range(max_iterations):
|
|
333
|
+
error_sol = self.solve(0.0, t, dt)
|
|
334
|
+
if error_sol < tolerance_fpi:
|
|
335
|
+
break
|
|
336
|
+
|
|
337
|
+
#catch convergence error
|
|
338
|
+
if error_sol > tolerance_fpi:
|
|
339
|
+
if success_sol: success_sol = False
|
|
340
|
+
|
|
341
|
+
#perform explicit component of timestep
|
|
342
|
+
success, error, scale = self.step(0.0, t, dt)
|
|
343
|
+
|
|
344
|
+
#step successful in total
|
|
345
|
+
success_total = success and success_sol
|
|
346
|
+
|
|
347
|
+
return success_total, error, scale
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def integrate(self,
|
|
351
|
+
time_start=0.0,
|
|
352
|
+
time_end=1.0,
|
|
353
|
+
dt=0.1,
|
|
354
|
+
dt_min=0.0,
|
|
355
|
+
dt_max=None,
|
|
356
|
+
adaptive=True,
|
|
357
|
+
tolerance_fpi=1e-12,
|
|
358
|
+
max_iterations=5000):
|
|
359
|
+
"""
|
|
360
|
+
Directly integrate the function 'func' from 'time_start' to 'time_end' with
|
|
361
|
+
timestep 'dt' for implicit solvers. This method is primarily intended for
|
|
362
|
+
testing purposes.
|
|
363
|
+
|
|
364
|
+
INPUTS :
|
|
365
|
+
time_start : (float) starting time for integration
|
|
366
|
+
time_end : (float) end time for integration
|
|
367
|
+
dt : (float) timestep or initial timestep for adaptive solvers
|
|
368
|
+
dt_min : (float) lower bound for timestep, default '0.0'
|
|
369
|
+
dt_max : (float) upper bound for timestep, default 'None'
|
|
370
|
+
adaptive : (bool) use adaptive timestepping if available
|
|
371
|
+
tolerance_fpi : (float) tolerance for fixed-point solver
|
|
372
|
+
max_iterations : (int) maximum number of fixed-point solver iterations
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
#output lists with initial state
|
|
376
|
+
output_states = [self.x]
|
|
377
|
+
output_times = [time_start]
|
|
378
|
+
|
|
379
|
+
#integration starting time
|
|
380
|
+
time = time_start
|
|
381
|
+
|
|
382
|
+
#step until duration is reached
|
|
383
|
+
while time < time_end + dt:
|
|
384
|
+
|
|
385
|
+
#integrate for single timestep
|
|
386
|
+
success, error, scale = self.integrate_singlestep(time, dt, tolerance_fpi, max_iterations)
|
|
387
|
+
|
|
388
|
+
#check if timestep was successful and adaptive
|
|
389
|
+
if adaptive and not success:
|
|
390
|
+
self.revert()
|
|
391
|
+
else:
|
|
392
|
+
time += dt
|
|
393
|
+
output_states.append(self.x)
|
|
394
|
+
output_times.append(time)
|
|
395
|
+
|
|
396
|
+
#rescale and apply bounds to timestep
|
|
397
|
+
if adaptive:
|
|
398
|
+
if scale*dt < dt_min:
|
|
399
|
+
raise RuntimeError("Error control requires timestep smaller 'dt_min'!")
|
|
400
|
+
dt = np.clip(scale*dt, dt_min, dt_max)
|
|
401
|
+
|
|
402
|
+
#return the evaluation times and the states
|
|
403
|
+
return np.array(output_times), np.array(output_states)
|
pathsim/solvers/bdf.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## BACKWARD DIFFERENTIATION FORMULAS
|
|
4
|
+
## (solvers/bdf.py)
|
|
5
|
+
##
|
|
6
|
+
## Milan Rother 2024
|
|
7
|
+
##
|
|
8
|
+
########################################################################################
|
|
9
|
+
|
|
10
|
+
# IMPORTS ==============================================================================
|
|
11
|
+
|
|
12
|
+
from ._solver import ImplicitSolver
|
|
13
|
+
from ..utils.funcs import numerical_jacobian
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# SOLVERS ==============================================================================
|
|
17
|
+
|
|
18
|
+
class BDF2(ImplicitSolver):
|
|
19
|
+
"""
|
|
20
|
+
2nd order backward differentiation formula
|
|
21
|
+
with order ramp up for the initial steps.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, initial_value=0, func=lambda x, u, t: u, jac=None, tolerance_lte=1e-6):
|
|
25
|
+
super().__init__(initial_value, func, jac, tolerance_lte)
|
|
26
|
+
|
|
27
|
+
#bdf coefficients
|
|
28
|
+
self.K = {1:[1.0], 2:[-1/3, 4/3]}
|
|
29
|
+
self.F = {1:1.0, 2:2/3}
|
|
30
|
+
|
|
31
|
+
#bdf solution buffer
|
|
32
|
+
self.B = [self.initial_value]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def reset(self):
|
|
36
|
+
""""
|
|
37
|
+
Resets integration engine to initial state.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
#reset buffer state with initial value
|
|
41
|
+
self.B = [self.initial_value]
|
|
42
|
+
|
|
43
|
+
#overwrite state with initial value
|
|
44
|
+
self.x = self.x_0 = self.initial_value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def solve(self, u, t, dt):
|
|
48
|
+
"""
|
|
49
|
+
Solves the implicit update equation via anderson acceleration.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
#buffer length for BDF order selection
|
|
53
|
+
n = len(self.B)
|
|
54
|
+
|
|
55
|
+
#fixed-point function update
|
|
56
|
+
g = self.F[n] * dt * self.func(self.x, u, t) + sum(b*k for b, k in zip(self.B, self.K[n]))
|
|
57
|
+
|
|
58
|
+
#use the jacobian
|
|
59
|
+
if self.jac is not None:
|
|
60
|
+
|
|
61
|
+
#compute jacobian
|
|
62
|
+
jac_g = self.F[n] * dt * self.jac(self.x, u, t)
|
|
63
|
+
|
|
64
|
+
#anderson acceleration step with local newton
|
|
65
|
+
self.x, err = self.acc.step(self.x, g, jac_g)
|
|
66
|
+
|
|
67
|
+
else:
|
|
68
|
+
#anderson acceleration step (pure)
|
|
69
|
+
self.x, err = self.acc.step(self.x, g, None)
|
|
70
|
+
|
|
71
|
+
#return the fixed-point residual
|
|
72
|
+
return err
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def step(self, u, t, dt):
|
|
76
|
+
"""
|
|
77
|
+
Performs the timestep by buffereing the previous state.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
#reset anderson accelerator
|
|
81
|
+
self.acc.reset()
|
|
82
|
+
|
|
83
|
+
#add to buffer
|
|
84
|
+
self.B.append(self.x)
|
|
85
|
+
if len(self.B) > 2:
|
|
86
|
+
self.B.pop(0)
|
|
87
|
+
|
|
88
|
+
return True, 0.0, 1.0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class BDF3(ImplicitSolver):
|
|
92
|
+
"""
|
|
93
|
+
3rd order backward differentiation formula
|
|
94
|
+
with order ramp up for the initial steps.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, initial_value=0, func=lambda x, u, t: u, jac=None, tolerance_lte=1e-6):
|
|
98
|
+
super().__init__(initial_value, func, jac, tolerance_lte)
|
|
99
|
+
|
|
100
|
+
#bdf coefficients
|
|
101
|
+
self.K = {1:[1.0],
|
|
102
|
+
2:[-1/3, 4/3],
|
|
103
|
+
3:[2/11, -9/11, 18/11]}
|
|
104
|
+
self.F = {1:1.0, 2:2/3, 3:6/11}
|
|
105
|
+
|
|
106
|
+
#bdf solution buffer
|
|
107
|
+
self.B = [self.initial_value]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def reset(self):
|
|
111
|
+
""""
|
|
112
|
+
Resets integration engine to initial state.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
#reset buffer state with initial value
|
|
116
|
+
self.B = [self.initial_value]
|
|
117
|
+
|
|
118
|
+
#overwrite state with initial value
|
|
119
|
+
self.x = self.x_0 = self.initial_value
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def solve(self, u, t, dt):
|
|
123
|
+
"""
|
|
124
|
+
Solves the implicit update equation via anderson acceleration.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
#buffer length for BDF order selection
|
|
128
|
+
n = len(self.B)
|
|
129
|
+
|
|
130
|
+
#fixed-point function update
|
|
131
|
+
g = self.F[n] * dt * self.func(self.x, u, t) + sum(b*k for b, k in zip(self.B, self.K[n]))
|
|
132
|
+
|
|
133
|
+
#use the jacobian
|
|
134
|
+
if self.jac is not None:
|
|
135
|
+
|
|
136
|
+
#compute jacobian
|
|
137
|
+
jac_g = self.F[n] * dt * self.jac(self.x, u, t)
|
|
138
|
+
|
|
139
|
+
#anderson acceleration step with local newton
|
|
140
|
+
self.x, err = self.acc.step(self.x, g, jac_g)
|
|
141
|
+
|
|
142
|
+
else:
|
|
143
|
+
#anderson acceleration step (pure)
|
|
144
|
+
self.x, err = self.acc.step(self.x, g, None)
|
|
145
|
+
|
|
146
|
+
#return the fixed-point residual
|
|
147
|
+
return err
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def step(self, u, t, dt):
|
|
152
|
+
"""
|
|
153
|
+
Performs the timestep by buffereing the previous state.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
#reset anderson accelerator
|
|
157
|
+
self.acc.reset()
|
|
158
|
+
|
|
159
|
+
#add to buffer
|
|
160
|
+
self.B.append(self.x)
|
|
161
|
+
if len(self.B) > 3:
|
|
162
|
+
self.B.pop(0)
|
|
163
|
+
|
|
164
|
+
return True, 0.0, 1.0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class BDF4(ImplicitSolver):
|
|
168
|
+
"""
|
|
169
|
+
4th order backward differentiation formula
|
|
170
|
+
with order ramp up for the initial steps.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def __init__(self, initial_value=0, func=lambda x, u, t: u, jac=None, tolerance_lte=1e-6):
|
|
174
|
+
super().__init__(initial_value, func, jac, tolerance_lte)
|
|
175
|
+
|
|
176
|
+
#bdf coefficients
|
|
177
|
+
self.K = {1:[1.0],
|
|
178
|
+
2:[-1/3, 4/3],
|
|
179
|
+
3:[2/11, -9/11, 18/11],
|
|
180
|
+
4:[-3/25, 16/25, -36/25, 48/25]}
|
|
181
|
+
self.F = {1:1.0, 2:2/3, 3:6/11, 4:12/25}
|
|
182
|
+
|
|
183
|
+
#bdf solution buffer
|
|
184
|
+
self.B = [self.initial_value]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def reset(self):
|
|
188
|
+
""""
|
|
189
|
+
Resets integration engine to initial state.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
#reset buffer state with initial value
|
|
193
|
+
self.B = [self.initial_value]
|
|
194
|
+
|
|
195
|
+
#overwrite state with initial value
|
|
196
|
+
self.x = self.x_0 = self.initial_value
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def solve(self, u, t, dt):
|
|
200
|
+
"""
|
|
201
|
+
Solves the implicit update equation via anderson acceleration.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
#buffer length for BDF order selection
|
|
205
|
+
n = len(self.B)
|
|
206
|
+
|
|
207
|
+
#fixed-point function update
|
|
208
|
+
g = self.F[n] * dt * self.func(self.x, u, t) + sum(b*k for b, k in zip(self.B, self.K[n]))
|
|
209
|
+
|
|
210
|
+
#use the jacobian
|
|
211
|
+
if self.jac is not None:
|
|
212
|
+
|
|
213
|
+
#compute jacobian
|
|
214
|
+
jac_g = self.F[n] * dt * self.jac(self.x, u, t)
|
|
215
|
+
|
|
216
|
+
#anderson acceleration step with local newton
|
|
217
|
+
self.x, err = self.acc.step(self.x, g, jac_g)
|
|
218
|
+
|
|
219
|
+
else:
|
|
220
|
+
#anderson acceleration step (pure)
|
|
221
|
+
self.x, err = self.acc.step(self.x, g, None)
|
|
222
|
+
|
|
223
|
+
#return the fixed-point residual
|
|
224
|
+
return err
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def step(self, u, t, dt):
|
|
228
|
+
"""
|
|
229
|
+
Performs the timestep by buffereing the previous state.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
#reset anderson accelerator
|
|
233
|
+
self.acc.reset()
|
|
234
|
+
|
|
235
|
+
#add to buffer
|
|
236
|
+
self.B.append(self.x)
|
|
237
|
+
if len(self.B) > 4:
|
|
238
|
+
self.B.pop(0)
|
|
239
|
+
|
|
240
|
+
return True, 0.0, 1.0
|