pid_calibrate: Move PID calibration logic from heater.py to new file

Drop support for M303 and PID_TUNE, and replace it with a new
PID_CALIBRATE command.  Move the logic for this command from heater.py
to a new pid_calibrate.py file in the extras/ directory.

Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
This commit is contained in:
Kevin O'Connor
2018-03-18 11:23:20 -04:00
parent 310cdf88cc
commit 973ef97143
5 changed files with 147 additions and 147 deletions

View File

@@ -0,0 +1,127 @@
# Calibration of heater PID settings
#
# Copyright (C) 2016-2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging
import extruder, heater
class PIDCalibrate:
def __init__(self, config):
self.printer = config.get_printer()
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command(
'PID_CALIBRATE', self.cmd_PID_CALIBRATE,
desc=self.cmd_PID_CALIBRATE_help)
cmd_PID_CALIBRATE_help = "Run PID calibration test"
def cmd_PID_CALIBRATE(self, params):
heater_name = self.gcode.get_str('HEATER', params)
target = self.gcode.get_float('TARGET', params)
write_file = self.gcode.get_int('WRITE_FILE', params, 0)
try:
heater = extruder.get_printer_heater(self.printer, heater_name)
except self.printer.config_error as e:
raise self.gcode.error(str(e))
print_time = self.printer.lookup_object('toolhead').get_last_move_time()
calibrate = ControlAutoTune(heater)
old_control = heater.set_control(calibrate)
try:
heater.set_temp(print_time, target)
except heater.error as e:
raise self.gcode.error(str(e))
self.gcode.bg_temp(heater)
heater.set_control(old_control)
if write_file:
calibrate.write_file('/tmp/heattest.txt')
Kp, Ki, Kd = calibrate.calc_final_pid()
logging.info("Autotune: final: Kp=%f Ki=%f Kd=%f", Kp, Ki, Kd)
self.gcode.respond_info(
"PID parameters: pid_Kp=%.3f pid_Ki=%.3f pid_Kd=%.3f\n"
"To use these parameters, update the printer config file with\n"
"the above and then issue a RESTART command" % (Kp, Ki, Kd))
TUNE_PID_DELTA = 5.0
class ControlAutoTune:
def __init__(self, heater):
self.heater = heater
# Heating control
self.heating = False
self.peak = 0.
self.peak_time = 0.
# Peak recording
self.peaks = []
# Sample recording
self.last_pwm = 0.
self.pwm_samples = []
self.temp_samples = []
# Heater control
def set_pwm(self, read_time, value):
if value != self.last_pwm:
self.pwm_samples.append((read_time + heater.PWM_DELAY, value))
self.last_pwm = value
self.heater.set_pwm(read_time, value)
def adc_callback(self, read_time, temp):
self.temp_samples.append((read_time, temp))
if self.heating and temp >= self.heater.target_temp:
self.heating = False
self.check_peaks()
elif (not self.heating
and temp <= self.heater.target_temp - TUNE_PID_DELTA):
self.heating = True
self.check_peaks()
if self.heating:
self.set_pwm(read_time, self.heater.max_power)
if temp < self.peak:
self.peak = temp
self.peak_time = read_time
else:
self.set_pwm(read_time, 0.)
if temp > self.peak:
self.peak = temp
self.peak_time = read_time
def check_busy(self, eventtime):
if self.heating or len(self.peaks) < 12:
return True
return False
# Analysis
def check_peaks(self):
self.peaks.append((self.peak, self.peak_time))
if self.heating:
self.peak = 9999999.
else:
self.peak = -9999999.
if len(self.peaks) < 4:
return
self.calc_pid(len(self.peaks)-1)
def calc_pid(self, pos):
temp_diff = self.peaks[pos][0] - self.peaks[pos-1][0]
time_diff = self.peaks[pos][1] - self.peaks[pos-2][1]
max_power = self.heater.max_power
Ku = 4. * (2. * max_power) / (abs(temp_diff) * math.pi)
Tu = time_diff
Ti = 0.5 * Tu
Td = 0.125 * Tu
Kp = 0.6 * Ku * heater.PID_PARAM_BASE
Ki = Kp / Ti
Kd = Kp * Td
logging.info("Autotune: raw=%f/%f Ku=%f Tu=%f Kp=%f Ki=%f Kd=%f",
temp_diff, max_power, Ku, Tu, Kp, Ki, Kd)
return Kp, Ki, Kd
def calc_final_pid(self):
cycle_times = [(self.peaks[pos][1] - self.peaks[pos-2][1], pos)
for pos in range(4, len(self.peaks))]
midpoint_pos = sorted(cycle_times)[len(cycle_times)/2][1]
return self.calc_pid(midpoint_pos)
# Offline analysis helper
def write_file(self, filename):
pwm = ["pwm: %.3f %.3f" % (time, value)
for time, value in self.pwm_samples]
out = ["%.3f %.3f" % (time, temp) for time, temp in self.temp_samples]
f = open(filename, "wb")
f.write('\n'.join(pwm + out))
f.close()
def load_config(config):
return PIDCalibrate(config)

