Python Example - Dynamic with GUI
Navigation: User Guide ➔ COM Automation ➔ Python Automation ➔ Example - Dynamic with GUI
| Python Setup | Python Examples | Python Script | Optimisation | Visulisation | Python GUI | Tags and Data | |||
|---|---|---|---|---|---|---|---|---|---|
| Installation & Troubleshooting | List of Examples | Simple Script (pywin32) | SysCAD COM Python Class | Automated Model Testing | Constrained FEM (numpy|matplotlib) | Optimisation (numpy|scipy) | Adding Plots (numpy|matplotlib) | Dynamic with GUI | Accessing Data (sqlite3|pandas) |
Creating a Simple Control Panel with Tkinter
Most COM interface commands are consistent across both Steady State and Dynamic models in SysCAD. However, for Dynamic models, commands such as Dynamic.Start() and Dynamic.Stop() are used instead of Probal.Run().
Basic Control Panel
The following example demonstrates how to use Tkinter (the default GUI toolkit for Python) to build a basic control panel for interacting with a Dynamic Plant Example Project.
## Example Automation Using Python for Dynamic model with GUI.
import syscadif
from tkinter import *
ScdDir = r'C:\SysCAD139'
ScdPrj = r'\ExamplesDynamic\Dynamic Plant Example.spf\Project.spj'
sc = syscadif.SysCADCom() ## Fire up SysCAD
sc.OpenProject(ScdDir+ScdPrj) ## Open project
sc["PID_Lvl.Cfg.[1].On"] = 0 ## Turn of level control
ctrlTag = "Output.FC.Qm.Capacity (t/h)" ## We will do this by hand...
## Tk Graphical Interface...
root = Tk()
Capacity = DoubleVar(value = 0)
Auto = BooleanVar(value = False)
pid = None
def capCallback(*args):
sc[ctrlTag] = Capacity.get()
def Stop():
sc.Dynamic.Stop()
def Done():
if pid is not None:
root.after_cancel(pid) ## Delete scheduled after command
sc.Dynamic.Stop()
sc.Close()
root.destroy()
def Ctrl():
global pid
if Auto.get():
lvl = sc["REACTION_TANK.Lvl (%)"]
if lvl>90:
Capacity.set(200)
elif lvl<30:
Capacity.set(5)
pid = root.after(200, Ctrl) ## reschedule call in 200ms
Capacity.trace("w", capCallback) ## The trace method of a DoubleVar executes the callback argument when the variable is written
Label(root, text = "Capacity Control").pack(side=TOP)
Scale(root, orient = HORIZONTAL, variable = Capacity).pack(side=TOP)
f = Frame(root)
Button(f, text="Run", command = sc.run).pack(side=LEFT, anchor=NW)
Button(f, text="Stop", command = Stop).pack(side=LEFT,anchor=NW)
Button(f, text="Quit", command = Done).pack(side=LEFT,anchor=NW)
Checkbutton(f, text="Auto", variable = Auto).pack(side=LEFT,anchor=NW)
f.pack(side=TOP)
Ctrl() ## Start control loop
root.mainloop()
|
Running the full Python script above will open a Tkinter window featuring a slider control.
Key Features Demonstrated
Manual vs. Automatic Control in the Dynamic Model In this example, the built-in SysCAD level control has been turned off.
When Auto mode is enabled:
⚠️ Note: Bang-Bang Control is a basic on/off control strategy. While it's simple to implement, it’s not ideal for precise or stable process control. For more information, see Bang-Bang Control on Wikipedia. |
|
Overlay Control Panels
import tkinter as tk
from tkinter import *
import win32gui
import syscadif
sc = syscadif.SysCADCom()
ScdDir = r'C:\SysCAD%d_37043' % sc.build
ScdPrj = r'\ExamplesDynamic\Dynamic Transfer Pull Network Example.spf\Project.spj'
sc.OpenProject(ScdDir + ScdPrj)
sc["GC_001.On"] = 0 ## Turn of General controller, will use the logic from Python
sc["PC_001.On"] = 0 ## Turn of profile
sc["PMP_011.Speed.Reqd (%)"] = 100
sc["PMP_011.Speed.Reqd (%)"] = 100
ctrlTags = [
"VLV_001.FC.Qm.Capacity (t/h)",
"VLV_002.FC.Qm.Capacity (t/h)",
"VLV_003.FC.Qm.Capacity (t/h)"
]
## Tk Graphical Interface...
root = tk.Tk()
Capacity1 = DoubleVar(value=50)
Capacity2 = DoubleVar(value=50)
Capacity3 = DoubleVar(value=50)
Auto = BooleanVar(value = True)
Profile = BooleanVar(value = True)
pid = None
def capCallback(*args):
sc[ctrlTags[0]] = Capacity1.get()
sc[ctrlTags[1]] = Capacity2.get()
sc[ctrlTags[2]] = Capacity3.get()
def update_value():
global pid
runtime = sc["$Solver.Dyn.Scenario.Elapsed.Time (min)"]
value_label.config(text=f"Run Time: {runtime:.2f} min")
if Auto.get():
# Control for TNK_001 using VLV_011
Tank1lvl = sc["TNK_001.Lvl (%)"]
if Tank1lvl > 90:
sc["VLV_011.Posn.On"] = 0
elif Tank1lvl < 10:
sc["VLV_011.Posn.On"] = 1
# Control for TNK_003 using VLV_013
Tank3lvl = sc["TNK_003.Lvl (%)"]
if Tank3lvl > 90:
sc["VLV_031.Posn.On"] = 0
elif Tank3lvl < 10:
sc["VLV_031.Posn.On"] = 1
pid = root.after(1000, update_value)
def PumpPair():
if Profile.get():
sc["PC_001.On"] = 1
sc["PMP_011.Speed.Run"] = 1
sc["PMP_012.Speed.Run"] = 1
else:
sc["PC_001.On"] = 0
sc["PMP_011.Speed.Run"] = 1
sc["PMP_012.Speed.Run"] = 0
sc["PMP_011.Speed.Reqd (%)"] = 100
sc["PMP_012.Speed.Reqd (%)"] = 100
Profile.trace("w", lambda *args: PumpPair())
Capacity1.trace("w", capCallback)
Capacity2.trace("w", capCallback)
Capacity3.trace("w", capCallback)
def Stop():
sc.Dynamic.Stop()
def Done():
global pid
if pid is not None:
root.after_cancel(pid)
sc.Dynamic.Stop()
sc.CloseProject()
sc.Close()
root.destroy()
def pump_on(tag):
sc[tag] = 1
def pump_off(tag):
sc[tag] = 0
def get_syscad_position():
hwnd = win32gui.FindWindow(None, "SysCAD")
if hwnd:
rect = win32gui.GetWindowRect(hwnd)
return rect[0], rect[1] # Top-left corner
return 100, 100 # Fallback position
def make_draggable(widget):
def on_press(event):
widget.startX = event.x
widget.startY = event.y
def on_drag(event):
x = widget.winfo_x() - widget.startX + event.x
y = widget.winfo_y() - widget.startY + event.y
widget.geometry(f"+{x}+{y}")
widget.bind("<ButtonPress-1>", on_press)
widget.bind("<B1-Motion>", on_drag)
root.withdraw() # Hide main window
x, y = get_syscad_position()
# Panel: Control Buttons
control_win = tk.Toplevel(bg="lightgrey")
control_win.geometry(f"160x50+{x+10}+{y+30}")
control_win.overrideredirect(True)
control_win.attributes("-topmost", True)
make_draggable(control_win)
tk.Button(control_win, text="Run", font=("Arial", 12), command=sc.run).pack(side="left", padx=5, pady=5)
tk.Button(control_win, text="Stop", font=("Arial", 12), command=Stop).pack(side="left", padx=5, pady=5)
tk.Button(control_win, text="Quit", font=("Arial", 12), command=Done).pack(side="left", padx=5, pady=5)
# Panel: Runtime
runtime_win = tk.Toplevel(bg="lightgrey")
runtime_win.geometry(f"200x50+{x+350}+{y+30}")
runtime_win.overrideredirect(True)
runtime_win.attributes("-topmost", True)
make_draggable(runtime_win)
value_label = Label(runtime_win, text="Run Time: 0.00 min", font=("Arial", 12),bg="lightgrey")
value_label.pack(expand=True)
# Tank Level Control Frame
tank_frame = tk.Toplevel(bg="white")
tank_frame.geometry(f"150x50+{x+10}+{y+150}")
tank_frame.overrideredirect(True)
tank_frame.attributes("-topmost", True)
make_draggable(tank_frame)
Label(tank_frame, text="Tank Level Control",font=("Arial", 12),bg="lightgrey").pack(side="top")
Checkbutton(tank_frame, text="Auto Tank Topup", variable=Auto).pack(side="top")
# Capacity Control Frame
Cap_frame = tk.Toplevel(bg="")
Cap_frame.geometry(f"220x150+{x+800}+{y+100}")
Cap_frame.overrideredirect(True)
Cap_frame.attributes("-topmost", True)
##make_draggable(Cap_frame)
Label(Cap_frame, text="Capacity 1").grid(row=1, column=0, sticky="w", padx=5)
Scale(Cap_frame, orient=HORIZONTAL, variable=Capacity1).grid(row=1, column=1, padx=5)
Label(Cap_frame, text="Capacity 2").grid(row=2, column=0, sticky="w", padx=5)
Scale(Cap_frame, orient=HORIZONTAL, variable=Capacity2).grid(row=2, column=1, padx=5)
Label(Cap_frame, text="Capacity 3").grid(row=3, column=0, sticky="w", padx=5)
Scale(Cap_frame, orient=HORIZONTAL, variable=Capacity3).grid(row=3, column=1, padx=5)
# Pump Control Frame
PumpPair_frame = tk.Toplevel(bg="white")
PumpPair_frame.geometry(f"220x110+{x+900}+{y+400}")
PumpPair_frame.overrideredirect(True)
PumpPair_frame.attributes("-topmost", True)
make_draggable(PumpPair_frame)
Label(PumpPair_frame, text="Pump Controls", font=("Arial", 12, "bold")).grid(row=0, column=0, sticky="w")
Checkbutton(PumpPair_frame, text="Pump Pair Profile", variable=Profile).grid(row=1, column=0, sticky="w")
Button(PumpPair_frame, text="Pump 11 ON", command=lambda: pump_on("PMP_011.Speed.Run")).grid(row=0, column=1, padx=10)
Button(PumpPair_frame, text="Pump 11 OFF", command=lambda: pump_off("PMP_011.Speed.Run")).grid(row=1, column=1, padx=10)
Button(PumpPair_frame, text="Pump 12 ON", command=lambda: pump_on("PMP_012.Speed.Run")).grid(row=2, column=1, padx=10)
Button(PumpPair_frame, text="Pump 12 OFF", command=lambda: pump_off("PMP_012.Speed.Run")).grid(row=3, column=1, padx=10)
PumpPair()
update_value()
root.mainloop()
|
|
In the previous example, we introduced some basic Python Tinker controls. In this example, we’ve expanded on that by using the distributed Dynamic Transfer Pull Network Example Project and adding several Python-based overlay control panels directly onto the SysCAD flowsheet. These enhancements enable more intuitive and interactive control of the model. Some of the implemented features include:
These enhancements demonstrate how Python can be used as a simple yet effective Operator Control Interface to drive and interact with the SysCAD model. |
Adding Ping button
You can even create a button for the machine that goes "ping". (Mandatory Monty Python reference from Meaning of Life)
import tkinter as tk
import winsound # For Windows sound playback
def ping_action():
winsound.MessageBeep(winsound.MB_ICONEXCLAMATION) # Plays a system 'ping' sound
root.destroy() # Close the window and clean up
root = tk.Tk() # Create a minimal window
root.title("")
root.overrideredirect(True) # Remove window decorations (optional)
root.geometry("200x100+500+300") #Set size and position: Width x Height + X + Y
# Create the button
ping_button = tk.Button(root, text="Ping!", font=("Aptos", 50, "bold"), bg="lightgrey", command=ping_action)
ping_button.pack(expand=True, fill="both")
root.mainloop()
|
