Experiment Setup

The overall structure of an AxoPy application is handled by the Experiment. You can think of the Experiment as a manager of a number of tasks that, when run in succession, form an actual experimental protocol. Let’s get started with AxoPy by immediately writing a bit of code to produce a running experiment. We can then re-run the application after making a number of changes to get a feel for how to set up an Experiment.

Hello, Experiment

AxoPy is written for experiments that involve collecting data from a hardware input device and producing visual [1] feedback to the subject. For most of our examples, we’ll make use of the built-in Oscilloscope task and a built-in device that works without requiring special hardware, like the NoiseGenerator. So here’s how we use those two items to put together a simple but functioning experiment:

import axopy

daq = axopy.daq.NoiseGenerator()
exp = axopy.experiment.Experiment(daq=daq)
exp.run(axopy.task.Oscilloscope())

We create the Experiment object with a NoiseGenerator as the input device (or DAQ, short for data acquisition), then run the experiment with Oscilloscope as the sole task to run.

It’s worth noting here that AxoPy’s submodules (e.g. experiment, daq, etc.) are useful for organizing the package into logical parts, but it can be annoying to type the module names repeatedly. You can write the above example with more verbose imports like the following so the code itself is a little more succinct:

from axopy.daq import NoiseGenerator
from axopy.experiment import Experiment
from axopy.task import Oscilloscope

daq = NoiseGenerator()
exp = Experiment(daq=daq)
exp.run(Oscilloscope())

When you run this example, you’ll notice the first thing that happens is a dialog window pops up prompting you to enter a subject ID. The Experiment needs a subject ID so that it can set up data storage. Once the subject ID is entered and accepted, you’ll see a screen that says “Ready”. This screen is shown in between all tasks in the experiment—hit the Enter or Return key to accept the prompt and start the task. You should then see an oscilloscope widget displaying a randomly generated signal in real time. You can press Enter again to finish the task (this is specific to Oscilloscope which is a “free-running” task). When the task finishes, the Experiment looks for the next task to run. Since there aren’t any more, the application exits.

Note

AxoPy supports multiple DAQs being passed to an Experiment in a dictionary, list, or tuple. In that case, your tasks need to accept the same type of collection in the prepare_daq() method. See Implementing Tasks for information on writing the prepare_* methods.

Experiment Configuration

Human-computer interface study designs often include one or more of the following complications:

  • subjects are split into groups

  • subjects are tested over multiple sessions

  • subjects fall into categories that require different configuration (e.g. mirror the screen contents for left-hand dominant subjects)

For these cases, Experiment provides the option to run a configuration step between creation of the experiment object and running the tasks. The options are entered in the same dialog window where you entered the subject ID in the example above. This allows you to set options on your tasks before running them or even run an entirely different list of tasks. It also means the person running an experiment (which isn’t necessarily the person who wrote the experiment code) doesn’t need to know how to write some configuration file or anything — they just run the experiment application and can enter the details in a graphical widget.

The Experiment.configure() method accepts as many configuration options as you want. You specify each one by providing a keyword argument with the option’s type (e.g. str, int, float) as the value, and it returns a dictionary with the values entered.

For example, say we want to input the subject’s age. We can do that with an int option called age:

from axopy.experiment import Experiment

exp = Experiment()
config = exp.configure(age=int)

print(config['age'])

If you run the code above, a dialog box will pop up just like it did for the first example, but now a text box for the subject ID and the age is shown. Note that you do not have to specify subject as an option—this is done for you. It’s up to you to handle the configuration options and modify how the experiment runs based on them.

Aside from primitive types like int, str, or float, you can enumerate all possible values for a configuration option, and these will be available to select in a combo box (drop-down menu). This way, the researcher running the experiment can’t enter an invalid value:

exp.configure(hand=('right', 'left'))

Tips for Experiment Writing

The Experiment class accepts a couple other keyword arguments that can be useful when debugging and/or developing an experiment application. You can specify a subject argument so that the configuration dialog isn’t shown when the Experiment is run:

from axopy.experiment import Experiment
from axopy.task import Oscilloscope
from axopy.daq import NoiseGenerator

exp = Experiment(daq=NoiseGenerator(), subject='test')
exp.run(Oscilloscope())

By default, if you run any tasks that write data to storage, AxoPy will complain and exit if you attempt to overwrite any data that exists. This will happen if you repeatedly run the Experiment with the same subject ID, so it can be useful (in conjunction with the subject keyword argument) to set allow_overwrite=True as well, quelling the error regarding overwriting data:

exp = Experiment(subject='test', allow_overwrite=True)

This setup is pretty handy when developing an experiment, just remember to switch it off! One way to make this a little more robust is to add a flag to your application so you have to explicitly enable this “debugging mode”.

How It Works

Skippable unless you want to dig into working on AxoPy itself

The Experiment manages a PyQt5 application and is responsible for giving each task a graphical container within the Qt application, access to hardware inputs, and data storage. The task implementation is responsible for making use of these experiment-wide resources and then handing control back to the experiment so it can run the next task.

Next Up

Now that we have an experiment running and the ability to set up some configuration options if needed, let’s look at how to write tasks.