Tutorial: Adding new functionality to RepTate

Goals

In this section, we will present how to create a new RepTate application. We will show the important steps needed to build a new RepTate application. To do so, we will need to modify some already existing RepTate code, as well as writing new custom code.

As an example, let us say that we want to create a new RepTate application that:

  1. accepts text files (.xy) which are supposed to be composed of:

    • a first line (header) containing the date and temperature T

    • two columns containing data \(x\) and \(y\)

  2. allows to view the data:

    • without transformation, \(x\) vs \(y\)

    • \(x\) vs \(\sqrt{y}\)

  3. has a theory: a simple line fitting with two parameters \(a\) and \(b\) such that \(y = ax+b\).

Tip

Each time we need to modify or add some line of Python code, the line number is indicated on the left hand side for information. This should correspond to the same line numbering for the “template” files, but might be different for the files in the core/ or gui/ folders.

New application

Create a new application file

To create a new RepTate application, we will use the template application file ApplicationTemplate.py that can be found in the applications/ folder.

  1. Make a copy of this file and rename it with a sensible name that relates to the application purpose. According to the Goals section, we name it ApplicationXY.py.

  2. Open ApplicationXY.py with your favourite text editor and replace all the occurrences of “Template” by “XY”. For example,

    46 class ApplicationTemplate(CmdBase):
    

    becomes

    46 class ApplicationXY(CmdBase):
    
  3. Give a brief description of the purpose of the application, e.g. “Application reading XY files”. The first lines of ApplicationXY.py should now look like

    33"""Module ApplicationXY
    34
    35Application reading XY files
    36
    37"""
    

The file ApplicationXY.py is ready for the next round of modifications that are (i) file types accepted by the application, (ii) the views, and (iii) the theories, as defined in the Goals section. But first, we have to let RepTate know about our new application.

Edit RepTate’s ApplicationManager

We need to add a reference to this new application into RepTate’s ApplicationManager, so it knows it exists. To do so:

  1. Insert this line in the top part of the file core/ApplicationManager.py, e.g.

    56from ApplicationXY import ApplicationXY
    
  2. Insert the following line to add an entry to the ApplicationManager dictionary

    115self.available_applications[ApplicationXY.appname] = ApplicationXY
    

Note

Our new application is ready to be used in Command Line RepTate!

Edit RepTate’s QApplicationManager

In order to have our new application available in the Graphical User Interface (GUI) version of RepTate (and not just available in the Command-Line version of RepTate), we need to create a new “button” that will launch our new application when clicked. We will edit the file gui/QApplicationManager.py in this purpose.

  1. Add a button in the main RepTate tool-bar by inserting the following lines in the __init__ method of gui/QApplicationManager.py. The icon name (filename) should correspond to the appmane, here XY.png. See the section New Icons to create and use your onwn icon in RepTate.

    142 # ApplicationXY button
    143 #choose the button icon
    144 icon = QIcon(':/Icon8/Images/new_icons/XY.png')
    145 tool_tip = 'XY'  # text that appear on hover
    146 self.actionXY = QAction(icon, tool_tip, self)
    147 #insert the new button before the "MWD" button
    148 self.toolBarApps.insertAction(self.actionMWD, self.actionXY)
    
  2. The new button has been successfully inserted into the application tool bar. However, if we click on it, nothing happens because it is not linked to any action. We need to “wire” (connect) this new button to a “function”. In the same file gui/QApplicationManager.py, below the previous lines, add

    149 #connect button
    150 self.actionXY.triggered.connect(lambda: self.handle_new_app('XY'))
    

    Warning

    The application name (appname), defined at line 79 of ApplicationXY.py, should then be “XY”. Additionally, the icon name defining the logo of the new application should be named “XY.png”, see the definition of the handle_new_app method.

Note

Our new application is ready to be used in GUI RepTate!

Note on default theories

By default, some “basic theories” are included with the application (e.g. polynomial, power-law, exponential). To remove all these “basic theories” from your new application, comment the following line in the __init__ method of class BaseApplicationXY

135self.add_common_theories()  # Add basic theories to the application

New file type

RepTate applications are designed to accept a only a predefined file extension. As defined in the Goals section, we want our new application ApplicationXY.py to accept .xy files. To do so, we modify ApplicationXY.py as follows.

In class ApplicationXY, before def __new__, add

53extension = "xy"  # drag and drop this extension automatically opens this application

