SIMCA Add-Ins

Creating ribbon buttons

Table of content

With a Python script you can add command buttons to SIMCAs 'Add-Ins' tab that will execute Python code when clicked.

Add-Ins tab The Add-Ins tab with two command buttons.

Hello World!

We will start with a very simple example that will pop up a message box containing 'Hello World!.

To create an add-in we must create a python module that contains a Python class which defines what the button should look like, it's name and the code that will be executed when the button is clicked.
In order for SIMCA to be able to find the class, it must inherit from a base class called RibbonAddInABC and the file must be in a location where SIMCA can find it.

The first thing we need to do is tell SIMCA where it should look for add-ins.
On the 'Developer' tab, click 'Set paths'. This dialog will pop up.

Set paths

In the dialog you can specify which directories SIMCA will search for add-ins. By default the 'AppData\Roaming\Umetrics\SIMCA\16.0\Python' directory of the current user is searched. The add-in can be created in that directory but in this example we will save it in your 'Documents' directory.

The dialog should now look something like this:

Set paths

Click OK.

Next we will create the source file that will contain our add-in.
On the 'Developer' tab, click 'Create new add-in'. This dialog pops up.

Add-in dialog

In the dialog we can enter the name of the new button (Caption:), the buttons tooltip, the icon the button should use, if the button should have an enabler (more on that later) and the name of the buttons ribbon group.
All of these except 'Caption:' are optional and we will skip them for now. Enter 'Hello World!' in the 'Caption:' edit field and click OK.

A save file dialog pops up asking for the name of the new python file. Enter hello_world.py (newer use spaces in python file names). The dialog will use the directory you just created as the default directory. Click OK.

A text editor will open up with the new file which contains some boiler plate code that SIMCA has added.

from umetrics import *
from ribbonabc import RibbonAddInABC

class AddInCommand(RibbonAddInABC):

    def on_command(Self) :
        # Remove 'pass' below and add the code to execute when the button is pushed.
        pass

    def get_button_name(Self):
        return 'Hello World!'

group_order = [AddInCommand]

The first line imports functionality from the umetrics package and the second line imports a class called RibbonAddInABC which is the base class we must use to create the button.

Next is the declaration of the class that implements the button. You can change the name of the class if you want to, SIMCA doesn't use the class name in any way so it can be called anything. The class inherits RibbonAddInABC which marks the class as an Add-In to SIMCA.

The class has two methods, on_command() and get_button_name(). on_command() will be executed whenever the user presses the button and get_button_name() returns the name of the button. This is the bare minimum that the class must contain.

The final line is only useful if the module contains more than one add-in class. It tells SIMCA the order of the buttons in the ribbon.

If you now close and restart SIMCA the ribbon should have a new tab called 'Add-Ins' with a button called 'Hello World!'.
If you can't see the Add-Ins tab, click 'File', 'Options', 'Customize' and check 'Add-Ins' in the right list.

Customize

Finally we will add the code that pops up the message box.

After 'from ribbonabc import RibbonAddInABC' add

from tkinter import Tk, messagebox

and replace the code in on_command with:

        root=Tk()
        root.withdraw()
        messagebox.showinfo(title= 'Hello', message= 'Hello World!')
        root.destroy()

The example should now look like this. Restart SIMCA and click the 'Hello World!' button. This will pop up:

Hello world

If something goes wrong

If the Add-Ins tab is empty, it is probably because there is an error in the Python file or because the file isn't saved in the right place.

Check that the file is located in one of the directories SIMCA searches for Python modules ('Developer' tab, 'Set paths').

If that is the case, try importing the file in the python console. Enter the following in the Python console:

import hello_world

If you get an error message it will tell you on which line the error was encountered. Ex:

>>> import hello_world
Traceback (most recent call last):
  File "UMETRICS", line 1, in <module>
  File "C:\Users\Jerkero\Documents\Add-In\hello_world.py", line 10
    root.withdraw()
                  ^
IndentationError: unindent does not match any outer indentation level

This tells you that line 10 in the Python file isn't indented properly. Python is very picky when it comes to indentation which is a common source of errors.

A more interesting example

In this example we will create two new buttons. The first will create a new PLS model and fit it, the second will export the scores from a model to a csv file and open it in whatever program you use to handle spreadsheets (probably Excel if you have that installed on your computer).

Create the source file

As before, on the 'Developer' tab, click 'Create new add-in'. This time we will use all the fields.

Click OK and save the file. Name it 'example2.py'.

This time the boiler plate code looks like this:

from umetrics import *
from ribbonabc import RibbonAddInABC

class AddInCommand(RibbonAddInABC):

    def __init__(Self):
        Self.icon_file_name = r'umetrics_cube.png'
        Self.tooltip = 'Create a new PLS model using the first dataset.'

    def enable_command(Self):
        # Disable button if SIMCA has no active project
        with SimcaApp.get_active_project() as project:
            if not project:
                return False
            return True

    def on_command(Self) :
        # Remove 'pass' below and add the code to execute when the button is pushed.
        pass

    def get_button_name(Self):
        return 'New PLS model'

group_name = 'Example 2'
group_order = [AddInCommand]

In the __init__() method there is code that defines the icon the button should use and the tooltip text.

The class also has an enable_command() method which will disable the button if SIMCA doesn't have an active project which is fine for this example.

If you restart SIMCA, the 'Add-Ins' tab will now look like this:

Add-Ins

If you move the cursor over the new button, the tooltip text you entered will pop-up.

Create a PLS model

The user will be able to choose which variable to use as the y-variable so the first thing we need to do is get the active project and find out the names of the variables.

Replace the code in on_command with this:

        project = SimcaApp.get_active_project()
        datasets = project.get_dataset_infos()
        dataset = project.get_dataset(datasets[0].ID)
        variables = dataset.get_var_names()

The first line gets the currently active project (the one open in SIMCA). We know that there is one, otherwise the enable_command() method would have disabled the button.
get_dataset_infos() returns a list of DatasetInfo objects, one for each dataset in the project.
In this example we will always use the first dataset which we get from the project in the third line and finally we get the names of the variables in the fourth line.

We now need a method that lets the user choose which variable we should use as the Y-variable. We will use tkinter (one of Pythons standard libraries) to create a dialog that lets the user choose one from a list of items.

Enter this after 'from ribbonabc import RibbonAddInABC'

from tkinter import Tk, ttk, LEFT

class ChooseOne():
    def __init__(self, items, message, default=0):
        """
        Create a dialog that lets the user choose one of several text strings.
        The dialog will have a label with a message, a combobox and an OK and Cancel button
        items   --- a list of strings to choose from.
        message --- a message to display in the dialog
        default --- the index of the default string in the list
        """
        self.root = Tk()
        self.root.title('Choose')
        label=ttk.Label(self.root, text=message)
        label.pack(padx=5, pady=5)

        self.combobox = ttk.Combobox(self.root, state='readonly')
        self.combobox['values'] = items
        self.combobox.current(default)
        self.combobox.pack(fill='x', padx=5, pady=5)

        # Call the on_ok() method when the button is clicked.
        ok_button = ttk.Button(self.root, text = "OK", command = self.on_ok)
        ok_button.pack(side=LEFT, padx=5, pady=5);

        # Call root.destroy() when the button is clicked
        cancel_button = ttk.Button(self.root, text = "Cancel", command = self.root.destroy)
        cancel_button.pack(side=LEFT, padx=5, pady=5);

        # Initialize the selected value to an empty string
        self.selected=''
        self.root.mainloop()

    def on_ok(self):
        # Get the currently selected string and close the dialog.
        self.selected=self.combobox.get()
        self.root.destroy()

    def get_selected(self):
        """
        Returns the selected string or an empty string if the user pressed cancel.
        """
        return self.selected

The details on how to use tkinter is beoynd this tutorial. You can find more information here.

Add this to the end of on_command().

        dialog = ChooseOne(variables, 'Select Y-variable')
        y_variable = dialog.get_selected();
        # If the string is empty, the user pressed Cancel.
        if not y_variable:
            return
        print("Selected Y-variable: ", y_variable)

It is time to test what we got so far. Save the code, and restart SIMCA. Open any project and click the button. Something like this will popup:

select y

If you click OK the script will print the selected variable in the Python console.

