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/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .differentiator import *
|
|
2
|
+
from .integrator import *
|
|
3
|
+
from .amplifier import *
|
|
4
|
+
from .function import *
|
|
5
|
+
from .spectrum import *
|
|
6
|
+
from .sources import *
|
|
7
|
+
from .multiplier import *
|
|
8
|
+
from .adder import*
|
|
9
|
+
from .scope import *
|
|
10
|
+
from .delay import *
|
|
11
|
+
from .lti import *
|
|
12
|
+
from .ode import *
|
|
13
|
+
from .rng import *
|
|
14
|
+
|
pathsim/blocks/_block.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## BASE BLOCK
|
|
4
|
+
## (blocks/_block.py)
|
|
5
|
+
##
|
|
6
|
+
## This module defines the base 'Block' class that is the parent
|
|
7
|
+
## to all other blocks and can serve as a base for new or custom blocks
|
|
8
|
+
##
|
|
9
|
+
##
|
|
10
|
+
## Milan Rother 2024
|
|
11
|
+
##
|
|
12
|
+
#########################################################################################
|
|
13
|
+
|
|
14
|
+
# IMPORTS ===============================================================================
|
|
15
|
+
|
|
16
|
+
#for unique identifiers of blocks
|
|
17
|
+
import uuid
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# BASE BLOCK CLASS ======================================================================
|
|
21
|
+
|
|
22
|
+
class Block:
|
|
23
|
+
"""
|
|
24
|
+
Base 'Block' object that defines the inputs, outputs and the connect method.
|
|
25
|
+
|
|
26
|
+
Block interconnections are handeled via the io interface of the blocks.
|
|
27
|
+
It is realized by dicts for the 'inputs' and for the 'outputs', where
|
|
28
|
+
the key of the dict is the input/output channel and the corresponding
|
|
29
|
+
value is the input/output value.
|
|
30
|
+
|
|
31
|
+
NOTE :
|
|
32
|
+
This block is not intended to be used directly and serves as a base
|
|
33
|
+
class definition for other blocks to be inherited.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
|
|
38
|
+
#dicts to hold input and output values
|
|
39
|
+
self.inputs = {0:0.0}
|
|
40
|
+
self.outputs = {0:0.0}
|
|
41
|
+
|
|
42
|
+
#initialize integration engine as 'None' by default
|
|
43
|
+
self.engine = None
|
|
44
|
+
|
|
45
|
+
#unique block identifier
|
|
46
|
+
self.id = uuid.uuid4()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def __str__(self):
|
|
50
|
+
return self.__class__.__name__
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def __len__(self):
|
|
54
|
+
"""
|
|
55
|
+
The '__len__' method of the block is used to compute the length of the
|
|
56
|
+
pass-throuh path of the block. For instant time blocks or blocks with
|
|
57
|
+
pass-through components (adders, amplifiers, etc.) it returns 1,
|
|
58
|
+
otherwise (integrator, buffer, etc.) it returns 0.
|
|
59
|
+
"""
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def __getitem__(self, key):
|
|
64
|
+
"""
|
|
65
|
+
This method is intended to make connection creation more convenient
|
|
66
|
+
and therefore just returns the block itself and the key directly after
|
|
67
|
+
doing some basic checks.
|
|
68
|
+
"""
|
|
69
|
+
if not isinstance(key, int):
|
|
70
|
+
raise ValueError(f"Port has to be of type 'int' but is '{type(key)}'!")
|
|
71
|
+
return (self, key)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def reset(self):
|
|
75
|
+
"""
|
|
76
|
+
Reset the blocks inputs and outputs and also its internal solver, if the
|
|
77
|
+
block has a solver instance.
|
|
78
|
+
"""
|
|
79
|
+
#reset inputs and outputs while maintaining ports
|
|
80
|
+
self.inputs = {k:0.0 for k in sorted(self.inputs.keys())}
|
|
81
|
+
self.outputs = {k:0.0 for k in sorted(self.outputs.keys())}
|
|
82
|
+
|
|
83
|
+
#reset engine if block has solver
|
|
84
|
+
if self.engine is not None:
|
|
85
|
+
self.engine.reset()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# methods for blocks with integration engines ---------------------------------------
|
|
89
|
+
|
|
90
|
+
def set_solver(self, Solver, tolerance_lte=1e-6):
|
|
91
|
+
"""
|
|
92
|
+
Initialize the numerical integration engine with local truncation error
|
|
93
|
+
tolerance if required.
|
|
94
|
+
If the block already has an integration engine, it is changed,
|
|
95
|
+
if it does not require an integration engine, this method just passes.
|
|
96
|
+
"""
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def revert(self):
|
|
101
|
+
"""
|
|
102
|
+
Revert the block to the state of the previous timestep, if the
|
|
103
|
+
block has a solver instance indicated by the 'has_engine' flag.
|
|
104
|
+
This is required for adaptive solvers to revert the state to the
|
|
105
|
+
previous timestep.
|
|
106
|
+
"""
|
|
107
|
+
if self.engine is not None:
|
|
108
|
+
self.engine.revert()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def buffer(self):
|
|
112
|
+
"""
|
|
113
|
+
Buffer current internal state of the block, if the block has
|
|
114
|
+
a solver instance (is stateful).
|
|
115
|
+
This is required for multistage and implicit solvers.
|
|
116
|
+
"""
|
|
117
|
+
if self.engine is not None:
|
|
118
|
+
self.engine.buffer()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# methods for sampling data ---------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def sample(self, t):
|
|
124
|
+
"""
|
|
125
|
+
Samples the data of the blocks inputs or internal state when called.
|
|
126
|
+
This can record block parameters after a succesful timestep such as
|
|
127
|
+
for the 'Scope' and 'Delay' blocks but also for sampling from a random
|
|
128
|
+
distribution in the 'RNG' and the noise blocks.
|
|
129
|
+
"""
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# methods for inter-block data transfer ---------------------------------------------
|
|
134
|
+
|
|
135
|
+
def set(self, port, value):
|
|
136
|
+
"""
|
|
137
|
+
Set the value of an input port of the block.
|
|
138
|
+
"""
|
|
139
|
+
self.inputs[port] = value
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get(self, port):
|
|
143
|
+
"""
|
|
144
|
+
Get the value of an output port of the block.
|
|
145
|
+
Uses the 'get' method of 'outputs' dict with default value '0.0'.
|
|
146
|
+
"""
|
|
147
|
+
return self.outputs.get(port, 0.0)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# methods for block output and state updates ----------------------------------------
|
|
151
|
+
|
|
152
|
+
def update(self, t):
|
|
153
|
+
"""
|
|
154
|
+
The 'update' method is called iteratively for all blocks BEFORE the timestep
|
|
155
|
+
to resolve algebraic loops (fixed-point iteraion).
|
|
156
|
+
|
|
157
|
+
It is meant for instant time blocks (blocks that dont have a delay due to the
|
|
158
|
+
timestep, such as Amplifier, etc.) and updates the 'outputs' of the block
|
|
159
|
+
directly based on the 'inputs' and possibly internal states.
|
|
160
|
+
|
|
161
|
+
It computes and returns the relative difference between the new output and
|
|
162
|
+
the previous output (before the step) to track convergence of the fixed-point
|
|
163
|
+
iteration.
|
|
164
|
+
|
|
165
|
+
RETURNS :
|
|
166
|
+
error : (float) relative error to previous iteration for convergence control
|
|
167
|
+
"""
|
|
168
|
+
return 0.0
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def solve(self, t, dt):
|
|
172
|
+
"""
|
|
173
|
+
The 'solve' method performes one iterative solution step that is required
|
|
174
|
+
to solve the implicit update equation of the solver if an implicit solver
|
|
175
|
+
(numerical integrator) is used.
|
|
176
|
+
|
|
177
|
+
It returns the relative difference between the new updated solution
|
|
178
|
+
and the previous iteration of the solution to track convergence within
|
|
179
|
+
an outer loop.
|
|
180
|
+
|
|
181
|
+
This only has to be implemented by blocks that have an internal
|
|
182
|
+
integration engine with an implicit solver.
|
|
183
|
+
|
|
184
|
+
RETURNS :
|
|
185
|
+
error : (float) solver residual norm
|
|
186
|
+
"""
|
|
187
|
+
return 0.0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def step(self, t, dt):
|
|
191
|
+
"""
|
|
192
|
+
The 'step' method is used in transient simulations and performs an action
|
|
193
|
+
(numeric integration timestep, recording data, etc.) based on the current
|
|
194
|
+
inputs and the current internal state.
|
|
195
|
+
|
|
196
|
+
It performes one timestep for the internal states. For instant time blocks,
|
|
197
|
+
the 'step' method does not has to be implemented specifically.
|
|
198
|
+
|
|
199
|
+
The method handles timestepping for dynamic blocks with internal states
|
|
200
|
+
such as 'Integrator', 'StateSpace', etc.
|
|
201
|
+
|
|
202
|
+
RETURNS :
|
|
203
|
+
success : (bool) step was successful
|
|
204
|
+
error : (float) local truncation error from adaptive integrators
|
|
205
|
+
scale : (float) timestep rescale from adaptive integrators
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
#by default no error estimate
|
|
209
|
+
return True, 0.0, 1.0
|
pathsim/blocks/adder.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## REDUCTION BLOCKS (blocks/adder.py)
|
|
4
|
+
##
|
|
5
|
+
## This module defines static 'Adder' block
|
|
6
|
+
##
|
|
7
|
+
## Milan Rother 2024
|
|
8
|
+
##
|
|
9
|
+
#########################################################################################
|
|
10
|
+
|
|
11
|
+
# IMPORTS ===============================================================================
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from ._block import Block
|
|
16
|
+
|
|
17
|
+
from ..utils.funcs import dict_to_array
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# MISO BLOCKS ===========================================================================
|
|
21
|
+
|
|
22
|
+
class Adder(Block):
|
|
23
|
+
"""
|
|
24
|
+
summs / adds all input signals (MISO)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def update(self, t):
|
|
28
|
+
prev_output = self.outputs[0]
|
|
29
|
+
self.outputs[0] = np.sum(dict_to_array(self.inputs), axis=0)
|
|
30
|
+
return abs(prev_output - self.outputs[0])
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## IDEAL AMPLIFIER BLOCK
|
|
4
|
+
## (blocks/amplifier.py)
|
|
5
|
+
##
|
|
6
|
+
## Milan Rother 2024
|
|
7
|
+
##
|
|
8
|
+
#########################################################################################
|
|
9
|
+
|
|
10
|
+
# IMPORTS ===============================================================================
|
|
11
|
+
|
|
12
|
+
from ._block import Block
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# SISO BLOCKS ===========================================================================
|
|
16
|
+
|
|
17
|
+
class Amplifier(Block):
|
|
18
|
+
"""
|
|
19
|
+
amplifies the input signal by
|
|
20
|
+
multiplication with a constant gain term
|
|
21
|
+
|
|
22
|
+
INPUTS :
|
|
23
|
+
gain : (float) amplifier gain
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, gain=1.0):
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.gain = gain
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def update(self, t):
|
|
32
|
+
prev_output = self.outputs[0]
|
|
33
|
+
self.outputs[0] = self.gain * self.inputs[0]
|
|
34
|
+
return abs(prev_output - self.outputs[0])
|
pathsim/blocks/delay.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## TIME DOMAIN DELAY BLOCK (blocks/delay.py)
|
|
4
|
+
##
|
|
5
|
+
## Milan Rother 2024
|
|
6
|
+
##
|
|
7
|
+
#########################################################################################
|
|
8
|
+
|
|
9
|
+
# IMPORTS ===============================================================================
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from ._block import Block
|
|
14
|
+
from ..utils.adaptivebuffer import AdaptiveBuffer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# BLOCKS ================================================================================
|
|
18
|
+
|
|
19
|
+
class Delay(Block):
|
|
20
|
+
"""
|
|
21
|
+
delays the input signal by a time constant 'tau' in seconds
|
|
22
|
+
using an adaptive rolling buffer
|
|
23
|
+
|
|
24
|
+
INPUTS :
|
|
25
|
+
tau : (float) delay time constant for
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, tau=1e-3):
|
|
29
|
+
super().__init__()
|
|
30
|
+
|
|
31
|
+
#time delay in seconds
|
|
32
|
+
self.tau = tau
|
|
33
|
+
|
|
34
|
+
#create adaptive buffer
|
|
35
|
+
self._buffer = AdaptiveBuffer(self.tau)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def __len__(self):
|
|
39
|
+
#no passthrough by definition
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def reset(self):
|
|
44
|
+
#reset inputs and outputs
|
|
45
|
+
self.inputs = {0:0.0}
|
|
46
|
+
self.outputs = {0:0.0}
|
|
47
|
+
|
|
48
|
+
#clear the buffer
|
|
49
|
+
self._buffer.clear()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def update(self, t):
|
|
53
|
+
"""
|
|
54
|
+
Evaluation of the buffer at different times.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
#retrieve value from buffer
|
|
58
|
+
self.outputs[0] = self._buffer.get(t)
|
|
59
|
+
|
|
60
|
+
return 0.0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def sample(self, t):
|
|
64
|
+
"""
|
|
65
|
+
Sample input values and time of sampling
|
|
66
|
+
and add them to the buffer.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
#add new value to buffer
|
|
70
|
+
self._buffer.add(t, self.inputs[0])
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## DIFFERENTIATOR BLOCK (blocks/differentiator.py)
|
|
4
|
+
##
|
|
5
|
+
## Milan Rother 2024
|
|
6
|
+
##
|
|
7
|
+
#########################################################################################
|
|
8
|
+
|
|
9
|
+
# IMPORTS ===============================================================================
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from ._block import Block
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# BLOCKS ================================================================================
|
|
17
|
+
|
|
18
|
+
class Differentiator(Block):
|
|
19
|
+
"""
|
|
20
|
+
Differentiates the input signal (SISO) using a first order transfer function
|
|
21
|
+
with a pole at the origin which implements a high pass filter.
|
|
22
|
+
|
|
23
|
+
H_diff(s) = s / (1 + s/f_max)
|
|
24
|
+
|
|
25
|
+
The approximation holds for signals up to a frequency of approximately f_max.
|
|
26
|
+
|
|
27
|
+
NOTE :
|
|
28
|
+
Depending on 'f_max', the resulting system might become stiff or ill conditioned!
|
|
29
|
+
As a practical choice set f_max to 3x the highest expected signal frequency.
|
|
30
|
+
|
|
31
|
+
INPUTS :
|
|
32
|
+
f_max : (float) highest expected signal frequency
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, f_max=1e2):
|
|
36
|
+
super().__init__()
|
|
37
|
+
|
|
38
|
+
#maximum frequency for differentiator approximation
|
|
39
|
+
self.f_max = f_max
|
|
40
|
+
|
|
41
|
+
def __len__(self):
|
|
42
|
+
return 1
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def set_solver(self, Solver, tolerance_lte=1e-6):
|
|
46
|
+
#change solver if already initialized
|
|
47
|
+
if self.engine is not None:
|
|
48
|
+
self.engine = self.engine.change(Solver, tolerance_lte)
|
|
49
|
+
return #quit early
|
|
50
|
+
#initialize the numerical integration engine with kernel
|
|
51
|
+
def _f(x, u, t): return - self.f_max * (x - u)
|
|
52
|
+
def _jac(x, u, t): return - self.f_max
|
|
53
|
+
self.engine = Solver(0.0, _f, _jac, tolerance_lte)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def update(self, t):
|
|
57
|
+
#compute implicit balancing update
|
|
58
|
+
prev_output = self.outputs[0]
|
|
59
|
+
self.outputs[0] = -self.f_max * (self.engine.get() - self.inputs[0])
|
|
60
|
+
return abs(prev_output - self.outputs[0])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def solve(self, t, dt):
|
|
64
|
+
#advance solution of implicit update equation
|
|
65
|
+
return self.engine.solve(self.inputs[0], t, dt)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def step(self, t, dt):
|
|
69
|
+
#compute update step with integration engine
|
|
70
|
+
return self.engine.step(self.inputs[0], t, dt)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## GENERIC MIMO FUNCTION BLOCK (blocks/function.py)
|
|
4
|
+
##
|
|
5
|
+
## Milan Rother 2024
|
|
6
|
+
##
|
|
7
|
+
#########################################################################################
|
|
8
|
+
|
|
9
|
+
# IMPORTS ===============================================================================
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from ._block import Block
|
|
14
|
+
|
|
15
|
+
from ..utils.funcs import (
|
|
16
|
+
max_error_dicts,
|
|
17
|
+
array_to_dict,
|
|
18
|
+
dict_to_array
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# MIMO BLOCKS ===========================================================================
|
|
23
|
+
|
|
24
|
+
class Function(Block):
|
|
25
|
+
"""
|
|
26
|
+
Arbitrary MIMO function block, defined by a callable object,
|
|
27
|
+
i.e. function or lambda expression.
|
|
28
|
+
|
|
29
|
+
The function can have multiple arguments that are then provided
|
|
30
|
+
by the input channels of the function block.
|
|
31
|
+
|
|
32
|
+
Form multi input, the function has to specify multiple arguments
|
|
33
|
+
and for multi output, the aoutputs have to be provided as a
|
|
34
|
+
tuple or list.
|
|
35
|
+
|
|
36
|
+
INPUTS :
|
|
37
|
+
func : (callable) MIMO function that defines block IO behaviour
|
|
38
|
+
|
|
39
|
+
NOTE :
|
|
40
|
+
If the outputs are provided as a single numpy array,
|
|
41
|
+
they are considered a single output
|
|
42
|
+
|
|
43
|
+
EXAMPLE :
|
|
44
|
+
consider the function:
|
|
45
|
+
func = lambda a, b, c : (a**2, a*b, b/c)
|
|
46
|
+
then the input channels of the block are assigned
|
|
47
|
+
to the function arguments following this scheme:
|
|
48
|
+
inputs[0] -> a
|
|
49
|
+
inputs[1] -> b
|
|
50
|
+
inputs[2] -> c
|
|
51
|
+
and the function outputs are assigned to the
|
|
52
|
+
output channels of the block in the same way:
|
|
53
|
+
a**2 -> outputs[0]
|
|
54
|
+
a*b -> outputs[1]
|
|
55
|
+
b/c -> outputs[2]
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, func=lambda x: x):
|
|
59
|
+
super().__init__()
|
|
60
|
+
|
|
61
|
+
#some checks to ensure that function works correctly
|
|
62
|
+
if not callable(func):
|
|
63
|
+
raise ValueError(f"'{func}' is not callable")
|
|
64
|
+
|
|
65
|
+
#function defining the block update
|
|
66
|
+
self.func = func
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def update(self, t):
|
|
70
|
+
|
|
71
|
+
#compute function output
|
|
72
|
+
output = self.func(*dict_to_array(self.inputs))
|
|
73
|
+
|
|
74
|
+
#check if the output is scalar
|
|
75
|
+
if np.isscalar(output):
|
|
76
|
+
prev_output = self.outputs[0]
|
|
77
|
+
self.outputs[0] = output
|
|
78
|
+
return abs(prev_output - self.outputs[0])
|
|
79
|
+
else:
|
|
80
|
+
prev_outputs = self.outputs.copy()
|
|
81
|
+
self.outputs = array_to_dict(output)
|
|
82
|
+
return max_error_dicts(prev_outputs, self.outputs)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## STANDARD INTEGRATOR BLOCK
|
|
4
|
+
## (blocks/integrator.py)
|
|
5
|
+
##
|
|
6
|
+
## Milan Rother 2024
|
|
7
|
+
##
|
|
8
|
+
#########################################################################################
|
|
9
|
+
|
|
10
|
+
# IMPORTS ===============================================================================
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from ._block import Block
|
|
15
|
+
|
|
16
|
+
from ..utils.funcs import (
|
|
17
|
+
dict_to_array,
|
|
18
|
+
array_to_dict
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# BLOCKS ================================================================================
|
|
23
|
+
|
|
24
|
+
class Integrator(Block):
|
|
25
|
+
"""
|
|
26
|
+
Integrates the input signal using a numerical integration engine.
|
|
27
|
+
The block is inherently MIMO capable.
|
|
28
|
+
|
|
29
|
+
INPUTS :
|
|
30
|
+
initial_value : (float or array) initial value of integrator
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, initial_value=0.0):
|
|
34
|
+
super().__init__()
|
|
35
|
+
|
|
36
|
+
#save initial value
|
|
37
|
+
self.initial_value = initial_value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def __len__(self):
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def set_solver(self, Solver, tolerance_lte=1e-6):
|
|
45
|
+
#change solver if already initialized
|
|
46
|
+
if self.engine is not None:
|
|
47
|
+
self.engine = self.engine.change(Solver, tolerance_lte)
|
|
48
|
+
return #quit early
|
|
49
|
+
#initialize the integration engine
|
|
50
|
+
def _f(x, u, t): return u
|
|
51
|
+
self.engine = Solver(self.initial_value, _f, None, tolerance_lte)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def update(self, t):
|
|
55
|
+
self.outputs = array_to_dict(self.engine.get())
|
|
56
|
+
return 0.0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def solve(self, t, dt):
|
|
60
|
+
#advance solution of implicit update equation and update block outputs
|
|
61
|
+
return self.engine.solve(dict_to_array(self.inputs), t, dt)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def step(self, t, dt):
|
|
65
|
+
#compute update step with integration engine and update block outputs
|
|
66
|
+
return self.engine.step(dict_to_array(self.inputs), t, dt)
|