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/blocks/lti.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## LINEAR TIME INVARIANT DYNAMICAL BLOCKS (blocks/lti.py)
|
|
4
|
+
##
|
|
5
|
+
## This module defines linear time invariant dynamical blocks
|
|
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 (
|
|
18
|
+
max_error_dicts,
|
|
19
|
+
dict_to_array,
|
|
20
|
+
array_to_dict
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from ..utils.gilbert import (
|
|
24
|
+
gilbert_realization
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# LTI BLOCKS ============================================================================
|
|
29
|
+
|
|
30
|
+
class StateSpace(Block):
|
|
31
|
+
"""
|
|
32
|
+
This block integrates a LTI MIMO state space model with the structure
|
|
33
|
+
|
|
34
|
+
d/dt x = A x + B u
|
|
35
|
+
y = C x + D u
|
|
36
|
+
|
|
37
|
+
where A, B, C and D are the state space matrices, x is the state,
|
|
38
|
+
u the input and y the output vector.
|
|
39
|
+
|
|
40
|
+
INPUTS :
|
|
41
|
+
A, B, C, D : (numpy arrays) state space matrices
|
|
42
|
+
initial_value : (array of floars) initial state / initial condition
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self,
|
|
46
|
+
A=-1.0, B=1.0, C=-1.0, D=1.0,
|
|
47
|
+
initial_value=None):
|
|
48
|
+
super().__init__()
|
|
49
|
+
|
|
50
|
+
#statespace matrices with input shape validation
|
|
51
|
+
self.A = np.atleast_2d(A)
|
|
52
|
+
self.B = np.atleast_1d(B)
|
|
53
|
+
self.C = np.atleast_1d(C)
|
|
54
|
+
self.D = np.atleast_1d(D)
|
|
55
|
+
|
|
56
|
+
#get statespace dimensions
|
|
57
|
+
n, _ = self.A.shape
|
|
58
|
+
if self.B.ndim == 1: n_in = 1
|
|
59
|
+
else: _, n_in = self.B.shape
|
|
60
|
+
if self.C.ndim == 1: n_out = 1
|
|
61
|
+
else: n_out, _ = self.C.shape
|
|
62
|
+
|
|
63
|
+
#set io channels
|
|
64
|
+
self.inputs = {i:0.0 for i in range(n_in)}
|
|
65
|
+
self.outputs = {i:0.0 for i in range(n_out)}
|
|
66
|
+
|
|
67
|
+
#initial condition
|
|
68
|
+
self.initial_value = np.zeros(n) if initial_value is None else initial_value
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def __len__(self):
|
|
72
|
+
#check if direct passthrough exists
|
|
73
|
+
return int(np.any(self.D))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def set_solver(self, Solver, tolerance_lte=1e-6):
|
|
77
|
+
|
|
78
|
+
if self.engine is None:
|
|
79
|
+
|
|
80
|
+
def _f(x, u, t):
|
|
81
|
+
return np.dot(self.A, x) + np.dot(self.B, u)
|
|
82
|
+
def _jac(x, u, t):
|
|
83
|
+
return self.A
|
|
84
|
+
|
|
85
|
+
#initialize the integration engine with right hand side
|
|
86
|
+
self.engine = Solver(self.initial_value, _f, _jac, tolerance_lte)
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
|
|
90
|
+
#change solver if already initialized
|
|
91
|
+
self.engine = self.engine.change(Solver, tolerance_lte)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def update(self, t):
|
|
95
|
+
#compute implicit balancing update
|
|
96
|
+
prev_outputs = self.outputs.copy()
|
|
97
|
+
u = dict_to_array(self.inputs)
|
|
98
|
+
y = np.dot(self.C, self.engine.get()) + np.dot(self.D, u)
|
|
99
|
+
self.outputs = array_to_dict(y)
|
|
100
|
+
return max_error_dicts(prev_outputs, self.outputs)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def solve(self, t, dt):
|
|
104
|
+
#advance solution of implicit update equation and update outputs
|
|
105
|
+
return self.engine.solve(dict_to_array(self.inputs), t, dt)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def step(self, t, dt):
|
|
109
|
+
#compute update step with integration engine and update outputs
|
|
110
|
+
return self.engine.step(dict_to_array(self.inputs), t, dt)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TransferFunction(StateSpace):
|
|
114
|
+
"""
|
|
115
|
+
This block integrates a LTI (MIMO for pole residue) transfer function.
|
|
116
|
+
|
|
117
|
+
The transfer function is defined in pole-residue form
|
|
118
|
+
|
|
119
|
+
H(s) = Const + sum( Residues / (s - Poles) )
|
|
120
|
+
|
|
121
|
+
where 'Poles' are the scalar poles of the transfer function and
|
|
122
|
+
'Residues' are the possibly matrix valued (in MIMO case) residues of
|
|
123
|
+
the transfer function. 'Const' has same shape as 'Residues'.
|
|
124
|
+
|
|
125
|
+
Upon initialization, the state space realization of the transfer
|
|
126
|
+
function is computed using a minimal gilbert realization.
|
|
127
|
+
|
|
128
|
+
The resulting statespace model of the form
|
|
129
|
+
|
|
130
|
+
d/dt x = A x + B u
|
|
131
|
+
y = C x + D u
|
|
132
|
+
|
|
133
|
+
is handled the same as the 'StateSpace' block, where A, B, C and D
|
|
134
|
+
are the state space matrices, x is the internal state, u the input and
|
|
135
|
+
y the output vector.
|
|
136
|
+
|
|
137
|
+
INPUTS :
|
|
138
|
+
Poles : (list or array of scalars) transfer function poles
|
|
139
|
+
Residues : (list or array of scalars or arrays) transfer function residues
|
|
140
|
+
Const : (scalar or array) constant term of transfer function
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(self,
|
|
144
|
+
Poles=[],
|
|
145
|
+
Residues=[],
|
|
146
|
+
Const=0.0):
|
|
147
|
+
|
|
148
|
+
#model parameters of transfer function in pole-residue form
|
|
149
|
+
self.Const, self.Poles, self.Residues = Const, Poles, Residues
|
|
150
|
+
|
|
151
|
+
#Statespace realization of transfer function
|
|
152
|
+
A, B, C, D = gilbert_realization(Poles, Residues, Const)
|
|
153
|
+
|
|
154
|
+
#initialize statespace model
|
|
155
|
+
super().__init__(A, B, C, D)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## REDUCTION BLOCKS (blocks/multiplier.py)
|
|
4
|
+
##
|
|
5
|
+
## This module defines static 'Multiplier' 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 Multiplier(Block):
|
|
23
|
+
"""
|
|
24
|
+
multiplies / product of all input signals (MISO)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def update(self, t):
|
|
28
|
+
prev_output = self.outputs[0]
|
|
29
|
+
self.outputs[0] = np.prod(dict_to_array(self.inputs), axis=0)
|
|
30
|
+
return abs(prev_output - self.outputs[0])
|
pathsim/blocks/ode.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## ORDINARY DIFFERENTIAL EQUATION BLOCK
|
|
4
|
+
## (blocks/ode.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
|
+
auto_jacobian
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# BLOCKS ================================================================================
|
|
24
|
+
|
|
25
|
+
class ODE(Block):
|
|
26
|
+
"""
|
|
27
|
+
This block implements an ordinary differential equation (ODE)
|
|
28
|
+
defined by its right hand side
|
|
29
|
+
|
|
30
|
+
d/dt x = func(x, u, t)
|
|
31
|
+
|
|
32
|
+
with inhomogenity (input) u and state vector x. The function
|
|
33
|
+
can be nonlinear and the ODE can be of arbitrary order.
|
|
34
|
+
The block utilizes the integration engine to solve the ODE
|
|
35
|
+
by integrating the 'func' right hand side function.
|
|
36
|
+
|
|
37
|
+
INPUTS :
|
|
38
|
+
func : (callable object) right hand side function of ODE
|
|
39
|
+
initial_value : (array of floats) initial state / initial condition
|
|
40
|
+
jac : (callable or None) jacobian of 'func' or 'None'
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self,
|
|
44
|
+
func=lambda x, u, t: -x,
|
|
45
|
+
initial_value=0.0,
|
|
46
|
+
jac=None):
|
|
47
|
+
|
|
48
|
+
super().__init__()
|
|
49
|
+
|
|
50
|
+
#right hand side function of ODE
|
|
51
|
+
self.func = func
|
|
52
|
+
|
|
53
|
+
#initial condition
|
|
54
|
+
self.initial_value = initial_value
|
|
55
|
+
|
|
56
|
+
#jacobian of 'func'
|
|
57
|
+
self.jac = jac
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def __len__(self):
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def set_solver(self, Solver, tolerance_lte=1e-6):
|
|
65
|
+
#change solver if already initialized
|
|
66
|
+
if self.engine is not None:
|
|
67
|
+
self.engine = self.engine.change(Solver, tolerance_lte)
|
|
68
|
+
return #quit early
|
|
69
|
+
#initialize the integration engine with right hand side
|
|
70
|
+
_jac = auto_jacobian(self.func) if self.jac is None else self.jac
|
|
71
|
+
self.engine = Solver(self.initial_value, self.func, _jac, tolerance_lte)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def update(self, t):
|
|
75
|
+
self.outputs = array_to_dict(self.engine.get())
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def solve(self, t, dt):
|
|
80
|
+
#advance solution of implicit update equation and update block outputs
|
|
81
|
+
return self.engine.solve(dict_to_array(self.inputs), t, dt)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def step(self, t, dt):
|
|
85
|
+
#compute update step with integration engine and update block outputs
|
|
86
|
+
return self.engine.step(dict_to_array(self.inputs), t, dt)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## RF FILTERS (filters.py)
|
|
4
|
+
##
|
|
5
|
+
## Milan Rother 2024
|
|
6
|
+
##
|
|
7
|
+
#########################################################################################
|
|
8
|
+
|
|
9
|
+
# IMPORTS ===============================================================================
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from scipy.signal import butter, tf2ss
|
|
14
|
+
|
|
15
|
+
from math import factorial
|
|
16
|
+
|
|
17
|
+
from ..lti import StateSpace
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# FILTER BLOCKS =========================================================================
|
|
22
|
+
|
|
23
|
+
class ButterworthLowpassFilter(StateSpace):
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
Direct implementation of a low pass butterworth filter block.
|
|
27
|
+
|
|
28
|
+
Follows the same structure as the 'StateSpace' block in the
|
|
29
|
+
'pathsim.blocks' module. The numerator and denominator of the
|
|
30
|
+
filter transfer function are generated and then the transfer
|
|
31
|
+
function is realized as a state space model.
|
|
32
|
+
|
|
33
|
+
INPUTS :
|
|
34
|
+
Fc : (float) corner frequency of the filter in [Hz]
|
|
35
|
+
n : (int) filter order
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, Fc, n):
|
|
39
|
+
|
|
40
|
+
#filter parameters
|
|
41
|
+
self.Fc = Fc
|
|
42
|
+
self.n = n
|
|
43
|
+
|
|
44
|
+
#use scipy.signal for filter design
|
|
45
|
+
num, den = butter(n, 2*np.pi*Fc, btype="low", analog=True, output="ba")
|
|
46
|
+
|
|
47
|
+
#initialize parent block
|
|
48
|
+
super().__init__(*tf2ss(num, den))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ButterworthHighpassFilter(StateSpace):
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
Direct implementation of a high pass butterworth filter block.
|
|
55
|
+
|
|
56
|
+
Follows the same structure as the 'StateSpace' block in the
|
|
57
|
+
'pathsim.blocks' module. The numerator and denominator of the
|
|
58
|
+
filter transfer function are generated and then the transfer
|
|
59
|
+
function is realized as a state space model.
|
|
60
|
+
|
|
61
|
+
INPUTS :
|
|
62
|
+
Fc : (float) corner frequency of the filter in [Hz]
|
|
63
|
+
n : (int) filter order
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, Fc, n):
|
|
67
|
+
|
|
68
|
+
#filter parameters
|
|
69
|
+
self.Fc = Fc
|
|
70
|
+
self.n = n
|
|
71
|
+
|
|
72
|
+
#use scipy.signal for filter design
|
|
73
|
+
num, den = butter(n, 2*np.pi*Fc, btype="high", analog=True, output="ba")
|
|
74
|
+
|
|
75
|
+
#initialize parent block
|
|
76
|
+
super().__init__(*tf2ss(num, den))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ButterworthBandpassFilter(StateSpace):
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
Direct implementation of a bandpass butterworth filter block.
|
|
83
|
+
|
|
84
|
+
Follows the same structure as the 'StateSpace' block in the
|
|
85
|
+
'pathsim.blocks' module. The numerator and denominator of the
|
|
86
|
+
filter transfer function are generated and then the transfer
|
|
87
|
+
function is realized as a state space model.
|
|
88
|
+
|
|
89
|
+
INPUTS :
|
|
90
|
+
Fc : (list, tuple) corner frequencies of the filter in [Hz]
|
|
91
|
+
n : (int) filter order
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, Fc, n):
|
|
95
|
+
|
|
96
|
+
#filter parameters
|
|
97
|
+
self.Fc = np.asarray(Fc)
|
|
98
|
+
self.n = n
|
|
99
|
+
|
|
100
|
+
if len(Fc) != 2:
|
|
101
|
+
raise ValueError("'ButterworthBandpassFilter' requires two corner frequencies!")
|
|
102
|
+
|
|
103
|
+
#use scipy.signal for filter design
|
|
104
|
+
num, den = butter(n, 2*np.pi*self.Fc, btype="bandpass", analog=True, output="ba")
|
|
105
|
+
|
|
106
|
+
#initialize parent block
|
|
107
|
+
super().__init__(*tf2ss(num, den))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ButterworthBandstopFilter(StateSpace):
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
Direct implementation of a bandstop butterworth filter block.
|
|
114
|
+
|
|
115
|
+
Follows the same structure as the 'StateSpace' block in the
|
|
116
|
+
'pathsim.blocks' module. The numerator and denominator of the
|
|
117
|
+
filter transfer function are generated and then the transfer
|
|
118
|
+
function is realized as a state space model.
|
|
119
|
+
|
|
120
|
+
INPUTS :
|
|
121
|
+
Fc : (list, tuple) corner frequencies of the filter in [Hz]
|
|
122
|
+
n : (int) filter order
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self, Fc, n):
|
|
126
|
+
|
|
127
|
+
#filter parameters
|
|
128
|
+
self.Fc = np.asarray(Fc)
|
|
129
|
+
self.n = n
|
|
130
|
+
|
|
131
|
+
if len(Fc) != 2:
|
|
132
|
+
raise ValueError("'ButterworthBandstopFilter' requires two corner frequencies!")
|
|
133
|
+
|
|
134
|
+
#use scipy.signal for filter design
|
|
135
|
+
num, den = butter(n, 2*np.pi*self.Fc, btype="bandstop", analog=True, output="ba")
|
|
136
|
+
|
|
137
|
+
#initialize parent block
|
|
138
|
+
super().__init__(*tf2ss(num, den))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class AllpassFilter(StateSpace):
|
|
142
|
+
|
|
143
|
+
"""
|
|
144
|
+
Direct implementation of an Allpass filter using Pade approximants.
|
|
145
|
+
The transfer function of the ideal allpass is
|
|
146
|
+
|
|
147
|
+
H(s) = exp(-sT) = exp(-sT/2) / exp(sT/2)
|
|
148
|
+
|
|
149
|
+
where T is the time delay. This implementation uses Pade approximation
|
|
150
|
+
of the exponential to create a n-th order LTI statespace model that is
|
|
151
|
+
used for the numerical integration internally.
|
|
152
|
+
|
|
153
|
+
INPUTS :
|
|
154
|
+
T : (float) time delay of the allpass in [s]
|
|
155
|
+
n : (int) order of the pade approximation
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(self, T, n=1):
|
|
159
|
+
|
|
160
|
+
#filter parameters
|
|
161
|
+
self.T = T
|
|
162
|
+
self.n = n
|
|
163
|
+
|
|
164
|
+
#taylor approximations for numerator and denominator
|
|
165
|
+
num = [(-T/2)**i/factorial(i) for i in range(n+1)]
|
|
166
|
+
den = [ (T/2)**i/factorial(i) for i in range(n+1)]
|
|
167
|
+
|
|
168
|
+
#initialize parent block
|
|
169
|
+
super().__init__(*tf2ss(num, den))
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#########################################################################################
|
|
2
|
+
##
|
|
3
|
+
## SPECIAL RF NOISE SOURCES
|
|
4
|
+
## (blocks/rf/noise.py)
|
|
5
|
+
##
|
|
6
|
+
## this module implements some noise sources for RF simulations
|
|
7
|
+
##
|
|
8
|
+
## Milan Rother 2024
|
|
9
|
+
##
|
|
10
|
+
#########################################################################################
|
|
11
|
+
|
|
12
|
+
# IMPORTS ===============================================================================
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from .._block import Block
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# NOISE SOURCE BLOCKS ===================================================================
|
|
20
|
+
|
|
21
|
+
class WhiteNoise(Block):
|
|
22
|
+
"""
|
|
23
|
+
White noise source with uniform spectral density. Samples from distribution
|
|
24
|
+
with 'sampling_rate' and holds noise values constant for time bins.
|
|
25
|
+
|
|
26
|
+
If no 'sampling_rate' (None) is specified, every simulation timestep
|
|
27
|
+
gets a new noise values. This is the default setting.
|
|
28
|
+
|
|
29
|
+
INPUTS :
|
|
30
|
+
spectral_density : (float) noise spectral density
|
|
31
|
+
sampling_rate : (float or None) frequency with which the noise is sampled
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, spectral_density=1, sampling_rate=None):
|
|
35
|
+
super().__init__()
|
|
36
|
+
|
|
37
|
+
self.spectral_density = spectral_density
|
|
38
|
+
self.sampling_rate = sampling_rate
|
|
39
|
+
self.sigma = np.sqrt(spectral_density)
|
|
40
|
+
self.n_samples = 0
|
|
41
|
+
|
|
42
|
+
def reset(self):
|
|
43
|
+
#reset inputs and outputs
|
|
44
|
+
self.inputs = {0:0.0}
|
|
45
|
+
self.outputs = {0:0.0}
|
|
46
|
+
|
|
47
|
+
#reset noise samples
|
|
48
|
+
self.n_samples = 0
|
|
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] = np.random.normal(scale=self.sigma)
|
|
57
|
+
self.n_samples += 1
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class OneOverFNoise(Block):
|
|
61
|
+
"""
|
|
62
|
+
1/f noise source that is realized by integrating white noise using a
|
|
63
|
+
numerical integrator. Samples from distribution with 'sampling_rate'
|
|
64
|
+
and holds noise values constant for time bins.
|
|
65
|
+
|
|
66
|
+
If no 'sampling_rate' (None) is specified, every simulation timestep
|
|
67
|
+
gets a new noise values. This is the default setting.
|
|
68
|
+
|
|
69
|
+
INPUTS :
|
|
70
|
+
spectral_density : (float) noise spectral density
|
|
71
|
+
sampling_rate : (float or None) frequency with which the noise is sampled
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, spectral_density=1, sampling_rate=None):
|
|
75
|
+
super().__init__()
|
|
76
|
+
|
|
77
|
+
#parameters of noise signal
|
|
78
|
+
self.spectral_density = spectral_density
|
|
79
|
+
self.sampling_rate = sampling_rate
|
|
80
|
+
self.sigma = np.sqrt(spectral_density)
|
|
81
|
+
self.white_noise_value = 0.0
|
|
82
|
+
self.n_samples = 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def set_solver(self, Solver, tolerance_lte=1e-6):
|
|
86
|
+
|
|
87
|
+
#change solver if already initialized
|
|
88
|
+
if self.engine is not None:
|
|
89
|
+
self.engine = self.engine.change(Solver, tolerance_lte)
|
|
90
|
+
return #quit early
|
|
91
|
+
|
|
92
|
+
#initialize the numerical integration engine with kernel
|
|
93
|
+
def _f(x, u, t): return u
|
|
94
|
+
self.engine = Solver(0.0, _f, None, tolerance_lte)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def reset(self):
|
|
98
|
+
#reset inputs and outputs
|
|
99
|
+
self.inputs = {0:0.0}
|
|
100
|
+
self.outputs = {0:0.0}
|
|
101
|
+
|
|
102
|
+
#reset noise samples
|
|
103
|
+
self.white_noise_value = 0.0
|
|
104
|
+
self.n_samples = 0
|
|
105
|
+
|
|
106
|
+
#reset engine
|
|
107
|
+
self.engine.reset()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def update(self, t):
|
|
111
|
+
#set outputs
|
|
112
|
+
self.outputs[0] = self.engine.get()
|
|
113
|
+
return 0.0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def sample(self, t):
|
|
117
|
+
"""
|
|
118
|
+
Sample from a normal distribution after successful timestep.
|
|
119
|
+
"""
|
|
120
|
+
if (self.sampling_rate is None or
|
|
121
|
+
self.n_samples < t * self.sampling_rate):
|
|
122
|
+
self.white_noise_value = np.random.normal(scale=self.sigma)
|
|
123
|
+
self.n_samples += 1
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def solve(self, t, dt):
|
|
127
|
+
#advance solution of implicit update equation
|
|
128
|
+
self.engine.solve(self.white_noise_value, t, dt)
|
|
129
|
+
return 0.0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def step(self, t, dt):
|
|
133
|
+
#compute update step with integration engine
|
|
134
|
+
self.engine.step(self.white_noise_value, t, dt)
|
|
135
|
+
|
|
136
|
+
#no error control for noise source
|
|
137
|
+
return True, 0.0, 1.0
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class SinusoidalPhaseNoiseSource(Block):
|
|
141
|
+
|
|
142
|
+
def __init__(self, frequency=1, amplitude=1, phase=0, sig_cum=0, sig_white=0, sampling_rate=10):
|
|
143
|
+
super().__init__()
|
|
144
|
+
|
|
145
|
+
self.amplitude = amplitude
|
|
146
|
+
self.frequency = frequency
|
|
147
|
+
self.phase = phase
|
|
148
|
+
|
|
149
|
+
self.sampling_rate = sampling_rate
|
|
150
|
+
|
|
151
|
+
self.omega = 2 * np.pi * self.frequency
|
|
152
|
+
|
|
153
|
+
self.sig_cum = sig_cum
|
|
154
|
+
self.sig_white = sig_white
|
|
155
|
+
|
|
156
|
+
#initial noise sampling
|
|
157
|
+
self.noise_1 = np.random.normal()
|
|
158
|
+
self.noise_2 = np.random.normal()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def set_solver(self, Solver, tolerance_lte=1e-6):
|
|
162
|
+
|
|
163
|
+
#change solver if already initialized
|
|
164
|
+
if self.engine is not None:
|
|
165
|
+
self.engine = self.engine.change(Solver, tolerance_lte)
|
|
166
|
+
return #quit early
|
|
167
|
+
|
|
168
|
+
#initialize the numerical integration engine with kernel
|
|
169
|
+
def _f(x, u, t): return u
|
|
170
|
+
self.engine = Solver(0.0, _f, None, tolerance_lte)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def reset(self):
|
|
174
|
+
#reset inputs and outputs
|
|
175
|
+
self.inputs = {0:0.0}
|
|
176
|
+
self.outputs = {0:0.0}
|
|
177
|
+
|
|
178
|
+
#reset bin counter
|
|
179
|
+
self.n_samples = 0
|
|
180
|
+
self.t_max = 0
|
|
181
|
+
|
|
182
|
+
#reset engine
|
|
183
|
+
self.engine.reset()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def update(self, t):
|
|
187
|
+
|
|
188
|
+
#compute phase error
|
|
189
|
+
phase_error = self.sig_white * self.noise_1 + self.sig_cum * self.engine.get()
|
|
190
|
+
|
|
191
|
+
#set output
|
|
192
|
+
self.outputs[0] = self.amplitude * np.sin(self.omega*t + self.phase + phase_error)
|
|
193
|
+
return 0.0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def sample(self, t):
|
|
197
|
+
"""
|
|
198
|
+
Sample from a normal distribution after successful timestep.
|
|
199
|
+
"""
|
|
200
|
+
if (self.sampling_rate is None or
|
|
201
|
+
self.n_samples < t * self.sampling_rate):
|
|
202
|
+
self.noise_1 = np.random.normal()
|
|
203
|
+
self.noise_2 = np.random.normal()
|
|
204
|
+
self.n_samples += 1
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def solve(self, t, dt):
|
|
208
|
+
#advance solution of implicit update equation
|
|
209
|
+
self.engine.solve(self.noise_2, t, dt)
|
|
210
|
+
return 0.0
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def step(self, t, dt):
|
|
214
|
+
#compute update step with integration engine
|
|
215
|
+
self.engine.step(self.noise_2, t, dt)
|
|
216
|
+
|
|
217
|
+
#no error control for noise source
|
|
218
|
+
return True, 0.0, 1.0
|