Manipulating a PennController table?

PennController for IBEX Forums Support Manipulating a PennController table?

This topic contains 4 replies, has 2 voices, and was last updated by Avatar MiekeS 2 weeks, 2 days ago.

Viewing 5 posts - 1 through 5 (of 5 total)
  • Author
    Posts
  • #5185
    Avatar
    MiekeS
    Participant

    Hi Jeremy,

    I am currently working on an experiment that has needs a tricky randomization of the trials. There are 54 trials, and these need to be distributed in two blocks (one of 30 trials and one of 24 trials). Then, within each block, each trials needs to be assigned to a ‘Prime Condition’ condition (which has two levels), and a ‘Prime Quantifier’ condition (which has two levels in one block, and three levels in the other).

    Ideally, the trials are randomized for each participant. I have managed to write a js script that seems to work (see https://github.com/MiekeSlim/RandomizationTest/blob/master/chunk_includes/RandomPrimeTrials.js). In this script, I created a toy set of trials (just for testing), stored in an array. Then, I loop over this array a couple of times to add the block and condition info to each trial.

    Eventually, this array with trials needs to be replaced by an object type that can be read by the PennController template function. I’m guessing this should be a PennController table. My question is, is it also possible to perform such manipulations on an Penncontroller Table (or, if not, is there a way to convert arrays into a PennController table?). Thus, would the script I wrote work with a PennController table?

    Thank you!

    Best,
    Mieke

    #5186
    Jeremy
    Jeremy
    Keymaster

    Hi Mieke,

    Yes, you can definitely accomplish what you want with PennController, but I think you’d need to take a slightly different approach. Let me give you an example, starting with a table of this format (of course you can use a CSV file directly instead of using AddTable):

    AddTable("myTable", `Item,Sentence,Group
    0,tree lost its leaves,possessive
    0,tree fell on itself,reflexive
    1,pond had its water drained,possessive
    1,pond replenished itself,reflexive
    2,cloud kept its shape,possessive
    2,cloud stood by itself,reflexive
    3,rock had its top smoothed,possessive
    3,rock spun on itself,reflexive
    4,hill hid a cave at its top,possessive
    4,hill hid on itself,reflexive`)

    Here I have 5 trials, numbered from 0 to 4, which appear in one of two conditions (possessive vs reflexive) every other time the experiment is run, because of the Group column. Nothing crucial relies on this, it’s just for the sake of illustration.

    Now I’ll define some variables in the script to configure the design you describe:

    firstBlockLength = 2
    secondBlockLength = 3
    primes = "EU"
    firstBlockQuantifiers = "EU"
    secondBlockQuantifiers = "EUN"

    I’m saying: there will be 2 trials in the first block and 3 trials in the second (you would replace those numbers with 30 and 24 instead). I’m also saying: each prime (regardless of block) will be either ‘E’ (for existential) or ‘U’ (for universal), and each in-sentence quantifier in the first block will be either ‘E’ (to be replaced with the determiner “A”) or ‘U’ (to be replaced with “Every”) and each in-sentence quantifier in the second block will be ‘E,’ ‘U’ or ‘N’ (to be replaced with “No”).

    Note: the numbers in my example are not ideal, since the second block has 3 trials over which you cannot properly distribute 2 prime types—just make sure the number of trials in each block is a multiple of the number of prime types and the numbers of quantifiers in both trials; 24 and 30 both being multiples of both 2 and 3, you should be fine

    Now here’s some javascript magic to automatically associate your trials with the desired conditions:

    // Generates a random, even array of LENGTH characters picked from a string
    randomArray = (length,list) => list.split('').map(v=>v.repeat(parseInt(length/list.length)))
                            .flat(1).sort(v=>Math.random()>=0.5)
    
    listOfPrimesFirst = randomArray(firstBlockLength,primes)
    listOfPrimesSecond = randomArray(secondBlockLength,primes)
    listOfQuantifiersFirst = randomArray(firstBlockLength,firstBlockQuantifiers)
    listOfQuantifiersSecond = randomArray(secondBlockLength,secondBlockQuantifiers)
    
    translator = {E: "One", U: "Every", N: "No"}
    // Randomly order the indices first
    indices = [...new Array(5)].map((v,i)=>i).sort(v=>Math.random()>=0.5)
    // Now assign properties to trials 
    trials = []
    listOfQuantifiersFirst.map( (v,i) => trials[indices[i]] = {
        block: "first",
        prime: translator[listOfPrimesFirst[i]], 
        quantifier: translator[v]
    } )
    listOfQuantifiersSecond.map( (v,i) => trials[indices[firstBlockLength+i]] = {
        block: "second", 
        prime: translator[listOfPrimesSecond[i]], 
        quantifier: translator[v]
    } )

    Once this is in place, you can use Template and GetTable().filter to generate trials:

    Template(
        // Only use the rows where Item appears in the indices of the first block
        GetTable("myTable").filter( row=>indices.slice(0,firstBlockLength).indexOf(Number(row.Item))>=0 ) 
        ,
        row => newTrial( 'firstblock' ,
            newText("space","Please press Space").print(),
            newKey(" ").wait(),
            getText("space").remove()
            ,
            newText( "prime" , trials[row.Item].prime ).log().print(),
            newTimer(20).start().log().wait(),  // 20ms prime
            getText("prime").remove()
            ,
            newController("DashedSentence", {s: trials[row.Item].quantifier+' '+row.Sentence})
                .print().log().wait().remove()
        )
        .log( "Group" , row.Group )
        .log( "Block" , trials[row.Item].block )
        .log( "Sentence" , row.Sentence )
        .log( "Prime" , trials[row.Item].prime )
        .log( "Quantifier" , trials[row.Item].quantifier )
    )
    
    
    Template(
        // Only use the rows where Item appears in the indices of the second block
        GetTable("myTable").filter( row=>indices.slice(firstBlockLength,).indexOf(Number(row.Item))>=0 ) 
        ,
        row => newTrial( 'secondblock' ,
            newButton("Please click here").print().wait().remove()
            ,
            newText( "prime" , trials[row.Item].prime ).log().print(),
            newTimer(10).start().log().wait(),  // 10ms prime (more-or-less, hardware constraints)
            getText("prime").remove()
            ,
            newController("DashedSentence", {s: trials[row.Item].quantifier+' '+row.Sentence})
                .print().log().wait().remove()
        )
        .log( "Group" , row.Group )
        .log( "Block" , trials[row.Item].block )
        .log( "Sentence" , row.Sentence )
        .log( "Prime" , trials[row.Item].prime )
        .log( "Quantifier" , trials[row.Item].quantifier )
    )

    Let me know if you have questions about adapting this to your case

    Jeremy

    #5188
    Avatar
    MiekeS
    Participant

    Hi Jeremy,

    Thank you very much for your quick and elaborate response! I hadn’t realised that you can retrieve elements from an array with the indices/row numbers within the template/getTable commands (even though it makes a lot of sense ;)). This does the trick perfectly for me!

    I do have one follow-up (for now at least). The order of the blocks needs to counterbalanced between the participants. So, half of the participants will see Block1 first, and the other half will see Block 2 first. In Python/Psychopy, I usually do this with the participant number (which is assigned to them based on their order of participation): those with an even participant number are assigned to version 1, and those with an uneven participant number are assigned to version 2. I was thinking of doing the same thing here, but then based on the value of the counter. Then, I could do something like this (here given in nice pseudo-code):

    if (counter%2 = 0){
        Sequence("block1", "block2")
        }
    else{
        Sequence("block2", "block1")
        }

    However, then I would need to retrieve the value from the counter. Is that possible, or wouldn’t such an approach work?

    Thanks again!

    Mieke

    #5193
    Jeremy
    Jeremy
    Keymaster

    Hi Mieke,

    You could test __counter_value_from_server__ in your Template command and assign labels accordingly: Template( row => newTrial( __counter_value_from_server__%2?"secondblock":"firstblock" , /* ... */ ) )

    Note: __counter_value_from_server__ is only defined after your script has been evaluated, so you cannot use it outside Template (whose execution is delayed to fetch the tables)

    Jeremy

    #5238
    Avatar
    MiekeS
    Participant

    Hi Jeremy,

    A bit late, but thanks for the suggestion, I’ll try it out! 🙂

    Best,
    Mieke

Viewing 5 posts - 1 through 5 (of 5 total)

You must be logged in to reply to this topic.