CFS tutorial#

Author: Gennadiy Belonosov <gennadiyb@mail.tau.ac.il>

๐Ÿ’ก Here we explain how to use the CFSVM package to create a CFS experiment. As a reference, we roughly implemented Experiment 2 from the original Tsuchiya and Koch study.

Start from installing the package#

A word of caution#

  • This tutorial is only intended to present the potential use and some functions of the package.

  • An exact replication of the original Tsuchiya & Koch experiments is possible, but requires deeper modifications of the code, which are not discussed here.

  • For simplicity, Gabor patch parameters such as spatial frequency, luminance etc. are ignored.

  • The Gabor patches were generated here.

Extracting parameters from Tsuchiya & Kochโ€™s experiment #2#

Methods figure from Tsuchiya & Koch, 2005

Methods figure from Tsuchiya & Koch, 2005

Figure created using the package

Implementation using the package: an example of the stimuli and the display. The left eye is presented with two gratings at different orientations. The right eye is presented with the CFS Mondrian display and a blank screen.

We extracted the following parameters from the Methods section and Supplementary Table 1 of the paper:

  • The contrast of two Gabor patches was set to 30%.

  • The phase and orientation of the Gabors were chosen randomly.

  • The flashing frequency of the Mondrians was 10 Hz.

  • Adaptation continued for 5 seconds.

  • After the adaptation a gray background was shown.

  • In 20 experimental trials two Gabor patches were simultaneously shown on both the left and right sides of the display presented to the left eye (see figure above).

  • In 10 catch trials, a Gabor patch was presented only on the non-suppressed side of the display (e.g., if the Mondrians are presented on the right side, as in the figure above, only the left side Gabor will be presented).

  • The 10 catch trials were randomly interleaved with the 20 experimental trials.

Preparing the experiment#

๐Ÿ’ก The following section explains how to use the package to program your experiment.

Create the experiment folder#

We will organize the experiment according to the HLC lab handbook. The initial folder structure will look like this (only relevant for this tutorial folders are shown):

TnK_experiment_2/
โ”œโ”€โ”€ Experiment/
โ”‚   โ””โ”€โ”€ RUN_ME/
|       โ”œโ”€โ”€ Code/
|       โ””โ”€โ”€ Stimuli/
โ””โ”€โ”€ Raw Data/
    โ””โ”€โ”€ Behavioral/

Stimuli#

Create a new folder called Suppressed/ inside the Stimuli/ and put in it two Gabor images with transparent backgrounds and opposite phases, like these:

Gabor1 Gabor2

Your RUN_ME folder structure should now look somewhat like this:

RUN_ME/
โ”œโ”€โ”€ Code/
โ””โ”€โ”€ Stimuli/
    โ””โ”€โ”€ Suppressed/
        โ”œโ”€โ”€ gabor_1.png
        โ””โ”€โ”€ gabor_2.png

Trial matrix#

We will start by generating a list of trials specifying the experimental conditions.

  1. Navigate to the Code/ folder in the MATLAB files panel (or using the cd command).

  2. To generate trials:

    • You can either use the example script TnK_generate_trials (simply run it in the MATLAB command window, it will generate the trial matrix for this specific experiment) and navigate to the last step (or two) before the run section in this tutorial.

    • Or you can create your own generate_trials.m file based on your needs. If you follow this way, your RUN_ME folder structure will now look like this:

RUN_ME/
โ”œโ”€โ”€ Code/
|   โ””โ”€โ”€ generate_trials.m
โ””โ”€โ”€ Stimuli/
    โ””โ”€โ”€ Suppressed/
        โ”œโ”€โ”€ gabor_1.png
        โ””โ”€โ”€ gabor_2.png

๐Ÿ’ก The main idea of this script is to create an experiment object for each trial and save it in one .mat file. Letโ€™s go over the script command by command.

Import the package#

import CFSVM.Experiment.* ...
    CFSVM.Element.Screen.* ...
    CFSVM.Element.Data.* ...
    CFSVM.Element.Evidence.* ...
    CFSVM.Element.Stimulus.*

Choose the experiment type#

There are two types of CFS experiment in the package: BCFS (breaking CFS) and VPCFS (visual priming CFS). The two types have many common properties, as they are both Continuous Flash Suppression, yet they are different from each other in the following ways:

  • The BCFS code includes a breakthrough property, which records the participantโ€™s response during the time, indicating that she detected the stimulus. The trial then stops when this response is recorded.

  • The VPCFS code includes the mafc and pas properties which define the objective (multiple-alternative forced choice) and subjective (perceptual awareness scale) measures of consciousness, respectively.

