parent
804e9b4a6a
commit
124782ace5
@ -0,0 +1,85 @@
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
def interp(v: float, table: Optional[Tuple]) -> float:
|
||||
if table is None:
|
||||
return 0
|
||||
if v < table[0][0]:
|
||||
return table[0][1]
|
||||
if v > table[-1][0]:
|
||||
return table[-1][1]
|
||||
for i in range(1, len(table)):
|
||||
if v <= table[i][0]:
|
||||
x0, y0 = table[i - 1]
|
||||
x1, y1 = table[i]
|
||||
x = (v - x0) / (x1 - x0)
|
||||
return y0 + (y1 - y0) * x
|
||||
return table[-1][1]
|
||||
|
||||
|
||||
class PID:
|
||||
def __init__(self,
|
||||
kp: float = 0,
|
||||
ki: float = 0,
|
||||
kd: float = 0,
|
||||
ff: tuple = None,
|
||||
deadband: Optional[tuple] = None,
|
||||
windup_limit: Optional[float] = None,
|
||||
lower_limit: float = None,
|
||||
upper_limit: float = None):
|
||||
self._kp = kp
|
||||
self._ki = ki
|
||||
self._kd = kd
|
||||
self._windup_limit = windup_limit
|
||||
self._ff = ff
|
||||
self._deadband = deadband
|
||||
self._lower_limit = lower_limit if lower_limit is not None else -1e12
|
||||
self._upper_limit = upper_limit if upper_limit is not None else 1e12
|
||||
self.enabled = True
|
||||
|
||||
self._sp = 0.
|
||||
self._ti = 0.
|
||||
self._errs = [0.] * 3
|
||||
self._t: Optional[float] = None
|
||||
|
||||
def step(self, t: float, pv: float) -> float:
|
||||
err = self._sp - pv
|
||||
|
||||
td = 0
|
||||
if self._t is not None:
|
||||
dt = t - self._t
|
||||
self._ti += err * dt
|
||||
|
||||
if self._windup_limit is not None:
|
||||
self._ti = max(-self._windup_limit, min(self._windup_limit, self._ti))
|
||||
self._t = t
|
||||
|
||||
if not self.enabled:
|
||||
self._ti = 0
|
||||
|
||||
u = self._kp * err + self._ki * self._ti + self._kd * td + interp(pv, self._ff)
|
||||
if self._deadband is not None:
|
||||
if u >= self._deadband[0]:
|
||||
u += self._deadband[1]
|
||||
elif u <= -self._deadband[0]:
|
||||
u -= self._deadband[1]
|
||||
u = min(self._upper_limit, max(self._lower_limit, u))
|
||||
|
||||
return u
|
||||
|
||||
class LPF:
|
||||
def __init__(self):
|
||||
self.c_ = 0
|
||||
self.vs_ = (0., 0., 0.)
|
||||
self.fs_ = (0., 0., 0.)
|
||||
|
||||
def step(self, v: float) -> float:
|
||||
c = self.c_
|
||||
vs = (v, self.vs_[0], self.vs_[1])
|
||||
fs = self.fs_
|
||||
f = (1 / (1 + c * c + 1.414 * c)) * (vs[2] + 2 * vs[1] + vs[0] -
|
||||
(c * c - 1.414 * c + 1) * fs[1] -
|
||||
(-2 * c * c + 2) * fs[0])
|
||||
self.vs_ = vs
|
||||
self.fs_ = (f, fs[0], fs[1])
|
||||
return f
|
@ -0,0 +1,41 @@
|
||||
import time
|
||||
|
||||
|
||||
class Rate:
|
||||
def __init__(self, cap):
|
||||
self._cap = cap
|
||||
self._t = None
|
||||
|
||||
def step(self, t):
|
||||
if self._t is None:
|
||||
self._t = t
|
||||
return True
|
||||
|
||||
elapsed = t - self._t
|
||||
if elapsed < self._cap:
|
||||
return False
|
||||
self._t += self._cap
|
||||
return True
|
||||
|
||||
|
||||
class SlewLimiter:
|
||||
def __init__(self, rate: float, now=time.time):
|
||||
self._rate = rate
|
||||
self._a = None
|
||||
self._t = None
|
||||
self._now = now
|
||||
|
||||
def __call__(self, v: float) -> float:
|
||||
if self._rate is None:
|
||||
return v
|
||||
|
||||
now = self._now()
|
||||
if self._t is None:
|
||||
self._v = v
|
||||
else:
|
||||
limit = self._rate * (now - self._t)
|
||||
delta = min(limit, max(-limit, v - self._v))
|
||||
self._v += delta
|
||||
|
||||
self._t = now
|
||||
return self._v
|
@ -0,0 +1,67 @@
|
||||
import controller
|
||||
|
||||
|
||||
def test_interp():
|
||||
table = (
|
||||
(5, 10),
|
||||
(6, 15),
|
||||
(8, 20),
|
||||
)
|
||||
# Below gives minimum
|
||||
assert controller.interp(4, table) == 10
|
||||
# Above gives maximum
|
||||
assert controller.interp(8.1, table) == 20
|
||||
# Between is interpolated
|
||||
assert controller.interp(5.5, table) == 12.5
|
||||
assert controller.interp(7, table) == 17.5
|
||||
|
||||
|
||||
def test_pid_kp():
|
||||
p = controller.PID(kp=5)
|
||||
# u = Kp * err
|
||||
assert p.step(0, 10) == -50
|
||||
|
||||
|
||||
def test_pid_ki():
|
||||
p = controller.PID(ki=4, windup_limit=100)
|
||||
p.step(0, 10)
|
||||
# u = Ki * err over 0.5s
|
||||
assert p.step(0.5, 10) == -20
|
||||
assert p.step(0.7, 20) == -40 * 0.5 + -80 * 0.2
|
||||
# Tops out at Ki * windup_limit
|
||||
assert p.step(10, 10) == -400
|
||||
assert p.step(30, -10) == 400
|
||||
# Disabling clears the integral
|
||||
p.enabled = False
|
||||
assert p.step(31, -10) == 0
|
||||
|
||||
|
||||
def test_pid_ff():
|
||||
ff = (
|
||||
(5, 10),
|
||||
(10, 20),
|
||||
(15, 40),
|
||||
)
|
||||
p = controller.PID(ff=ff)
|
||||
# u = interp(ff, pv)
|
||||
assert p.step(1, 12) == 28
|
||||
|
||||
|
||||
def test_deadband():
|
||||
p = controller.PID(kp=1, deadband=(0.5, 1.0))
|
||||
# No deadband below limit
|
||||
assert p.step(0, 0.4) == -0.4
|
||||
assert p.step(0, -0.4) == 0.4
|
||||
# Deadband above limit
|
||||
assert p.step(0, 0.6) == -1.6
|
||||
assert p.step(0, -0.6) == 1.6
|
||||
|
||||
|
||||
def test_lpf():
|
||||
f = controller.LPF()
|
||||
|
||||
for _ in range(20):
|
||||
print(round(f.step(10), 3))
|
||||
|
||||
|
||||
test_lpf()
|
Loading…
Reference in new issue