Quickstart to custom C++ modules

Updated: August 13th, 2011

User modules are actually custom C++ classes. The easiest way to create a module is to use a base class named DefaultGUIModel. DefaultGUIModel constructs a simple graphical user interface that allow users to interact with parameters and activate real-time code. These modules also inherit methods for hard real-time execution and event handling, the ability to generate and accept signals, and the ability to have metadata automatically captured by the Data Recorder to HDF5 data files. The following sections describe the Neuron module, which provides a Hodgkin-Huxley model neuron that generates a membrane voltage signal and accepts an optional external current input. The GUI consists of a column of textboxes and associated labels to display the module’s param- eters and internal state variables. Each instantiation of a user module is given a unique instance ID that appears in the left corner of the window’s title bar. Parameters are editable variables and their textboxes are shown in black. The internal state variables of a module are traditionally considered intermediate computed values that should not be edited manually by the user. States are shown in gray.

Jump to: Define module parameters, Module Initialization, execute() loop, update() function

Create your own module class

The quickest way to create a new user module is to copy an existing module directory and rename the class. This involves renaming the class header (*.h) file, the class implementation (*.cpp) file, editing the Makefile and editing any instances of the old class name within each of these files. The latter include the class name, scope names, the constructor, and the deconstructor must be updated. A template user module is available in /rtxi/doc/my plugin. A sample Makefile (compatible with RTXI v1.2 and later) is provided below:

PLUGIN_NAME = my_plugin

HEADERS = my_plugin.h

SOURCES = my_plugin.cpp

LIBS =

### Do not edit below this line ###

include $$(shell rtxi_plugin_config --pkgdata-dir)/Makefile.plugin_compile

The PLUGIN NAME is the name that will be given the shared object library (*.so) file when it is compiled. You should edit the HEADERS and SOURCES to reflect the new source file names. For simple modules based on a single class, there will be a single header file and a single source file. You may base your module on additional custom classes whose sources must then be included here as well. The LIBS flag is used for any additional library flags. Here, -lgsl links this module against the GNU Scientific Library. As noted, do not edit the last line. This will append additional code that correctly links your new module with other shared libraries on your system and makes them available to RTXI. For more examples of Makefiles, see any of the downloadable modules on this site.

Throughout the class header and implementation file, change all the class names to your own class. If you started from the Hodgkin-Huxley neuron model, you would replace every instance of “Neuron” before the function declarations and implementations. You should have unique class names for all of your custom modules. Even if you create a new module within a new directory with new source file names, if you have the same module class name defined in the header, you will overwrite an already existing class.

Test that you have made all the changes necessary for creating a new class by compiling the module:

$ make
$ sudo make install

You should see some libraries appear in that directory and my_plugin.so should be copied to /usr/local/lib/rtxi. Your module should appear when you select Modules->Load User Module from the RTXI toolbar.


Define model parameters, inputs, and outputs

Each line in the structure below defines an INPUT, OUTPUT, PARAMETER, STATE variable, or COMMENT for the module. Declaring an INPUT creates a slot for your module to acquire data from the DAQ card or from another module. Declaring an OUTPUT creates a signal that is emitted from your module that can be send to your DAQ card or any other module. These inputs and outputs will be accessible when you open the Connector module or the Data Recorder module. In the example code below, there is only one input, Iapp, and its value is accessed as the variable input(0). The values of additional inputs would be accessed as input(1), input(2), and so on. The same rule applies for outputs eg. the value of Vm is accessed as output(0). RTXI’s signals-and-slots architecture allows you to connect any signal to any slot. There is no error checking that the connection is valid, eg. that quantities with matching units of measurement are connected.

STATE variables and PARAMETERs are numeric datatypes. State variables are internal model variables that cannot be modified by the user through the GUI. Their values may be constant or they may change over time. It is common to use a STATE to track the values of intermediate or computed quantities. These state variables can also be saved by the Data Recorder. A PARAMETER will accept user input through the GUI and can be modified on the fly during real-time execution. State variables and parameters appear in the GUI in the order that they are declared. However, state variables are not editable and are displayed in gray. You can simply monitor the values of the state variables as they change. In the example code, this mechanism is used to monitor the ion channel’s activation variables, which are dependent on membrane voltage and integrated in real-time. A COMMENT is similar to a PARAMETER, but is used to store text strings such as information about the experiment that you would like to log. These are saved to the Data Recorder just like parameters, but should not be modified in real-time during model execution.

