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