Skip to content

refactor behavior to isolate intra-trial logic from trial selection logic #104

@neuromusic

Description

@neuromusic

every Trial instance should be a self-contained object that runs one trial of an experiment.

each trial will be initialized with the minimal set of conditions needed to provide a stimulus and consequate a response.

for example, a trial that plays stimulus 'a.wav' and provides a feed would be initialized with something like...

trial = Trial(panel=panel,
              stimulus='a.wav',
              consequences={
                  'left': {
                      'feed': 2.0,
                      'flash': 1.0,
                      'timeout': False,
                      },
                  'right': {
                      'feed': False,
                      'flash': False,
                      'timeout': 10.0
                      },
                  },
              )

The consequence argument takes a nested dictionary of the form {response:{consequence:value}}

This design relies on a couple of assumptions

  • there are a limited number of possible responses and these are known to the Trial
  • there are a limited number of possible consequences and these are known to the Trial

This means that the Trial object needs to know:

  • what to do with the stimulus
  • what to do with each response's consequence:value pair.

Once a trial is initialized, it would be run with trial.run(), which would execute the trial and save the relevant outcomes to trial attributes. The rest of the script would then access these values in order to save the trial.

This structure has a few advantages:

  1. This structure moves the logic of checking the trial "type" (correction, probe, etc) OUT of the trial execution and into the experimental trial generation logic. This means that we will need to know how we will want to consequate this trial before this trial starts.
  2. Isolating the intra-trial execution should allow us to more readily design new Trial classes for more complex interactions while using existing trial generation logic or visa versa.
  3. Further down the line, this may even allow a Trial object to simply send of relevant parameters to an Arduino or other embedded system that maintains the trial logic.

Pushing the consequation logic out of the trial execution changes the way we think about consequences a little bit. For example, a "correction" trial (where there is no feed for a correct response, but still a secondary reinforcer) would be initialized like...

trial = Trial(panel=panel,
              stimulus='a.wav',
              consequences={
                  'left': {
                      'feed': False,
                      'flash': 1.0,
                      'timeout': False,
                      },
                  'right': {
                      'feed': False,
                      'flash': False,
                      'timeout': 10.0
                      },
                  },
              )

on the other hand, a probe trial might be reinforced with...

trial = Trial(panel=panel,
              stimulus='a.wav',
              consequences={
                  'left': {
                      'feed': 2.0,
                      'flash': 1.0,
                      'timeout': False,
                      },
                  'right': {
                      'feed': 2.0,
                      'flash': 1.0,
                      'timeout': False,
                      },
                  },
              )

More complex Trial objects might need a more complex set of arguments than just "stimulus"

trial = Trial(panel=panel,
              stim='a.wav',
              target='b.wav',
              cue='blue',
              consequences={
                  'left': {
                      'feed': 2.0,
                      'flash': 1.0,
                      'timeout': False,
                      },
                  'right': {
                      'feed': 2.0,
                      'flash': 1.0,
                      'timeout': False,
                      },
                  },
              )

Concerns:

@MarvinT wants callbacks. I really think that the trial object should not be calling back out of the trial, but there might be a need, e.g., to make a feed duration depend on reaction time in a graded fashion.

This could work by allowing a callback that takes the trial as an argument to be passed.

NORMAL_CORRECT = {'feed': 2.0,'flash': 1.0,'timeout': False}
NORMAL_WRONG = {'feed': False,'flash': False,'timeout': 10.0}

def normal_left_consequences(response,trial):
    if response =='left':
        return NORMAL_CORRECT
    elif response=='right':
        return NORMAL_WRONG

class Trial(object):
    def __init__(self, panel, stimulus, consequences):
        self.consequences = consequences
    def consequate(self):
        return self.consequences(self.response,self)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions