How to write your own process

Authors: Francois Tadel

Brainstorm offers a flexible plug-in structure. All the operations available when using the Process1 and Process2 tabs, which means most of the Brainstorm features, are in fact written as plug-ins.

If you are interested in running your own code from the Brainstorm interface and benefit from the powerful database and visualization systems, the best option is probably for you to create your own process functions. It can take some time to get used to this logic but it is time well invested: you will be able to exchange code easily with your collaborators and the methods you develop could immediately reach thousands of users. Once your functions are stable, we can integrate them in the main Brainstorm distribution and maintain the code for you to ensure it stays compatible with the future releases of the software.

This tutorial looks long and complicated, but don't let it scare you. Putting your code in a process is not so difficult. Most of it is a reference manual that details all the possible options, you don't need to understand it completely. The last part explains how to copy an existing process and modify it to do what you want.



Before you start reading this long technical document, you should be aware of a simpler solution to execute your own code from the Brainstorm interface.

The process "Pre-process > Run Matlab command" is simple but very powerful. It loads the files in input and run them through a piece of Matlab code that you can edit freely. It can extend a lot the flexibility of the Brainstorm pipeline manager, providing an easy access to any Matlab function or script.


Process folders

A Brainstorm plug-in, or "process", is a single Matlab .m script that is automatically identified and added to the menus in the pipeline editor. Two folders are parsed for plug-ins:

If you write a new process function, name the script "process_...m" and place it in your user folder, it will automatically become available in the pipeline editor menus, when you use the Process1 or Process2 tabs. Avoid using capital letters, spaces or special characters in the process file name.
Warning: Do not work directly in the brainstorm3 folder, or you will lose all your work the next time Brainstorm gets updated.

Send it to another Brainstorm user and your code will automatically be available into the other person's Brainstorm interface. It is a very efficient solution for exchanging code without the nightmare of understanding what are the inputs of the functions (units of the values, dimensions of the matrices, etc.)

Structure of the process scripts


A process function must be named "process_...m" and located in one of the two process folders in order to be recognized by the software. Let's call our example function "process_test.m". It contains at least 4 functions:

You are free to add as many sub-functions as needed to the process file. If your process needs some sub-functions to run, it is preferable to copy the full code directly into the "process_test.m" code, rather than leaving it in separate functions. This way it prevents from spreading sub-functions everywhere, which are later lost or forgotten in the distribution when the process is deleted. It might be uncomfortable at the beginning if you are not used to work with scripts with over 100 lines, but you'll get used to it, the Matlab code editor offers many solutions to make long scripts easy to edit (cells, code folding...). It makes your process easier to maintain and to exchange with other users, which is important in the long run.

Optional function: Compute()

A process can be designed to be called at the same time:

We can leave what is specific to the Brainstorm structure in the Run() function, and move the actual computation to additional sub-functions. In this case, we recommend that you respect the following convention: name the main external sub-function Compute(). The following example will help clarifying this concept.

Example: Notch filter

Let's take the example of the process "Pre-process > Notch filter", which is defined in the plug-in function brainstorm3/toolbox/process/functions/process_notch.m.

The function GetDescription() defines the process properties (category, type of inputs, options...):

function sProcess = GetDescription()
    % Description the process
    sProcess.Comment     = 'Notch filter';
    sProcess.Category    = 'Filter';

The function FormatComment() returns a string that represents the process in the interface:

function Comment = FormatComment(sProcess) %#ok<DEFNU>
    if isempty(sProcess.options.freqlist.Value{1})
        Comment = 'Notch filter: No frequency selected';
        strValue = sprintf('%1.0fHz ', sProcess.options.freqlist.Value{1});
        Comment = ['Notch filter: ' strValue(1:end-1)];

The function Run() reads and tests the options defined by the user and then calls Compute():

function sInput = Run(sProcess, sInput)
    % Get options
    FreqList = sProcess.options.freqlist.Value{1};
    % Filter data
    sInput.A = Compute(sInput.A, sfreq, FreqList);

The function Compute() applies a notch filter to the recordings in input:

% USAGE: x = process_notch('Compute', x, sfreq, FreqList)
function x = Compute(x, sfreq, FreqList)
    % Remove the mean of the data before filtering
    xmean = mean(x,2);
    x = bst_bsxfun(@minus, x, xmean);
    % Remove all the frequencies sequencially

This mechanism allows us to access this notch filter at different levels. We can call it as a Brainstorm process that takes Brainstorm structures in input (this is usually not done manually):

sInput = process_zscore('Run', sProcess, sInput);

As part of a script generated from the pipeline editor:

% Process: Notch filter: 60Hz 120Hz 180Hz
sFiles = bst_process('CallProcess', 'process_notch', sFiles, [], ...
    'freqlist',    [60, 120, 180], ...
    'sensortypes', 'MEG, EEG', ...
    'overwrite',   0);

