Python Example - Dynamic with GUI

From SysCAD Documentation
Jump to navigation Jump to search

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.

Python Code      
## 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.
  • This slider adjusts the outflow capacity from a tank in the model.
  • Using a GUI interface like this makes it much easier to interact with a Dynamic model—such as opening/closing valves or toggling pumps—compared to manual command-line input.

Key Features Demonstrated

  1. Tkinter DoubleVar: The variable Capacity is defined as a DoubleVar, which represents a floating-point number and supports additional features like event callbacks.
  2. Callback Function: A callback is attached to Capacity, so whenever its value changes, a function is triggered to update the corresponding SysCAD tag.
  3. Scale Widget Integration: Capacity is linked to a Tkinter Scale widget. When the user drags the slider, the value updates automatically, triggering the callback to write the new value to SysCAD.
  4. Controller Logic: The method CtrlLevel uses Capacity to implement a basic control strategy
  5. Recurring Execution:CtrlLevel schedules itself to run every 200 milliseconds, allowing continuous monitoring and adjustment of the tank level.

Manual vs. Automatic Control in the Dynamic Model

In this example, the built-in SysCAD level control has been turned off.

  1. You can now manually adjust the tank's outflow capacity using the slider in the Tkinter GUI.
  2. Alternatively, you can enable a simple automatic control strategy by clicking the Auto checkbutton. This activates a Python-based control method known as Bang-Bang Control.

When Auto mode is enabled:

  • If the tank level exceeds 90%, the outflow capacity is set to 200.
  • If the level drops below 30%, the capacity is reduced to 5.
  • This control method is executed repeatedly every 200 milliseconds, allowing the system to respond to changing conditions in real time.

⚠️ 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.

DynamicTk.png

Overlay Control Panels

Python Code - tk GUI      
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()
DynPullExample.png

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:

  • Running, stopping, and closing the SysCAD project from the Python interface.
  • Displaying the simulation runtime directly on the screen.
  • Allowing users to adjust the flow capacities of Valves 001, 002, and 003 using interactive sliders.
  • Disabling the existing General Controller and using Python logic to manage tank levels instead.
  • Disabling the pump pair swap profile, giving users manual control to toggle Pumps 11 and 12 on or off.

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)

Python Code - Ping      
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()