View File

@@ -369,7 +369,7 @@ class GCodeParser:
'G1', 'G4', 'G28', 'M18', 'M400',
'G20', 'M82', 'M83', 'G90', 'G91', 'G92', 'M114', 'M206', 'M220', 'M221',
'M105', 'M104', 'M109', 'M140', 'M190', 'M106', 'M107',
'M112', 'M115', 'IGNORE', 'QUERY_ENDSTOPS', 'GET_POSITION', 'PID_TUNE',
'M112', 'M115', 'IGNORE', 'QUERY_ENDSTOPS', 'GET_POSITION',
'RESTART', 'FIRMWARE_RESTART', 'ECHO', 'STATUS', 'HELP']
# G-Code movement commands
cmd_G1_aliases = ['G0']
@@ -569,18 +569,6 @@ class GCodeParser:
"gcode homing: %s" % (
mcu_pos, stepper_pos, kinematic_pos, toolhead_pos,
gcode_pos, origin_pos, homing_pos))
cmd_PID_TUNE_help = "Run PID Tuning"
cmd_PID_TUNE_aliases = ["M303"]
def cmd_PID_TUNE(self, params):
# Run PID tuning
heater_index = self.get_int('E', params, 0)
if (heater_index < -1 or heater_index >= len(self.heaters) - 1
or self.heaters[heater_index] is None):
self.respond_error("Heater not configured")
heater = self.heaters[heater_index]
temp = self.get_float('S', params)
heater.start_auto_tune(temp)
self.bg_temp(heater)
def request_restart(self, result):
if self.is_printer_ready:
self.respond_info("Preparing to restart...")

View File

