import csv import argparse # Using the method described in "Ziegler–Nichols Tuning Method∗" by Vishakha Vijay Patel # (https://www.ias.ac.in/article/fulltext/reso/025/10/1385-1397) def line(a, b, x): return a * x + b def invline(a, b, y): return (y - b) / a def plot(xdata, ydata, tangent_min, tangent_max, tangent_slope, tangent_offset, lower_crossing_x, upper_crossing_x): from matplotlib import pyplot minx = min(xdata) maxx = max(xdata) miny = min(ydata) maxy = max(ydata) pyplot.scatter(xdata, ydata) pyplot.plot([minx, maxx], [miny, miny], '--', color='purple') pyplot.plot([minx, maxx], [maxy, maxy], '--', color='purple') pyplot.plot(tangent_min[0], tangent_min[1], 'v', color='red') pyplot.plot(tangent_max[0], tangent_max[1], 'v', color='red') pyplot.plot([minx, maxx], [line(tangent_slope, tangent_offset, minx), line(tangent_slope, tangent_offset, maxx)], '--', color='red') pyplot.plot([lower_crossing_x, lower_crossing_x], [miny, maxy], '--', color='black') pyplot.plot([upper_crossing_x, upper_crossing_x], [miny, maxy], '--', color='black') pyplot.show() def calculate(filename, tangentdivisor, showplot): # parse the csv file xdata = [] ydata = [] filemintime = None with open(filename) as f: for row in csv.DictReader(f): try: time = float(row['pid_time']) temp = float(row['pid_ispoint']) if filemintime is None: filemintime = time xdata.append(time - filemintime) ydata.append(temp) except ValueError: continue # just ignore bad values! # gather points for tangent line miny = min(ydata) maxy = max(ydata) midy = (maxy + miny) / 2 yoffset = int((maxy - miny) / tangentdivisor) tangent_min = tangent_max = None for i in range(0, len(xdata)): rowx = xdata[i] rowy = ydata[i] if rowy >= (midy - yoffset) and tangent_min is None: tangent_min = (rowx, rowy) elif rowy >= (midy + yoffset) and tangent_max is None: tangent_max = (rowx, rowy) # calculate tangent line to the main temperature curve tangent_slope = (tangent_max[1] - tangent_min[1]) / (tangent_max[0] - tangent_min[0]) tangent_offset = tangent_min[1] - line(tangent_slope, 0, tangent_min[0]) # determine the point at which the tangent line crosses the min/max temperaturess lower_crossing_x = invline(tangent_slope, tangent_offset, miny) upper_crossing_x = invline(tangent_slope, tangent_offset, maxy) # compute parameters L = lower_crossing_x - min(xdata) T = upper_crossing_x - lower_crossing_x # Magic Ziegler-Nicols constants ahead! Kp = 1.2 * (T / L) Ti = 2 * L Td = 0.5 * L Ki = Kp / Ti Kd = Kp * Td # outut to the user print(f"Kp: {Kp} 1/Ki: {1/ Ki}, Kd: {Kd}") if showplot: plot(xdata, ydata, tangent_min, tangent_max, tangent_slope, tangent_offset, lower_crossing_x, upper_crossing_x) if __name__ == "__main__": parser = argparse.ArgumentParser(description='Perform Ziegler-Nichols PID tuning') parser.add_argument('csvfile', type=str, help="The CSV file to read from. Must contain two columns called pid_time (time in seconds) and pid_ispoint (observed temperature)") parser.add_argument('--showplot', action='store_true', help="If set, also plot results (requires pyplot to be pip installed)") parser.add_argument('--tangentdivisor', type=float, default=4, help="Adjust the tangent calculation to fit better. Must be >= 2.") args = parser.parse_args() if args.tangentdivisor < 2: raise ValueError("tangentdivisor must be >= 2") calculate(args.csvfile, args.tangentdivisor, args.showplot)