combine tools into one
This commit is contained in:
parent
816043b012
commit
9603332bfb
@ -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 .
|
It will display a plot of the parameters. It should look simular to this .
|
||||||
@ -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)
|
||||||
|
|||||||
147
kiln-tuner.py
147
kiln-tuner.py
@ -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}")
|
||||||
|
|||||||
115
zieglernicols.py
115
zieglernicols.py
@ -1,115 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
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['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)
|
|
||||||
Loading…
Reference in New Issue
Block a user