@@ -98,6 +98,7 @@ REPORT_TIME = 0.300
MAX_HEAT_TIME = 5.0
AMBIENT_TEMP = 25.
PID_PARAM_BASE = 255.
PWM_DELAY = REPORT_TIME + SAMPLE_TIME*SAMPLE_COUNT
class error(Exception):
pass
@@ -141,8 +142,9 @@ class PrinterHeater:
# pwm caching
self.next_pwm_time = 0.
self.last_pwm_value = 0.
# Load verify_heater module
# Load additional modules
printer.try_load_module(config, "verify_heater %s" % (self.name,))
printer.try_load_module(config, "pid_calibrate")
def set_pwm(self, read_time, value):
if self.target_temp <= 0.:
value = 0.
@@ -150,7 +152,7 @@ class PrinterHeater:
and abs(value - self.last_pwm_value) < 0.05):
# No significant change in value - can suppress update
return
pwm_time = read_time + REPORT_TIME + SAMPLE_TIME*SAMPLE_COUNT
pwm_time = read_time + PWM_DELAY
self.next_pwm_time = pwm_time + 0.75 * MAX_HEAT_TIME
self.last_pwm_value = value
logging.debug("%s: pwm=%.3f@%.3f (from %.3f@%.3f [%.3f])",
@@ -181,16 +183,12 @@ class PrinterHeater:
def check_busy(self, eventtime):
with self.lock:
return self.control.check_busy(eventtime)
def start_auto_tune(self, degrees):
if degrees and (degrees < self.min_temp or degrees > self.max_temp):
raise error("Requested temperature (%.1f) out of range (%.1f:%.1f)"
% (degrees, self.min_temp, self.max_temp))
def set_control(self, control):
with self.lock:
self.control = ControlAutoTune(self, self.control)
self.target_temp = degrees
def finish_auto_tune(self, old_control):
self.control = old_control
self.target_temp = 0
old_control = self.control
self.control = control
self.target_temp = 0.
return old_control
def stats(self, eventtime):
with self.lock:
target_temp = self.target_temp
@@ -278,125 +276,6 @@ class ControlPID:
return (abs(temp_diff) > PID_SETTLE_DELTA
or abs(self.prev_temp_deriv) > PID_SETTLE_SLOPE)
######################################################################
# Ziegler-Nichols PID autotuning
######################################################################
TUNE_PID_DELTA = 5.0
class ControlAutoTune:
def __init__(self, heater, old_control):
self.heater = heater
self.old_control = old_control
self.heating = False
self.peaks = []
self.peak = 0.
self.peak_time = 0.
def adc_callback(self, read_time, temp):
if self.heating and temp >= self.heater.target_temp:
self.heating = False
self.check_peaks()
elif (not self.heating
and temp <= self.heater.target_temp - TUNE_PID_DELTA):
self.heating = True
self.check_peaks()
if self.heating:
self.heater.set_pwm(read_time, self.heater.max_power)
if temp < self.peak:
self.peak = temp
self.peak_time = read_time
else:
self.heater.set_pwm(read_time, 0.)
if temp > self.peak:
self.peak = temp
self.peak_time = read_time
def check_peaks(self):
self.peaks.append((self.peak, self.peak_time))
if self.heating:
self.peak = 9999999.
else:
self.peak = -9999999.
if len(self.peaks) < 4:
return
self.calc_pid(len(self.peaks)-1)
def calc_pid(self, pos):
temp_diff = self.peaks[pos][0] - self.peaks[pos-1][0]
time_diff = self.peaks[pos][1] - self.peaks[pos-2][1]
max_power = self.heater.max_power
Ku = 4. * (2. * max_power) / (abs(temp_diff) * math.pi)
Tu = time_diff
Ti = 0.5 * Tu
Td = 0.125 * Tu
Kp = 0.6 * Ku * PID_PARAM_BASE
Ki = Kp / Ti
Kd = Kp * Td
logging.info("Autotune: raw=%f/%f Ku=%f Tu=%f Kp=%f Ki=%f Kd=%f",
temp_diff, max_power, Ku, Tu, Kp, Ki, Kd)
return Kp, Ki, Kd
def final_calc(self):
cycle_times = [(self.peaks[pos][1] - self.peaks[pos-2][1], pos)
for pos in range(4, len(self.peaks))]
midpoint_pos = sorted(cycle_times)[len(cycle_times)/2][1]
Kp, Ki, Kd = self.calc_pid(midpoint_pos)
logging.info("Autotune: final: Kp=%f Ki=%f Kd=%f", Kp, Ki, Kd)
gcode = self.heater.printer.lookup_object('gcode')
gcode.respond_info(
"PID parameters: pid_Kp=%.3f pid_Ki=%.3f pid_Kd=%.3f\n"
"To use these parameters, update the printer config file with\n"
"the above and then issue a RESTART command" % (Kp, Ki, Kd))
def check_busy(self, eventtime):
if self.heating or len(self.peaks) < 12:
return True
self.final_calc()
self.heater.finish_auto_tune(self.old_control)
return False
######################################################################
# Tuning information test
######################################################################
class ControlBumpTest:
def __init__(self, heater, old_control):
self.heater = heater
self.old_control = old_control
self.temp_samples = {}
self.pwm_samples = {}
self.state = 0
def set_pwm(self, read_time, value):
self.pwm_samples[read_time + 2*REPORT_TIME] = value
self.heater.set_pwm(read_time, value)
def adc_callback(self, read_time, temp):
self.temp_samples[read_time] = temp
if not self.state:
self.set_pwm(read_time, 0.)
if len(self.temp_samples) >= 20:
self.state += 1
elif self.state == 1:
if temp < self.heater.target_temp:
self.set_pwm(read_time, self.heater.max_power)
return
self.set_pwm(read_time, 0.)
self.state += 1
elif self.state == 2:
self.set_pwm(read_time, 0.)
if temp <= (self.heater.target_temp + AMBIENT_TEMP) / 2.:
self.dump_stats()
self.state += 1
def dump_stats(self):
out = ["%.3f %.1f %d" % (time, temp, self.pwm_samples.get(time, -1.))
for time, temp in sorted(self.temp_samples.items())]
f = open("/tmp/heattest.txt", "wb")
f.write('\n'.join(out))
f.close()
def check_busy(self, eventtime):
if self.state < 3:
return True
self.heater.finish_auto_tune(self.old_control)
return False
def add_printer_objects(printer, config):
if config.has_section('heater_bed'):
printer.add_object('heater_bed', PrinterHeater(