NeuroAnalyzer Plugins

NeuroAnalyzer can be extended with plugins: Julia source files that add new functions (analyses, plots, importers, …) without modifying the package itself.

How plugins work

Plugins are stored in the $HOME/NeuroAnalyzer/plugins folder (the folder is created when NeuroAnalyzer is loaded for the first time; its location is stored in NeuroAnalyzer.plugins_path). Each plugin has its own folder with a src subfolder:

$HOME/NeuroAnalyzer/
|- pipelines
|- plugins
   |- test_plugin
      |- src
         |- test_plugin.jl
   |- another_plugin
      |- src
         |- another_plugin.jl
         |- helpers.jl

Every .jl file in the src subfolder is included into the NeuroAnalyzer module:

  • when NeuroAnalyzer is loaded (using NeuroAnalyzer)
  • after a plugin is installed, updated or removed
  • when plugins_reload() is called

Plugin functions are part of the NeuroAnalyzer module and are called with the module prefix:

NeuroAnalyzer.test_plugin()

Tip: Plugins are ordinary Julia code executed with your user permissions. Install plugins only from sources you trust.

Managing plugins

Listing installed plugins:

plugins_list()

Reloading all installed plugins (e.g. after editing a plugin file):

plugins_reload()

Installing

Plugins are installed from Git repositories (the URL must start with https://, http:// or git@); the repository is cloned into the plugins folder:

plugins_install("https://codeberg.org/AdamWysokinski/plot_ITPC.jl")

Plugins may also be installed from local .zip or .tar.gz archives. The archive is extracted into the plugins folder, so it must contain the plugin folder (with its src subfolder) at the top level:

plugins_install("/home/user/Downloads/plot_ITPC.jl-main.zip")

Tip: Installing from a local archive requires the unzip or tar command.

Tip: Existing files are overwritten when installing from a local archive.

Updating

Plugins installed from Git repositories are updated with git pull; plugins installed from archives are skipped (install the new archive instead).

Updating all plugins:

plugins_update()

Updating a single plugin (the name of its folder):

plugins_update("plot_ITPC.jl")

Removing

Removing a plugin deletes its whole folder:

plugins_remove("plot_ITPC.jl")

Tip: Functions of a removed plugin stay available until Julia is restarted.

Writing a plugin

A minimal plugin is a folder with a single file:

plugins/
|- my_plugin
   |- src
      |- my_plugin.jl

Because plugin files are included into the NeuroAnalyzer module, a plugin can use all NeuroAnalyzer functions, including the internal ones (e.g. _info(), _warn()), and all packages NeuroAnalyzer depends on. Packages that are not NeuroAnalyzer dependencies cannot be loaded from a plugin.

Recommended conventions:

  • use a common prefix for all names (e.g. my_plugin_…), so they do not clash with NeuroAnalyzer functions or with other plugins (a clash silently overwrites the existing function)
  • follow the NeuroAnalyzer conventions: a private method _name(s) working on arrays and a public method name(obj; ch) working on NEURO objects; obj.data is an Array{Float64, 3} (channels × samples × epochs)
  • validate the arguments and throw ArgumentError with a clear message
  • add a docstring, so that ?NeuroAnalyzer.name shows help
  • add a self-test function, so that users can check whether the plugin works

The bundled test_plugin is a complete example: it calculates the zero-crossing rate of the selected channels and contains a self-test.

"""
    _test_plugin_zcr(s; fs)

Zero-crossing rate of a signal (crossings per second).
"""
function _test_plugin_zcr(s::AbstractVector; fs::Int64)::Float64
    fs >= 1 || throw(ArgumentError("fs must be ≥ 1."))
    length(s) >= 2 || throw(ArgumentError("Signal must contain at least 2 samples."))
    return _zeros(s) / (length(s) / fs)
end

"""
    test_plugin_zcr(obj; ch)

Zero-crossing rate of the selected channels, shape (channels, epochs).
"""
function test_plugin_zcr(obj::NeuroAnalyzer.NEURO; ch::Union{String, Vector{String}, Regex})::Matrix{Float64}
    ch = get_channel(obj; ch = ch)
    isempty(ch) && throw(ArgumentError("No channels selected."))
    zcr = zeros(length(ch), nepochs(obj))
    for (idx, c) in enumerate(ch), ep in 1:nepochs(obj)
        zcr[idx, ep] = _test_plugin_zcr(obj.data[c, :, ep]; fs = sr(obj))
    end
    return zcr
end

Using the plugin:

eeg = import_edf("files/eeg.edf")
zcr = NeuroAnalyzer.test_plugin_zcr(eeg; ch = "eeg")

# self-test: zero-crossing rate of a 10 Hz sine must be 20 per second
NeuroAnalyzer.test_plugin()

During development, edit the plugin file and call plugins_reload() to load the new version without restarting Julia.

Sharing a plugin

To make a plugin installable with plugins_install(), publish its folder as a Git repository (e.g. on Codeberg or GitHub) with the src subfolder at the top level of the repository:

my_plugin/          (repository)
|- README.md
|- LICENSE
|- src
   |- my_plugin.jl

The repository name becomes the plugin folder name; this is the name used by plugins_update() and plugins_remove().