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/subsystem.py ADDED
@@ -0,0 +1,267 @@
1
+ #########################################################################################
2
+ ##
3
+ ## SUBSYSTEM DEFINITION
4
+ ## (subsystem.py)
5
+ ##
6
+ ## This module contains the 'Subsystem' and 'Interface' classes
7
+ ## that manage subsystems that can be embedded within a larger simulation
8
+ ##
9
+ ## Milan Rother 2024
10
+ ##
11
+ #########################################################################################
12
+
13
+ # IMPORTS ===============================================================================
14
+
15
+ from .blocks._block import Block
16
+ from .utils.funcs import path_length_dfs
17
+
18
+
19
+ # IO CLASS ==============================================================================
20
+
21
+ class Interface(Block):
22
+ """
23
+ Bare-bone block that serves as a data interface for the 'Subsystem' class.
24
+
25
+ It works like this:
26
+ - Internal blocks of the subsystem are connected to the inputs and outputs
27
+ of this Interface block via the internal connections.
28
+ - It behaves like a normal block (inherits the main 'Block' class methods).
29
+ - It implements some special methods to get and set the inputs and outputs
30
+ of the blocks, that are used to translate between the internal blocks of the
31
+ subsystem and the inputs and outputs of the subsystem.
32
+ - Handles data transfer to and from the internal subsystem blocks
33
+ to and from the inputs and outputs of the subsystem.
34
+ """
35
+
36
+ def set_output(self, port, value):
37
+ self.outputs[port] = value
38
+
39
+ def get_input(self, port):
40
+ return self.inputs.get(port, 0.0)
41
+
42
+
43
+ # MAIN SUBSYSTEM CLASS ==================================================================
44
+
45
+ class Subsystem(Block):
46
+ """
47
+ Subsystem class that holds its own blocks and connecions and
48
+ can natively interface with the main simulation loop.
49
+
50
+ IO interface is realized by a special 'Interface' block, that has extra
51
+ methods for setting and getting inputs and outputs and serves
52
+ as the interface of the internal blocks to the outside.
53
+
54
+ The subsystem doesnt use its 'inputs' and 'outputs' dicts directly.
55
+ It exclusively handles data transfer via the 'Interface' block.
56
+
57
+ This class can be used just like any other block during the simulation,
58
+ since it implements the required methods 'update' for the fixed-point
59
+ iteration (resolving algebraic loops with instant time blocks),
60
+ the 'step' method that performs timestepping (especially for dynamic
61
+ blocks with internal states) and the 'solve' method for solving the
62
+ implicit update equation for implicit solvers.
63
+
64
+ INPUTS :
65
+ blocks : (list of 'Block' objects) internal blocks of the subsystem
66
+ connections : (list of 'Connection' objects) internal connections of the subsystem
67
+ """
68
+
69
+ def __init__(self, blocks=None, connections=None):
70
+ super().__init__()
71
+
72
+ #internal connecions
73
+ self.connections = [] if connections is None else connections
74
+
75
+ #collect and organize internal blocks
76
+ self.blocks, self.interface = [], None
77
+
78
+ if blocks is not None:
79
+ for block in blocks:
80
+ if isinstance(block, Interface): self.interface = block
81
+ else: self.blocks.append(block)
82
+
83
+ #check if interface is defined
84
+ if self.interface is None:
85
+ raise ValueError("Subsystem 'blocks' list needs to contain 'Interface' block!")
86
+
87
+ #validate the internal connections upon initialization
88
+ self._check_connections()
89
+
90
+
91
+ def __str__(self):
92
+ _b = "\n".join([" " + str(block) for block in self.blocks])
93
+ return f"Subsystem\n" + _b
94
+
95
+
96
+ def __len__(self):
97
+ """
98
+ Recursively compute the longest signal path in the subsytem by
99
+ depth first search, leveraging the '__len__' methods of the blocks.
100
+ This enables the path length computation even for nested subsystems.
101
+
102
+ Iterate internal blocks and compute longest path from each block
103
+ as starting block.
104
+
105
+ Basically the same as in the 'Simulation' class.
106
+ """
107
+ max_path_length = 0
108
+ for block in self.blocks:
109
+ path_length = path_length_dfs(self.connections, block)
110
+ if path_length > max_path_length:
111
+ max_path_length = path_length
112
+ return max_path_length + 1 #longer due to 'Interface' block
113
+
114
+
115
+ def _check_connections(self):
116
+ """
117
+ Check if connections are valid and if there is no input port that recieves
118
+ multiple outputs and could be overwritten unintentionally.
119
+
120
+ If multiple outputs are assigned to the same input, an error is raised.
121
+ """
122
+
123
+ #iterate connections and check if they are valid
124
+ for i, conn_1 in enumerate(self.connections):
125
+
126
+ #check if connections overwrite each other and raise exception
127
+ for conn_2 in self.connections[(i+1):]:
128
+ if conn_1.overwrites(conn_2):
129
+ _msg = f"{conn_1} overwrites {conn_2}"
130
+ raise ValueError(_msg)
131
+
132
+
133
+ def reset(self):
134
+
135
+ #reset interface
136
+ self.interface.reset()
137
+
138
+ #reset internal blocks
139
+ for block in self.blocks:
140
+ block.reset()
141
+
142
+
143
+ # methods for inter-block data transfer -------------------------------------------------
144
+
145
+ def set(self, port, value):
146
+ """
147
+ The 'set' method of the 'Subsystem' sets the output
148
+ values of the 'Interface' block.
149
+ """
150
+ self.interface.set_output(port, value)
151
+
152
+
153
+ def get(self, port):
154
+ """
155
+ The 'get' method of the 'Subsystem' retrieves the input
156
+ values of the 'Interface' block.
157
+ """
158
+ return self.interface.get_input(port)
159
+
160
+
161
+ # methods for data recording ------------------------------------------------------------
162
+
163
+ def sample(self, t):
164
+
165
+ """
166
+ Update the internal connections again and sample data from
167
+ the internal blocks that implement the 'sample' method.
168
+ """
169
+
170
+ #update internal connenctions (data transfer)
171
+ for connection in self.connections:
172
+ connection.update()
173
+
174
+ #record data if required
175
+ for block in self.blocks:
176
+ block.sample(t)
177
+
178
+
179
+ # methods for block output and state updates --------------------------------------------
180
+
181
+ def update(self, t):
182
+ """
183
+ Update the instant time components of the internal blocks
184
+ to evaluate the (distributed) system equation.
185
+ """
186
+
187
+ #update internal connections (data transfer)
188
+ for connection in self.connections:
189
+ connection.update()
190
+
191
+ #update internal blocks
192
+ max_error = 0.0
193
+ for block in self.blocks:
194
+ error = block.update(t)
195
+ if error > max_error:
196
+ max_error = error
197
+
198
+ #return subsystem convergence error
199
+ return max_error
200
+
201
+
202
+ def solve(self, t, dt):
203
+ """
204
+ Advance solution of implicit update equation for internal blocks.
205
+ """
206
+ max_error = 0.0
207
+ for block in self.blocks:
208
+ error = block.solve(t, dt)
209
+ if error > max_error:
210
+ max_error = error
211
+ return max_error
212
+
213
+
214
+ def step(self, t, dt):
215
+ """
216
+ Explicit component of timestep for internal blocks including error propagation.
217
+ """
218
+
219
+ #initial timestep rescale and error estimate
220
+ success, max_error, scale = True, 0.0, 1.0
221
+
222
+ #step blocks and get error estimates if available
223
+ for block in self.blocks:
224
+ ss, error, scl = block.step(t, dt)
225
+ if not ss: success = False
226
+ if error > max_error:
227
+ max_error, scale = error, scl
228
+
229
+ return success, max_error, scale
230
+
231
+
232
+ # methods for blocks with integration engines -------------------------------------------
233
+
234
+ def set_solver(self, Solver, tolerance_lte=1e-6):
235
+ """
236
+ Initialize all blocks with solver for numerical integration
237
+ and tolerance for local truncation error 'tolerance_lte'.
238
+
239
+ If blocks already have solvers, change the numerical integrator
240
+ to the 'Solver' class.
241
+
242
+ INPUTS:
243
+ Solver : ('Solver' class) numerical solver definition
244
+ tolerance_lte : (float) tolerance for local truncation error
245
+ """
246
+
247
+ #iterate all blocks and set integration engines
248
+ for block in self.blocks:
249
+ block.set_solver(Solver, tolerance_lte)
250
+
251
+
252
+ def revert(self):
253
+ """
254
+ revert the internal blocks to the state
255
+ of the previous timestep
256
+ """
257
+ for block in self.blocks:
258
+ block.revert()
259
+
260
+
261
+ def buffer(self):
262
+ """
263
+ buffer internal states of blocks with
264
+ internal integration engines
265
+ """
266
+ for block in self.blocks:
267
+ block.buffer()
File without changes
@@ -0,0 +1,87 @@
1
+
2
+ ########################################################################################
3
+ ##
4
+ ## ADAPTIV BUFFER CLASS DEFINITION
5
+ ## (utils/adaptivebuffer.py)
6
+ ##
7
+ ## Milan Rother 2024
8
+ ##
9
+ ########################################################################################
10
+
11
+ # IMPORTS ==============================================================================
12
+
13
+ from collections import deque
14
+ from bisect import bisect_left
15
+
16
+
17
+ # HELPER CLASS =========================================================================
18
+
19
+ class AdaptiveBuffer:
20
+ """
21
+ A class that manages an adaptive buffer for delay modeling which is primarily
22
+ used in the pathsim 'Delay' block. It implements a linear interpolation for
23
+ arbitraty time lookup.
24
+
25
+ INPUTS :
26
+ delay : (float) time delay in seconds
27
+ """
28
+
29
+ def __init__(self, delay):
30
+
31
+ #the buffer uses a double ended queue
32
+ self.buffer = deque()
33
+ self.delay = delay
34
+
35
+ #for buffer cleanup every 100 lookups
36
+ self.clean_every = 100
37
+ self.counter = 0
38
+
39
+
40
+ def __len__(self):
41
+ return len(self.buffer)
42
+
43
+
44
+ def add(self, t, value):
45
+
46
+ #add the time-value tuple
47
+ self.buffer.append((t, value))
48
+
49
+ #clean up the buffer
50
+ if self.counter > self.clean_every:
51
+ self.counter = 0
52
+ while len(self.buffer) > 1 and t > self.delay + self.buffer[0][0] :
53
+ self.buffer.popleft()
54
+ else:
55
+ self.counter += 1
56
+
57
+
58
+ def get(self, t):
59
+
60
+ #default 0
61
+ if not self.buffer:
62
+ return 0.0
63
+
64
+ #interpolation
65
+ target_time = t - self.delay
66
+
67
+ #requested time too small -> return first value
68
+ if target_time <= self.buffer[0][0]:
69
+ return self.buffer[0][1]
70
+
71
+ #requested time too large -> return last value
72
+ if target_time >= self.buffer[-1][0]:
73
+ return self.buffer[-1][1]
74
+
75
+ #find buffer index for requested time
76
+ i = bisect_left(self.buffer, (target_time,))
77
+ t0, y0 = self.buffer[i-1]
78
+ t1, y1 = self.buffer[i]
79
+
80
+ #linear interpolation
81
+ return y0 + (y1 - y0) * (target_time - t0) / (t1 - t0)
82
+
83
+
84
+ def clear(self):
85
+ #reset everything
86
+ self.buffer = deque()
87
+ self.counter = 0
@@ -0,0 +1,180 @@
1
+ ########################################################################################
2
+ ##
3
+ ## ANDERSON ACCELERATION
4
+ ## (utils/anderson.py)
5
+ ##
6
+ ## Milan Rother 2024
7
+ ##
8
+ ########################################################################################
9
+
10
+ # IMPORTS ==============================================================================
11
+
12
+ import numpy as np
13
+ import matplotlib.pyplot as plt
14
+
15
+
16
+ # CLASS ================================================================================
17
+
18
+ class AndersonAcceleration:
19
+ """
20
+ Class for accelerated fixed-point iteration through anderson acceleration.
21
+ Solves a nonlinear set of equations given in the fixed-point form:
22
+
23
+ x = g(x)
24
+
25
+ Anderson Accelerstion tracks the evolution of the solution from the previous
26
+ iterations. The next step in the iteration is computed as a linear combination
27
+ of the previous iterates. The coefficients are computed to minimize the least
28
+ squares error of the fixed-point problem.
29
+
30
+ INPUTS :
31
+ m : (int) buffer length
32
+ restart : (bool) clear buffer when full
33
+ """
34
+
35
+ def __init__(self, m=1, restart=True):
36
+
37
+ #length of buffer for next estimate
38
+ self.m = m
39
+
40
+ #restart after buffer length is reached?
41
+ self.restart = restart
42
+
43
+ #rolling buffers
44
+ self.x_buffer = []
45
+ self.f_buffer = []
46
+
47
+ #iteration counter for debugging
48
+ self.counter = 0
49
+
50
+
51
+ def reset(self):
52
+ """
53
+ reset the anderson accelerator
54
+ """
55
+
56
+ #clear buffers
57
+ self.x_buffer = []
58
+ self.f_buffer = []
59
+
60
+ #reset iteration counter
61
+ self.counter = 0
62
+
63
+
64
+ def step(self, x, g):
65
+ """
66
+ Perform one iteration on the fixed-point solution.
67
+
68
+ INPUTS :
69
+ x : (float or array) current solution
70
+ g : (float or array) current evaluation of g(x)
71
+ """
72
+
73
+ #increment counter
74
+ self.counter += 1
75
+
76
+ #residual (this gets minimized)
77
+ f = g - x
78
+
79
+ #fallback to regular fpi if 'm == 0'
80
+ if self.m == 0:
81
+ return g, np.linalg.norm(f)
82
+
83
+ #make x vectorial if g is vector
84
+ if np.isscalar(x) and not np.isscalar(g):
85
+ x *= np.ones_like(g)
86
+
87
+ #append to buffer
88
+ self.x_buffer.append(x)
89
+ self.f_buffer.append(f)
90
+
91
+ #total buffer length
92
+ k = len(self.f_buffer)
93
+
94
+ #if no buffer, regular fixed-point update
95
+ if k == 1:
96
+ return g, np.linalg.norm(f)
97
+
98
+ #if buffer size 'm' reached, restart or truncate
99
+ elif self.m is not None and k > self.m + 1:
100
+ if self.restart:
101
+ self.x_buffer = []
102
+ self.f_buffer = []
103
+ return g, np.linalg.norm(f)
104
+ else:
105
+ self.x_buffer.pop(0)
106
+ self.f_buffer.pop(0)
107
+
108
+ #get deltas
109
+ dX = np.diff(self.x_buffer, axis=0)
110
+ dF = np.diff(self.f_buffer, axis=0)
111
+
112
+ #exit for scalar values
113
+ if np.isscalar(f):
114
+
115
+ #delta squared norm
116
+ dF2 = np.linalg.norm(dF)**2
117
+
118
+ #catch division by zero
119
+ if dF2 <= 1e-14:
120
+ return g, abs(f)
121
+
122
+ #new solution and residual
123
+ return x - f * sum(dF * dX) / dF2, abs(f)
124
+
125
+ #compute coefficients from least squares problem
126
+ C, *_ = np.linalg.lstsq(dF.T, f, rcond=None)
127
+
128
+ #new solution and residual
129
+ return x - C @ dX, np.linalg.norm(f)
130
+
131
+
132
+
133
+ class NewtonAndersonAcceleration(AndersonAcceleration):
134
+ """
135
+ Modified class for hybrid anderson acceleration that can use a jacobian 'jac' of
136
+ the function 'g' for a newton step before the fixed point step for the initial
137
+ estimate before applying the anderson acceleration.
138
+
139
+ If a jacobian 'jac' is available, this significantly improves the convergence
140
+ (speed and robustness) of the solution.
141
+ """
142
+
143
+ def _newton(self, x, g, jac):
144
+ """
145
+ Newton step on solution, where 'f=g-x' is the
146
+ residual and 'jac' is the jacobian of 'g'.
147
+ """
148
+
149
+ #compute residual
150
+ f = g - x
151
+
152
+ #early exit for scalar or purely vectorial values
153
+ if np.isscalar(f) or np.ndim(jac) == 1:
154
+ return x - f / (jac - 1.0), abs(f)
155
+
156
+ #vectorial values (newton raphson)
157
+ jac_f = jac - np.eye(len(f))
158
+ return x - np.linalg.solve(jac_f, f), np.linalg.norm(f)
159
+
160
+
161
+ def step(self, x, g, jac=None):
162
+ """
163
+ Perform one iteration on the fixed-point solution.
164
+ If the jacobian of g 'jac' is provided, a newton step
165
+ is performed previous to anderson acceleration.
166
+ """
167
+
168
+ #newton step if jacobian available
169
+ if jac is None:
170
+
171
+ #regular anderson step with residual
172
+ return super().step(x, g)
173
+ else:
174
+ #newton step with residual
175
+ _x, res = self._newton(x, g, jac)
176
+
177
+ #anderson step with no residual
178
+ y, _ = super().step(_x, g)
179
+
180
+ return y, res