What makes an experiment?

An experiment is known as a paradigm. A paradigm is a collection of plug-in modules (some required, some optional) that interact with each other to run the experiment. The core paradigms that ship with psiexperiment can be found in psi.paradigms.descriptions. Psiexperiment ships with a collection of paradigms for operant behavior (go-nogo), auditory testing (ABR, DPOAE, MEMR, EFR), noise exposure, and basic calibration of acoustic systems.

What is a paradigm?

A paradigm is described using the ParadigmDescription class. Here’s an example description:

from psi.experiment.api import ParadigmDescription
PATH = 'psi.paradigms.behavior.'
CORE_PATH = 'psi.paradigms.core.'

ParadigmDescription(
    'auto_gonogo', 'Auto GO-NOGO', 'animal', [
        {'manifest': PATH + 'behavior_auto_gonogo.BehaviorManifest'},
        {'manifest': PATH + 'behavior_mixins.BaseGoNogoMixin'},
        {'manifest': PATH + 'behavior_mixins.WaterBolusDispenser'},
        {'manifest': CORE_PATH + 'video_mixins.PSIVideo'},
        {'manifest': CORE_PATH + 'signal_mixins.SignalFFTViewManifest',
         'attrs': {'fft_time_span': 1, 'fft_freq_lb': 5, 'fft_freq_ub': 24000, 'y_label': 'Level (dB)', 'source': 'microphone'}
         },
    ],
)

The first argument, 'auto_gonogo' is a unique identifier for the paradigm. To launch this experiment, you would type psi auto_gonogo at the command line. The fourth argument is a list of manifests that can be loaded as part of the experiment. Some of them, such as the BehaviorManifest, are required. Others can be optionally loaded. The manifests are referenced by the module name plus class name. To override defaults on the class, a dictionary can be passed via attrs.

What is a manifest?

Briefly, a manifest defines how the associated plugin interacts with other plugins in the psiexperiment ecosystem. The manifest should subclass either PSIManifest or ExperimentManifest. For most custom-written plugins, you will use ExperimentManifest. Here’s the definition for the SignalFFTViewManifest:

from enaml.workbench.api import Extension

from psi.core.enaml.api import ExperimentManifest
from psi.data.plots import (ChannelPlot, FFTChannelPlot, FFTContainer,
                            TimeContainer, ViewBox)

enamldef SignalFFTViewManifest(ExperimentManifest): manifest:

    id = 'signal_fft_view'
    name = 'signal_fft_view'
    title = 'Signal view (PSD)'

    # By re-defining attributes of the "children" in the graph hierarchy as
    # attributes of the top-level manifest, we can easily modify the values
    # of these attributes in the PluginDescription by passing in an attrs
    # dictionary.
    alias fft_time_span: fft_plot.time_span
    alias fft_freq_lb: fft_container.freq_lb
    alias fft_freq_ub: fft_container.freq_ub
    alias source_name: fft_plot.source_name
    alias y_label: fft_vb.y_label
    alias apply_calibration: fft_plot.apply_calibration
    alias waveform_averages: fft_plot.waveform_averages

    Extension:
        id = manifest.id  + '.plots'
        point = 'psi.data.plots'

        FFTContainer: fft_container:
            name << manifest.name + '_container'
            label << manifest.title
            freq_lb = 5
            freq_ub = 50000

            ViewBox: fft_vb:
                name << manifest.name + '_vb'
                y_min = -10
                y_max = 100
                y_mode = 'mouse'
                save_limits = True

                FFTChannelPlot: fft_plot:
                    name << manifest.name + '_plot'
                    source_name = 'microphone'
                    pen_color = 'k'
                    time_span = 0.25

This manifest extends the psi.data.plots extension point by adding a new FFT plot that will plot the running FFT of the signal source defined by source_name. Another good example is the reward dispenser. This example demonstrates how you can define some basic functionality and then subclass the manifest to customize it:

enamldef BaseRewardDispenser(ExperimentManifest): manifest:

    attr duration
    attr output_name

    Extension:
        id = manifest.id + '.actions'
        point = 'psi.controller.actions'
        ExperimentAction:
            event = 'deliver_reward'
            command = f'{manifest.output_name}.fire'
            kwargs = {'duration': manifest.duration}

    Extension:
        id = manifest.id + '.status_item'
        point = 'psi.experiment.status'

        StatusItem:
            label = 'Total dispensed'
            Label:
                text << str(workbench \
                            .get_plugin('psi.controller') \
                            .get_output(manifest.output_name) \
                            .total_fired)

    Extension:
        id = manifest.id + '.toolbar'
        point = 'psi.experiment.toolbar'
        rank = 2000
        Action:
            text = 'Dispense reward'
            triggered ::
                controller = workbench.get_plugin('psi.controller')
                controller.invoke_actions('deliver_reward')
            enabled <<  workbench.get_plugin('psi.controller').experiment_state \
                not in ('initialized', 'stopped')


enamldef WaterBolusDispenser(BaseRewardDispenser): manifest:

    id = 'water_bolus_dispenser'
    name = 'Water bolus dispenser'
    required = True

    duration = C.lookup('water_dispense_duration')
    output_name = 'water_dispense'

    Extension:
        id = manifest.id + '.parameters'
        point = 'psi.context.items'

        Parameter:
            name = 'water_dispense_duration'
            label = 'Water dispense trigger duration (s)'
            compact_label = 'D'
            default = 1
            scope = 'arbitrary'
            group_name = 'trial'

The first extension is to the psi.controller.actions point where we define a command that is called each time the deliver_reward event occurs. The deliver_reward event is generated by the core behavior controller whenever it determines that the subject has met the criteria for recieving a reward. By defining this as an event, we can link any number of actions (defined by ExperimentAction). Here, the action is to find the digital output defined by output_name and call the fire command with the specified trigger duration. In the LBHB system, this digital output is linked to a solenoid that opens when the trigger goes high, thereby allowing water to flow through into a lick spout that the subject has access to. This is a good example of where you can easily customize what happens during the deliver_reward stage to switch to an alternate reward system by loading a different plugin.

The second extension is to the psi.experiment.status point which is a user-facing panel that shows information about the experiment. This must subclass StatusItem and contribute a specific widget (in this case, a Label, but other good widgets include a ProgressBar). The extension to psi.experiment.toolbar adds a button that invokes all actions connected to the deliver_reward event. Finally, the psi.context.items extension manages all parameters (subclasses of ContextItems). Parameters are variables (e.g., intertrial duration, reward trigger duration, number fo trials, etc.) that the user may want to control.