As in this experiment neither of mAFC or PAS is used, we will build the experiment with the BCFS template.

experiment = BCFS();

Initialize the objectโ€™s properties#

The BCFS template has multiple required properties:

Property

Class

screen

CustomScreen

subject_info

SubjectData

instructions

Instructions

trials

TrialsData

fixation

Fixation

frame

CheckFrame

masks

Mondrians

breakthrough

BreakResponse

We will initialize only a few of them here: fixation, frame, masks, breakthrough. The remaining properties will be initialized at the start of the experiment. We will also add two dynamic properties for stimuli, representing two Gabor patches.

Checkframe#

As for the CheckFrame: although the frames donโ€™t appear in the original experiment, we will use them here as it is a common practice in the HLC lab. We will set hex_colors to black and white (#000000 and #FFFFFF), checker_length to 30 pixels and checker_width to 15 pixels (checker is the small rectangle; the frame consists of multiple checkers).

experiment.frame = CheckFrame( ...
    hex_colors={'#000000', '#FFFFFF'}, ...
    checker_length=30, ...
    checker_width=15);
Masks generation#

Next, we move on to the Mondrians. If you have pre-generated Mondrians, put them into RUN_ME/Stimuli/Masks/ folder and skip to the masks initialization. Otherwise, we will use the MondrianGenerator. Note that it provides only basic functionality. If you want to create spatially, temporally or orientationally filtered or phase scrambled masks, refer either to the CFSCrafter class or to the original CFS-crafter GUI: (Wang et al., 2022), GitHub.

  1. Import the MondrianGenerator class and construct the generator object, using the code below. The generator will randomly locate a specific numbers of figures (defined as n_figures, 1000 by default) that are shaped in a specific manner (defined as type, which can be, for example, a rectangle, a circle or a rhombus; for more shapes check the documentation) with the minimum radius equal to x(y)_pixelsร—min_fraction and the maximum radius equal to x(y)_pixelsร—max_fraction inside the provided_path/Masks/ folder (here Stimuli/Masks/).

import CFSVM.Generators.MondrianGenerator
generator = MondrianGenerator( ...
    '../Stimuli/', ... % Will generate Mondrians inside path/Masks/
    type='rectangle', ... % Available shapes are โ€˜rectangleโ€™, โ€˜squareโ€™, โ€˜ellipseโ€™, โ€˜circleโ€™, โ€˜rhombusโ€™ and โ€˜45_rotated_squareโ€™.
    x_pixels=512, ... % Width of generated images in pixels
    y_pixels=512, ... % Height of generated images in pixels
    min_fraction=1/20, ... % Minimal possible radius of a figure.
    max_fraction=1/8, ... % Maximal possible radius of a figure.
    n_figures=1000,
    cmap='grayscale' % Can be either string or MATLAB-styled RGB array.
    );
  1. For the colormap use the cmap argument from the code above. It can be either a string for the supported colormaps (โ€˜grayscaleโ€™, โ€˜rgbโ€™, โ€˜originalโ€™ etc., for other options refer to the documentation) or you can provide the argument with the MATLAB-formatted RGB array. For the grayscale with 5 tones it will look something like this:

cmap = [0 0 0;
      0.25 0.25 0.25 ;
      0.5 0.5 0.5;
      0.75 0.75 0.75;
      1 1 1]

In addition, you can use the set_shades() method if you want to use grayscale with different numbers of tones. set_shades() will take the RGB array as a first argument and create a gradient from this color to white with a number of tones provided as a second argument. In the code below it creates a gradient from black to white with 8 tones, i.e., black, white and 6 tones in between.

generator.set_shades([0,0,0], 8);
  1. We will provide the generator object with the physical properties of the display, so it will be able to calculate the power spectral densities of the Mondrians (the output .png and .csv files will be saved inside the provided_path/ folder). The arguments are: width in cm, width in pixels, height in cm, height in pixels, viewing distance in cm.

generator.set_physical_properties(60, 1920, 33.5, 1080, 45)

Now we will run the generator using the generate() method, which takes n_mondrians as an argument. In general, n_mondrians=temporal_frequencyร—duration will generate enough masks, so that they are not repeated in a single trial. Here we will generate 10Hz*5sec=50 Mondrians.

generator.generate(50)
Masks initialization#

Now, with generated Mondrians, we will set the masks property. The first parameter we will define is the relative path to the folder containing the Mondrian images. If youโ€™d like to work with CFS-crafter generated masks, you can set a path to the .mat file using crafter_masks argument (crafter_masks will override the dirpath if both have been provided). We will set temporal_frequency to 10 Hz, duration to 5 seconds, and position to โ€œRightโ€ (so that the masks suppress only the right Gabor patch). To adjust the Mondriansโ€™ position, we will set size to 0.45 (1 will fill the whole right eye field, 0.5 will fill half of the right eye field depending on position), padding to 0.3 (0 will align to the frame, 1 will align to the center of the fixation cross), and xy_ratio to 1 (square). You can also change the contrast and rotation if desired. Finally, we will set blank to 5 seconds, to present a blank screen with fixation crosses and frames after the CFS stimulation has ended.

experiment.masks = Mondrians( ...
    dirpath='../Stimuli/Masks', ...
    temporal_frequency=10, ...
    duration=5, ...
    position="Right", ...
    size=0.45, ...
    padding=0.3, ...
    xy_ratio=1, ...
    contrast=1, ...
    rotation=0,
    blank=5);
Fixation generator#

Fixation target also has its own generator, you can find a tutorial on how to use it here. There are multiple available shapes, mainly based on Thaler et al., 2013. Here we will generate the simplest cross:

import CFSVM.Generators.FixationGenerator

generator = FixationGenerator( ...
    '../Stimuli/', ...
    radius=256, ...
    hex_color='#000000', ...
    is_smooth_edges=true, ...
    smoothing_cycles=5);

generator.C(cross_width=64)
Fixation#

To initialize the fixation property, we will first provide a directory that contains our generated fixation target. Second, we will set duration to 1 second and size to 0.05 (5%) of the frame. It also has additional parameters, like rotation, contrast etc. (for more, please, check the documentation), which we wonโ€™t set here.

experiment.fixation = Fixation( ...
    '../Stimuli/Fixation', ...
    duration=1, ...
    size=0.05);
Stimuli#

Now we will add two stimuli properties for the Gabor patches.

Create the properties first:

experiment.addprop('stimulus_1');
experiment.addprop('stimulus_2');

Then we will initialize them. As a first parameter we will provide a path to the folder with two Gabors that we presented here earlier.

We will set duration for both of the parameters to 5 seconds. We will set a different position for them: โ€œLeftโ€ for the first and โ€œRightโ€ for the second. We will set contrast to 0.3 (30%), size to 0.5, padding to 0.5 and xy_ratio to 1.

experiment.stimulus_1 = SuppressedStimulus( ...
    '../Stimuli/Suppressed/', ...
    duration=5, ...
    position="Left", ...
    contrast=0.3, ...
    size=0.5, ...
    padding=0.5, ...
    xy_ratio=1);
experiment.stimulus_2 = SuppressedStimulus( ...
    '../Stimuli/Suppressed/', ...
    duration=5, ...
    position="Right", ...
    contrast=0.3, ...
    size=0.5, ...
    padding=0.5, ...
    xy_ratio=1);
Breakthrough#

Here you can set the parameters for the breakthrough property, which basically records the time and the key pressed by the participant. This is not very relevant for our needs here, so we will just initialize it and move on.

experiment.breakthrough = BreakResponse();

Our experiment object is ready now. Letโ€™s set the number of blocks and trials. The number of blocks should be a natural number and the number of trials is defined by a list of length=n_blocks containing the number of trials in every block. For example, if we want to have one block with 20 trials and another block with 30 trials we will set n_blocks = 2, n_trials = [20, 30]. But in the current experiment, there is only one block with 30 trials. The last line will initialize a cell array for the trial matrix.

n_blocks = 1;
n_trials = [30];
trial_matrix = cell(1, n_blocks);

Now, for all 30 trials we will create a copy of our experiment object, randomize the Gabor images and their orientations for both stimuli properties in this copied object and then add the object to the trial matrix for every trial (as the code that runs the experiment will expect trial matrix to be an array of arrays of objects).

For 10 of these 30 trials we will set the contrast for the second stimulus to 0, so that the right Gabor doesnโ€™t appear.

After every block (here we have only one), we will shuffle the trials.

orientations = [0, 45, 90, 135];
n_images = 2;
for block = 1:n_blocks
    for trial = 1:n_trials(block)
        exp_copy = copy(experiment);
        exp_copy.stimulus_1.index = randi(n_images);
        exp_copy.stimulus_1.rotation = orientations(randi(length(orientations), 1));
        exp_copy.stimulus_2.index = randi(n_images);
        exp_copy.stimulus_2.rotation = orientations(randi(length(orientations), 1));
        if trial >= 20
            exp_copy.stimulus_2.contrast = 0;
        end
        trial_matrix{block}{trial} = exp_copy;
    end
    trial_matrix{1} = trial_matrix{1}(randperm(numel(trial_matrix{1})));
end

Save the trial matrix to the file:

if ~exist('../TrialMatrix', 'dir')
    mkdir('../TrialMatrix')
end
save('../TrialMatrix/experiment.mat', 'trial_matrix')

Donโ€™t forget to execute the code before running the experiment! After executing it you will find the file with the trial matrix in the RUN_ME/TrialMatrix/ directory and generated Mondrians in the RUN_ME/Stimuli/Masks/. Your RUN_ME folder structure now should look like this:

RUN_ME/
โ”œโ”€โ”€ Code/
|   โ”œโ”€โ”€ generate_trials.m
|   โ””โ”€โ”€ main.m
โ”œโ”€โ”€ Stimuli/
|   โ”œโ”€โ”€ Suppressed/
|   |   โ”œโ”€โ”€ gabor_1.png
|   |   โ””โ”€โ”€ gabor_2.png
|   โ””โ”€โ”€ Masks/
|       โ”œโ”€โ”€ mondrian_1.png
|       |   โ‹ฎ    โ‹ฎ    โ‹ฎ    โ‹ฎ
|       โ””โ”€โ”€ mondrian_<n>.png
โ””โ”€โ”€ TrialMatrix/
    โ””โ”€โ”€experiment.mat

Congrats! We are almost there!

The last step (or two) before the run#

Customizing the instructions#

There are multiple types of instructions:

  • Introduction - a general description of the experiment.

  • Block introduction - a description of the forthcoming block (different for every block).

  • Rest - an instruction appearing after each trial (same for every trial).

  • Farewell

We will create an Instructions folder in the RUN_ME directory. Next, each type of the instruction will have its own subfolder. In each subfolder we will put corresponding images. When the experiment is running, the code will present the images by their natural order. You can set the background of the images as transparent in an image editor and save them as .png files if you want the background color the same as in the experiment. The exact names of the subfolders should be: introduction, introduction, block_introduction_1, โ€ฆ, block_introduction_n (where n is the number of blocks), rest, farewell. For the experiment we are building here the folder structure will look like this:

RUN_ME/
โ””โ”€โ”€ Instructions/
    โ”œโ”€โ”€ introduction/
    |   โ”œโ”€โ”€ 1.png
    |   โ””โ”€โ”€ 2.png
    โ”œโ”€โ”€ block_introduction_1/
    |   โ”œโ”€โ”€ 1.png
    |   โ”œโ”€โ”€ 2.png
    |   โ””โ”€โ”€ 3.png
    โ”œโ”€โ”€ rest/
    |   โ””โ”€โ”€ 1.png
    โ””โ”€โ”€ farewell/
        โ””โ”€โ”€ 1.png

Writing main.m#

So far weโ€™ve generated the trials. Now we want to run the experiment. Letโ€™s start building our main.m script, which we will put inside the Code/ folder near the generate_trials.m script. We will now go over a few parameters that can be adjusted to create specific settings you want. First, we will make sure that the workspace and the screen are cleared:

Screen('CloseAll');
close all;
clear;

Import parts of the package we will use here:

import CFSVM.Experiment.* ...
    CFSVM.Element.Screen.* ...
    CFSVM.Element.Data.*

Initialize the BCFS object and provide it with the save_to_dir - path to the folder you want the results to be saved. According to the lab handbook this will be Raw Data/Behavioral/. If you created your own instruction functions you can pass the folder containing these functions to the path_to_functions argument .

experiment = BCFS(save_to_dir='../../../Raw Data/Behavioral');

Initialize the subject_info property by providing a directory to save the info in. We will put it in Tnk_experiment_2/Raw Data/Demographics/.

experiment.subject_info = SubjectData( ...
    dirpath='../../../Raw Data/Demographics');

Initialize trials property by providing a path to the already generated file with the trial matrix.

experiment.trials = TrialsData( ...
    filepath='../TrialMatrix/experiment.mat');

Initialize the instructions property by providing a path to the folder containing instructions subfolders which were created in the previous step. backward_key, forward_key are the key names for moving between screens and proceed_key for proceeding to the experiment. If proceed_key='' (empty char), it will be the same as the forward_key. You can also provide multiple keys for every argument, e.g., backward_key={'LeftArrow', 'a'}. The key names should correspond to the unified PTB style. To find the correct spelling of keys, run in the matlab console two commands: KbName('UnifyKeyNames') and KbName('KeyNames').

experiment.instructions = Instructions( ...
    "../Instructions/", ...
    backward_key='LeftArrow', ...
    forward_key='RightArrow',
    proceed_key='Return');

Adjust frames

Initialize the screen property by setting is_stereo to true and defining the size and location of the left rectangle by providing initial_rect with pixel coordinates of its upper left and lower right vertices, like this: [x0, y0, x1, y1]. The first two numbers in this array are x and y pixels of the upper left vertex, the last two numbers are x and y pixels of the lower right vertex. The right rectangle will be set symmetrically to the left one.

We will also provide hex code for the background color - #AEAEAE gray here.

experiment.screen = CustomScreen( ...
    is_stereo=true, ...
    initial_rect=[30, 90, 930, 990], ...
    background_color='#AEAEAE');

The last two lines of code are quite self explanatory:

experiment.run()

Screen('CloseAll')

Thatโ€™s it! You can run the main.m script now!

Running the experiment#

Right after executing the script youโ€™ll see the dialogue for collecting the subject data:

>> main
Subject code
> HZ4
Date of birth
> 01011900
Dominant eye
> Right
Dominant hand
> Both

Now you will proceed to the experiment itself.

And thatโ€™s it! Once youโ€™ve adjusted the screens, youโ€™ll see the introduction screen. After that, the CFS will begin.

The data#

The raw trial data will appear in the Tnk_experiment_2/Raw Data/Behavioral/RawTrials/. Every .mat file will contain an experiment object similar to the ones we have generated while creating the trial matrix, but containing timings and subject responses. You can run CFSVM.Utils.extract_from_raw_cfs(path_to_raw_trials, subject_code) (โ€˜../../../Raw Data/Behavioral/RawTrialsโ€™ and โ€˜HZ4โ€™ in this example) script to extract the data from the raw files. It will create two csv tables in Behavioral/Extracted/<subject code>/ and Behavioral/Processed/ folders, the first one containing raw timings and the second one processed ones (e.g., durations instead of onsets and offsets). The final folder structure will look like this:

TnK_experiment_2/
โ”œโ”€โ”€ Experiment/
โ”‚   โ””โ”€โ”€ RUN_ME/
|       โ”œโ”€โ”€ Code/
|       |   โ”œโ”€โ”€ generate_trials.m
|       |   โ””โ”€โ”€ main.m
|       โ”œโ”€โ”€ Stimuli/
|       |   โ”œโ”€โ”€ Suppressed/
|       |   |   โ”œโ”€โ”€ gabor_1.png
|       |   |   โ””โ”€โ”€ gabor_2.png
|       |   โ””โ”€โ”€ Masks/
|       |       โ”œโ”€โ”€ mondrian_1.png
|       |       |   โ‹ฎ    โ‹ฎ    โ‹ฎ    โ‹ฎ
|       |       โ””โ”€โ”€ mondrian_<n>.png
|       โ”œโ”€โ”€ TrialMatrix/
|       |   โ””โ”€โ”€experiment.mat
|       โ””โ”€โ”€ Instructions/
|           โ”œโ”€โ”€ introduction/
|           |   โ”œโ”€โ”€ 1.png
|           |   โ””โ”€โ”€ 2.png
|           โ”œโ”€โ”€ block_introduction_1/
|           |   โ”œโ”€โ”€ 1.png
|           |   โ”œโ”€โ”€ 2.png
|           |   โ””โ”€โ”€ 3.png
|           โ”œโ”€โ”€ rest/
|           |   โ””โ”€โ”€ 1.png
|           โ””โ”€โ”€ farewell/
|               โ””โ”€โ”€ 1.png
โ””โ”€โ”€ Raw Data/
    โ”œโ”€โ”€ Demographics/
    |	โ””โ”€โ”€ HZ4.csv
    โ””โ”€โ”€ Behavioral/
        โ”œโ”€โ”€ Raw Trials/
        |   โ””โ”€โ”€ HZ4/
        |       โ”œโ”€โ”€ block1_trial1.mat
        |       |   โ‹ฎ    โ‹ฎ    โ‹ฎ    โ‹ฎ
        |       โ””โ”€โ”€ block<n>_trial<m>.mat
        โ”œโ”€โ”€ Extracted/
        |   โ””โ”€โ”€ HZ4/
        |       โ”œโ”€โ”€ HZ4_extracted.csv
        |       โ””โ”€โ”€ HZ4_ifis_histogram.png
        โ””โ”€โ”€ Processed/
            โ””โ”€โ”€ HZ4_processed.csv

Raw

Raw trials

Raw

Extracted, not processed csv

Raw

Processed csv

Thanks!#