Skip to content

Customized Analyses

One of Pyneal's most powerful features is its ability to run customized analyses during a real-time scan. This means you can design your own analyses to fit your unique experimental needs, and Pyneal will execute those analyses on every new timepoint and store the results.

To take advantage of this feature, you write a custom analysis script (in python), and select the Custom button in the Analysis pane of the GUI:

Custom Analysis Script

While custom analyses offer a lot of flexibility, there are a couple of important constraints that need to be included in the script to ensure it can be integrated into the typical Pyneal data flow.

The best way to ensure these constraints are met is to base your script off of the template file included with Pyneal. You can find this template in

pyneal/utils/customAnalyses/customAnalysisTemplate.py

Or, you can simply copy the text here:

import sys
import os
from os.path import join
import logging

import numpy as np
import nibabel as nib


class CustomAnalysis:
    """ Custom Analysis Module

    This class contains all of the methods needed for setting up and executing
    customized analyses in Pyneal during a real-time scan

    """
    def __init__(self, maskFile, weightMask, numTimepts):
        """ Initialize the class

        Everything in the `__init__` method will be executed BEFORE the scan
        begins. This is a place to run any necessary setup code.

        The `__init__` method provides a number of inputs from the setup GUI
        that can be used to help set up a customized analyses. You are free to
        use or ignore these inputs as needed.

        Parameters
        ----------
        maskFile : string
            full path to the mask file specified in the Pyneal setup GUI
        weightMask : boolean
            flag indicating whether the "weight mask?" option in setup GUI was
            checked.
        numTimepts : int
            number of timepts in the run, as specified in the setup GUI

        """
        # Load masks and weights, and create an within-class reference to
        # each for use in later methods.
        mask_img = nib.load(maskFile)
        if weightMask is True:
            self.weights = mask_img.get_data().copy()
        self.mask = mask_img.get_data() > 0  # 3D boolean array of mask voxels

        # within-class reference to numTimepts for use in later methods
        self.numTimepts = numTimepts

        # Add the directory that this script lives in to the path. This way it
        # is easy to load any additional files you want to put in the same
        # directory as your custom analysis script
        self.customAnalysisDir = os.path.abspath(os.path.dirname(__file__))
        sys.path.append(self.customAnalysisDir)

        # Import the logger. If desired, you can write log messages to the
        # Pyneal log file using:
        # self.logger.info('my log message') - log file and stdOut
        # self.logger.debug('my log message') - log file only
        self.logger = logging.getLogger('PynealLog')

        ########################################################################
        ############# vvv INSERT USER-SPECIFIED CODE BELOW vvv #################
        self.myResult = 1


        ############# ^^^ END USER-SPECIFIED CODE ^^^ ##########################
        ########################################################################

    def compute(self, vol, volIdx):
        """ Compute method

        This method will be executed on EACH new 3D volume that arrives
        DURING the real-time scan. Results must be returned in a dictionary. No
        restrictions on dict key names or values, but note that the volume
        index will get added automatically by Pyneal before the result gets
        placed on the results server, so no need to specify that here

        Parameters
        ----------
        vol : numpy-array
            3D array of voxel data for the current volume
        volIdx : int
            0-based index indicating where, in time (4th dimension), the volume
            belongs

        Returns
        -------
        dict
            dictionary containing key:value pair(s) for the results for the
            current volume

        """
        ########################################################################
        ############# vvv INSERT USER-SPECIFIED CODE BELOW vvv #################
        self.myResult += 1


        ############# ^^^ END USER-SPECIFIED CODE ^^^ ##########################
        ########################################################################

        return {'result': self.myResult}

The script class consists of a single class (CustomAnalysis) with two methods: __init__ and compute. You are welcome to include any additional methods that are useful, just make sure these two methods exist. See below for details on what each method does and how to customize them.

__init__ (initialize)

The __init__ method is called as soon as you hit submit on the setup GUI. In other words, the code in this method runs before the scan begins. Thus, the __init__ method is where you will specify any set up code that needs to be initialized before the scan begins (e.g. loading multiple mask files).

Let's take a look at the code in more detail:

def __init__(self, maskFile, weightMask, numTimepts):
    """ Initialize the class

    Everything in the `__init__` method will be executed BEFORE the scan
    begins. This is a place to run any necessary setup code.

    The `__init__` method provides a number of inputs from the setup GUI
    that can be used to help set up a customized analyses. You are free to
    use or ignore these inputs as needed.

    Parameters
    ----------
    maskFile : string
        full path to the mask file specified in the Pyneal setup GUI
    weightMask : boolean
        flag indicating whether the "weight mask?" option in setup GUI was
        checked.
    numTimepts : int
        number of timepts in the run, as specified in the setup GUI

    """
    # Load masks and weights, and create an within-class reference to
    # each for use in later methods.
    mask_img = nib.load(maskFile)
    if weightMask is True:
        self.weights = mask_img.get_data().copy()
    self.mask = mask_img.get_data() > 0  # 3D boolean array of mask voxels

    # within-class reference to numTimepts for use in later methods
    self.numTimepts = numTimepts

    # Add the directory that this script lives in to the path. This way it
    # is easy to load any additional files you want to put in the same
    # directory as your custom analysis script
    self.customAnalysisDir = os.path.abspath(os.path.dirname(__file__))
    sys.path.append(self.customAnalysisDir)

    # Import the logger. If desired, you can write log messages to the
    # Pyneal log file using:
    # self.logger.info('my log message') - log file and stdOut
    # self.logger.debug('my log message') - log file only
    self.logger = logging.getLogger('PynealLog')

    ########################################################################
    ############# vvv INSERT USER-SPECIFIED CODE BELOW vvv #################
    self.myResult = 1


    ############# ^^^ END USER-SPECIFIED CODE ^^^ ##########################
    ########################################################################

