Develop, organize, and deploy vision science experiments.
Bassoon is no-code visual graphics software to easily create, manage, and execute custom visual stimuli for vision science experiments. Bassoon is python based, and is built on the popular Psychopy API.
Maintained by the Dunn Lab at UCSF.
Bassoon relies on Python 3 and the Psychopy libraries. Follow these steps to install Bassoon on your computer:
conda create -n Bassoon python=3.10. Once the environment is created, activate it using conda activate Bassoon and install psychopy by entering pip install psychopy (note that there used to be a conda distribution for Psychopy, but it is no longer up to date). If you run into trouble, look through the Psychopy documentation.pip install tk and pip install pyserial. Make sure that your conda or virtual environment is activated before pip installing (typically, this is indicated by the name of the environment being listed in parentheses in your terminal/anaconda prompt. For instnace, (base) C:\Users\mrsco> indicates that the base environment is active). conda activate Bassoon) and then enter conda install spyder into the terminal (or conda prompt). Once installed, you can type spyder into the terminal, and it should launch an IDE from which you can view, test, and run Bassoon. If you prefer an IDE other than spyder, a popular option is VS Code (you can follow these instructions on how to connect VS code to python, especially if you are not using a conda environment).python /path/to/main.py). If the Bassoon window launches then you're ready to go! If you encounter an error (either here or at any further step) make sure you have the neccessary dependencies installed.
This section contains a list of known errors that users have encountered while trying to install Bassoon.
pip install tk==8pip install pyserial==3.5pip install psychopy may not work with python version 3.12+. If you get an error message that includes something similar to AttributeError: module 'pkgutil' has no attribute 'ImpImporter'. Did you mean: 'zipimporter'? when trying to install psychopy, try checking the python version. Type conda python --version into the terminal (make sure the Bassoon environment is activated). If you see 3.12, try downgrading python by typing conda install python=3.11. Then try reinstalling psychopy using pip install psychopy.ERROR: Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools". The solution is to download and install Visual Studio from Microsoft, making sure to select the C++ packages during the installation process. Sometimes a link to dowload the software is also reported with the error message, which works equally well. Once you've installed Visual Studio, try rerunning pip install psychopy.General Advice for Python Beginners: If you're new to programming, one of the most tempting things to do when you encounter a new or unknown error is to give up and ask for help. However, a closely kept secret is that even for experienced programmers, almost every error is a new error! I strongly encourage you to try to debug errors and problems yourself (at least for a bit). It's by far the best way to learn and build confidence. The biggest tips I can provide are to read the error messages and terminal outputs, look through the code (especially if an error message points you to a particular line), experiment, and USE GOOGLE! There's a 99% chance that the solution is documented somewhere online - Stack Overflow, Reddit, and even ChatGPT are great starting points. If you really can't figure something out, then ask for help - but don't be surprised if the "expert" you turn to just as quickly goes to Google to find the answer.
This section describes how to create custom experiments using Bassoon's no-code GUI. For a detailed explanation of how Bassoon operates behind the scenes, including descriptions on how to create new protocol classes, see Nuts And Bolts.
To launch Bassoon run the main.py file. Enter python /path/to/main.py in your terminal, where /path/to/ is replaced by the path on your local machine to the directory that houses Bassoon's main.py file (you can also cd to the directory and then just enter python main.py).
The following window will open 
The general work flow is to create (or load) an "experiment sketch" and then run it. The experiment sketch consists of a list (in order) of all the stimuli that you will run, customizations that you have made to each of them, and the settings that you would like to use for the experiment. Once you've built an experiment sketch, you can run it and save it. You can also load previously built experiment sketches directly into Bassoon, which can then be run immediately.
The red numbers in the image above label the basic buttons to use in Bassoon. Broadly, you will add stimuli to your experiment sketch, customize their parameters, customize the experiment settings, and then run the experiment. Below is specific information on each button:
You can change the values of each parameter using the text boxes next to their name in the new window. The information in brackets < > on the right side tell you the data type that you must enter (these correspond to python classes). For lists, you'll see two entries, one that says 'list' and a second that specifies the data type of each list element, such as 'float'. You can click the information buttons next to each parameter to print out a description of what they do to the console. Once you've customized the stimulus by changing all necessary parameters, click the Apply Changes button. You should receive a report in your python console indicating that the properties were successfully updated. After pressing Apply, the Estimated Time value will also update, indicating the amount of time that the stimulus is expected to take. Once you are finished customizing the parameters, you may exit the edit window. Note that for lists, you must use the correct syntax, including square brackets on either end and commas between entries. If you enter the wrong data type, your changes may be rejected, and this will be reflected in the python console.


