combine tools into one

This commit is contained in:
Andrew de Quincey 2021-05-01 14:18:27 +01:00
parent 816043b012
commit 9603332bfb
3 changed files with 137 additions and 137 deletions

View File

@ -21,7 +21,7 @@ There needs to be no abnormal source of temperature change to the kiln: eg if yo
To record the profile, run: To record the profile, run:
``` ```
python kiln-tuner.py zn.csv python kiln-tuner.py recordprofile zn.csv
``` ```
The above will drive your kiln to 400 and record the temperature profile to the file `zn.csv`. The file will look something like this: The above will drive your kiln to 400 and record the temperature profile to the file `zn.csv`. The file will look something like this:
@ -40,7 +40,7 @@ time,temperature
Once you have your zn.csv profile, run the following: Once you have your zn.csv profile, run the following:
``` ```
python zieglernicols.py zn.csv python kiln-tuner.py zn zn.csv
``` ```
The values will be output to stdout, for example: The values will be output to stdout, for example:
@ -55,7 +55,7 @@ Kp: 3.853985144980333 1/Ki: 87.78173053095107 Kd: 325.9599328488931
If you run If you run
``` ```
python zieglernicols.py zn.csv --showplot python kiln-tuner.py zn zn.csv --showplot
``` ```
It will display a plot of the parameters. It should look simular to this ![kiln-tuner-example.png](kiln-tuner-example.png). It will display a plot of the parameters. It should look simular to this ![kiln-tuner-example.png](kiln-tuner-example.png).
@ -71,7 +71,7 @@ The red diagonal line: this **must** follow the smooth part of your chart closel
You might need to adjust the line parameters to make it fit your data properly. You can do this as follows: You might need to adjust the line parameters to make it fit your data properly. You can do this as follows:
``` ```
python zieglernicols.py zn.csv --tangentdivisor 8 python kiln-tuner.py zn zn.csv --tangentdivisor 8
``` ```
`tangentdivisor` modifies which parts of the profile is used to calculate the line. `tangentdivisor` modifies which parts of the profile is used to calculate the line.
@ -83,7 +83,7 @@ It is a floating point number >= 2; If necessary, try varying it till you get a
By default it is 400. You can change this as follows: By default it is 400. You can change this as follows:
``` ```
python kiln-tuner.py zn.csv --targettemp 500 python kiln-tuner.py recordprofile zn.csv --targettemp 500
``` ```
(where the target temperature has been changed to 500) (where the target temperature has been changed to 500 in the example above)

View File

@ -7,24 +7,24 @@ import time
import argparse import argparse
try: def recordprofile(csvfile, targettemp):
sys.dont_write_bytecode = True
import config
sys.dont_write_bytecode = False
except: try:
print("Could not import config file.") sys.dont_write_bytecode = True
print("Copy config.py.EXAMPLE to config.py and adapt it for your setup.") import config
exit(1) sys.dont_write_bytecode = False
script_dir = os.path.dirname(os.path.realpath(__file__)) except:
sys.path.insert(0, script_dir + '/lib/') print("Could not import config file.")
profile_path = os.path.join(script_dir, "storage", "profiles") print("Copy config.py.EXAMPLE to config.py and adapt it for your setup.")
exit(1)
from oven import RealOven, SimulatedOven script_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, script_dir + '/lib/')
profile_path = os.path.join(script_dir, "storage", "profiles")
from oven import RealOven, SimulatedOven
def tune(csvfile, targettemp):
# open the file to log data to # open the file to log data to
f = open(csvfile, 'w') f = open(csvfile, 'w')
csvout = csv.writer(f) csvout = csv.writer(f)
@ -78,10 +78,125 @@ def tune(csvfile, targettemp):
oven.output.heat(0) oven.output.heat(0)
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['time'])
temp = float(row['temperature'])
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__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Record data for kiln tuning') parser = argparse.ArgumentParser(description='Record data for kiln tuning')
parser.add_argument('csvfile', type=str, help="The CSV file to write to.") subparsers = parser.add_subparsers()
parser.add_argument('--targettemp', type=int, default=400, help="The target temperature to drive the kiln to (default 400).")
parser_profile = subparsers.add_parser('recordprofile', help='Record kiln temperature profile')
parser_profile.add_argument('csvfile', type=str, help="The CSV file to write to.")
parser_profile.add_argument('--targettemp', type=int, default=400, help="The target temperature to drive the kiln to (default 400).")
parser_profile.set_defaults(mode='recordprofile')
parser_zn = subparsers.add_parser('zn', help='Calculate Ziegler-Nicols parameters')
parser_zn.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_zn.add_argument('--showplot', action='store_true', help="If set, also plot results (requires pyplot to be pip installed)")
parser_zn.add_argument('--tangentdivisor', type=float, default=4, help="Adjust the tangent calculation to fit better. Must be >= 2.")
parser_zn.set_defaults(mode='zn')
args = parser.parse_args() args = parser.parse_args()
tune(args.csvfile, args.targettemp) if args.mode == 'recordprofile':
recordprofile(args.csvfile, args.targettemp)
elif args.mode == 'zn':
if args.tangentdivisor < 2:
raise ValueError("tangentdivisor must be >= 2")
calculate(args.csvfile, args.tangentdivisor, args.showplot)
else:
raise NotImplementedError(f"Unknown mode {args.mode}")

View File

@ -1,115 +0,0 @@
#!/usr/bin/env python
import csv
import argparse
# Using the method described in "ZieglerNichols 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['time'])
temp = float(row['temperature'])
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)