mirror of
https://github.com/sinseman44/PyCNC.git
synced 2026-01-11 02:30:05 +00:00
pid with test
This commit is contained in:
84
cnc/pid.py
Normal file
84
cnc/pid.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import time
|
||||
import math
|
||||
|
||||
|
||||
class Pid(object):
|
||||
# PID coefficients
|
||||
P = 0.422
|
||||
I = 0.208
|
||||
D = 0.014
|
||||
WINDUP_LIMIT = 3.0
|
||||
FIX_ACCURACY = 0.01
|
||||
FIX_TIME_S = 2.5
|
||||
|
||||
def __init__(self, target_value, start_time=time.time()):
|
||||
"""
|
||||
Proportional-integral-derivative controller implementation.
|
||||
:param target_value: value which PID should achieve.
|
||||
:param start_time: start time, current system time by default.
|
||||
"""
|
||||
self._last_time = start_time
|
||||
self._target_value = target_value
|
||||
self._integral = 0
|
||||
self._last_error = 0
|
||||
self._is_target_fixed = False
|
||||
self._target_fix_timer = None
|
||||
|
||||
def update(self, current_value, current_time=time.time()):
|
||||
"""
|
||||
Update PID with new current value.
|
||||
:param current_value: current value.
|
||||
:param current_time: time when current value measured, current system
|
||||
time if not specified.
|
||||
:return: value in range 0..1.0 which represents PID output.
|
||||
"""
|
||||
delta_time = current_time - self._last_time
|
||||
self._last_time = current_time
|
||||
error = self._target_value - current_value
|
||||
self._integral += error * delta_time
|
||||
# integral windup protection
|
||||
if abs(self._integral) > self.WINDUP_LIMIT:
|
||||
self._integral = math.copysign(self.WINDUP_LIMIT, self._integral)
|
||||
delta_error = error - self._last_error
|
||||
self._last_error = error
|
||||
|
||||
res = self.P * error + self.I * self._integral + self.D * delta_error
|
||||
if res > 1.0:
|
||||
res = 1.0
|
||||
if res < 0.0:
|
||||
res = 0.0
|
||||
|
||||
if not self._is_target_fixed:
|
||||
if abs(error) < self._target_value * self.FIX_ACCURACY \
|
||||
and res < 1.0:
|
||||
if self._target_fix_timer is None:
|
||||
self._target_fix_timer = current_time
|
||||
elif current_time - self._target_fix_timer > self.FIX_TIME_S:
|
||||
self._is_target_fixed = True
|
||||
else:
|
||||
self._target_fix_timer = None
|
||||
return res
|
||||
|
||||
def is_fixed(self):
|
||||
"""
|
||||
Check if target value is reached and PID maintains this value.
|
||||
:return: boolean value
|
||||
"""
|
||||
return self._is_target_fixed
|
||||
|
||||
|
||||
# for test purpose, see details in corresponding test file
|
||||
if __name__ == "__main__":
|
||||
p = Pid(230, 0)
|
||||
c = 0.0039
|
||||
h = 3.09
|
||||
t0 = 25
|
||||
t = t0
|
||||
r = 0
|
||||
for i in range(1, 601):
|
||||
# natural cooling
|
||||
t -= (t - t0) * c
|
||||
# heating
|
||||
t += h * r
|
||||
r = p.update(t, i)
|
||||
print(i, t, r, p.is_fixed())
|
||||
66
tests/test_pid.py
Normal file
66
tests/test_pid.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import unittest
|
||||
|
||||
from cnc.pid import *
|
||||
|
||||
|
||||
class TestPid(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._environment_temp = 25
|
||||
# Coefficients below were chosen by an experimental way with a real
|
||||
# hardware: reprap heating bed and extruder.
|
||||
self._bed_c = 0.00231 # bed cooling coefficient
|
||||
self._bed_h = 0.25 # bed heating coefficient
|
||||
self._extruder_c = 0.0039 # extruder cooling coefficient
|
||||
self._extruder_h = 3.09 # extruder heating coefficient
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def __simulate(self, target_temp, environment_temp, cool, heat):
|
||||
# Simulate heating some hypothetical thing with heater(h is a heat
|
||||
# transfer coefficient, which becomes just a delta temperature each
|
||||
# second) from environment temperature to target_temp. Consider that
|
||||
# there is natural air cooling process with some heat transfer
|
||||
# coefficient c. Heating power is controlled by PID.
|
||||
pid = Pid(target_temp, 0)
|
||||
temperature = environment_temp
|
||||
heater_power = 0
|
||||
fixed_at = None
|
||||
zeros_counter = 0
|
||||
for j in range(1, 15 * 60 + 1): # simulate for 15 minutes
|
||||
# natural cooling
|
||||
temperature -= (temperature - environment_temp) * cool
|
||||
# heating
|
||||
temperature += heat * heater_power
|
||||
heater_power = pid.update(temperature, j)
|
||||
if fixed_at is None:
|
||||
if pid.is_fixed():
|
||||
fixed_at = j
|
||||
else:
|
||||
self.assertLess(abs(temperature - target_temp),
|
||||
pid.FIX_ACCURACY * target_temp,
|
||||
msg="PID failed to control temperature "
|
||||
"{}/{} {}".format(temperature, target_temp, j))
|
||||
if heater_power == 0.0:
|
||||
zeros_counter += 1
|
||||
self.assertLess(zeros_counter, 20, msg="PID turns on/off, instead of "
|
||||
"fine control")
|
||||
self.assertLess(fixed_at, 600,
|
||||
msg="failed to heat in 10 minutes, final temperature "
|
||||
"{}/{}".format(temperature, target_temp))
|
||||
|
||||
def test_bed(self):
|
||||
# check if bed typical temperatures can be reached in simulation
|
||||
for target in range(50, 101, 10):
|
||||
self.__simulate(target, self._environment_temp,
|
||||
self._bed_c, self._bed_h)
|
||||
|
||||
def test_extruder(self):
|
||||
# check if extruder typical temperatures can be reached in simulation
|
||||
for target in range(150, 251, 10):
|
||||
self.__simulate(target, self._environment_temp,
|
||||
self._extruder_c, self._extruder_h)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user