Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

code format/lint #64

Merged
merged 1 commit into from
Oct 10, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 77 additions & 50 deletions klippy/extras/pid_calibrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def cmd_PID_CALIBRATE(self, gcmd):
heater_name = gcmd.get("HEATER")
target = gcmd.get_float("TARGET")
write_file = gcmd.get_int("WRITE_FILE", 0)
tolerance = gcmd.get_float('TOLERANCE', TUNE_PID_TOL, above=0.)
tolerance = gcmd.get_float("TOLERANCE", TUNE_PID_TOL, above=0.0)
pheaters = self.printer.lookup_object("heaters")
try:
heater = pheaters.lookup_heater(heater_name)
Expand Down Expand Up @@ -58,17 +58,19 @@ def cmd_PID_CALIBRATE(self, gcmd):
configfile.set(heater_name, "pid_Ki", "%.3f" % (Ki,))
configfile.set(heater_name, "pid_Kd", "%.3f" % (Kd,))


TUNE_PID_DELTA = 5.0
TUNE_PID_TOL = 0.02
TUNE_PID_SAMPLES = 3
TUNE_PID_MAX_PEAKS = 60


class ControlAutoTune:
def __init__(self, heater, target, tolerance):
self.heater = heater
self.heater_max_power = heater.get_max_power()
# store the reference so we can push messages if needed
self.gcode = heater.printer.lookup_object('gcode')
self.gcode = heater.printer.lookup_object("gcode")
# holds the various max power settings used during the test.
self.powers = [self.heater_max_power]
# holds the times the power setting was changed.
Expand All @@ -79,9 +81,9 @@ def __init__(self, heater, target, tolerance):
# acceptable level
self.tolerance = tolerance
# the the temp that determines when to turn the heater off
self.temp_high = target + TUNE_PID_DELTA/2.
self.temp_high = target + TUNE_PID_DELTA / 2.0
# the the temp that determines when to turn the heater on
self.temp_low = target - TUNE_PID_DELTA/2.
self.temp_low = target - TUNE_PID_DELTA / 2.0
# is the heater on
self.heating = False
# the current potential peak value
Expand All @@ -106,8 +108,9 @@ def temperature_update(self, read_time, temp, target_temp):
if self.done:
return
# store test data
self.data.append((read_time, temp, self.heater.last_pwm_value,
self.target))
self.data.append(
(read_time, temp, self.heater.last_pwm_value, self.target)
)
# ensure the starting temp is low enough to run the test.
if not self.started and temp >= self.temp_low:
self.errored = True
Expand All @@ -123,12 +126,12 @@ def temperature_update(self, read_time, temp, target_temp):
self.gcode.respond_info("calibration did not finish in time")
return
# indicate that the target temp has been crossed at-least once
if temp > self.target and self.target_crossed == False:
if temp > self.target and self.target_crossed is False:
self.target_crossed = True
# only do work if the target temp has been crossed at-least once
if self.target_crossed:
# check for a new peak value
if temp > self.temp_high or temp < self.temp_low :
if temp > self.temp_high or temp < self.temp_low:
self.check_peak(read_time, temp)
# it's time to calculate and store a high peak
if self.peak > self.temp_high and temp < self.target:
Expand All @@ -137,9 +140,9 @@ def temperature_update(self, read_time, temp, target_temp):
if self.peak < self.temp_low and temp > self.target:
self.store_peak()
# check if the conditions are right to evaluate a new sample
peaks = float(len(self.peaks)) - 1.
peaks = float(len(self.peaks)) - 1.0
powers = float(len(self.powers))
if (peaks % 2.) == 0. and (powers * 2.) == peaks:
if (peaks % 2.0) == 0.0 and (powers * 2.0) == peaks:
self.log_info()
# check for convergence
if self.converged():
Expand All @@ -161,6 +164,7 @@ def temperature_update(self, read_time, temp, target_temp):
self.heater.set_pwm(read_time, self.powers[-1])
else:
self.heater.set_pwm(read_time, 0)

def check_peak(self, time, temp):
# deal with duplicate temps
if temp == self.peak:
Expand All @@ -173,16 +177,18 @@ def check_peak(self, time, temp):
if temp < self.target and temp < self.peak:
self.peak = temp
self.peak_times = [time]

def store_peak(self):
time = sum(self.peak_times)/float(len(self.peak_times))
time = sum(self.peak_times) / float(len(self.peak_times))
self.peaks.append((time, self.peak))
self.peak = self.target
self.peak_times = []

def log_info(self):
# provide some useful info to the user
sample = len(self.powers)
pwm = self.powers[-1]
asymmetry = (self.peaks[-2][1] + self.peaks[-1][1])/2. - self.target
asymmetry = (self.peaks[-2][1] + self.peaks[-1][1]) / 2.0 - self.target
tolerance = self.get_sample_tolerance()
if tolerance is False:
fmt = "sample:%d pwm:%.4f asymmetry:%.4f tolerance:n/a\n"
Expand All @@ -192,86 +198,106 @@ def log_info(self):
fmt = "sample:%d pwm:%.4f asymmetry:%.4f tolerance:%.4f\n"
data = (sample, pwm, asymmetry, tolerance)
self.gcode.respond_info(fmt % data)

def get_sample_tolerance(self):
powers = len(self.powers)
if powers < TUNE_PID_SAMPLES + 1:
return False
powers = self.powers[-1*(TUNE_PID_SAMPLES+1):]
return max(powers)-min(powers)
powers = self.powers[-1 * (TUNE_PID_SAMPLES + 1) :]
return max(powers) - min(powers)