Or as regular functions that takes standard Matlab matrices in input:

% Generate some random signal
sfreq = 1000; F = rand(1,10*sfreq);
% Filter the signal
F = process_notch('Compute', F, 1000, [60 120 180]);

Process description

The function GetDescription() creates a structure sProcess that documents the process: its name, the way it is supposed to be used in the interface and all the options it needs. It contains the following fields:

Not all the fields have to be defined in the function GetDescription(). The missing ones will be set to their default values, as defined in db_template('ProcessDesc').

Definition of the options

Options structure

The field sProcess.options describes the list of options that are displayed in the pipeline editor window when the process is selected. It is a structure with one field per option. If we have an option named "overwrite", it is described in the structure sProcess.options.overwrite. Every option is a structure with the following fields:


Example of two options defined in process_zscore.m:

    % === Baseline time window
    sProcess.options.baseline.Comment = 'Baseline:';
    sProcess.options.baseline.Type    = 'baseline';
    sProcess.options.baseline.Value   = [];
    % === Sensor types
    sProcess.options.sensortypes.Comment = 'Sensor types or names (empty=all): ';
    sProcess.options.sensortypes.Type    = 'text';
    sProcess.options.sensortypes.Value   = 'MEG, EEG';
    sProcess.options.sensortypes.InputTypes = {'data'};

User preferences

Note that the default values defined in sProcess.options are usually displayed only once. When the user modifies the option, the new value is saved in the user preferences and offered as the default the next time the process is selected in the pipeline editor.

If you modify the Value field in your process function, the default offered when you select the process in the pipeline editor may not change accordingly. This means that another default has been saved in the user preferences. To reset all the options to their real default values (as defined in the process functions), you can use the menu Reset options in the Pipeline menu of pipeline editor window.

Option types

Categories of process

There are three different types of processes: Filter, File, Custom. The category of the process is defined by the field sProcess.Category. For the processes with two sets of input files (Process2), the logic is the same but the category are called: Filter2, File2, Custom.

Category: 'Filter' and 'Filter2'

Brainstorm considers independently each file in the input list (the files that have been dropped in the Process1 or Process2 files lists) and is responsible for the following operations:

In the process, the function Run():

Advantages: All the complicated things are taken care of automatically, the functions can be very short.

Limitations: There is no control over the file names and locations, one file in input = one file in output, and the file type cannot be changed (InputTypes=OutputTypes). Additionally, it is not possible to modify any field in the file other than the data matrix .F and the vector .Time.

For example, let's consider one of the simplest processes: process_absolute.m. It just calculates the absolute value of the input data matrix. The Run() function is only one line long:

function sInput = Run(sProcess, sInput)
    sInput.A = abs(sInput.A);

The sInput structure gives lots of information about the input file coming from the database, and one additional field "A" that contains the block of data to process. This process just applies the function abs() to the data sInput.A and returns modified values. A new file is created by Brainstorm in the database to store this result.

Category: 'File' and 'File2'

Brainstorm considers independently each file in the input list. It creates a structure sInput that documents the input file but does not load the data in the "A" field, as in the Filter case.

In the process, the function Run() is called once for each input file and is responsible for:

The resulting functions are much longer, but this time the process is free do anything, there are no restrictions. The outline of the typical Run() function can be described as following:

function OutputFile = Run(sProcess, sInput)
    % Load input file
    DataMat = in_bst_data(sInput.FileName);
    % Apply some function to the data in DataMat
    OutputMat = some_function(DataMat);
    % Generate a new file name in the same folder
    % Save the new file
    save(OutputFile, '-struct', 'OutputMat');
    % Reference OutputFile in the database:
    db_add_data(sInput.iStudy, OutputFile, OutputMat);

Category: 'Custom'

Similar to the previous case "File", but this time all the input files are passed at once to the process.

The function Run() is called only once. It receives all the input file names in an array of structures "sInputs". It can create zero, one or many files. The list of output files is returned in a cell array of strings "OutputFiles".

function OutputFiles = Run(sProcess, sInputs)
    % Load input files
    % Do something interesting
    % Save new files
    % Reference the new files in the database
    % Return all the new file names in the cell-array OutputFiles

Input description

The structure sInput contains the following fields:

Running a process

Three ways to run a process:

Create your own process

The easiest way for you to write your own plug-in function is to start working from an existing example. There are over a hundred processes currently available in the main Brainstorm distribution. Take some time to find one that is close to what you are planning to do. By order of importance: same category (Filter/File/Custom), same types of input and output files, similar options, same logic. Then follow these guidelines:


Here are some additional sample processes that can help you for specific tasks:

Feedback: Comments, bug reports, suggestions, questions
Email address (if you expect an answer):

Tutorials/TutUserProcess (last edited 2020-11-21 10:08:29 by FrancoisTadel)