Preset variables, and creating new ones

First, note that certain settings from the setup GUI are passed into the custom analysis script. Namely:

  • maskFile: the path to the mask specified in the GUI
  • weightMask: a True/False boolean flag indicating whether the weight mask? option was checked or not.
  • numTimepts: the number of timepts in the run, as specified in the GUI.

At the top of the __init__ method, you'll see some code that reads in each of those settings, and creates local variables for each:

# Load masks and weights, and create an within-class reference to
# each for use in later methods.
mask_img = nib.load(maskFile)
if weightMask == True:
    self.weights = mask_img.get_data().copy()
self.mask = mask_img.get_data() > 0  # 3D boolean array of mask voxels

# within-class reference to numTimepts for use in later methods
self.numTimepts = numTimepts

This enables you to reference these variables in any other method by referring to self.mask, self.weights, and self.numTimepts. In fact, the same thing is true for any variable you create in the __init__ method; in order to access the variable in other methods, you must prepend the variable name with self.

The __init__ method also contains a couple of other useful tidbits near the top. First, we add the directory that contains your custom analysis script to the path. This way, you can easily load any additional files that are exist in that same directory.

# Add the directory that this script lives in to the path. This way it
# is easy to load any additional files you want to put in the same
# directory as your custom analysis script
self.customAnalysisDir = os.path.abspath(os.path.dirname(__file__))
sys.path.append(self.customAnalysisDir)

Adding log messages

It also creates a reference to the logger, which is the tool that adds new messages to the pynealLog.log output file. You can use this reference anywhere in your script to add new log messages, which will be automatically timestamped and included in the output file. You can even control the type of log message

  • info messages will get written to the output file AND appear in the stdOut of the terminal
  • debug messages will only get written to the output file
# Import the logger. If desired, you can write log messages to the
# Pyneal log file using:
# self.logger.info('my log message') - log file and stdOut
# self.logger.debug('my log message') - log file only
self.logger = logging.getLogger('PynealLog')

User-specified __init__ code

Finally, at the bottom of the __init__ method is space for you to include any additional code that is needed.

########################################################################
############# vvv INSERT USER-SPECIFIED CODE BELOW vvv #################
self.myResult = 1



############# ^^^ END USER-SPECIFIED CODE ^^^ ##########################
########################################################################

In order to illustrate, in the example above we're simply creating a new variable (self.myResult) and assigning it a value of 1.

compute

The compute method is what actually gets called during a scan. In fact, it gets called each time a new 3D volume appears from the scanner. Here's the code in more detail:

def compute(self, vol, volIdx):
    """ Compute method

    This method will be executed on EACH new 3D volume that arrives
    DURING the real-time scan. Results must be returned in a dictionary. No
    restrictions on dict key names or values, but note that the volume
    index will get added automatically by Pyneal before the result gets
    placed on the results server, so no need to specify that here

    Parameters
    ----------
    vol : numpy-array
        3D array of voxel data for the current volume
    volIdx : int
        0-based index indicating where, in time (4th dimension), the volume
        belongs

    Returns
    -------
    dict
        dictionary containing key:value pair(s) for the results for the
        current volume

    """
    ########################################################################
    ############# vvv INSERT USER-SPECIFIED CODE BELOW vvv #################
    self.myResult += 1


    ############# ^^^ END USER-SPECIFIED CODE ^^^ ##########################
    ########################################################################

    return {'result': self.myResult}

Note that the compute method passes in references to the current volume (3D numpy array, [x,y,z]), as well as the volIdx, an integer representing the current volume index (in 4th dimension; 0-based index).

User-specified compute code

The compute method includes space for you to insert your own analysis code:

########################################################################
############# vvv INSERT USER-SPECIFIED CODE BELOW vvv #################
self.myResult += 1




############# ^^^ END USER-SPECIFIED CODE ^^^ ##########################
########################################################################

You can use this area to write customized analysis code that will be executed on each volume. In the simple example above, each time a new volume arrives, the compute method will increment the self.myResult variable (created in the __init__ method) by 1.

Storing results

The compute method must return a python dictionary containing the analysis result (or results) for the current volume.

return {'result': self.myResult}

That said, you are free to customize the contents of this dictionary as needed to fit your experimental needs. For instance, if you are calculating the mean signal in multiple ROIs (and storing those value in variables named roi1_meanSignal, roi2_meanSignal, and roi3_meanSignal) your dictionary could look something like this:

{'roi1_mean': roi1_meanSignal,
'roi2_mean`: roi2_meanSignal,
'roi3_mean`: roi3_meanSignal}

Pyneal automatically tacks on the current volIdx as an entry to the dictionary later on, so no need to include that information here.