The declaration of these types follows a simple syntax. Every DefaultGUIModel module must have a workspace variable called variable t as shown below. The first argument on each line is the label for the textbox in the GUI. This does not have to be the same as the variable name you use in the code to actually store the parameter value. The second argument is displayed as a Tooltip when you use your mouse to hover the cursor over that entry in the GUI. Enter any descriptive information here about the variable, such as an expanded form of your text label or the correct units of measurement. The third argument defines the variable as an input, output, etc. Notice that for parameters, you can also specify whether it is a double or integer numeric type.

If your parameter name contains a forward slash “/”, its values will not be automatically saved by the Data Recorder. This is a limitation of the HDF5 file format, which uses a directory-like syntax for specifying the data structure.

static DefaultGUIModel::variable_t vars[] =
{
{"Iapp","A",DefaultGUIModel::INPUT,},
{"Vm","V",DefaultGUIModel::OUTPUT,},
{"V0","mV",DefaultGUIModel::PARAMETER|DefaultGUIModel::DOUBLE,},
{"Cm","uF_cm^2",DefaultGUIModel::PARAMETER|DefaultGUIModel::DOUBLE,},
{"G_Na_max","mS_cm^2",DefaultGUIModel::PARAMETER|DefaultGUIModel::DOUBLE,},
{"E_Na","mV",DefaultGUIModel::PARAMETER|DefaultGUIModel::DOUBLE,},
{"G_K_max","mS_cm^2",DefaultGUIModel::PARAMETER|DefaultGUIModel::DOUBLE,},
{"E_K","mV",DefaultGUIModel::PARAMETER|DefaultGUIModel::DOUBLE,},
{"G_L","mS_cm^2",DefaultGUIModel::PARAMETER|DefaultGUIModel::DOUBLE,},
{"E_L","mV",DefaultGUIModel::PARAMETER|DefaultGUIModel::DOUBLE,},
{"rate","Hz", DefaultGUIModel::PARAMETER|DefaultGUIModel::UINTEGER,},
{"m" "Sodium Activation",DefaultGUIModel::STATE,},
{"h","Sodium Inactivation",DefaultGUIModel::STATE,},
{"n","Potassium Activation",DefaultGUIModel::STATE,},
};

Return to top

Initialize the model

The next section is the model constructor. If you changed the class name, this would read “YOURMODEL::YOURMODEL”. You can set the text that will appear in the title bar of your module window using the first argument of the constructor method. For RTXI v1.3, the next required line is a call to the createGUI() function which actually generates the GUI. This line is not needed in previous versions of RTXI. In this section, you should also initialize all the variables and parameters and make sure that the GUI reflects the actual values that are being used. In this example, much of this code is performed by the update() function under the INIT flag. In other modules downloadable from our website, you will find a separate initParameters() function that handles all variable initializations.

It is convenient to perform unit conversions when calling these functions so that the GUI accepts input in more user-friendly units. Finally, you should call refresh() to update the GUI to reflect your changes. The GUI textboxes will be initialized to the current values of the variables and STATE variables will be updated periodically during model execution.

Neuron::Neuron(void) :
DefaultGUIModel("Neuron", ::vars, ::num_vars) {
  createGUI(vars, num_vars); // comment this line out for RTXI v1.2
  V = V0;
  m = m_inf(V0);
  h = h_inf(V0);
  n = n_inf(V0);
  period = RT::System::getInstance()->getPeriod() * 1e-6;
  update(INIT);
  refresh();
}

Notice the method for retrieving the real-time period (sampling rate) of the system:
RT::System::getInstance()->getPeriod();This returns the period in nanoseconds.Return to top

The execute() loop