To create and fit the model, add this to the end of on_command():

        # Create a new workset using the first dataset
        workset=project.create_workset(datasets[0].ID)
        # Set all variables as X variables
        workset.set_x([])
        # And set the slected variable as Y
        workset.set_y(y_variable)
        workset.set_type(simca.modeltype.pls)
        models=workset.create_model()
        # create_model returns a list of the created models numbers.
        # If the workset contains classes more than one model will be created.
        # In this case we will only get one but we still fit the model in a loop.
        for model in models:
            # Fit the model with at least one component.
            project.fit_model(model, mincomp=1)
        project.save()

We are done with the first button. The example should now look like this. Restart SIMCA and open any (non-batch) project and click the button.

A new PLS model will be created and fitted with at least one component.

Export data

We will now add a new button that exports the scores from the active model to a spreadsheet. To export the data we will create a .csv file and let windows open it with whatever program you use to work with spreadsheets (probably Excel if you have that installed). The reason we use a .csv file is that the Python standard library supports it. To write a .xlsx file we would have to install and use an external library like Openpyxl or XlsxWriter.

To add a new button in the same group as the previous one we will need to add a new class to the same Python source file.

Add this skeleton code after the AddInCommand class declaration:

class ExportData(RibbonAddInABC):
    def __init__(Self):
        Self.tooltip = 'Exports scores from the last model to a spreadsheet.'

    def on_command(Self):
        pass

    def get_button_name(Self):
        return 'Export scores'

Also add the new class to the last line where the order of the buttons in the ribbon group is defined.

group_order = [AddInCommand, ExportData]

Add an enabler

There is no point in clicking the button if there is no active project or if the active model in the project doesn't have any components. To disable the button if this is the case, add an enable_command() method to the class so it looks like this:

class ExportData(RibbonAddInABC):
    def __init__(Self):
        Self.tooltip = 'Exports scores from the last model to a spreadsheet.'

    def enable_command(Self):
        with SimcaApp.get_active_project() as project:
            if not project:
                return False
            model=project.get_active_model();
            if model <= 0:
                return False
            model_infos=project.get_model_infos()
            active_model_info=next(info for info in model_infos if info.number == model)
            if not active_model_info.components:
                return False
            return True

    def on_command(Self):
        pass

    def get_button_name(Self):
        return 'Export scores'

get_model_infos returns a list of ModelInfo objects that contains various information about the models. The line

            active_model_info=next(info for info in model_infos if info.number == model)

finds the active models model info in the list. It uses a very powerfull feature in Python called list comprehension.

(info for info in model_infos if info.number == model)

Creates a 'generator' which can be seen as a 'lazy' list meaning that the list isn't actually created until we use it and 'next' returns the first element in the list. We could have used a foor loop instead but this is more concise and readable.

Get the scores from the model

To get the score from the last model in the project, modify the on_command() method so it looks like this.

    def on_command(Self):
        project = SimcaApp.get_active_project()
        models=project.get_model_infos();
        data_builder=project.data_builder()
        scores=data_builder.create('t', model=models[-1].number)

data_builder() returns an object of type ProjectDataBuilder from which you can obtain any data from the project through the create() method. The first argument is the name of the data type we want, in this case 't'. The method also needs to know the model.

The create() method returns a ProjectData object which basicaly is a table with each serie (score vector) as rows together with names that identifies the rows and columns.

Write the .CSV file

To write the file we need to import csv (for csv support), tempfile (for temporary file support) and io (for writing files).

Add this after 'from tkinter import Tk, ttk, LEFT':

import csv
import tempfile
import io

And this at the end of on_command() to write the file.

        # Create a csv file in windows temp directory
        file_name = tempfile.mktemp(suffix='.csv')
        file=io.open(file_name, mode='w', newline='')
        csv_file=csv.writer(file, delimiter=';')
        # Write the value identifiers (in this case observation names) to the first row. 
        # We need to insert an empty string first since the first column contains series names.
        first_line=[''] + scores.get_value_ids().get_names()
        csv_file.writerow(first_line)
        series_names = scores.series_names()
        data=scores.matrix()
        for serie_index in range(len(series_names)):
            serie=[series_names[serie_index]] + list(data[serie_index])
            csv_file.writerow(serie)
        file.close()

Finally we will let windows open the file with some suitable application. Add this at the end of the import section:

import os

And this at the end of on_command():

        os.startfile(file_name)

The complete example should now look like this:

Restart SIMCA, select a model with at least one component and click the button. Excel (or perhaps libre office calc depending on what you have installed) will open with a spreadsheet that looks something like this: