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,163 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## SPECIAL RF SOURCES
|
|
4
|
+
## (blocks/rf/sources.py)
|
|
5
|
+
##
|
|
6
|
+
## this module implements some premade source blocks
|
|
7
|
+
## that produce waveforms for RF simulations
|
|
8
|
+
##
|
|
9
|
+
## Milan Rother 2024
|
|
10
|
+
##
|
|
11
|
+
#########################################################################################
|
|
12
|
+
|
|
13
|
+
# IMPORTS ===============================================================================
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from .._block import Block
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# HELPER FUNCTIONS ======================================================================
|
|
21
|
+
|
|
22
|
+
def gaussian(t, f_max):
|
|
23
|
+
"""
|
|
24
|
+
gaussian pulse with its maximum at t=0
|
|
25
|
+
"""
|
|
26
|
+
tau = 0.5 / f_max
|
|
27
|
+
return np.exp(-(t/tau)**2)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def triangle_wave(t, f):
|
|
31
|
+
"""
|
|
32
|
+
triangle wave with amplitude '1' and frequency 'f'
|
|
33
|
+
"""
|
|
34
|
+
return 2 * abs(t*f - np.floor(t*f + 0.5)) - 1
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def square_wave(t, f):
|
|
38
|
+
"""
|
|
39
|
+
square wave with amplitude '1' and frequency 'f'
|
|
40
|
+
"""
|
|
41
|
+
return np.sign(np.sin(2*np.pi*f*t))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# SOURCE BLOCKS =========================================================================
|
|
45
|
+
|
|
46
|
+
class SquareWaveSource(Block):
|
|
47
|
+
|
|
48
|
+
def __init__(self, frequency=1, amplitude=1):
|
|
49
|
+
super().__init__()
|
|
50
|
+
|
|
51
|
+
self.amplitude = amplitude
|
|
52
|
+
self.frequency = frequency
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def update(self, t):
|
|
56
|
+
self.outputs[0] = self.amplitude * square_wave(t, self.frequency)
|
|
57
|
+
return 0.0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TriangleWaveSource(Block):
|
|
61
|
+
|
|
62
|
+
def __init__(self, frequency=1, amplitude=1):
|
|
63
|
+
super().__init__()
|
|
64
|
+
|
|
65
|
+
self.amplitude = amplitude
|
|
66
|
+
self.frequency = frequency
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def update(self, t):
|
|
70
|
+
self.outputs[0] = self.amplitude * triangle_wave(t, self.frequency)
|
|
71
|
+
return 0.0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SinusoidalSource(Block):
|
|
75
|
+
|
|
76
|
+
def __init__(self, frequency=1, amplitude=1, phase=0):
|
|
77
|
+
super().__init__()
|
|
78
|
+
|
|
79
|
+
self.amplitude = amplitude
|
|
80
|
+
self.frequency = frequency
|
|
81
|
+
self.phase = phase
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def update(self, t):
|
|
85
|
+
omega = 2*np.pi*self.frequency
|
|
86
|
+
self.outputs[0] = self.amplitude * np.sin(omega*t + self.phase)
|
|
87
|
+
return 0.0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class GaussianPulseSource(Block):
|
|
91
|
+
|
|
92
|
+
def __init__(self, amplitude=1, f_max=1e3, tau=0.0):
|
|
93
|
+
super().__init__()
|
|
94
|
+
|
|
95
|
+
self.amplitude = amplitude
|
|
96
|
+
self.f_max = f_max
|
|
97
|
+
self.tau = tau
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def update(self, t):
|
|
101
|
+
self.outputs[0] = self.amplitude * gaussian(t-self.tau, self.f_max)
|
|
102
|
+
return 0.0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class StepSource(Block):
|
|
106
|
+
|
|
107
|
+
def __init__(self, amplitude=1, tau=0.0):
|
|
108
|
+
super().__init__()
|
|
109
|
+
|
|
110
|
+
self.amplitude = amplitude
|
|
111
|
+
self.tau = tau
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def update(self, t):
|
|
115
|
+
self.outputs[0] = self.amplitude * float(t > self.tau)
|
|
116
|
+
return 0.0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ChirpSource(Block):
|
|
120
|
+
|
|
121
|
+
def __init__(self, amplitude=1, f0=1, BW=1, T=1):
|
|
122
|
+
super().__init__()
|
|
123
|
+
|
|
124
|
+
#parameters of chirp signal
|
|
125
|
+
self.amplitude = amplitude
|
|
126
|
+
self.f0 = f0
|
|
127
|
+
self.BW = BW
|
|
128
|
+
self.T = T
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def set_solver(self, Solver, tolerance_lte=1e-6):
|
|
132
|
+
|
|
133
|
+
#change solver if already initialized
|
|
134
|
+
if self.engine is not None:
|
|
135
|
+
self.engine = self.engine.change(Solver, tolerance_lte)
|
|
136
|
+
return #quit early
|
|
137
|
+
|
|
138
|
+
#initialize the numerical integration engine with kernel
|
|
139
|
+
def _f(x, u, t): return self.BW * (1 + triangle_wave(t, 1/self.T))/2
|
|
140
|
+
self.engine = Solver(self.f0, _f, None, tolerance_lte)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def update(self, t):
|
|
144
|
+
#compute implicit balancing update
|
|
145
|
+
phase = 2 * np.pi * self.engine.get()
|
|
146
|
+
self.outputs[0] = self.amplitude * np.sin(phase)
|
|
147
|
+
return 0.0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def solve(self, t, dt):
|
|
151
|
+
#advance solution of implicit update equation
|
|
152
|
+
self.engine.solve(0.0, t, dt)
|
|
153
|
+
|
|
154
|
+
#no error for chirp source
|
|
155
|
+
return 0.0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def step(self, t, dt):
|
|
159
|
+
#compute update step with integration engine
|
|
160
|
+
self.engine.step(0.0, t, dt)
|
|
161
|
+
|
|
162
|
+
#no error control for chirp source
|
|
163
|
+
return True, 0.0, 1.0
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## WIENER AND HAMMERSTEIN NONLINEAR DYNAMICAL MODELS
|
|
4
|
+
## (blocks/wienerhammerstein.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
|
+
max_error_dicts,
|
|
18
|
+
dict_to_array,
|
|
19
|
+
array_to_dict,
|
|
20
|
+
auto_jacobian
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from ...utils.gilbert import (
|
|
24
|
+
gilbert_realization
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# BLOCKS ================================================================================
|
|
29
|
+
|
|
30
|
+
class WienerHammersteinModel(Block):
|
|
31
|
+
"""
|
|
32
|
+
Wiener-Hammerstein nonlinear dynamical model.
|
|
33
|
+
It consists of a dynamical LTI system, followed
|
|
34
|
+
by a static nonlinearity, followed by another
|
|
35
|
+
dynamical LTI system.
|
|
36
|
+
|
|
37
|
+
u -> LTI_1 -> f(.) -> LTI_2 -> y
|
|
38
|
+
|
|
39
|
+
The LTI systems are implemented as linear statespace
|
|
40
|
+
models with ABCD system matrices.
|
|
41
|
+
The system matrices are realized from the poles, residues
|
|
42
|
+
and constant of the corresponding transfer functions.
|
|
43
|
+
|
|
44
|
+
Can serve as a behavioral model for nonlinear
|
|
45
|
+
dynamical systems such as RF components.
|
|
46
|
+
|
|
47
|
+
This implementation is inherently MIMO-capable
|
|
48
|
+
if the IO dimensions of the two LTI systems and
|
|
49
|
+
the nonlinearity match.
|
|
50
|
+
|
|
51
|
+
INPUTS :
|
|
52
|
+
Poles_1 : (array of complex) poles of 1st LTI system transfer function
|
|
53
|
+
Residues_1 : (array of arrays) residues of 1st LTI system transfer function
|
|
54
|
+
Const_1 : (array of arrays) constant term of 1st LTI system transfer function
|
|
55
|
+
Poles_2 : (array of complex) poles of 2nd LTI system transfer function
|
|
56
|
+
Residues_2 : (array of arrays) residues of 2nd LTI system transfer function
|
|
57
|
+
Const_2 : (array of arrays) constant term of 2nd LTI system transfer function
|
|
58
|
+
func : (callable) function that defines the system nonlienarity, can be MIMO
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self,
|
|
63
|
+
Poles_1=[],
|
|
64
|
+
Residues_1=[],
|
|
65
|
+
Const_1=1.0,
|
|
66
|
+
Poles_2=[],
|
|
67
|
+
Residues_2=[],
|
|
68
|
+
Const_2=1.0,
|
|
69
|
+
func=lambda x: x,
|
|
70
|
+
jac=None):
|
|
71
|
+
|
|
72
|
+
super().__init__()
|
|
73
|
+
|
|
74
|
+
#nonlinearity
|
|
75
|
+
self.func = func
|
|
76
|
+
|
|
77
|
+
#jacobian of nonlinearity
|
|
78
|
+
self.jac = auto_jacobian(self.func) if jac is None else jac
|
|
79
|
+
|
|
80
|
+
#Statespace realization of 1st LTI transfer function
|
|
81
|
+
self.A_1, self.B_1, self.C_1, self.D_1 = gilbert_realization(Poles_1, Residues_1, Const_1)
|
|
82
|
+
|
|
83
|
+
#Statespace realization of 2nd LTI transfer function
|
|
84
|
+
self.A_2, self.B_2, self.C_2, self.D_2 = gilbert_realization(Poles_2, Residues_2, Const_2)
|
|
85
|
+
|
|
86
|
+
#get statespace dimensions after realization
|
|
87
|
+
_, n_in = self.B_1.shape
|
|
88
|
+
n_out, _ = self.C_2.shape
|
|
89
|
+
|
|
90
|
+
self.n_1, _ = self.A_1.shape
|
|
91
|
+
self.n_2, _ = self.A_2.shape
|
|
92
|
+
|
|
93
|
+
#set io channels
|
|
94
|
+
self.inputs = {i:0.0 for i in range(n_in)}
|
|
95
|
+
self.outputs = {i:0.0 for i in range(n_out)}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def __len__(self):
|
|
99
|
+
#check if direct passthrough exists
|
|
100
|
+
return int((np.any(self.D_1) and np.any(self.D_2)))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def set_solver(self, Solver, tolerance_lte=1e-6):
|
|
104
|
+
|
|
105
|
+
#change solver if already initialized
|
|
106
|
+
if self.engine is not None:
|
|
107
|
+
self.engine = self.engine.change(Solver, tolerance_lte)
|
|
108
|
+
return #quit early
|
|
109
|
+
|
|
110
|
+
#right hand side function for ODE
|
|
111
|
+
def _f(x, u, t):
|
|
112
|
+
x_1, x_2 = x[:self.n_1], x[self.n_1:]
|
|
113
|
+
dx_1 = np.dot(self.A_1, x_1) + np.dot(self.B_1, u)
|
|
114
|
+
dx_2 = np.dot(self.A_2, x_2) + np.dot(self.B_2, self.func(np.dot(self.C_1, x_1) + np.dot(self.D_1, u)))
|
|
115
|
+
return np.hstack([dx_1, dx_2])
|
|
116
|
+
|
|
117
|
+
def _jac(x, u, t):
|
|
118
|
+
x_1, x_2 = x[:self.n_1], x[self.n_1:]
|
|
119
|
+
J_12 = np.zeros_like(self.A_1)
|
|
120
|
+
J_21 = np.dot(self.B_2, np.dot(self.C_1, self.jac(np.dot(self.C_1, x_1) + np.dot(self.D_1, u))))
|
|
121
|
+
return np.block([[self.A_1, J_12], [J_21, self.A_2]])
|
|
122
|
+
|
|
123
|
+
#combined solver
|
|
124
|
+
self.engine = Solver(np.zeros(self.n_1+self.n_2), _f, _jac, tolerance_lte)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def update(self, t):
|
|
128
|
+
#compute implicit balancing update
|
|
129
|
+
prev_outputs = self.outputs.copy()
|
|
130
|
+
|
|
131
|
+
x = self.engine.get()
|
|
132
|
+
x_1, x_2 = x[:self.n_1], x[self.n_1:]
|
|
133
|
+
|
|
134
|
+
u = dict_to_array(self.inputs)
|
|
135
|
+
|
|
136
|
+
y_1 = np.dot(self.C_1, x_1) + np.dot(self.D_1, u)
|
|
137
|
+
y_2 = np.dot(self.C_2, x_2) + np.dot(self.D_2, self.func(y_1))
|
|
138
|
+
|
|
139
|
+
self.outputs = array_to_dict(y_2)
|
|
140
|
+
return max_error_dicts(prev_outputs, self.outputs)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def solve(self, t, dt):
|
|
144
|
+
#advance solution of implicit update equation
|
|
145
|
+
return self.engine.solve(dict_to_array(self.inputs), t, dt)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def step(self, t, dt):
|
|
149
|
+
#compute update step with integration engine
|
|
150
|
+
return self.engine.step(dict_to_array(self.inputs), t, dt)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class HammersteinModel(Block):
|
|
158
|
+
"""
|
|
159
|
+
Hammerstein nonlinear dynamical model. It consists of a
|
|
160
|
+
dynamical LTI system, followed by a static nonlinearity.
|
|
161
|
+
|
|
162
|
+
u -> f(.) -> LTI -> y
|
|
163
|
+
|
|
164
|
+
The LTI system is implemented as a linear statespace
|
|
165
|
+
model with ABCD system matrices.
|
|
166
|
+
The system matrices are realized from the poles, residues
|
|
167
|
+
and constant of the corresponding transfer function.
|
|
168
|
+
|
|
169
|
+
Can serve as a behavioral model for nonlinear
|
|
170
|
+
dynamical systems such as RF components.
|
|
171
|
+
|
|
172
|
+
This implementation is inherently MIMO-capable
|
|
173
|
+
if the IO dimensions of the LTI system and
|
|
174
|
+
the nonlinearity match.
|
|
175
|
+
|
|
176
|
+
INPUTS :
|
|
177
|
+
Poles : (array of complex) poles of LTI system transfer function
|
|
178
|
+
Residues : (array of arrays) residues of LTI system transfer function
|
|
179
|
+
Const : (array of arrays) constant term of LTI system transfer function
|
|
180
|
+
func : (callable) function that defines the system nonlienarity, can be MIMO
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def __init__(self,
|
|
184
|
+
Poles=[],
|
|
185
|
+
Residues=[],
|
|
186
|
+
Const=1.0,
|
|
187
|
+
func=lambda x: x):
|
|
188
|
+
|
|
189
|
+
super().__init__()
|
|
190
|
+
|
|
191
|
+
#nonlinearity
|
|
192
|
+
self.func = func
|
|
193
|
+
|
|
194
|
+
#Statespace realization of transfer function
|
|
195
|
+
self.A, self.B, self.C, self.D = gilbert_realization(Poles, Residues, Const)
|
|
196
|
+
|
|
197
|
+
#get statespace dimensions after realization
|
|
198
|
+
_, n_in = self.B.shape
|
|
199
|
+
n_out, _ = self.C.shape
|
|
200
|
+
|
|
201
|
+
#set io channels
|
|
202
|
+
self.inputs = {i:0.0 for i in range(n_in)}
|
|
203
|
+
self.outputs = {i:0.0 for i in range(n_out)}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def __len__(self):
|
|
207
|
+
#check if direct passthrough exists
|
|
208
|
+
return int(np.any(self.D))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def set_solver(self, Solver, tolerance_lte=1e-6):
|
|
212
|
+
|
|
213
|
+
#change solver if already initialized
|
|
214
|
+
if self.engine is not None:
|
|
215
|
+
self.engine = self.engine.change(Solver, tolerance_lte)
|
|
216
|
+
return #quit early
|
|
217
|
+
|
|
218
|
+
#right hand side function for ODE
|
|
219
|
+
def _f(x, u, t): return np.dot(self.A, x) + np.dot(self.B, u)
|
|
220
|
+
def _jac(x, u, t): return self.A
|
|
221
|
+
|
|
222
|
+
#solver
|
|
223
|
+
n, _ = self.A.shape
|
|
224
|
+
self.engine = Solver(np.zeros(n), _f, _jac, tolerance_lte)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def update(self, t):
|
|
228
|
+
#compute implicit balancing update
|
|
229
|
+
prev_outputs = self.outputs.copy()
|
|
230
|
+
u = dict_to_array(self.inputs)
|
|
231
|
+
y = np.dot(self.C, self.engine.get()) + np.dot(self.D, self.func(u))
|
|
232
|
+
self.outputs = array_to_dict(y)
|
|
233
|
+
return max_error_dicts(prev_outputs, self.outputs)
|
|
234
|
+
|
|
235
|
+
def solve(self, t, dt):
|
|
236
|
+
#advance solution of implicit update equation
|
|
237
|
+
u = dict_to_array(self.inputs)
|
|
238
|
+
return self.engine.solve(self.func(u), t, dt)
|
|
239
|
+
|
|
240
|
+
def step(self, t, dt):
|
|
241
|
+
#compute update step with integration engine
|
|
242
|
+
u = dict_to_array(self.inputs)
|
|
243
|
+
return self.engine.step(self.func(u), t, dt)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class WienerModel(Block):
|
|
252
|
+
|
|
253
|
+
"""
|
|
254
|
+
Wiener nonlinear dynamical model. It consists of
|
|
255
|
+
a static nonlinearity, followed by a dynamical LTI system.
|
|
256
|
+
|
|
257
|
+
u -> LTI -> f(.) -> y
|
|
258
|
+
|
|
259
|
+
The LTI system is implemented as a linear statespace
|
|
260
|
+
model with ABCD system matrices.
|
|
261
|
+
The system matrices are realized from the poles, residues
|
|
262
|
+
and constant of the corresponding transfer function.
|
|
263
|
+
|
|
264
|
+
Can serve as a behavioral model for nonlinear
|
|
265
|
+
dynamical systems such as RF components.
|
|
266
|
+
|
|
267
|
+
This implementation is inherently MIMO-capable
|
|
268
|
+
if the IO dimensions of the LTI system and
|
|
269
|
+
the nonlinearity match.
|
|
270
|
+
|
|
271
|
+
INPUTS :
|
|
272
|
+
Poles : (array of complex) poles of LTI system transfer function
|
|
273
|
+
Residues : (array of arrays) residues of LTI system transfer function
|
|
274
|
+
Const : (array of arrays) constant term of LTI system transfer function
|
|
275
|
+
func : (callable) function that defines the system nonlienarity, can be MIMO
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
def __init__(self,
|
|
279
|
+
b=None,
|
|
280
|
+
a=None,
|
|
281
|
+
Poles=[],
|
|
282
|
+
Residues=[],
|
|
283
|
+
Const=1.0,
|
|
284
|
+
func=lambda x: x):
|
|
285
|
+
|
|
286
|
+
super().__init__()
|
|
287
|
+
|
|
288
|
+
#nonlinearity
|
|
289
|
+
self.func = func
|
|
290
|
+
|
|
291
|
+
#Statespace realization of transfer function
|
|
292
|
+
self.A, self.B, self.C, self.D = gilbert_realization(Poles, Residues, Const)
|
|
293
|
+
|
|
294
|
+
#get statespace dimensions after realization
|
|
295
|
+
_, n_in = self.B.shape
|
|
296
|
+
n_out, _ = self.C.shape
|
|
297
|
+
|
|
298
|
+
#set io channels
|
|
299
|
+
self.inputs = {i:0.0 for i in range(n_in)}
|
|
300
|
+
self.outputs = {i:0.0 for i in range(n_out)}
|
|
301
|
+
|
|
302
|
+
def __len__(self):
|
|
303
|
+
#check if direct passthrough exists
|
|
304
|
+
return int(np.any(self.D))
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def set_solver(self, Solver, tolerance_lte=1e-6):
|
|
308
|
+
|
|
309
|
+
#change solver if already initialized
|
|
310
|
+
if self.engine is not None:
|
|
311
|
+
self.engine = self.engine.change(Solver, tolerance_lte)
|
|
312
|
+
return #quit early
|
|
313
|
+
|
|
314
|
+
#right hand side function for ODE
|
|
315
|
+
def _f(x, u, t): return np.dot(self.A, x) + np.dot(self.B, u)
|
|
316
|
+
def _jac(x, u, t): return self.A
|
|
317
|
+
|
|
318
|
+
#solver
|
|
319
|
+
n, _ = self.A.shape
|
|
320
|
+
self.engine = Solver(np.zeros(n), _f, _jac, tolerance_lte)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def update(self, t):
|
|
324
|
+
#compute implicit balancing update
|
|
325
|
+
prev_outputs = self.outputs.copy()
|
|
326
|
+
y = np.dot(self.C, self.engine.get()) + np.dot(self.D, dict_to_array(self.inputs))
|
|
327
|
+
self.outputs = array_to_dict(self.func(y))
|
|
328
|
+
return max_error_dicts(prev_outputs, self.outputs)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def solve(self, t, dt):
|
|
332
|
+
#advance solution of implicit update equation
|
|
333
|
+
return self.engine.solve(dict_to_array(self.inputs), t, dt)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def step(self, t, dt):
|
|
337
|
+
#compute update step with integration engine
|
|
338
|
+
return self.engine.step(dict_to_array(self.inputs), t, dt)
|
pathsim/blocks/rng.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## RANDOM NUMBER GENERATOR BLOCK (rng.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
|
+
# NOISE SOURCE BLOCKS ===================================================================
|
|
17
|
+
|
|
18
|
+
class RNG(Block):
|
|
19
|
+
"""
|
|
20
|
+
Generates a random output value beteween -1 and 1
|
|
21
|
+
from a uniform distribution.
|
|
22
|
+
|
|
23
|
+
If no 'sampling_rate' (None) is specified, every
|
|
24
|
+
simulation timestep gets a random value.
|
|
25
|
+
|
|
26
|
+
INPUTS :
|
|
27
|
+
sampling_rate : (float or None) number of samples per second
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, sampling_rate=None):
|
|
31
|
+
super().__init__()
|
|
32
|
+
|
|
33
|
+
self.sampling_rate = sampling_rate
|
|
34
|
+
self.n_samples = 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def __len__(self):
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def reset(self):
|
|
42
|
+
#reset inputs and outputs
|
|
43
|
+
self.inputs = {0:0.0}
|
|
44
|
+
self.outputs = {0:0.0}
|
|
45
|
+
|
|
46
|
+
#reset noise samples
|
|
47
|
+
self.n_samples = 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def sample(self, t):
|
|
51
|
+
"""
|
|
52
|
+
Sample from a normal distribution after successful timestep.
|
|
53
|
+
"""
|
|
54
|
+
if (self.sampling_rate is None or
|
|
55
|
+
self.n_samples < t * self.sampling_rate):
|
|
56
|
+
self.outputs[0] = 2.0*np.random.rand() - 1.0
|
|
57
|
+
self.n_samples += 1
|