def converged(self):
tolerance = self.get_sample_tolerance()
if tolerance is False:
return False
return False
if tolerance <= self.tolerance:
return True
return False

def set_power(self):
peak_low = self.peaks[-2][1]
peak_high = self.peaks[-1][1]
power = self.powers[-1]
mid = power * ((self.target - peak_low)/(peak_high - peak_low))
if mid * 2. > self.heater_max_power:
mid = power * ((self.target - peak_low) / (peak_high - peak_low))
if mid * 2.0 > self.heater_max_power:
# the new power is to high so just return max power
self.powers.append(self.heater_max_power)
return
self.powers.append(mid * 2.)
self.powers.append(mid * 2.0)

def finish(self, time):
self.heater.set_pwm(time, 0)
self.heater.alter_target(0)
self.done = True
self.heating = False
self.heater.set_pwm(time, 0)
self.heater.alter_target(0)
self.done = True
self.heating = False

def check_busy(self, eventtime, smoothed_temp, target_temp):
if eventtime == 0. and smoothed_temp == 0. and target_temp == 0.:
if eventtime == 0.0 and smoothed_temp == 0.0 and target_temp == 0.0:
if self.errored:
return True
return False
if self.done:
return False
return True

def write_file(self, filename):
f = open(filename, "w")
f.write('time, temp, pwm, target\n')
data = ["%.5f, %.5f, %.5f, %.5f" % (time, temp, pwm, target)
for time, temp, pwm, target in self.data]
f.write('\n'.join(data))
f.write("time, temp, pwm, target\n")
data = [
"%.5f, %.5f, %.5f, %.5f" % (time, temp, pwm, target)
for time, temp, pwm, target in self.data
]
f.write("\n".join(data))
peaks = self.peaks[:]
powers = self.powers[:]
# pop off the
peaks.pop(0)
samples = []
for i in range(len(powers)):
samples.append((i, peaks[i*2][0], peaks[i*2][1], peaks[i*2+1][0],
peaks[i*2+1][1], powers[i]))
f.write('\nsample, low time, low, high time, high, max power\n')
data = ["%.5f, %.5f, %.5f, %.5f, %.5f, %.5f" % (sample, low_time,
low, high_time, high, max_power) for sample, low_time, low,
high_time, high, max_power in samples]
f.write('\n'.join(data))
samples.append(
(
i,
peaks[i * 2][0],
peaks[i * 2][1],
peaks[i * 2 + 1][0],
peaks[i * 2 + 1][1],
powers[i],
)
)
f.write("\nsample, low time, low, high time, high, max power\n")
data = [
"%.5f, %.5f, %.5f, %.5f, %.5f, %.5f"
% (sample, low_time, low, high_time, high, max_power)
for sample, low_time, low, high_time, high, max_power in samples
]
f.write("\n".join(data))
f.close()

def calc_pid(self):
temp_diff = 0.
time_diff = 0.
theta = 0.
temp_diff = 0.0
time_diff = 0.0
theta = 0.0
for i in range(1, TUNE_PID_SAMPLES * 2, 2):
temp_diff = temp_diff + self.peaks[-i][1] - self.peaks[-i-1][1]
time_diff = time_diff + self.peaks[-i][0] - self.peaks[-i-2][0]
temp_diff = temp_diff + self.peaks[-i][1] - self.peaks[-i - 1][1]
time_diff = time_diff + self.peaks[-i][0] - self.peaks[-i - 2][0]
theta = theta + self.peaks[-i][0] - self.times[-i]
temp_diff = temp_diff/float(TUNE_PID_SAMPLES)
time_diff = time_diff/float(TUNE_PID_SAMPLES)
theta = theta/float(TUNE_PID_SAMPLES)
temp_diff = temp_diff / float(TUNE_PID_SAMPLES)
time_diff = time_diff / float(TUNE_PID_SAMPLES)
theta = theta / float(TUNE_PID_SAMPLES)
amplitude = 0.5 * abs(temp_diff)
power = self.powers[-1*(TUNE_PID_SAMPLES):]
power = sum(power)/float(len(power))
power = self.powers[-1 * (TUNE_PID_SAMPLES) :]
power = sum(power) / float(len(power))
# calculate the various parameters
Ku = 4. * power / (math.pi * amplitude)
Ku = 4.0 * power / (math.pi * amplitude)
Tu = time_diff
Wu = (2. * math.pi)/Tu
tau = math.tan(math.pi - theta *Wu)/Wu
Km = -math.sqrt(tau**2 * Wu**2 + 1.)/Ku
Wu = (2.0 * math.pi) / Tu
tau = math.tan(math.pi - theta * Wu) / Wu
Km = -math.sqrt(tau**2 * Wu**2 + 1.0) / Ku
# log the extra details
logging.info("Ziegler-Nichols constants: Ku=%f Tu=%f", Ku, Tu)
logging.info("Cohen-Coon constants: Km=%f Theta=%f Tau=%f", Km,
theta, tau)
logging.info(
"Cohen-Coon constants: Km=%f Theta=%f Tau=%f", Km, theta, tau
)
# Use Ziegler-Nichols method to generate PID parameters
Ti = 0.5 * Tu
Td = 0.125 * Tu
Expand All @@ -280,5 +306,6 @@ def calc_pid(self):
Kd = Kp * Td
return Kp, Ki, Kd


def load_config(config):
return PIDCalibrate(config)