SysCAD COM Interface Class

From SysCAD Documentation
Jump to navigation Jump to search

Navigation: User Guide ➔ COM Automation ➔ Python Automation ➔ Python Class

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)

Encapsulating COM functionality in a Python Class

t’s good practice to encapsulate all required COM functionality within a Python class. This keeps your code tidy, centralised, and easy to reuse. The syscadif.py module demonstrates this by defining a class called SysCADCom, which wraps basic SysCAD COM operations. We’ll be using this class in the examples that follow.

syscadif.py Python Class

Creating an instance of the SysCADCom class will fire up SysCAD (or attach if SysCAD is already running). To see the code below click on the Expand link:

Python Code for module: syscadif.py      
import win32com.client
import time, os
from tkinter import filedialog

PrjFolder = "C:/SysCAD139/Examples"   # Where to look for SysCAD projects when opening with the file dialog

class SysCADCom:
    def __init__(self):
        self.ProgID = r"SysCADSimulator93.Application"
        self.SysCAD = self.Prj = self.Tags = self.Solver = None
        self.Probal = self.Dynamic = None
        self.SysCAD = win32com.client.DispatchEx(self.ProgID)
        self.build = self.SysCAD.VersionNumber(2)

    def OpenProject(self, fn=None, checkFile = True):
        print (self.ProgID)
        if fn is None:
            self.fn = filedialog.askopenfilename(filetypes=[("SysCAD Projects", "*spj")],
                                             initialdir = PrjFolder,
                                             defaultextension="spj")
        else:
            self.fn = fn
        if not self.fn: return 0
        if checkFile and not os.path.isfile(self.fn):
            print ("Project File not found")
            return 0
        self.Prj = self.SysCAD.OpenProject(self.fn)
        self.Tags = self.Prj.Tags
        self.Solver = self.Prj.Solver
        self.RunMode()
        time.sleep(1)

    def RunMode(self):
        '''Select Run mode depending if project is SS or dynamic'''
        if self.Solver.RunMode & 1:
            self.Probal = self.Solver.Probal
            self.Dynamic = None
        else:
            self.Dynamic = self.Solver.Dynamic
            self.Probal = None

    def CloseProject(self):
        self.SysCAD.CloseProject()
        
    def Close(self):    
        '''remove all references to COM objects...'''
        del self.Probal
        del self.Dynamic
        del self.Solver
        del self.Tags
        del self.Prj
        del self.SysCAD

    def startSysCAD(self):
        if self.Solver.RunMode & 1:
            self.Probal.Start()
        else:
            self.Dynamic.Start()
        
    def run(self, sleepTime=0.1):
        '''Run a Probal project to completion. For a project that takes a longer time to solve
         increase sleepTime'''
        if self.Probal is not None:
            self.Probal.Start()
            while not self.Probal.IsStopped:
                time.sleep(sleepTime)
        elif self.Dynamic is not None:
                self.Dynamic.Start()
        else:
            print ("No project")

    def stop(self):
        if self.Solver.RunMode & 1:
            self.Probal.Stop()
        else:
            self.Dynamic.Stop()
            
    def RunScenarios(self, XVals, Tag, getTagList, verbose=True):
        '''Run a number of scenarios. 
        If Tag is a string, XVals should be a list or iterable of appropriate elements
        Otherwise Tag should be a list of tags or iterable, and XVals a list of multiple values
        Accumulate the results and return'''
        Y = []
        for x in XVals:
            if verbose: print (x)
            if isinstance(Tag, str):
                self.setTag(Tag, x)  ## Change tag value
            else:
                self.setTags(Tag, x)
            self.run()
            Y.append(self.getTags(getTagList))
        return Y
            
    def getTag(self, tag):
        '''Fetch a single tag'''
        return self.Tags.TagValue(tag)
    def __getitem__(self, tag):
        return self.Tags.TagValue(tag)

    def getTags(self, tagList):
        '''Fetch a list of tags'''
        return [self.Tags.TagValue(tag) for tag in tagList]

    def setTag(self, tag, value):
        '''Set a single tag'''
        self.Tags.SetTagValue(tag, value)
    def __setitem__(self, tag, value):
        self.Tags.SetTagValue(tag, value)

    def setTags(self, tagLis, valLis):
        '''Set a list of tags to corresponding values'''
        for t, v in zip(tagLis, valLis):
            self.Tags.SetTagValue(t, v)

    def state(self, timeout = 0.0):
        if timeout: time.sleep(timeout)
        return self.Probal.IsStopped

Defined Class functions

