Hello World for Dareplane with python modules
This example will guide you through the process of creating a simple motor imagery task as a Dareplane module and then hook it up with a mock-up data streamer as well as LSL recording. Completing this example you will have a data source (mockup only), a paradigm providing visual queues and markers and finally recording of markers and streaming data with LSL into an *.xdf
file.
Get the Dareplane pyutils
Install the dareplane-utils
to make use of the default TCP server. E.g. via pip install dareplane-utils
.
Building the paradigm module
First, lets decide to call the module dp-mi-paradigm
. The prefix of dp-
for Dareplane is arbitrary and you can of course choose not to use it.
Initialize the project
To start, get the dp-strawman-module
and read the README.md
therein carefully. After that you should know how to build upon the strawman. So lets rename the relevant folders. The content of our new module folder ./dp-mi-paradigm
should then look like this:
├── LICENSE
├── README.md
├── api
│ └── server.py
├── configs
├── mi_paradigm
│ ├── main.py
│ └── utils
│ └── logging.py
└── tests
Develop the paradigm
For our paradigm we decide to show simple instructions for motor imagination of left (L) and right ( R) hand movement by displaying letters ‘L’ and ‘R’ as well as a fixation cross ‘+’ using psychopy
. In addition, we want to send markers to an LSL stream capturing when a direction is shown.
So our ./mi_paradigm/main.py
could look like this.
from fire import Fire
import time
import random
import pylsl
from psychopy.visual import TextStim, Window
from mi_paradigm.utils.logging import logger
10)
logger.setLevel(
= (0, 0, 0)
BG_COLOR = (1, 0, 0)
TEXT_COLOR
# timing parameters
= 1
t_pre = 1
t_show = 1
t_post
# LSL outlet - for convenience we also display to the logger
class Outlet:
def __init__(self):
self.logger = logger
= pylsl.StreamInfo(name="markers", channel_format="string")
info self.outlet = pylsl.StreamOutlet(info)
def push_sample(self, sample: str):
self.logger.debug(f"Pushing sample {sample}")
self.outlet.push_sample([sample])
# Sometimes is is more convenient to have a paradigm instance which can be
# kept alive globally. This especially holds if the server we will wrap around
# this module will not call psychopy in a subprocess
# --> So for the example, add a class
class Paradigm:
def __init__(self):
self.open_window()
def open_window(self):
self.win = Window((800, 600), screen=1, color=BG_COLOR)
self.rstim = TextStim(win=self.win, text="R", color=TEXT_COLOR)
self.lstim = TextStim(win=self.win, text="L", color=TEXT_COLOR)
self.fix_cross = TextStim(win=self.win, text="+", color=TEXT_COLOR)
def close_window(self):
self.win.close()
def run_mi_task(paradigm: Paradigm, nrepetitions: int = 4) -> int:
= Outlet()
outlet = paradigm.win
win = paradigm.fix_cross
fix_cross = paradigm.rstim
rstim = paradigm.lstim
lstim
fix_cross.draw()
win.flip()
# create a balanced set
= ["R"] * (nrepetitions // 2) + ["L"] * (nrepetitions // 2)
directions
random.shuffle(directions)
for i, dir in enumerate(directions):
fix_cross.draw()
win.flip()"new_trial")
outlet.push_sample(
time.sleep(t_pre)
if dir == "R":
rstim.draw()else:
lstim.draw()
win.flip()dir)
outlet.push_sample(
# clear screen and sleep for post
time.sleep(t_show)
win.flip()"cleared")
outlet.push_sample(
win.flip()
time.sleep(t_post)
return 0
if __name__ == "__main__":
from functools import partial
= Paradigm()
pdm Fire(partial(run_mi_task, pdm))
This should be all we need for being able to test the paradigm via python -m mi_paradigm.main
. Test it like this and make sure it works (especially installing requirements!).
Wrapping the server around
Next we need to add the server which will allow communication within a Dareplane setup. This requires just a few lines and since we started from the strawman, it actually just requires us to properly import the run_mi_task
and Paradigm
, intializing a Paradigm
instance and adding it to the primary commands dictionary in ./api/server.py
, partially defining the correct Paradigm
instance.
from fire import Fire
from functools import partial
from mi_paradigm.main import run_mi_task, Paradigm
from mi_paradigm.utils.logging import logger
from dareplane_utils.default_server.server import DefaultServer
def main(port: int = 8080, ip: str = "127.0.0.1", loglevel: int = 30):
= Paradigm()
pdm
logger.setLevel(loglevel)"Paradigm created")
logger.debug(
# partial is used so taht the function call will use the pdm instance
= {"RUN": partial(run_mi_task, pdm)}
pcommand_map
# ... rest is left unchanged
Now you are ready for the next level of testing, which is to make sure, we can run the task via the server. Just spawn up the server with python -m api.server
and connect via e.g. telnet 127.0.0.1 8080
. Then send the RUN
command in telnet and verify that the paradigm is played correctly.
Once this is successful, you have completed your first Dareplane module. Congratulations !
Running your module from the control room
It is now time to integrate the dp-mi-paradigm
with other modules. This is done using the dp-control-room
. If you do not yet have it, clone it from git and place it e.g. in the parent directory of pd-mi-paradigm
. So that you have the pd-mi-paradigm
and pd-control-room
paradigm in the same folder. Make sure to have all dependencies of the control room installed. Try pip install -r requirements.txt
from within the pd-control-room
folder.
Then move into the dp-control-room
directory and create a config at ./configs/mi_experiment.toml
with the following content:
[python]
modules_root = '../'
# -------------------- used modules ---------------------------------------
[python.modules.dp-mi-paradigm]
type = 'paradigm'
port = 8081
ip = '127.0.0.1'
loglevel = 10
Then change the config which is loaded by the control room for convenience. So within ./control_room/main.py
we place:
= Path("./configs/mi_experiment.toml").resolve() setup_cfg_path: Path
Now spawn up the control_room by calling python rcr.py
or python -m control_room.main
. You should now be able to see the control_room at 127.0.0.1:8050
within your browser. Make sure you see the mi_experiment
section and a RUN
button. If you click the button, you should see the paradigm being played.
Composing the module with others
As a final step, we add other modules and create a macro to control all with a single button push. So get the dp-mockup-streamer
and the dp-lsl-recording
module and place them in the same parent directory as the other modules.
.
├── dp-control-room
├── dp-lsl-recording
├── dp-mockup-streamer
└── dp-mi-paradigm
Also install the requirements for the other two modules via pip install -r requirements.txt
within each of the folders.
Then add the following to the ./configs/mi_experiment.toml
config:
[python]
modules_root = '../'
# -------------------- used modules ---------------------------------------
[python.modules.dp-mi-paradigm]
type = 'paradigm'
port = 8081
ip = '127.0.0.1'
loglevel = 10
[python.modules.dp-mockup-streamer]
type = 'source'
port = 8082
ip = '127.0.0.1'
loglevel = 10
[python.modules.dp-lsl-recording]
type = 'paradigm'
port = 8083
ip = '127.0.0.1'
loglevel = 10
[macros.start_test]
name = 'START_TEST'
description = 'start all modules for simulation'
delay_s = 1
[macros.start_test.default_json]
nrep = 6
[macros.start_test.cmds]
# variable names are arbitrary, the commands will be executes in the same order as they are read by tomllib
com1 = ['dp-mockup-streamer', 'START_RANDOM']
com2 = ['dp-lsl-recording', 'SELECT_ALL']
com4 = ['dp-lsl-recording', 'RECORD']
com5 = ['dp-mi-paradigm', 'RUN', 'nrepetitions=nrep']
[macros.stop]
name = 'STOP_TEST'
description = 'Send a stop command to all involved modules'
[macros.stop.cmds]
com1 = ['dp-lsl-recording', 'STOPRECORD']
For more details about how create a config for dp-control-room
, please be referred to the README.
Before we restart the control room to check our new configuration including the macro, we need to start the LabRecorder. Otherwise the we will not be able to start the control room GUI. (Note - this is a temporary necessity. In later versions of the dp-lsl-recording
module, this will be done automatically).
Now restart the control room from within dp-control-room
by using python rcr.py
and you should see a Macro
section on the GUI at 127.0.0.1:8050
.