When Bassoon saves experiments, it creates one file with the extension .EXPERIMENT, and another that is in JSON format. The former is a pickled python file that can be easily loaded into python for analysis. The latter contains all of the same data, but is visualizable and more easily used with other programming languages such as MATLAB or R. All of the parameters from each protocol that you added to the experiment are available through either file.
Bassoon's goal is to present a temporally precise sequence of customized stimuli to the user. It does this by organizing stimuli into three levels of abstraction. From lowest to highest level:
__init__(), estimateTime(), and run():
__init()__: Initializes the protocol (i.e., is executed when the protocol object is created by Bassoon - specifically, this happens when the user adds the protocol to their experiment sketch using the "Add Stimulus" button in the GUI). Listed at the top of this function are several attributes. Some are common across stimuli, while others are particular to certain stimuli. When a user "customizes" a stimulus, they are modifying these attributes. Any attribute that starts with an underscore "_" charcter, however, is not customizable by the user (e.g., self._angleOffset). These underscore properties are manipulated programmatically by Bassoon.estimateTime(): A function that approximates the total time that the stimulus will take to run, based on key properties. The mechanics of this function are similar across protocols, but vary according to the specifics of how the protocol's run() function is designed.run(): This is where Bassoon calls on the psychopy libraries to build and execute stimuli. The first half of the function typically involves creating the initial state of the stimulus and performing some computations that will be used to dynamically modify the stimulus during the stimTime. The run() function almost always also contains a for-loop, which iterates through each epoch to sequentially present them. In the loop, the psychopy stimulus is repeatedly updated and pushed to the stimulus monitor on every frame. This update step typically involves updating some parameter of the stimulus to make it dynamic across time (e.g., changing the position of a drifting bar stimulus on each frame such that it glides across the screen). For every epoch, nearly all protocols have 4 distinct sections of the for-loop that execute:
run() function, before the loop). Thus, stimuli that are computationally/graphically heavy can slow down the execution. If this occurs, it will be documented in several saved parameters that are created during the execution of the stimulus, such as self._stimulusStartLog and self._stimulusEndLog, which save the start and end time of each epoch. Note that these attributes start with the underscore "_" character. As described above, that makes them (and many similar ones like them that are created in the run() and other functions noneditable by the user. They are, however, saved with the stimulus so they can be used for analysis.__init__(): Same as the corresponding function for each individual protocol, but these attributes are inherited across all protocols (i.e., in addition to the attributes that are specified within each protocol's own __init__() function).internalValidation(): Used to check whether the user has entered valid values for editable attributes when an experiment is being created. Several individual protocols also have functions called internalValidation() that overwrites this function when it exists (several protocols do not have their own version of this method, in which case this general version is called). This function returns two values: tf is a bool value that is true only if all validations are passed, otherwise it is false; errorMessage is a list of strings containing messages to report back to the user for each of the validations that are not passed. If tf is true, then errorMessage should be and empty list. See the MovingGratingScotoma.py file for an example of how to write a validation function (and you can validate any and all parameters that you like!).
validateColorInput(): An add on to the validation function that will check all color parameters. It does this by looking for attributes of the protocol that have the word "color" in their name and checking each one to make sure that it is a list of 3 rgb values between -1 and 1 (the accepted range for rgb values in psychopy). This is a useful method to add onto any protocol's internalValidation function.printDescription(attributeName): Called when a user presses the information button for a parameter in the edit protocol window. This function prints the description of that parameter to the console. Specifically, the description is defined by the comment in the __init__() function in the protocol's .py file, or in the protocol.py superclass._get_attribute_descriptions(cls): A helper function that is called by self.printDescription(attributeName), this function traverses the protocol's __init__() function to extract the informational comments next to each attribute. It also caches the results so that the process only needs to be performed once per protocol.getFR(win): Returns the frame rate of the stimulus monitor (win) and calculates the number of frames needed for the pretime, stimtime, and tailtime. This function is usually called near the top of each protocol's run() function. Note, that because this is an inherited function by all protocols, they all must have a pretime, stimtime, and tailtime field (sometimes regardless of if they're used). The MovingGratingScotoma stimulus, for instance, should not have a user editable stim time based on its mechanics. However, it needs an attribute called self.stimTime to avoid raising an exception in this and other functions. To get around this, the stimtime is checked in the internalValidation() function and reassigned to 0 if the user attempts to change it. A notice is also provided to the user through a print statement in the console.getPixPerDeg(stimMonitor): Calculates the number of pixels per visual degree for the stimulus monitor. To do this, the function draws on the attributes of the saved/loaded monitor that the user has indicated that they are using (through the attributes of the experiment object). Before running Bassoon for the first time, you may want to set up (and save!) a new psychopy monitor. This can be done entirely through the psychopy api (outside of Bassoon) and/or in the psychopy GUI through the monitor center. If you have saved your monitor correctly, it should show up as a selectable option in the dropdown menu under "monitors" in Bassoon's "options" menu (accessible through the GUI).showInformationText(stimWin, txt): Pushes a message to the information window if the user has enabled it. This is typically updated for each epoch (i.e., for each iteration of a protocol's run() function.checkQuitOrPause(): Checks for two keystrokes that the user can execute while a stimulus is running: p pauses a stimulus and q quits a stimulus (jumps to the next one in the experiment, or ends the experiment if it's the last one). Both pauses and quits are documented in saved parameters.sendTTL(): This function is used to send timing signals over a TTL port. This is useful if you want to align the stimulus to something outside of Bassoon (e.g., a recording of animal behavior). Enable a port from the options menu in the GUI. Hardware wise, you can simply use a USB-to-TTL serial cable such as this one and the system should recognize it once plugged in. Bassoon sends TTL pulses via the RTS pinout. There are two different TTL writing modes that provide different levels of precision:
sendTTL() happens inside the for-loop in the run() function.burstTTL(win): Sends a stereptyped burst of TTL pulses when in Pulse mode in order to mark the start of a stimulus. This is only implemented for a few stimuli. This function can be modified to change the signature burst to be anything you like, but currently it sends 20 TTL pulses at frame rate, pauses for 0.2 seconds, and then sends 20 more TTL pulses at frame rate. Note: in some contexts, it can be critical to not send TTL pulses faster than the frame rate because depending on the temporal resolution of your detection system, pulses can be missed.reportTime(displayName): Prints a timing report for each protocol to the console when the user checks the corresponding box in the options menu of the GUI. This can be very helpful to understand whether a stimulus is too computationally/graphically heavy to execute at frame rate. It's recommended that each stimulus is tested and timing reports are printed prior to using them in actual experiments.printProgressBar(iteration, total): Used to print a progress bar to the console when needed. This is typically only used for stimuli that have a lot of upfront computation to perform (usually in the run() function) prior to executing in order to inform the user as to approximately how long it will take to load the stimulus.__init__() function. There are also several other attributes that are not accessible to the user through the options menu.
__init__(): Establishes the default attributes of the experiment when its first loaded (which happens when Bassoon launches). If the user has previously saved experiment properties, it will load this from a JSON file.addProtocol(newProtocol): This function adds protocols to the experiment as their created. Using the analogy above, this is what runs when you place a new protocol into your experiment bag.establishPort(portInfo): Handles the TTL port that is used to send timing signals. The mechanics are not so important, but just know that the status of the port has to be given special attention in order to avoid memory errors.activate(): This is the function that runs when you run the experiment from the Bassoon GUI. It launches the psychopy windows that the stimuli and information are presented on, sets up some basic parameters, and then loops through each stimulus that has been added to the experiment bag, executing them in order. It also has some safety features for TTL handling, and calls the reportTime() function after each protocol if the user has requested it.Experiments and protocols are created and manipulated through the Bassoon GUI. This is to make it as easy as possible to create, customize, and execute experiments. However, you can also access them programmatically, outside of the GUI:
from psychopy import core, visual, data, event, monitors
from experiments.experiment import experiment
from protocols.protocol import protocol
# You must import each protocol that you want to use. Here I import just the flash stimulus as an example.
from protocols.Flash import Flash
# Load an experiment
exp = experiment()
# Load the flash stimulus
stim = Flash()
# Edit an attribute of the flash
stim.stimTime=5 #in seconds
# Add the stimulus to the experiment (put it in the "bag")
exp.addProtocol(stim)
# Change an attribute of the experiment
exp.useInformationMonitor=True
# Run the experiment
exp.activate()
The main.py file is the outter-most layer of Bassoon. It is used to launch the GUI, contains all of the code for every button and action, and can be thought of as the orchestrator of all Bassoon functionality. Tkinter is used for the graphical interface. The best way to understand how the GUI works is to look through the code and experiment with it. There are plenty of example snippets that can be used as a reference to implement your own buttons and functionality.
For most users, it will not be necessary to make any changes to the GUI. The only section of the main.py file that requires regular attention is the import statements at the top of the script. If you create a new stimulus (which can easily be done by making a new .py file in the Bassoon/src/protocols folder, copying the structure of a previously existing protocol file, and making adjustments for your new stimulus as needed), you will have to add a corresponding import statement to the top of main.py. This takes the form of from protocols.myprotocol import myprotocol (replacing myprotocol with whatever you named your new protocol).
Bassoon is intended for noncommercial use only. If you would like to use this software for commercial research, please send us a message to inquire about a license.
If you use Bassoon for published science, please include a citation.
Contributions to the Github repository are welcomed. Suggestions or questions can also be directed to Scott Harris.