Inability to call createTrialHandlerRecordTable in several loops

Hello! I am currently playing around with the new builder eye tracking components (they make eye tracking setup a breeze!), and I’ve gotten stuck when trying to create more than one TrialHandlerRecordTable in the same experiment.
Basic info about the task:
I created a basic testing design, with a loop called trials and a routine within, where i called ioServer.createTrialHandlerRecordTable(trials). This loop used an excel file just to register some dummy variables
Description of the problem:
Everything was working just fine, but when i added a second loop that called createTrialHandlerRecordTable a second time to create a second table that gathered the variables of my second loop (say, ioServer.createTrialHandlerRecordTable(trials_2)), it did not actually create the second table; the Runner raises a KeyError (code below).

File "D:\Descargas\EyeTrackerBETATESTING\BETATEST_lastrun.py", line 524, in <module>
    ioServer.addTrialHandlerRecord(thisTrial)
  File "D:\Descargas\Psychopy3\lib\site-packages\psychopy\iohub\client\__init__.py", line 644, in addTrialHandlerRecord
    data.append(cv_row[cv_name])
KeyError: 'TestVariable4'

Perhaps ioHub is unable to create more than one table for appending into the hdf5 file? (thus the KeyError would maybe come from an assignation of the differently named variables of the second loop into the table that has columns representing those of the first one.

If i disabled the creation of the second table, and only called ioServer.addTrialHandlerRecord(thisTrial), the values of the new rows are the same of the last trial of the first loop; image for reference

Is there any other way to create several ioHub TrialHandler tables? Since the main utility i’m trying to accomplish is labeling eye tracking data, a workaround for this problem could be using messages to send the values of the different variables in the loops to ioHub and then using pandas (or R) to extract the different values and correctly label each eye tracking refresh

You are correct, iohub only supports creating one conditions table in an hdf5 data file. When createTrialHandlerRecordTable(trials) is called, a trial in trials must have all the columns that need to be written to during the experiment, otherwise you will get a key error.

As you suggest, a work around would be to use experiment messages to save the variable values to the hdf5 file and then parse them out. Not as easy / nice as using a conditions table, but workable.

Are your two conditions files within nested loops, or are they in sequential loops in the experiment?

Sequential; I could try nesting the two smaller loops within a bigger loop that gathered all the different variables of all loops and creating a single table outside of the smaller loops. I’ll try calling addTrialHandlerRecord to see if the table fills the corresponding variables

I’ll edit this post with the outcome in a second
Edit: Still not working; after creating the table in a routine outside of the smaller loops and creating the table in the “allvariablegatherer” routine,


the same KeyError is raised when ioHub tries to add the trial data to the table

Traceback (most recent call last):
  File "D:\Descargas\EyeTrackerBETATESTING\BETATEST_lastrun.py", line 451, in <module>
    ioServer.addTrialHandlerRecord(thisTrial)
  File "D:\Descargas\Psychopy3\lib\site-packages\psychopy\iohub\client\__init__.py", line 644, in addTrialHandlerRecord
    data.append(cv_row[cv_name])
KeyError: 'TestVariable4'

This could work, but I think you would need to make a single call to addTrialHandlerRecord with all the variable values for the single ‘combined’ trial conditions file you created. The variable names in this file will (I think) need to be different then the ones you actually use in your experiment, so if you are using a column called testVariable1 in your experiment, the combined table column name could be testVariable1_ or something.

I see; I actually managed to fix the problem that was raised in my earlier post!: the inner loop file needs to have the exact same variables than the outer loop file, and their datatype has to be the same (e.g., if you fill one of them with integers and the other with a string you will get another error). I tried calling addTrialHandlerRecord inside each inner loop, and i found the same problem i was getting earlier, where the data from the second loop was getting improperly filled with the values of the last trial of the first loop.


I noticed, however, than the TRIAL_START and TRIAL_END variables were correctly increasing each trial, so i manually called

thisTrial['TestVariable1'] = ''

on routine end, and voila!

So it seems the recipe for having several different loops while using ioHub and recording variable values in the TrialHandlerRecordTable is to: (a) create an outer loop that encompasses your trial blocks, with an excel file that gathers all different excel variables, and dummy values in each column that will have data, of the same datatype as the real variable values, as an example: image
**
(b) Have a routine inside your outer loop, before your inner loops, where you create the table for the outer loop. As my outer loop was called trials_3, i used this in the Begin Routine section of a code component;

if trials_3.thisN == 0:
    ioServer.createTrialHandlerRecordTable(trials_3)

(c) Your inner loop condition files MUST contain the exact same variable names (and order, just in case).
(d) You can register your first loop variables by calling ioServer.addTrialHandlerRecord(thisTrial) at the end of the routine inside the first loop, but your second block will not register the values from the excel at all. In order to do so, you can manually update the values of your variables inside the code component of your routine in the End Routine tab before calling addTrialHandlerRecord (for example: image
You can even use python loops, list comprehension or packing/unpacking to update all the variables at the same time.
Within the hdf5 file, all the variables you update in a code component within your routines will get correctly updated and added to your table, and then you can use it to analyze your data with the ease that the table allows for


Thank you very much for your assistance and the idea to use nested loops, which absolutely ended up solving the problem

edit: it is worth noting that, since you can only have one table per experiment, you will probably need to add a variable to register the block you are in if you want to analyze your blocks in a separate manner

I do not think you need to have the outer trials_3 loop. Here is what I would try:

  1. Create a .xlsx conditions file that contains all the columns you want across both trial loops in your hdf5 condition variables table. I called it hdf5_conditions.xlsx. It only needs to have 1 row / trial worth of data in it since it is only being used to tell iohub what conditions to expect and what the default values that should be if it is not updated for any trial.

  2. In your project custom code where you create the hdf5 conditions table (near the start of the experiment), update it to use the new hdf5_conditions file and access an hdf5_trial instance from it:

    hdf5_conditions = importConditions('hdf5_conditions.xlsx')
    hdf5_trials = TrialHandler(hdf5_conditions, 1)
    io_hub.createTrialHandlerRecordTable(hdf5_trials)
  1. To reset the condition variables to default values, so you do not need to update every column on every trial, get a copy of the first hdf5_trial at the start of each trial loop:
    hdf5_trial = copy.deepcopy(hdf5_trials.trialList[0])
  1. Now, whenever you need to set values for the current trial, use code like:
    hdf5_trial['trial_id'] = t+1
    hdf5_trial['testVariable1'] = 'test1'
  1. to save a row to the hdf5 conditions table, use:
    io_hub.addTrialHandlerRecord(hdf5_trial)

Each time this is called, a row will be added to the hdf5 conditions table containing the current state of hdf5_trial.

Edit: I see you figured out a workaround yourself; nicely done.
Edit2: Updated example code to show how to reset condition defaults at the start of each full trial.

1 Like

That will probably work just as well, since you have to update the variables within the code regardless of nested loop usage, and the custom TrialHandler should do exactly the same as the outer loop TrialHandler (since it is really only used for creating the table)

Hi!

I need to do something similar for my experiment, but I’m struggling with the first steps even. Could you please explain how to include the custom code in the Builder to get the trials info in the hdf5 file?

Thank you,
Manuel

You will need to insert a code component inside a routine and then call createTrialHandlerRecordTable
I believe that this part of the psychopy documentation provides an example Starting the psychopy.iohub Process — PsychoPy v2023.2.3

Hi! Thank you for the reply. I’ve been trying what you say after having read that iohub guide. But I’m still not able to get any condition table.

Would you mind sharing what’s inside your code component, just to compare it with mine?

Sorry for bothering and thank you again.

As stated earlier on in the chain, I created a routine with a code component inside the outer loop (but not any of the inner loops). In the Begin Routine tab of the code component, the following is stated:

#Create ioHub table
if Outerloop.thisN == 0:
ioServer.createTrialHandlerRecordTable(Outerloop)

Edit: remember to indent the line below the if statement, for some reason the post is showing the code without indents
Outerloop is the name of my (your) outer loop. Now, remember that this only creates the table (and ensures that it is only created once unless you call the same method elsewhere). Now, you will need to fill it.

Inside another code component, this time in a routine within an inner loop, the following records trial variables from the excel condition file into the table:

ioServer.addTrialHandlerRecord(thisTrial)

Remember that you can, instead of the TrialHandlerRecord table, record your values using messages (via ioServer.sendMessageEvent(text = [THIS IS THE CONTENT OF YOUR MESSAGE, AND CAN BE A LIST CONVERTED TO STRING]) but you will have to parse them then.

What error does psychopy return when you try to generate the condition table?

Hi. I don’t have any particular error, yet I don’t get any new data concerning the conditions. I am using the ioServer.sendMessageEvent method, but the problem is that, by doing so, I get a different amount of data points for each trial, which is not what I would want ideally.

I will try and follow your suggestions and let you know how it goes. Thank you! :slight_smile:

Ok, I still cannot get the famous table. I’ll try and be more precise on what my experiment looks like.

I’m attaching a picture of a temporary version of my paradigm.

What I did, for now, is including a Routine in my loop of interest in which I inserted the following:

if trials.thisN == 0:
      ioServer.createTrialHandlerRecordTable(trials)

(If I inserted this in the outer loop it would give me an error: "self._cv_oder = trial.keys() → AttributeError: “NonType” has no attribute “keys”. Anyway, reading also other posts, I think the inner loop is where I want to initialise the table)

I then created another routine at the end of the loop with a code component for the second part, i.e., adding the information:

ioServer.addTrialHandlerRecord(thisTrial)

Alternatively, just to try different things out, I also included this code component in the “Trials” routine, which contains the videos I’m showing the participants (whose information I want to keep track of).

In both cases, I still don’t get any new data, that is the condition table. But I also don’t get any error, the experiment runs smoothly until the end.

Btw, where exactly would I see this condition table when I will be finally able to get it?

Thanks a lot for your help!

The table appears within the Hdf5 file, so perhaps the table is actually appearing as it should, just not in the typical .csv file of a regular psychopy experiment. Did you check the box to also generate hdf5 files in properties → data?

If you have enabled the hdf5 output, you will find them next to the corresponding csv files. You can navigate them for visual inspection using HDFview, or just process them using h5py or other similar libraries/programs.
The structure is usually as follows:
image

You will find the table and its contents inside data collection → condition_variables, and (if you are also sending messages) the messages within data collection → events → experiment.

Hopefully this information is useful to you!

Hi, thank you so much.

I get the EXP_CV_1 file now, but it still doesn’t look like it’s supposed to I think. Ideally, I would want something like the data table you attached earlier in this post, with TRIAL_START and TRIAL_END included. Do I have to combine the createTrialHandlerRecordTable() and sendMessageEvent() to get that? If so, how?

My ultimate goal is to match all the eye-tracking samples I have in the hdf5 file with the relative trial start and end info, so I can do proper analyses on them.

Thank you for your help,
Manuel

Remember that the messages and the table are two separate things.
You have successfully created the table, and now what you need to do is to populate it with your data. You will need to call ioServer.addTrialHandlerRecord(thisTrial) at the end of your trial routine to update the table with the variables within your excel file. If you want to record trial start and end times you can call thisTrial[‘TRIAL_START’]=core.getTime() within the “begin routine” tab and thisTrial[‘TRIAL_END’]=core.getTime() within the “end routine” tab (previous to the addTrialHandlerRecord call).

Remember to include condition files that have columns named TRIAL_START and TRIAL_END if you do so.

Once you have all your trials with their start and end points labelled in the table you can simply slice your hdf5 eye-tracking data to the time bracket you want.

You could alternatively do this with the messages by using regex (e.g., the library re in python) to select the strings you sent as messages and the slicing the file between desired start and end times you sent as messages

1 Like

Hi @gartean.

I managed to get the conditions table in the end, thanks to your help. Thank you so much!