Method Description
sc = SysCADCom() Create a SysCADCom class instance.
OpenProject(file) Open a SysCAD project
CloseProject() Close the current project
Close(): Close SysCAD and clean up COM objects
getTag(str) Get the value of tag str
setTag(str, val) Get the value of tag str to val
getTags(tagLis) Get the value of tags in list or iterable
setTags(tagLis, vals) Set the value of tags in list to items in vals
run() Run a Probal model
runScenarios(Vals, TagsIn, TagsOut) Run scenarios setting a single or mutiple tags

Making the Python Class Available for use

To use the module, save the provided code in a file named syscadif.py. Make sure this file is either:

  • In the working directory of your Python script, or
  • Located somewhere on your PYTHONPATH, this ensures the module can be imported from any script.

Alternatively, you can manually add the module’s path to your import path using Python code. Once the module is accessible, you can import it and create an instance of the SysCADCom class as shown below:

import sys
sys.path.append(r"C:\SysCAD139\BaseFilesUser\Scripts")   ## syscadif.py is saved in a subdirectory of BaseFilesUser

import syscadif
sc = syscadif.SysCADCom()
sc.OpenProject(   )  ## Open project, add the path and name of project inside ()

Using the Python Class to run Scenarios

Running simple Scenarios

After a project is loaded, you can get and set tags and run the model or scenarios.

import syscadif
sc = syscadif.SysCADCom()

ScdDir = r'C:\SysCAD%d' % sc.build   ## Installation folder, change if using a different location
ScdPrj = r'\Examples\25 Gold\Demo Gold Project.spf\Project.spj'    ## Saved Project version
sc.OpenProject(ScdDir+ScdPrj)  ## Open project 

sc.setTag("SLURRY_IN.QmReqd (t/h)", 50.0)
sc["SLURRY_IN.QmReqd (t/h)"] =50.0    ## equivalent to the line above
sc.run()
prod = sc.getTag("P_007.Qo.QM.Au(s) (oz/d)")
prod = sc["P_007.Qo.QM.Au(s) (oz/d)"] ## equivalent to the line above

## Setting a single tag for three different scenarios: Flow in P_001 and P_003 for feeds of 1, 3, and 5 tph
print(sc.RunScenarios([45,50,55], "SLURRY_IN.QmReqd (t/h)", ["P_007.Qo.Calc.GoldinOre_ppm", "P_016.Qo.Calc.GoldonCarbon_ppm"])) 
# Four different scenarios, setting two tags
print(sc.RunScenarios([[250,18], [250,20], [300, 18], [300, 20]],    ## List of value lists
                 ["CSTR.TankVolume (m^3)","CSTR.CarbonConc (g/L)"], 
                 ["P_001.Qo.Calc.GoldinOre_ppm", "P_007.Qo.Calc.GoldinOre_ppm"]))
                 
input('Enter to finish')
sc.Close()

Streamlined Data Capture for SysCAD Dynamic Projects

The following example Python script automates the running and monitoring of a SysCAD dynamic simulation. It connects to a specified SysCAD project, executes the simulation until a user-defined simulated time is reached, and records key process variables at regular intervals. The collected data is saved to a CSV file, optionally visualised in plots, and added to a summary report for comparing results across multiple runs. The script supports interactive control, making it easy to initiate, repeat, and analyse simulation runs efficiently.

This example uses the Dynamic Plant Example Project.

Python Code for Streamlined Data Capture for SysCAD Dynamic Projects      
import os
import sys
import csv
import time
import matplotlib.pyplot as plt

# Import SysCAD interface module
import syscadif  # Ensure syscadif is installed and accessible

# Define project and report paths
ScdDir = r'C:\SysCAD139'
ScdPrj = r'\ExamplesDynamic\Dynamic Plant Example.spf\Project.spj'
project_path = os.path.join(ScdDir, ScdPrj.lstrip("\\"))
project_folder = os.path.dirname(project_path)
report_dir = os.path.join(project_folder, "Reports")
os.makedirs(report_dir, exist_ok=True)  # Create report directory if it doesn't exist

# Connect to SysCAD
sc = syscadif.SysCADCom()
sc.OpenProject(project_path)

# Define tags to monitor during simulation
tags = [
    "PlantModel.Stats.SimTime.Total (h)",  # Simulated time
    "Acid.Qm (t/h)",                       # Acid flow rate
    "REACTION_TANK.Lvl (%)",               # Reaction tank level
    "Output.Qo.CMC.H2SO4 (g/L)",           # Sulfuric acid concentration
    "General_Control.Valve_1Position (%)", # Valve position
    "General_Control.PumpSpeed (%)"        # Pump speed
]