In the __init__ method of class BaseApplicationXY add

120# set the type of files that ApplicationTemplate can open
121ftype = TXTColumnFile(
122    name='XY data',  # name the type of data
123    extension='xy',  # file extension
124    description='XY data from XY-experiment',
125    col_names=['X', 'Y'],  # name the variables for legend
126    basic_file_parameters=['date', 'T'],  # parameter in file header
127    col_units=['-', '-'])  # units of X and Y (here none)

New view

About the “old” view

At the moment, only one view is allowed in our new ApplicationXY. That view is located in the __init__ method of class BaseApplicationXY:

 95# VIEWS
 96# set the views that can be selected in the view combobox
 97self.views['y(x)'] = View(
 98    name='y(x)',
 99    description='y as a function of x',
100    x_label='x',
101    y_label='y(x)',
102    x_units='-',
103    y_units='-',
104    log_x=False,
105    log_y=False,
106    view_proc=self.viewyx,
107    n=1,
108    snames=['y(x)'])

The important attributes of the view called “y(x)” are:

  • the x- and y-label to be used in the plot,

  • the units that are appended to the x- and y-labels,

  • the log_x and log_y define whether the axes should be in in log-scale (base 10)

  • self.viewyx is the method that defines what operations are done on the data before plotting them (see below),

  • n defines the number of series the view is plotting.

In the line below, you can define the default number of view, i.e., the number of views that appear when you open the appliction. In case the new application would benefit from having multiple views shown at the same time (similar to the React or Stress Relaxation applications), this number can be increased (up to 4)

110# set multiviews
111# default view order in multiplot views, set nplots=1 for single view
112self.nplots = 1

The definition of the method viewyx is given by

140def viewyx(self, dt, file_parameters):
141    """Documentation"""
142    x = np.zeros((dt.num_rows, 1))
143    y = np.zeros((dt.num_rows, 1))
144    x[:, 0] = dt.data[:, 0]
145    y[:, 0] = dt.data[:, 1]
146    return x, y, True

The two lines x[:, 0] = dt.data[:, 0] and y[:, 0] = dt.data[:, 1] tell us that viewyx does not perform any operations on the data. It simply copies the input data into x and y arrays. It means that we already have one of the views required from the Goals section.

Definition of a new view

To define a new view that shows \(x\) vs \(\sqrt{y}\), as requested in the Goals section, we add a view to self.views dictionary. The new view is called “sqrt(y)”. In the __init__ method of class BaseApplicationXY, add

110self.views['sqrt(y)'] = View(
111    name='sqrt(y)',
112    description='sqrt(y) as a function of x',
113    x_label='x',
114    y_label='$y^{1/2}$',
115    x_units='-',
116    y_units='-',
117    log_x=False,
118    log_y=False,
119    view_proc=self.view_sqrt_y,
120    n=1,
121    snames=['sqrt(y)'])

Tip

The x_label and y_label support LaTeX-like syntax.

We also need to define the new method view_sqrt_y. In class BaseApplicationXY, add the definition

158def view_sqrt_y(self, dt, file_parameters):
159    """Documentation"""
160    x = np.zeros((dt.num_rows, 1))
161    y = np.zeros((dt.num_rows, 1))
162    x[:, 0] = dt.data[:, 0]
163    y[:, 0] = (dt.data[:, 1])**0.5
164    return x, y, True

Note

The new view is ready!

New theory

Create a new theory file

To create a new RepTate application, we will use the template theory file TheoryTemplate.py that can be found in RepTate theories/ folder.

  1. Make a copy of this file and rename it with a sensible name that relates to the theory purpose. According to the Goals section, we name it TheoryLine.py.

  2. Open TheoryLine.py with your favourite text editor and replace all the occurrences of “Template” by “Line”. For example,

    45 class TheoryTemplate(CmdBase):
    

    becomes

    45 class TheoryLine(CmdBase):
    
  3. Give a brief description of the purpose of the application, e.g. ” Theory fitting a line to the data”. The first lines of TheoryLine.py should now look like

    33"""Module TheoryLine
    34
    35Theory fitting a line to the data
    36
    37"""
    38import numpy as np
    

The file TheoryLine.py is ready for the next round of modifications that are (i) define the parameters, (ii) define the theory “function”. But first, we have to let ApplicationXY (developed just above) know about our new theory.

Edit ApplicationXY.py

