Skip to content

Commit c378f26

Browse files
committed
first version
1 parent 4478f11 commit c378f26

File tree

2 files changed

+147
-2
lines changed

2 files changed

+147
-2
lines changed

README.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
1-
# pid
2-
Minimal PID controller
1+
# tiny pid
2+
Minimal PID controller in Python.
3+
4+
Optionally can use
5+
- output limiting
6+
- anti-windup mechanism
7+
- lowpass filtering of derivative component
8+
- bumpless transfer between manual and automatic control
9+

tinypid/pid.py

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
from typing import Optional, Tuple
2+
3+
4+
class PID:
5+
"""
6+
A simple Proportional-Integral-Derivative controller
7+
with optional
8+
- output limiting
9+
- anti-windup mechanism
10+
- lowpass filtering of derivative component
11+
- bumpless transfer
12+
"""
13+
14+
def __init__(
15+
self,
16+
K_p: float = 1,
17+
K_i: float = 0.1,
18+
K_d: float = 0,
19+
setpoint: float = 0,
20+
dt: float = 1,
21+
derivative_lowpass: float = 1,
22+
upper_limit: Optional[float] = None,
23+
lower_limit: Optional[float] = None,
24+
) -> None:
25+
"""
26+
Initialize PID controller
27+
28+
Parameters:
29+
K_p : Proportional gain
30+
K_i : Integral gain
31+
K_d : Derivative gain
32+
dt : Time step
33+
derivative_lowpass: lowpass constant (between 1 and 0, 1 meaning no lowpass)
34+
upper_limit : Upper limit for the output
35+
lower_limit : Lower limit for the output
36+
"""
37+
if dt <= 0:
38+
raise ValueError("Time step (dt) must be positive.")
39+
40+
self.K_p = K_p
41+
self.K_i = K_i
42+
self.K_d = K_d
43+
self.P, self.I, self.D = None, None, None
44+
self.dt = dt
45+
self.alpha = derivative_lowpass
46+
self._setpoint = setpoint
47+
self.integral = 0
48+
self._previous_error = 0
49+
self._previous_derivative = 0
50+
self.upper_limit = upper_limit
51+
self.lower_limit = lower_limit
52+
53+
def reset(self) -> None:
54+
"""
55+
Clear the history.
56+
"""
57+
self.integral = 0
58+
self._previous_error = 0
59+
60+
@property
61+
def setpoint(self) -> float:
62+
"""
63+
Get the setpoint.
64+
"""
65+
return self._setpoint
66+
67+
@setpoint.setter
68+
def setpoint(self, value: float) -> None:
69+
"""
70+
Set the setpoint.
71+
72+
Parameters:
73+
value : The new setpoint.
74+
"""
75+
self._setpoint = value
76+
self._previous_error = 0
77+
78+
def limit(self, output: float) -> Tuple[bool, float]:
79+
"""
80+
Limits the output to the specified bounds.
81+
82+
Parameters:
83+
output : The given output.
84+
85+
Returns:
86+
tuple: A tuple containing a boolean indicating whether saturation occurred
87+
and the limited output.
88+
"""
89+
unlimited = output
90+
if self.upper_limit is not None:
91+
output = min(output, self.upper_limit)
92+
if self.lower_limit is not None:
93+
output = max(output, self.lower_limit)
94+
saturated = output != unlimited
95+
96+
return saturated, output
97+
98+
def __call__(self, process_variable : float, manual_output: Optional[float] = None, anti_windup: bool = True) -> float:
99+
"""
100+
Process the input signal and return the controller output.
101+
102+
Parameters:
103+
manual_output: Manually controlled output value (optional).
104+
anti_windup : Whether to enable anti-windup mechanism.
105+
"""
106+
107+
error = self._setpoint - process_variable
108+
self.integral += error * self.dt
109+
derivative = (error - self._previous_error) / self.dt if self.dt != 0 else 0
110+
111+
self.P = self.K_p * error
112+
self.I = self.K_i * self.integral
113+
self.D = self.K_d * (
114+
self.alpha * derivative + (1 - self.alpha) * self._previous_derivative
115+
)
116+
117+
output = self.P + self.I + self.D
118+
119+
self._previous_error = error
120+
self._previous_derivative = derivative
121+
122+
saturated, output = self.limit(output)
123+
124+
if saturated and anti_windup:
125+
# Don't increase integral if we are saturated
126+
self.integral -= error * self.dt
127+
128+
if manual_output:
129+
# Use setpoint tracking by calculating integral so that the output matches the manual setpoint
130+
self.integral = -(self.P + self.D - manual_output) / self.K_i
131+
output = manual_output
132+
133+
return output
134+
135+
def __repr__(self):
136+
return (f"PID controller\nSetpoint: {self.setpoint}, Output: {self.P + self.I + self.D}\n"
137+
f"P: {self.P}, I: {self.I}, D: {self.D}\n"\
138+
f"Limits: {self.lower_limit} < output < {self.upper_limit}")

0 commit comments

Comments
 (0)