diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 587ef28df..46deb82f5 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,10 +2,25 @@ ## Current Release +### New Levels: + +1. DMLab-30. + + 1. contributed/dmlab30/psychlab_arbitrary_visuomotor_mapping + 2. contributed/dmlab30/psychlab_continuous_recognition + +2. Psychlab. + + 1. contributed/psychlab/arbitrary_visuomotor_mapping + 2. contributed/psychlab/continuous_recognition + ### New Features: 1. Support for level caching for improved performance in the Python module. 2. Add the ability to spawn pickups dynamically at arbitrary locations. +3. Add implementations to read datasets including Cifar10 and Stimuli. +4. Add the ability to specify custom actions via 'customDiscreteActionSpec' and + 'customDiscreteAction' callbacks. ### Bug Fixes: diff --git a/game_scripts/factories/psychlab/arbitrary_visuomotor_mapping_factory.lua b/game_scripts/factories/psychlab/arbitrary_visuomotor_mapping_factory.lua new file mode 100644 index 000000000..34b009a03 --- /dev/null +++ b/game_scripts/factories/psychlab/arbitrary_visuomotor_mapping_factory.lua @@ -0,0 +1,454 @@ +--[[ Copyright (C) 2018 Google Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +]] + +local brady_konkle_oliva2008 = require 'datasets.brady_konkle_oliva2008' +local game = require 'dmlab.system.game' +local helpers = require 'common.helpers' +local psychlab_factory = require 'factories.psychlab.factory' +local psychlab_helpers = require 'factories.psychlab.helpers' +local point_and_click = require 'factories.psychlab.point_and_click' +local random = require 'common.random' +local tensor = require 'dmlab.system.tensor' + +--[[ This task goes by various names in the literature, they include: + +---- conditional visuomotor learning +---- conditional discrimination +---- arbitrary visuomotor mapping + +See Wise & Murray. Trends Neurosci. (2000) 23, 271-276. + +It can be seen as a special kind of cued recall task where the item to be +be recalled is the response itself. That is, the associations to be learned in +this task are between images and responses (locations to point at). + +This experiment measures how many arbitrary visuomotor maps can be learned and +maintained in memory for the duration of an episode. + +The specific image-response pairs to be remembered are generated anew for each +episode. +]] + +local TIME_TO_FIXATE_CROSS = 1 -- in frames +local FAST_INTER_TRIAL_INTERVAL = 1 -- in frames +local SCREEN_SIZE = {width = 512, height = 512} +local BG_COLOR = {255, 255, 255} +local TRIALS_PER_EPISODE_CAP = 60 + +local TARGET_SIZE = 0.75 + +local FIXATION_REWARD = 0 +local CORRECT_REWARD = 1 +local INCORRECT_REWARD = 0 + +local FIXATION_SIZE = 0.1 +local FIXATION_COLOR = {255, 0, 0} -- RGB +local CENTER = {0.5, 0.5} +local BUTTON_SIZE = 0.1 +local BUTTONS = {'north', 'south', 'east', 'west'} + +local BUTTON_POSITIONS = { + north = psychlab_helpers.getUpperLeftFromCenter( + {CENTER[1], BUTTON_SIZE / 2}, BUTTON_SIZE), + south = psychlab_helpers.getUpperLeftFromCenter( + {CENTER[1], 1 - BUTTON_SIZE / 2}, BUTTON_SIZE), + east = psychlab_helpers.getUpperLeftFromCenter( + {BUTTON_SIZE / 2, CENTER[2]}, BUTTON_SIZE), + west = psychlab_helpers.getUpperLeftFromCenter( + {1 - BUTTON_SIZE / 2, CENTER[2]}, BUTTON_SIZE) +} + +local factory = {} + +function factory.createLevelApi(kwargs) + kwargs.timeToFixateCross = kwargs.timeToFixateCross or TIME_TO_FIXATE_CROSS + kwargs.fastInterTrialInterval = kwargs.fastInterTrialInterval or + FAST_INTER_TRIAL_INTERVAL + kwargs.screenSize = kwargs.screenSize or SCREEN_SIZE + kwargs.bgColor = kwargs.bgColor or BG_COLOR + kwargs.trialsPerEpisodeCap = kwargs.trialsPerEpisodeCap or + TRIALS_PER_EPISODE_CAP + kwargs.targetSize = kwargs.targetSize or TARGET_SIZE + kwargs.fixationReward = kwargs.fixationReward or FIXATION_REWARD + kwargs.correctReward = kwargs.correctReward or CORRECT_REWARD + kwargs.incorrectReward = kwargs.incorrectReward or INCORRECT_REWARD + kwargs.fixationSize = kwargs.fixationSize or FIXATION_SIZE + kwargs.fixationColor = kwargs.fixationColor or FIXATION_COLOR + kwargs.center = kwargs.center or CENTER + kwargs.buttonSize = kwargs.buttonSize or BUTTON_SIZE + kwargs.buttons = kwargs.buttons or BUTTONS + kwargs.buttonPositions = kwargs.buttonPositions or BUTTON_POSITIONS + + local ARG = { + screenSize = kwargs.screenSize, + jitter = false, + } + + --[[ 'initAssociationsArray' defines the array of associations and its + methods. + (a 'class') + ]] + local function initAssociationsArray(dataset) + local associations = { + _array = {}, + _order = {}, + _index = 0 + } + + local unusedIds = tensor.Int64Tensor{range = {dataset:getSize()}}:shuffle( + random:generator()) + local usedIdCount = 0 + + local function getNewId() + if usedIdCount < dataset:getSize() then + usedIdCount = usedIdCount + 1 + return unusedIds(usedIdCount):val() + else + error('Unable to get new image id. Not enough remain.') + end + end + + function associations:shuffle() + local perm = tensor.Int64Tensor{range = {#self._order}}:shuffle( + random:generator()) + for i = 1, #self._order do + self._order[i] = perm(i):val() + end + self._index = 0 + end + + function associations:add() + local association = { + imageId = getNewId(), + correctResponse = psychlab_helpers.randomFrom(kwargs.buttons), + timesPreviouslyDisplayed = 0, + mostRecentTrial = -1 + } + table.insert(self._array, association) + table.insert(self._order, #self._order + 1) + end + + -- 'associations.step' is called during each trial. + function associations:step(trialId) + self._index = self._index + 1 + + -- Copy the association so as to return it before updating its recency + -- data. + self._output = helpers.shallowCopy(self._array[self._order[ + self._index]]) + + -- Update the recency data. + local ref = self._array[self._order[self._index]] + ref.timesPreviouslyDisplayed = ref.timesPreviouslyDisplayed + 1 + ref.mostRecentTrial = trialId + + -- Reset _index if necessary and return. + if self._index == #self._array then + self._index = 0 + end + return self._output + end + + return associations + end + + --[[ Function to define the adaptive staircase procedure (a 'class'). + This procedure promotes from difficulty level K to level K + 1 when K + consecutive trials are correct. + ]] + local function initStaircase(opt) + local staircase = { + _difficultyLevel = 1, + _perfectSoFar = true, + _index = 0, + _promoteLevel = opt.promoteFunction, + _repeatLevel = opt.repeatFunction + } + + function staircase.promoteLevel(self) + self._difficultyLevel = self._difficultyLevel + 1 + self._promoteLevel() + end + + function staircase.repeatLevel(self) + self._repeatLevel() + end + + function staircase.endLevel(self) + if self._perfectSoFar then + self:promoteLevel() + else + self:repeatLevel() + end + self._perfectSoFar = true + end + + -- 'staircase.step' is called at the end of each trial. + function staircase.step(self, correct) + self._index = self._index + 1 + + -- Track whether all trials are correct. + if correct ~= 1 then + self._perfectSoFar = false + end + + -- Reset _index if necessary and call the endLevel function. + if self._index == self._difficultyLevel then + self._index = 0 + self:endLevel() + end + end + + return staircase + end + + -- Class definition for arbitrary visuomotor mapping psychlab environment. + local env = {} + env.__index = env + + setmetatable(env, { + __call = function (cls, ...) + local self = setmetatable({}, cls) + self:_init(...) + return self + end + }) + + -- 'init' gets called at the start of each episode. + function env:_init(pac, opts) + print('opts passed to _init:') + print(helpers.tostring(opts)) + print('ARG in _init:') + print(helpers.tostring(ARG)) + + if self.dataset == nil then + self.dataset = brady_konkle_oliva2008(opts) + end + + self.screenSize = opts.screenSize + + -- If requested, randomly perturb the target location for each trial. + self.jitter = ARG.jitter + if self.jitter then + print('Jitter target location') + self.jitteredCenter = {} + end + + self:setupImages() + + -- Store a copy of the 'point_and_click' api. + self.pac = pac + end + + --[[ Reset is called after init. It is called only once per episode. + Note: the episodeId passed to this function may not be correct if the job + has resumed from a checkpoint after preemption. + ]] + function env:reset(episodeId, seed) + random:seed(seed) + + self.pac:setBackgroundColor(kwargs.bgColor) + self.pac:clearWidgets() + psychlab_helpers.addFixation(self, kwargs.fixationSize) + + self.currentTrial = {} + self._previousTrialId = 0 + + psychlab_helpers.setTrialsPerEpisodeCap(self, kwargs.trialsPerEpisodeCap) + + -- Initialize associations array and adaptive staircase objects. + self.associations = initAssociationsArray(self.dataset) + self.staircase = initStaircase{ + repeatFunction = function () + self.associations:shuffle() + end, + promoteFunction = function () + self.associations:add() + self.associations:shuffle() + end + } + + -- Start out with one association in the array. Do this after the seed has + -- been set. + self.associations:add() + end + + function env:setupImages() + self.images = {} + + self.images.fixation = psychlab_helpers.getFixationImage(self.screenSize, + kwargs.bgColor, kwargs.fixationColor, kwargs.fixationSize) + + local h = kwargs.buttonSize * self.screenSize.height + local w = kwargs.buttonSize * self.screenSize.width + + self.images.greenImage = tensor.ByteTensor(h, w, 3) + self.images.greenImage:select(3, 1):fill(100) + self.images.greenImage:select(3, 2):fill(255) + self.images.greenImage:select(3, 3):fill(100) + + self.images.dullGreenImage = tensor.ByteTensor(h, w, 3) + self.images.dullGreenImage:select(3, 1):fill(100) + self.images.dullGreenImage:select(3, 2):fill(200) + self.images.dullGreenImage:select(3, 3):fill(100) + + self.images.redImage = tensor.ByteTensor(h, w, 3) + self.images.redImage:select(3, 1):fill(255) + self.images.redImage:select(3, 2):fill(100) + self.images.redImage:select(3, 3):fill(100) + + self.images.dullRedImage = tensor.ByteTensor(h, w, 3) + self.images.dullRedImage:select(3, 1):fill(200) + self.images.dullRedImage:select(3, 2):fill(100) + self.images.dullRedImage:select(3, 3):fill(100) + + self.images.whiteImage = tensor.ByteTensor(h, w, 3):fill(255) + self.images.blackImage = tensor.ByteTensor(h, w, 3) + + self.target = tensor.ByteTensor(256, 256, 3) + end + + function env:finishTrial(delay) + self.currentTrial.memorySetSize = #self.associations._array + self.currentTrial.reactionTime = + game:episodeTimeSeconds() - self._currentTrialStartTime + -- It is necessary to record memorySetSize before stepping the staircase. + self.staircase:step(self.currentTrial.correct) + + self.currentTrial.stepCount = self.pac:elapsedSteps() + psychlab_helpers.publishTrialData(self.currentTrial, kwargs.schema) + psychlab_helpers.finishTrialCommon(self, delay, kwargs.fixationSize) + end + + function env:fixationCallback(name, mousePos, hoverTime, userData) + if hoverTime == kwargs.timeToFixateCross then + self.pac:addReward(kwargs.fixationReward) + self.pac:removeWidget('fixation') + self.pac:removeWidget('center_of_fixation') + + self.currentTrial.trialId = self._previousTrialId + 1 + -- Measure reaction time since the trial started. + self._currentTrialStartTime = game:episodeTimeSeconds() + self.pac:resetSteps() + + -- 'trialId' must be set before adding array to compute recency. + self:addArray(self.currentTrial.trialId) + end + end + + function env:revealCorrectResponse() + for _, button in ipairs(kwargs.buttons) do + if button == self.currentTrial.correctResponse then + self.pac:updateWidget(button, self.images.greenImage) + else + self.pac:updateWidget(button, self.images.redImage) + end + end + end + + function env:onHoverEnd(name, mousePos, hoverTime, userData) + self.pac:addReward(self._rewardToDeliver) + self:finishTrial(kwargs.fastInterTrialInterval) + end + + function env:correctResponseCallback(name, mousePos, hoverTime, userData) + self.currentTrial.response = name + self.currentTrial.correct = 1 + self._rewardToDeliver = kwargs.correctReward + + self:revealCorrectResponse() + end + + function env:incorrectResponseCallback(name, mousePos, hoverTime, userData) + self.currentTrial.response = name + self.currentTrial.correct = 0 + self._rewardToDeliver = kwargs.incorrectReward + + self:revealCorrectResponse() + end + + function env:addResponseButtons() + for _, button in ipairs(kwargs.buttons) do + local callback + if button == self.currentTrial.correctResponse then + callback = self.correctResponseCallback + else + callback = self.incorrectResponseCallback + end + + -- When an association is shown for the first time, show the correct + -- answer. + local buttonImage + if self.currentTrial.timesPreviouslyDisplayed > 0 then + buttonImage = self.images.blackImage + else + if button == self.currentTrial.correctResponse then + buttonImage = self.images.dullGreenImage + else + buttonImage = self.images.dullRedImage + end + end + + self.pac:addWidget{ + name = button, + image = buttonImage, + pos = kwargs.buttonPositions[button], + size = {kwargs.buttonSize, kwargs.buttonSize}, + mouseHoverCallback = callback, + mouseHoverEndCallback = self.onHoverEnd, + } + end + end + + function env:getNextTrial(trialId) + local pair = self.associations:step(trialId) + self.currentTrial.imageIndex = pair.imageId + self.currentTrial.correctResponse = pair.correctResponse + + self.currentTrial.timesPreviouslyDisplayed = pair.timesPreviouslyDisplayed + + if pair.mostRecentTrial > 0 then + self.currentTrial.recency = trialId - pair.mostRecentTrial + else + -- 'recency' is -1 the first time a new association is introduced. + self.currentTrial.recency = -1 + end + end + + function env:addArray(trialId) + self:getNextTrial(trialId) + psychlab_helpers.addTargetImage(self, + self.dataset:getImage( + self.currentTrial.imageIndex), + kwargs.targetSize) + self:addResponseButtons() + end + + function env:removeArray() + self.pac:removeWidget('target') + for _, button in ipairs(kwargs.buttons) do + self.pac:removeWidget(button) + end + end + + return psychlab_factory.createLevelApi{ + env = point_and_click, + envOpts = {environment = env, screenSize = ARG.screenSize} + } +end + +return factory diff --git a/game_scripts/factories/psychlab/continuous_recognition_factory.lua b/game_scripts/factories/psychlab/continuous_recognition_factory.lua new file mode 100644 index 000000000..c0fc5f7a5 --- /dev/null +++ b/game_scripts/factories/psychlab/continuous_recognition_factory.lua @@ -0,0 +1,315 @@ +--[[ Copyright (C) 2018 Google Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +]] + +local brady_konkle_oliva2008 = require 'datasets.brady_konkle_oliva2008' +local game = require 'dmlab.system.game' +local helpers = require 'common.helpers' +local psychlab_factory = require 'factories.psychlab.factory' +local psychlab_helpers = require 'factories.psychlab.helpers' +local point_and_click = require 'factories.psychlab.point_and_click' +local random = require 'common.random' +local tensor = require 'dmlab.system.tensor' + +--[[ In each trial, the agent must indicate whether an image is old or new. +50% of the time the answer is new. The set of old images keeps growing. + +Unlike most probe recognition paradigms, in this continuous recognition paradigm +there are no separate study and test phases. + +It is a 'varied mapping' paradigm since the same images can appear in different +orders in different episodes. This terminology is standard. It comes from +Schneider & Shifrin (1977). +]] + +local TIME_TO_FIXATE_CROSS = 1 -- in frames +local FAST_INTER_TRIAL_INTERVAL = 1 -- in frames +local BG_COLOR = {255, 255, 255} +local TRIALS_PER_EPISODE_CAP = 60 + +local TARGET_SIZE = 0.75 + +local FIXATION_REWARD = 0 +local CORRECT_REWARD = 1 +local INCORRECT_REWARD = 0 +local FIXATION_SIZE = 0.1 +local FIXATION_COLOR = {255, 0, 0} -- RGB +local BUTTON_WIDTH = 0.1 +local BUTTON_HEIGHT = 0.1 + +local factory = {} + +function factory.createLevelApi(kwargs) + kwargs.timeToFixateCross = kwargs.timeToFixateCross or TIME_TO_FIXATE_CROSS + kwargs.fastInterTrialInterval = kwargs.fastInterTrialInterval or + FAST_INTER_TRIAL_INTERVAL + kwargs.bgColor = kwargs.bgColor or BG_COLOR + kwargs.trialsPerEpisodeCap = kwargs.trialsPerEpisodeCap or + TRIALS_PER_EPISODE_CAP + kwargs.targetSize = kwargs.targetSize or TARGET_SIZE + kwargs.fixationReward = kwargs.fixationReward or FIXATION_REWARD + kwargs.correctReward = kwargs.correctReward or CORRECT_REWARD + kwargs.incorrectReward = kwargs.incorrectReward or INCORRECT_REWARD + kwargs.fixationSize = kwargs.fixationSize or FIXATION_SIZE + kwargs.fixationColor = kwargs.fixationColor or FIXATION_COLOR + kwargs.buttonWidth = kwargs.buttonWidth or BUTTON_WIDTH + kwargs.buttonHeight = kwargs.buttonHeight or BUTTON_HEIGHT + + local ARG = { + screenSize = {width = 512, height = 512}, + jitter = false, + } + + -- Class definition for continuous recognition psychlab environment. + local env = {} + env.__index = env + + setmetatable(env, { + __call = function (cls, ...) + local self = setmetatable({}, cls) + self:_init(...) + return self + end + }) + + -- 'init' gets called at the start of each episode. + function env:_init(pac, opts) + print('opts passed to _init:') + print(helpers.tostring(opts)) + print('ARG in _init:') + print(helpers.tostring(ARG)) + + if self.dataset == nil then + self.dataset = brady_konkle_oliva2008(opts) + end + + self.screenSize = opts.screenSize + + -- If requested, randomly perturb the target location for each trial. + self.jitter = ARG.jitter + if self.jitter then + print('Jitter target location') + self.jitteredCenter = {} + end + + self:setupImages() + + -- Store a copy of the 'point_and_click' api. + self.pac = pac + end + + -- 'env:setupOrder' determines the order in which images are shown. + function env:setupOrder() + local firstTrial = true + local newIDs = tensor.Int64Tensor{range = {self.dataset:getSize()}}:shuffle( + random:generator()) + self._oldIDs = {} + self._trialLastDisplayed = {} + self._timesPreviouslyDisplayed = {} + + local function newTrial(trialId) + assert(#self._oldIDs < self.dataset:getSize(), + 'Unable to get new trial id.') + local stimID = newIDs(#self._oldIDs + 1):val() + table.insert(self._oldIDs, stimID) + table.insert(self._trialLastDisplayed, trialId) + table.insert(self._timesPreviouslyDisplayed, 0) + + self.currentTrial.imageIndex = stimID + self.currentTrial.isNew = true + -- 'recency' is -1 the first time a new association is introduced. + self.currentTrial.recency = -1 + self.currentTrial._timesPreviouslyDisplayed = 0 + + return self.currentTrial.imageIndex, self.currentTrial.isNew + end + + local function oldTrial(trialId) + local stimID, stimIndex = psychlab_helpers.randomFrom(self._oldIDs) + + self.currentTrial.imageIndex = stimID + self.currentTrial.isNew = false + -- 'recency' denotes the number of trials since item was last shown. + self.currentTrial.recency = trialId - self._trialLastDisplayed[stimIndex] + self.currentTrial._timesPreviouslyDisplayed = + self._timesPreviouslyDisplayed[stimIndex] + + -- Keep track of the last trial id when each oldID was last shown. + self._trialLastDisplayed[stimIndex] = trialId + self._timesPreviouslyDisplayed[stimIndex] = + self._timesPreviouslyDisplayed[stimIndex] + 1 + return self.currentTrial.imageIndex, self.currentTrial.isNew + end + + self.getNextTrial = function (trialId) + if firstTrial then + firstTrial = false + return newTrial(trialId) + else + if #self._oldIDs < self.dataset:getSize() and + random:uniformReal(0, 1) > .5 then + return newTrial(trialId) + else + return oldTrial(trialId) + end + end + end + end + + --[[ Reset is called after init. It is called only once per episode. + Note: the episodeId passed to this function may not be correct if the job + has resumed from a checkpoint after preemption. + ]] + function env:reset(episodeId, seed, ...) + random:seed(seed) + + self.pac:setBackgroundColor(kwargs.bgColor) + self.pac:clearWidgets() + psychlab_helpers.addFixation(self, kwargs.fixationSize) + + self.currentTrial = {} + self._previousTrialId = 0 + + psychlab_helpers.setTrialsPerEpisodeCap(self, kwargs.trialsPerEpisodeCap) + + self:setupOrder() + end + + function env:setupImages() + self.images = {} + + self.images.fixation = psychlab_helpers.getFixationImage(self.screenSize, + kwargs.bgColor, + kwargs.fixationColor, + kwargs.fixationSize) + + local h = kwargs.buttonHeight * self.screenSize.height + local w = kwargs.buttonWidth * self.screenSize.width + + self.images.greenImage = tensor.ByteTensor(h, w, 3) + self.images.greenImage:select(3, 1):fill(100) + self.images.greenImage:select(3, 2):fill(255) + self.images.greenImage:select(3, 3):fill(100) + + self.images.redImage = tensor.ByteTensor(h, w, 3) + self.images.redImage:select(3, 1):fill(255) + self.images.redImage:select(3, 2):fill(100) + self.images.redImage:select(3, 3):fill(100) + + self.images.whiteImage = tensor.ByteTensor(h, w, 3):fill(255) + self.images.blackImage = tensor.ByteTensor(h, w, 3) + + self.target = tensor.ByteTensor(256, 256, 3) + end + + function env:finishTrial(delay) + self.currentTrial.memorySetSize = #self._oldIDs + self.currentTrial.reactionTime = + game:episodeTimeSeconds() - self._currentTrialStartTime + self.currentTrial.stepCount = self.pac:elapsedSteps() + psychlab_helpers.publishTrialData(self.currentTrial, kwargs.schema) + psychlab_helpers.finishTrialCommon(self, delay, kwargs.fixationSize) + end + + function env:fixationCallback(name, mousePos, hoverTime, userData) + if hoverTime == kwargs.timeToFixateCross then + self.pac:addReward(kwargs.fixationReward) + self.pac:removeWidget('fixation') + self.pac:removeWidget('center_of_fixation') + + self.currentTrial.trialId = self._previousTrialId + 1 + -- Measure reaction time since the trial started. + self._currentTrialStartTime = game:episodeTimeSeconds() + self.pac:resetSteps() + + -- 'trialId' must be set before adding array to compute recency. + self:addArray(self.currentTrial.trialId) + end + end + + function env:onHoverEnd(name, mousePos, hoverTime, userData) + self.pac:addReward(self._rewardToDeliver) + self:finishTrial(kwargs.fastInterTrialInterval) + end + + function env:correctResponseCallback(name, mousePos, hoverTime, userData) + self.currentTrial.response = name + self.currentTrial.correct = 1 + + self.pac:updateWidget(name, self.images.greenImage) + self._rewardToDeliver = kwargs.correctReward + end + + function env:incorrectResponseCallback(name, mousePos, hoverTime, userData) + self.currentTrial.response = name + self.currentTrial.correct = 0 + + self.pac:updateWidget(name, self.images.redImage) + self._rewardToDeliver = kwargs.incorrectReward + end + + function env:addResponseButtons(isNew) + local buttonPosY = 0.5 - kwargs.buttonHeight / 2 + local buttonSize = {kwargs.buttonWidth, kwargs.buttonHeight} + + local newCallback, oldCallback + if isNew then + newCallback = self.correctResponseCallback + oldCallback = self.incorrectResponseCallback + else + newCallback = self.incorrectResponseCallback + oldCallback = self.correctResponseCallback + end + + self.pac:addWidget{ + name = 'newButton', + image = self.images.blackImage, + pos = {0, buttonPosY}, + size = buttonSize, + mouseHoverCallback = newCallback, + mouseHoverEndCallback = self.onHoverEnd, + } + self.pac:addWidget{ + name = 'oldButton', + image = self.images.blackImage, + pos = {1 - kwargs.buttonWidth, buttonPosY}, + size = buttonSize, + mouseHoverCallback = oldCallback, + mouseHoverEndCallback = self.onHoverEnd, + } + end + + function env:addArray(trialId) + self.currentTrial.imageIndex, self.currentTrial.isNew = + self.getNextTrial(trialId) + local img = self.dataset:getImage(self.currentTrial.imageIndex) + psychlab_helpers.addTargetImage(self, img, kwargs.targetSize) + self:addResponseButtons(self.currentTrial.isNew) + end + + function env:removeArray() + self.pac:removeWidget('target') + self.pac:removeWidget('newButton') + self.pac:removeWidget('oldButton') + end + + return psychlab_factory.createLevelApi{ + env = point_and_click, + envOpts = {environment = env, screenSize = ARG.screenSize} + } +end + +return factory diff --git a/game_scripts/levels/contributed/dmlab30/README.md b/game_scripts/levels/contributed/dmlab30/README.md index a5eb8772f..8afa2825b 100644 --- a/game_scripts/levels/contributed/dmlab30/README.md +++ b/game_scripts/levels/contributed/dmlab30/README.md @@ -2,8 +2,7 @@ DMLab-30 is a set of environments designed for DeepMind Lab. These environments enable a researcher to develop agents for a large spectrum of interesting tasks -either individually or in a multi-task setting. We have released 28 levels. Two -remaining levels will be added soon. +either individually or in a multi-task setting. 1. [`rooms_collect_good_objects_{test,train}`](#collect-good-objects) 1. [`rooms_exploit_deferred_effects_{test,train}`](#exploit-deferred-effects) @@ -23,6 +22,8 @@ remaining levels will be added soon. 1. [`natlab_varying_map_randomized`](#varying-map-randomized) 1. [`skymaze_irreversible_path_hard`](#irreversible-path-hard) 1. [`skymaze_irreversible_path_varied`](#irreversible-path-varied) +1. [`psychlab_arbitrary_visuomotor_mapping`](#arbitrary-visuomotor-mapping) +1. [`psychlab_continuous_recognition`](#continuous-recognition) 1. [`psychlab_sequential_comparison`](#sequential-comparison) 1. [`psychlab_visual_search`](#visual-search) 1. [`explore_object_locations_small`](#object-locations-small) @@ -569,6 +570,34 @@ Level Name: `skymaze_irreversible_path_varied` For details, see: [Leibo, Joel Z. et al. "Psychlab: A Psychology Laboratory for Deep Reinforcement Learning Agents (2018)"](https://arxiv.org/abs/1801.08116). +### Arbitrary Visuomotor Mapping + +In this task, the agent is shown consecutive images with which they must +remember associations with specific movement patterns (locations to point at). +The agent is rewarded if it can remember the action associated with a given +object. The images are drawn from a set of ~ 2500, and the specific associations +are randomly generated and different in each episode. + +Test Regime: Training and testing levels drawn from the same distribution. + +Observation Spec: RGBD + +Level Name: `psychlab_arbitrary_visuomotor_mapping` + +### Continuous Recognition + +This task tests familiarity memory. Consecutive images are shown, and the agent +must indicate whether or not they have seen the image before during that +episode. Looking at the left square indicates no, and right indicates yes. The +images (drawn from a set of ~2500) are shown in a different random order in +every episode. + +Test Regime: Training and testing levels drawn from the same distribution. + +Observation Spec: RGBD + +Level Name: `psychlab_continuous_recognition` + ### Sequential Comparison
diff --git a/game_scripts/levels/contributed/dmlab30/psychlab_arbitrary_visuomotor_mapping.lua b/game_scripts/levels/contributed/dmlab30/psychlab_arbitrary_visuomotor_mapping.lua new file mode 100644 index 000000000..06159558b --- /dev/null +++ b/game_scripts/levels/contributed/dmlab30/psychlab_arbitrary_visuomotor_mapping.lua @@ -0,0 +1,20 @@ +--[[ Copyright (C) 2018 Google Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +]] + +local factory = require 'factories.psychlab.arbitrary_visuomotor_mapping_factory' + +return factory.createLevelApi{} diff --git a/game_scripts/levels/contributed/dmlab30/psychlab_continuous_recognition.lua b/game_scripts/levels/contributed/dmlab30/psychlab_continuous_recognition.lua new file mode 100644 index 000000000..84ab9f10d --- /dev/null +++ b/game_scripts/levels/contributed/dmlab30/psychlab_continuous_recognition.lua @@ -0,0 +1,20 @@ +--[[ Copyright (C) 2018 Google Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +]] + +local factory = require 'factories.psychlab.continuous_recognition_factory' + +return factory.createLevelApi{} diff --git a/game_scripts/levels/contributed/psychlab/arbitrary_visuomotor_mapping.lua b/game_scripts/levels/contributed/psychlab/arbitrary_visuomotor_mapping.lua new file mode 100644 index 000000000..2e2288a5d --- /dev/null +++ b/game_scripts/levels/contributed/psychlab/arbitrary_visuomotor_mapping.lua @@ -0,0 +1,23 @@ +--[[ Copyright (C) 2018 Google Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +]] + +local factory = require 'factories.psychlab.arbitrary_visuomotor_mapping_factory' + +return factory.createLevelApi{ + schema = 'arbitrary_visuomotor_mapping', + episodeLengthSeconds = 300, +} diff --git a/game_scripts/levels/contributed/psychlab/continuous_recognition.lua b/game_scripts/levels/contributed/psychlab/continuous_recognition.lua new file mode 100644 index 000000000..84403de10 --- /dev/null +++ b/game_scripts/levels/contributed/psychlab/continuous_recognition.lua @@ -0,0 +1,23 @@ +--[[ Copyright (C) 2018 Google Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +]] + +local factory = require 'factories.psychlab.continuous_recognition_factory' + +return factory.createLevelApi{ + schema = 'continuous_recognition', + episodeLengthSeconds = 300, +}