We need to add a reference to this new theory into ApplicationXY.py, so it knows it exists. To do so:

  1. Insert the following line in the __init__ method of class BaseApplicationXY, after the “# IMPORT THEORIES” comment

    89 # IMPORT THEORIES
    90 # Import theories specific to the Application e.g.:
    91 from TheoryLine import TheoryLine
    

    Hint

    We choose to place the theories import inside the __init__ method of class BaseApplicationXY rather than in the very top of the file ApplicationXY.py as this prevents RepTate from loading all theories at start. Instead, theories are loaded only when an application using them is opened.

  2. Insert the following line, also in the __init__ method of class BaseApplicationXY, after the # THEORIES, and before self.add_common_theories(), the line

    134 self.theories[TheoryLine.thname] = TheoryLine
    

Edit the theory

According to the Goals section, the theory should define a straight line \(y=ax+b\), hence there are two parameters. We will (i) write a short documentation of our new theory, (ii) define the parameters, and (iii) write the main function that calculates the theory values.

  1. Add a Python docstring to (auto)-document the theory. Place some description of the goal of the theory as well as a description of the parameters. This will help future reader of the file understand the purpose of the theory and it will be automatically integrated to the online RepTate documentation (reptate.readthedocs).

    46 class TheoryLine(CmdBase):
    47     """Fit a straight line.
    48
    49     * **Function**
    50         .. math::
    51             y = a x + b
    52
    53     * **Parameters**
    54     - :math:`a`: slope of the line
    55     - :math:`b`: the :math:`y`:-intercept
    56
    57     """
    
  2. To define the theory parameters, \(a\) and \(b\), we modify the __init__ method of class BaseTheoryLine to have only these two parameter definitions

     95 self.parameters['a'] = Parameter(
     96     name='a',
     97     value=1,
     98     description='parameter a',
     99     type=ParameterType.real,
    100     opt_type=OptType.opt)
    101 self.parameters['b'] = Parameter(
    102     name='b',
    103     value=0,
    104     description='parameter b',
    105     type=ParameterType.real,
    106     opt_type=OptType.opt)
    

    The important attributes of the parameters are:

    • value: the initial value of the parameter

    • type: defines if he parameter is real, integer or discrete

    • opt_type: optimisation type is either const for constant parameter (cannot be optimised), opt if the parameter is optimised by default, nopt if the parameter can be optimised but is not by default.

  3. Modify the method calculate of class BaseTheoryLine

    143 ft = f.data_table
    144 tt = self.tables[f.file_name_short]
    145 tt.num_columns = ft.num_columns
    146 tt.num_rows = ft.num_rows
    147 tt.data = np.zeros((tt.num_rows, tt.num_columns))
    148 a = self.parameters['a'].value
    149 b = self.parameters['b'].value
    150 tt.data[:, 0] = ft.data[:, 0]  # x values
    151 tt.data[:, 1] = a * ft.data[:, 0] + b  # y values
    

    Hint

    • The file type of ApplicationXY defined in section New file type tells us that there are two columns in the data files. Hence, the theory data also have two columns to populate. For example of application/theory using more than two data columns, see class BaseApplicationLVE of ApplicationLVE.py and class BaseTheoryMaxwellModesFrequency of TheoryMaxwellModes.py.

    • The information from the data file header, in our example date and T, can be called via, e.g. T = float(f.file_parameters["T"]). Parameters are stored as strings, hence the float conversion.

Note

The new “Line” theory is ready to be used in our new ApplicationXY!

New Icons

Application icons are stored in a compiled resource file gui/MainWindow_rc.py. In order to add a new icon to this resource file, that can later be used as a button icon for instance, we need to

  1. Modify the file gui/MainWindow.qrc by opening it in a text editor and add the relative path of the new image or icon we want to have in the resource file. For instance:

    • copy and paste you favourite icon my_favourite_icon.png in the gui/Images/new_icons/ folder.

    • add the line <file>Images/new_icons/my_favourite_icon.png</file> to the file gui/MainWindow.qrc

  2. Re-compile the file MainWindow_rc.py into a resource file MainWindow_rc.py by running the following command in a terminal (assuming the current working directory is gui/)

    $ pyrcc5 MainWindow.qrc -o MainWindow_rc.py
    

Note

Your new icon my_favourite_icon.png is now ready to be used by Qt:

icon = QIcon(':/Icons/Images/new_icons/my_favourite_icon.png')