From ea6087c8bd2df357feabbe032e9881c1a9b8dba3 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 7 Jan 2015 17:31:25 -0500 Subject: [PATCH 1/9] Add third kind of run condition for blocks. Involved making new class hierarchy for RunIf. --- typescript/block.ts | 27 ++++++++++-------- typescript/experiment.ts | 2 +- typescript/record.ts | 59 ++++++++++++++++++++++++++++++++-------- typescript/runif.ts | 45 ++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 24 deletions(-) create mode 100644 typescript/runif.ts diff --git a/typescript/block.ts b/typescript/block.ts index a6c6b6d..b37ea59 100644 --- a/typescript/block.ts +++ b/typescript/block.ts @@ -2,6 +2,7 @@ /// /// /// +/// /// /// @@ -16,7 +17,20 @@ class Block{ constructor(jsonBlock, public container: Container){ jsonBlock = _.defaults(jsonBlock, {runIf: null, banks: {}}); - this.runIf = jsonBlock.runIf; // {pageID, optionID | regex} + this.runIf = jsonBlock.runIf; + if (jsonBlock.runIf){ + if (_.has(jsonBlock.runIf, 'optionID')){ + this.runIf = new RunIfSelected(jsonBlock.runIf.pageID, jsonBlock.runIf.optionID); + } else if (_.has(jsonBlock.runIf, 'regex')){ + this.runIf = new RunIfMatched(jsonBlock.runIf.pageID, jsonBlock.runIf.regex); + } else if (_.has(jsonBlock.runIf, 'permutation')){ + this.runIf = new RunIfPermutation(jsonBlock.runIf.permutation); + } else { + this.runIf = new RunIf(); + } + } else { + this.runIf = new RunIf(); + } this.id = jsonBlock.id; this.banks = shuffleBanks(jsonBlock.banks); this.oldContents = []; @@ -27,17 +41,8 @@ class Block{ run(nextUp, experimentRecord: ExperimentRecord){} - // whether this block should run, depending on a previous answer - shouldRun(experimentRecord: ExperimentRecord): boolean { - if (this.runIf){ - return experimentRecord.responseGiven(this.runIf); - } else { - return true; - } - } - advance(experimentRecord: ExperimentRecord): void { - if (!_.isEmpty(this.contents) && this.shouldRun(experimentRecord)){ + if (!_.isEmpty(this.contents) && this.runIf.shouldRun(experimentRecord)){ var nextUp = this.contents.shift(); this.oldContents.push(nextUp); this.run(nextUp, experimentRecord); diff --git a/typescript/experiment.ts b/typescript/experiment.ts index d0dda7a..aa3259e 100644 --- a/typescript/experiment.ts +++ b/typescript/experiment.ts @@ -31,7 +31,7 @@ class Experiment implements Container{ this.counterbalance = jsonExperiment.counterbalance; this.contents = makeBlocks(jsonExperiment.blocks, this); this.contents = orderBlocks(this.contents, this.exchangeable, this.permutation, this.counterbalance); - this.experimentRecord = new ExperimentRecord(psiturk); + this.experimentRecord = new ExperimentRecord(psiturk, permutation); this.banks = shuffleBanks(jsonExperiment.banks); } diff --git a/typescript/record.ts b/typescript/record.ts index 0e9d905..f2b9a25 100644 --- a/typescript/record.ts +++ b/typescript/record.ts @@ -38,10 +38,12 @@ class TrialRecord { class ExperimentRecord { private trialRecords; // {pageID: TrialRecord[]} private psiturk; + private permutation; - constructor(psiturk){ + constructor(psiturk, permutation){ this.psiturk = psiturk; this.trialRecords = {}; + this.permutation = permutation; } public addRecord(trialRecord: TrialRecord){ @@ -54,22 +56,55 @@ class ExperimentRecord { } } - public responseGiven(runIf){ - if (_.has(this.trialRecords, runIf.pageID)) { - var pageResponses: TrialRecord[] = this.trialRecords[runIf.pageID]; - var response: TrialRecord = _.last(pageResponses); - if (runIf.optionID){ - return _.contains(response.selectedID, runIf.optionID); - } else if (runIf.regex && response.selectedID.length === 1){ - return response.selectedText[0].search(runIf.regex) >= 0; - } else { - throw "runIf does not contain optionID or regex."; - } + getPermutation(){ + return this.permutation; + } + + private getLatestPageInfo(pageID: string): TrialRecord { + if (_.has(this.trialRecords, pageID)) { + var pageResponses: TrialRecord[] = this.trialRecords[pageID]; + return _.last(pageResponses); + } else { + return null; + } + } + + responseGiven(pageID: string, optionID: string): boolean { + var pageInfo = this.getLatestPageInfo(pageID); + if (pageInfo) { + return _.contains(pageInfo.selectedID, optionID); } else { return false; } } + textMatch(pageID: string, regex: string): boolean { + var pageInfo = this.getLatestPageInfo(pageID); + if (pageInfo && pageInfo.selectedID.length === 1) { + return pageInfo.selectedText[0].search(regex) >= 0; + } else { + return false; + } + } + + // public responseGiven(runIf){ + // if (_.has(this.trialRecords, runIf.pageID)) { + // var pageResponses: TrialRecord[] = this.trialRecords[runIf.pageID]; + // var response: TrialRecord = _.last(pageResponses); + // if (runIf.optionID){ + // return _.contains(response.selectedID, runIf.optionID); + // } else if (runIf.regex && response.selectedID.length === 1){ + // return response.selectedText[0].search(runIf.regex) >= 0; + // } else if (runIf.counterbalance){ + // return runIf.counterbalance === this.permutation; + // } else { + // throw "runIf does not contain optionID or regex."; + // } + // } else { + // return false; + // } + // } + public getBlockGrades(blockID: string): boolean[] { // get the last iteration of each page var recentRecords: TrialRecord[] = _.map(this.trialRecords, (trlist: TrialRecord[]) => { diff --git a/typescript/runif.ts b/typescript/runif.ts new file mode 100644 index 0000000..ad6ec5d --- /dev/null +++ b/typescript/runif.ts @@ -0,0 +1,45 @@ +/// +/// +/// +/// +/// +/// + +class RunIf{ + constructor(){} + shouldRun(experimentRecord: ExperimentRecord): boolean { + return true; + } +} + +class RunIfSelected extends RunIf{ + constructor(private pageID, private optionID){ + super(); + } + + shouldRun(experimentRecord: ExperimentRecord): boolean { + return experimentRecord.responseGiven(this.pageID, this.optionID); + } +} + +class RunIfMatched extends RunIf{ + constructor(private pageID, private regex){ + super(); + } + + shouldRun(experimentRecord: ExperimentRecord): boolean { + return experimentRecord.textMatch(this.pageID, this.regex); + } +} + +class RunIfPermutation extends RunIf{ + constructor(private permutation) { + super(); + } + + shouldRun(experimentRecord: ExperimentRecord){ + return experimentRecord.getPermutation() === this.permutation; + } +} + + From ce5c19c8dc59e5919c723c3852c7278250aee8ba Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 7 Jan 2015 19:01:39 -0500 Subject: [PATCH 2/9] Added treatments, third RunIf def to Python, JSON. Passed a simple unit test but need to expand example script. --- speriment/speriment.py | 49 ++++++++++++++++++++++++++++---- speriment/sperimentschema.json | 6 ++++ speriment/test/test_speriment.py | 15 +++++++++- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/speriment/speriment.py b/speriment/speriment.py index c74c54e..91e9483 100644 --- a/speriment/speriment.py +++ b/speriment/speriment.py @@ -50,14 +50,25 @@ def rename_key(self, dictionary, key, new_key): else: return dictionary + def compile_treatments(self, obj): + '''Treatments are lists of lists of blocks to run conditionally. Remove + this variable and add RunIf objects to those blocks.''' + for i, treatment in enumerate(obj.treatments): + for block in treatment: + block.run_if = RunIf(permutation = i) + del obj.treatments + return obj + def default(self, obj): if isinstance(obj, Component): + if hasattr(obj, 'treatments'): + obj = self.compile_treatments(obj) obj.validate() dict_copy = copy.deepcopy(obj.__dict__) # make keys follow JS conventions renamed_ls = self.rename_key(obj.__dict__, 'latin_square', 'latinSquare') renamed_ri = self.rename_key(renamed_ls, 'run_if', 'runIf') - renamed_id = self.rename_key(renamed_ls, 'id_str', 'id') + renamed_id = self.rename_key(renamed_ri, 'id_str', 'id') return renamed_id if isinstance(obj, RunIf): return obj.__dict__ @@ -122,7 +133,8 @@ def __exit__(self, etype, evalue, etrace): ### Special kind of experimental components (not in the hierarchy) class RunIf: - def __init__(self, page, option = None, regex = None): + def __init__(self, page = None, option = None, regex = None, permutation = + None): ''' page: Page, the Page to look at to see which answer was given. @@ -133,16 +145,22 @@ def __init__(self, page, option = None, regex = None): only run if a response matching a regular expression made from the string regex was given the last time page was displayed. - Exactly one of option and regex must be given. + permutation: integer, optional. If given, the block containing this + RunIf will only run if PsiTurk gives the experiment a permutation + variable matching this integer. This requires editing the counterbalance + setting in config.txt. The reason RunIfs depend on "the last time" their page was displayed is that Pages can display multiple times if they are in Blocks with a criterion.''' - self.page_id = page.id_str + if page != None: + self.page_id = page.id_str if option != None: self.option_id = option.id_str elif regex != None: self.regex = regex + if permutation != None: + self.permutation = permutation class SampleFrom: def __init__(self, bank): @@ -302,7 +320,7 @@ def validate(self): class Block(Component): def __init__(self, pages = None, groups = None, blocks = None, id_str = None, - exchangeable = [], counterbalance = [], latin_square = None, pseudorandom = None, **kwargs): + exchangeable = [], counterbalance = [], treatments = [], latin_square = None, pseudorandom = None, **kwargs): ''' Exactly one of pages, groups, and blocks must be provided. @@ -402,10 +420,28 @@ def __init__(self, pages = None, groups = None, blocks = None, id_str = None, self.pseudorandom = pseudorandom self.validate_pseudorandom() + # TODO what about mutated blocks + if treatments: + self.treatments = treatments + # for (i, treatment) in enumerate(treatments): + # for block in treatment: + # block.run_if = RunIf(permutation = i) + def validate(self): self.validate_contents() self.validate_pseudorandom() self.validate_latin_square() + self.validate_counterbalancing() + + def validate_counterbalancing(self): + if hasattr(self, 'counterbalance') and hasattr(self, 'treatments'): + print '''Warning: counterbalance and treatments depend on the same + variable, so using both in one experiment will cause + correlations between which blocks are used and how blocks are + ordered. If you want these to be decided independently, change + 'counterbalance' to 'exchangeable' so the order will be decided + randomly for each participant.''' + def validate_contents(self): content_types = [attribute for attribute in @@ -465,7 +501,8 @@ def __init__(self, blocks, exchangeable = [], counterbalance = [], banks = self.blocks = [b for b in blocks] if exchangeable: self.exchangeable = [b.id_str for b in exchangeable] - self.counterbalance = [b.id_str for b in counterbalance] + if counterbalance: + self.counterbalance = [b.id_str for b in counterbalance] if banks: self.banks = banks diff --git a/speriment/sperimentschema.json b/speriment/sperimentschema.json index 177f4cb..ddce9ba 100644 --- a/speriment/sperimentschema.json +++ b/speriment/sperimentschema.json @@ -138,6 +138,12 @@ } }, "required": ["pageID"] + }, + { + "properties": { + "permutation": "number" + }, + "required": ["permutation"] } ] }, diff --git a/speriment/test/test_speriment.py b/speriment/test/test_speriment.py index d48d06b..4a01f53 100644 --- a/speriment/test/test_speriment.py +++ b/speriment/test/test_speriment.py @@ -1,4 +1,5 @@ from speriment import * +import json def test_new(): with make_experiment(IDGenerator()): @@ -20,4 +21,16 @@ def test_get_dicts(): rows2 = get_dicts('speriment/test/comma_sep.csv') assert rows2 == answer - +def test_compile_treatments(): + with make_experiment(IDGenerator()): + b1 = Block(pages = []) + b2 = Block(pages = []) + b3 = Block(pages = []) + outer = Block(blocks = [b1, b2, b3], treatments = [[b1, b3], [b2]]) + exp = Experiment(blocks = [outer]) + json_exp = exp.to_JSON() + compiled_exp = json.loads(json_exp) + print compiled_exp + assert compiled_exp['blocks'][0]['blocks'][0]['runIf']['permutation'] == 0 + assert compiled_exp['blocks'][0]['blocks'][1]['runIf']['permutation'] == 1 + assert compiled_exp['blocks'][0]['blocks'][2]['runIf']['permutation'] == 0 From 8cd86dca293d595a33120ce27d72799f91c8d86c Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 7 Jan 2015 19:02:59 -0500 Subject: [PATCH 3/9] Add testing for RunIf changes. Just forgot to commit it before. --- test/testBlock.js | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/test/testBlock.js b/test/testBlock.js index ced891d..91bb8de 100644 --- a/test/testBlock.js +++ b/test/testBlock.js @@ -52,14 +52,14 @@ test("create inner block", function(){ ok(p instanceof Question, "should be a Question"); } }); - strictEqual(b.runIf, null, "runIf default not working properly"); + strictEqual(b.runIf.shouldRun({}), true, "runIf defaults to returning true"); var er = new ExperimentRecord(); - strictEqual(b.shouldRun(er), true, "shouldRun not defaulting to true"); + strictEqual(b.runIf.shouldRun(er), true, "shouldRun not defaulting to true"); jsonb.runIf = {"pageID": "p2", "optionID": "o3"}; var b2 = new InnerBlock(jsonb, fakeContainer); strictEqual(b2.runIf.optionID, "o3", "runIf not set properly"); er.addRecord("p2", {'blockID': 'b1', 'startTime': 0, 'endTime': 0, 'selected': ['o1'], 'correct': 'o1'}); - strictEqual(b2.shouldRun(er), false, "shouldRun not working"); + strictEqual(b2.runIf.shouldRun(er), false, "shouldRun not working"); var jsongroup = {id: "b1", groups:[[{text: "page1", id: "p1"}, {text:"page2", id:"p2", options: [{id: "o1"}]}]]}; var b3 = new InnerBlock(jsongroup, {version: 0, containerIDs: []}); strictEqual(b3.contents.length, 1, "initialization from groups"); @@ -391,7 +391,7 @@ test('create outerblock', function(){ var b = new OuterBlock(jsonb, fakeContainer); strictEqual(b.id, 'b1', 'block id should be set'); strictEqual(b.exchangeable.length, 0, 'exchangeable default should be set'); - strictEqual(b.runIf, null, 'runIf default should be set'); + strictEqual(b.runIf.shouldRun(), true, 'runIf default should be set'); strictEqual(b.contents.length, 2, 'contents should be set'); ok(b.contents[0] instanceof InnerBlock, 'contents should be InnerBlock'); @@ -400,9 +400,9 @@ test('create outerblock', function(){ var b2 = new OuterBlock(jsonb2, fakeContainer); deepEqual(b2.exchangeable, ['b3', 'b4'], 'exchangeable should be set when passed in'); - jsonb2.runIf = 'o1'; + jsonb2.runIf = {'pageID': 'p1', 'optionID': 'o1'}; var b3 = new OuterBlock(jsonb2, fakeContainer); - strictEqual(b3.runIf, 'o1', 'runIf should be set when passed in'); + strictEqual(b3.runIf.optionID, 'o1', 'runIf should be set when passed in'); }); @@ -571,7 +571,7 @@ test('run blocks conditionally: when text has been entered', function(){ $('#o1').val('hello'); clickNext(); // b2 should run - ok(runBoth.experimentRecord.responseGiven({'pageID': 'p1', 'regex': 'hello'}), 'record should note that a matching response was given'); + ok(runBoth.experimentRecord.textMatch('p1', 'hello'), 'record should note that a matching response was given'); strictEqual($('p.question').text(), 'page2', 'block 2 runs because hello was given as text answer'); cleanUp(); @@ -584,12 +584,27 @@ test('run blocks conditionally: when condition is unsatisfied', function(){ var runOne = new Experiment({blocks: [b2, b1]}, 0); runOne.start(); - clickNext(); //breakoff notice + clickNext(); ok(_.contains(['page1', 'page2'], $('p.question').text()), 'b1 should run, skipping b2 because o1 has not been chosen'); cleanUp(); }); +test('run blocks conditionally: based on permutation', function(){ + var b1 = {id: 'b1', pages: pgs, runIf: {permutation: 0}}; + var b2 = {id: 'b2', pages: pgs2}; + + var runOne = new Experiment({blocks: [b1, b2]}, 0, 1); + runOne.start(); + ok(_.contains(['page3', 'page4'], $('p.question').text()), "b2 should run because b1's runIf is not satisfied"); + cleanUp(); + + var runBoth = new Experiment({blocks: [b1, b2]}, 0, 0); + runBoth.start(); + ok(_.contains(['page1', 'page2'], $('p.question').text()), 'b1 should run because its runIf is satisfied'); + cleanUp(); +}); + var ps = [{id: 'p1', text: 'page1', options: [{id: 'o1', text:'A', correct:true}, {id:'o2', text:'B', correct:false}]}, {id: 'p2', text:'page2', options: [{id: 'o1', text:'A', correct:true}, {id:'o2', text:'B', correct:false}]}]; From 075ef3daa28549f6b4670fc8158192e6fe5177cd Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 9 Jan 2015 02:39:42 -0500 Subject: [PATCH 4/9] Fix #17 and #18. Needed to parse strings as integers to use condition and counterbalance. --- typescript/experiment.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typescript/experiment.ts b/typescript/experiment.ts index aa3259e..39a15f7 100644 --- a/typescript/experiment.ts +++ b/typescript/experiment.ts @@ -25,13 +25,13 @@ class Experiment implements Container{ constructor(jsonExperiment, version, permutation, psiturk){ jsonExperiment = _.defaults(jsonExperiment, {exchangeable: [], counterbalance: [], banks: {}}); - this.version = version; - this.permutation = permutation; + this.version = parseInt(version); + this.permutation = parseInt(permutation); this.exchangeable = jsonExperiment.exchangeable; this.counterbalance = jsonExperiment.counterbalance; this.contents = makeBlocks(jsonExperiment.blocks, this); this.contents = orderBlocks(this.contents, this.exchangeable, this.permutation, this.counterbalance); - this.experimentRecord = new ExperimentRecord(psiturk, permutation); + this.experimentRecord = new ExperimentRecord(psiturk, this.permutation); this.banks = shuffleBanks(jsonExperiment.banks); } From d5cb145dcb94f5fad4a8f5df281c33f87e45cda2 Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 9 Jan 2015 02:42:14 -0500 Subject: [PATCH 5/9] Fix #15, fix #16. --- speriment/speriment.py | 45 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/speriment/speriment.py b/speriment/speriment.py index 91e9483..99b77a6 100644 --- a/speriment/speriment.py +++ b/speriment/speriment.py @@ -64,14 +64,15 @@ def default(self, obj): if hasattr(obj, 'treatments'): obj = self.compile_treatments(obj) obj.validate() - dict_copy = copy.deepcopy(obj.__dict__) # make keys follow JS conventions renamed_ls = self.rename_key(obj.__dict__, 'latin_square', 'latinSquare') renamed_ri = self.rename_key(renamed_ls, 'run_if', 'runIf') renamed_id = self.rename_key(renamed_ri, 'id_str', 'id') return renamed_id if isinstance(obj, RunIf): - return obj.__dict__ + renamed_option = self.rename_key(obj.__dict__, 'option_id', 'optionID') + renamed_page = self.rename_key(renamed_option, 'page_id', 'pageID') + return renamed_page if isinstance(obj, SampleFrom): return {"sampleFrom": obj.bank} # Let the base class default method raise the TypeError @@ -418,7 +419,6 @@ def __init__(self, pages = None, groups = None, blocks = None, id_str = None, if pseudorandom: self.pseudorandom = pseudorandom - self.validate_pseudorandom() # TODO what about mutated blocks if treatments: @@ -451,25 +451,26 @@ def validate_contents(self): and blocks.''' def validate_pseudorandom(self): - if hasattr(self, 'groups'): - if self.latin_square == False: - raise ValueError, '''Can't choose pages from groups randomly and - ensure that pseudorandomization will work. Supply pages instead of - groups, change latin_square to True, or change pseudorandom to - False.''' - try: - conditions = [page.condition for group in self.groups for page in - group] - except AttributeError: - raise ValueError, '''Can't pseudorandomize pages without - conditions.''' - cond_counter = Counter(conditions) - cond_counts = cond_counter.values() - num_cond_counts = len(set(cond_counts)) - if num_cond_counts != 1: - raise ValueError, '''Can't pseudorandomize pages if not all - conditions are represented the same number of times in the - block.''' + if hasattr(self, 'pseudorandom'): + if hasattr(self, 'groups'): + if self.latin_square == False: + raise ValueError, '''Can't choose pages from groups randomly and + ensure that pseudorandomization will work. Supply pages instead of + groups, change latin_square to True, or change pseudorandom to + False.''' + try: + conditions = [page.condition for group in self.groups for page in + group] + except AttributeError: + raise ValueError, '''In block {0}, can't pseudorandomize pages without + conditions.'''.format(self.id_str) + cond_counter = Counter(conditions) + cond_counts = cond_counter.values() + num_cond_counts = len(set(cond_counts)) + if num_cond_counts != 1: + raise ValueError, '''Can't pseudorandomize pages if not all + conditions are represented the same number of times in the + block.''' #TODO elif hasattr('pages') def validate_latin_square(self): From 463b764f40097d2ac8bfa6a5a190892f01b5642c Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 9 Jan 2015 02:43:49 -0500 Subject: [PATCH 6/9] Fix permutation branch of runIf. --- speriment/sperimentschema.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/speriment/sperimentschema.json b/speriment/sperimentschema.json index ddce9ba..4fb6a51 100644 --- a/speriment/sperimentschema.json +++ b/speriment/sperimentschema.json @@ -141,7 +141,9 @@ }, { "properties": { - "permutation": "number" + "permutation": { + "type": "number" + } }, "required": ["permutation"] } From 844d130a0c6c3661947014fe1d2a56336e7363e3 Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 9 Jan 2015 02:45:03 -0500 Subject: [PATCH 7/9] Add LatinSquare testing for debugging. --- test/testBlock.js | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/testBlock.js b/test/testBlock.js index 91bb8de..9df9da9 100644 --- a/test/testBlock.js +++ b/test/testBlock.js @@ -111,6 +111,47 @@ test("choosing pages", function(){ ok(_.every(condgroups4, function(g){return g.length === 2;}), "check latin square"); }); +test("test latin square", function(){ + var gps = [[{id: '1a', text: '1a'}, {id: '1b', text: '1b'}], [{id: '2a', text: '2a'}, {id: '2b', text: '2b'}]]; + var b1 = new InnerBlock({id: 'b1', groups: gps, latinSquare: true}, {version: 0, containerIDs: []}); + var ids = _.pluck(b1.contents, 'id'); + ids.sort(); + ok(_.isEqual(ids, ['1a', '2b']), 'Latin Square should work on version 0.'); + var b2 = new InnerBlock({id: 'b2', groups: gps, latinSquare: true}, {version: 1, containerIDs: []}); + var ids2 = _.pluck(b2.contents, 'id'); + ids2.sort(); + ok(_.isEqual(ids2, ['1b', '2a']), 'Latin Square should work on version 1.'); + + var jb3 = { + "latinSquare": true, + "groups": [ + [ + { + "text": "1A", + "id": "15" + }, + { + "text": "1B", + "id": "16" + } + ], + [ + { + "text": "2A", + "id": "17" + }, + { + "text": "2B", + "id": "18" + } + ] + ], + "id": "19" + }; + var b3 = new InnerBlock(jb3, {version: 0, containerIDs: []}); + ok(_.isEqual(_.pluck(b3.contents, 'text'), ['1A', '2B']), 'latin square should work on excerpt from example JSON'); +}); + test("ordering pages", function(){ // groups with three conditions each, in the same order var grps = _.map(_.range(6), function(i){ @@ -384,6 +425,7 @@ var jsons = {blocks:[ ] }; test('create outerblock', function(){ + setupForm(); var jsonb = {id:'b1', blocks:[ { id:'b3', pages: pgs2 }, { id:'b4', pages: pgs3 } @@ -403,6 +445,21 @@ test('create outerblock', function(){ jsonb2.runIf = {'pageID': 'p1', 'optionID': 'o1'}; var b3 = new OuterBlock(jsonb2, fakeContainer); strictEqual(b3.runIf.optionID, 'o1', 'runIf should be set when passed in'); + + b.advance(new ExperimentRecord()); + //p3 + clickNext(); + clickNext(); + //p4 + clickNext(); + clickNext(); + //p5 + clickNext(); + clickNext(); + //p6 + clickNext(); + throws(clickNext, CustomError, "should call container's advance"); + cleanUp(); }); From 71b270dec9ee52187d59dadd6fffea313cbf27c7 Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 9 Jan 2015 02:45:19 -0500 Subject: [PATCH 8/9] Rewrite example script. --- doc/example.py | 101 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 85 insertions(+), 16 deletions(-) diff --git a/doc/example.py b/doc/example.py index fe0480c..d060602 100644 --- a/doc/example.py +++ b/doc/example.py @@ -1,9 +1,12 @@ -from speriment import * # get access to the module +##### Import the module ###### +from speriment import * # import * is generally bad practice, but Speriment scripts are unlikely to get # complicated enough that you would have clashing variable names in your # namespace. But, you can always do "import speriment" and use names like # speriment.Block instead of Block. +##### IDs ####### + # Every option, page, and block of your experiment needs an ID. There are two ways to get # them: # 1. Use this "with" statement to generate them magically, and indent ALL @@ -13,6 +16,8 @@ # pass in other information, like: page1 = Page('hello', ID = row[0]) with make_experiment(IDGenerator()): + ####### Read in your materials ####### + # Imagine you have two csv files, items1.csv, which looks like this: # What is your favorite animal?,cat,dog,animate-condition @@ -30,6 +35,8 @@ # This is how to read in items2.csv items2 = get_dicts('items2.csv') + ###### Create experiment components. ##### + # You can use a loop or list comprehension to make pages from your items. # after using get_rows, access cells with indices, starting from 0 @@ -51,11 +58,34 @@ block1 = Block(pages = pages1) + ###### Make blocks run conditionally (RunIf) ####### + # I'll make the second block run only for those participants who answer "cat" to # the animal question in block1 animal_question = pages1[0] cat_condition = RunIf(page = animal_question, option = animal_question.options[0]) - block2 = Block(pages = pages2, run_if = cat_condition) + conditional_block = Block(pages = pages2, run_if = cat_condition) + + ###### Make a Latin Square (groups, latin_square) ##### + + # Normally blocks have pages. Latin square blocks have groups, which are + # lists of pages. + + page_1a = Page("1A") + page_1b = Page("1B") + page_2a = Page("2A") + page_2b = Page("2B") + + latin_square_block = Block(groups = [[page_1a, page_1b], [page_2a, + page_2b]], latin_square = True) + + # Now, half your participants will see page_1a and page_2b, and half will + # see page_1b and page_2a. The order of the pages will be shuffled as usual. + + # If you use groups and latin_square is False, pages will be chosen from + # them randomly rather than according to a Latin Square. + + ###### Create components differently across participants (SampleFrom) ###### # Let's make a block where the texts of pages are combined with the other # data for the page differently across participants. We need to replace the text string @@ -68,48 +98,87 @@ sampled_pages = [Page(SampleFrom('bank1'), condition = row['Condition']) for row in items2] - block3 = Block(pages = sampled_pages, banks = {'bank1': ['sampled1', + sampling_block = Block(pages = sampled_pages, banks = {'bank1': ['sampled1', 'sampled2']}) + ####### Copy and tweak components without messing up IDs (new) ###### + # Now I want to make another block just like block 1, and then just tweak it # a little bit. # The "new" method ensures they get separate IDs, which can be important for how # the experiment runs. Do this whenever you copy an option, page, or block # if you're using the "with" statement. - block4 = block1.new() + copied_block = block1.new() # I just want block4 to have one more page. This page doesn't have options, # which is fine; it'll just show some text. - block4.pages.append(Page('This is almost the last block.')) + copied_block.pages.append(Page('This is an extra page.')) + + ####### Change order or choice of blocks across participants (exchangeable, + ####### counterbalance, treatments) ##### + + # So now block1 and copied_block are mostly the same. Maybe we want to show + # half the participants block1 and half copied_block, like this: + + block_of_blocks = Block(blocks = [block1, copied_block], treatments = + [[block1], [copied_block]]) + + # This means that in treatment 1, block1 will show, and in treatment 2, + # copied_block will show. We could have put other blocks in these + # treatments, or had blocks in block_of_blocks that aren't in any treatments + # and thus show for all participants. + + # They don't have to be adjacent or in the same larger block for this to work - we could + # have put the treatments argument on the entire Experiment. + + # The same rules apply to the exchangeable argument and the counterbalance + # argument. If you want two or more blocks to switch places with each other + # across participants, you can do: + + alternative_block_of_blocks = Block(blocks = [block1, copied_block], + counterbalance = [block1, copied_block]) + + # or the same with exchangeable instead of counterbalance. Exchangeable + # decides the order randomly. Counterbalance decides deterministically, so + # you'll get a more even distribution across participants. Counterbalance + # and treatments use the same variable to make decisions, so you probably + # don't want to use them in the same experiment. This variable is based on + # num_counters in config.txt, so make sure to set it if you use + # counterbalance or treatments. + + # Note that if we use block_of_blocks in the experiment, the animal_question + # that conditional_block depends on might not ever show! The copied version + # of it is not the same as the original. If it doesn't show, then + # conditional_block will not show either. + + ####### Control the order of pages via blocks ####### # That page will occur somewhere in block 4, but we don't know exactly where. # Blocks stay put unless they're exchangeable, but questions move around in # their blocks. Here's a block with just one page so we know it'll come last. - block5 = Block(pages = [Page('Goodbye!')]) + last_block = Block(pages = [Page('Goodbye!')]) + + ###### Make an Experiment ###### # Finally, wrap the Blocks in an Experiment. Remember that Pages take an # optional list of Options, Blocks take a list of Pages (or a list of lists of # Pages, or a list of Blocks), and Experiments take a list of Blocks. - # The counterbalance argument says that block1 and block4 will switch places - # for approximately half of participants. It needs to be used in conjunction - # with setting the counterbalance parameter in PsiTurk's config.txt, - # whereas the exchangeable argument could be used in the same way but - # without setting that parameter (but it will accordingly give a less even - # distribution across participants). - experiment = Experiment([block1, block2, block3, block4, block5], counterbalance = - [block1, block4]) + experiment = Experiment([block_of_blocks, + #block1, + latin_square_block, conditional_block, sampling_block, + last_block]) # You can generate the JSON just to look at it, for instance by printing this # variable. This step is optional. exp_json = experiment.to_JSON() - # Finally, run this line to make sure your experiment is written properly, - # convert it to JSON, write it to a file, and tell PsiTurk where to find + # This line checks that your experiment is written properly, + # converts it to JSON, writes it to a file, and tells PsiTurk where to find # Speriment and your JSON. Just make up a name for this experiment, which # will be used to name the JSON object and the JavaScript file it's stored # in. Make sure to run this script in the top level of your PsiTurk project From 6ce298cdae90d6b6fb8f64a215e817665f1502fb Mon Sep 17 00:00:00 2001 From: Presley Date: Fri, 9 Jan 2015 16:24:44 -0500 Subject: [PATCH 9/9] Fix bug in test. --- test/testBlock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/testBlock.js b/test/testBlock.js index 9df9da9..3145be2 100644 --- a/test/testBlock.js +++ b/test/testBlock.js @@ -149,7 +149,7 @@ test("test latin square", function(){ "id": "19" }; var b3 = new InnerBlock(jb3, {version: 0, containerIDs: []}); - ok(_.isEqual(_.pluck(b3.contents, 'text'), ['1A', '2B']), 'latin square should work on excerpt from example JSON'); + ok(_.isEqual(_.pluck(b3.contents, 'text').sort(), ['1A', '2B']), 'latin square should work on excerpt from example JSON'); }); test("ordering pages", function(){