pyadrc 0.5.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.
- pyadrc/__init__.py +11 -0
- pyadrc/models.py +180 -0
- pyadrc/pyadrc.py +949 -0
- pyadrc-0.5.0.dist-info/METADATA +78 -0
- pyadrc-0.5.0.dist-info/RECORD +9 -0
- pyadrc-0.5.0.dist-info/WHEEL +5 -0
- pyadrc-0.5.0.dist-info/licenses/AUTHORS.rst +13 -0
- pyadrc-0.5.0.dist-info/licenses/LICENSE +22 -0
- pyadrc-0.5.0.dist-info/top_level.txt +1 -0
pyadrc/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Top-level package for pyadrc."""
|
|
2
|
+
|
|
3
|
+
__author__ = """Ongun Türkcüoglu"""
|
|
4
|
+
__email__ = 'onguntoglu@gmail.com'
|
|
5
|
+
__version__ = '0.5.0'
|
|
6
|
+
|
|
7
|
+
from .pyadrc import StateSpace, TransferFunction, FeedbackTF, saturation
|
|
8
|
+
from .models import QuadAltitude, System
|
|
9
|
+
|
|
10
|
+
__all__ = ['StateSpace', 'TransferFunction', 'FeedbackTF', 'saturation',
|
|
11
|
+
'QuadAltitude', 'System', '__version__']
|
pyadrc/models.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy.signal
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class QuadAltitude(object):
|
|
6
|
+
"""Discrete-time model of altitude of a quadcopter
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
dt : float, optional
|
|
11
|
+
Discretization time (zero-order hold), by default 0.001
|
|
12
|
+
m : float, optional
|
|
13
|
+
Mass of the quadcopter, by default 0.028
|
|
14
|
+
g : float, optional
|
|
15
|
+
Gravitational acceleration, by default 9.807
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, dt: float = 0.001,
|
|
19
|
+
m: float = 0.028, g: float = 9.807):
|
|
20
|
+
|
|
21
|
+
assert isinstance(g, float), 'Gravity needs to be of type float'
|
|
22
|
+
assert isinstance(m, float), 'Mass of quadcopter \
|
|
23
|
+
needs to be of type float'
|
|
24
|
+
|
|
25
|
+
self.g = g
|
|
26
|
+
self.m = m
|
|
27
|
+
self.dt = dt
|
|
28
|
+
|
|
29
|
+
self.vel = 0
|
|
30
|
+
self.pos = 0
|
|
31
|
+
self.acc = 0
|
|
32
|
+
|
|
33
|
+
def __call__(self, u: float) -> float:
|
|
34
|
+
"""Input port to the quadcopter
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
u : float
|
|
39
|
+
Thrust of the rotors
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
float:
|
|
44
|
+
Current altitude of quadcopter
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
acc = u * (1/self.m) - self.g
|
|
49
|
+
|
|
50
|
+
self.vel += acc * self.dt
|
|
51
|
+
self.pos += self.vel * self.dt
|
|
52
|
+
self.acc = acc
|
|
53
|
+
|
|
54
|
+
# Include ground
|
|
55
|
+
if self.pos <= 0:
|
|
56
|
+
self.vel = 0
|
|
57
|
+
self.pos = 0
|
|
58
|
+
|
|
59
|
+
return self.pos
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def states(self):
|
|
63
|
+
"""Return the state of the quadcopter
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
return self.pos, self.vel, self.acc
|
|
67
|
+
|
|
68
|
+
@states.setter
|
|
69
|
+
def states(self, value):
|
|
70
|
+
"""Sets the state of the quadcopter to value
|
|
71
|
+
value = (pos, vel, acc). If a state is None, it is not changed.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
value : tuple
|
|
76
|
+
new state vector of quadcopter
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
self.pos = value[0] if isinstance(value[0], (float, int)) else self.pos
|
|
80
|
+
self.vel = value[1] if isinstance(value[1], (float, int)) else self.vel
|
|
81
|
+
self.acc = value[2] if isinstance(value[2], (float, int)) else self.acc
|
|
82
|
+
|
|
83
|
+
def reset(self):
|
|
84
|
+
"""Reset position and velocity to zero
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
self.pos = 0.
|
|
88
|
+
self.vel = 0.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class System(object):
|
|
92
|
+
|
|
93
|
+
"""Python class to generate a first-or second-order process
|
|
94
|
+
for simulation and verification purposes
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
K : float, optional
|
|
99
|
+
system gain, by default 1.0
|
|
100
|
+
T : float, optional
|
|
101
|
+
time constant, by default 1.0
|
|
102
|
+
D : float, optional
|
|
103
|
+
damping factor, by default None
|
|
104
|
+
delta : float, optional
|
|
105
|
+
discretization time in seconds, by default 0.001
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(self, K: float = 1.0, T: float = 1.0,
|
|
109
|
+
D: float = None, delta: float = 0.001) -> None:
|
|
110
|
+
|
|
111
|
+
assert isinstance(K, float)
|
|
112
|
+
assert isinstance(T, float)
|
|
113
|
+
assert delta > 0, "Sampling time has to be positive"
|
|
114
|
+
|
|
115
|
+
# First-order process
|
|
116
|
+
num = np.array([K]).reshape(-1)
|
|
117
|
+
den = np.array([T, 1]).reshape(-1)
|
|
118
|
+
|
|
119
|
+
if D is not None:
|
|
120
|
+
assert isinstance(D, float),\
|
|
121
|
+
"Damping factor D must be type float"
|
|
122
|
+
# Second-order process
|
|
123
|
+
den = np.array([T**2, 2 * D * T, 1]).reshape(-1)
|
|
124
|
+
|
|
125
|
+
system = scipy.signal.tf2ss(num, den)
|
|
126
|
+
|
|
127
|
+
system = scipy.signal.cont2discrete(system, delta)
|
|
128
|
+
|
|
129
|
+
self.A = system[0]
|
|
130
|
+
self.B = system[1]
|
|
131
|
+
self.C = system[2]
|
|
132
|
+
|
|
133
|
+
self.x = np.zeros((len(den) - 1, 1), dtype=np.float64)
|
|
134
|
+
|
|
135
|
+
def __call__(self, u: float) -> float:
|
|
136
|
+
"""System response w.r.t. control signal u
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
u : float
|
|
141
|
+
control signal u
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
float
|
|
146
|
+
system response y
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
self.x = self.A.dot(self.x) + self.B.dot(u)
|
|
150
|
+
|
|
151
|
+
return (self.C @ self.x).item()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class RandomSystem(object):
|
|
155
|
+
|
|
156
|
+
def __init__(self, states=1) -> None:
|
|
157
|
+
"""Create random discrete state space system
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
states (int, optional): number of states. Defaults to 1.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
None: [description]
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
import control
|
|
167
|
+
|
|
168
|
+
system = control.drss(states=states)
|
|
169
|
+
|
|
170
|
+
self.A = np.array(system.A)
|
|
171
|
+
self.B = np.array(system.B)
|
|
172
|
+
self.C = np.array(system.C)
|
|
173
|
+
self.D = np.array(system.D)
|
|
174
|
+
|
|
175
|
+
self.x = np.zeros((states, 1), dtype=np.float64)
|
|
176
|
+
|
|
177
|
+
def __call__(self, u: float) -> float:
|
|
178
|
+
|
|
179
|
+
self.x = self.A.dot(self.x) + self.B.dot(u)
|
|
180
|
+
return (self.C @ self.x + self.D * u).item()
|
pyadrc/pyadrc.py
ADDED
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
"""Active Disturbance Rejection Control for Python
|
|
2
|
+
|
|
3
|
+
It is highly recommended to check the documentation first before attempting
|
|
4
|
+
to use the package. Although it is simple in nature, ADRC requires some
|
|
5
|
+
basic knowledge about PID control, observers and state-feedback.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from collections import deque
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def saturation(_limits: tuple, _val: float) -> float:
|
|
16
|
+
"""Saturation function
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
_limits : tuple
|
|
21
|
+
saturation limits (low, high)
|
|
22
|
+
_val : float
|
|
23
|
+
sat(_val)
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
float
|
|
28
|
+
saturated signal
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
lo, hi = _limits
|
|
32
|
+
|
|
33
|
+
if _val is None:
|
|
34
|
+
return None
|
|
35
|
+
elif hi is not None and _val > hi:
|
|
36
|
+
return hi
|
|
37
|
+
elif lo is not None and _val < lo:
|
|
38
|
+
return lo
|
|
39
|
+
|
|
40
|
+
return _val
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class StateSpace():
|
|
44
|
+
|
|
45
|
+
"""Discrete linear time-invariant state space implementation\
|
|
46
|
+
of ADRC
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
order : int
|
|
51
|
+
first- or second-order ADRC
|
|
52
|
+
delta : float
|
|
53
|
+
sampling time in seconds
|
|
54
|
+
b0 : float
|
|
55
|
+
gain parameter b0
|
|
56
|
+
w_cl : float
|
|
57
|
+
desired closed-loop bandwidth [rad/s], 4 / w_cl and 6 / w_cl is the
|
|
58
|
+
corresponding settling time in seconds for first- and second-order ADRC
|
|
59
|
+
respectively
|
|
60
|
+
k_eso : float
|
|
61
|
+
relational observer bandwidth
|
|
62
|
+
inc_form : float
|
|
63
|
+
toggle incremental form of ADRC, by default False. If the incremental
|
|
64
|
+
form is toggled, the controller will return the incrementation to the
|
|
65
|
+
current control signal. This value needs to be accumulated by the user.
|
|
66
|
+
Functionally identical to the non-incremental (normal) form.
|
|
67
|
+
eso_init : tuple, optional
|
|
68
|
+
initial state for the extended state observer, by default False
|
|
69
|
+
r_lim : tuple, optional
|
|
70
|
+
rate limits for the control signal, by default (None, None)
|
|
71
|
+
m_lim : tuple, optional
|
|
72
|
+
magnitude limits for the control signal, by default (None, None)
|
|
73
|
+
half_gain : tuple, optional
|
|
74
|
+
half gain tuning for controller/observer gains,\
|
|
75
|
+
by default (False, False)
|
|
76
|
+
|
|
77
|
+
References
|
|
78
|
+
----------
|
|
79
|
+
.. [1] G. Herbst, "Practical active disturbance rejection control:
|
|
80
|
+
Bumpless transfer, rate limitation, and incremental algorithm",
|
|
81
|
+
https://arxiv.org/abs/1908.04610
|
|
82
|
+
|
|
83
|
+
.. [2] G. Herbst, "Half-Gain Tuning for Active Disturbance Rejection
|
|
84
|
+
Control", https://arxiv.org/abs/2003.03986
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self,
|
|
88
|
+
order: int,
|
|
89
|
+
delta: float,
|
|
90
|
+
b0: float,
|
|
91
|
+
w_cl: float,
|
|
92
|
+
k_eso: float,
|
|
93
|
+
inc_form: bool = False,
|
|
94
|
+
eso_init: tuple = False,
|
|
95
|
+
r_lim: tuple = (None, None),
|
|
96
|
+
m_lim: tuple = (None, None),
|
|
97
|
+
half_gain: tuple = (False, False)):
|
|
98
|
+
|
|
99
|
+
assert (order == 1) or (order == 2),\
|
|
100
|
+
'Only first- and second-order ADRC is implemented'
|
|
101
|
+
|
|
102
|
+
self.b0 = b0
|
|
103
|
+
nx = order + 1
|
|
104
|
+
self.delta = delta
|
|
105
|
+
|
|
106
|
+
if order == 1:
|
|
107
|
+
self.Ad = np.vstack(([1, delta], [0, 1]))
|
|
108
|
+
self.Bd = np.vstack((b0 * delta, 0))
|
|
109
|
+
self.Cd = np.hstack((1, 0)).reshape(1, -1)
|
|
110
|
+
self.Dd = 0
|
|
111
|
+
|
|
112
|
+
# Controller parameters for closed-loop dynamics
|
|
113
|
+
t_settle = 4 / w_cl
|
|
114
|
+
sCL = -4 / t_settle
|
|
115
|
+
self.Kp = -sCL
|
|
116
|
+
|
|
117
|
+
# Observer dynamics
|
|
118
|
+
sESO = k_eso * sCL
|
|
119
|
+
zESO = np.exp(sESO * delta)
|
|
120
|
+
|
|
121
|
+
# Observer gains resulting in common-location observer poles
|
|
122
|
+
self.L = np.array([1 - (zESO)**2,
|
|
123
|
+
(1 / delta) * (1 - zESO)**2]).reshape(-1, 1)
|
|
124
|
+
|
|
125
|
+
# Controller gains
|
|
126
|
+
self.w = np.array([self.Kp / self.b0,
|
|
127
|
+
1 / self.b0]).reshape(-1, 1)
|
|
128
|
+
|
|
129
|
+
elif order == 2:
|
|
130
|
+
self.Ad = np.vstack((
|
|
131
|
+
[1, delta, (delta**2) / 2],
|
|
132
|
+
[0, 1, delta],
|
|
133
|
+
[0, 0, 1]))
|
|
134
|
+
self.Bd = np.vstack((b0 * (delta**2) / 2, b0 * delta, 0))
|
|
135
|
+
self.Cd = np.hstack((1, 0, 0)).reshape(1, -1)
|
|
136
|
+
self.Dd = 0
|
|
137
|
+
|
|
138
|
+
# Controller parameters for closed-loop dynamics
|
|
139
|
+
t_settle = 6 / w_cl
|
|
140
|
+
sCL = -6 / t_settle
|
|
141
|
+
self.Kp = sCL**2
|
|
142
|
+
self.Kd = -2 * sCL
|
|
143
|
+
|
|
144
|
+
# Observer dynamics
|
|
145
|
+
sESO = k_eso * sCL
|
|
146
|
+
zESO = np.exp(sESO * delta)
|
|
147
|
+
|
|
148
|
+
# Observer gains resulting in common-location observer poles
|
|
149
|
+
self.L = np.array([1 - (zESO)**3,
|
|
150
|
+
(3 / (2 * delta)) * (1 - zESO)**2 * (1 + zESO),
|
|
151
|
+
(1 / delta**2) * (1 - zESO)**3]
|
|
152
|
+
).reshape(-1, 1)
|
|
153
|
+
|
|
154
|
+
# Controller gains
|
|
155
|
+
self.w = np.array([self.Kp / self.b0,
|
|
156
|
+
self.Kd / self.b0,
|
|
157
|
+
1 / self.b0]).reshape(-1, 1)
|
|
158
|
+
|
|
159
|
+
self.xhat = np.zeros((nx, 1), dtype=np.float64)
|
|
160
|
+
|
|
161
|
+
self.ukm1 = 0.
|
|
162
|
+
|
|
163
|
+
self.m_lim = m_lim
|
|
164
|
+
self.r_lim = r_lim
|
|
165
|
+
|
|
166
|
+
if half_gain[0] is True:
|
|
167
|
+
self.w = self.w / 2
|
|
168
|
+
if half_gain[1] is True:
|
|
169
|
+
self.L = self.L / 2
|
|
170
|
+
|
|
171
|
+
if eso_init is not False:
|
|
172
|
+
assert len(eso_init) == nx,\
|
|
173
|
+
'Length of initial state vector of LESO not compatible\
|
|
174
|
+
with order'
|
|
175
|
+
self.xhat = np.fromiter(eso_init, np.float64).reshape(-1, 1)
|
|
176
|
+
|
|
177
|
+
self._last_time = None
|
|
178
|
+
self._last_output = None
|
|
179
|
+
self._last_input = None
|
|
180
|
+
self.inc_form = inc_form
|
|
181
|
+
|
|
182
|
+
self._linear_extended_state_observer()
|
|
183
|
+
|
|
184
|
+
def _linear_extended_state_observer(self):
|
|
185
|
+
"""Internal function implementing the one-step update
|
|
186
|
+
equation for the linear extended state observer
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
self.oA = self.Ad - self.L @ self.Cd @ self.Ad
|
|
190
|
+
self.oB = self.Bd - self.L @ self.Cd @ self.Bd
|
|
191
|
+
self.oC = self.Cd
|
|
192
|
+
|
|
193
|
+
if self.inc_form is True:
|
|
194
|
+
self.oA = self.oA - np.eye(self.oA.shape[0])
|
|
195
|
+
self.rkm1 = 0.
|
|
196
|
+
|
|
197
|
+
def _update_eso(self, y: float, ukm1: float):
|
|
198
|
+
"""Update the linear extended state observer
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
y : float
|
|
203
|
+
Current measurement y[k]
|
|
204
|
+
ukm1 : float
|
|
205
|
+
Previous control signal u[k-1]
|
|
206
|
+
"""
|
|
207
|
+
if self.inc_form is False:
|
|
208
|
+
self.xhat = self.oA.dot(self.xhat) + self.oB.dot(
|
|
209
|
+
ukm1).reshape(-1, 1) + self.L.dot(y)
|
|
210
|
+
else: # self.inc_form is True
|
|
211
|
+
self.xhat_delta = self.oA.dot(self.xhat) + self.oB.dot(
|
|
212
|
+
ukm1).reshape(-1, 1) + self.L.dot(y)
|
|
213
|
+
|
|
214
|
+
self.xhat = self.xhat + self.xhat_delta
|
|
215
|
+
|
|
216
|
+
def _limiter(self, u_control: float) -> float:
|
|
217
|
+
"""Implements rate and magnitude limiter
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
u_control : float
|
|
222
|
+
control signal to be limited
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
float
|
|
227
|
+
float: rate and magnitude limited control signal
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
# Limiting the rate of u (delta_u)
|
|
231
|
+
delta_u = saturation((self.r_lim[0], self.r_lim[1]),
|
|
232
|
+
u_control - self.ukm1)
|
|
233
|
+
|
|
234
|
+
# Limiting the magnitude of u
|
|
235
|
+
self.ukm1 = saturation((self.m_lim[0],
|
|
236
|
+
self.m_lim[1]),
|
|
237
|
+
delta_u + self.ukm1)
|
|
238
|
+
|
|
239
|
+
return self.ukm1
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def eso_states(self) -> tuple:
|
|
243
|
+
"""Returns the states of the linear extended state observer
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
tuple
|
|
248
|
+
States of the linear extended state observer
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
return self.xhat
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def limiter(self) -> tuple:
|
|
255
|
+
"""Returns the value of both limiters of the controller
|
|
256
|
+
|
|
257
|
+
Returns
|
|
258
|
+
-------
|
|
259
|
+
tuple of tuples
|
|
260
|
+
Returns (magnitude_limits, rate_limits)
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
return self.m_lim, self.r_lim
|
|
264
|
+
|
|
265
|
+
@limiter.setter
|
|
266
|
+
def limiter(self, lim_tuple: tuple) -> None:
|
|
267
|
+
"""Setter for magnitude and rate limiter
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
lim_tuple : tuple of tuples
|
|
272
|
+
New magnitude limits
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
assert len(lim_tuple) == 2
|
|
276
|
+
assert len(lim_tuple[0]) == 2 and len(lim_tuple[1]) == 2
|
|
277
|
+
# assert lim[0] < lim[1]
|
|
278
|
+
self.m_lim = lim_tuple[0]
|
|
279
|
+
self.r_lim = lim_tuple[1]
|
|
280
|
+
|
|
281
|
+
def __call__(self, y: float, u: float, r: float, zoh: bool = False):
|
|
282
|
+
"""Returns value of the control signal depending on current
|
|
283
|
+
measurements, previous control action, reference signal.
|
|
284
|
+
|
|
285
|
+
Parameters
|
|
286
|
+
----------
|
|
287
|
+
y : float
|
|
288
|
+
Current measurement y[k] of the process
|
|
289
|
+
u : float
|
|
290
|
+
Previous control signal u[k-1]
|
|
291
|
+
r : float
|
|
292
|
+
Current reference signal r[k]
|
|
293
|
+
zoh : bool, optional
|
|
294
|
+
Only update every delta seconds, by default False
|
|
295
|
+
|
|
296
|
+
Returns
|
|
297
|
+
-------
|
|
298
|
+
float
|
|
299
|
+
Current control signal u[k]
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
now = time.monotonic()
|
|
303
|
+
|
|
304
|
+
if zoh is True:
|
|
305
|
+
try:
|
|
306
|
+
dt = now - self._last_time if now - self._last_time else 1e-16
|
|
307
|
+
except TypeError:
|
|
308
|
+
dt = 1e-16
|
|
309
|
+
|
|
310
|
+
if zoh is True and dt < self.delta and self._last_output is not None:
|
|
311
|
+
# Return last output of the controller if dt < delta
|
|
312
|
+
return self._last_output
|
|
313
|
+
|
|
314
|
+
self._update_eso(y, u)
|
|
315
|
+
|
|
316
|
+
if self.inc_form is False:
|
|
317
|
+
u = (self.Kp / self.b0) * r - (self.w.T @ self.xhat).item()
|
|
318
|
+
u = self._limiter(u)
|
|
319
|
+
else: # self.inc_form is True:
|
|
320
|
+
delta_r = r - self.rkm1
|
|
321
|
+
delta_u = ((self.Kp / self.b0) * delta_r
|
|
322
|
+
- (self.w.T @ self.xhat_delta).item())
|
|
323
|
+
u = delta_u
|
|
324
|
+
|
|
325
|
+
self.rkm1 = r
|
|
326
|
+
|
|
327
|
+
self._last_output = float(u)
|
|
328
|
+
self._last_time = now
|
|
329
|
+
return float(u)
|
|
330
|
+
|
|
331
|
+
def reset(self, x0=None):
|
|
332
|
+
"""Resets the extended state observer
|
|
333
|
+
|
|
334
|
+
Parameters
|
|
335
|
+
----------
|
|
336
|
+
x0 : np.array, optional
|
|
337
|
+
new initial state vector for the extended state observer,
|
|
338
|
+
by default None, i.e. resets to zero
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
nx = len(self.xhat)
|
|
342
|
+
|
|
343
|
+
assert x0 is None or len(np.fromiter(x0, float)) == nx,\
|
|
344
|
+
"Invalid state vector"
|
|
345
|
+
|
|
346
|
+
if x0 is None:
|
|
347
|
+
self.xhat = np.zeros((nx, 1), dtype=np.float64)
|
|
348
|
+
else:
|
|
349
|
+
self.xhat = np.fromiter(x0, np.float64).reshape(-1, 1)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class FeedbackTF(object):
|
|
353
|
+
|
|
354
|
+
"""Minimal footprint ADRC implemented with transfer functions in\
|
|
355
|
+
feedback path
|
|
356
|
+
|
|
357
|
+
Parameters
|
|
358
|
+
----------
|
|
359
|
+
order : int
|
|
360
|
+
first- or second-order ADRC
|
|
361
|
+
delta : float
|
|
362
|
+
sampling time in seconds
|
|
363
|
+
b0 : float
|
|
364
|
+
gain parameter b0
|
|
365
|
+
w_cl : float
|
|
366
|
+
desired closed-loop bandwidth [rad/s], 4 / w_cl and 6 / w_cl is the
|
|
367
|
+
corresponding settling time in seconds for first- and second-order ADRC
|
|
368
|
+
respectively
|
|
369
|
+
k_eso : float
|
|
370
|
+
relational observer bandwidth
|
|
371
|
+
x_init : tuple, optional
|
|
372
|
+
initial state for the storage variables, by default False
|
|
373
|
+
r_lim : tuple, optional
|
|
374
|
+
rate limits of the controller, by default (None, None)
|
|
375
|
+
m_lim : tuple, optional
|
|
376
|
+
magnitude limits of the controller, by default (None, None)
|
|
377
|
+
|
|
378
|
+
References
|
|
379
|
+
----------
|
|
380
|
+
.. [3] G. Herbst, "A Minimum-Footprint Implementation
|
|
381
|
+
of Discrete-Time ADRC", https://arxiv.org/abs/2104.01943
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
def __init__(self,
|
|
385
|
+
order: int,
|
|
386
|
+
delta: float,
|
|
387
|
+
b0: float,
|
|
388
|
+
w_cl: float,
|
|
389
|
+
k_eso: float,
|
|
390
|
+
x_init: tuple = False,
|
|
391
|
+
r_lim: tuple = (None, None),
|
|
392
|
+
m_lim: tuple = (None, None)) -> None:
|
|
393
|
+
|
|
394
|
+
assert order == 1 or order == 2, 'First- and second-order FBTF-ADRC\
|
|
395
|
+
implemented'
|
|
396
|
+
|
|
397
|
+
self.order = order
|
|
398
|
+
self.delta = delta
|
|
399
|
+
|
|
400
|
+
zESO = np.exp(-k_eso * w_cl * delta)
|
|
401
|
+
|
|
402
|
+
if order == 1:
|
|
403
|
+
self.coeff = {'a1': -2 * zESO, 'a2': zESO**2,
|
|
404
|
+
'b0': delta * w_cl * zESO**2 - (1 - zESO)**2,
|
|
405
|
+
'b1': - delta * w_cl * zESO**2,
|
|
406
|
+
'g0': (1/(b0 * delta)) * (delta * w_cl
|
|
407
|
+
* (1-zESO**2)
|
|
408
|
+
+ (1 - zESO)**2),
|
|
409
|
+
'g1': (1/(b0 * delta)) * (2 * delta * w_cl
|
|
410
|
+
* (zESO**2 - zESO)
|
|
411
|
+
- (1 - zESO)**2),
|
|
412
|
+
'k': w_cl / b0}
|
|
413
|
+
|
|
414
|
+
self.states = {'x1': 0., 'x2': 0.}
|
|
415
|
+
|
|
416
|
+
elif order == 2:
|
|
417
|
+
_GC = 1/(b0 * delta**2) # coefficient gamma_n constant
|
|
418
|
+
self.coeff = {'a1': -3 * zESO, 'a2': 3 * zESO**2, 'a3': -zESO**3,
|
|
419
|
+
'b0': 0.5 * (- delta * w_cl * zESO**3
|
|
420
|
+
* (4 - delta * w_cl)
|
|
421
|
+
+ delta * w_cl * (1 + zESO)**3
|
|
422
|
+
- (1 - zESO)**3),
|
|
423
|
+
'b1': 0.5 * (- delta * w_cl
|
|
424
|
+
* (1 + zESO)**3 - (1 - zESO)**3),
|
|
425
|
+
'b2': 0.5 * (delta * w_cl * zESO**3
|
|
426
|
+
* (4 - delta * w_cl)),
|
|
427
|
+
'g0': _GC * (delta**2 * w_cl**2 * (1 - zESO**3)
|
|
428
|
+
+ 3 * delta * w_cl
|
|
429
|
+
* (1 - zESO - zESO**2 + zESO**3)
|
|
430
|
+
+ (1 - zESO)**3),
|
|
431
|
+
'g1': _GC * (3 * delta**2 * w_cl**2
|
|
432
|
+
* (- zESO + zESO**3)
|
|
433
|
+
+ 4 * delta * w_cl
|
|
434
|
+
* (-1 + 3 * zESO**2 - 2*zESO**3)
|
|
435
|
+
- 2 * (1 - zESO)**3),
|
|
436
|
+
'g2': _GC * (3 * delta**2 * w_cl**2
|
|
437
|
+
* (zESO**2 - zESO**3)
|
|
438
|
+
+ delta * w_cl
|
|
439
|
+
* (1 + 3 * zESO - 9*zESO**2 + 5*zESO**3)
|
|
440
|
+
+ (1 - zESO)**3),
|
|
441
|
+
'k': w_cl**2 / b0}
|
|
442
|
+
|
|
443
|
+
self.states = {'x1': 0., 'x2': 0., 'x3': 0.}
|
|
444
|
+
|
|
445
|
+
# Initialize limiters
|
|
446
|
+
self.ukm1 = 0.
|
|
447
|
+
self.r_lim = r_lim
|
|
448
|
+
self.m_lim = m_lim
|
|
449
|
+
|
|
450
|
+
# Zero-order hold variables
|
|
451
|
+
self._last_time = None
|
|
452
|
+
self._last_output = None
|
|
453
|
+
self._last_input = None
|
|
454
|
+
|
|
455
|
+
@property
|
|
456
|
+
def x_states(self) -> tuple:
|
|
457
|
+
"""Returns the storage variables
|
|
458
|
+
|
|
459
|
+
Returns
|
|
460
|
+
-------
|
|
461
|
+
tuple
|
|
462
|
+
Storage variables
|
|
463
|
+
"""
|
|
464
|
+
|
|
465
|
+
return tuple(self.states.values())
|
|
466
|
+
|
|
467
|
+
@property
|
|
468
|
+
def limiter(self) -> tuple:
|
|
469
|
+
"""Returns the value of both limiters of the controller
|
|
470
|
+
|
|
471
|
+
Returns
|
|
472
|
+
-------
|
|
473
|
+
tuple of tuples
|
|
474
|
+
Returns (magnitude_limits, rate_limits)
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
return self.m_lim, self.r_lim
|
|
478
|
+
|
|
479
|
+
@limiter.setter
|
|
480
|
+
def limiter(self, lim_tuple: tuple) -> None:
|
|
481
|
+
"""Setter for magnitude and rate limiter
|
|
482
|
+
|
|
483
|
+
Parameters
|
|
484
|
+
----------
|
|
485
|
+
lim_tuple : tuple of tuples
|
|
486
|
+
New magnitude limits
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
assert len(lim_tuple) == 2
|
|
490
|
+
assert len(lim_tuple[0]) == 2 and len(lim_tuple[1]) == 2
|
|
491
|
+
# assert lim[0] < lim[1]
|
|
492
|
+
self.m_lim = lim_tuple[0]
|
|
493
|
+
self.r_lim = lim_tuple[1]
|
|
494
|
+
|
|
495
|
+
def _limiter(self, u_control: float) -> float:
|
|
496
|
+
"""Implements rate and magnitude limiter
|
|
497
|
+
|
|
498
|
+
Parameters
|
|
499
|
+
----------
|
|
500
|
+
u_control : float
|
|
501
|
+
control signal to be limited
|
|
502
|
+
|
|
503
|
+
Returns
|
|
504
|
+
-------
|
|
505
|
+
float
|
|
506
|
+
float: rate and magnitude limited control signal
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
# Limiting the rate of u (delta_u)
|
|
510
|
+
delta_u = saturation((self.r_lim[0], self.r_lim[1]),
|
|
511
|
+
u_control - self.ukm1)
|
|
512
|
+
|
|
513
|
+
# Limiting the magnitude of u
|
|
514
|
+
self.ukm1 = saturation((self.m_lim[0],
|
|
515
|
+
self.m_lim[1]),
|
|
516
|
+
delta_u + self.ukm1)
|
|
517
|
+
|
|
518
|
+
return self.ukm1
|
|
519
|
+
|
|
520
|
+
def _update_xn(self, ck: float, u_lim: float, y: float) -> None:
|
|
521
|
+
"""Internal function to update the storage variables after the control
|
|
522
|
+
signal is calculated
|
|
523
|
+
|
|
524
|
+
Parameters
|
|
525
|
+
----------
|
|
526
|
+
ck : float
|
|
527
|
+
combined transfer function output
|
|
528
|
+
u_lim : float
|
|
529
|
+
limited control signal
|
|
530
|
+
y : float
|
|
531
|
+
plant output
|
|
532
|
+
"""
|
|
533
|
+
self.states['x1'] = (self.states['x2']
|
|
534
|
+
- self.coeff['a1'] * ck
|
|
535
|
+
+ self.coeff['b0'] * u_lim
|
|
536
|
+
+ self.coeff['g1'] * y)
|
|
537
|
+
|
|
538
|
+
if self.order == 1:
|
|
539
|
+
self.states['x2'] = (- self.coeff['a2'] * ck
|
|
540
|
+
+ self.coeff['b1'] * u_lim)
|
|
541
|
+
|
|
542
|
+
elif self.order == 2:
|
|
543
|
+
self.states['x2'] = (self.states['x3']
|
|
544
|
+
- self.coeff['a2'] * ck
|
|
545
|
+
+ self.coeff['b1'] * u_lim
|
|
546
|
+
+ self.coeff['g2'] * y)
|
|
547
|
+
|
|
548
|
+
self.states['x3'] = (- self.coeff['a3'] * ck
|
|
549
|
+
+ self.coeff['b2'] * u_lim)
|
|
550
|
+
|
|
551
|
+
def __call__(self, y: float, r: float, zoh: bool = False) -> float:
|
|
552
|
+
"""Returns value of the control signal depending on current
|
|
553
|
+
measurements and reference signal.
|
|
554
|
+
|
|
555
|
+
Parameters
|
|
556
|
+
----------
|
|
557
|
+
y : float
|
|
558
|
+
Current measurement y[k] of the process
|
|
559
|
+
r : float
|
|
560
|
+
Current reference signal r[k]
|
|
561
|
+
zoh : bool, optional
|
|
562
|
+
Only update every delta seconds, by default False
|
|
563
|
+
|
|
564
|
+
Returns
|
|
565
|
+
-------
|
|
566
|
+
float
|
|
567
|
+
Current control signal u[k]
|
|
568
|
+
"""
|
|
569
|
+
now = time.monotonic()
|
|
570
|
+
|
|
571
|
+
if zoh is True:
|
|
572
|
+
try:
|
|
573
|
+
dt = now - self._last_time if now - self._last_time else 1e-16
|
|
574
|
+
except TypeError:
|
|
575
|
+
dt = 1e-16
|
|
576
|
+
|
|
577
|
+
if zoh is True and dt < self.delta and self._last_output is not None:
|
|
578
|
+
# Return last output of the controller if dt < delta
|
|
579
|
+
return self._last_output
|
|
580
|
+
|
|
581
|
+
# Compute the combined transfer function output c(k)
|
|
582
|
+
ck = self.coeff['g0'] * y + self.states['x1']
|
|
583
|
+
# Compute unlimited control output u(k)
|
|
584
|
+
u = self.coeff['k'] * r - ck
|
|
585
|
+
# Limit controller output to obtain u_lim(k)
|
|
586
|
+
u_lim = self._limiter(u)
|
|
587
|
+
|
|
588
|
+
# Update storage variables x_n
|
|
589
|
+
self._update_xn(ck, u_lim, y)
|
|
590
|
+
|
|
591
|
+
self._last_output = float(u_lim)
|
|
592
|
+
self._last_time = now
|
|
593
|
+
return float(u_lim)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class TransferFunction(object):
|
|
597
|
+
|
|
598
|
+
"""Discrete time linear active disturbance rejection control\
|
|
599
|
+
in transfer function representation
|
|
600
|
+
|
|
601
|
+
Parameters
|
|
602
|
+
----------
|
|
603
|
+
order : int
|
|
604
|
+
first- or second-order ADRC TF
|
|
605
|
+
delta : float
|
|
606
|
+
sampling time in seconds
|
|
607
|
+
b0 : float
|
|
608
|
+
modelling parameter b0
|
|
609
|
+
w_cl : float
|
|
610
|
+
desired closed-loop bandwidth [rad/s], 4 / w_cl and 6 / w_cl is the
|
|
611
|
+
corresponding settling time in seconds for first- and second-order ADRC
|
|
612
|
+
respectively
|
|
613
|
+
k_eso : float
|
|
614
|
+
observer bandwidth is parametrized as k_eso-multiple faster than w_cl
|
|
615
|
+
method : str, optional, 'general_terms' or 'bandwidth'
|
|
616
|
+
method with which the transfer function parameters are calculated,
|
|
617
|
+
functionally identical, by default 'general_terms'
|
|
618
|
+
|
|
619
|
+
References
|
|
620
|
+
----------
|
|
621
|
+
.. [4] G. Herbst, Transfer Function Analysis and
|
|
622
|
+
Implementation of Active Disturbance Rejection Control
|
|
623
|
+
https://arxiv.org/abs/2011.01044
|
|
624
|
+
"""
|
|
625
|
+
|
|
626
|
+
def __init__(self, order: int, delta: float, b0: float,
|
|
627
|
+
w_cl: float, k_eso: float,
|
|
628
|
+
method: str = 'general_terms') -> None:
|
|
629
|
+
|
|
630
|
+
assert order == 1 or order == 2, 'First- and second-order ADRC TF\
|
|
631
|
+
implemented'
|
|
632
|
+
|
|
633
|
+
# Define attributes
|
|
634
|
+
self.order = order
|
|
635
|
+
self.acc = 0.
|
|
636
|
+
zESO = np.exp(-k_eso * w_cl * delta)
|
|
637
|
+
|
|
638
|
+
self.params = self._calculate_parameters(order, delta, b0, w_cl, zESO,
|
|
639
|
+
method=method)
|
|
640
|
+
|
|
641
|
+
self._calculate_coeffs()
|
|
642
|
+
self._create_states()
|
|
643
|
+
|
|
644
|
+
@property
|
|
645
|
+
def accumulator(self):
|
|
646
|
+
"""Get accumulator
|
|
647
|
+
|
|
648
|
+
Returns
|
|
649
|
+
-------
|
|
650
|
+
float
|
|
651
|
+
accumulator output
|
|
652
|
+
"""
|
|
653
|
+
return self.acc
|
|
654
|
+
|
|
655
|
+
@accumulator.setter
|
|
656
|
+
def accumulator(self, value):
|
|
657
|
+
"""Set accumulator output to value
|
|
658
|
+
|
|
659
|
+
Parameters
|
|
660
|
+
----------
|
|
661
|
+
value : float
|
|
662
|
+
value to set the accumulator
|
|
663
|
+
"""
|
|
664
|
+
self.acc = float(value)
|
|
665
|
+
|
|
666
|
+
@property
|
|
667
|
+
def parameters(self):
|
|
668
|
+
"""Get parameters
|
|
669
|
+
|
|
670
|
+
Returns
|
|
671
|
+
-------
|
|
672
|
+
dict
|
|
673
|
+
calculated paramaters
|
|
674
|
+
"""
|
|
675
|
+
return self.params
|
|
676
|
+
|
|
677
|
+
def _create_states(self):
|
|
678
|
+
"""Create state vectors to store past values for transfer functions
|
|
679
|
+
"""
|
|
680
|
+
|
|
681
|
+
if self.order == 1:
|
|
682
|
+
|
|
683
|
+
# Reference prefilter
|
|
684
|
+
self.r_k = deque([0, 0], maxlen=2)
|
|
685
|
+
self.r_yk = deque([0], maxlen=1)
|
|
686
|
+
|
|
687
|
+
# Feedback controller
|
|
688
|
+
self.e_k = deque([0], maxlen=1)
|
|
689
|
+
self.u_k = deque([0], maxlen=1)
|
|
690
|
+
|
|
691
|
+
else:
|
|
692
|
+
|
|
693
|
+
# Reference prefilter
|
|
694
|
+
self.r_k = deque([0, 0, 0], maxlen=3)
|
|
695
|
+
self.r_yk = deque([0, 0], maxlen=2)
|
|
696
|
+
|
|
697
|
+
# Feedback controller
|
|
698
|
+
self.e_k = deque([0, 0], maxlen=2)
|
|
699
|
+
self.u_k = deque([0, 0], maxlen=2)
|
|
700
|
+
|
|
701
|
+
def _control_loop(self, y, r_k):
|
|
702
|
+
|
|
703
|
+
if self.order == 1:
|
|
704
|
+
|
|
705
|
+
# reference prefilter
|
|
706
|
+
ref_yk = (r_k * self.pf['num'][0]
|
|
707
|
+
+ self.r_k[0] * self.pf['num'][1]
|
|
708
|
+
+ self.r_k[1] * self.pf['num'][2]
|
|
709
|
+
- self.r_yk[0] * self.pf['den'][1])
|
|
710
|
+
|
|
711
|
+
# advance time
|
|
712
|
+
self.r_k.appendleft(r_k)
|
|
713
|
+
self.r_yk.appendleft(ref_yk)
|
|
714
|
+
|
|
715
|
+
# calculate error
|
|
716
|
+
e_k = ref_yk - y
|
|
717
|
+
|
|
718
|
+
# feedback controller
|
|
719
|
+
u_k = (e_k * self.fb['num'][0]
|
|
720
|
+
+ self.e_k[0] * self.fb['num'][1]
|
|
721
|
+
- self.u_k[0] * self.fb['den'][1])
|
|
722
|
+
|
|
723
|
+
# advance time
|
|
724
|
+
self.e_k.appendleft(e_k)
|
|
725
|
+
self.u_k.appendleft(u_k)
|
|
726
|
+
|
|
727
|
+
else: # self.order == 2
|
|
728
|
+
|
|
729
|
+
# reference prefilter
|
|
730
|
+
ref_yk = (r_k * self.pf['num'][0]
|
|
731
|
+
+ self.r_k[0] * self.pf['num'][1]
|
|
732
|
+
+ self.r_k[1] * self.pf['num'][2]
|
|
733
|
+
+ self.r_k[2] * self.pf['num'][3]
|
|
734
|
+
- self.r_yk[0] * self.pf['den'][1]
|
|
735
|
+
- self.r_yk[1] * self.pf['den'][2])
|
|
736
|
+
|
|
737
|
+
# advance time
|
|
738
|
+
self.r_k.appendleft(r_k)
|
|
739
|
+
self.r_yk.appendleft(ref_yk)
|
|
740
|
+
|
|
741
|
+
# calculate error
|
|
742
|
+
e_k = ref_yk - y
|
|
743
|
+
|
|
744
|
+
# feedback controller
|
|
745
|
+
u_k = (e_k * self.fb['num'][0]
|
|
746
|
+
+ self.e_k[0] * self.fb['num'][1]
|
|
747
|
+
+ self.e_k[1] * self.fb['num'][2]
|
|
748
|
+
- self.u_k[0] * self.fb['den'][1]
|
|
749
|
+
- self.u_k[1] * self.fb['den'][2])
|
|
750
|
+
|
|
751
|
+
# advance time
|
|
752
|
+
self.e_k.appendleft(e_k)
|
|
753
|
+
self.u_k.appendleft(u_k)
|
|
754
|
+
|
|
755
|
+
# Accumulate feedback controller output
|
|
756
|
+
self.accumulator = self.accumulator + u_k
|
|
757
|
+
|
|
758
|
+
return self.accumulator
|
|
759
|
+
|
|
760
|
+
def _calculate_coeffs(self):
|
|
761
|
+
"""Internal function to calculate the coefficients for the reference
|
|
762
|
+
prefilter and feedback controller (denominator and numerator)
|
|
763
|
+
Uses the z^-1 notation, coefficients in descending order, i.e
|
|
764
|
+
z^0, z^-1, ..., z^-n
|
|
765
|
+
"""
|
|
766
|
+
|
|
767
|
+
if self.order == 1:
|
|
768
|
+
fb = {'num': [self.params['be0'], self.params['be1']],
|
|
769
|
+
'den': [1, self.params['a1']]}
|
|
770
|
+
pf = {'num': [self.params['g0'],
|
|
771
|
+
self.params['g1'], self.params['g2']],
|
|
772
|
+
'den': [1, self.params['be1'] / self.params['be0']]}
|
|
773
|
+
else:
|
|
774
|
+
fb = {'num': [self.params['be0'], self.params['be1'],
|
|
775
|
+
self.params['be2']],
|
|
776
|
+
'den': [1, self.params['a1'], self.params['a2']]}
|
|
777
|
+
pf = {'num': [self.params['g0'], self.params['g1'],
|
|
778
|
+
self.params['g2'], self.params['g3']],
|
|
779
|
+
'den': [1, self.params['be1'] / self.params['be0'],
|
|
780
|
+
self.params['be2'] / self.params['be0']]}
|
|
781
|
+
|
|
782
|
+
self.fb = fb
|
|
783
|
+
self.pf = pf
|
|
784
|
+
|
|
785
|
+
def _calculate_parameters(self, order, delta, b0,
|
|
786
|
+
w_cl, zESO, method='general_terms'):
|
|
787
|
+
|
|
788
|
+
"""Calculate the coefficients of num and den of TransferFunction ADRC,
|
|
789
|
+
using either general terms or bandwidth parameterization
|
|
790
|
+
|
|
791
|
+
Parameters
|
|
792
|
+
----------
|
|
793
|
+
order : int
|
|
794
|
+
internal variable
|
|
795
|
+
delta : float
|
|
796
|
+
internal variable
|
|
797
|
+
b0 : float
|
|
798
|
+
internal variable
|
|
799
|
+
w_cl : float
|
|
800
|
+
internal variable
|
|
801
|
+
zESO : float
|
|
802
|
+
internal variable
|
|
803
|
+
method : str, optional
|
|
804
|
+
Calculation via general terms (i.e. controller and observer gains)
|
|
805
|
+
or bandwidth parametrization, functionally identical,
|
|
806
|
+
by default 'general_terms'
|
|
807
|
+
|
|
808
|
+
Returns
|
|
809
|
+
-------
|
|
810
|
+
list
|
|
811
|
+
Ordered transfer function parameters
|
|
812
|
+
|
|
813
|
+
Notes
|
|
814
|
+
-----
|
|
815
|
+
* For first-order ADRC: [a1, be0, be1, g0, g1, g2]
|
|
816
|
+
* For second-order ADRC: [a1, a2, be0, be1, be2, g0, g1, g2, g3]
|
|
817
|
+
"""
|
|
818
|
+
|
|
819
|
+
if method == 'general_terms':
|
|
820
|
+
if order == 1:
|
|
821
|
+
# Controller gains
|
|
822
|
+
k1 = w_cl
|
|
823
|
+
|
|
824
|
+
# Observer gains
|
|
825
|
+
l1 = 1 - zESO**2
|
|
826
|
+
l2 = (1 / delta) * (1 - zESO)**2
|
|
827
|
+
|
|
828
|
+
_C_TERM = (k1 * l1 + l2)
|
|
829
|
+
|
|
830
|
+
a1 = (delta * k1 - 1) * (1 - l1)
|
|
831
|
+
be0 = (1 / b0) * _C_TERM
|
|
832
|
+
be1 = (1 / b0) * (delta * k1 * l2 - k1 * l1 - l2)
|
|
833
|
+
g0 = k1 / _C_TERM
|
|
834
|
+
g1 = k1 * (delta * l2 + l1 - 2) / _C_TERM
|
|
835
|
+
g2 = k1 * (1 - l1) / _C_TERM
|
|
836
|
+
|
|
837
|
+
elif order == 2:
|
|
838
|
+
|
|
839
|
+
k1 = w_cl**2
|
|
840
|
+
k2 = 2 * w_cl
|
|
841
|
+
|
|
842
|
+
l1 = 1 - zESO**3
|
|
843
|
+
l2 = (3 / (2 * delta)) * (1 - zESO)**2 * (1 + zESO)
|
|
844
|
+
l3 = (1 / delta**2) * (1 - zESO)**3
|
|
845
|
+
|
|
846
|
+
_C_TERM = (k1 * l1 + k2 * l2 + l3)
|
|
847
|
+
|
|
848
|
+
a1 = ((delta ** 2 / 2) * (k1 - k1 * l1 - k2 * l2)
|
|
849
|
+
+ delta * k2 + delta * l2 + l1 - 2)
|
|
850
|
+
|
|
851
|
+
a2 = ((((delta**2 * k1) / 2) - delta * k2 + 1) * (1 - l1))
|
|
852
|
+
|
|
853
|
+
be0 = (1 / b0) * _C_TERM
|
|
854
|
+
|
|
855
|
+
be1 = ((1 / b0) * (((delta**2 * k1 * l3) / 2)
|
|
856
|
+
+ delta * k1 * l2 + delta
|
|
857
|
+
* k2 * l3 - 2
|
|
858
|
+
* _C_TERM))
|
|
859
|
+
|
|
860
|
+
be2 = ((1 / b0) * (((delta**2 * k1 * l3) / 2)
|
|
861
|
+
- delta * k1 * l2 - delta
|
|
862
|
+
* k2 * l3 + _C_TERM))
|
|
863
|
+
|
|
864
|
+
g0 = k1 / _C_TERM
|
|
865
|
+
|
|
866
|
+
g1 = ((k1 * (delta**2 * l3 + 2 * delta * l2 + 2 * l1 - 6))
|
|
867
|
+
/ (2 * _C_TERM))
|
|
868
|
+
|
|
869
|
+
g2 = ((k1 * (delta**2 * l3 - 2 * delta * l2 - 4 * l1 + 6))
|
|
870
|
+
/ (2 * _C_TERM))
|
|
871
|
+
|
|
872
|
+
g3 = (k1 * (l1 - 1)) / _C_TERM
|
|
873
|
+
|
|
874
|
+
elif method == 'bandwidth':
|
|
875
|
+
if order == 1:
|
|
876
|
+
|
|
877
|
+
_C_TERM = ((delta * w_cl) * (1 - zESO**2) + (1 - zESO)**2)
|
|
878
|
+
|
|
879
|
+
a1 = (delta * w_cl - 1) * zESO**2
|
|
880
|
+
|
|
881
|
+
be0 = ((1/(b0 * delta)) * _C_TERM)
|
|
882
|
+
|
|
883
|
+
be1 = ((1/(b0 * delta)) * ((-2 * delta * w_cl * zESO)
|
|
884
|
+
* (1 - zESO) - (1 - zESO)**2))
|
|
885
|
+
|
|
886
|
+
g0 = (delta * w_cl) / _C_TERM
|
|
887
|
+
g1 = (-2 * delta * w_cl * zESO) / _C_TERM
|
|
888
|
+
g2 = (delta * w_cl * zESO**2) / _C_TERM
|
|
889
|
+
|
|
890
|
+
elif order == 2:
|
|
891
|
+
|
|
892
|
+
_C_TERM = (delta**2 * w_cl**2 * (1 - zESO**3)
|
|
893
|
+
+ 3 * delta * w_cl * (1 - zESO - zESO**2 + zESO**3)
|
|
894
|
+
+ (1 - zESO)**3)
|
|
895
|
+
|
|
896
|
+
a1 = ((1/2) * (delta**2 * w_cl**2 * zESO**3
|
|
897
|
+
+ delta * w_cl
|
|
898
|
+
* (1 + 3 * zESO + 3 * zESO**2 - 3 * zESO**3)
|
|
899
|
+
+ (1 - 3 * zESO - 3 * zESO**2 + zESO**3)))
|
|
900
|
+
|
|
901
|
+
a2 = ((zESO**3 / 2)
|
|
902
|
+
* (delta**2 * w_cl**2 - 4 * delta * w_cl + 2))
|
|
903
|
+
|
|
904
|
+
be0 = ((1 / (b0 * delta**2)) * _C_TERM)
|
|
905
|
+
|
|
906
|
+
be1 = ((1 / (b0 * delta**2))
|
|
907
|
+
* (-3 * delta**2 * w_cl**2 * zESO * (1 - zESO**2)
|
|
908
|
+
- 4 * delta * w_cl * (1 - 3 * zESO**2 + 2 * zESO**3)
|
|
909
|
+
- 2 * (1 - zESO)**3))
|
|
910
|
+
|
|
911
|
+
be2 = ((1 / (b0 * delta**2))
|
|
912
|
+
* (3 * delta**2 * w_cl**2 * zESO**2 * (1 - zESO)
|
|
913
|
+
+ delta * w_cl
|
|
914
|
+
* (1 + 3 * zESO - 9 * zESO**2 + 5 * zESO**3)
|
|
915
|
+
+ (1 - zESO)**3))
|
|
916
|
+
|
|
917
|
+
g0 = (delta**2 * w_cl**2) / _C_TERM
|
|
918
|
+
g1 = (-3 * delta**2 * w_cl**2 * zESO) / _C_TERM
|
|
919
|
+
g2 = (3 * delta**2 * w_cl**2 * zESO**2) / _C_TERM
|
|
920
|
+
g3 = (-1 * delta**2 * w_cl**2 * zESO**3) / _C_TERM
|
|
921
|
+
|
|
922
|
+
if order == 1:
|
|
923
|
+
params = {'a1': a1, 'be0': be0,
|
|
924
|
+
'be1': be1, 'g0': g0,
|
|
925
|
+
'g1': g1, 'g2': g2}
|
|
926
|
+
else:
|
|
927
|
+
params = {'a1': a1, 'a2': a2, 'be0': be0,
|
|
928
|
+
'be1': be1, 'be2': be2, 'g0': g0,
|
|
929
|
+
'g1': g1, 'g2': g2, 'g3': g3}
|
|
930
|
+
|
|
931
|
+
return params
|
|
932
|
+
|
|
933
|
+
def __call__(self, y, r):
|
|
934
|
+
"""Control loop call
|
|
935
|
+
|
|
936
|
+
Parameters
|
|
937
|
+
----------
|
|
938
|
+
y : float
|
|
939
|
+
current output signal
|
|
940
|
+
r : float
|
|
941
|
+
current reference signal
|
|
942
|
+
|
|
943
|
+
Returns
|
|
944
|
+
-------
|
|
945
|
+
float
|
|
946
|
+
control signal to the plant (accumulator output)
|
|
947
|
+
"""
|
|
948
|
+
|
|
949
|
+
return self._control_loop(y, r)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyadrc
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Active Disturbance Rejection Control for Python
|
|
5
|
+
Author-email: Ongun Türkcüoglu <onguntoglu@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/onguntoglu/pyadrc
|
|
8
|
+
Project-URL: Documentation, https://pyadrc.readthedocs.io
|
|
9
|
+
Keywords: pyadrc,adrc,control
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
License-File: AUTHORS.rst
|
|
22
|
+
Requires-Dist: numpy
|
|
23
|
+
Requires-Dist: scipy
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff; extra == "dev"
|
|
27
|
+
Requires-Dist: control; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# pyadrc's Documentation
|
|
31
|
+
|
|
32
|
+
[](https://pypi.python.org/pypi/pyadrc)
|
|
33
|
+
[](https://github.com/onguntoglu/pyadrc/actions/workflows/ci.yml)
|
|
34
|
+
[](https://pyadrc.readthedocs.io/en/latest/?badge=latest)
|
|
35
|
+
[](https://github.com/onguntoglu/pyadrc/blob/master/LICENSE)
|
|
36
|
+
[](https://pepy.tech/project/pyadrc)
|
|
37
|
+
|
|
38
|
+
Active Disturbance Rejection Control for Python
|
|
39
|
+
|
|
40
|
+
A simple, yet powerful control method for when a PID controller just doesn't cut it.
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- Discrete linear time invariant active disturbance rejection controller for digital control systems.
|
|
45
|
+
- First- and second-order ADRC in state-space, transfer function and minimum-footprint (FeedbackTF) form.
|
|
46
|
+
- For practical application - half-gain tuning, magnitude and rate limiter for actuator limitations
|
|
47
|
+
- User guide and/or theoretical background (for when you don't have anything else to read).
|
|
48
|
+
- A first/second-order LTI model generator and a quadcopter altitude model for experimentation, testing and verification.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
Installing **pyadrc** is really simple, just run this command in your terminal:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install pyadrc
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
or you can clone the repository and install it manually:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone https://github.com/onguntoglu/pyadrc
|
|
62
|
+
cd pyadrc
|
|
63
|
+
pip install .
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Licence
|
|
67
|
+
|
|
68
|
+
**pyadrc** is licenced under the MIT Licence
|
|
69
|
+
|
|
70
|
+
## Acknowledgements
|
|
71
|
+
|
|
72
|
+
None of this work would be possible without **Gernot Herbst** and his papers:
|
|
73
|
+
|
|
74
|
+
- *[Practical Active Disturbance Rejection Control: Bumpless Transfer, Rate Limitation and Incremental Algorithm](https://arxiv.org/abs/1908.04610)*
|
|
75
|
+
- *[A Simulative Study on Active Disturbance Rejection Control (ADRC) as a Control Tool for Practitioners](https://arxiv.org/abs/1908.04596)*
|
|
76
|
+
- *[Transfer Function Analysis and Implementation of Active Disturbance Rejection Control](https://arxiv.org/abs/2011.01044)*
|
|
77
|
+
- *[Half-Gain Tuning for Active Disturbance Rejection Control](https://arxiv.org/abs/2003.03986)*
|
|
78
|
+
- *[A Minimum-Footprint Implementation of Discrete-Time ADRC](https://arxiv.org/abs/2104.01943)*
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pyadrc/__init__.py,sha256=EjRk0BEOE8D0dllkwHj14Dtwr5aJJUCijh1WXWmb614,373
|
|
2
|
+
pyadrc/models.py,sha256=C9srGX5FJBTO9sunvwXvCQrVrRQrkIX74bt_d6WGl5I,4477
|
|
3
|
+
pyadrc/pyadrc.py,sha256=wx6OZvWo3UKQB_3aGm26cxvDzSGwZ8xjfzuG5dTil0c,30169
|
|
4
|
+
pyadrc-0.5.0.dist-info/licenses/AUTHORS.rst,sha256=w9A68eGmFO3a9taKcnJh_NWUZbkj38cuPTLLiOB31Gc,164
|
|
5
|
+
pyadrc-0.5.0.dist-info/licenses/LICENSE,sha256=_0pf3xnvyOF8LaQNKgm75XW9Byjp2CBgYbZbhwlpDQw,1077
|
|
6
|
+
pyadrc-0.5.0.dist-info/METADATA,sha256=4DL5qjBz0VGsXuT5mvdmEZbfIEVHNeZK_8HSNh5Jnjg,3361
|
|
7
|
+
pyadrc-0.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
pyadrc-0.5.0.dist-info/top_level.txt,sha256=DiqtzK4DShwozIn-DOeRaXW19dIqGTnuoO13_r1gF0A,7
|
|
9
|
+
pyadrc-0.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021, Ongun Türkcüoglu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyadrc
|