The execute() function will run to completion on every time step. The computations performed here must complete within the real-time period that you have set in the System Control panel to maintain system stability. The efficiency of your code here will affect the performance of your system. You should use private variables defined in the class header rather than creating variables inside the function on every time step. If you absolutely must create a variable inside execute(), use a static call so that the same memory block is used each time. You should be wary of using do-while and for structures if you are uncertain how long these loops will take to complete. Within the execute function, you must also be careful to bound the output signal and perform your own error checking to maintain the stability of the closed-loop. Notice that at the end, we have set output(0) to update the membrane voltage signal emitted by this module.

void Neuron::execute(void) {
  for (int i = 0; i < steps; ++i)
    solve(period / steps, y);
  output(0) = V * 1e-3;
}

Return to top

The update() function

The update function is provided with several flags to help you organize your code and handle events in your module. The flags provided with the DefaultGUIModel class are:

  • INIT: non-event related but useful for placing code to initialize the model
  • MODIFY: called when the "Modify" button is pressed
  • PAUSE: called when the model is paused
  • UNPAUSE: called when the model is unpaused
  • PERIOD: called when the real-time period of the system is changed

Under the INIT flag, you should initialize any additional variables or GUI settings that were not already addressed in the constructor. To assign a variable to be updated as a STATE variable in the GUI, use:
setState("YOUR_GUI_LABEL", YOUR_VARIABLE);

YOUR_GUI_LABEL must exactly match the label that you set in variable_t vars[] above. Similarly, you initialize the GUI for a PARAMETER with:
setParameter("YOUR_GUI_LABEL", YOUR_VARIABLE);
It is often the case that you may want to display units in the GUI with more convenient physiological units of measurement, eg. mV instead of V. In that case, you can call the function as follows:
setParameter("E Na", E Na*1000); // convert to mV

Under the MODIFY flag, you should grab all the values in the GUI textboxes and update the values of the parameters as follows:

YOUR_VARIABLE = getParameter("YOUR_GUI_LABEL").toDouble();

If you do any unit conversions with setParameter(), make sure you do the inverse with getParameter(). You may also want to add code to the PAUSE flag to set the output of your module to zero, e.g. the amplitude of an injected current. In some cases, you will want to reset certain internal variables when you stop or start the model eg. a counter that keeps track of your model execution time. Under the PERIOD flag, you will always want to update your model with the new real-time period.

void Neuron::update(DefaultGUIModel::update_flags_t flag) { switch (flag) {
  case INIT:
    setState("m", m);
    setState("h", h);
    setState("n", n);
    setParameter("V0", V0);
    setParameter("Cm", Cm);
    setParameter("G_Na_max", G_Na_max);
    setParameter("E_Na", E_Na);
    setParameter("G_K_max", G_K_max);
    setParameter("E_K", E_K);
    setParameter("G_L", G_L);
    setParameter("E_L", E_L);
    setParameter("Iapp_offset", Iapp_offset);
    setParameter("rate", rate);
    break;
  case MODIFY:
    V0 = getParameter("V0").toDouble();
    Cm = getParameter("Cm").toDouble();
    G_Na_max = getParameter("G_Na_max").toDouble();
    E_Na = getParameter("E_Na").toDouble();
    G_K_max = getParameter("G_K_max").toDouble();
    E_K = getParameter("E_K").toDouble();
    G_L = getParameter("G_L").toDouble();
    E_L = getParameter("E_L").toDouble();
    Iapp_offset = getParameter("Iapp_offset").toDouble();
    rate = getParameter("rate").toDouble();
    steps = static_cast (ceil(period * rate / 1000.0));
    V = V0; // reinitialize the model
    m = m_inf(V0);
    h = h_inf(V0);
    n = n_inf(V0);
    break;
  case PAUSE:
    break;
  case UNPAUSE:
    break;
  case PERIOD:
    period = RT::System::getInstance()->getPeriod() * 1e-6; // ms
    steps = static_cast (ceil(period * rate / 1000.0));
    break;
  default:
    break;
  }
}

Return to top