def run_simulation(run_number, required_sim_time=10.0):
    """
    Runs the SysCAD simulation until the required simulated time is reached.
    Collects tag data at regular intervals and saves to CSV.
    """
    sc.run()
    data = []
    interval_seconds = 0.1  # Sampling interval

    print(f"\n🚀 Run {run_number} started...")

    while sc.Dynamic.IsRunning:
        try:
            sim_time = sc[tags[0]]
            if sim_time >= required_sim_time:
                sc.stop()
                print(f"✅ Simulated time {required_sim_time} h reached. Simulation stopped.")
                break

            # Record current values for all tags
            row = [sim_time]
            for tag in tags[1:]:
                try:
                    value = sc[tag]
                except Exception as e:
                    print(f"⚠️ Error reading tag {tag}: {e}")
                    value = None
                row.append(value)

            data.append(row)
            time.sleep(interval_seconds)

        except Exception as e:
            print(f"❌ Unexpected error: {e}")
            break

    if not sc.Dynamic.IsRunning:
        print("🛑 Simulation stopped.")
    else:
        print("✅ Run completed.")

    # Save collected data to CSV
    csv_filename = os.path.join(report_dir, f"syscad_data_log_run_{run_number}.csv")
    with open(csv_filename, 'w', newline='') as f:
        writer = csv.writer(f)
        writer.writerow(["Simulated Time (h)"] + tags[1:])
        writer.writerows(data)

    print(f"📊 Collected {len(data)} data points.")
    print(f"📁 Data saved to: {csv_filename}\n")
    return data

def plot_data(data, run_number):
    """
    Generates plots for each tag against simulated time and saves as PDF.
    """
    sim_times = [row[0] for row in data]

    # Define y-axis ranges based on units
    y_ranges = {
        "%": (0, 100),
        "t/h": (0, 20),
        "g/L": (0, 50)
    }

    num_plots = len(tags) - 1
    cols = (num_plots + 1) // 2
    fig, axs = plt.subplots(2, cols, figsize=(12, 6), sharex=True)
    axs = axs.flatten()

    for i, tag in enumerate(tags[1:]):
        values = [row[i + 1] for row in data]
        axs[i].plot(sim_times, values)
        axs[i].set_ylabel(tag)
        axs[i].set_title(tag)
        axs[i].grid(True)

        # Apply y-axis range based on unit match
        for unit, (ymin, ymax) in y_ranges.items():
            if unit in tag:
                axs[i].set_ylim(ymin, ymax)
                break
        else:
            axs[i].set_ylim(bottom=0)  # Default lower bound

    # Hide unused subplots
    for ax in axs[num_plots:]:
        ax.axis('off')

    axs[-1].set_xlabel("Simulated Time (h)")
    plt.xticks(rotation=45)
    plt.tight_layout()

    # Save plot to PDF
    plot_filename = os.path.join(report_dir, f"syscad_plot_run_{run_number}.pdf")
    plt.savefig(plot_filename)
    plt.show()

    print(f"🖼️ Plot saved to: {plot_filename}")

def update_output_summary(run_number):
    """
    Updates the OutputSummary.csv file with final tag values from the current run.
    """
    summary_path = os.path.join(report_dir, "OutputSummary.csv")

    # Read existing summary
    with open(summary_path, 'r', newline='') as f:
        reader = csv.reader(f)
        rows = list(reader)

    # Extract variable tags from first column (ignoring header cell)
    variable_tags = [row[0] for row in rows[1:]]

    # Fetch values from SysCAD
    new_column = [f"Run {run_number}"]
    for tag in variable_tags:
        try:
            value = sc[tag]
        except Exception as e:
            print(f"⚠️ Error reading summary tag {tag}: {e}")
            value = None
        new_column.append(value)

    # Append new column to each row
    for i in range(1, len(rows)):
        rows[i].append(new_column[i])

    # Update header
    rows[0].append(new_column[0])

    # Write updated summary
    with open(summary_path, 'w', newline='') as f:
        writer = csv.writer(f)
        writer.writerows(rows)

    print(f"📄 OutputSummary.csv updated with results from Run {run_number}")

# Main loop for interactive simulation runs
run_number = 1
while True:
    if input("Start run? (y/n): ").strip().lower() != 'y':
        break

    try:
        required_sim_time = float(input("Enter required simulated time in hours (default 10.0): ").strip())
    except ValueError:
        required_sim_time = 10.0

    # Run simulation and collect data
    data = run_simulation(run_number, required_sim_time)

    # Update summary file
    update_output_summary(run_number)

    # Optional plotting
    if input("Plot data? (y/n): ").strip().lower() == 'y':
        plot_data(data, run_number)

    # Prompt for another run
    if input("Another run? (y/n): ").strip().lower() != 'y':
        break

    run_number += 1

# Cleanup SysCAD connection
sc.Close()
print("🧹 Simulation closed and cleaned up.")

Access other Functions

Not all COM methods are directly implemented through the class interface, but you can still access them via member attributes.

For example, the Python scripts used in the examples on the Programmatic Model Generation page call functions to create a new project, add unit models, and establish links between them.

Distributed Example Projects

The SysCAD distributed example projects that uses this Python class are: