Skip to content

Plugins

ccoughlin edited this page · 2 revisions
Clone this wiki locally

Plugins

If NDIToolbox doesn't do something you need out of the box, why not write it yourself? We've set it up to make it easy to add your own data analysis code by writing plugins in Python. Here we'll discuss what a plugin is, how to write one, and how to distribute your plugins to others.

When a user plots a data file, NDIToolbox scans the user's plugins folder looking for valid plugins and automatically adds them to the Plugins submenu (under Tools) in the plot window. When a user selects a plugin to run, NDIToolbox starts your code and configures it if necessary, then passes your program a copy of the data to analyze. Your plugin does its work and makes a copy of the data to pass back to NDIToolbox, which updates the plot with the results.

Installation

NDIToolbox plugins must be installed in the user's plugins folder. Plugins can simply be copied over, or if you're working with a plugin archive (see below), NDIToolbox can do it for you.

Structure Of A Plugin

All NDIToolbox plugins must be a subclass of AbstractPlugin, and must define a few key fields. For convenience you can subclass CompanyPlugin (itself a subclass of AbstractPlugin) instead to get generic entries for these fields if you don't want to set them yourself. If nothing else you should at least set the name field to something unique as this is the label your plugin is given in the Plugin menu.

  • data property (get and set): NumPy array. NDIToolbox sets the data property on your plugin on creation, and reads it back to plot the results.
  • description
  • authors
  • copyright
  • name
  • version
  • url
  • run() method

Here's an example of a plugin that ships with NDIToolbox to normalize data.

"""normalize_plugin.py - simple A7117 plugin that normalizes the current
data, used to demonstrate the plugin architecture

Chris R. Coughlin (TRI/Austin, Inc.)
"""

__author__ = 'Chris R. Coughlin'

from models.abstractplugin import TRIPlugin
import numpy as np

class NormalizePlugin(TRIPlugin):
    """Normalizes the current dataset, demonstrates
    how to write plugins for the A7117 project"""

    # At a minimum plugin developers should specify a plugin name and a
    # short description as these are displayed to the user.  The fields
    # required for a plugin are detailed below.
    #
    # Sub-classing a company plugin such as TRIPlugin or
    # ComputationalToolsPlugin will pre-populate these fields with
    # default values.
    name = "Normalize Data" # Name in the Plugin menu
    description = "Normalizes current data set"
    authors = "Chris R. Coughlin (TRI/Austin, Inc.)"
    version = "1.0"
    url = "www.tri-austin.com"
    copyright = "Copyright (C) 2012 TRI/Austin, Inc.  All rights reserved."

    def __init__(self):
        super(NormalizePlugin, self).__init__(self.name, self.description,
            self.authors, self.url, self.copyright)

    def run(self):
        """Executes the plugin - if data are not None they are normalized
        against the largest single element in the array."""
        if self._data is not None:
            max_el = np.max(self._data)
            self._data = self._data / max_el

In addition, if you define a dict named config in your plugin, NDIToolbox will show the user a configuration dialog with each option and allow the user to make changes to the defaults you set in your code. For example, here's the code for the Median Filter plugin that ships with NDIToolbox. By default, the Median Filter will apply a median filter of rank 3 to the current data set, but the user is free to specify any other size.

"""medfilter_plugin.py - applies a median filter to the current data set,
used to demonstrate incorporating configuration options in an A7117 plugin

Chris R. Coughlin (TRI/Austin, Inc.)
"""

__author__ = 'Chris R. Coughlin'

from models.abstractplugin import TRIPlugin
import scipy.signal

class MedianFilterPlugin(TRIPlugin):
    """Applies a median filter to the
    current data set"""

    name = "Median Filter"
    description = "Applies a median filter to the current data set."

    def __init__(self):
        super(MedianFilterPlugin, self).__init__(self.name, self.description,
            self.authors, self.url, self.copyright)
        # If a config dict is defined in a Plugin, the UI will present the user
        # with a dialog box allowing run-time configuration (populated with the
        # default values set here).  Although vals can be of any pickle-able type,
        # they are returned as str.
        self.config = {'kernel size': '3'}

    def run(self):
        """Runs the plugin, asking the user to specify a kernel size for the median filter.
        A filter of rank A where A is the specified kernel size is then applied to the
        current data set in each dimension.  An even kernel size is automatically
        incremented by one to use an odd number-SciPy's medfilt function requires odd
        numbers for kernel size.
        """
        if self._data is not None:
            # The UI returns configuration options as str - the Plugin is
            # responsible for casting them to required type
            kernel_size = int(self.config.get('kernel size', 3))
            if kernel_size % 2 == 0:
                # medfilt function requires odd number for kernel size
                kernel_size += 1
            self._data = scipy.signal.medfilt(self._data,
                kernel_size)

There are few restrictions on what your plugin does or how you organize your code. The only hard restriction is that the run() method can't spawn subprocesses (threads are ok however) because NDIToolbox runs the plugin in a separate process. You might also find it tough to build a full GUI for a plugin because of this restriction. Generally you should think of a plugin as a fairly small application; if you need more functionality a better home might be under the main UI's Tools menu (as we've done with the POD Toolkit for example).

Distributing Plugins

If you'd like to share your plugin with others you can simply send them the files, or if you have access to a server you can host a plugin archive and have NDIToolbox download and install it for them automatically. To host a plugin you'll need to make a ZIP archive of all its files, and the ZIP has to follow a few rules.

  1. The ZIP has to have the same basename as your plugin's main Python source file, e.g. if your plugin is named super_filter_plugin.py, the ZIP must be named super_filter_plugin.zip.
  2. The plugin's main Python source file must be in the root folder of the ZIP.
  3. The ZIP must also contain an ASCII readme (or README, readme.txt, README.TXT) file in the root folder of the ZIP. If the user asks for more information about the plugin before installing, this is the file that's displayed to them. You should summarize what it does, who wrote it, changelog, etc.
  4. All other files you need to ship with your plugin must be in a subfolder, and the subfolder must have the same basename as the ZIP and the plugin's main source file. So for example if your plugin's Python file is MyFantasticPlugin.py all ancillary files must be in the MyFantasticPlugin folder in the MyFantasticPlugin.zip archive.
  5. If you want to password-protect your plugin's ZIP file, you must use a global password rather than set passwords for individual files inside the ZIP.
  6. If you want to require a username and password to access the plugin archive on your server, NDIToolbox only supports basic access authentication via HTTP/HTTPS. Note that this feature has not been extensively tested and should be considered experimental.

There's an example of the proper plugin archive structure under models/tests/support_files/good_medfilter_plugin.zip . Remember that if your plugin uses third-party dependencies like RPy http://rpy.sourceforge.net/ you'll need to make sure your end user has these dependencies installed. Generally for an NDIToolbox plugin you can count on NumPy, SciPy, and matplotlib to be installed; other packages may vary from installation to installation.

Something went wrong with that request. Please try again.