From 1223246776ca6b24ae1f7ecab7836a4f163e7638 Mon Sep 17 00:00:00 2001 From: courtois-neuromod-machine Date: Mon, 4 Feb 2019 16:19:41 -0500 Subject: [PATCH 001/170] hope that this sticks --- env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env.sh b/env.sh index f21b5714..2f4e24a7 100644 --- a/env.sh +++ b/env.sh @@ -1,2 +1,2 @@ -export PUPIL_PATH=$HOME/data/src/pupil +export PUPIL_PATH=$HOME/git/pupil From a3bd27098e337d7c8f0c0f4c79e212bcf89f5c0f Mon Sep 17 00:00:00 2001 From: courtois-neuromod-machine Date: Fri, 8 Feb 2019 11:57:18 -0500 Subject: [PATCH 002/170] switch to audio filtered videos --- src/sessions/ses-video1.py | 6 +++--- src/sessions/ses-video2.py | 8 ++++---- src/sessions/ses-video3.py | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/sessions/ses-video1.py b/src/sessions/ses-video1.py index 904d3101..38a63ef8 100644 --- a/src/sessions/ses-video1.py +++ b/src/sessions/ses-video1.py @@ -10,13 +10,13 @@ task_base.Pause(), video.SingleVideo( - 'data/videos/Oceans_fs_10m/Oceans_fs_10m_1.mp4', + 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_1_filt.mp4', name='Oceans_fs_10m_1'), video.SingleVideo( - 'data/videos/Oceans_fs_10m/Oceans_fs_10m_2.mp4', + 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_2_filt.mp4', name='Oceans_fs_10m_2'), video.SingleVideo( - 'data/videos/Oceans_fs_10m/Oceans_fs_10m_3.mp4', + 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_3_filt.mp4', name='Oceans_fs_10m_3'), diff --git a/src/sessions/ses-video2.py b/src/sessions/ses-video2.py index 2e192e14..33ce2eb2 100644 --- a/src/sessions/ses-video2.py +++ b/src/sessions/ses-video2.py @@ -10,16 +10,16 @@ task_base.Pause(), video.SingleVideo( - 'data/videos/Oceans_fs_10m/Oceans_fs_10m_4.mp4', + 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_4_filt.mp4', name='Oceans_fs_10m_4'), video.SingleVideo( - 'data/videos/Oceans_fs_10m/Oceans_fs_10m_5.mp4', + 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_5_filt.mp4', name='Oceans_fs_10m_5'), video.SingleVideo( - 'data/videos/Oceans_fs_10m/Oceans_fs_10m_6.mp4', + 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_6_filt.mp4', name='Oceans_fs_10m_6'), video.SingleVideo( - 'data/videos/Oceans_fs_10m/Oceans_fs_10m_7.mp4', + 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_7_filt.mp4', name='Oceans_fs_10m_7'), ] diff --git a/src/sessions/ses-video3.py b/src/sessions/ses-video3.py index 4858f04b..5da1e185 100644 --- a/src/sessions/ses-video3.py +++ b/src/sessions/ses-video3.py @@ -10,16 +10,16 @@ task_base.Pause(), video.SingleVideo( - 'data/videos/Oceans_fs_10m/Oceans_fs_10m_8.mp4', + 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_8_filt.mp4', name='Oceans_fs_10m_8'), video.SingleVideo( - 'data/videos/Oceans_fs_10m/Oceans_fs_10m_9.mp4', + 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_9_filt.mp4', name='Oceans_fs_10m_9'), video.SingleVideo( - 'data/videos/Oceans_fs_10m/Oceans_fs_10m_10.mp4', + 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_10_filt.mp4', name='Oceans_fs_10m_10'), video.SingleVideo( - 'data/videos/Oceans_fs_10m/Oceans_fs_10m_11.mp4', + 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_11_filt.mp4', name='Oceans_fs_10m_11'), ] From 7df1db36038d581641f4bb215ee089d9183dee88 Mon Sep 17 00:00:00 2001 From: courtois-neuromod-machine Date: Mon, 11 Feb 2019 12:19:23 -0500 Subject: [PATCH 003/170] use inscapes filtered --- src/sessions/ses-video1.py | 4 ++-- src/sessions/ses-video2.py | 4 ++-- src/sessions/ses-video3.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sessions/ses-video1.py b/src/sessions/ses-video1.py index 38a63ef8..f34e62f8 100644 --- a/src/sessions/ses-video1.py +++ b/src/sessions/ses-video1.py @@ -3,9 +3,9 @@ TASKS = [ video.SingleVideo( - 'data/videos/Oceans_fs_10m/Inscapes_sound_normed.mp4', name='Inscapes'), + 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), video.SingleVideo( - 'data/videos/Oceans_fs_10m/Inscapes_sound_normed.mp4', name='Inscapes'), + 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), task_base.Pause(), diff --git a/src/sessions/ses-video2.py b/src/sessions/ses-video2.py index 33ce2eb2..7f1ad4fa 100644 --- a/src/sessions/ses-video2.py +++ b/src/sessions/ses-video2.py @@ -3,9 +3,9 @@ TASKS = [ video.SingleVideo( - 'data/videos/Oceans_fs_10m/Inscapes_sound_normed.mp4', name='Inscapes'), + 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), video.SingleVideo( - 'data/videos/Oceans_fs_10m/Inscapes_sound_normed.mp4', name='Inscapes'), + 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), task_base.Pause(), diff --git a/src/sessions/ses-video3.py b/src/sessions/ses-video3.py index 5da1e185..9bfd84fd 100644 --- a/src/sessions/ses-video3.py +++ b/src/sessions/ses-video3.py @@ -3,9 +3,9 @@ TASKS = [ video.SingleVideo( - 'data/videos/Oceans_fs_10m/Inscapes_sound_normed.mp4', name='Inscapes'), + 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), video.SingleVideo( - 'data/videos/Oceans_fs_10m/Inscapes_sound_normed.mp4', name='Inscapes'), + 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), task_base.Pause(), From eda4afb050b9d6fcd24922090083699505f7dbf4 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 16 Dec 2019 12:25:58 -0500 Subject: [PATCH 004/170] add friends-s2 --- src/sessions/ses-friends-s2.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/sessions/ses-friends-s2.py diff --git a/src/sessions/ses-friends-s2.py b/src/sessions/ses-friends-s2.py new file mode 100644 index 00000000..0a74b03d --- /dev/null +++ b/src/sessions/ses-friends-s2.py @@ -0,0 +1,11 @@ +from ..tasks import video + +TASKS = [] + +for episode in range(1,25): + for segment in 'ab': + TASKS.append( + video.SingleVideo( + 'data/videos/friends/s2/friends_s2e%02d%s.mkv'%(episode, segment), + aspect_ratio = 4/3., + name='task-friends-s2e%d%s'%(episode, segment))) From 4826c7601b1e1c4b8517fff00e9ef5301f49b25d Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 19 Dec 2019 17:00:42 -0500 Subject: [PATCH 005/170] check if power is plugged in at startup, to avoid any failed scans due to laptop shutdown --- main.py | 2 +- src/shared/cli.py | 15 +++++++++++++-- src/shared/config.py | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index ab8a7c9f..e857c546 100755 --- a/main.py +++ b/main.py @@ -21,4 +21,4 @@ tasks = ses_mod.TASKS except ImportError: raise(ValueError('session tasks file cannot be found for %s'%parsed.session)) - cli.main_loop(tasks[parsed.skip_n_tasks:], parsed.subject, parsed.session, parsed.eyetracking, parsed.fmri, parsed.meg) + cli.main_loop(tasks[parsed.skip_n_tasks:], parsed.subject, parsed.session, parsed.eyetracking, parsed.fmri, parsed.meg, parsed.ctl_win, parsed.run_on_battery) diff --git a/src/shared/cli.py b/src/shared/cli.py index 7e4550d6..527d9d85 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -9,10 +9,15 @@ globalClock = core.MonotonicClock(0) logging.setDefaultClock(globalClock) -from . import config, fmri, eyetracking +from . import config, fmri, eyetracking, utils from ..tasks import task_base, video -def main_loop(all_tasks, subject, session, enable_eyetracker=False, use_fmri=False, use_meg=False, show_ctl_win = False): +def main_loop(all_tasks, subject, session, enable_eyetracker=False, use_fmri=False, use_meg=False, show_ctl_win = False, allow_run_on_battery=False): + + if not utils.check_power_plugged(): + print('*'*25+'WARNING: the power cord is not connected'+'*'*25) + if not allow_run_on_battery: + return log_path = os.path.abspath(os.path.join(config.OUTPUT_DIR, 'sub-%s'%subject,'ses-%s'%session)) if not os.path.exists(log_path): @@ -171,4 +176,10 @@ def parse_args(): help='skip n of the tasks', default=0, type=int) + parser.add_argument('--ctl_win', + help='show control window', + action='store_true') + parser.add_argument('--run_on_battery', + help='allow the script to run on battery', + action='store_true') return parser.parse_args() diff --git a/src/shared/config.py b/src/shared/config.py index f75192d4..4c739fdc 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -29,7 +29,7 @@ # task parameters INSTRUCTION_DURATION = 6 -WRAP_WIDTH = 1.6 +WRAP_WIDTH = 1 # port for meg setup PARALLEL_PORT_ADDRESS = '/dev/parport0' From 9e709ddc759ea289dfcb7ad7121621a6de9ae900 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 27 Feb 2020 15:58:17 -0500 Subject: [PATCH 006/170] working on eyetracking automatization --- src/shared/eyetracking.py | 65 +++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index 49fd0fa9..b3af59f4 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -28,6 +28,14 @@ # remove pupil samples with low confidence PUPIL_CONFIDENCE_THRESHOLD = .4 +CAPTURE_SETTINGS = { + "frame_size": [640, 480], + "frame_rate": 250, + "exposure_time": 4000, + "global_gain": 1, + "uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", + } + class EyetrackerCalibration(Task): def __init__(self,eyetracker, order='random', marker_fill_color=MARKER_FILL_COLOR, **kwargs): @@ -139,34 +147,71 @@ def __init__(self, output_path, output_fname_base): self._req_socket.connect('tcp://localhost:50020') # start eye0 if not started yet (from pupil saved config) - self.send_recv_notification({ + notif = self.send_recv_notification({ 'subject':'eye_process.should_start.0', 'eye_id':0, 'args':{}}) - # setup recorder output path + time.sleep(1) + self.send_recv_notification( + { + 'subject': 'start_eye_plugin', + 'name': 'Aravis_Manager', + 'target': 'eye0' + } + ) + self.send_recv_notification( + { + 'subject': 'start_eye_plugin', + 'name': 'Aravis_Source', + 'target': 'eye0', + 'args' : CAPTURE_SETTINGS + } + ) + # quit existing plugin self.send_recv_notification({ 'subject':'stop_plugin', - 'name':'Recorder',}) + 'name':'Recorder' + }) self.send_recv_notification({ 'subject':'stop_plugin', - 'name':'Accuracy_Visualizer','args':{}}) + 'name':'Accuracy_Visualizer','args':{} + }) #restart with new params +# self.send_recv_notification({ +# 'subject':'start_plugin', +# 'name':'Fixed_Screen_Marker_Calibration', +# 'args':{'fullscreen':True, 'marker_scale':.8, 'sample_duration':120, 'monitor_idx':1}}) + # setup recorder output path self.send_recv_notification({ 'subject':'start_plugin', - 'name':'Fixed_Screen_Marker_Calibration', - 'args':{'fullscreen':True, 'marker_scale':.8, 'sample_duration':120, 'monitor_idx':1}}) - self.send_recv_notification({ - 'subject':'start_plugin', - 'name':'Recorder','args':{'rec_path':self.record_dir,'rec_root_dir':self.record_dir,'raw_jpeg':False}}) + 'name':'Recorder','args':{ + 'rec_root_dir':self.record_dir, + 'session_name':self.output_fname_base + '.pupil', + 'raw_jpeg':False, + 'record_eye':False} + }) self.send_recv_notification({ 'subject':'start_plugin', 'name':'Pupil_Remote','args':{}}) - self.send_recv_notification({'subject':'recording.should_start',}) + self.send_recv_notification({ + "subject": "set_detection_mapping_mode", + "mode": "2d"}) + + """ + self.send_recv_notification({ + 'subject':'start_plugin', + 'name':'Detector2DPlugin', + 'target':'eye0', + 'args':{}}) + """ + + #self.send_recv_notification({'subject':'recording.should_start',}) # wait for the whole schmilblick to boot time.sleep(4) + def send_recv_notification(self, n): # REQ REP requirese lock step communication with multipart msg (topic,msgpack_encoded dict) self._req_socket.send_multipart((bytes('notify.%s'%n['subject'],'utf-8'), msgpack.dumps(n))) From c3d195869a0cd65159699b19161d488508e9c641 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 27 Feb 2020 16:01:55 -0500 Subject: [PATCH 007/170] working on push-to-talk function --- main.py | 11 ++++++++++- src/shared/cli.py | 26 ++++++++++++++++++++++---- src/shared/ptt.py | 1 + 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 src/shared/ptt.py diff --git a/main.py b/main.py index e857c546..82008f1f 100755 --- a/main.py +++ b/main.py @@ -21,4 +21,13 @@ tasks = ses_mod.TASKS except ImportError: raise(ValueError('session tasks file cannot be found for %s'%parsed.session)) - cli.main_loop(tasks[parsed.skip_n_tasks:], parsed.subject, parsed.session, parsed.eyetracking, parsed.fmri, parsed.meg, parsed.ctl_win, parsed.run_on_battery) + cli.main_loop( + tasks[parsed.skip_n_tasks:], + parsed.subject, + parsed.session, + parsed.eyetracking, + parsed.fmri, + parsed.meg, + parsed.ctl_win, + parsed.run_on_battery, + parsed.ptt) diff --git a/src/shared/cli.py b/src/shared/cli.py index 527d9d85..b83cbb40 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -9,10 +9,19 @@ globalClock = core.MonotonicClock(0) logging.setDefaultClock(globalClock) -from . import config, fmri, eyetracking, utils +from . import config #import first separately +from . import fmri, eyetracking, utils from ..tasks import task_base, video -def main_loop(all_tasks, subject, session, enable_eyetracker=False, use_fmri=False, use_meg=False, show_ctl_win = False, allow_run_on_battery=False): +def main_loop(all_tasks, + subject, + session, + enable_eyetracker=False, + use_fmri=False, + use_meg=False, + show_ctl_win=False, + allow_run_on_battery=False, + enable_ptt=False): if not utils.check_power_plugged(): print('*'*25+'WARNING: the power cord is not connected'+'*'*25) @@ -36,6 +45,11 @@ def main_loop(all_tasks, subject, session, enable_eyetracker=False, use_fmri=Fal exp_win = visual.Window(**config.EXP_WINDOW) exp_win.mouseVisible = False + ptt = None + if enable_ptt: + from .ptt import PushToTalk + ptt = PushToTalk() + if enable_eyetracker: print('creating et client') eyetracker_client = eyetracking.EyeTrackerClient( @@ -45,8 +59,9 @@ def main_loop(all_tasks, subject, session, enable_eyetracker=False, use_fmri=Fal print('starting et client') eyetracker_client.start() print('done') - #all_tasks.insert(0, eyetracking.EyetrackerCalibration(eyetracker_client,name='EyeTracker-Calibration')) - gaze_drawer = eyetracking.GazeDrawer(ctl_win) + all_tasks.insert(0, eyetracking.EyetrackerCalibration(eyetracker_client, name='EyeTracker-Calibration')) + if show_ctl_win: + gaze_drawer = eyetracking.GazeDrawer(ctl_win) if use_fmri: setup_video_path = glob.glob(os.path.join('data','videos','subject_setup_videos','sub-%s_*'%subject)) if not len(setup_video_path): @@ -182,4 +197,7 @@ def parse_args(): parser.add_argument('--run_on_battery', help='allow the script to run on battery', action='store_true') + parser.add_argument('--ptt', + help='enable Push-To-Talk function', + action='store_true') return parser.parse_args() diff --git a/src/shared/ptt.py b/src/shared/ptt.py new file mode 100644 index 00000000..2a134b2f --- /dev/null +++ b/src/shared/ptt.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s872--93c8903d354a60847f307cc408a97f31b61c837d99becf29a552d5af05fd0312.py From 80566ac4a2ed3728bda05e139f1df667dd0fe002 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 27 Feb 2020 16:02:30 -0500 Subject: [PATCH 008/170] friends s3 --- src/sessions/ses-friends-s3.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/sessions/ses-friends-s3.py diff --git a/src/sessions/ses-friends-s3.py b/src/sessions/ses-friends-s3.py new file mode 100644 index 00000000..b78c59dc --- /dev/null +++ b/src/sessions/ses-friends-s3.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s330--09ddf42fbb4847cc93925786dd8c2b9af8666e974584929be2a5e1bac7ffdae1.py From dc566e4fd4e6784704647488d767fb74b258ec7b Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 27 Feb 2020 16:03:13 -0500 Subject: [PATCH 009/170] test session for audio video sync --- src/sessions/ses-synctest.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/sessions/ses-synctest.py diff --git a/src/sessions/ses-synctest.py b/src/sessions/ses-synctest.py new file mode 100644 index 00000000..64c135ed --- /dev/null +++ b/src/sessions/ses-synctest.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s131--51d0592b2002c7d05e8f89e13a30648798d1eef808d7c858e47cd193a6d6a572.py From 23ee3312842d807c301e56cc718ae304e6e50b74 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 27 Feb 2020 16:04:03 -0500 Subject: [PATCH 010/170] code to check if laptop is plugged on power outlet --- src/shared/utils.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/shared/utils.py diff --git a/src/shared/utils.py b/src/shared/utils.py new file mode 100644 index 00000000..37f6ba00 --- /dev/null +++ b/src/shared/utils.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s115--d3c1dfc2145b4cdb761481182f5018b49a583c3a2fb195b791d904460a363eb9.py From be4950f3ae40cc2054a97f3f0cd0bbf3e0ad861c Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 27 Feb 2020 16:05:25 -0500 Subject: [PATCH 011/170] switch to MovieStim2 due to video lags in MovieStim3 --- src/tasks/video.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tasks/video.py b/src/tasks/video.py index 79ca6e7d..bbc61a50 100644 --- a/src/tasks/video.py +++ b/src/tasks/video.py @@ -34,7 +34,8 @@ def instructions(self, exp_win, ctl_win): def _setup(self, exp_win): - self.movie_stim = visual.MovieStim3(exp_win, self.filepath, units='pixels') + self.movie_stim = visual.MovieStim2(exp_win, self.filepath, units='pixels') + #print(self.movie_stim._audioStream.__class__) aspect_ratio = self._aspect_ratio or self.movie_stim.size[0]/self.movie_stim.size[1] min_ratio = min( exp_win.size[0]/ self.movie_stim.size[0], @@ -64,7 +65,6 @@ def _run(self, exp_win, ctl_win): self.movie_stim.draw(exp_win) if ctl_win: self.movie_stim.draw(ctl_win) - yield for frameN in range(config.FRAME_RATE * FADE_TO_GREY_DURATION): exp_win.setColor([float(frameN)/config.FRAME_RATE/FADE_TO_GREY_DURATION-1]*3) From 130c79f13f11a679edf5ac1e0c7a383c8871da8b Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 4 Mar 2020 13:32:50 -0500 Subject: [PATCH 012/170] avoid clearing buffer for videos --- src/shared/cli.py | 12 +++++++++--- src/tasks/task_base.py | 11 ++++++----- src/tasks/video.py | 6 +++--- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/shared/cli.py b/src/shared/cli.py index b83cbb40..7636ff3d 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -13,6 +13,8 @@ from . import fmri, eyetracking, utils from ..tasks import task_base, video + + def main_loop(all_tasks, subject, session, @@ -45,6 +47,7 @@ def main_loop(all_tasks, exp_win = visual.Window(**config.EXP_WINDOW) exp_win.mouseVisible = False + ptt = None if enable_ptt: from .ptt import PushToTalk @@ -113,6 +116,7 @@ def main_loop(all_tasks, #force focus on the task window to ensure getting keys, TTL, ... exp_win.winHandle.activate() + exp_win.recordFrameIntervals = True for draw in task.run(exp_win, ctl_win): if use_eyetracking: @@ -120,12 +124,12 @@ def main_loop(all_tasks, if not gaze is None: gaze_drawer.draw_gazepoint(gaze) # check for global event keys - exp_win.flip() + exp_win.flip(clearBuffer=draw) if show_ctl_win: - ctl_win.flip() + ctl_win.flip(clearBuffer=draw) if any([k[1]&event.MOD_CTRL for k in event._keyBuffer]): - allKeys = event.getKeys(['n','c','q'], modifiers=True) + allKeys = event.getKeys(['n','c','q','t'], modifiers=True) ctrl_pressed = any([k[1]['ctrl'] for k in allKeys]) all_keys_only = [k[0] for k in allKeys] if len(allKeys) and ctrl_pressed: @@ -148,6 +152,8 @@ def main_loop(all_tasks, logging.exp(msg="task - %s: restart"%str(task)) task.restart() task.unload() + exp_win.recordFrameIntervals = True + exp_win.saveFrameIntervals('frame_intervals.txt') if ctrl_pressed: if 'q' in all_keys_only: diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index e62fb933..fc904095 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -46,7 +46,8 @@ def run(self, exp_win, ctl_win): # show instruction if hasattr(self, 'instructions'): for _ in self.instructions(exp_win, ctl_win): - yield True + yield _ + yield True # wait for TTL fmri.get_ttl() # flush any remaining TTL keys @@ -85,7 +86,7 @@ def run(self, exp_win, ctl_win): if not frame_idx%config.FRAME_RATE: progress_bar.update(1) - yield True + yield _ # send stop trigger/marker to MEG + Biopac (or anything else on parallel port) if self.use_meg: @@ -130,7 +131,7 @@ def _run(self, exp_win, ctl_win): screen_text.draw(exp_win) if ctl_win: screen_text.draw(ctl_win) - yield + yield True class Fixation(Task): @@ -155,7 +156,7 @@ def instructions(self, exp_win, ctl_win): screen_text.draw(exp_win) if ctl_win: screen_text.draw(ctl_win) - yield + yield True def _run(self, exp_win, ctl_win): screen_text = visual.TextStim( @@ -167,4 +168,4 @@ def _run(self, exp_win, ctl_win): screen_text.draw(exp_win) if ctl_win: screen_text.draw(ctl_win) - yield + yield True diff --git a/src/tasks/video.py b/src/tasks/video.py index bbc61a50..7d6f069e 100644 --- a/src/tasks/video.py +++ b/src/tasks/video.py @@ -30,7 +30,7 @@ def instructions(self, exp_win, ctl_win): screen_text.draw(exp_win) if ctl_win: screen_text.draw(ctl_win) - yield + yield True def _setup(self, exp_win): @@ -65,10 +65,10 @@ def _run(self, exp_win, ctl_win): self.movie_stim.draw(exp_win) if ctl_win: self.movie_stim.draw(ctl_win) - yield + yield False for frameN in range(config.FRAME_RATE * FADE_TO_GREY_DURATION): exp_win.setColor([float(frameN)/config.FRAME_RATE/FADE_TO_GREY_DURATION-1]*3) - yield + yield True def stop(self): self.movie_stim.stop() From d4ed142c50919e9cf61eb1e4a0dd274b96f47cda Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 4 Mar 2020 13:33:24 -0500 Subject: [PATCH 013/170] changetoptt --- src/shared/ptt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/ptt.py b/src/shared/ptt.py index 2a134b2f..7830ac35 100644 --- a/src/shared/ptt.py +++ b/src/shared/ptt.py @@ -1 +1 @@ -/annex/objects/SHA256E-s872--93c8903d354a60847f307cc408a97f31b61c837d99becf29a552d5af05fd0312.py +/annex/objects/SHA256E-s985--35480e4c4129fa3418d2f5f41b60bf672a7a51cb702adee59d17c8f52ae9847d.py From 26cc65f12e5a4bb265a19fb126b98a1d1a35a48e Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 4 Mar 2020 16:10:40 -0500 Subject: [PATCH 014/170] large restructuration of the code to make it cleaner --- src/shared/cli.py | 116 +++++++++++++++++++++++--------------- src/shared/config.py | 4 +- src/shared/eyetracking.py | 6 +- src/shared/fmri.py | 13 +++++ src/tasks/images.py | 4 +- src/tasks/language.py | 10 ++-- src/tasks/memory.py | 4 +- src/tasks/speech.py | 6 +- src/tasks/task_base.py | 59 ++++++++----------- src/tasks/video.py | 5 +- src/tasks/videogame.py | 6 +- 11 files changed, 129 insertions(+), 104 deletions(-) diff --git a/src/shared/cli.py b/src/shared/cli.py index 7636ff3d..fe13fa44 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -10,10 +10,57 @@ logging.setDefaultClock(globalClock) from . import config #import first separately -from . import fmri, eyetracking, utils +from . import fmri, eyetracking, utils, meg from ..tasks import task_base, video +def listen_shortcuts(): + if any([k[1]&event.MOD_CTRL for k in event._keyBuffer]): + allKeys = event.getKeys(['n','c','q'], modifiers=True) + ctrl_pressed = any([k[1]['ctrl'] for k in allKeys]) + all_keys_only = [k[0] for k in allKeys] + if len(allKeys) and ctrl_pressed: + return all_keys_only[0] + return False + +def run_task_loop(loop, gaze_drawer=None): + for _ in loop: + if gaze_drawer: + gaze = eyetracker_client.get_gaze() + if not gaze is None: + gaze_drawer.draw_gazepoint(gaze) + # check for global event keys + shortcut_evt = listen_shortcuts() + if shortcut_evt: + return shortcut_evt + +def run_task(task, exp_win, ctl_win=None, gaze_drawer=None): + print('Next task: %s'%str(task)) + + # show instruction + shortcut_evt = run_task_loop(task.instructions(exp_win, ctl_win), gaze_drawer) + if shortcut_evt: return shortcut_evt + + if task.use_fmri: + shortcut_evt = run_task_loop(fmri.wait_for_ttl(), gaze_drawer) + if shortcut_evt: return shortcut_evt + + logging.info('GO') + + # send start trigger/marker to MEG + Biopac (or anything else on parallel port) + if task.use_meg: + meg.send_signal(meg.MEG_settings['TASK_START_CODE']) + + shortcut_evt = run_task_loop(task.run(exp_win, ctl_win), gaze_drawer) + + # send stop trigger/marker to MEG + Biopac (or anything else on parallel port) + if task.use_meg: + meg.send_signal(meg.MEG_settings['TASK_STOP_CODE']) + + # now that time is less sensitive: save files + task.save() + + return shortcut_evt def main_loop(all_tasks, subject, @@ -53,6 +100,7 @@ def main_loop(all_tasks, from .ptt import PushToTalk ptt = PushToTalk() + gaze_drawer = None if enable_eyetracker: print('creating et client') eyetracker_client = eyetracking.EyeTrackerClient( @@ -93,10 +141,6 @@ def main_loop(all_tasks, #clear events buffer in case the user pressed a lot of buttoons event.clearEvents() - # ensure to clear the screen if task aborted - exp_win.flip() - if show_ctl_win: - ctl_win.flip() use_eyetracking = False if enable_eyetracker and task.use_eyetracking: @@ -109,62 +153,44 @@ def main_loop(all_tasks, use_meg=use_meg) print('READY') - allKeys = [] - ctrl_pressed = False - while True: #force focus on the task window to ensure getting keys, TTL, ... exp_win.winHandle.activate() - + # record frame intervals for debug exp_win.recordFrameIntervals = True - for draw in task.run(exp_win, ctl_win): - - if use_eyetracking: - gaze = eyetracker_client.get_gaze() - if not gaze is None: - gaze_drawer.draw_gazepoint(gaze) - # check for global event keys - exp_win.flip(clearBuffer=draw) - if show_ctl_win: - ctl_win.flip(clearBuffer=draw) - - if any([k[1]&event.MOD_CTRL for k in event._keyBuffer]): - allKeys = event.getKeys(['n','c','q','t'], modifiers=True) - ctrl_pressed = any([k[1]['ctrl'] for k in allKeys]) - all_keys_only = [k[0] for k in allKeys] - if len(allKeys) and ctrl_pressed: - break + + shortcut_evt = run_task(task, exp_win, ctl_win, gaze_drawer) + + if shortcut_evt == 'n': + # restart the task + logging.exp(msg="task - %s: restart"%str(task)) + task.restart() + continue + elif shortcut_evt: + # abort/skip or quit + logging.exp(msg="task - %s: abort"%str(task)) + break else: # task completed - task.save() + logging.exp(msg="task - %s: complete"%str(task)) + # send stop trigger/marker to MEG + Biopac (or anything else on parallel port) break - task.save() logging.flush() task.stop() - # ensure last frame or task clear is draw - exp_win.flip() - if show_ctl_win: - ctl_win.flip() - - if ctrl_pressed and ('c' in all_keys_only or 'q' in all_keys_only): - break - logging.exp(msg="task - %s: restart"%str(task)) - task.restart() task.unload() exp_win.recordFrameIntervals = True exp_win.saveFrameIntervals('frame_intervals.txt') - if ctrl_pressed: - if 'q' in all_keys_only: - print('quit') - break - else: - print('skip') - else: + if shortcut_evt=='q': + print('quit') + break + elif shortcut_evt is None: # add a delay between tasks to avoid remaining TTL to start next task + # do that only if the task was not aborted to save time + # there is anyway the duration of the instruction before listening to TTL for i in range(DELAY_BETWEEN_TASK*config.FRAME_RATE): - exp_win.flip() + exp_win.flip(clearBuffer=False) except KeyboardInterrupt as ki: print(traceback.format_exc()) diff --git a/src/shared/config.py b/src/shared/config.py index 4c739fdc..0de6fea4 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -11,14 +11,14 @@ EXP_WINDOW = dict( # size = (800,600), #size = (1024,768), - size = (1280,1024), + size = (1920, 1080), screen=1, fullscr=True, gammaErrorPolicy='warn', ) CTL_WINDOW = dict( - size = (1024,768), + size = (1920, 1080), pos = (100,0), screen=0, gammaErrorPolicy='warn', diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index b3af59f4..3aee8e43 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -45,12 +45,12 @@ def __init__(self,eyetracker, order='random', marker_fill_color=MARKER_FILL_COLO super().__init__(**kwargs) self.eyetracker = eyetracker - def instructions(self, exp_win, ctl_win): + def _instructions(self, exp_win, ctl_win): instruction_text = """We're going to calibrate the eyetracker. Please look at the markers that appear on the screen.""" screen_text = visual.TextStim( exp_win, text=instruction_text, - alignHoriz="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) for frameN in range(config.FRAME_RATE * INSTRUCTION_DURATION): screen_text.draw(exp_win) @@ -206,7 +206,7 @@ def __init__(self, output_path, output_fname_base): 'target':'eye0', 'args':{}}) """ - + #self.send_recv_notification({'subject':'recording.should_start',}) # wait for the whole schmilblick to boot time.sleep(4) diff --git a/src/shared/fmri.py b/src/shared/fmri.py index 879f3ad4..e77b6541 100644 --- a/src/shared/fmri.py +++ b/src/shared/fmri.py @@ -15,3 +15,16 @@ def get_ttl(): if key.lower() == MR_settings['sync']: return True return False + +# blocking function (iterator) +def wait_for_ttl(): + get_ttl() # flush any remaining TTL keys + ttl_index = 0 + logging.exp(msg="waiting for fMRI TTL") + while True: + if get_ttl(): + #TODO: log real timing of TTL? + logging.exp(msg="fMRI TTL %d"%ttl_index) + ttl_index += 1 + return + yield diff --git a/src/tasks/images.py b/src/tasks/images.py index 0f830186..c58706f6 100644 --- a/src/tasks/images.py +++ b/src/tasks/images.py @@ -23,10 +23,10 @@ def __init__(self, images_list, images_path, *args,**kwargs): else: raise ValueError('Cannot find the listed images in %s '%images_path) - def instructions(self, exp_win, ctl_win): + def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( exp_win, text=self.instruction, - alignHoriz="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): screen_text.draw(exp_win) diff --git a/src/tasks/language.py b/src/tasks/language.py index 891b87e0..7dc4fc17 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -33,10 +33,10 @@ def __init__(self, words_file,*args,**kwargs): else: raise ValueError('File %s does not exists'%words_file) - def instructions(self, exp_win, ctl_win): + def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( exp_win, text=self.instruction, - alignHoriz="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) def _draw_instr(): screen_text.draw(exp_win) @@ -61,17 +61,17 @@ def _run(self, exp_win, ctl_win): target_stim = visual.TextStim( exp_win, text='', pos = (0,.25), - alignHoriz="center", color = 'white') + alignText="center", color = 'white') r1_stim = visual.TextStim( exp_win, text='', pos = (-.5,-.25), - alignHoriz="center", color = 'white') + alignText="center", color = 'white') r2_stim = visual.TextStim( exp_win, text='', pos = (.5,-.25), - alignHoriz="center", color = 'white') + alignText="center", color = 'white') exp_win.logOnFlip(level=logging.EXP,msg='triplet: task starting at %f'%time.time()) diff --git a/src/tasks/memory.py b/src/tasks/memory.py index fc019aea..1df9a790 100644 --- a/src/tasks/memory.py +++ b/src/tasks/memory.py @@ -29,10 +29,10 @@ def __init__(self, items_list,*args,**kwargs): #TODO: image lists as params, subjects .... self.item_list = data.importConditions(items_list) - def instructions(self, exp_win, ctl_win): + def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( exp_win, text=self.instruction, - alignHoriz="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): screen_text.draw(exp_win) diff --git a/src/tasks/speech.py b/src/tasks/speech.py index e124ee15..c67cb352 100644 --- a/src/tasks/speech.py +++ b/src/tasks/speech.py @@ -21,10 +21,10 @@ def __init__(self, words_file,*args,**kwargs): else: raise ValueError('File %s does not exists'%words_file) - def instructions(self, exp_win, ctl_win): + def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( exp_win, text=self.instruction, - alignHoriz="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): screen_text.draw(exp_win) @@ -38,7 +38,7 @@ def _run(self, exp_win, ctl_win): text = visual.TextStim( exp_win, text='', - alignHoriz="center", color = 'white') + alignText="center", color = 'white') exp_win.logOnFlip(level=logging.EXP,msg='speech: task starting at %f'%time.time()) diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index fc904095..01a89a74 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -41,31 +41,23 @@ def unload(self): def __str__(self): return '%s : %s'%(self.__class__, self.name) + def _flip_all_windows(self, exp_win, ctl_win=None, clearBuffer=True): + exp_win.flip(clearBuffer=clearBuffer) + if ctl_win is None: + return + ctl_win.flip(clearBuffer=clearBuffer) + + def instructions(self, exp_win, ctl_win): + if hasattr(self, '_instructions'): + for clearBuffer in self._instructions(exp_win, ctl_win): + self._flip_all_windows(exp_win, ctl_win, clearBuffer) + yield + # last/only flip to clear screen + self._flip_all_windows(exp_win, ctl_win) + yield + def run(self, exp_win, ctl_win): - print('Next task: %s'%str(self)) - # show instruction - if hasattr(self, 'instructions'): - for _ in self.instructions(exp_win, ctl_win): - yield _ - yield True - - # wait for TTL - fmri.get_ttl() # flush any remaining TTL keys - if self.use_fmri: - ttl_index = 0 - logging.exp(msg="waiting for fMRI TTL") - while True: - if fmri.get_ttl(): - #TODO: log real timing of TTL? - logging.exp(msg="fMRI TTL %d"%ttl_index) - ttl_index += 1 - break - yield False # no need to draw - logging.info('GO') - # send start trigger/marker to MEG + Biopac (or anything else on parallel port) - if self.use_meg: - meg.send_signal(meg.MEG_settings['TASK_START_CODE']) self.task_timer = core.Clock() # initialize a progress bar if we know the duration of the task @@ -74,11 +66,8 @@ def run(self, exp_win, ctl_win): progress_bar = tqdm.tqdm(total=self.duration) frame_idx = 0 - for _ in self._run(exp_win, ctl_win): - if self.use_fmri: - if fmri.get_ttl(): - logging.exp(msg="fMRI TTL %d"%ttl_index) - ttl_index += 1 + for clearBuffer in self._run(exp_win, ctl_win): + self._flip_all_windows(exp_win, ctl_win, clearBuffer) # increment the progress bar every second if progress_bar: @@ -86,11 +75,7 @@ def run(self, exp_win, ctl_win): if not frame_idx%config.FRAME_RATE: progress_bar.update(1) - yield _ - - # send stop trigger/marker to MEG + Biopac (or anything else on parallel port) - if self.use_meg: - meg.send_signal(meg.MEG_settings['TASK_STOP_CODE']) + yield if progress_bar: progress_bar.clear() @@ -122,7 +107,7 @@ def _setup(self, exp_win): def _run(self, exp_win, ctl_win): screen_text = visual.TextStim( exp_win, text=self.text, - alignHoriz="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) while True: if not self.wait_key is False: @@ -147,10 +132,10 @@ def __init__(self, duration=7*60, symbol="+", **kwargs): self.duration = duration self.symbol = symbol - def instructions(self, exp_win, ctl_win): + def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( exp_win, text=self.instruction, - alignHoriz="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): screen_text.draw(exp_win) @@ -161,7 +146,7 @@ def instructions(self, exp_win, ctl_win): def _run(self, exp_win, ctl_win): screen_text = visual.TextStim( exp_win, text=self.symbol, - alignHoriz="center", color = 'white') + alignText="center", color = 'white') screen_text.height = .2 for frameN in range(config.FRAME_RATE * self.duration): diff --git a/src/tasks/video.py b/src/tasks/video.py index 7d6f069e..2295a723 100644 --- a/src/tasks/video.py +++ b/src/tasks/video.py @@ -20,11 +20,12 @@ def __init__(self, filepath, *args,**kwargs): if not os.path.exists(self.filepath): raise ValueError('File %s does not exists'%self.filepath) - def instructions(self, exp_win, ctl_win): + def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( exp_win, text=self.instruction, - alignHoriz="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + print(screen_text.pos) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): exp_win.setColor([-float(frameN)/config.FRAME_RATE/config.INSTRUCTION_DURATION]*3) screen_text.draw(exp_win) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index f9673f38..518b5eec 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -96,11 +96,11 @@ def __init__(self, self.max_duration = max_duration self.instruction = self.instruction%(self.game_name, self.state_name) - def instructions(self, exp_win, ctl_win): + def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( exp_win, text=self.instruction, - alignHoriz="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): screen_text.draw(exp_win) @@ -196,7 +196,7 @@ def instructions(self, exp_win, ctl_win): instruction_text = "You are going to watch someone play %s."%self.game_name screen_text = visual.TextStim( exp_win, text=instruction_text, - alignHoriz="center", color = 'white') + alignText="center", color = 'white') for frameN in range(config.FRAME_RATE * INSTRUCTION_DURATION): screen_text.draw(exp_win) From f6b84872909781d2cbf753c56f492f008ef7cc59 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 5 Mar 2020 16:03:13 -0500 Subject: [PATCH 015/170] fixes following the major restructuring --- src/shared/cli.py | 10 +++++----- src/shared/config.py | 2 +- src/shared/eyetracking.py | 17 +++++++++-------- src/tasks/video.py | 4 +++- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/shared/cli.py b/src/shared/cli.py index fe13fa44..19aae1d9 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -23,7 +23,7 @@ def listen_shortcuts(): return all_keys_only[0] return False -def run_task_loop(loop, gaze_drawer=None): +def run_task_loop(loop, eyetracker=None, gaze_drawer=None): for _ in loop: if gaze_drawer: gaze = eyetracker_client.get_gaze() @@ -34,15 +34,15 @@ def run_task_loop(loop, gaze_drawer=None): if shortcut_evt: return shortcut_evt -def run_task(task, exp_win, ctl_win=None, gaze_drawer=None): +def run_task(task, exp_win, ctl_win=None, eyetracker=None, gaze_drawer=None): print('Next task: %s'%str(task)) # show instruction - shortcut_evt = run_task_loop(task.instructions(exp_win, ctl_win), gaze_drawer) + shortcut_evt = run_task_loop(task.instructions(exp_win, ctl_win), eyetracker, gaze_drawer) if shortcut_evt: return shortcut_evt if task.use_fmri: - shortcut_evt = run_task_loop(fmri.wait_for_ttl(), gaze_drawer) + shortcut_evt = run_task_loop(fmri.wait_for_ttl(), eyetracker, gaze_drawer) if shortcut_evt: return shortcut_evt logging.info('GO') @@ -51,7 +51,7 @@ def run_task(task, exp_win, ctl_win=None, gaze_drawer=None): if task.use_meg: meg.send_signal(meg.MEG_settings['TASK_START_CODE']) - shortcut_evt = run_task_loop(task.run(exp_win, ctl_win), gaze_drawer) + shortcut_evt = run_task_loop(task.run(exp_win, ctl_win), eyetracker, gaze_drawer) # send stop trigger/marker to MEG + Biopac (or anything else on parallel port) if task.use_meg: diff --git a/src/shared/config.py b/src/shared/config.py index 0de6fea4..fd29b75f 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -10,7 +10,7 @@ EXP_WINDOW = dict( # size = (800,600), - #size = (1024,768), + #size = (1280,1024), size = (1920, 1080), screen=1, fullscr=True, diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index 3aee8e43..1bb93e59 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -55,7 +55,7 @@ def _instructions(self, exp_win, ctl_win): for frameN in range(config.FRAME_RATE * INSTRUCTION_DURATION): screen_text.draw(exp_win) screen_text.draw(ctl_win) - yield() + yield True def _setup(self, exp_win): self.use_fmri = False @@ -69,7 +69,7 @@ def _run(self, exp_win, ctl_win): start_calibration = True if start_calibration: break - yield + yield False print('calibration started') window_size_frame = exp_win.size-MARKER_SIZE*2 @@ -90,7 +90,7 @@ def _run(self, exp_win, ctl_win): pupil = None while pupil is None: # wait until we get at least a pupil pupil = self.eyetracker.get_pupil() - yield + yield False exp_win.logOnFlip(level=logging.EXP,msg='eyetracker_calibration: starting at %f'%time.time()) for site_id in random_order: @@ -117,7 +117,8 @@ def _run(self, exp_win, ctl_win): 'timestamp': pupil['timestamp']} all_refs_per_flip.append(ref) all_pupils.append(pupil) - yield + yield True + yield True self.eyetracker.calibrate(all_pupils, all_refs_per_flip, exp_win.size) from subprocess import Popen @@ -207,11 +208,11 @@ def __init__(self, output_path, output_fname_base): 'args':{}}) """ + #self.send_recv_notification({'subject':'recording.should_start',}) # wait for the whole schmilblick to boot time.sleep(4) - def send_recv_notification(self, n): # REQ REP requirese lock step communication with multipart msg (topic,msgpack_encoded dict) self._req_socket.send_multipart((bytes('notify.%s'%n['subject'],'utf-8'), msgpack.dumps(n))) @@ -259,12 +260,12 @@ def get_gaze(self): def calibrate(self, pupil_list, ref_list, frame_size): if len(pupil_list) < 100: - # TODO: log - return + logging.error('Calibration: not enough pupil captured for calibration') + #return self.send_recv_notification({ 'subject':'start_plugin', - 'name':'Mock_Calibration', + 'name':'External_Calibration', 'args':{'frame_size': frame_size.tolist()}}) self.send_recv_notification({ diff --git a/src/tasks/video.py b/src/tasks/video.py index 2295a723..15c45a8e 100644 --- a/src/tasks/video.py +++ b/src/tasks/video.py @@ -27,9 +27,11 @@ def _instructions(self, exp_win, ctl_win): print(screen_text.pos) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): - exp_win.setColor([-float(frameN)/config.FRAME_RATE/config.INSTRUCTION_DURATION]*3) + grey = [-float(frameN)/config.FRAME_RATE/config.INSTRUCTION_DURATION]*3 + exp_win.setColor(grey) screen_text.draw(exp_win) if ctl_win: + ctl_win.setColor(grey) screen_text.draw(ctl_win) yield True From df86c8586be45073b6f720694aa79b7d1169ac9c Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 6 Mar 2020 15:24:56 -0500 Subject: [PATCH 016/170] wip: polishing eyetracker client --- src/shared/cli.py | 7 +++++-- src/shared/eyetracking.py | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/shared/cli.py b/src/shared/cli.py index 19aae1d9..64d6083c 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -46,7 +46,8 @@ def run_task(task, exp_win, ctl_win=None, eyetracker=None, gaze_drawer=None): if shortcut_evt: return shortcut_evt logging.info('GO') - + if eyetracker: + eyetracker.start_recording(task.name) # send start trigger/marker to MEG + Biopac (or anything else on parallel port) if task.use_meg: meg.send_signal(meg.MEG_settings['TASK_START_CODE']) @@ -57,6 +58,8 @@ def run_task(task, exp_win, ctl_win=None, eyetracker=None, gaze_drawer=None): if task.use_meg: meg.send_signal(meg.MEG_settings['TASK_STOP_CODE']) + if eyetracker: + eyetracker.stop_recording() # now that time is less sensitive: save files task.save() @@ -159,7 +162,7 @@ def main_loop(all_tasks, # record frame intervals for debug exp_win.recordFrameIntervals = True - shortcut_evt = run_task(task, exp_win, ctl_win, gaze_drawer) + shortcut_evt = run_task(task, exp_win, ctl_win, eyetracker_client, gaze_drawer) if shortcut_evt == 'n': # restart the task diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index 1bb93e59..fb5b489e 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -222,14 +222,25 @@ def get_pupil_timestamp(self): self._req_socket.send('t') #see Pupil Remote Plugin for details return float(self._req_socket.recv()) + def start_recording(self, recording_name): + logging.info('starting eyetracking recording') + return self.send_recv_notification({ + 'subject':'recording.should_start', + 'session_name': recording_name}) + + def stop_recording(self): + logging.info('stopping eyetracking recording') + return self.send_recv_notification({ + 'subject':'recording.should_stop'}) + def join(self, timeout=None): self.stoprequest.set() # stop recording self.send_recv_notification({'subject':'recording.should_stop',}) # stop world and children process - self.send_recv_notification({'subject':'world_process.should_stop'}) + self.send_recv_notification({'subject':'launcher_process.should_stop'}) self._pupil_process.wait(timeout) - + self._pupil_process.terminate() super(EyeTrackerClient, self).join(timeout) def run(self): From 7beb378f4be69e1da5929490b06676d5768514cd Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 9 Mar 2020 15:29:22 -0400 Subject: [PATCH 017/170] save eye video for tests --- src/shared/eyetracking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index fb5b489e..ceb51a84 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -190,7 +190,7 @@ def __init__(self, output_path, output_fname_base): 'rec_root_dir':self.record_dir, 'session_name':self.output_fname_base + '.pupil', 'raw_jpeg':False, - 'record_eye':False} + 'record_eye':True} }) self.send_recv_notification({ 'subject':'start_plugin', From f55677fbccdcd231b8a774155897e0fe1ccb2d1e Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 9 Mar 2020 15:38:53 -0400 Subject: [PATCH 018/170] un-annex stuff!! --- src/sessions/ses-friends-s3.py | 2 +- src/sessions/ses-synctest.py | 2 +- src/shared/ptt.py | 2 +- src/shared/utils.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sessions/ses-friends-s3.py b/src/sessions/ses-friends-s3.py index b78c59dc..39211fd8 100644 --- a/src/sessions/ses-friends-s3.py +++ b/src/sessions/ses-friends-s3.py @@ -1 +1 @@ -/annex/objects/SHA256E-s330--09ddf42fbb4847cc93925786dd8c2b9af8666e974584929be2a5e1bac7ffdae1.py +/annex/objects/SHA256E-s335--64b666ffd253d14d0f266d17ddfc8d084c08363bca2dd0fb7262b8b080bc7483.py diff --git a/src/sessions/ses-synctest.py b/src/sessions/ses-synctest.py index 64c135ed..5dc91849 100644 --- a/src/sessions/ses-synctest.py +++ b/src/sessions/ses-synctest.py @@ -1 +1 @@ -/annex/objects/SHA256E-s131--51d0592b2002c7d05e8f89e13a30648798d1eef808d7c858e47cd193a6d6a572.py +/annex/objects/SHA256E-s133--d2c06f005ef44161dd3b0a4e313d537fa94e6591665af20784993a380faf2fd1.py diff --git a/src/shared/ptt.py b/src/shared/ptt.py index 7830ac35..7fedf7ab 100644 --- a/src/shared/ptt.py +++ b/src/shared/ptt.py @@ -1 +1 @@ -/annex/objects/SHA256E-s985--35480e4c4129fa3418d2f5f41b60bf672a7a51cb702adee59d17c8f52ae9847d.py +/annex/objects/SHA256E-s972--8459c99df0f68fd640c10f4fd8a9c5f4e1c0e63a495df1709a15dca84751caf1.py diff --git a/src/shared/utils.py b/src/shared/utils.py index 37f6ba00..fac6f663 100644 --- a/src/shared/utils.py +++ b/src/shared/utils.py @@ -1 +1 @@ -/annex/objects/SHA256E-s115--d3c1dfc2145b4cdb761481182f5018b49a583c3a2fb195b791d904460a363eb9.py +/annex/objects/SHA256E-s114--b6a5133ef587bfc5ecaf7dac0f11a5b72b03c10377a0e2ca98893cfb60a965a0.py From 561c76f9141d49d6cab3adfcdf36199a8e7a6257 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 9 Mar 2020 15:42:02 -0400 Subject: [PATCH 019/170] un-annex stuff!! --- src/sessions/ses-friends-s3.py | 1 - src/sessions/ses-synctest.py | 1 - src/shared/ptt.py | 1 - src/shared/utils.py | 1 - 4 files changed, 4 deletions(-) delete mode 100644 src/sessions/ses-friends-s3.py delete mode 100644 src/sessions/ses-synctest.py delete mode 100644 src/shared/ptt.py delete mode 100644 src/shared/utils.py diff --git a/src/sessions/ses-friends-s3.py b/src/sessions/ses-friends-s3.py deleted file mode 100644 index 39211fd8..00000000 --- a/src/sessions/ses-friends-s3.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s335--64b666ffd253d14d0f266d17ddfc8d084c08363bca2dd0fb7262b8b080bc7483.py diff --git a/src/sessions/ses-synctest.py b/src/sessions/ses-synctest.py deleted file mode 100644 index 5dc91849..00000000 --- a/src/sessions/ses-synctest.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s133--d2c06f005ef44161dd3b0a4e313d537fa94e6591665af20784993a380faf2fd1.py diff --git a/src/shared/ptt.py b/src/shared/ptt.py deleted file mode 100644 index 7fedf7ab..00000000 --- a/src/shared/ptt.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s972--8459c99df0f68fd640c10f4fd8a9c5f4e1c0e63a495df1709a15dca84751caf1.py diff --git a/src/shared/utils.py b/src/shared/utils.py deleted file mode 100644 index fac6f663..00000000 --- a/src/shared/utils.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s114--b6a5133ef587bfc5ecaf7dac0f11a5b72b03c10377a0e2ca98893cfb60a965a0.py From 5c828248993ea18b44c3698dc1cb2b5c5e8d6a43 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 9 Mar 2020 15:42:33 -0400 Subject: [PATCH 020/170] un-annex stuff!! --- src/sessions/ses-friends-s3.py | 1 + src/sessions/ses-synctest.py | 1 + src/shared/ptt.py | 1 + src/shared/utils.py | 1 + 4 files changed, 4 insertions(+) create mode 100644 src/sessions/ses-friends-s3.py create mode 100644 src/sessions/ses-synctest.py create mode 100644 src/shared/ptt.py create mode 100644 src/shared/utils.py diff --git a/src/sessions/ses-friends-s3.py b/src/sessions/ses-friends-s3.py new file mode 100644 index 00000000..39211fd8 --- /dev/null +++ b/src/sessions/ses-friends-s3.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s335--64b666ffd253d14d0f266d17ddfc8d084c08363bca2dd0fb7262b8b080bc7483.py diff --git a/src/sessions/ses-synctest.py b/src/sessions/ses-synctest.py new file mode 100644 index 00000000..5dc91849 --- /dev/null +++ b/src/sessions/ses-synctest.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s133--d2c06f005ef44161dd3b0a4e313d537fa94e6591665af20784993a380faf2fd1.py diff --git a/src/shared/ptt.py b/src/shared/ptt.py new file mode 100644 index 00000000..7fedf7ab --- /dev/null +++ b/src/shared/ptt.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s972--8459c99df0f68fd640c10f4fd8a9c5f4e1c0e63a495df1709a15dca84751caf1.py diff --git a/src/shared/utils.py b/src/shared/utils.py new file mode 100644 index 00000000..fac6f663 --- /dev/null +++ b/src/shared/utils.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s114--b6a5133ef587bfc5ecaf7dac0f11a5b72b03c10377a0e2ca98893cfb60a965a0.py From fb967ba3c64bc5ef2ece72abc919c88f07be23f1 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 9 Mar 2020 15:46:23 -0400 Subject: [PATCH 021/170] unannex --- src/sessions/ses-friends-s3.py | 1 - src/sessions/ses-synctest.py | 1 - src/shared/ptt.py | 1 - src/shared/utils.py | 1 - 4 files changed, 4 deletions(-) delete mode 100644 src/sessions/ses-friends-s3.py delete mode 100644 src/sessions/ses-synctest.py delete mode 100644 src/shared/ptt.py delete mode 100644 src/shared/utils.py diff --git a/src/sessions/ses-friends-s3.py b/src/sessions/ses-friends-s3.py deleted file mode 100644 index 39211fd8..00000000 --- a/src/sessions/ses-friends-s3.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s335--64b666ffd253d14d0f266d17ddfc8d084c08363bca2dd0fb7262b8b080bc7483.py diff --git a/src/sessions/ses-synctest.py b/src/sessions/ses-synctest.py deleted file mode 100644 index 5dc91849..00000000 --- a/src/sessions/ses-synctest.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s133--d2c06f005ef44161dd3b0a4e313d537fa94e6591665af20784993a380faf2fd1.py diff --git a/src/shared/ptt.py b/src/shared/ptt.py deleted file mode 100644 index 7fedf7ab..00000000 --- a/src/shared/ptt.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s972--8459c99df0f68fd640c10f4fd8a9c5f4e1c0e63a495df1709a15dca84751caf1.py diff --git a/src/shared/utils.py b/src/shared/utils.py deleted file mode 100644 index fac6f663..00000000 --- a/src/shared/utils.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s114--b6a5133ef587bfc5ecaf7dac0f11a5b72b03c10377a0e2ca98893cfb60a965a0.py From fa298ad7eafce7bb22e18bffadb6c122034bb431 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 9 Mar 2020 15:47:47 -0400 Subject: [PATCH 022/170] unannex --- src/sessions/ses-friends-s3.py | 1 + src/sessions/ses-synctest.py | 1 + src/shared/ptt.py | 1 + src/shared/utils.py | 1 + 4 files changed, 4 insertions(+) create mode 100644 src/sessions/ses-friends-s3.py create mode 100644 src/sessions/ses-synctest.py create mode 100644 src/shared/ptt.py create mode 100644 src/shared/utils.py diff --git a/src/sessions/ses-friends-s3.py b/src/sessions/ses-friends-s3.py new file mode 100644 index 00000000..b78c59dc --- /dev/null +++ b/src/sessions/ses-friends-s3.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s330--09ddf42fbb4847cc93925786dd8c2b9af8666e974584929be2a5e1bac7ffdae1.py diff --git a/src/sessions/ses-synctest.py b/src/sessions/ses-synctest.py new file mode 100644 index 00000000..5dc91849 --- /dev/null +++ b/src/sessions/ses-synctest.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s133--d2c06f005ef44161dd3b0a4e313d537fa94e6591665af20784993a380faf2fd1.py diff --git a/src/shared/ptt.py b/src/shared/ptt.py new file mode 100644 index 00000000..7fedf7ab --- /dev/null +++ b/src/shared/ptt.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s972--8459c99df0f68fd640c10f4fd8a9c5f4e1c0e63a495df1709a15dca84751caf1.py diff --git a/src/shared/utils.py b/src/shared/utils.py new file mode 100644 index 00000000..fac6f663 --- /dev/null +++ b/src/shared/utils.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s114--b6a5133ef587bfc5ecaf7dac0f11a5b72b03c10377a0e2ca98893cfb60a965a0.py From 51d4e79769fe6eb79bd57b42175d9f4f40cfe006 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 9 Mar 2020 15:54:44 -0400 Subject: [PATCH 023/170] tabarnakkkkkkkkkkkkkk --- src/shared/utils2.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/shared/utils2.py diff --git a/src/shared/utils2.py b/src/shared/utils2.py new file mode 100644 index 00000000..fac6f663 --- /dev/null +++ b/src/shared/utils2.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s114--b6a5133ef587bfc5ecaf7dac0f11a5b72b03c10377a0e2ca98893cfb60a965a0.py From 444a940d969d38fb25b37c23bf2737c3a9b0e172 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 9 Mar 2020 15:59:10 -0400 Subject: [PATCH 024/170] remove git-annex shit --- src/sessions/ses-friends-s3.py | 1 - src/shared/ptt.py | 1 - src/shared/utils.py | 1 - 3 files changed, 3 deletions(-) delete mode 100644 src/sessions/ses-friends-s3.py delete mode 100644 src/shared/ptt.py delete mode 100644 src/shared/utils.py diff --git a/src/sessions/ses-friends-s3.py b/src/sessions/ses-friends-s3.py deleted file mode 100644 index b78c59dc..00000000 --- a/src/sessions/ses-friends-s3.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s330--09ddf42fbb4847cc93925786dd8c2b9af8666e974584929be2a5e1bac7ffdae1.py diff --git a/src/shared/ptt.py b/src/shared/ptt.py deleted file mode 100644 index 2a134b2f..00000000 --- a/src/shared/ptt.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s872--93c8903d354a60847f307cc408a97f31b61c837d99becf29a552d5af05fd0312.py diff --git a/src/shared/utils.py b/src/shared/utils.py deleted file mode 100644 index 37f6ba00..00000000 --- a/src/shared/utils.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s115--d3c1dfc2145b4cdb761481182f5018b49a583c3a2fb195b791d904460a363eb9.py From 68f8b154f9407ed81b61880070c05f0799d89f2a Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 9 Mar 2020 16:00:29 -0400 Subject: [PATCH 025/170] git-annex wtf happened --- src/sessions/ses-friends-s3.py | 1 + src/sessions/ses-synctest.py | 2 +- src/shared/ptt.py | 1 + src/shared/utils.py | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 src/sessions/ses-friends-s3.py create mode 100644 src/shared/ptt.py create mode 100644 src/shared/utils.py diff --git a/src/sessions/ses-friends-s3.py b/src/sessions/ses-friends-s3.py new file mode 100644 index 00000000..b78c59dc --- /dev/null +++ b/src/sessions/ses-friends-s3.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s330--09ddf42fbb4847cc93925786dd8c2b9af8666e974584929be2a5e1bac7ffdae1.py diff --git a/src/sessions/ses-synctest.py b/src/sessions/ses-synctest.py index 64c135ed..5dc91849 100644 --- a/src/sessions/ses-synctest.py +++ b/src/sessions/ses-synctest.py @@ -1 +1 @@ -/annex/objects/SHA256E-s131--51d0592b2002c7d05e8f89e13a30648798d1eef808d7c858e47cd193a6d6a572.py +/annex/objects/SHA256E-s133--d2c06f005ef44161dd3b0a4e313d537fa94e6591665af20784993a380faf2fd1.py diff --git a/src/shared/ptt.py b/src/shared/ptt.py new file mode 100644 index 00000000..7fedf7ab --- /dev/null +++ b/src/shared/ptt.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s972--8459c99df0f68fd640c10f4fd8a9c5f4e1c0e63a495df1709a15dca84751caf1.py diff --git a/src/shared/utils.py b/src/shared/utils.py new file mode 100644 index 00000000..fac6f663 --- /dev/null +++ b/src/shared/utils.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s114--b6a5133ef587bfc5ecaf7dac0f11a5b72b03c10377a0e2ca98893cfb60a965a0.py From e946ece9041f037c9020645d999ffb58b2e1ac83 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 9 Mar 2020 16:03:38 -0400 Subject: [PATCH 026/170] remove git-nan --- src/sessions/ses-friends-s3.py | 1 - src/sessions/ses-synctest.py | 1 - src/shared/ptt.py | 1 - src/shared/utils.py | 1 - 4 files changed, 4 deletions(-) delete mode 100644 src/sessions/ses-friends-s3.py delete mode 100644 src/sessions/ses-synctest.py delete mode 100644 src/shared/ptt.py delete mode 100644 src/shared/utils.py diff --git a/src/sessions/ses-friends-s3.py b/src/sessions/ses-friends-s3.py deleted file mode 100644 index b78c59dc..00000000 --- a/src/sessions/ses-friends-s3.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s330--09ddf42fbb4847cc93925786dd8c2b9af8666e974584929be2a5e1bac7ffdae1.py diff --git a/src/sessions/ses-synctest.py b/src/sessions/ses-synctest.py deleted file mode 100644 index 5dc91849..00000000 --- a/src/sessions/ses-synctest.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s133--d2c06f005ef44161dd3b0a4e313d537fa94e6591665af20784993a380faf2fd1.py diff --git a/src/shared/ptt.py b/src/shared/ptt.py deleted file mode 100644 index 7fedf7ab..00000000 --- a/src/shared/ptt.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s972--8459c99df0f68fd640c10f4fd8a9c5f4e1c0e63a495df1709a15dca84751caf1.py diff --git a/src/shared/utils.py b/src/shared/utils.py deleted file mode 100644 index fac6f663..00000000 --- a/src/shared/utils.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s114--b6a5133ef587bfc5ecaf7dac0f11a5b72b03c10377a0e2ca98893cfb60a965a0.py From cc545851e283b52f8a08a352626ebe325af23c57 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 9 Mar 2020 16:04:35 -0400 Subject: [PATCH 027/170] trying for 1h to fix git-annex mess --- src/sessions/ses-friends-s3.py | 1 + src/sessions/ses-synctest.py | 1 + src/shared/ptt.py | 1 + src/shared/utils.py | 1 + 4 files changed, 4 insertions(+) create mode 100644 src/sessions/ses-friends-s3.py create mode 100644 src/sessions/ses-synctest.py create mode 100644 src/shared/ptt.py create mode 100644 src/shared/utils.py diff --git a/src/sessions/ses-friends-s3.py b/src/sessions/ses-friends-s3.py new file mode 100644 index 00000000..b78c59dc --- /dev/null +++ b/src/sessions/ses-friends-s3.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s330--09ddf42fbb4847cc93925786dd8c2b9af8666e974584929be2a5e1bac7ffdae1.py diff --git a/src/sessions/ses-synctest.py b/src/sessions/ses-synctest.py new file mode 100644 index 00000000..5dc91849 --- /dev/null +++ b/src/sessions/ses-synctest.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s133--d2c06f005ef44161dd3b0a4e313d537fa94e6591665af20784993a380faf2fd1.py diff --git a/src/shared/ptt.py b/src/shared/ptt.py new file mode 100644 index 00000000..7fedf7ab --- /dev/null +++ b/src/shared/ptt.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s972--8459c99df0f68fd640c10f4fd8a9c5f4e1c0e63a495df1709a15dca84751caf1.py diff --git a/src/shared/utils.py b/src/shared/utils.py new file mode 100644 index 00000000..fac6f663 --- /dev/null +++ b/src/shared/utils.py @@ -0,0 +1 @@ +/annex/objects/SHA256E-s114--b6a5133ef587bfc5ecaf7dac0f11a5b72b03c10377a0e2ca98893cfb60a965a0.py From 56e3985719e2024d9df67c3512915849a07413af Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 9 Mar 2020 16:08:29 -0400 Subject: [PATCH 028/170] delete --- src/sessions/ses-friends-s3.py | 1 - src/sessions/ses-synctest.py | 1 - src/shared/ptt.py | 1 - src/shared/utils.py | 1 - src/shared/utils2.py | 1 - 5 files changed, 5 deletions(-) delete mode 100644 src/sessions/ses-friends-s3.py delete mode 100644 src/sessions/ses-synctest.py delete mode 100644 src/shared/ptt.py delete mode 100644 src/shared/utils.py delete mode 100644 src/shared/utils2.py diff --git a/src/sessions/ses-friends-s3.py b/src/sessions/ses-friends-s3.py deleted file mode 100644 index b78c59dc..00000000 --- a/src/sessions/ses-friends-s3.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s330--09ddf42fbb4847cc93925786dd8c2b9af8666e974584929be2a5e1bac7ffdae1.py diff --git a/src/sessions/ses-synctest.py b/src/sessions/ses-synctest.py deleted file mode 100644 index 5dc91849..00000000 --- a/src/sessions/ses-synctest.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s133--d2c06f005ef44161dd3b0a4e313d537fa94e6591665af20784993a380faf2fd1.py diff --git a/src/shared/ptt.py b/src/shared/ptt.py deleted file mode 100644 index 7fedf7ab..00000000 --- a/src/shared/ptt.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s972--8459c99df0f68fd640c10f4fd8a9c5f4e1c0e63a495df1709a15dca84751caf1.py diff --git a/src/shared/utils.py b/src/shared/utils.py deleted file mode 100644 index fac6f663..00000000 --- a/src/shared/utils.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s114--b6a5133ef587bfc5ecaf7dac0f11a5b72b03c10377a0e2ca98893cfb60a965a0.py diff --git a/src/shared/utils2.py b/src/shared/utils2.py deleted file mode 100644 index fac6f663..00000000 --- a/src/shared/utils2.py +++ /dev/null @@ -1 +0,0 @@ -/annex/objects/SHA256E-s114--b6a5133ef587bfc5ecaf7dac0f11a5b72b03c10377a0e2ca98893cfb60a965a0.py From 2cd6067ebbb6ffaf7a40462b1f64946dfe911e44 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 9 Mar 2020 16:10:28 -0400 Subject: [PATCH 029/170] readd files --- src/sessions/ses-friends-s3.py | 11 +++++++++++ src/sessions/ses-synctest.py | 7 +++++++ src/shared/ptt.py | 29 +++++++++++++++++++++++++++++ src/shared/utils.py | 5 +++++ 4 files changed, 52 insertions(+) create mode 100644 src/sessions/ses-friends-s3.py create mode 100644 src/sessions/ses-synctest.py create mode 100644 src/shared/ptt.py create mode 100644 src/shared/utils.py diff --git a/src/sessions/ses-friends-s3.py b/src/sessions/ses-friends-s3.py new file mode 100644 index 00000000..1bdc1c59 --- /dev/null +++ b/src/sessions/ses-friends-s3.py @@ -0,0 +1,11 @@ +from ..tasks import video + +TASKS = [] + +for episode in range(1,26): + for segment in 'ab': + TASKS.append( + video.SingleVideo( + 'data/videos/friends/s3/friends_s3e%02d%s.mkv'%(episode, segment), + aspect_ratio = 4/3., + name='task-friends-s3e%d%s'%(episode, segment))) diff --git a/src/sessions/ses-synctest.py b/src/sessions/ses-synctest.py new file mode 100644 index 00000000..34cb53de --- /dev/null +++ b/src/sessions/ses-synctest.py @@ -0,0 +1,7 @@ +from ..tasks import video + +TASKS = [ +video.SingleVideo( + 'data/videos/Sync-Footage-V1-H264.mp4', + name='task-synctest') +] * 10 diff --git a/src/shared/ptt.py b/src/shared/ptt.py new file mode 100644 index 00000000..772a0ed1 --- /dev/null +++ b/src/shared/ptt.py @@ -0,0 +1,29 @@ +# Push-To-Talk functions +import pulsectl + +MIC_SOURCE_NAME = 'alsa_input.pci-0000_00_1f.3.analog-stereo' +DSP_SINK_NAME = 'dsp' +DSP_SINK_NAME = 'alsa_output.usb-Lenovo_ThinkPad_USB-C_Dock_Audio_000000000000-00.analog-stereo' + +class PushToTalk(): + + def __init__(self): + self._pa_client = pulsectl.Pulse('ptt_loopback') + self._source_idx = self._pa_client.get_source_by_name(MIC_SOURCE_NAME) + self._sink_idx = self._pa_client.get_sink_by_name(DSP_SINK_NAME) + + def _init_loopback(self): + self._loopback_mod_idx = self._pa_client.module_load( + 'module-loopback', + 'latency_msec=5 source=%d sink=%d'%( + self._source_idx, + self._sink_idx)) + + def _destroy_loopback(self): + self._pa_client.unload_module(self._loopback_mod_idx) + + def mute(self): + self._pa_client.source_mute(self._source_idx, 1) + + def unmute(self): + self._pa_client.source_mute(self._source_idx, 0) diff --git a/src/shared/utils.py b/src/shared/utils.py new file mode 100644 index 00000000..0fc1f742 --- /dev/null +++ b/src/shared/utils.py @@ -0,0 +1,5 @@ +import psutil + +def check_power_plugged(): + battery = psutil.sensors_battery() + return battery.power_plugged From 50eb931d9650216b5971ba48c9fc03dfaa8c621d Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 9 Mar 2020 16:14:41 -0400 Subject: [PATCH 030/170] fix battery checking when using desktop --- src/shared/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/shared/utils.py b/src/shared/utils.py index 0fc1f742..9e2a7ea0 100644 --- a/src/shared/utils.py +++ b/src/shared/utils.py @@ -2,4 +2,7 @@ def check_power_plugged(): battery = psutil.sensors_battery() - return battery.power_plugged + if battery: + return battery.power_plugged + else: + return True From 1d7ddbe143fc63a9034d8c4b3ce507746137a8c8 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 10 Mar 2020 11:43:37 -0400 Subject: [PATCH 031/170] add profiling option to improve perfs --- main.py | 16 +++++++++++++++- src/shared/cli.py | 4 ++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 82008f1f..a29f4168 100755 --- a/main.py +++ b/main.py @@ -15,7 +15,12 @@ if __name__ == "__main__": parsed = cli.parse_args() - print(parsed) + if parsed.profile: + run_profile(parsed) + else: + run(parsed) + +def run(parsed): try: ses_mod = importlib.import_module('src.sessions.ses-%s'%parsed.session) tasks = ses_mod.TASKS @@ -31,3 +36,12 @@ parsed.ctl_win, parsed.run_on_battery, parsed.ptt) + +def run_profile(parsed): + import cProfile + cProfile.runctx( + "run()", + {'parsed':parsed}, + locals(), + "task_stimuli.pstats" + ) diff --git a/src/shared/cli.py b/src/shared/cli.py index 64d6083c..79a8c3cb 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -235,4 +235,8 @@ def parse_args(): parser.add_argument('--ptt', help='enable Push-To-Talk function', action='store_true') + parser.add_argument('--profile', + help='enable profiling', + action='store_true') + return parser.parse_args() From 32670b8abbd52b4737ba7c7a726fd2ce938ad5e2 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 10 Mar 2020 11:58:31 -0400 Subject: [PATCH 032/170] profiling and bug fix --- main.py | 19 ++++++++++--------- src/shared/cli.py | 1 + src/shared/config.py | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/main.py b/main.py index a29f4168..8cff3649 100755 --- a/main.py +++ b/main.py @@ -13,13 +13,6 @@ from src.shared import cli -if __name__ == "__main__": - parsed = cli.parse_args() - if parsed.profile: - run_profile(parsed) - else: - run(parsed) - def run(parsed): try: ses_mod = importlib.import_module('src.sessions.ses-%s'%parsed.session) @@ -37,11 +30,19 @@ def run(parsed): parsed.run_on_battery, parsed.ptt) -def run_profile(parsed): +def run_profiled(parsed): import cProfile + from main import run cProfile.runctx( - "run()", + "run(parsed)", {'parsed':parsed}, locals(), "task_stimuli.pstats" ) + +if __name__ == "__main__": + parsed = cli.parse_args() + if parsed.profile: + run_profiled(parsed) + else: + run(parsed) diff --git a/src/shared/cli.py b/src/shared/cli.py index 79a8c3cb..7c6e58c0 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -103,6 +103,7 @@ def main_loop(all_tasks, from .ptt import PushToTalk ptt = PushToTalk() + eyetracker_client = None gaze_drawer = None if enable_eyetracker: print('creating et client') diff --git a/src/shared/config.py b/src/shared/config.py index fd29b75f..080c5f68 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -1,7 +1,7 @@ from psychopy import prefs # avoids delay in movie3 audio seek -prefs.general['audioLib'] = ['sounddevice'] +prefs.hardware['audioLib'] = ['sounddevice'] OUTPUT_DIR = 'output' From 22b5d6ce30eb260901b4ff42e3c2a6cb6da3d35b Mon Sep 17 00:00:00 2001 From: unf Date: Fri, 13 Mar 2020 11:25:12 -0400 Subject: [PATCH 033/170] fix eyetracking issues --- src/shared/cli.py | 2 +- src/shared/config.py | 9 +++++---- src/shared/eyetracking.py | 23 ++++++++++++++++++----- src/tasks/task_base.py | 6 +++--- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/shared/cli.py b/src/shared/cli.py index 7c6e58c0..7e06fa6a 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -26,7 +26,7 @@ def listen_shortcuts(): def run_task_loop(loop, eyetracker=None, gaze_drawer=None): for _ in loop: if gaze_drawer: - gaze = eyetracker_client.get_gaze() + gaze = eyetracker.get_gaze() if not gaze is None: gaze_drawer.draw_gazepoint(gaze) # check for global event keys diff --git a/src/shared/config.py b/src/shared/config.py index 080c5f68..dafd5588 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -9,16 +9,17 @@ EYETRACKING_ROI = (60,30,660,450) EXP_WINDOW = dict( -# size = (800,600), - #size = (1280,1024), - size = (1920, 1080), +# size = (1280,1024), + size = (1024, 768), +# size = (1920, 1080), screen=1, fullscr=True, gammaErrorPolicy='warn', ) CTL_WINDOW = dict( - size = (1920, 1080), +# size = (1920, 1080), + size = (1280, 1024), pos = (100,0), screen=0, gammaErrorPolicy='warn', diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index ceb51a84..06456412 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -123,6 +123,17 @@ def _run(self, exp_win, ctl_win): from subprocess import Popen +from contextlib import contextmanager + +@contextmanager +def nonblocking(lock): + locked = lock.acquire(False) + try: + yield locked + finally: + if locked: + lock.release() + class EyeTrackerClient(threading.Thread): def __init__(self, output_path, output_fname_base): @@ -249,6 +260,7 @@ def run(self): ipc_sub_port = int(self._req_socket.recv()) self.pupil_monitor = Msg_Receiver(self._ctx,'tcp://localhost:%d'%ipc_sub_port,topics=('gaze','pupil')) while not self.stoprequest.isSet(): + time.sleep(.001) msg = self.pupil_monitor.recv() if not msg is None: topic, tmp = msg @@ -260,14 +272,15 @@ def run(self): print('eyetracker listener: stopping') - def get_pupil(self): - with self.lock: - return self.pupil + with nonblocking(self.lock) as locked: + if locked: + return self.pupil def get_gaze(self): - with self.lock: - return self.gaze + with nonblocking(self.lock) as locked: + if locked: + return self.gaze def calibrate(self, pupil_list, ref_list, frame_size): if len(pupil_list) < 100: diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index 01a89a74..44e1e8f6 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -67,16 +67,16 @@ def run(self, exp_win, ctl_win): frame_idx = 0 for clearBuffer in self._run(exp_win, ctl_win): + # yield first to allow external draw before flipping + yield self._flip_all_windows(exp_win, ctl_win, clearBuffer) - + # increment the progress bar every second if progress_bar: frame_idx += 1 if not frame_idx%config.FRAME_RATE: progress_bar.update(1) - yield - if progress_bar: progress_bar.clear() progress_bar.close() From f93ece8be8575e3175eef751a388f925b76dee5f Mon Sep 17 00:00:00 2001 From: basile Date: Sat, 5 Sep 2020 13:50:51 -0400 Subject: [PATCH 034/170] add a new class for looping on set of videogame levels --- src/tasks/videogame.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 518b5eec..5c12ba50 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -182,6 +182,34 @@ def _run(self, exp_win, ctl_win): ctl_win.winHandle.on_key_press = event._onPygletKey del ctl_win.winHandle.on_key_release +class VideoGameMultiLevel(VideoGame): + + def __init__(self, *args,**kwargs): + + self._state_names = kwargs.pop('state_names') + self._scenarii = kwargs.pop('scenarii') + self._repeat_scenario_multilevel = kwargs.get('repeat_scenario', False) + + kwargs['repeat_scenario'] = False + super().__init__( + state_name = self._state_names[0], + scenario=self._scenarii[0], + **kwargs) + + def _run(self, exp_win, ctl_win): + while True: + for level, scenario in zip(self._state_names, self._scenarii): + self.emulator.load_state(level) + self.emulator.data.load( + retro.data.get_file_path(self.game_name, 'data.json'), + scenario) + yield from super()._run(exp_win, ctl_win) + time_exceeded = self.max_duration and self.task_timer.getTime() > self.max_duration + if time_exceeded: # stop if we are above the planned duration + break + if time_exceeded or not self._repeat_scenario_multilevel: + break + class VideoGameReplay(VideoGameBase): def __init__(self, movie_filename, game_name=DEFAULT_GAME_NAME, scenario=None, *args, **kwargs): From d045a7431e2c1244680ee515221c07e9ba7805c6 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 7 Sep 2020 09:47:44 -0400 Subject: [PATCH 035/170] add session script for 3 shinobi levels looping --- src/sessions/ses-shinobi3levels.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/sessions/ses-shinobi3levels.py diff --git a/src/sessions/ses-shinobi3levels.py b/src/sessions/ses-shinobi3levels.py new file mode 100644 index 00000000..2c2d1357 --- /dev/null +++ b/src/sessions/ses-shinobi3levels.py @@ -0,0 +1,12 @@ + +from ..tasks import images, videogame, memory, task_base + +TASKS = [ + + videogame.VideoGameMultiLevel( + state_names=['Level1-0','Level4-1','Level5-0'], + scenarii=['data/videogames/%s.json'%sc for sc in ['scenario_repeat1', 'scenario_Level4-1', 'scenario_Level5-1']], # this scenario repeats the same level + repeat_scenario=True, + max_duration=10*60, # if when level completed or dead we exceed that time in secs, stop the task + name='ShinobiIIIReturnOfTheNinjaMaster-multilevel-test') +] * 5 From 7513466a6f95e4123d0b473edb422b7c9d0a8533 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 9 Sep 2020 17:00:50 -0400 Subject: [PATCH 036/170] restructuring, improve video games --- main.py | 3 ++- src/sessions/ses-shinobi3levels.py | 5 +++-- src/shared/cli.py | 34 ++++++++++++++++++++---------- src/shared/config.py | 1 + src/tasks/task_base.py | 17 ++++++++++----- src/tasks/video.py | 13 +++++++----- src/tasks/videogame.py | 19 +++++++++++------ 7 files changed, 62 insertions(+), 30 deletions(-) diff --git a/main.py b/main.py index 8cff3649..10ce40dd 100755 --- a/main.py +++ b/main.py @@ -15,7 +15,7 @@ def run(parsed): try: - ses_mod = importlib.import_module('src.sessions.ses-%s'%parsed.session) + ses_mod = importlib.import_module('src.sessions.ses-%s'%parsed.tasks) tasks = ses_mod.TASKS except ImportError: raise(ValueError('session tasks file cannot be found for %s'%parsed.session)) @@ -23,6 +23,7 @@ def run(parsed): tasks[parsed.skip_n_tasks:], parsed.subject, parsed.session, + parsed.output, parsed.eyetracking, parsed.fmri, parsed.meg, diff --git a/src/sessions/ses-shinobi3levels.py b/src/sessions/ses-shinobi3levels.py index 2c2d1357..316e4af1 100644 --- a/src/sessions/ses-shinobi3levels.py +++ b/src/sessions/ses-shinobi3levels.py @@ -8,5 +8,6 @@ scenarii=['data/videogames/%s.json'%sc for sc in ['scenario_repeat1', 'scenario_Level4-1', 'scenario_Level5-1']], # this scenario repeats the same level repeat_scenario=True, max_duration=10*60, # if when level completed or dead we exceed that time in secs, stop the task - name='ShinobiIIIReturnOfTheNinjaMaster-multilevel-test') -] * 5 + name=f"shinobi-3levels-{run+1:02d}") + for run in range(5) +] diff --git a/src/shared/cli.py b/src/shared/cli.py index 7e06fa6a..55907587 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -39,23 +39,23 @@ def run_task(task, exp_win, ctl_win=None, eyetracker=None, gaze_drawer=None): # show instruction shortcut_evt = run_task_loop(task.instructions(exp_win, ctl_win), eyetracker, gaze_drawer) - if shortcut_evt: return shortcut_evt - if task.use_fmri: + if task.use_fmri and not shortcut_evt: shortcut_evt = run_task_loop(fmri.wait_for_ttl(), eyetracker, gaze_drawer) if shortcut_evt: return shortcut_evt logging.info('GO') - if eyetracker: + if eyetracker and not shortcut_evt: eyetracker.start_recording(task.name) # send start trigger/marker to MEG + Biopac (or anything else on parallel port) - if task.use_meg: + if task.use_meg and not shortcut_evt: meg.send_signal(meg.MEG_settings['TASK_START_CODE']) - shortcut_evt = run_task_loop(task.run(exp_win, ctl_win), eyetracker, gaze_drawer) + if not shortcut_evt: + shortcut_evt = run_task_loop(task.run(exp_win, ctl_win), eyetracker, gaze_drawer) # send stop trigger/marker to MEG + Biopac (or anything else on parallel port) - if task.use_meg: + if task.use_meg and not shortcut_evt: meg.send_signal(meg.MEG_settings['TASK_STOP_CODE']) if eyetracker: @@ -63,11 +63,14 @@ def run_task(task, exp_win, ctl_win=None, eyetracker=None, gaze_drawer=None): # now that time is less sensitive: save files task.save() + run_task_loop(task.stop(exp_win, ctl_win), eyetracker, gaze_drawer) + return shortcut_evt def main_loop(all_tasks, subject, session, + output_ds, enable_eyetracker=False, use_fmri=False, use_meg=False, @@ -80,10 +83,11 @@ def main_loop(all_tasks, if not allow_run_on_battery: return - log_path = os.path.abspath(os.path.join(config.OUTPUT_DIR, 'sub-%s'%subject,'ses-%s'%session)) + bids_sub_ses = ('sub-%s'%subject,'ses-%s'%session) + log_path = os.path.abspath(os.path.join(output_ds, 'sourcedata', *bids_sub_ses)) if not os.path.exists(log_path): os.makedirs(log_path, exist_ok=True) - log_name_prefix = 'sub-%s_ses-%s_%s'%(subject,session, datetime.datetime.now().strftime('%Y%m%d_%H%M%S')) + log_name_prefix = 'sub-%s_ses-%s_%s'%(subject, session, datetime.datetime.now().strftime('%Y%m%d-%H%M%S')) logfile_path = os.path.join(log_path, log_name_prefix+'.log') log_file = logging.LogFile( logfile_path, @@ -151,7 +155,8 @@ def main_loop(all_tasks, use_eyetracking = True #setup task files (eg. video) - task.setup(exp_win, log_path, log_name_prefix, + task.setup( + exp_win, log_path, log_name_prefix, use_fmri=use_fmri, use_eyetracking=use_eyetracking, use_meg=use_meg) @@ -180,7 +185,6 @@ def main_loop(all_tasks, break logging.flush() - task.stop() task.unload() exp_win.recordFrameIntervals = True @@ -211,9 +215,17 @@ def parse_args(): description=('Run all tasks in a session'), formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('--subject', '-s', + required=True, help='Subject ID') parser.add_argument('--session', '-ss', - help='Session ID') + required=True, + help='Session') + parser.add_argument('--tasks', '-t', + required=True, + help='tasks set') + parser.add_argument('--output', '-o', + required=True, + help='output dataset') parser.add_argument('--fmri', '-f', help='Wait for fmri TTL to start each task', action='store_true') diff --git a/src/shared/config.py b/src/shared/config.py index dafd5588..e397b8b8 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -23,6 +23,7 @@ pos = (100,0), screen=0, gammaErrorPolicy='warn', + waitBlanking=False, # avoid ctrl window to block the script in case of differing refresh rate. ) FRAME_RATE=60 diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index 44e1e8f6..87a94019 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -50,11 +50,11 @@ def _flip_all_windows(self, exp_win, ctl_win=None, clearBuffer=True): def instructions(self, exp_win, ctl_win): if hasattr(self, '_instructions'): for clearBuffer in self._instructions(exp_win, ctl_win): - self._flip_all_windows(exp_win, ctl_win, clearBuffer) yield + self._flip_all_windows(exp_win, ctl_win, clearBuffer) # last/only flip to clear screen - self._flip_all_windows(exp_win, ctl_win) yield + self._flip_all_windows(exp_win, ctl_win, True) def run(self, exp_win, ctl_win): @@ -70,7 +70,6 @@ def run(self, exp_win, ctl_win): # yield first to allow external draw before flipping yield self._flip_all_windows(exp_win, ctl_win, clearBuffer) - # increment the progress bar every second if progress_bar: frame_idx += 1 @@ -81,8 +80,14 @@ def run(self, exp_win, ctl_win): progress_bar.clear() progress_bar.close() - def stop(self): - pass + def stop(self, exp_win, ctl_win): + if hasattr(self, '_stop'): + for clearBuffer in self._stop(exp_win, ctl_win): + yield + self._flip_all_windows(exp_win, ctl_win, clearBuffer) + # 2 flips to clear screen and backbuffer + for i in range(2): + self._flip_all_windows(exp_win, ctl_win, True) def restart(self): if hasattr(self, '_restart'): @@ -118,6 +123,8 @@ def _run(self, exp_win, ctl_win): screen_text.draw(ctl_win) yield True + def _stop(self, exp_win, ctl_win): + yield True class Fixation(Task): diff --git a/src/tasks/video.py b/src/tasks/video.py index 15c45a8e..9d9e572b 100644 --- a/src/tasks/video.py +++ b/src/tasks/video.py @@ -69,13 +69,16 @@ def _run(self, exp_win, ctl_win): if ctl_win: self.movie_stim.draw(ctl_win) yield False - for frameN in range(config.FRAME_RATE * FADE_TO_GREY_DURATION): - exp_win.setColor([float(frameN)/config.FRAME_RATE/FADE_TO_GREY_DURATION-1]*3) - yield True - def stop(self): + + def _stop(self, exp_win, ctl_win): self.movie_stim.stop() - self.movie_stim.win.setColor([0,0,0]) + for frameN in range(config.FRAME_RATE * FADE_TO_GREY_DURATION): + grey = [float(frameN)/config.FRAME_RATE/FADE_TO_GREY_DURATION-1]*3 + exp_win.setColor(grey) + if ctl_win: + ctl_win.setColor(grey) + yield True def _restart(self): self.movie_stim.setMovie(self.filepath) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 5c12ba50..1a654f46 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -70,8 +70,9 @@ def _render_graphics_sound(self, obs, sound_block, exp_win, ctl_win): if not self.game_sound.status == constants.PLAYING: self.game_sound.play() - def stop(self): + def _stop(self, exp_win, ctl_win): self.game_sound.stop() + yield True def unload(self): self.emulator.close() @@ -107,6 +108,7 @@ def _instructions(self, exp_win, ctl_win): if ctl_win: screen_text.draw(ctl_win) yield + yield True def _setup(self, exp_win): @@ -134,14 +136,14 @@ def _run(self, exp_win, ctl_win): self.emulator.reset() nnn = 0 while True: - movie_path = os.path.join( + self.movie_path = os.path.join( self.output_path, - "%s_%s_%s_%03d.bk2"%(self.output_fname_base,self.game_name,self.state_name, nnn)) - if not os.path.exists(movie_path): + "%s_%s_%s_%03d.bk2"%(self.output_fname_base, self.game_name, self.state_name, nnn)) + if not os.path.exists(self.movie_path): break nnn += 1 - logging.exp('VideoGame: recording movie in %s'%movie_path) - self.emulator.record_movie(movie_path) + logging.exp('VideoGame: recording movie in %s'%self.movie_path) + self.emulator.record_movie(self.movie_path) total_reward = 0 exp_win.logOnFlip( @@ -169,6 +171,9 @@ def _run(self, exp_win, ctl_win): self._render_graphics_sound(_obs,self.emulator.em.get_audio(),exp_win, ctl_win) yield if _done: + exp_win.logOnFlip( + level=logging.EXP, + msg='VideoGame %s: %s stopped at %f'%(self.game_name, self.state_name, time.time())) break if not self.repeat_scenario or \ @@ -199,10 +204,12 @@ def __init__(self, *args,**kwargs): def _run(self, exp_win, ctl_win): while True: for level, scenario in zip(self._state_names, self._scenarii): + self.state_name = level self.emulator.load_state(level) self.emulator.data.load( retro.data.get_file_path(self.game_name, 'data.json'), scenario) + yield from super()._run(exp_win, ctl_win) time_exceeded = self.max_duration and self.task_timer.getTime() > self.max_duration if time_exceeded: # stop if we are above the planned duration From ce5412e072abf3c437dc4bf93e9cac3902ada306 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 11 Sep 2020 15:58:14 -0400 Subject: [PATCH 037/170] refactor videogame code and optimize to avoid lags --- src/shared/cli.py | 4 +- src/tasks/videogame.py | 168 ++++++++++++++++++++++++++--------------- 2 files changed, 109 insertions(+), 63 deletions(-) diff --git a/src/shared/cli.py b/src/shared/cli.py index 55907587..f2bae747 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -1,8 +1,10 @@ # CLI: command line interface options and main loop -import os, datetime, traceback, glob +import os, datetime, traceback, glob, time from psychopy import core, visual, logging, event +visual.window.reportNDroppedFrames = 10e10 + TIMEOUT = 5 DELAY_BETWEEN_TASK = 5 diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 1a654f46..68f48c22 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -61,13 +61,31 @@ def _nextBlock(self): class VideoGameBase(Task): + + def _setup(self, exp_win): + self.game_vis_stim = visual.ImageStim( + exp_win, + size=exp_win.size, + units='pixels', + interpolate=False, + flipVert=True, + autoLog=False) + self.game_sound = SoundDeviceBlockStream(stereo=True, blockSize=735) + first_frame = self.emulator.reset() + first_audio_block = self.emulator.em.get_audio() + self.game_sound.add_block(self._transform_soundblock(first_audio_block)) + + def _transform_soundblock(self, sound_block): + return sound_block[:735]/float(2**15) + def _render_graphics_sound(self, obs, sound_block, exp_win, ctl_win): - self.game_vis_stim.image = np.flip(obs,0)/255. + self.game_vis_stim.image = obs/255. #np.flip(obs, 0)/255. self.game_vis_stim.draw(exp_win) if ctl_win: self.game_vis_stim.draw(ctl_win) - self.game_sound.add_block(sound_block[:735]/float(2**15)) + self.game_sound.add_block(self._transform_soundblock(sound_block)) if not self.game_sound.status == constants.PLAYING: + print("start sound") self.game_sound.play() def _stop(self, exp_win, ctl_win): @@ -79,7 +97,7 @@ def unload(self): class VideoGame(VideoGameBase): - DEFAULT_INSTRUCTION = "Let's play a video game.\n%s : %s\nHave fun!" + DEFAULT_INSTRUCTION = "Let's play a video game.\n%s: %s\nHave fun!" def __init__(self, game_name=DEFAULT_GAME_NAME, @@ -95,12 +113,13 @@ def __init__(self, self.scenario = scenario self.repeat_scenario = repeat_scenario self.max_duration = max_duration - self.instruction = self.instruction%(self.game_name, self.state_name) def _instructions(self, exp_win, ctl_win): + instruction = self.instruction%(self.game_name, self.state_name) + screen_text = visual.TextStim( - exp_win, text=self.instruction, + exp_win, text=instruction, alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): @@ -118,74 +137,87 @@ def _setup(self, exp_win): scenario=self.scenario, record=False) - self.game_vis_stim = visual.ImageStim(exp_win,size=exp_win.size,units='pixels',autoLog=False) - self.game_sound = SoundDeviceBlockStream(stereo=True, blockSize=735) + super()._setup(exp_win) + + self._set_recording_file() + + def _set_recording_file(self): + nnn = 0 + while True: + self.movie_path = os.path.join( + self.output_path, + "%s_%s_%s_%03d.bk2"%(self.output_fname_base, self.game_name, self.state_name, nnn)) + if not os.path.exists(self.movie_path): + break + nnn += 1 + logging.exp('VideoGame: recording movie in %s'%self.movie_path) + self.emulator.record_movie(self.movie_path) + + def _run_emulator(self, exp_win, ctl_win): - def _run(self, exp_win, ctl_win): global _keyReleaseBuffer, _keyPressBuffer + + total_reward = 0 + _done = False + level_step = 0 + keys = [False]*12 + + while not _done: + for k in _keyReleaseBuffer: + #print('release',k) + if k[0] in KEY_SET: + keys[KEY_SET.index(k[0])] = False + _keyReleaseBuffer.clear() + for k in _keyPressBuffer: + #print('press',k) + if k[0] in KEY_SET: + keys[KEY_SET.index(k[0])] = True + _keyPressBuffer.clear() + + _obs, _rew, _done, _info = self.emulator.step(keys) + total_reward += _rew + if _rew > 0 : + exp_win.logOnFlip(level=logging.EXP, msg='Reward %f'%(total_reward)) + self._render_graphics_sound(_obs, self.emulator.em.get_audio(), exp_win, ctl_win) + if _done: + exp_win.logOnFlip( + level=logging.EXP, + msg='VideoGame %s: %s stopped at %f'%(self.game_name, self.state_name, time.time())) + if not level_step%config.FRAME_RATE: + exp_win.logOnFlip(level=logging.EXP, msg="level step: %d"%level_step) + yield + level_step+=1 + + def _set_key_handler(self, exp_win): # activate repeat keys exp_win.winHandle.on_key_press = _onPygletKeyPress exp_win.winHandle.on_key_release = _onPygletKeyRelease - if ctl_win: - ctl_win.winHandle.on_key_press = _onPygletKeyPress - ctl_win.winHandle.on_key_release = _onPygletKeyRelease - keys = [False]*12 + def _unset_key_handler(self, exp_win): + # deactivate custom keys handling + exp_win.winHandle.on_key_press = event._onPygletKey + del exp_win.winHandle.on_key_release - while True: - self.emulator.reset() - nnn = 0 - while True: - self.movie_path = os.path.join( - self.output_path, - "%s_%s_%s_%03d.bk2"%(self.output_fname_base, self.game_name, self.state_name, nnn)) - if not os.path.exists(self.movie_path): - break - nnn += 1 - logging.exp('VideoGame: recording movie in %s'%self.movie_path) - self.emulator.record_movie(self.movie_path) + def _run(self, exp_win, ctl_win): + + self._set_key_handler(exp_win) + exp_win.waitBlanking = False - total_reward = 0 + while True: exp_win.logOnFlip( level=logging.EXP, msg='VideoGame %s: %s starting at %f'%(self.game_name, self.state_name, time.time())) - while True: - # TODO: get real action from controller - #gamectrl_keys = event.getKeys(list(KEY_SET)) - #keys = [k in gamectrl_keys for k in KEY_SET] - for k in _keyReleaseBuffer: - #print('release',k) - if k[0] in KEY_SET: - keys[KEY_SET.index(k[0])] = False - _keyReleaseBuffer.clear() - for k in _keyPressBuffer: - #print('press',k) - if k[0] in KEY_SET: - keys[KEY_SET.index(k[0])] = True - _keyPressBuffer.clear() - - _obs, _rew, _done, _info = self.emulator.step(keys) - total_reward += _rew - if _rew > 0 : - exp_win.logOnFlip(level=logging.EXP, msg='Reward %f'%(total_reward)) - self._render_graphics_sound(_obs,self.emulator.em.get_audio(),exp_win, ctl_win) - yield - if _done: - exp_win.logOnFlip( - level=logging.EXP, - msg='VideoGame %s: %s stopped at %f'%(self.game_name, self.state_name, time.time())) - break + + yield from self._run_emulator(exp_win, ctl_win) if not self.repeat_scenario or \ (self.max_duration and self.task_timer.getTime() > self.max_duration): # stop if we are above the planned duration break - # deactivate custom keys handling - exp_win.winHandle.on_key_press = event._onPygletKey - del exp_win.winHandle.on_key_release - if ctl_win: - ctl_win.winHandle.on_key_press = event._onPygletKey - del ctl_win.winHandle.on_key_release + self.emulator.reset() + + self._unset_key_handler(exp_win) + exp_win.waitBlanking = True class VideoGameMultiLevel(VideoGame): @@ -202,21 +234,34 @@ def __init__(self, *args,**kwargs): **kwargs) def _run(self, exp_win, ctl_win): + + self._set_key_handler(exp_win) + exp_win.waitBlanking = False + + nlevels = 0 while True: for level, scenario in zip(self._state_names, self._scenarii): + nlevels += 1 self.state_name = level self.emulator.load_state(level) self.emulator.data.load( retro.data.get_file_path(self.game_name, 'data.json'), scenario) + self.emulator.reset() + if nlevels > 1: + self._set_recording_file() + yield from self._instructions(exp_win, ctl_win) - yield from super()._run(exp_win, ctl_win) + yield from super()._run_emulator(exp_win, ctl_win) time_exceeded = self.max_duration and self.task_timer.getTime() > self.max_duration if time_exceeded: # stop if we are above the planned duration break if time_exceeded or not self._repeat_scenario_multilevel: break + self._unset_key_handler(exp_win) + exp_win.waitBlanking = True + class VideoGameReplay(VideoGameBase): def __init__(self, movie_filename, game_name=DEFAULT_GAME_NAME, scenario=None, *args, **kwargs): @@ -239,7 +284,7 @@ def instructions(self, exp_win, ctl_win): screen_text.draw(ctl_win) yield - def setup(self, exp_win, output_path, output_fname_base): + def _setup(self, exp_win): super().setup(exp_win, output_path, output_fname_base) self.movie = retro.Movie(self.movie_filename) self.emulator = retro.make( @@ -249,11 +294,10 @@ def setup(self, exp_win, output_path, output_fname_base): scenario=self.scenario, #use_restricted_actions=retro.Actions.ALL, players=self.movie.players) + self.emulator.initial_state = self.movie.get_state() - self.emulator.reset() + super()._setup(exp_win) - self.game_vis_stim = visual.ImageStim(exp_win,size=exp_win.size,units='pixels',autoLog=False) - self.game_sound = SoundDeviceBlockStream(stereo=True, blockSize=735) def _run(self, exp_win, ctl_win): # give the original size of the movie in pixels: From be0b3668491176a3cc323d620e5009ccd8d971c2 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 14 Sep 2020 13:12:49 -0400 Subject: [PATCH 038/170] add pause to shinobi --- src/sessions/ses-shinobi3levels.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sessions/ses-shinobi3levels.py b/src/sessions/ses-shinobi3levels.py index 316e4af1..5b1b5357 100644 --- a/src/sessions/ses-shinobi3levels.py +++ b/src/sessions/ses-shinobi3levels.py @@ -1,13 +1,15 @@ from ..tasks import images, videogame, memory, task_base -TASKS = [ +TASKS = sum([ - videogame.VideoGameMultiLevel( + [videogame.VideoGameMultiLevel( state_names=['Level1-0','Level4-1','Level5-0'], scenarii=['data/videogames/%s.json'%sc for sc in ['scenario_repeat1', 'scenario_Level4-1', 'scenario_Level5-1']], # this scenario repeats the same level repeat_scenario=True, max_duration=10*60, # if when level completed or dead we exceed that time in secs, stop the task - name=f"shinobi-3levels-{run+1:02d}") + name=f"shinobi-3levels-{run+1:02d}"), + task_base.Pause()] for run in range(5) -] + +],[]) From edeb9241a5392ce1db511be72dabf0e46672ff5e Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 14 Sep 2020 16:36:46 -0400 Subject: [PATCH 039/170] change doc for CLI changes --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f84ff926..fb36c15d 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,13 @@ mkdir output ## how to launch a session -`python3 main.py --subject test --ses videoshorttest --eyetracking --fmri` +`python3 main.py --subject test --session video003 --tasks videoshorttest --eyetracking --fmri -o /path/to/dataset/` - --subject: can be whatever, will be used to save data in a bids-like structure -- --session: must match the name of a session script in `src/ses-.py`, which contains the tasks to be ran on that session +- --session: a session identifier that will be used to save the data in the BIDS +- --tasks: must match the name of a session script in `src/ses-.py`, which contains the tasks to be ran on that session - --eyetracking: turn on eyetracking, start pupil software and recording of eye +- -o : specifies the path to the root of the dataset where to output the data (in sourcedata or BIDS ) - --fmri: will wait for TTL (can be emulated with character `5` on the keyboard) to start the tasks that are labeled as fmri dependent. When not using that flag, tasks will run back to back. It will also append a video loop at the beginning of the session in order for the participant to have sound and visual stimuli to test the setup (then skip to start the session). From bf1c273721a962f93776f696627af5aa6285cf4f Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 14 Sep 2020 16:37:13 -0400 Subject: [PATCH 040/170] display initial frame and soundblock --- src/tasks/videogame.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 68f48c22..0183a62e 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -71,9 +71,7 @@ def _setup(self, exp_win): flipVert=True, autoLog=False) self.game_sound = SoundDeviceBlockStream(stereo=True, blockSize=735) - first_frame = self.emulator.reset() - first_audio_block = self.emulator.em.get_audio() - self.game_sound.add_block(self._transform_soundblock(first_audio_block)) + self._first_frame = self.emulator.reset() def _transform_soundblock(self, sound_block): return sound_block[:735]/float(2**15) @@ -162,7 +160,13 @@ def _run_emulator(self, exp_win, ctl_win): level_step = 0 keys = [False]*12 + + # render the initial frame and audio + self._render_graphics_sound(self._first_frame, self.emulator.em.get_audio(), exp_win, ctl_win) + exp_win.logOnFlip(level=logging.EXP, msg="level step: %d"%level_step) + yield while not _done: + level_step += 1 for k in _keyReleaseBuffer: #print('release',k) if k[0] in KEY_SET: @@ -186,7 +190,6 @@ def _run_emulator(self, exp_win, ctl_win): if not level_step%config.FRAME_RATE: exp_win.logOnFlip(level=logging.EXP, msg="level step: %d"%level_step) yield - level_step+=1 def _set_key_handler(self, exp_win): # activate repeat keys @@ -216,8 +219,10 @@ def _run(self, exp_win, ctl_win): break self.emulator.reset() + def _stop(self, exp_win): self._unset_key_handler(exp_win) exp_win.waitBlanking = True + yield from super()._stop() class VideoGameMultiLevel(VideoGame): @@ -247,12 +252,13 @@ def _run(self, exp_win, ctl_win): self.emulator.data.load( retro.data.get_file_path(self.game_name, 'data.json'), scenario) - self.emulator.reset() + self._first_frame = self.emulator.reset() if nlevels > 1: self._set_recording_file() yield from self._instructions(exp_win, ctl_win) yield from super()._run_emulator(exp_win, ctl_win) + self.game_sound.stop() time_exceeded = self.max_duration and self.task_timer.getTime() > self.max_duration if time_exceeded: # stop if we are above the planned duration break From 953f3a6f994d7509cf04db1def965159784488be Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 15 Sep 2020 16:36:53 -0400 Subject: [PATCH 041/170] code basic likert scale for games self-eval during scan --- src/sessions/ses-shinobi3levels.py | 18 ++++--- src/tasks/videogame.py | 87 ++++++++++++++++++++++++------ 2 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/sessions/ses-shinobi3levels.py b/src/sessions/ses-shinobi3levels.py index 5b1b5357..5f67b3ef 100644 --- a/src/sessions/ses-shinobi3levels.py +++ b/src/sessions/ses-shinobi3levels.py @@ -3,13 +3,17 @@ TASKS = sum([ - [videogame.VideoGameMultiLevel( - state_names=['Level1-0','Level4-1','Level5-0'], - scenarii=['data/videogames/%s.json'%sc for sc in ['scenario_repeat1', 'scenario_Level4-1', 'scenario_Level5-1']], # this scenario repeats the same level - repeat_scenario=True, - max_duration=10*60, # if when level completed or dead we exceed that time in secs, stop the task - name=f"shinobi-3levels-{run+1:02d}"), - task_base.Pause()] + [ + videogame.VideoGameMultiLevel( + state_names=['Level1-0','Level4-1','Level5-0'], + scenarii=['data/videogames/%s.json'%sc for sc in ['scenario_repeat1', 'scenario_Level4-1', 'scenario_Level5-1']], # this scenario repeats the same level + repeat_scenario=True, + max_duration=10*60, # if when level completed or dead we exceed that time in secs, stop the task + name=f"shinobi-3levels-{run+1:02d}", + post_level_ratings = [("rate your performance", 7), ("rate your flow", 7)] + ), + task_base.Pause() + ] for run in range(5) ],[]) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 0183a62e..8327703b 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -103,6 +103,7 @@ def __init__(self, scenario=None, repeat_scenario=True, max_duration=0, + post_level_ratings=None, *args,**kwargs): super().__init__(**kwargs) @@ -111,6 +112,7 @@ def __init__(self, self.scenario = scenario self.repeat_scenario = repeat_scenario self.max_duration = max_duration + self.post_level_ratings = post_level_ratings def _instructions(self, exp_win, ctl_win): @@ -151,32 +153,34 @@ def _set_recording_file(self): logging.exp('VideoGame: recording movie in %s'%self.movie_path) self.emulator.record_movie(self.movie_path) - def _run_emulator(self, exp_win, ctl_win): + def _handle_controller_presses(self): + global _keyPressBuffer, _keyReleaseBuffer + + for k in _keyReleaseBuffer: + #print('release',k) + self.pressed_keys.discard(k[0]) + _keyReleaseBuffer.clear() + for k in _keyPressBuffer: + #print('press',k) + self.pressed_keys.add(k[0]) + _keyPressBuffer.clear() + return self.pressed_keys - global _keyReleaseBuffer, _keyPressBuffer + def _run_emulator(self, exp_win, ctl_win): total_reward = 0 _done = False level_step = 0 keys = [False]*12 - # render the initial frame and audio self._render_graphics_sound(self._first_frame, self.emulator.em.get_audio(), exp_win, ctl_win) exp_win.logOnFlip(level=logging.EXP, msg="level step: %d"%level_step) yield while not _done: level_step += 1 - for k in _keyReleaseBuffer: - #print('release',k) - if k[0] in KEY_SET: - keys[KEY_SET.index(k[0])] = False - _keyReleaseBuffer.clear() - for k in _keyPressBuffer: - #print('press',k) - if k[0] in KEY_SET: - keys[KEY_SET.index(k[0])] = True - _keyPressBuffer.clear() + self._handle_controller_presses() + keys = [k in self.pressed_keys for k in KEY_SET] _obs, _rew, _done, _info = self.emulator.step(keys) total_reward += _rew @@ -195,6 +199,7 @@ def _set_key_handler(self, exp_win): # activate repeat keys exp_win.winHandle.on_key_press = _onPygletKeyPress exp_win.winHandle.on_key_release = _onPygletKeyRelease + self.pressed_keys = set() def _unset_key_handler(self, exp_win): # deactivate custom keys handling @@ -212,17 +217,65 @@ def _run(self, exp_win, ctl_win): msg='VideoGame %s: %s starting at %f'%(self.game_name, self.state_name, time.time())) yield from self._run_emulator(exp_win, ctl_win) - + if self.post_level_ratings: + yield from self._run_ratings(exp_win, ctl_win) if not self.repeat_scenario or \ (self.max_duration and self.task_timer.getTime() > self.max_duration): # stop if we are above the planned duration break self.emulator.reset() - def _stop(self, exp_win): + def _run_ratings(self, exp_win, ctl_win): + for question, n_pts in self.post_level_ratings: + yield from self._likert_scale_answer(exp_win, ctl_win, question, n_pts) + + text = visual.TextStim(exp_win, 'Thanks for you answers', pos=(0, .5)) + for i in range(config.FRAME_RATE): + text.draw(exp_win) + yield i<2 + + def _likert_scale_answer(self, exp_win, ctl_win, question, n_pts=7, extent=.6): + extent *= config.EXP_WINDOW['size'][0] + value = n_pts//2 + answered = False + text = visual.TextStim(exp_win, question, pos=(0, .5)) + line = visual.Line(exp_win, (-extent, 0), (extent, 0), units='pixels', lineWidth=2) + x_spacing = extent*2/(n_pts-1) + circles = [ + visual.Circle( + exp_win, + units='pixels', + radius=40, pos=(-extent+i*x_spacing, 0), + fillColor=(-1,-1,-1), lineColor=(-1,-1,-1), lineWidth=10) + for i in range(n_pts) + ] + circles[value].fillColor = (1,1,1) + while not answered: + self._handle_controller_presses() + if 'r' in self.pressed_keys and value < n_pts-1: + value+=1 + self.pressed_keys.discard('r') + elif 'l' in self.pressed_keys and value > 0: + value-=1 + self.pressed_keys.discard('l') + for c in circles: + c.fillColor = (-1,-1,-1) + circles[value].fillColor = (1,1,1) + + if 'a' in self.pressed_keys: + for i in range(config.FRAME_RATE): + yield i<2 + break + self.pressed_keys.clear() + + for stim in [text, line] + circles: + stim.draw(exp_win) + yield True + + def _stop(self, exp_win, ctl_win): self._unset_key_handler(exp_win) exp_win.waitBlanking = True - yield from super()._stop() + yield from super()._stop(exp_win, ctl_win) class VideoGameMultiLevel(VideoGame): @@ -259,6 +312,8 @@ def _run(self, exp_win, ctl_win): yield from super()._run_emulator(exp_win, ctl_win) self.game_sound.stop() + if self.post_level_ratings: + yield from self._run_ratings(exp_win, ctl_win) time_exceeded = self.max_duration and self.task_timer.getTime() > self.max_duration if time_exceeded: # stop if we are above the planned duration break From b879a0e221cb90e5234410594d59925c8b2577aa Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 16 Sep 2020 09:48:22 -0400 Subject: [PATCH 042/170] remove autolog from likerts stimuli --- src/tasks/videogame.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 8327703b..0e4ebc05 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -126,7 +126,7 @@ def _instructions(self, exp_win, ctl_win): screen_text.draw(exp_win) if ctl_win: screen_text.draw(ctl_win) - yield + yield frameN < 2 yield True def _setup(self, exp_win): @@ -210,8 +210,10 @@ def _run(self, exp_win, ctl_win): self._set_key_handler(exp_win) exp_win.waitBlanking = False + self._nlevels = 0 while True: + self._nlevels += 1 exp_win.logOnFlip( level=logging.EXP, msg='VideoGame %s: %s starting at %f'%(self.game_name, self.state_name, time.time())) @@ -239,14 +241,15 @@ def _likert_scale_answer(self, exp_win, ctl_win, question, n_pts=7, extent=.6): value = n_pts//2 answered = False text = visual.TextStim(exp_win, question, pos=(0, .5)) - line = visual.Line(exp_win, (-extent, 0), (extent, 0), units='pixels', lineWidth=2) + line = visual.Line(exp_win, (-extent, 0), (extent, 0), units='pixels', lineWidth=2, autoLog=False) x_spacing = extent*2/(n_pts-1) circles = [ visual.Circle( exp_win, units='pixels', radius=40, pos=(-extent+i*x_spacing, 0), - fillColor=(-1,-1,-1), lineColor=(-1,-1,-1), lineWidth=10) + fillColor=(-1,-1,-1), lineColor=(-1,-1,-1), lineWidth=10, + autoLog=False) for i in range(n_pts) ] circles[value].fillColor = (1,1,1) @@ -263,6 +266,9 @@ def _likert_scale_answer(self, exp_win, ctl_win, question, n_pts=7, extent=.6): circles[value].fillColor = (1,1,1) if 'a' in self.pressed_keys: + exp_win.logOnFlip( + level=logging.EXP, + msg='nlevel: %d, question: %s, answer: %d'%(self._nlevels, question, value)) for i in range(config.FRAME_RATE): yield i<2 break @@ -296,17 +302,17 @@ def _run(self, exp_win, ctl_win): self._set_key_handler(exp_win) exp_win.waitBlanking = False - nlevels = 0 + self._nlevels = 0 while True: for level, scenario in zip(self._state_names, self._scenarii): - nlevels += 1 + self._nlevels += 1 self.state_name = level self.emulator.load_state(level) self.emulator.data.load( retro.data.get_file_path(self.game_name, 'data.json'), scenario) self._first_frame = self.emulator.reset() - if nlevels > 1: + if self._nlevels > 1: self._set_recording_file() yield from self._instructions(exp_win, ctl_win) @@ -314,6 +320,7 @@ def _run(self, exp_win, ctl_win): self.game_sound.stop() if self.post_level_ratings: yield from self._run_ratings(exp_win, ctl_win) + time_exceeded = self.max_duration and self.task_timer.getTime() > self.max_duration if time_exceeded: # stop if we are above the planned duration break From d5764e3c389dbb731660e29668cbfa4623e5f8b5 Mon Sep 17 00:00:00 2001 From: unf Date: Fri, 18 Sep 2020 10:14:57 -0400 Subject: [PATCH 043/170] bunch of changes before covid, need to merge with all changes done on different computers --- src/shared/cli.py | 20 +++++++++++++------- src/shared/config.py | 5 ++++- src/shared/eyetracking.py | 5 +++-- src/tasks/task_base.py | 9 ++++----- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/shared/cli.py b/src/shared/cli.py index 7e06fa6a..5cc66b08 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -89,14 +89,18 @@ def main_loop(all_tasks, logfile_path, level=logging.INFO, filemode='w') + exp_win = visual.Window(**config.EXP_WINDOW) + exp_win.mouseVisible = False + exp_win.recordFrameIntervals = True + exp_win.waitBlanking = False + if show_ctl_win: ctl_win = visual.Window(**config.CTL_WINDOW) - ctl_win.winHandle.set_caption('Stimuli') + ctl_win.name = 'Stimuli' + ctl_win.waitBlanking = False + ctl_win.recordFrameIntervals = True else: ctl_win = None - exp_win = visual.Window(**config.EXP_WINDOW) - exp_win.mouseVisible = False - ptt = None if enable_ptt: @@ -161,7 +165,7 @@ def main_loop(all_tasks, #force focus on the task window to ensure getting keys, TTL, ... exp_win.winHandle.activate() # record frame intervals for debug - exp_win.recordFrameIntervals = True + shortcut_evt = run_task(task, exp_win, ctl_win, eyetracker_client, gaze_drawer) @@ -183,8 +187,6 @@ def main_loop(all_tasks, task.stop() task.unload() - exp_win.recordFrameIntervals = True - exp_win.saveFrameIntervals('frame_intervals.txt') if shortcut_evt=='q': print('quit') @@ -196,6 +198,10 @@ def main_loop(all_tasks, for i in range(DELAY_BETWEEN_TASK*config.FRAME_RATE): exp_win.flip(clearBuffer=False) + exp_win.saveFrameIntervals('exp_win_frame_intervals.txt') + if ctl_win: + ctl_win.saveFrameIntervals('ctl_win_frame_intervals.txt') + except KeyboardInterrupt as ki: print(traceback.format_exc()) logging.exp(msg="user killing the program") diff --git a/src/shared/config.py b/src/shared/config.py index dafd5588..d3f2ca83 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -2,13 +2,14 @@ # avoids delay in movie3 audio seek prefs.hardware['audioLib'] = ['sounddevice'] - +#prefs.hardware['general'] = ['glfw'] OUTPUT_DIR = 'output' EYETRACKING_ROI = (60,30,660,450) EXP_WINDOW = dict( +# winType='glfw', # size = (1280,1024), size = (1024, 768), # size = (1920, 1080), @@ -18,11 +19,13 @@ ) CTL_WINDOW = dict( +# winType='glfw', # size = (1920, 1080), size = (1280, 1024), pos = (100,0), screen=0, gammaErrorPolicy='warn', + swapInterval=0., ) FRAME_RATE=60 diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index 06456412..cee3bb57 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -260,15 +260,16 @@ def run(self): ipc_sub_port = int(self._req_socket.recv()) self.pupil_monitor = Msg_Receiver(self._ctx,'tcp://localhost:%d'%ipc_sub_port,topics=('gaze','pupil')) while not self.stoprequest.isSet(): - time.sleep(.001) + time.sleep(1/120.) msg = self.pupil_monitor.recv() - if not msg is None: + while not msg is None: topic, tmp = msg with self.lock: if topic.startswith('pupil'): self.pupil = tmp if topic.startswith('gaze'): self.gaze = tmp + msg = self.pupil_monitor.recv() print('eyetracker listener: stopping') diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index 44e1e8f6..f88922e5 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -42,10 +42,9 @@ def __str__(self): return '%s : %s'%(self.__class__, self.name) def _flip_all_windows(self, exp_win, ctl_win=None, clearBuffer=True): + if not ctl_win is None: + ctl_win.flip(clearBuffer=clearBuffer) exp_win.flip(clearBuffer=clearBuffer) - if ctl_win is None: - return - ctl_win.flip(clearBuffer=clearBuffer) def instructions(self, exp_win, ctl_win): if hasattr(self, '_instructions'): @@ -67,10 +66,10 @@ def run(self, exp_win, ctl_win): frame_idx = 0 for clearBuffer in self._run(exp_win, ctl_win): - # yield first to allow external draw before flipping + # yield first to allow external draw before flip yield self._flip_all_windows(exp_win, ctl_win, clearBuffer) - + # increment the progress bar every second if progress_bar: frame_idx += 1 From a93e6f3439358bda7c662ac612f90619bad05121 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 21 Sep 2020 14:16:43 -0400 Subject: [PATCH 044/170] coding videogame questionaire. --- src/shared/config.py | 4 +- src/tasks/videogame.py | 109 +++++++++++++++++++++++++++++++++-------- 2 files changed, 90 insertions(+), 23 deletions(-) diff --git a/src/shared/config.py b/src/shared/config.py index ee6d7bfa..dddb5b2c 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -10,8 +10,8 @@ EXP_WINDOW = dict( # winType='glfw', -# size = (1280,1024), - size = (1024, 768), + size = (1280,1024), +# size = (1024, 768), # size = (1920, 1080), screen=1, fullscr=True, diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 0e4ebc05..43808719 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -41,13 +41,17 @@ class SoundDeviceBlockStream(sound.backend_sounddevice.SoundDeviceSound): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.blocks = queue.Queue() + self.blocks = queue.SimpleQueue() self.lock = threading.Lock() def add_block(self, block): with self.lock: self.blocks.put(block) + def flush(self): + with self.lock: + self.blocks = queue.SimpleQueue() + def _nextBlock(self): if self.status == constants.STOPPED: return @@ -83,8 +87,7 @@ def _render_graphics_sound(self, obs, sound_block, exp_win, ctl_win): self.game_vis_stim.draw(ctl_win) self.game_sound.add_block(self._transform_soundblock(sound_block)) if not self.game_sound.status == constants.PLAYING: - print("start sound") - self.game_sound.play() + exp_win.callOnFlip(self.game_sound.play) # start sound only at flip def _stop(self, exp_win, ctl_win): self.game_sound.stop() @@ -138,7 +141,7 @@ def _setup(self, exp_win): record=False) super()._setup(exp_win) - + self._events = [] self._set_recording_file() def _set_recording_file(self): @@ -173,6 +176,8 @@ def _run_emulator(self, exp_win, ctl_win): level_step = 0 keys = [False]*12 + # flush all keys to avoid unwanted actions + self.pressed_keys.clear() # render the initial frame and audio self._render_graphics_sound(self._first_frame, self.emulator.em.get_audio(), exp_win, ctl_win) exp_win.logOnFlip(level=logging.EXP, msg="level step: %d"%level_step) @@ -231,12 +236,69 @@ def _run_ratings(self, exp_win, ctl_win): for question, n_pts in self.post_level_ratings: yield from self._likert_scale_answer(exp_win, ctl_win, question, n_pts) - text = visual.TextStim(exp_win, 'Thanks for you answers', pos=(0, .5)) + text = visual.TextStim(exp_win, 'Thanks for your answers', pos=(0, 0)) for i in range(config.FRAME_RATE): text.draw(exp_win) - yield i<2 + yield i<3 + # clear screen + for i in range(2): + yield True + + def _questionnaire(self, exp_win, ctl_win): + if self.post_level_ratings is None: + return + lines = [] + bullets = [] + responses = [] + y_spacing = 40 + win_width = exp_win.size[0] + scales_block_x = win_width*.25 + scales_block_y = -len(self.post_level_ratings)/2 * y_spacing + extent = win_width * .2 + + + # create all stimuli + all_questions_text = "" + for q_n, q_vals in enumerate(self.post_level_ratings): + question, n_pts = q_vals + print(question) + default_response = n_pts//2 + responses.append(default_response) + x_spacing = extent*2/(n_pts-1) + all_questions_text += question + '\n\n' + + lines.append( + visual.Line( + exp_win, + (scales_block_x-extent, scales_block_y+q_n*y_spacing), (scales_block_x+extent, scales_block_y+q_n*y_spacing), + units='pixels', lineWidth=2, autoLog=False, + lineColor=((0,-1,-1) if q_n==0 else(-1,-1,-1)) + ) + ) + bullets.append([ + visual.Circle( + exp_win, + units='pixels', + radius=10, pos=(scales_block_x-extent+i*x_spacing, scales_block_y+q_n*y_spacing), + fillColor=((1,1,1) if default_response==i else (-1,-1,-1)), + lineColor=(-1,-1,-1), lineWidth=10, + autoLog=False) + for i in range(n_pts) + ]) + + text = visual.TextStim( + exp_win, all_questions_text, units='pixels', + pos=(.1*win_width, 0), wrapWidth=win_width*.5, + height = y_spacing/2, + anchorHoriz='right') + + # questionnaire interaction loop + while True: + for stim in lines + sum(bullets,[]) + [text]: + stim.draw(exp_win) + yield True - def _likert_scale_answer(self, exp_win, ctl_win, question, n_pts=7, extent=.6): + def _likert_scale_answer(self, exp_win, ctl_win, question, n_pts=7, extent=.6, autoLog=False): extent *= config.EXP_WINDOW['size'][0] value = n_pts//2 answered = False @@ -253,30 +315,34 @@ def _likert_scale_answer(self, exp_win, ctl_win, question, n_pts=7, extent=.6): for i in range(n_pts) ] circles[value].fillColor = (1,1,1) + frame = 0 while not answered: + frame += 1 + for stim in [text, line] + circles: + stim.draw(exp_win) self._handle_controller_presses() - if 'r' in self.pressed_keys and value < n_pts-1: - value+=1 - self.pressed_keys.discard('r') - elif 'l' in self.pressed_keys and value > 0: - value-=1 - self.pressed_keys.discard('l') - for c in circles: - c.fillColor = (-1,-1,-1) - circles[value].fillColor = (1,1,1) - if 'a' in self.pressed_keys: exp_win.logOnFlip( level=logging.EXP, msg='nlevel: %d, question: %s, answer: %d'%(self._nlevels, question, value)) for i in range(config.FRAME_RATE): - yield i<2 - break + yield True self.pressed_keys.clear() + break + if 'r' in self.pressed_keys and value < n_pts-1: + value += 1 + elif 'l' in self.pressed_keys and value > 0: + value -= 1 + else: + yield frame < 4 + continue + self.pressed_keys.clear() + for c in circles: + c.fillColor = (-1,-1,-1) + circles[value].fillColor = (1,1,1) - for stim in [text, line] + circles: - stim.draw(exp_win) yield True + yield True def _stop(self, exp_win, ctl_win): self._unset_key_handler(exp_win) @@ -316,6 +382,7 @@ def _run(self, exp_win, ctl_win): self._set_recording_file() yield from self._instructions(exp_win, ctl_win) + yield from self._questionnaire(exp_win, ctl_win) yield from super()._run_emulator(exp_win, ctl_win) self.game_sound.stop() if self.post_level_ratings: From f30c8767c38ed8163d651c1cf47b913a62719f40 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 1 Oct 2020 11:14:36 -0400 Subject: [PATCH 045/170] wip --- src/sessions/ses-shinobi3levels.py | 16 +++++++++++++++- src/tasks/videogame.py | 2 ++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/sessions/ses-shinobi3levels.py b/src/sessions/ses-shinobi3levels.py index 5f67b3ef..18a85ff8 100644 --- a/src/sessions/ses-shinobi3levels.py +++ b/src/sessions/ses-shinobi3levels.py @@ -1,6 +1,20 @@ from ..tasks import images, videogame, memory, task_base +flow_ratings = [ + "I feel just the right amount of challenge.", + "My thoughts/activities run fluidly and smoothly.", + "I don’t notice time passing.", + "I have no difficulty concentrating.", + "My mind is completely clear.", + "I am totally absorbed in what I am doing.", + "The right thoughts/movements occur of their own accord.", + "I know what I have to do each step of the way.", + "I feel that I have everything under control.", + "I am completely lost in thought." +] + + TASKS = sum([ [ @@ -10,7 +24,7 @@ repeat_scenario=True, max_duration=10*60, # if when level completed or dead we exceed that time in secs, stop the task name=f"shinobi-3levels-{run+1:02d}", - post_level_ratings = [("rate your performance", 7), ("rate your flow", 7)] + #post_level_ratings = [(q, 7) for q in flow_ratings] ), task_base.Pause() ] diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 43808719..d01d4012 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -199,6 +199,8 @@ def _run_emulator(self, exp_win, ctl_win): if not level_step%config.FRAME_RATE: exp_win.logOnFlip(level=logging.EXP, msg="level step: %d"%level_step) yield + self.game_sound.stop() + self.game_sound.flush() def _set_key_handler(self, exp_win): # activate repeat keys From b28bdd9582a2bb00f9d8f71c5cc4cc39a67e6a15 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 5 Oct 2020 16:24:35 -0400 Subject: [PATCH 046/170] fix vg sizing issues --- src/tasks/videogame.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index d01d4012..54a8f450 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -67,15 +67,22 @@ class VideoGameBase(Task): def _setup(self, exp_win): + self.game_sound = SoundDeviceBlockStream(stereo=True, blockSize=735) + self._first_frame = self.emulator.reset() + + min_ratio = min( + exp_win.size[0]/ self._first_frame.shape[1], + exp_win.size[1]/ self._first_frame.shape[0]) + width = int(min_ratio*self._first_frame.shape[1]) + height = int(min_ratio*self._first_frame.shape[0]) + self.game_vis_stim = visual.ImageStim( exp_win, - size=exp_win.size, + size=(width, height), units='pixels', interpolate=False, flipVert=True, autoLog=False) - self.game_sound = SoundDeviceBlockStream(stereo=True, blockSize=735) - self._first_frame = self.emulator.reset() def _transform_soundblock(self, sound_block): return sound_block[:735]/float(2**15) @@ -181,7 +188,7 @@ def _run_emulator(self, exp_win, ctl_win): # render the initial frame and audio self._render_graphics_sound(self._first_frame, self.emulator.em.get_audio(), exp_win, ctl_win) exp_win.logOnFlip(level=logging.EXP, msg="level step: %d"%level_step) - yield + yield True while not _done: level_step += 1 self._handle_controller_presses() @@ -198,7 +205,7 @@ def _run_emulator(self, exp_win, ctl_win): msg='VideoGame %s: %s stopped at %f'%(self.game_name, self.state_name, time.time())) if not level_step%config.FRAME_RATE: exp_win.logOnFlip(level=logging.EXP, msg="level step: %d"%level_step) - yield + yield True self.game_sound.stop() self.game_sound.flush() @@ -216,8 +223,10 @@ def _unset_key_handler(self, exp_win): def _run(self, exp_win, ctl_win): self._set_key_handler(exp_win) - exp_win.waitBlanking = False self._nlevels = 0 + exp_win.setColor([-1.0]*3) + if ctl_win: + ctl_win.setColor([-1.0]*3) while True: self._nlevels += 1 @@ -234,6 +243,10 @@ def _run(self, exp_win, ctl_win): break self.emulator.reset() + exp_win.setColor([0]*3) + if ctl_win: + ctl_win.setColor([0]*3) + def _run_ratings(self, exp_win, ctl_win): for question, n_pts in self.post_level_ratings: yield from self._likert_scale_answer(exp_win, ctl_win, question, n_pts) @@ -370,6 +383,10 @@ def _run(self, exp_win, ctl_win): self._set_key_handler(exp_win) exp_win.waitBlanking = False + exp_win.setColor([-1.0]*3) + if ctl_win: + ctl_win.setColor([-1.0]*3) + self._nlevels = 0 while True: for level, scenario in zip(self._state_names, self._scenarii): @@ -396,6 +413,10 @@ def _run(self, exp_win, ctl_win): if time_exceeded or not self._repeat_scenario_multilevel: break + exp_win.setColor([0]*3) + if ctl_win: + ctl_win.setColor([0]*3) + self._unset_key_handler(exp_win) exp_win.waitBlanking = True From a35cfa59efb10b8820312b21629492006cc55cfd Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 5 Oct 2020 16:25:59 -0400 Subject: [PATCH 047/170] cleanup --- src/shared/cli.py | 4 ---- src/shared/config.py | 5 ----- src/tasks/video.py | 1 - 3 files changed, 10 deletions(-) diff --git a/src/shared/cli.py b/src/shared/cli.py index b1a4fdc6..46c3aff1 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -97,14 +97,10 @@ def main_loop(all_tasks, exp_win = visual.Window(**config.EXP_WINDOW) exp_win.mouseVisible = False - exp_win.recordFrameIntervals = True - exp_win.waitBlanking = False if show_ctl_win: ctl_win = visual.Window(**config.CTL_WINDOW) ctl_win.name = 'Stimuli' - ctl_win.waitBlanking = False - ctl_win.recordFrameIntervals = True else: ctl_win = None diff --git a/src/shared/config.py b/src/shared/config.py index dddb5b2c..dac0a7ac 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -9,18 +9,13 @@ EYETRACKING_ROI = (60,30,660,450) EXP_WINDOW = dict( -# winType='glfw', size = (1280,1024), -# size = (1024, 768), -# size = (1920, 1080), screen=1, fullscr=True, gammaErrorPolicy='warn', ) CTL_WINDOW = dict( -# winType='glfw', -# size = (1920, 1080), size = (1280, 1024), pos = (100,0), screen=0, diff --git a/src/tasks/video.py b/src/tasks/video.py index 9d9e572b..c37d9669 100644 --- a/src/tasks/video.py +++ b/src/tasks/video.py @@ -25,7 +25,6 @@ def _instructions(self, exp_win, ctl_win): exp_win, text=self.instruction, alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) - print(screen_text.pos) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): grey = [-float(frameN)/config.FRAME_RATE/config.INSTRUCTION_DURATION]*3 exp_win.setColor(grey) From 0a1ddacf4f050096747f2e5bdb28dc16e86fee0c Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 6 Oct 2020 13:10:12 -0400 Subject: [PATCH 048/170] pass parsed params to sessions task loading for emotions/prisme and code neuromod emotion session --- main.py | 2 +- src/sessions/ses-emotions.py | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/sessions/ses-emotions.py diff --git a/main.py b/main.py index 10ce40dd..a4f7c5d5 100755 --- a/main.py +++ b/main.py @@ -16,7 +16,7 @@ def run(parsed): try: ses_mod = importlib.import_module('src.sessions.ses-%s'%parsed.tasks) - tasks = ses_mod.TASKS + tasks = ses_mod.get_tasks(parsed) if hasattr(ses_mod, 'get_tasks') else ses_mod.TASKS except ImportError: raise(ValueError('session tasks file cannot be found for %s'%parsed.session)) cli.main_loop( diff --git a/src/sessions/ses-emotions.py b/src/sessions/ses-emotions.py new file mode 100644 index 00000000..714a0d17 --- /dev/null +++ b/src/sessions/ses-emotions.py @@ -0,0 +1,37 @@ +from ..tasks import video, task_base +import numpy as np + +NUM_VID_PRISME = 10 + +def get_videos(subject, session): + video_idx = np.loadtxt( + 'data/emotions/order_fmri_neuromod.csv', + delimiter=',', + skiprows=1, + dtype=np.int + ) + selected_idx = video_idx[video_idx[:,0]==session, subject+1] + print(selected_idx) + return ["data/emotions/videos/%03d.mp4"%i for i in selected_idx] + +def get_tasks(parsed): + + tasks = [] + + video_paths = get_videos(int(parsed.subject), int(parsed.session)) + + for p in video_paths: + tasks.append( + video.SingleVideo( + p, + name='emotion_%s'%video + ) + ) + tasks.append( + task_base.Pause( + """The video is finished. +The scanner might run for a few seconds to acquire more images. +Please remain still.""" + ) + ) + return tasks From 45e6af994c90a6827a96e50c90a0ba4d762db248 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 8 Oct 2020 13:47:24 -0400 Subject: [PATCH 049/170] cleanup --- src/sessions/ses-emotions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sessions/ses-emotions.py b/src/sessions/ses-emotions.py index 714a0d17..e5b34003 100644 --- a/src/sessions/ses-emotions.py +++ b/src/sessions/ses-emotions.py @@ -1,8 +1,6 @@ from ..tasks import video, task_base import numpy as np -NUM_VID_PRISME = 10 - def get_videos(subject, session): video_idx = np.loadtxt( 'data/emotions/order_fmri_neuromod.csv', From 5c3127ccf0b5aeb059eb98af9513bb85edb1a7e3 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 8 Oct 2020 14:43:32 -0400 Subject: [PATCH 050/170] fix button presses buffer clearing between runs --- src/tasks/videogame.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 54a8f450..039ac1b7 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -98,6 +98,9 @@ def _render_graphics_sound(self, obs, sound_block, exp_win, ctl_win): def _stop(self, exp_win, ctl_win): self.game_sound.stop() + exp_win.setColor([0]*3) + if ctl_win: + ctl_win.setColor([0]*3) yield True def unload(self): @@ -122,6 +125,7 @@ def __init__(self, self.scenario = scenario self.repeat_scenario = repeat_scenario self.max_duration = max_duration + self.duration = max_duration self.post_level_ratings = post_level_ratings def _instructions(self, exp_win, ctl_win): @@ -176,6 +180,12 @@ def _handle_controller_presses(self): _keyPressBuffer.clear() return self.pressed_keys + def clear_key_buffers(self): + global _keyPressBuffer, _keyReleaseBuffer + self.pressed_keys.clear() + _keyReleaseBuffer.clear() + _keyPressBuffer.clear() + def _run_emulator(self, exp_win, ctl_win): total_reward = 0 @@ -184,7 +194,8 @@ def _run_emulator(self, exp_win, ctl_win): keys = [False]*12 # flush all keys to avoid unwanted actions - self.pressed_keys.clear() + self.clear_key_buffers() + # render the initial frame and audio self._render_graphics_sound(self._first_frame, self.emulator.em.get_audio(), exp_win, ctl_win) exp_win.logOnFlip(level=logging.EXP, msg="level step: %d"%level_step) @@ -193,7 +204,6 @@ def _run_emulator(self, exp_win, ctl_win): level_step += 1 self._handle_controller_presses() keys = [k in self.pressed_keys for k in KEY_SET] - _obs, _rew, _done, _info = self.emulator.step(keys) total_reward += _rew if _rew > 0 : @@ -400,7 +410,6 @@ def _run(self, exp_win, ctl_win): if self._nlevels > 1: self._set_recording_file() yield from self._instructions(exp_win, ctl_win) - yield from self._questionnaire(exp_win, ctl_win) yield from super()._run_emulator(exp_win, ctl_win) self.game_sound.stop() @@ -413,10 +422,6 @@ def _run(self, exp_win, ctl_win): if time_exceeded or not self._repeat_scenario_multilevel: break - exp_win.setColor([0]*3) - if ctl_win: - ctl_win.setColor([0]*3) - self._unset_key_handler(exp_win) exp_win.waitBlanking = True From c38731956f4bf27cfc2618e2b6b348cfc666882d Mon Sep 17 00:00:00 2001 From: courtois-neuromod-machine Date: Thu, 8 Oct 2020 15:15:56 -0400 Subject: [PATCH 051/170] config for new projector --- src/shared/config.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/shared/config.py b/src/shared/config.py index e397b8b8..54f9613f 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -9,21 +9,20 @@ EYETRACKING_ROI = (60,30,660,450) EXP_WINDOW = dict( -# size = (1280,1024), - size = (1024, 768), -# size = (1920, 1080), + #size = (1280,1024), + #size = (1024, 768), + size = (1920, 1080), screen=1, fullscr=True, gammaErrorPolicy='warn', ) CTL_WINDOW = dict( -# size = (1920, 1080), - size = (1280, 1024), + size = (1920, 1080), + #size = (1280, 1024), pos = (100,0), screen=0, gammaErrorPolicy='warn', - waitBlanking=False, # avoid ctrl window to block the script in case of differing refresh rate. ) FRAME_RATE=60 From 3aa1e8e61878eccd80da5caed1502b7e696777d4 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 9 Oct 2020 15:07:43 -0400 Subject: [PATCH 052/170] fix set/unset game handler --- src/tasks/videogame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 039ac1b7..8f631695 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -154,6 +154,8 @@ def _setup(self, exp_win): super()._setup(exp_win) self._events = [] self._set_recording_file() + self._set_key_handler(exp_win) + def _set_recording_file(self): nnn = 0 @@ -390,7 +392,6 @@ def __init__(self, *args,**kwargs): def _run(self, exp_win, ctl_win): - self._set_key_handler(exp_win) exp_win.waitBlanking = False exp_win.setColor([-1.0]*3) @@ -422,7 +423,6 @@ def _run(self, exp_win, ctl_win): if time_exceeded or not self._repeat_scenario_multilevel: break - self._unset_key_handler(exp_win) exp_win.waitBlanking = True class VideoGameReplay(VideoGameBase): From 39b107c76b14df090f9b3c94674b72649d04d62f Mon Sep 17 00:00:00 2001 From: unf Date: Fri, 9 Oct 2020 15:37:31 -0400 Subject: [PATCH 053/170] tests --- src/shared/cli.py | 5 +++-- src/shared/config.py | 10 ++++++---- src/shared/eyetracking.py | 11 ++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/shared/cli.py b/src/shared/cli.py index b1a4fdc6..ac502137 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -26,11 +26,12 @@ def listen_shortcuts(): return False def run_task_loop(loop, eyetracker=None, gaze_drawer=None): + gaze = None for _ in loop: - if gaze_drawer: - gaze = eyetracker.get_gaze() + if gaze_drawer and False: if not gaze is None: gaze_drawer.draw_gazepoint(gaze) + gaze = eyetracker.get_gaze() # check for global event keys shortcut_evt = listen_shortcuts() if shortcut_evt: diff --git a/src/shared/config.py b/src/shared/config.py index ee6d7bfa..86f681d7 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -10,22 +10,24 @@ EXP_WINDOW = dict( # winType='glfw', -# size = (1280,1024), - size = (1024, 768), + size = (1280,1024), +# size = (800, 600), # size = (1920, 1080), - screen=1, + screen=0, fullscr=True, gammaErrorPolicy='warn', + waitBlanking=False, ) CTL_WINDOW = dict( # winType='glfw', # size = (1920, 1080), size = (1280, 1024), +# size = (1024, 768), pos = (100,0), screen=0, gammaErrorPolicy='warn', - swapInterval=0., +# swapInterval=0., waitBlanking=False, # avoid ctrl window to block the script in case of differing refresh rate. ) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index cee3bb57..d1e9b369 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -189,11 +189,6 @@ def __init__(self, output_path, output_fname_base): 'name':'Accuracy_Visualizer','args':{} }) - #restart with new params -# self.send_recv_notification({ -# 'subject':'start_plugin', -# 'name':'Fixed_Screen_Marker_Calibration', -# 'args':{'fullscreen':True, 'marker_scale':.8, 'sample_duration':120, 'monitor_idx':1}}) # setup recorder output path self.send_recv_notification({ 'subject':'start_plugin', @@ -220,12 +215,11 @@ def __init__(self, output_path, output_fname_base): """ - #self.send_recv_notification({'subject':'recording.should_start',}) # wait for the whole schmilblick to boot time.sleep(4) def send_recv_notification(self, n): - # REQ REP requirese lock step communication with multipart msg (topic,msgpack_encoded dict) + # REQ REP requires lock step communication with multipart msg (topic,msgpack_encoded dict) self._req_socket.send_multipart((bytes('notify.%s'%n['subject'],'utf-8'), msgpack.dumps(n))) return self._req_socket.recv() @@ -249,6 +243,7 @@ def join(self, timeout=None): # stop recording self.send_recv_notification({'subject':'recording.should_stop',}) # stop world and children process + self.send_recv_notification({'subject':'world_process.should_stop'}) self.send_recv_notification({'subject':'launcher_process.should_stop'}) self._pupil_process.wait(timeout) self._pupil_process.terminate() @@ -288,11 +283,13 @@ def calibrate(self, pupil_list, ref_list, frame_size): logging.error('Calibration: not enough pupil captured for calibration') #return + logging.info('starting pupil calibration plugin') self.send_recv_notification({ 'subject':'start_plugin', 'name':'External_Calibration', 'args':{'frame_size': frame_size.tolist()}}) + logging.info('calibrating, %s %s'%(str(pupil_list),str(ref_list))) self.send_recv_notification({ 'subject':'calibrate.from_external_data', 'pupil_list':pupil_list, From 8fda8e6b1fae9eea2a13ed9a441e903b4a1c7927 Mon Sep 17 00:00:00 2001 From: courtois-neuromod-machine Date: Tue, 20 Oct 2020 08:54:13 -0400 Subject: [PATCH 054/170] difference in python libs/versions --- src/tasks/videogame.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 039ac1b7..fe6f65dd 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -41,7 +41,7 @@ class SoundDeviceBlockStream(sound.backend_sounddevice.SoundDeviceSound): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.blocks = queue.SimpleQueue() + self.blocks = queue.Queue() self.lock = threading.Lock() def add_block(self, block): @@ -50,7 +50,7 @@ def add_block(self, block): def flush(self): with self.lock: - self.blocks = queue.SimpleQueue() + self.blocks = queue.Queue() def _nextBlock(self): if self.status == constants.STOPPED: @@ -228,7 +228,7 @@ def _set_key_handler(self, exp_win): def _unset_key_handler(self, exp_win): # deactivate custom keys handling exp_win.winHandle.on_key_press = event._onPygletKey - del exp_win.winHandle.on_key_release + #del exp_win.winHandle.on_key_release def _run(self, exp_win, ctl_win): From 7497593840b21b24d9786d5167be6fe69ad05bef Mon Sep 17 00:00:00 2001 From: courtois-neuromod-machine Date: Tue, 20 Oct 2020 08:55:42 -0400 Subject: [PATCH 055/170] add friends s4 --- src/sessions/ses-friends-s4.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/sessions/ses-friends-s4.py diff --git a/src/sessions/ses-friends-s4.py b/src/sessions/ses-friends-s4.py new file mode 100644 index 00000000..7e8fded0 --- /dev/null +++ b/src/sessions/ses-friends-s4.py @@ -0,0 +1,19 @@ +from ..tasks import video + +TASKS = [] + +for episode in range(1,23): + for segment in 'ab': + TASKS.append( + video.SingleVideo( + 'data/videos/friends/s4/friends_s4e%02d%s.mkv'%(episode, segment), + aspect_ratio = 4/3., + name='task-friends-s4e%d%s'%(episode, segment))) +""" +for segment in 'abcd': + TASKS.append( + video.SingleVideo( + 'data/videos/friends/s4/friends_s4e%02d%s.mkv'%(23, segment), + aspect_ratio = 4/3., + name='task-friends-s4e%d%s'%(23, segment))) +""" From e3c2ea08161678e8282c1499e20ab2e53342bf33 Mon Sep 17 00:00:00 2001 From: courtois-neuromod-machine Date: Tue, 20 Oct 2020 10:35:46 -0400 Subject: [PATCH 056/170] force resolution at start: wip --- src/shared/cli.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/shared/cli.py b/src/shared/cli.py index 46c3aff1..566dd770 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -12,9 +12,10 @@ logging.setDefaultClock(globalClock) from . import config #import first separately -from . import fmri, eyetracking, utils, meg +from . import fmri, eyetracking, utils, meg, config from ..tasks import task_base, video +from subprocess import Popen def listen_shortcuts(): if any([k[1]&event.MOD_CTRL for k in event._keyBuffer]): @@ -80,6 +81,14 @@ def main_loop(all_tasks, allow_run_on_battery=False, enable_ptt=False): + # force screen resolution to solve issues with video splitter at scanner + xrandr = Popen([ + 'xrandr', + '--output', 'HDMI-2', + '--mode', '%dx%d'%config.EXP_WINDOW['size'], + '--rate', str(config.FRAME_RATE)]) + time.sleep(5) + if not utils.check_power_plugged(): print('*'*25+'WARNING: the power cord is not connected'+'*'*25) if not allow_run_on_battery: From baed491e1cf0d464056e6f957253379894d2aef3 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 20 Oct 2020 13:56:02 -0400 Subject: [PATCH 057/170] force screen resolution at startup, restructure code --- main.py | 19 ++++++---------- src/shared/cli.py | 54 +++----------------------------------------- src/shared/config.py | 4 +++- src/shared/parser.py | 46 +++++++++++++++++++++++++++++++++++++ src/shared/screen.py | 9 ++++++++ 5 files changed, 68 insertions(+), 64 deletions(-) create mode 100644 src/shared/parser.py create mode 100644 src/shared/screen.py diff --git a/main.py b/main.py index a4f7c5d5..3bb5fba6 100755 --- a/main.py +++ b/main.py @@ -1,24 +1,19 @@ #!/usr/bin/python3 -import os, sys, importlib -from psychopy import visual, logging # need to import visual first things to avoid pyglet related crash +from subprocess import Popen -# threading and processing -from multiprocessing import ( - Process, - Value, - active_children, - set_start_method, - freeze_support, -) +import os, sys, importlib -from src.shared import cli +from src.shared import parser, config, screen def run(parsed): + # initializing the screen need to be done before loading any psychopy + screen.init_exp_screen() try: ses_mod = importlib.import_module('src.sessions.ses-%s'%parsed.tasks) tasks = ses_mod.get_tasks(parsed) if hasattr(ses_mod, 'get_tasks') else ses_mod.TASKS except ImportError: raise(ValueError('session tasks file cannot be found for %s'%parsed.session)) + from src.shared import cli cli.main_loop( tasks[parsed.skip_n_tasks:], parsed.subject, @@ -42,7 +37,7 @@ def run_profiled(parsed): ) if __name__ == "__main__": - parsed = cli.parse_args() + parsed = parser.parse_args() if parsed.profile: run_profiled(parsed) else: diff --git a/src/shared/cli.py b/src/shared/cli.py index 566dd770..a62a7413 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -15,8 +15,6 @@ from . import fmri, eyetracking, utils, meg, config from ..tasks import task_base, video -from subprocess import Popen - def listen_shortcuts(): if any([k[1]&event.MOD_CTRL for k in event._keyBuffer]): allKeys = event.getKeys(['n','c','q'], modifiers=True) @@ -82,12 +80,12 @@ def main_loop(all_tasks, enable_ptt=False): # force screen resolution to solve issues with video splitter at scanner - xrandr = Popen([ + """ xrandr = Popen([ 'xrandr', - '--output', 'HDMI-2', + '--output', 'eDP-1', '--mode', '%dx%d'%config.EXP_WINDOW['size'], '--rate', str(config.FRAME_RATE)]) - time.sleep(5) + time.sleep(5)""" if not utils.check_power_plugged(): print('*'*25+'WARNING: the power cord is not connected'+'*'*25) @@ -220,49 +218,3 @@ def main_loop(all_tasks, finally: if enable_eyetracker: eyetracker_client.join(TIMEOUT) - -def parse_args(): - import argparse - parser = argparse.ArgumentParser( - prog='main.py', - description=('Run all tasks in a session'), - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument('--subject', '-s', - required=True, - help='Subject ID') - parser.add_argument('--session', '-ss', - required=True, - help='Session') - parser.add_argument('--tasks', '-t', - required=True, - help='tasks set') - parser.add_argument('--output', '-o', - required=True, - help='output dataset') - parser.add_argument('--fmri', '-f', - help='Wait for fmri TTL to start each task', - action='store_true') - parser.add_argument('--meg', '-m', - help='Send signal to parallel port to start trigger to MEG and Biopac.', - action='store_true') - parser.add_argument('--eyetracking', '-e', - help='Enable eyetracking', - action='store_true') - parser.add_argument('--skip_n_tasks', - help='skip n of the tasks', - default=0, - type=int) - parser.add_argument('--ctl_win', - help='show control window', - action='store_true') - parser.add_argument('--run_on_battery', - help='allow the script to run on battery', - action='store_true') - parser.add_argument('--ptt', - help='enable Push-To-Talk function', - action='store_true') - parser.add_argument('--profile', - help='enable profiling', - action='store_true') - - return parser.parse_args() diff --git a/src/shared/config.py b/src/shared/config.py index 40d2007d..6c3ff62a 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -8,8 +8,10 @@ EYETRACKING_ROI = (60,30,660,450) +EXP_SCREEN_XRANDR_NAME='eDP-1' + EXP_WINDOW = dict( - size = (1920, 1080), + size = (1280, 1024), screen=1, fullscr=True, gammaErrorPolicy='warn', diff --git a/src/shared/parser.py b/src/shared/parser.py new file mode 100644 index 00000000..e36a7007 --- /dev/null +++ b/src/shared/parser.py @@ -0,0 +1,46 @@ +import argparse + +def parse_args(): + parser = argparse.ArgumentParser( + prog='main.py', + description=('Run all tasks in a session'), + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('--subject', '-s', + required=True, + help='Subject ID') + parser.add_argument('--session', '-ss', + required=True, + help='Session') + parser.add_argument('--tasks', '-t', + required=True, + help='tasks set') + parser.add_argument('--output', '-o', + required=True, + help='output dataset') + parser.add_argument('--fmri', '-f', + help='Wait for fmri TTL to start each task', + action='store_true') + parser.add_argument('--meg', '-m', + help='Send signal to parallel port to start trigger to MEG and Biopac.', + action='store_true') + parser.add_argument('--eyetracking', '-e', + help='Enable eyetracking', + action='store_true') + parser.add_argument('--skip_n_tasks', + help='skip n of the tasks', + default=0, + type=int) + parser.add_argument('--ctl_win', + help='show control window', + action='store_true') + parser.add_argument('--run_on_battery', + help='allow the script to run on battery', + action='store_true') + parser.add_argument('--ptt', + help='enable Push-To-Talk function', + action='store_true') + parser.add_argument('--profile', + help='enable profiling', + action='store_true') + + return parser.parse_args() diff --git a/src/shared/screen.py b/src/shared/screen.py new file mode 100644 index 00000000..d3f326c7 --- /dev/null +++ b/src/shared/screen.py @@ -0,0 +1,9 @@ +from . import config +from subprocess import Popen + +def init_exp_screen(): + xrandr = Popen([ + 'xrandr', + '--output', config.EXP_SCREEN_XRANDR_NAME, + '--mode', '%dx%d'%config.EXP_WINDOW['size'], + '--rate', str(config.FRAME_RATE)]) From ca2de3e0a77b8d19d6dd069a7394efbc735a580c Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 23 Oct 2020 16:50:40 -0400 Subject: [PATCH 058/170] optimizing things --- src/tasks/task_base.py | 20 ++++++++------------ src/tasks/video.py | 1 + 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index 3a245999..f2e4cccf 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -26,7 +26,8 @@ def setup(self, exp_win, output_path, output_fname_base, use_fmri=False, use_eye self._setup(exp_win) def _setup(self, exp_win): - pass + # initialize a progress bar if we know the duration of the task + self.progress_bar = tqdm.tqdm(total=self.duration) if hasattr(self, 'duration') else False def _generate_tsv_filename(self): for fi in range(1000): @@ -58,26 +59,21 @@ def instructions(self, exp_win, ctl_win): def run(self, exp_win, ctl_win): self.task_timer = core.Clock() - - # initialize a progress bar if we know the duration of the task - progress_bar = False - if hasattr(self, 'duration'): - progress_bar = tqdm.tqdm(total=self.duration) - frame_idx = 0 + frame_idx = 0 for clearBuffer in self._run(exp_win, ctl_win): # yield first to allow external draw before flip yield self._flip_all_windows(exp_win, ctl_win, clearBuffer) # increment the progress bar every second - if progress_bar: + if self.progress_bar: frame_idx += 1 if not frame_idx%config.FRAME_RATE: - progress_bar.update(1) + self.progress_bar.update(1) - if progress_bar: - progress_bar.clear() - progress_bar.close() + if self.progress_bar: + self.progress_bar.clear() + self.progress_bar.close() def stop(self, exp_win, ctl_win): if hasattr(self, '_stop'): diff --git a/src/tasks/video.py b/src/tasks/video.py index c37d9669..22013dea 100644 --- a/src/tasks/video.py +++ b/src/tasks/video.py @@ -55,6 +55,7 @@ def _setup(self, exp_win): self.duration = self.movie_stim.duration # print(self.movie_stim.size) # print(self.movie_stim.duration) + super()._setup(exp_win) def _run(self, exp_win, ctl_win): # give the original size of the movie in pixels: From b4caebbeb6f020ff27d65c212f5a518768027186 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 23 Oct 2020 16:51:43 -0400 Subject: [PATCH 059/170] things task: wip --- src/sessions/ses-things.py | 86 ++++++++++++++++++++++++++++++++++ src/tasks/things.py | 94 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 src/sessions/ses-things.py create mode 100644 src/tasks/things.py diff --git a/src/sessions/ses-things.py b/src/sessions/ses-things.py new file mode 100644 index 00000000..fc51cddc --- /dev/null +++ b/src/sessions/ses-things.py @@ -0,0 +1,86 @@ +import os +from ..tasks.things import Things + +THINGS_DATA_PATH = os.path.join('data','things') +IMAGE_PATH = os.path.join(THINGS_DATA_PATH,'images') + +def get_tasks(parsed): + + session_design_filename = os.path.join( + THINGS_DATA_PATH, 'designs', + f"sub-{parsed.subject}_ses-{parsed.session}_design.tsv") + tasks = [ + Things( + session_design_filename, + IMAGE_PATH, + run, + name=f"sub-{parsed.subject}_ses-{parsed.session}_run-{run}") \ + for run in range(1,10) + ] + return tasks + + + + + + +# experiment + +n_sessions = 12 # number of sessions +n_runs = 10 # number of runs +n_trials_exp = 72 # number of trials for each run +n_trials_catch = 10 # catch trials where response is required +n_trials_test = 10 # for test data, separate images +n_trials_total = n_trials_exp + n_trials_test + n_trials_catch +final_wait = 9 # time to wait after last trial +initial_wait = 3 # time until first trial starts + +# trial +trial_duration = 4.5 # mean trial duration +jitters = 0 # chosen to be a range that minimizes phase synchrony and that can be presented exactly on most screens +image_duration = 0.5 # duration of image presentation +rm_duration = 4.0 # duration of response mapping screen +max_rt = 4.0 # from stimulus onset + +# constraints +min_catch_spacing = 3 +max_catch_spacing = 20 + +def generate_design_file(subject, session): + + import pandas + import numpy as np + images_list = pandas.read_csv(os.path.join(THINGS_DATA_PATH, 'image_paths_fmri.csv')) + + images_exp = images_list[images_list.condition.eq('exp') & images_list.exemplar_nr.eq(session)].sample(frac=1) + images_catch = images_list[images_list.condition.eq('catch')].sample(frac=1) + images_test = images_list[images_list.condition.eq('test')].sample(frac=1) + + design = pandas.DataFrame() + + np.random.seed(abs(hash(subject + session))%(2**32 - 1)) + + for run in range(n_runs): + while True: + randorder = np.random.permutation(n_trials_total) + n_noncatch_trial = n_trials_exp + n_trials_test + catch_indices = np.where(randorder >= n_noncatch_trial)[0] + catch_indices_bounds = np.hstack([[0], catch_indices, [n_trials_total]]) + catch_spacings = np.diff(catch_indices_bounds) + if np.all(catch_spacings>min_catch_spacing) and \ + np.all(catch_spacings 0 + trial['response_time'] = trial['onset'] - keypress + trial['duration'] = trial['offset']-trial['onset'] + + for frameN in range(config.FRAME_RATE * BASELINE_END): + self.fixation_cross.draw(exp_win) + if ctl_win: + self.fixation_cross.draw(ctl_win) + yield frameN<2 + + def save(self): + self.trials.saveAsWideText(self._generate_tsv_filename()) From e0b4cce6c634ee4f0e3f50f0cd5669119d3472ea Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 26 Oct 2020 14:51:35 -0400 Subject: [PATCH 060/170] generator of design and fix the task, almost working --- src/sessions/ses-things.py | 57 ++++++++++++++++++++++++++------------ src/tasks/things.py | 44 ++++++++++++++++------------- 2 files changed, 64 insertions(+), 37 deletions(-) diff --git a/src/sessions/ses-things.py b/src/sessions/ses-things.py index fc51cddc..88588291 100644 --- a/src/sessions/ses-things.py +++ b/src/sessions/ses-things.py @@ -1,10 +1,10 @@ import os -from ..tasks.things import Things THINGS_DATA_PATH = os.path.join('data','things') IMAGE_PATH = os.path.join(THINGS_DATA_PATH,'images') def get_tasks(parsed): + from ..tasks.things import Things session_design_filename = os.path.join( THINGS_DATA_PATH, 'designs', @@ -15,7 +15,7 @@ def get_tasks(parsed): IMAGE_PATH, run, name=f"sub-{parsed.subject}_ses-{parsed.session}_run-{run}") \ - for run in range(1,10) + for run in range(1,n_runs+1) ] return tasks @@ -47,21 +47,29 @@ def get_tasks(parsed): max_catch_spacing = 20 def generate_design_file(subject, session): - import pandas import numpy as np + import random + import hashlib images_list = pandas.read_csv(os.path.join(THINGS_DATA_PATH, 'image_paths_fmri.csv')) - images_exp = images_list[images_list.condition.eq('exp') & images_list.exemplar_nr.eq(session)].sample(frac=1) + images_exp = images_list[images_list.condition.eq('exp') & images_list.exemplar_nr.eq(int(session))].sample(frac=1) images_catch = images_list[images_list.condition.eq('catch')].sample(frac=1) images_test = images_list[images_list.condition.eq('test')].sample(frac=1) design = pandas.DataFrame() - np.random.seed(abs(hash(subject + session))%(2**32 - 1)) + print("%s-%s"%(subject, session)) + seed = int(hashlib.sha1(("%s-%s"%(subject, session)).encode('utf-8')).hexdigest(), 16) % (2**32 - 1) + print('seed', seed) + np.random.seed(seed) + + all_run_trials = pandas.DataFrame() for run in range(n_runs): + niter = 0 while True: + niter+=1 randorder = np.random.permutation(n_trials_total) n_noncatch_trial = n_trials_exp + n_trials_test catch_indices = np.where(randorder >= n_noncatch_trial)[0] @@ -70,17 +78,32 @@ def generate_design_file(subject, session): if np.all(catch_spacings>min_catch_spacing) and \ np.all(catch_spacings 0 - trial['response_time'] = trial['onset'] - keypress + trial['response_time'] = trial['onset'] - keypress[0][1] if len(keypress) else None trial['duration'] = trial['offset']-trial['onset'] + del trial['stim'] for frameN in range(config.FRAME_RATE * BASELINE_END): self.fixation_cross.draw(exp_win) From 975996ff56048af4497a60af20e616bda8633cd4 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 26 Oct 2020 16:13:36 -0400 Subject: [PATCH 061/170] fix pb with movie recording. it is still very RAM-intensive to store all frames --- main.py | 3 ++- src/shared/cli.py | 54 ++++++++++++++++++++++++++++++++++++-------- src/shared/parser.py | 4 +++- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/main.py b/main.py index 3bb5fba6..b1d329e5 100755 --- a/main.py +++ b/main.py @@ -24,7 +24,8 @@ def run(parsed): parsed.meg, parsed.ctl_win, parsed.run_on_battery, - parsed.ptt) + parsed.ptt, + parsed.record_movie) def run_profiled(parsed): import cProfile diff --git a/src/shared/cli.py b/src/shared/cli.py index a62a7413..193b746d 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -24,25 +24,36 @@ def listen_shortcuts(): return all_keys_only[0] return False -def run_task_loop(loop, eyetracker=None, gaze_drawer=None): +def run_task_loop(loop, eyetracker=None, gaze_drawer=None, record_movie=False): for _ in loop: if gaze_drawer: gaze = eyetracker.get_gaze() if not gaze is None: gaze_drawer.draw_gazepoint(gaze) + if record_movie: + record_movie.getMovieFrame(buffer='back') # check for global event keys shortcut_evt = listen_shortcuts() if shortcut_evt: return shortcut_evt -def run_task(task, exp_win, ctl_win=None, eyetracker=None, gaze_drawer=None): +def run_task(task, exp_win, ctl_win=None, eyetracker=None, gaze_drawer=None, record_movie=False): print('Next task: %s'%str(task)) # show instruction - shortcut_evt = run_task_loop(task.instructions(exp_win, ctl_win), eyetracker, gaze_drawer) + shortcut_evt = run_task_loop( + task.instructions(exp_win, ctl_win), + eyetracker, + gaze_drawer, + record_movie=exp_win if record_movie else False + ) if task.use_fmri and not shortcut_evt: - shortcut_evt = run_task_loop(fmri.wait_for_ttl(), eyetracker, gaze_drawer) + shortcut_evt = run_task_loop( + fmri.wait_for_ttl(), + eyetracker, + gaze_drawer + ) if shortcut_evt: return shortcut_evt logging.info('GO') @@ -53,7 +64,12 @@ def run_task(task, exp_win, ctl_win=None, eyetracker=None, gaze_drawer=None): meg.send_signal(meg.MEG_settings['TASK_START_CODE']) if not shortcut_evt: - shortcut_evt = run_task_loop(task.run(exp_win, ctl_win), eyetracker, gaze_drawer) + shortcut_evt = run_task_loop( + task.run(exp_win, ctl_win), + eyetracker, + gaze_drawer, + record_movie=exp_win if record_movie else False + ) # send stop trigger/marker to MEG + Biopac (or anything else on parallel port) if task.use_meg and not shortcut_evt: @@ -64,7 +80,12 @@ def run_task(task, exp_win, ctl_win=None, eyetracker=None, gaze_drawer=None): # now that time is less sensitive: save files task.save() - run_task_loop(task.stop(exp_win, ctl_win), eyetracker, gaze_drawer) + run_task_loop( + task.stop(exp_win, ctl_win), + eyetracker, + gaze_drawer, + record_movie=exp_win if record_movie else False + ) return shortcut_evt @@ -77,7 +98,9 @@ def main_loop(all_tasks, use_meg=False, show_ctl_win=False, allow_run_on_battery=False, - enable_ptt=False): + enable_ptt=False, + record_movie=False, + ): # force screen resolution to solve issues with video splitter at scanner """ xrandr = Popen([ @@ -176,8 +199,13 @@ def main_loop(all_tasks, exp_win.winHandle.activate() # record frame intervals for debug - - shortcut_evt = run_task(task, exp_win, ctl_win, eyetracker_client, gaze_drawer) + shortcut_evt = run_task( + task, + exp_win, + ctl_win, + eyetracker_client, + gaze_drawer, + record_movie=record_movie) if shortcut_evt == 'n': # restart the task @@ -194,7 +222,12 @@ def main_loop(all_tasks, break logging.flush() - + if record_movie: + out_fname = os.path.join( + task.output_path, + '%s_%s.mp4'%(task.output_fname_base, task.name)) + print(f"saving movie as {out_fname}") + exp_win.saveMovieFrames(out_fname) task.unload() if shortcut_evt=='q': @@ -207,6 +240,7 @@ def main_loop(all_tasks, for i in range(DELAY_BETWEEN_TASK*config.FRAME_RATE): exp_win.flip(clearBuffer=False) + exp_win.saveFrameIntervals('exp_win_frame_intervals.txt') if ctl_win: ctl_win.saveFrameIntervals('ctl_win_frame_intervals.txt') diff --git a/src/shared/parser.py b/src/shared/parser.py index e36a7007..9e045c8d 100644 --- a/src/shared/parser.py +++ b/src/shared/parser.py @@ -42,5 +42,7 @@ def parse_args(): parser.add_argument('--profile', help='enable profiling', action='store_true') - + parser.add_argument('--record-movie', + help='record a movie of each task', + action='store_true') return parser.parse_args() From 58ca916daca15d554bbaec6f361c00fe5e59d2c2 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 27 Oct 2020 11:30:08 -0400 Subject: [PATCH 062/170] things, and record movie downsampling --- src/sessions/ses-things.py | 2 +- src/shared/cli.py | 6 +++--- src/tasks/things.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sessions/ses-things.py b/src/sessions/ses-things.py index 88588291..75b2c876 100644 --- a/src/sessions/ses-things.py +++ b/src/sessions/ses-things.py @@ -14,7 +14,7 @@ def get_tasks(parsed): session_design_filename, IMAGE_PATH, run, - name=f"sub-{parsed.subject}_ses-{parsed.session}_run-{run}") \ + name=f"task-things_run-{run}") \ for run in range(1,n_runs+1) ] return tasks diff --git a/src/shared/cli.py b/src/shared/cli.py index 193b746d..97b9c9de 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -25,12 +25,12 @@ def listen_shortcuts(): return False def run_task_loop(loop, eyetracker=None, gaze_drawer=None, record_movie=False): - for _ in loop: + for frameN, _ in enumerate(loop): if gaze_drawer: gaze = eyetracker.get_gaze() if not gaze is None: gaze_drawer.draw_gazepoint(gaze) - if record_movie: + if record_movie and frameN%6==0: record_movie.getMovieFrame(buffer='back') # check for global event keys shortcut_evt = listen_shortcuts() @@ -227,7 +227,7 @@ def main_loop(all_tasks, task.output_path, '%s_%s.mp4'%(task.output_fname_base, task.name)) print(f"saving movie as {out_fname}") - exp_win.saveMovieFrames(out_fname) + exp_win.saveMovieFrames(out_fname, fps=10) task.unload() if shortcut_evt=='q': diff --git a/src/tasks/things.py b/src/tasks/things.py index 5864e932..00f9f9a7 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -82,9 +82,9 @@ def set_trial_timing(trial, key): if ctl_win: self.fixation_cross.draw(ctl_win) yield frameN < 2 - keypress = event.getKeys([RESPONSE_KEY], timeStamped=True) + keypress = event.getKeys([RESPONSE_KEY], timeStamped=self.task_timer) trial['response'] = len(keypress) > 0 - trial['response_time'] = trial['onset'] - keypress[0][1] if len(keypress) else None + trial['response_time'] = (keypress[0][1] - trial['onset']) if len(keypress) else None trial['duration'] = trial['offset']-trial['onset'] del trial['stim'] From 8db527000c1645087f47261382ac1437b684532c Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 27 Oct 2020 15:28:46 -0400 Subject: [PATCH 063/170] change Things task for non-slip timing, --- src/sessions/ses-things.py | 1 + src/shared/cli.py | 9 +++---- src/tasks/things.py | 54 +++++++++++++++++++++----------------- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/sessions/ses-things.py b/src/sessions/ses-things.py index 75b2c876..0bbad4c9 100644 --- a/src/sessions/ses-things.py +++ b/src/sessions/ses-things.py @@ -86,6 +86,7 @@ def generate_design_file(subject, session): ]) run_trials = run_trials.iloc[randorder] run_trials['run'] = run + 1 + run_trials['onset'] = initial_wait + np.arange(n_trials_total) * trial_duration run_trials['duration'] = image_duration all_run_trials = pandas.concat([all_run_trials, run_trials]) out_fname = os.path.join(THINGS_DATA_PATH, 'designs', f"sub-{parsed.subject}_ses-{parsed.session}_design.tsv") diff --git a/src/shared/cli.py b/src/shared/cli.py index 97b9c9de..e7950676 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -49,12 +49,9 @@ def run_task(task, exp_win, ctl_win=None, eyetracker=None, gaze_drawer=None, rec ) if task.use_fmri and not shortcut_evt: - shortcut_evt = run_task_loop( - fmri.wait_for_ttl(), - eyetracker, - gaze_drawer - ) - if shortcut_evt: return shortcut_evt + while fmri.wait_for_ttl(): + shortcut_evt = listen_shortcuts() + if shortcut_evt: return shortcut_evt logging.info('GO') if eyetracker and not shortcut_evt: diff --git a/src/tasks/things.py b/src/tasks/things.py index 00f9f9a7..a58f676e 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -4,11 +4,8 @@ from ..shared import config -STIMULI_DURATION=.5 -BASELINE_BEGIN=5 -BASELINE_END=5 -ISI=4 RESPONSE_KEY='d' +RESPONSE_TIME=4 class Things(Task): @@ -38,7 +35,7 @@ def _setup(self, exp_win): exp_win, os.path.join(self.images_path, trial['image_path'])) self.trials = data.TrialHandler(self.design, 1, method='sequential') - self.duration = len(self.design)*(STIMULI_DURATION+ISI) + BASELINE_BEGIN + BASELINE_END + self.duration = len(self.design) super()._setup(exp_win) def _instructions(self, exp_win, ctl_win): @@ -55,33 +52,42 @@ def _instructions(self, exp_win, ctl_win): def _run(self, exp_win, ctl_win): exp_win.logOnFlip(level=logging.EXP, msg='Things: task starting at %f'%time.time()) - for frameN in range(config.FRAME_RATE * BASELINE_BEGIN): - self.fixation_cross.draw(exp_win) - if ctl_win: - self.fixation_cross.draw(ctl_win) - yield frameN<2 + self.fixation_cross.draw(exp_win) + if ctl_win: + self.fixation_cross.draw(ctl_win) + yield True def set_trial_timing(trial, key): trial[key] = self.task_timer.getTime() for trial in self.trials: - exp_win.logOnFlip(level=logging.EXP, msg=f"image: display {trial['image_path']}") + exp_win.logOnFlip(level=logging.EXP, msg=f"image: {trial['condition']}:{trial['image_path']}") exp_win.callOnFlip(set_trial_timing, trial, 'onset') self.progress_bar.set_description(f"Trial:: {trial['condition']}:{trial['image_path']}" ) - for frameN in range(int(config.FRAME_RATE * trial['duration'])): - trial['stim'].draw(exp_win) - self.fixation_cross.draw(exp_win) - if ctl_win: - trial['stim'].draw(ctl_win) - self.fixation_cross.draw(ctl_win) - yield frameN < 2 + + # draw to backbuffer + trial['stim'].draw(exp_win) + self.fixation_cross.draw(exp_win) + if ctl_win: + trial['stim'].draw(ctl_win) + self.fixation_cross.draw(ctl_win) + # wait onset + while self.task_timer.getTime() < trial['onset'] - 1/(config.FRAME_RATE*2): + pass + yield True #flip + + # draw to backbuffer exp_win.callOnFlip(set_trial_timing, trial, 'offset') - exp_win.logOnFlip(level=logging.EXP, msg='image: rest') - for frameN in range(config.FRAME_RATE * ISI): - self.fixation_cross.draw(exp_win) - if ctl_win: - self.fixation_cross.draw(ctl_win) - yield frameN < 2 + exp_win.logOnFlip(level=logging.EXP, msg='fixation') + self.fixation_cross.draw(exp_win) + if ctl_win: + self.fixation_cross.draw(ctl_win) + while self.task_timer.getTime() < trial['onset'] + trial['duration']: + pass + yield True #flip + + while self.task_timer.getTime() < trial['onset'] + RESPONSE_TIME: + pass keypress = event.getKeys([RESPONSE_KEY], timeStamped=self.task_timer) trial['response'] = len(keypress) > 0 trial['response_time'] = (keypress[0][1] - trial['onset']) if len(keypress) else None From 3b2adaa3d39567cd16ab6d8842a80274c02e313e Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 27 Oct 2020 15:35:56 -0400 Subject: [PATCH 064/170] reset screen after script run --- main.py | 1 + src/shared/screen.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 3bb5fba6..4104fbe5 100755 --- a/main.py +++ b/main.py @@ -25,6 +25,7 @@ def run(parsed): parsed.ctl_win, parsed.run_on_battery, parsed.ptt) + screen.reset_exp_screen() def run_profiled(parsed): import cProfile diff --git a/src/shared/screen.py b/src/shared/screen.py index d3f326c7..69330222 100644 --- a/src/shared/screen.py +++ b/src/shared/screen.py @@ -6,4 +6,12 @@ def init_exp_screen(): 'xrandr', '--output', config.EXP_SCREEN_XRANDR_NAME, '--mode', '%dx%d'%config.EXP_WINDOW['size'], - '--rate', str(config.FRAME_RATE)]) + '--rate', str(config.FRAME_RATE), + ]) + +def reset_exp_screen(): + xrandr = Popen([ + 'xrandr', + '--output', config.EXP_SCREEN_XRANDR_NAME, + '--preferred', + ]) From 198316daa9e236db096e4b7ca80142a4483048ed Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 28 Oct 2020 14:39:01 -0400 Subject: [PATCH 065/170] improve non-slip timing --- main.py | 28 ++++++++++++++++------------ src/shared/cli.py | 2 +- src/shared/fmri.py | 2 ++ src/tasks/task_base.py | 16 +++++++++++----- src/tasks/things.py | 35 +++++++++++++++-------------------- 5 files changed, 45 insertions(+), 38 deletions(-) diff --git a/main.py b/main.py index b1d329e5..f80f2ea6 100755 --- a/main.py +++ b/main.py @@ -14,18 +14,22 @@ def run(parsed): except ImportError: raise(ValueError('session tasks file cannot be found for %s'%parsed.session)) from src.shared import cli - cli.main_loop( - tasks[parsed.skip_n_tasks:], - parsed.subject, - parsed.session, - parsed.output, - parsed.eyetracking, - parsed.fmri, - parsed.meg, - parsed.ctl_win, - parsed.run_on_battery, - parsed.ptt, - parsed.record_movie) + try: + cli.main_loop( + tasks[parsed.skip_n_tasks:], + parsed.subject, + parsed.session, + parsed.output, + parsed.eyetracking, + parsed.fmri, + parsed.meg, + parsed.ctl_win, + parsed.run_on_battery, + parsed.ptt, + parsed.record_movie, + ) + finally: + screen.reset_exp_screen() def run_profiled(parsed): import cProfile diff --git a/src/shared/cli.py b/src/shared/cli.py index e7950676..d7fcb435 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -49,7 +49,7 @@ def run_task(task, exp_win, ctl_win=None, eyetracker=None, gaze_drawer=None, rec ) if task.use_fmri and not shortcut_evt: - while fmri.wait_for_ttl(): + for _ in fmri.wait_for_ttl(): shortcut_evt = listen_shortcuts() if shortcut_evt: return shortcut_evt diff --git a/src/shared/fmri.py b/src/shared/fmri.py index e77b6541..36a9b030 100644 --- a/src/shared/fmri.py +++ b/src/shared/fmri.py @@ -1,5 +1,6 @@ from psychopy import core, event, logging from psychopy.hardware.emulator import launchScan +import time MR_settings = { 'TR': 2.000, # duration (sec) per whole-brain volume @@ -27,4 +28,5 @@ def wait_for_ttl(): logging.exp(msg="fMRI TTL %d"%ttl_index) ttl_index += 1 return + time.sleep(.0005) #just to avoid looping to fast yield diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index f2e4cccf..2a9f8856 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -24,10 +24,13 @@ def setup(self, exp_win, output_path, output_fname_base, use_fmri=False, use_eye self.use_meg = use_meg self.use_eyetracking = use_eyetracking self._setup(exp_win) - - def _setup(self, exp_win): # initialize a progress bar if we know the duration of the task self.progress_bar = tqdm.tqdm(total=self.duration) if hasattr(self, 'duration') else False + if not hasattr(self,'_progress_bar_refresh_rate'): + self._progress_bar_refresh_rate = config.FRAME_RATE + + def _setup(self, exp_win): + pass def _generate_tsv_filename(self): for fi in range(1000): @@ -44,8 +47,8 @@ def __str__(self): def _flip_all_windows(self, exp_win, ctl_win=None, clearBuffer=True): if not ctl_win is None: - ctl_win.flip(clearBuffer=clearBuffer) - exp_win.flip(clearBuffer=clearBuffer) + self._ctl_win_last_flip_time = ctl_win.flip(clearBuffer=clearBuffer) + self._exp_win_last_flip_time = exp_win.flip(clearBuffer=clearBuffer) def instructions(self, exp_win, ctl_win): if hasattr(self, '_instructions'): @@ -65,10 +68,12 @@ def run(self, exp_win, ctl_win): # yield first to allow external draw before flip yield self._flip_all_windows(exp_win, ctl_win, clearBuffer) + if not hasattr(self, '_exp_win_first_flip_time'): + self._exp_win_first_flip_time = self._exp_win_last_flip_time # increment the progress bar every second if self.progress_bar: frame_idx += 1 - if not frame_idx%config.FRAME_RATE: + if not frame_idx % self._progress_bar_refresh_rate: self.progress_bar.update(1) if self.progress_bar: @@ -103,6 +108,7 @@ def __init__(self, text="Taking a short break, relax...", **kwargs): def _setup(self, exp_win): self.use_fmri = False self.use_eyetracking = False + super()._setup(exp_win) def _run(self, exp_win, ctl_win): screen_text = visual.TextStim( diff --git a/src/tasks/things.py b/src/tasks/things.py index a58f676e..9c95da50 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -26,7 +26,7 @@ def __init__(self, design, images_path, run, *args,**kwargs): def _setup(self, exp_win): self.fixation_cross = visual.ImageStim( exp_win, - os.path.join('data','things', 'images','fixation_cross.png'), + os.path.join('data','things', 'images', 'fixation_cross.png'), size=(.1,.1), units='height', opacity=.5) #preload all images @@ -36,6 +36,7 @@ def _setup(self, exp_win): os.path.join(self.images_path, trial['image_path'])) self.trials = data.TrialHandler(self.design, 1, method='sequential') self.duration = len(self.design) + self._progress_bar_refresh_rate = 2 # 2 flips per trial super()._setup(exp_win) def _instructions(self, exp_win, ctl_win): @@ -57,13 +58,9 @@ def _run(self, exp_win, ctl_win): self.fixation_cross.draw(ctl_win) yield True - def set_trial_timing(trial, key): - trial[key] = self.task_timer.getTime() - - for trial in self.trials: + for trial_n, trial in enumerate(self.trials): exp_win.logOnFlip(level=logging.EXP, msg=f"image: {trial['condition']}:{trial['image_path']}") - exp_win.callOnFlip(set_trial_timing, trial, 'onset') - self.progress_bar.set_description(f"Trial:: {trial['condition']}:{trial['image_path']}" ) + self.progress_bar.set_description(f"Trial {trial_n}:: {trial['condition']}:{trial['image_path']}" ) # draw to backbuffer trial['stim'].draw(exp_win) @@ -72,33 +69,31 @@ def set_trial_timing(trial, key): trial['stim'].draw(ctl_win) self.fixation_cross.draw(ctl_win) # wait onset - while self.task_timer.getTime() < trial['onset'] - 1/(config.FRAME_RATE*2): - pass + while self.task_timer.getTime() < trial['onset'] - 1/config.FRAME_RATE: + time.sleep(.0005) #just to avoid looping to fast yield True #flip + trial['onset_flip'] = self._exp_win_last_flip_time - self._exp_win_first_flip_time # draw to backbuffer - exp_win.callOnFlip(set_trial_timing, trial, 'offset') exp_win.logOnFlip(level=logging.EXP, msg='fixation') self.fixation_cross.draw(exp_win) if ctl_win: self.fixation_cross.draw(ctl_win) - while self.task_timer.getTime() < trial['onset'] + trial['duration']: - pass + while self.task_timer.getTime() < trial['onset'] + trial['duration'] - 1/config.FRAME_RATE: + time.sleep(.0005) #just to avoid looping to fast yield True #flip + trial['offset_flip'] = self._exp_win_last_flip_time - self._exp_win_first_flip_time - while self.task_timer.getTime() < trial['onset'] + RESPONSE_TIME: - pass + while self.task_timer.getTime() < trial['onset'] + RESPONSE_TIME - 1/config.FRAME_RATE: + time.sleep(.0005) #just to avoid looping to fast keypress = event.getKeys([RESPONSE_KEY], timeStamped=self.task_timer) trial['response'] = len(keypress) > 0 trial['response_time'] = (keypress[0][1] - trial['onset']) if len(keypress) else None - trial['duration'] = trial['offset']-trial['onset'] + trial['duration_flip'] = trial['offset_flip']-trial['onset_flip'] del trial['stim'] - for frameN in range(config.FRAME_RATE * BASELINE_END): - self.fixation_cross.draw(exp_win) - if ctl_win: - self.fixation_cross.draw(ctl_win) - yield frameN<2 + while self.task_timer.getTime() < trial['onset'] + RESPONSE_TIME: + time.sleep(.0005) def save(self): self.trials.saveAsWideText(self._generate_tsv_filename()) From e7ecd6f80e5d766cee2cc68f5ea41068050d2514 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 3 Nov 2020 10:21:44 -0500 Subject: [PATCH 066/170] update zmq_tools and eyetracking for pupil2 --- src/shared/eyetracking.py | 37 +++++++++++++++++++++++-------------- src/shared/zmq_tools.py | 26 ++++++++++++++------------ 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index cee3bb57..bb990439 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -33,6 +33,7 @@ "frame_rate": 250, "exposure_time": 4000, "global_gain": 1, +# "uid": "Aravis-Fake-GV01", # for test purposes "uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", } @@ -73,7 +74,6 @@ def _run(self, exp_win, ctl_win): print('calibration started') window_size_frame = exp_win.size-MARKER_SIZE*2 - print(window_size_frame) circle_marker = visual.Circle( exp_win, edges=64, units='pixels', lineColor=None,fillColor=self.marker_fill_color, @@ -152,7 +152,7 @@ def __init__(self, output_path, output_fname_base): self._pupil_process = Popen([ 'python3', os.path.join(os.environ['PUPIL_PATH'],'pupil_src','main.py'), - 'capture']) + 'capture', '--debug']) self._ctx = zmq.Context() self._req_socket = self._ctx.socket(zmq.REQ) @@ -163,6 +163,7 @@ def __init__(self, output_path, output_fname_base): 'subject':'eye_process.should_start.0', 'eye_id':0, 'args':{}}) time.sleep(1) + """ self.send_recv_notification( { 'subject': 'start_eye_plugin', @@ -170,6 +171,7 @@ def __init__(self, output_path, output_fname_base): 'target': 'eye0' } ) + """ self.send_recv_notification( { 'subject': 'start_eye_plugin', @@ -184,11 +186,19 @@ def __init__(self, output_path, output_fname_base): 'subject':'stop_plugin', 'name':'Recorder' }) + """ + # stop NDSI for performance, doesn't work + self.send_recv_notification({ + 'subject':'stop_eye_plugin', + 'name':'NDSI_Manager' + }) + """ + """ self.send_recv_notification({ 'subject':'stop_plugin', 'name':'Accuracy_Visualizer','args':{} }) - + """ #restart with new params # self.send_recv_notification({ # 'subject':'start_plugin', @@ -203,6 +213,9 @@ def __init__(self, output_path, output_fname_base): 'raw_jpeg':False, 'record_eye':True} }) + + + """ self.send_recv_notification({ 'subject':'start_plugin', 'name':'Pupil_Remote','args':{}}) @@ -211,7 +224,6 @@ def __init__(self, output_path, output_fname_base): "subject": "set_detection_mapping_mode", "mode": "2d"}) - """ self.send_recv_notification({ 'subject':'start_plugin', 'name':'Detector2DPlugin', @@ -283,22 +295,19 @@ def get_gaze(self): if locked: return self.gaze - def calibrate(self, pupil_list, ref_list, frame_size): + def calibrate(self, pupil_list, ref_list): if len(pupil_list) < 100: logging.error('Calibration: not enough pupil captured for calibration') #return - self.send_recv_notification({ - 'subject':'start_plugin', - 'name':'External_Calibration', - 'args':{'frame_size': frame_size.tolist()}}) + calib_data = {"ref_list": ref_list, "pupil_list": pupil_list} self.send_recv_notification({ - 'subject':'calibrate.from_external_data', - 'pupil_list':pupil_list, - 'ref_list':ref_list}) - - + 'subject':'start_plugin', + 'name':'Gazer2D', + 'args': {'calib_data':calib_data}, + 'raise_calibration_error':True, + }) class GazeDrawer(): diff --git a/src/shared/zmq_tools.py b/src/shared/zmq_tools.py index a8799271..c1942017 100644 --- a/src/shared/zmq_tools.py +++ b/src/shared/zmq_tools.py @@ -1,7 +1,7 @@ """ (*)~--------------------------------------------------------------------------- Pupil - eye tracking platform -Copyright (C) 2012-2018 Pupil Labs +Copyright (C) 2012-2020 Pupil Labs Distributed under the terms of the GNU Lesser General Public License (LGPL v3.0). @@ -42,6 +42,8 @@ def emit(self, record): try: self.socket.send(record_dict) except TypeError: + # stringify message in case it is not a string yet + record_dict["msg"] = str(record_dict["msg"]) # stringify `exc_info` since it includes unserializable objects if record_dict["exc_info"]: # do not convert if it is None record_dict["exc_info"] = str(record_dict["exc_info"]) @@ -105,16 +107,13 @@ def recv(self): Any addional message frames will be added as a list in the payload dict with key: '__raw_data__' . """ - try: - topic = self.recv_topic() - remaining_frames = self.recv_remaining_frames() - payload = self.deserialize_payload(*remaining_frames) - return topic, payload - except zmq.ZMQError: - return None + topic = self.recv_topic() + remaining_frames = self.recv_remaining_frames() + payload = self.deserialize_payload(*remaining_frames) + return topic, payload def recv_topic(self): - return self.socket.recv_string(zmq.NOBLOCK) + return self.socket.recv_string() def recv_remaining_frames(self): while self.socket.get(zmq.RCVMORE): @@ -137,13 +136,16 @@ class Msg_Streamer(ZMQ_Socket): Not threadsave. Make a new one for each thread """ - def __init__(self, ctx, url): + def __init__(self, ctx, url, hwm=None): self.socket = zmq.Socket(ctx, zmq.PUB) + if hwm is not None: + self.socket.set_hwm(hwm) + self.socket.connect(url) def send(self, payload, deprecated=()): """Send a message with topic, payload -` + Topic is a unicode string. It will be sent as utf-8 encoded byte array. Payload is a python dict. It will be sent as a msgpack serialized dict. @@ -153,7 +155,7 @@ def send(self, payload, deprecated=()): the contents of the iterable in '__raw_data__' require exposing the pyhton memoryview interface. """ - assert deprecated is (), "Depracted use of send()" + assert deprecated == (), "Depracted use of send()" assert "topic" in payload, "`topic` field required in {}".format(payload) if "__raw_data__" not in payload: From 3836236e8af6ce07161e9c36251f2ed7ba038453 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 11 Nov 2020 11:40:23 -0500 Subject: [PATCH 067/170] add dev option for debug/profile, cleanup and reorde init --- src/shared/cli.py | 4 +- src/shared/eyetracking.py | 96 +++++++++++++++------------------------ 2 files changed, 39 insertions(+), 61 deletions(-) diff --git a/src/shared/cli.py b/src/shared/cli.py index d7fcb435..f8d1a446 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -142,7 +142,9 @@ def main_loop(all_tasks, print('creating et client') eyetracker_client = eyetracking.EyeTrackerClient( output_path=log_path, - output_fname_base=log_name_prefix + output_fname_base=log_name_prefix, + profile=False, + debug=False, ) print('starting et client') eyetracker_client.start() diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index bb990439..ae4fc75c 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -136,7 +136,7 @@ def nonblocking(lock): class EyeTrackerClient(threading.Thread): - def __init__(self, output_path, output_fname_base): + def __init__(self, output_path, output_fname_base, profile=False, debug=False): super(EyeTrackerClient, self).__init__() self.stoprequest = threading.Event() self.lock = threading.Lock() @@ -147,94 +147,70 @@ def __init__(self, output_path, output_fname_base): self.output_path = output_path self.output_fname_base = output_fname_base self.record_dir = os.path.join(self.output_path, self.output_fname_base + '.pupil') - os.makedirs(self.record_dir) + os.makedirs(self.record_dir, exist_ok=True) + + dev_opts = [] + if debug: + dev_opts.append('--debug') + if profile: + dev_opts.append('--profile') self._pupil_process = Popen([ 'python3', os.path.join(os.environ['PUPIL_PATH'],'pupil_src','main.py'), - 'capture', '--debug']) + 'capture'] + dev_opts + ) self._ctx = zmq.Context() self._req_socket = self._ctx.socket(zmq.REQ) self._req_socket.connect('tcp://localhost:50020') + # stop eye1 if started: monocular eyetracking in the MRI + notif = self.send_recv_notification({ + 'subject':'eye_process.should_stop.1', + 'eye_id':1, 'args':{}}) + # start eye0 if not started yet (from pupil saved config) notif = self.send_recv_notification({ 'subject':'eye_process.should_start.0', 'eye_id':0, 'args':{}}) + + # wait for eye process to start before starting plugins time.sleep(1) - """ - self.send_recv_notification( - { - 'subject': 'start_eye_plugin', - 'name': 'Aravis_Manager', - 'target': 'eye0' - } - ) - """ - self.send_recv_notification( - { - 'subject': 'start_eye_plugin', - 'name': 'Aravis_Source', - 'target': 'eye0', - 'args' : CAPTURE_SETTINGS - } - ) - # quit existing plugin + # quit existing recorder plugin self.send_recv_notification({ 'subject':'stop_plugin', - 'name':'Recorder' + 'name':'Recorder', }) - """ - # stop NDSI for performance, doesn't work - self.send_recv_notification({ - 'subject':'stop_eye_plugin', - 'name':'NDSI_Manager' - }) - """ - """ - self.send_recv_notification({ - 'subject':'stop_plugin', - 'name':'Accuracy_Visualizer','args':{} - }) - """ - #restart with new params -# self.send_recv_notification({ -# 'subject':'start_plugin', -# 'name':'Fixed_Screen_Marker_Calibration', -# 'args':{'fullscreen':True, 'marker_scale':.8, 'sample_duration':120, 'monitor_idx':1}}) - # setup recorder output path + # restart recorder plugin with custom output settings self.send_recv_notification({ 'subject':'start_plugin', 'name':'Recorder','args':{ 'rec_root_dir':self.record_dir, 'session_name':self.output_fname_base + '.pupil', 'raw_jpeg':False, - 'record_eye':True} + 'record_eye':True, + } }) - - """ - self.send_recv_notification({ - 'subject':'start_plugin', - 'name':'Pupil_Remote','args':{}}) + # stop a bunch of eye plugins for performance + for plugin in ['NDSI_Manager', 'Detector3DPlugin']: + self.send_recv_notification({ + 'subject':'stop_eye_plugin', + 'target':'eye0', + 'name': plugin, + }) self.send_recv_notification({ - "subject": "set_detection_mapping_mode", - "mode": "2d"}) - - self.send_recv_notification({ - 'subject':'start_plugin', - 'name':'Detector2DPlugin', - 'target':'eye0', - 'args':{}}) - """ - + 'subject': 'start_eye_plugin', + 'name': 'Aravis_Source', + 'target': 'eye0', + 'args' : CAPTURE_SETTINGS, + }) - #self.send_recv_notification({'subject':'recording.should_start',}) - # wait for the whole schmilblick to boot - time.sleep(4) + # wait for the whole schmilblick to boot before running the thread + #time.sleep(4) def send_recv_notification(self, n): # REQ REP requirese lock step communication with multipart msg (topic,msgpack_encoded dict) From c691d3afb498d48c74d4f46d7c078d9fe3fdd294 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 11 Nov 2020 13:34:29 -0500 Subject: [PATCH 068/170] fix call to calibration --- src/shared/eyetracking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index 6f87fa01..e6ba9efe 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -119,7 +119,7 @@ def _run(self, exp_win, ctl_win): all_pupils.append(pupil) yield True yield True - self.eyetracker.calibrate(all_pupils, all_refs_per_flip, exp_win.size) + self.eyetracker.calibrate(all_pupils, all_refs_per_flip) from subprocess import Popen From fdae44f6c9b38d2e20edce2846f7ab83025f8768 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 9 Dec 2020 14:36:37 -0500 Subject: [PATCH 069/170] better saving of task events and data, unified name generation to follow BIDS like structure --- src/sessions/ses-shinobi3levels.py | 7 +++-- src/shared/eyetracking.py | 50 ++++++++++++++++++------------ src/tasks/images.py | 2 +- src/tasks/language.py | 2 +- src/tasks/speech.py | 2 +- src/tasks/task_base.py | 11 ++++--- src/tasks/things.py | 2 +- src/tasks/videogame.py | 21 +++++++++++-- 8 files changed, 64 insertions(+), 33 deletions(-) diff --git a/src/sessions/ses-shinobi3levels.py b/src/sessions/ses-shinobi3levels.py index 18a85ff8..708bcf88 100644 --- a/src/sessions/ses-shinobi3levels.py +++ b/src/sessions/ses-shinobi3levels.py @@ -1,3 +1,4 @@ +import random from ..tasks import images, videogame, memory, task_base @@ -14,16 +15,18 @@ "I am completely lost in thought." ] +levels = ['scenario_repeat1', 'scenario_Level4-1', 'scenario_Level5-0'] +random.shuffle(levels) # randomize order TASKS = sum([ [ videogame.VideoGameMultiLevel( state_names=['Level1-0','Level4-1','Level5-0'], - scenarii=['data/videogames/%s.json'%sc for sc in ['scenario_repeat1', 'scenario_Level4-1', 'scenario_Level5-1']], # this scenario repeats the same level + scenarii=['data/videogames/%s.json'%sc for sc in levels], # this scenario repeats the same level repeat_scenario=True, max_duration=10*60, # if when level completed or dead we exceed that time in secs, stop the task - name=f"shinobi-3levels-{run+1:02d}", + name=f"task-shinobi_run-{run+1:02d}", #post_level_ratings = [(q, 7) for q in flow_ratings] ), task_base.Pause() diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index e6ba9efe..1c226103 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -25,16 +25,16 @@ # number of frames to eliminate at start and end of marker CALIBRATION_LEAD_IN = 20 CALIBRATION_LEAD_OUT = 20 -# remove pupil samples with low confidence -PUPIL_CONFIDENCE_THRESHOLD = .4 +# Pupil settings +PUPIL_REMOTE_PORT = 50123 CAPTURE_SETTINGS = { "frame_size": [640, 480], "frame_rate": 250, "exposure_time": 4000, "global_gain": 1, -# "uid": "Aravis-Fake-GV01", # for test purposes - "uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", + "uid": "Aravis-Fake-GV01", # for test purposes +# "uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", } class EyetrackerCalibration(Task): @@ -81,8 +81,8 @@ def _run(self, exp_win, ctl_win): random_order = np.random.permutation(np.arange(len(MARKER_POSITIONS))) - all_refs_per_flip = [] - all_pupils = [] + self.all_refs_per_flip = [] + self.all_pupils = [] radius_anim = np.hstack([np.linspace(MARKER_SIZE,0,MARKER_DURATION_FRAMES/2), np.linspace(0,MARKER_SIZE,MARKER_DURATION_FRAMES/2)]) @@ -106,20 +106,28 @@ def _run(self, exp_win, ctl_win): pupil = self.eyetracker.get_pupil() - exp_win.logOnFlip(level=logging.EXP, - msg="pupil: pos=(%f,%f), diameter=%d"%tuple(pupil['norm_pos']+[pupil['diameter']])) + if pupil: + print(pupil) + exp_win.logOnFlip(level=logging.EXP, + msg="pupil: pos=(%f,%f), diameter=%d"%tuple(pupil['norm_pos']+[pupil['diameter']])) if f > CALIBRATION_LEAD_IN and f < len(radius_anim)-CALIBRATION_LEAD_OUT: - if pupil and pupil['confidence'] > PUPIL_CONFIDENCE_THRESHOLD: - pos_decenter = (pos/exp_win.size*2).tolist() - ref = { - 'norm_pos': pos_decenter, - 'screen_pos': pos_decenter, - 'timestamp': pupil['timestamp']} - all_refs_per_flip.append(ref) - all_pupils.append(pupil) + pos_decenter = (pos/exp_win.size*2).tolist() + ref = { + 'norm_pos': pos_decenter, + 'screen_pos': pos_decenter, + 'timestamp': time.monotonic() # =pupil frame timestamp on same computer + } + self.all_refs_per_flip.append(ref) # accumulate all refs + if pupil: + self.all_pupils.append(pupil) yield True yield True - self.eyetracker.calibrate(all_pupils, all_refs_per_flip) + self.eyetracker.calibrate(self.all_pupils, self.all_refs_per_flip) + + def save(self): + if hasattr(self, 'all_pupils'): + fname = self._generate_unique_filename("calib-data", ".npz") + np.savez(fname, pupils=self.all_pupils, markers=self.all_refs_per_flip) from subprocess import Popen @@ -158,12 +166,14 @@ def __init__(self, output_path, output_fname_base, profile=False, debug=False): self._pupil_process = Popen([ 'python3', os.path.join(os.environ['PUPIL_PATH'],'pupil_src','main.py'), - 'capture'] + dev_opts + 'capture', + '--port', str(PUPIL_REMOTE_PORT), + ] + dev_opts ) self._ctx = zmq.Context() self._req_socket = self._ctx.socket(zmq.REQ) - self._req_socket.connect('tcp://localhost:50020') + self._req_socket.connect(f"tcp://localhost:{PUPIL_REMOTE_PORT}") # stop eye1 if started: monocular eyetracking in the MRI notif = self.send_recv_notification({ @@ -276,7 +286,7 @@ def calibrate(self, pupil_list, ref_list): calib_data = {"ref_list": ref_list, "pupil_list": pupil_list} - logging.info('calibrating, %s %s'%(str(pupil_list),str(ref_list))) + logging.info('sending calibration data to pupil') self.send_recv_notification({ 'subject':'start_plugin', 'name':'Gazer2D', diff --git a/src/tasks/images.py b/src/tasks/images.py index c58706f6..3870eb40 100644 --- a/src/tasks/images.py +++ b/src/tasks/images.py @@ -60,7 +60,7 @@ def _run(self, exp_win, ctl_win): yield() def save(self): - self.trials.saveAsWideText(self._generate_tsv_filename()) + self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) class BOLD5000Images(Images): pass diff --git a/src/tasks/language.py b/src/tasks/language.py index 7dc4fc17..7ba5338c 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -111,4 +111,4 @@ def _run(self, exp_win, ctl_win): yield() def save(self): - self.trials.saveAsWideText(self._generate_tsv_filename()) + self.trials.saveAsWideText(self._generate_unique_filename("events","tsv")) diff --git a/src/tasks/speech.py b/src/tasks/speech.py index c67cb352..f5319f9e 100644 --- a/src/tasks/speech.py +++ b/src/tasks/speech.py @@ -62,4 +62,4 @@ def _run(self, exp_win, ctl_win): yield() def save(self): - self.trials.saveAsWideText(self._generate_tsv_filename()) + self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index 2a9f8856..139d1b49 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -32,11 +32,12 @@ def setup(self, exp_win, output_path, output_fname_base, use_fmri=False, use_eye def _setup(self, exp_win): pass - def _generate_tsv_filename(self): - for fi in range(1000): - fname = os.path.join(self.output_path, '%s_%s_%03d.tsv'%(self.output_fname_base, self.name,fi)) - if not os.path.exists(fname): - break + def _generate_unique_filename(self, suffix, ext='tsv'): + fname = os.path.join(self.output_path, f"{self.output_fname_base}_{self.name}_{suffix}.{ext}") + fi = 1 + while os.path.exists(fname): + fname = os.path.join(self.output_path, f"{self.output_fname_base}_{self.name}_{suffix}-{fi:03d}.{ext}") + fi += 1 return fname def unload(self): diff --git a/src/tasks/things.py b/src/tasks/things.py index 9c95da50..4f7814d2 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -96,4 +96,4 @@ def _run(self, exp_win, ctl_win): time.sleep(.0005) def save(self): - self.trials.saveAsWideText(self._generate_tsv_filename()) + self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index abf1d646..2c858a1e 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -1,5 +1,6 @@ import os, sys, time, queue import numpy as np +import pandas import threading from psychopy import visual, core, data, logging, event, sound, constants @@ -201,6 +202,13 @@ def _run_emulator(self, exp_win, ctl_win): # render the initial frame and audio self._render_graphics_sound(self._first_frame, self.emulator.em.get_audio(), exp_win, ctl_win) exp_win.logOnFlip(level=logging.EXP, msg="level step: %d"%level_step) + exp_win.callOnFlip(self._log_event, + { + 'trial_type': 'gym-retro_game', + 'game': self.game_name, + 'level': self.state_name, + 'stim_file': self.movie_path, + }) yield True while not _done: level_step += 1 @@ -232,6 +240,10 @@ def _unset_key_handler(self, exp_win): exp_win.winHandle.on_key_press = event._onPygletKey #del exp_win.winHandle.on_key_release + def _log_event(self, event): + event.update({'onset':self.task_timer.getTime()}) + self._events.append(event) + def _run(self, exp_win, ctl_win): self._set_key_handler(exp_win) @@ -245,7 +257,6 @@ def _run(self, exp_win, ctl_win): exp_win.logOnFlip( level=logging.EXP, msg='VideoGame %s: %s starting at %f'%(self.game_name, self.state_name, time.time())) - yield from self._run_emulator(exp_win, ctl_win) if self.post_level_ratings: yield from self._run_ratings(exp_win, ctl_win) @@ -376,6 +387,11 @@ def _stop(self, exp_win, ctl_win): exp_win.waitBlanking = True yield from super()._stop(exp_win, ctl_win) + def save(self): + fname = _generate_unique_filename("events", "tsv") + df = pandas.DataFrame(self._events) + df.to_csv(fname, sep='\t', index=False) + class VideoGameMultiLevel(VideoGame): def __init__(self, *args,**kwargs): @@ -411,7 +427,8 @@ def _run(self, exp_win, ctl_win): if self._nlevels > 1: self._set_recording_file() yield from self._instructions(exp_win, ctl_win) - yield from self._questionnaire(exp_win, ctl_win) + yield from self._questionnaire(exp_win, ctl_win) # here for tests, to move after _run_emulator + yield from super()._run_emulator(exp_win, ctl_win) self.game_sound.stop() if self.post_level_ratings: From f1fef132f2b28ddeffe79acdaa85dc169be8aae6 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 9 Dec 2020 15:43:54 -0500 Subject: [PATCH 070/170] rename emotions as liris --- src/sessions/{ses-emotions.py => ses-liris.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/sessions/{ses-emotions.py => ses-liris.py} (100%) diff --git a/src/sessions/ses-emotions.py b/src/sessions/ses-liris.py similarity index 100% rename from src/sessions/ses-emotions.py rename to src/sessions/ses-liris.py From 5ae2b547b6bacc536ab6ddfd5ce7c41f4174354b Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 9 Dec 2020 15:50:31 -0500 Subject: [PATCH 071/170] fix task naming for liris --- src/sessions/ses-liris.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/sessions/ses-liris.py b/src/sessions/ses-liris.py index e5b34003..4f275344 100644 --- a/src/sessions/ses-liris.py +++ b/src/sessions/ses-liris.py @@ -3,26 +3,25 @@ def get_videos(subject, session): video_idx = np.loadtxt( - 'data/emotions/order_fmri_neuromod.csv', + 'data/liris/order_fmri_neuromod.csv', delimiter=',', skiprows=1, dtype=np.int ) selected_idx = video_idx[video_idx[:,0]==session, subject+1] - print(selected_idx) - return ["data/emotions/videos/%03d.mp4"%i for i in selected_idx] + return selected_idx def get_tasks(parsed): tasks = [] - video_paths = get_videos(int(parsed.subject), int(parsed.session)) + video_indices = get_videos(int(parsed.subject), int(parsed.session)) - for p in video_paths: + for idx in video_indices: tasks.append( video.SingleVideo( - p, - name='emotion_%s'%video + f"data/liris/videos/{idx:03d}.mp4", + name=f"task-liris{idx:03d}" ) ) tasks.append( From 56f07daa8829b15b9b49c53bed8afe7dfd1361f9 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 10 Dec 2020 14:18:04 -0500 Subject: [PATCH 072/170] update eyetracking, fix threading issues --- src/shared/eyetracking.py | 41 +++++++++++++++++++++++++-------------- src/shared/zmq_tools.py | 5 ++++- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index 1c226103..fb574d0a 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -33,15 +33,21 @@ "frame_rate": 250, "exposure_time": 4000, "global_gain": 1, - "uid": "Aravis-Fake-GV01", # for test purposes -# "uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", + "gev_packet_size": 9136, +# "uid": "Aravis-Fake-GV01", # for test purposes + "uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", } class EyetrackerCalibration(Task): - def __init__(self,eyetracker, order='random', marker_fill_color=MARKER_FILL_COLOR, **kwargs): + def __init__(self,eyetracker, + markers_order='random', + marker_fill_color=MARKER_FILL_COLOR, + markers=MARKER_POSITIONS, + **kwargs): self.use_eyetracking = True - self.order = order + self.markers_order = markers_order + self.markers = markers self.marker_fill_color = marker_fill_color super().__init__(**kwargs) self.eyetracker = eyetracker @@ -71,6 +77,7 @@ def _run(self, exp_win, ctl_win): if start_calibration: break yield False + logging.info('calibration started') print('calibration started') window_size_frame = exp_win.size-MARKER_SIZE*2 @@ -79,7 +86,9 @@ def _run(self, exp_win, ctl_win): lineColor=None,fillColor=self.marker_fill_color, autoLog=False) - random_order = np.random.permutation(np.arange(len(MARKER_POSITIONS))) + markers_order = np.arange(len(self.markers)) + if self.markers_order == 'random': + markers_order = np.random.permutation(markers_order) self.all_refs_per_flip = [] self.all_pupils = [] @@ -93,8 +102,8 @@ def _run(self, exp_win, ctl_win): yield False exp_win.logOnFlip(level=logging.EXP,msg='eyetracker_calibration: starting at %f'%time.time()) - for site_id in random_order: - marker_pos = MARKER_POSITIONS[site_id] + for site_id in markers_order: + marker_pos = self.markers[site_id] pos = (marker_pos-.5)*window_size_frame circle_marker.pos = pos exp_win.logOnFlip(level=logging.EXP, @@ -107,7 +116,6 @@ def _run(self, exp_win, ctl_win): pupil = self.eyetracker.get_pupil() if pupil: - print(pupil) exp_win.logOnFlip(level=logging.EXP, msg="pupil: pos=(%f,%f), diameter=%d"%tuple(pupil['norm_pos']+[pupil['diameter']])) if f > CALIBRATION_LEAD_IN and f < len(radius_anim)-CALIBRATION_LEAD_OUT: @@ -248,26 +256,29 @@ def join(self, timeout=None): self.send_recv_notification({'subject':'launcher_process.should_stop'}) self._pupil_process.wait(timeout) self._pupil_process.terminate() + time.sleep(1/60.) super(EyeTrackerClient, self).join(timeout) def run(self): self._req_socket.send_string('SUB_PORT') ipc_sub_port = int(self._req_socket.recv()) - self.pupil_monitor = Msg_Receiver(self._ctx,'tcp://localhost:%d'%ipc_sub_port,topics=('gaze','pupil')) + logging.info(f"ipc_sub_port: {ipc_sub_port}") + self.pupil_monitor = Msg_Receiver( + self._ctx, + f"tcp://localhost:{ipc_sub_port}", + topics=('gaze','pupil')) while not self.stoprequest.isSet(): - time.sleep(1/120.) msg = self.pupil_monitor.recv() - while not msg is None: + if not msg is None: topic, tmp = msg with self.lock: if topic.startswith('pupil'): self.pupil = tmp - if topic.startswith('gaze'): + elif topic.startswith('gaze'): self.gaze = tmp - msg = self.pupil_monitor.recv() - - print('eyetracker listener: stopping') + time.sleep(1/120.) + logging.info('eyetracker listener: stopping') def get_pupil(self): with nonblocking(self.lock) as locked: diff --git a/src/shared/zmq_tools.py b/src/shared/zmq_tools.py index c1942017..ce6ee18b 100644 --- a/src/shared/zmq_tools.py +++ b/src/shared/zmq_tools.py @@ -25,6 +25,9 @@ # import ujson as serializer # uncomment for json serialization assert zmq.__version__ > "15.1" +assert ( + serializer.version[0] == 1 +), "msgpack out of date, please upgrade to version (1, 0, 0)" class ZMQ_handler(logging.Handler): @@ -120,7 +123,7 @@ def recv_remaining_frames(self): yield self.socket.recv() def deserialize_payload(self, payload_serialized, *extra_frames): - payload = serializer.loads(payload_serialized, encoding="utf-8") + payload = serializer.loads(payload_serialized) if extra_frames: payload["__raw_data__"] = extra_frames return payload From 87bdf787da1f87a7e2807abd4c4f4adb967fb9ed Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 11 Dec 2020 08:45:02 -0500 Subject: [PATCH 073/170] use callback to store pupils during calibration --- src/shared/eyetracking.py | 64 +++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index fb574d0a..91511352 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -33,9 +33,9 @@ "frame_rate": 250, "exposure_time": 4000, "global_gain": 1, - "gev_packet_size": 9136, -# "uid": "Aravis-Fake-GV01", # for test purposes - "uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", + "gev_packet_size": 2000, + "uid": "Aravis-Fake-GV01", # for test purposes +# "uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", } class EyetrackerCalibration(Task): @@ -66,6 +66,14 @@ def _instructions(self, exp_win, ctl_win): def _setup(self, exp_win): self.use_fmri = False + self._pupils_list = [] + + def _pupil_cb(self, pupil): + if pupil['timestamp'] > self.task_stop: + self.eyetracker.unset_pupil_cb() + return + if pupil['timestamp'] > self.task_start: + self._pupils_list.append(pupil) def _run(self, exp_win, ctl_win): while True: @@ -91,17 +99,18 @@ def _run(self, exp_win, ctl_win): markers_order = np.random.permutation(markers_order) self.all_refs_per_flip = [] - self.all_pupils = [] radius_anim = np.hstack([np.linspace(MARKER_SIZE,0,MARKER_DURATION_FRAMES/2), np.linspace(0,MARKER_SIZE,MARKER_DURATION_FRAMES/2)]) - pupil = None - while pupil is None: # wait until we get at least a pupil - pupil = self.eyetracker.get_pupil() + self.task_start = time.monotonic() + self.task_stop = np.inf + self.eyetracker.set_pupil_cb(self._pupil_cb) + + while not len(self._pupils_list): # wait until we get at least a pupil yield False - exp_win.logOnFlip(level=logging.EXP,msg='eyetracker_calibration: starting at %f'%time.time()) + exp_win.logOnFlip(level=logging.EXP, msg='eyetracker_calibration: starting at %f'%time.time()) for site_id in markers_order: marker_pos = self.markers[site_id] pos = (marker_pos-.5)*window_size_frame @@ -113,24 +122,24 @@ def _run(self, exp_win, ctl_win): circle_marker.draw(exp_win) circle_marker.draw(ctl_win) - pupil = self.eyetracker.get_pupil() - - if pupil: - exp_win.logOnFlip(level=logging.EXP, - msg="pupil: pos=(%f,%f), diameter=%d"%tuple(pupil['norm_pos']+[pupil['diameter']])) if f > CALIBRATION_LEAD_IN and f < len(radius_anim)-CALIBRATION_LEAD_OUT: - pos_decenter = (pos/exp_win.size*2).tolist() + screen_pos = (pos + exp_win.size/2) + norm_pos = screen_pos/exp_win.size ref = { - 'norm_pos': pos_decenter, - 'screen_pos': pos_decenter, + 'norm_pos': norm_pos.tolist(), + 'screen_pos': screen_pos.tolist(), 'timestamp': time.monotonic() # =pupil frame timestamp on same computer } self.all_refs_per_flip.append(ref) # accumulate all refs - if pupil: - self.all_pupils.append(pupil) yield True yield True - self.eyetracker.calibrate(self.all_pupils, self.all_refs_per_flip) + self.task_stop = time.monotonic() + logging.info(f"calibrating on {len(self._pupils_list)} pupils and {len(self.all_refs_per_flip)} markers") + self.eyetracker.calibrate(self._pupils_list, self.all_refs_per_flip) + + def stop(self, exp_win, ctl_win): + self.eyetracker.unset_pupil_cb() + yield def save(self): if hasattr(self, 'all_pupils'): @@ -157,8 +166,9 @@ def __init__(self, output_path, output_fname_base, profile=False, debug=False): self.stoprequest = threading.Event() self.lock = threading.Lock() - self.pupil = None - self.gaze = None + self.pupils = [] + self.gazes = [] + self.unset_pupil_cb() self.output_path = output_path self.output_fname_base = output_fname_base @@ -274,12 +284,20 @@ def run(self): topic, tmp = msg with self.lock: if topic.startswith('pupil'): - self.pupil = tmp + self.pupils.append(tmp) + if self._pupil_cb: + self._pupil_cb(tmp) elif topic.startswith('gaze'): - self.gaze = tmp + self.gazes.append(tmp) time.sleep(1/120.) logging.info('eyetracker listener: stopping') + def set_pupil_cb(self, pupil_cb): + self._pupil_cb = pupil_cb + + def unset_pupil_cb(self): + self._pupil_cb = None + def get_pupil(self): with nonblocking(self.lock) as locked: if locked: From 80f9ad4631e91e73ef52d4c169af14d00196fd9b Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 11 Dec 2020 08:52:36 -0500 Subject: [PATCH 074/170] factorize events saving --- src/shared/eyetracking.py | 2 +- src/tasks/images.py | 3 ++- src/tasks/language.py | 3 ++- src/tasks/speech.py | 3 ++- src/tasks/task_base.py | 19 ++++++++++++++++++- src/tasks/things.py | 3 ++- src/tasks/videogame.py | 10 ---------- 7 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index 91511352..94e2dafd 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -141,7 +141,7 @@ def stop(self, exp_win, ctl_win): self.eyetracker.unset_pupil_cb() yield - def save(self): + def _save(self): if hasattr(self, 'all_pupils'): fname = self._generate_unique_filename("calib-data", ".npz") np.savez(fname, pupils=self.all_pupils, markers=self.all_refs_per_flip) diff --git a/src/tasks/images.py b/src/tasks/images.py index 3870eb40..8bfce8d2 100644 --- a/src/tasks/images.py +++ b/src/tasks/images.py @@ -59,8 +59,9 @@ def _run(self, exp_win, ctl_win): for frameN in range(config.FRAME_RATE * BASELINE_END): yield() - def save(self): + def _save(self): self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) + return False class BOLD5000Images(Images): pass diff --git a/src/tasks/language.py b/src/tasks/language.py index 7ba5338c..88147cd0 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -110,5 +110,6 @@ def _run(self, exp_win, ctl_win): for frameN in range(config.FRAME_RATE * BASELINE_END): yield() - def save(self): + def _save(self): self.trials.saveAsWideText(self._generate_unique_filename("events","tsv")) + return False diff --git a/src/tasks/speech.py b/src/tasks/speech.py index f5319f9e..eaa70a2e 100644 --- a/src/tasks/speech.py +++ b/src/tasks/speech.py @@ -61,5 +61,6 @@ def _run(self, exp_win, ctl_win): for frameN in range(config.FRAME_RATE * BASELINE_END): yield() - def save(self): + def _save(self): self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) + return False diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index 139d1b49..60198af9 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -23,6 +23,7 @@ def setup(self, exp_win, output_path, output_fname_base, use_fmri=False, use_eye self.use_fmri = use_fmri self.use_meg = use_meg self.use_eyetracking = use_eyetracking + self._events = [] self._setup(exp_win) # initialize a progress bar if we know the duration of the task self.progress_bar = tqdm.tqdm(total=self.duration) if hasattr(self, 'duration') else False @@ -94,9 +95,25 @@ def restart(self): if hasattr(self, '_restart'): self._restart() - def save(self): + def _log_event(self, event): + event.update({'onset': self.task_timer.getTime()}) + self._events.append(event) + + def _save(self): + # to be overriden + # return False if events need not be saved + # allow to override events saving if transformation are needed pass + def save(self): + # call custom task _save() + save_events = self._save() + if save_events is None and len(self._events): + fname = _generate_unique_filename("events", "tsv") + df = pandas.DataFrame(self._events) + df.to_csv(fname, sep='\t', index=False) + + class Pause(Task): def __init__(self, text="Taking a short break, relax...", **kwargs): diff --git a/src/tasks/things.py b/src/tasks/things.py index 4f7814d2..72d6ee00 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -95,5 +95,6 @@ def _run(self, exp_win, ctl_win): while self.task_timer.getTime() < trial['onset'] + RESPONSE_TIME: time.sleep(.0005) - def save(self): + def _save(self): self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) + return False diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 2c858a1e..8aa11434 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -153,7 +153,6 @@ def _setup(self, exp_win): record=False) super()._setup(exp_win) - self._events = [] self._set_recording_file() self._set_key_handler(exp_win) @@ -240,10 +239,6 @@ def _unset_key_handler(self, exp_win): exp_win.winHandle.on_key_press = event._onPygletKey #del exp_win.winHandle.on_key_release - def _log_event(self, event): - event.update({'onset':self.task_timer.getTime()}) - self._events.append(event) - def _run(self, exp_win, ctl_win): self._set_key_handler(exp_win) @@ -387,11 +382,6 @@ def _stop(self, exp_win, ctl_win): exp_win.waitBlanking = True yield from super()._stop(exp_win, ctl_win) - def save(self): - fname = _generate_unique_filename("events", "tsv") - df = pandas.DataFrame(self._events) - df.to_csv(fname, sep='\t', index=False) - class VideoGameMultiLevel(VideoGame): def __init__(self, *args,**kwargs): From 9ffbb032d8e8cf7c24029b768f78c25bd5837d76 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 11 Dec 2020 08:58:08 -0500 Subject: [PATCH 075/170] generate event file for calibration task, black --- src/shared/eyetracking.py | 327 +++++++++++++++++++++++--------------- 1 file changed, 197 insertions(+), 130 deletions(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index 94e2dafd..4203dfc1 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -13,14 +13,26 @@ INSTRUCTION_DURATION = 5 -CALIBRATE_HOTKEY = 'c' +CALIBRATE_HOTKEY = "c" INSTRUCTION_DURATION = 5 MARKER_SIZE = 50 -MARKER_FILL_COLOR = (.8,0,.5) +MARKER_FILL_COLOR = (0.8, 0, 0.5) MARKER_DURATION_FRAMES = 240 -MARKER_POSITIONS = np.asarray([(.25, .5), (0, .5), (0., 1.), (.5, 1.), (1., 1.), - (1., .5), (1., 0.), (.5, 0.), (0., 0.), (.75, .5)]) +MARKER_POSITIONS = np.asarray( + [ + (0.25, 0.5), + (0, 0.5), + (0.0, 1.0), + (0.5, 1.0), + (1.0, 1.0), + (1.0, 0.5), + (1.0, 0.0), + (0.5, 0.0), + (0.0, 0.0), + (0.75, 0.5), + ] +) # number of frames to eliminate at start and end of marker CALIBRATION_LEAD_IN = 20 @@ -29,22 +41,25 @@ # Pupil settings PUPIL_REMOTE_PORT = 50123 CAPTURE_SETTINGS = { - "frame_size": [640, 480], - "frame_rate": 250, - "exposure_time": 4000, - "global_gain": 1, - "gev_packet_size": 2000, - "uid": "Aravis-Fake-GV01", # for test purposes -# "uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", - } + "frame_size": [640, 480], + "frame_rate": 250, + "exposure_time": 4000, + "global_gain": 1, + "gev_packet_size": 2000, + #"uid": "Aravis-Fake-GV01", # for test purposes + "uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", +} -class EyetrackerCalibration(Task): - def __init__(self,eyetracker, - markers_order='random', +class EyetrackerCalibration(Task): + def __init__( + self, + eyetracker, + markers_order="random", marker_fill_color=MARKER_FILL_COLOR, markers=MARKER_POSITIONS, - **kwargs): + **kwargs, + ): self.use_eyetracking = True self.markers_order = markers_order self.markers = markers @@ -56,8 +71,12 @@ def _instructions(self, exp_win, ctl_win): instruction_text = """We're going to calibrate the eyetracker. Please look at the markers that appear on the screen.""" screen_text = visual.TextStim( - exp_win, text=instruction_text, - alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + exp_win, + text=instruction_text, + alignText="center", + color="white", + wrapWidth=config.WRAP_WIDTH, + ) for frameN in range(config.FRAME_RATE * INSTRUCTION_DURATION): screen_text.draw(exp_win) @@ -69,10 +88,10 @@ def _setup(self, exp_win): self._pupils_list = [] def _pupil_cb(self, pupil): - if pupil['timestamp'] > self.task_stop: + if pupil["timestamp"] > self.task_stop: self.eyetracker.unset_pupil_cb() return - if pupil['timestamp'] > self.task_start: + if pupil["timestamp"] > self.task_start: self._pupils_list.append(pupil) def _run(self, exp_win, ctl_win): @@ -85,56 +104,78 @@ def _run(self, exp_win, ctl_win): if start_calibration: break yield False - logging.info('calibration started') - print('calibration started') + logging.info("calibration started") + print("calibration started") - window_size_frame = exp_win.size-MARKER_SIZE*2 + window_size_frame = exp_win.size - MARKER_SIZE * 2 circle_marker = visual.Circle( - exp_win, edges=64, units='pixels', - lineColor=None,fillColor=self.marker_fill_color, - autoLog=False) + exp_win, + edges=64, + units="pixels", + lineColor=None, + fillColor=self.marker_fill_color, + autoLog=False, + ) markers_order = np.arange(len(self.markers)) - if self.markers_order == 'random': + if self.markers_order == "random": markers_order = np.random.permutation(markers_order) self.all_refs_per_flip = [] - radius_anim = np.hstack([np.linspace(MARKER_SIZE,0,MARKER_DURATION_FRAMES/2), - np.linspace(0,MARKER_SIZE,MARKER_DURATION_FRAMES/2)]) + radius_anim = np.hstack( + [ + np.linspace(MARKER_SIZE, 0, MARKER_DURATION_FRAMES / 2), + np.linspace(0, MARKER_SIZE, MARKER_DURATION_FRAMES / 2), + ] + ) self.task_start = time.monotonic() self.task_stop = np.inf self.eyetracker.set_pupil_cb(self._pupil_cb) - while not len(self._pupils_list): # wait until we get at least a pupil + while not len(self._pupils_list): # wait until we get at least a pupil yield False - exp_win.logOnFlip(level=logging.EXP, msg='eyetracker_calibration: starting at %f'%time.time()) + exp_win.logOnFlip( + level=logging.EXP, + msg="eyetracker_calibration: starting at %f" % time.time(), + ) for site_id in markers_order: marker_pos = self.markers[site_id] - pos = (marker_pos-.5)*window_size_frame + pos = (marker_pos - 0.5) * window_size_frame circle_marker.pos = pos - exp_win.logOnFlip(level=logging.EXP, - msg="calibrate_position,%d,%d,%d,%d"%(marker_pos[0],marker_pos[1], pos[0],pos[1])) - for f,r in enumerate(radius_anim): + exp_win.logOnFlip( + level=logging.EXP, + msg="calibrate_position,%d,%d,%d,%d" + % (marker_pos[0], marker_pos[1], pos[0], pos[1]), + ) + exp_win.callOnFlip( + self._log_event, {"marker_x": pos[0], "marker_y": pos[1]} + ) + for f, r in enumerate(radius_anim): circle_marker.radius = r circle_marker.draw(exp_win) circle_marker.draw(ctl_win) - if f > CALIBRATION_LEAD_IN and f < len(radius_anim)-CALIBRATION_LEAD_OUT: - screen_pos = (pos + exp_win.size/2) - norm_pos = screen_pos/exp_win.size + if ( + f > CALIBRATION_LEAD_IN + and f < len(radius_anim) - CALIBRATION_LEAD_OUT + ): + screen_pos = pos + exp_win.size / 2 + norm_pos = screen_pos / exp_win.size ref = { - 'norm_pos': norm_pos.tolist(), - 'screen_pos': screen_pos.tolist(), - 'timestamp': time.monotonic() # =pupil frame timestamp on same computer - } - self.all_refs_per_flip.append(ref) # accumulate all refs + "norm_pos": norm_pos.tolist(), + "screen_pos": screen_pos.tolist(), + "timestamp": time.monotonic(), # =pupil frame timestamp on same computer + } + self.all_refs_per_flip.append(ref) # accumulate all refs yield True yield True self.task_stop = time.monotonic() - logging.info(f"calibrating on {len(self._pupils_list)} pupils and {len(self.all_refs_per_flip)} markers") + logging.info( + f"calibrating on {len(self._pupils_list)} pupils and {len(self.all_refs_per_flip)} markers" + ) self.eyetracker.calibrate(self._pupils_list, self.all_refs_per_flip) def stop(self, exp_win, ctl_win): @@ -142,14 +183,16 @@ def stop(self, exp_win, ctl_win): yield def _save(self): - if hasattr(self, 'all_pupils'): + if hasattr(self, "all_pupils"): fname = self._generate_unique_filename("calib-data", ".npz") np.savez(fname, pupils=self.all_pupils, markers=self.all_refs_per_flip) + from subprocess import Popen from contextlib import contextmanager + @contextmanager def nonblocking(lock): locked = lock.acquire(False) @@ -159,8 +202,8 @@ def nonblocking(lock): if locked: lock.release() -class EyeTrackerClient(threading.Thread): +class EyeTrackerClient(threading.Thread): def __init__(self, output_path, output_fname_base, profile=False, debug=False): super(EyeTrackerClient, self).__init__() self.stoprequest = threading.Event() @@ -172,125 +215,143 @@ def __init__(self, output_path, output_fname_base, profile=False, debug=False): self.output_path = output_path self.output_fname_base = output_fname_base - self.record_dir = os.path.join(self.output_path, self.output_fname_base + '.pupil') + self.record_dir = os.path.join( + self.output_path, self.output_fname_base + ".pupil" + ) os.makedirs(self.record_dir, exist_ok=True) dev_opts = [] if debug: - dev_opts.append('--debug') + dev_opts.append("--debug") if profile: - dev_opts.append('--profile') - - self._pupil_process = Popen([ - 'python3', - os.path.join(os.environ['PUPIL_PATH'],'pupil_src','main.py'), - 'capture', - '--port', str(PUPIL_REMOTE_PORT), - ] + dev_opts - ) + dev_opts.append("--profile") + + self._pupil_process = Popen( + [ + "python3", + os.path.join(os.environ["PUPIL_PATH"], "pupil_src", "main.py"), + "capture", + "--port", + str(PUPIL_REMOTE_PORT), + ] + + dev_opts + ) self._ctx = zmq.Context() self._req_socket = self._ctx.socket(zmq.REQ) self._req_socket.connect(f"tcp://localhost:{PUPIL_REMOTE_PORT}") # stop eye1 if started: monocular eyetracking in the MRI - notif = self.send_recv_notification({ - 'subject':'eye_process.should_stop.1', - 'eye_id':1, 'args':{}}) + notif = self.send_recv_notification( + {"subject": "eye_process.should_stop.1", "eye_id": 1, "args": {}} + ) # start eye0 if not started yet (from pupil saved config) - notif = self.send_recv_notification({ - 'subject':'eye_process.should_start.0', - 'eye_id':0, 'args':{}}) + notif = self.send_recv_notification( + {"subject": "eye_process.should_start.0", "eye_id": 0, "args": {}} + ) # wait for eye process to start before starting plugins time.sleep(1) # quit existing recorder plugin - self.send_recv_notification({ - 'subject':'stop_plugin', - 'name':'Recorder', - }) + self.send_recv_notification( + { + "subject": "stop_plugin", + "name": "Recorder", + } + ) # restart recorder plugin with custom output settings - self.send_recv_notification({ - 'subject':'start_plugin', - 'name':'Recorder','args':{ - 'rec_root_dir':self.record_dir, - 'session_name':self.output_fname_base + '.pupil', - 'raw_jpeg':False, - 'record_eye':True, - } - }) + self.send_recv_notification( + { + "subject": "start_plugin", + "name": "Recorder", + "args": { + "rec_root_dir": self.record_dir, + "session_name": self.output_fname_base + ".pupil", + "raw_jpeg": False, + "record_eye": True, + }, + } + ) # stop a bunch of eye plugins for performance - for plugin in ['NDSI_Manager', 'Detector3DPlugin']: - self.send_recv_notification({ - 'subject':'stop_eye_plugin', - 'target':'eye0', - 'name': plugin, - }) - - self.send_recv_notification({ - 'subject': 'start_eye_plugin', - 'name': 'Aravis_Source', - 'target': 'eye0', - 'args' : CAPTURE_SETTINGS, - }) + for plugin in ["NDSI_Manager", "Detector3DPlugin"]: + self.send_recv_notification( + { + "subject": "stop_eye_plugin", + "target": "eye0", + "name": plugin, + } + ) + + self.send_recv_notification( + { + "subject": "start_eye_plugin", + "name": "Aravis_Source", + "target": "eye0", + "args": CAPTURE_SETTINGS, + } + ) def send_recv_notification(self, n): # REQ REP requires lock step communication with multipart msg (topic,msgpack_encoded dict) - self._req_socket.send_multipart((bytes('notify.%s'%n['subject'],'utf-8'), msgpack.dumps(n))) + self._req_socket.send_multipart( + (bytes("notify.%s" % n["subject"], "utf-8"), msgpack.dumps(n)) + ) return self._req_socket.recv() def get_pupil_timestamp(self): - self._req_socket.send('t') #see Pupil Remote Plugin for details + self._req_socket.send("t") # see Pupil Remote Plugin for details return float(self._req_socket.recv()) def start_recording(self, recording_name): - logging.info('starting eyetracking recording') - return self.send_recv_notification({ - 'subject':'recording.should_start', - 'session_name': recording_name}) + logging.info("starting eyetracking recording") + return self.send_recv_notification( + {"subject": "recording.should_start", "session_name": recording_name} + ) def stop_recording(self): - logging.info('stopping eyetracking recording') - return self.send_recv_notification({ - 'subject':'recording.should_stop'}) + logging.info("stopping eyetracking recording") + return self.send_recv_notification({"subject": "recording.should_stop"}) def join(self, timeout=None): self.stoprequest.set() # stop recording - self.send_recv_notification({'subject':'recording.should_stop',}) + self.send_recv_notification( + { + "subject": "recording.should_stop", + } + ) # stop world and children process - self.send_recv_notification({'subject':'world_process.should_stop'}) - self.send_recv_notification({'subject':'launcher_process.should_stop'}) + self.send_recv_notification({"subject": "world_process.should_stop"}) + self.send_recv_notification({"subject": "launcher_process.should_stop"}) self._pupil_process.wait(timeout) self._pupil_process.terminate() - time.sleep(1/60.) + time.sleep(1 / 60.0) super(EyeTrackerClient, self).join(timeout) def run(self): - self._req_socket.send_string('SUB_PORT') + self._req_socket.send_string("SUB_PORT") ipc_sub_port = int(self._req_socket.recv()) logging.info(f"ipc_sub_port: {ipc_sub_port}") self.pupil_monitor = Msg_Receiver( - self._ctx, - f"tcp://localhost:{ipc_sub_port}", - topics=('gaze','pupil')) + self._ctx, f"tcp://localhost:{ipc_sub_port}", topics=("gaze", "pupil") + ) while not self.stoprequest.isSet(): msg = self.pupil_monitor.recv() if not msg is None: topic, tmp = msg with self.lock: - if topic.startswith('pupil'): + if topic.startswith("pupil"): self.pupils.append(tmp) if self._pupil_cb: self._pupil_cb(tmp) - elif topic.startswith('gaze'): + elif topic.startswith("gaze"): self.gazes.append(tmp) - time.sleep(1/120.) - logging.info('eyetracker listener: stopping') + time.sleep(1 / 120.0) + logging.info("eyetracker listener: stopping") def set_pupil_cb(self, pupil_cb): self._pupil_cb = pupil_cb @@ -310,42 +371,48 @@ def get_gaze(self): def calibrate(self, pupil_list, ref_list): if len(pupil_list) < 100: - logging.error('Calibration: not enough pupil captured for calibration') - #return + logging.error("Calibration: not enough pupil captured for calibration") + # return calib_data = {"ref_list": ref_list, "pupil_list": pupil_list} - logging.info('sending calibration data to pupil') - self.send_recv_notification({ - 'subject':'start_plugin', - 'name':'Gazer2D', - 'args': {'calib_data':calib_data}, - 'raise_calibration_error':True, - }) + logging.info("sending calibration data to pupil") + self.send_recv_notification( + { + "subject": "start_plugin", + "name": "Gazer2D", + "args": {"calib_data": calib_data}, + "raise_calibration_error": True, + } + ) -class GazeDrawer(): +class GazeDrawer: def __init__(self, win): self.win = win self._gazepoint_stim = visual.Circle( self.win, radius=30, - units='pixels', - lineColor=(1,0,0),fillColor=None, lineWidth=2, - autoLog=False) + units="pixels", + lineColor=(1, 0, 0), + fillColor=None, + lineWidth=2, + autoLog=False, + ) def draw_gazepoint(self, gaze): - pos = gaze['norm_pos'] - self._gazepoint_stim.pos = (int(pos[0]/2*self.win.size[0]), - int(pos[1]/2*self.win.size[1])) - #self._gazepoint_stim.radius = self.pupils['diameter']/2 - #print(self._gazepoint_stim.pos, self._gazepoint_stim.radius) + pos = gaze["norm_pos"] + self._gazepoint_stim.pos = ( + int(pos[0] / 2 * self.win.size[0]), + int(pos[1] / 2 * self.win.size[1]), + ) + # self._gazepoint_stim.radius = self.pupils['diameter']/2 + # print(self._gazepoint_stim.pos, self._gazepoint_stim.radius) self._gazepoint_stim.draw(self.win) - def read_pl_data(fname): with open(fname, "rb") as fh: for data in msgpack.Unpacker(fh, raw=False, use_list=False): - yield(data) + yield (data) From 3de441a8ce00516644591d676f245f801738ebf4 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 11 Dec 2020 09:32:46 -0500 Subject: [PATCH 076/170] fix event logging --- src/shared/eyetracking.py | 4 ++-- src/tasks/task_base.py | 3 ++- src/tasks/videogame.py | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index 4203dfc1..acd57853 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -46,8 +46,8 @@ "exposure_time": 4000, "global_gain": 1, "gev_packet_size": 2000, - #"uid": "Aravis-Fake-GV01", # for test purposes - "uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", + "uid": "Aravis-Fake-GV01", # for test purposes + #"uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", } diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index 60198af9..bc846489 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -1,5 +1,6 @@ import os import tqdm +import pandas from psychopy import logging, visual, core, event from ..shared import fmri, meg, config @@ -109,7 +110,7 @@ def save(self): # call custom task _save() save_events = self._save() if save_events is None and len(self._events): - fname = _generate_unique_filename("events", "tsv") + fname = self._generate_unique_filename("events", "tsv") df = pandas.DataFrame(self._events) df.to_csv(fname, sep='\t', index=False) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 8aa11434..a499011a 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -1,6 +1,5 @@ import os, sys, time, queue import numpy as np -import pandas import threading from psychopy import visual, core, data, logging, event, sound, constants From 0f309b2ac898c1b34bd6d55766950f1b9446b808 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 11 Dec 2020 09:33:54 -0500 Subject: [PATCH 077/170] apply black --- src/sessions/ses-bourne_supremacy1.py | 13 +- src/sessions/ses-bourne_supremacy2.py | 10 +- src/sessions/ses-friends-s1.py | 12 +- src/sessions/ses-friends-s2.py | 12 +- src/sessions/ses-friends-s3.py | 12 +- src/sessions/ses-friends-s4.py | 12 +- src/sessions/ses-hiddenfigures1.py | 10 +- src/sessions/ses-hiddenfigures2.py | 10 +- src/sessions/ses-life1.py | 10 +- src/sessions/ses-liris.py | 14 +- src/sessions/ses-megmotion.py | 28 +- src/sessions/ses-motion.py | 23 +- src/sessions/ses-shinobi.py | 17 +- src/sessions/ses-shinobi3levels.py | 40 +-- src/sessions/ses-shinobi_3levels.py | 39 +-- src/sessions/ses-synctest.py | 4 +- src/sessions/ses-test64.py | 11 +- src/sessions/ses-things.py | 119 +++++---- src/sessions/ses-triplet_test.py | 24 +- src/sessions/ses-video1.py | 26 +- src/sessions/ses-video2.py | 30 ++- src/sessions/ses-video3.py | 30 ++- src/sessions/ses-video3b.py | 30 ++- src/sessions/ses-video_images_pilot.py | 29 ++- src/sessions/ses-video_pilot.py | 11 +- src/sessions/ses-videogame_test.py | 17 +- src/sessions/ses-videoshorttest.py | 6 +- src/sessions/ses-wolfwallstreet1.py | 11 +- src/sessions/ses-wolfwallstreet2.py | 11 +- src/sessions/ses-wolfwallstreet3.py | 11 +- src/shared/__init__.py | 3 +- src/shared/cli.py | 184 ++++++++----- src/shared/config.py | 28 +- src/shared/ellipse.py | 24 +- src/shared/eyetracking.py | 2 +- src/shared/fmri.py | 22 +- src/shared/meg.py | 7 +- src/shared/parser.py | 80 +++--- src/shared/ptt.py | 19 +- src/shared/screen.py | 32 ++- src/shared/utils.py | 1 + src/tasks/images.py | 58 +++-- src/tasks/language.py | 93 ++++--- src/tasks/memory.py | 63 ++--- src/tasks/speech.py | 53 ++-- src/tasks/task_base.py | 80 ++++-- src/tasks/things.py | 113 +++++--- src/tasks/video.py | 56 ++-- src/tasks/videogame.py | 344 +++++++++++++++---------- 49 files changed, 1072 insertions(+), 822 deletions(-) diff --git a/src/sessions/ses-bourne_supremacy1.py b/src/sessions/ses-bourne_supremacy1.py index a2a7f1f5..c3e772f6 100644 --- a/src/sessions/ses-bourne_supremacy1.py +++ b/src/sessions/ses-bourne_supremacy1.py @@ -1,10 +1,11 @@ from ..tasks import images, video, memory, task_base -TASKS = [ -] -for seg_idx in range(1,6): +TASKS = [] +for seg_idx in range(1, 6): TASKS.append( video.SingleVideo( - 'data/videos/bourne_supremacy/bourne_supremacy_seg%02d.mkv'%seg_idx, - aspect_ratio = 372/157, - name='bourne_supremacy_seg-%d'%seg_idx)) + "data/videos/bourne_supremacy/bourne_supremacy_seg%02d.mkv" % seg_idx, + aspect_ratio=372 / 157, + name="bourne_supremacy_seg-%d" % seg_idx, + ) + ) diff --git a/src/sessions/ses-bourne_supremacy2.py b/src/sessions/ses-bourne_supremacy2.py index b71af830..34ea7a2a 100644 --- a/src/sessions/ses-bourne_supremacy2.py +++ b/src/sessions/ses-bourne_supremacy2.py @@ -1,9 +1,11 @@ from ..tasks import images, video, memory, task_base TASKS = [] -for seg_idx in range(6,11): +for seg_idx in range(6, 11): TASKS.append( video.SingleVideo( - 'data/videos/bourne_supremacy/bourne_supremacy_seg%02d.mkv'%seg_idx, - aspect_ratio = 372/157, - name='bourne_supremacy_seg-%d'%seg_idx)) + "data/videos/bourne_supremacy/bourne_supremacy_seg%02d.mkv" % seg_idx, + aspect_ratio=372 / 157, + name="bourne_supremacy_seg-%d" % seg_idx, + ) + ) diff --git a/src/sessions/ses-friends-s1.py b/src/sessions/ses-friends-s1.py index 9091a9b5..5ae6dd41 100644 --- a/src/sessions/ses-friends-s1.py +++ b/src/sessions/ses-friends-s1.py @@ -2,10 +2,12 @@ TASKS = [] -for episode in range(1,25): - for segment in 'ab': +for episode in range(1, 25): + for segment in "ab": TASKS.append( video.SingleVideo( - 'data/videos/friends/s1/friends_s1e%02d%s.mkv'%(episode, segment), - aspect_ratio = 4/3., - name='task-friends-s1e%d%s'%(episode, segment))) + "data/videos/friends/s1/friends_s1e%02d%s.mkv" % (episode, segment), + aspect_ratio=4 / 3.0, + name="task-friends-s1e%d%s" % (episode, segment), + ) + ) diff --git a/src/sessions/ses-friends-s2.py b/src/sessions/ses-friends-s2.py index 0a74b03d..7245021b 100644 --- a/src/sessions/ses-friends-s2.py +++ b/src/sessions/ses-friends-s2.py @@ -2,10 +2,12 @@ TASKS = [] -for episode in range(1,25): - for segment in 'ab': +for episode in range(1, 25): + for segment in "ab": TASKS.append( video.SingleVideo( - 'data/videos/friends/s2/friends_s2e%02d%s.mkv'%(episode, segment), - aspect_ratio = 4/3., - name='task-friends-s2e%d%s'%(episode, segment))) + "data/videos/friends/s2/friends_s2e%02d%s.mkv" % (episode, segment), + aspect_ratio=4 / 3.0, + name="task-friends-s2e%d%s" % (episode, segment), + ) + ) diff --git a/src/sessions/ses-friends-s3.py b/src/sessions/ses-friends-s3.py index 1bdc1c59..d783c79a 100644 --- a/src/sessions/ses-friends-s3.py +++ b/src/sessions/ses-friends-s3.py @@ -2,10 +2,12 @@ TASKS = [] -for episode in range(1,26): - for segment in 'ab': +for episode in range(1, 26): + for segment in "ab": TASKS.append( video.SingleVideo( - 'data/videos/friends/s3/friends_s3e%02d%s.mkv'%(episode, segment), - aspect_ratio = 4/3., - name='task-friends-s3e%d%s'%(episode, segment))) + "data/videos/friends/s3/friends_s3e%02d%s.mkv" % (episode, segment), + aspect_ratio=4 / 3.0, + name="task-friends-s3e%d%s" % (episode, segment), + ) + ) diff --git a/src/sessions/ses-friends-s4.py b/src/sessions/ses-friends-s4.py index 7e8fded0..68b204d8 100644 --- a/src/sessions/ses-friends-s4.py +++ b/src/sessions/ses-friends-s4.py @@ -2,13 +2,15 @@ TASKS = [] -for episode in range(1,23): - for segment in 'ab': +for episode in range(1, 23): + for segment in "ab": TASKS.append( video.SingleVideo( - 'data/videos/friends/s4/friends_s4e%02d%s.mkv'%(episode, segment), - aspect_ratio = 4/3., - name='task-friends-s4e%d%s'%(episode, segment))) + "data/videos/friends/s4/friends_s4e%02d%s.mkv" % (episode, segment), + aspect_ratio=4 / 3.0, + name="task-friends-s4e%d%s" % (episode, segment), + ) + ) """ for segment in 'abcd': TASKS.append( diff --git a/src/sessions/ses-hiddenfigures1.py b/src/sessions/ses-hiddenfigures1.py index f5b76d13..ecc5a082 100644 --- a/src/sessions/ses-hiddenfigures1.py +++ b/src/sessions/ses-hiddenfigures1.py @@ -1,11 +1,13 @@ from ..tasks import images, video, memory, task_base TASKS = [] -for seg_idx in range(1,7): +for seg_idx in range(1, 7): TASKS.append( video.SingleVideo( - 'data/videos/hidden_figures/hidden_figures_seg%02d.mkv'%seg_idx, - aspect_ratio = 12/5, - name='hidden_figures_seg-%d'%seg_idx)) + "data/videos/hidden_figures/hidden_figures_seg%02d.mkv" % seg_idx, + aspect_ratio=12 / 5, + name="hidden_figures_seg-%d" % seg_idx, + ) + ) # 410 volumes each diff --git a/src/sessions/ses-hiddenfigures2.py b/src/sessions/ses-hiddenfigures2.py index 12420fc4..2886063e 100644 --- a/src/sessions/ses-hiddenfigures2.py +++ b/src/sessions/ses-hiddenfigures2.py @@ -1,11 +1,13 @@ from ..tasks import images, video, memory, task_base TASKS = [] -for seg_idx in range(7,13): +for seg_idx in range(7, 13): TASKS.append( video.SingleVideo( - 'data/videos/hidden_figures/hidden_figures_seg%02d.mkv'%seg_idx, - aspect_ratio = 12/5, - name='hidden_figures_seg-%d'%seg_idx)) + "data/videos/hidden_figures/hidden_figures_seg%02d.mkv" % seg_idx, + aspect_ratio=12 / 5, + name="hidden_figures_seg-%d" % seg_idx, + ) + ) # 410 volumes each, last run is 375 volumes long diff --git a/src/sessions/ses-life1.py b/src/sessions/ses-life1.py index ba8f2df8..b3a5f4f6 100644 --- a/src/sessions/ses-life1.py +++ b/src/sessions/ses-life1.py @@ -1,11 +1,13 @@ from ..tasks import images, video, memory, task_base TASKS = [] -for seg_idx in range(1,6): +for seg_idx in range(1, 6): TASKS.append( video.SingleVideo( - 'data/videos/life1/life1_seg%02d.mkv'%seg_idx, - aspect_ratio = 12/5, - name='life1_seg-%d'%seg_idx)) + "data/videos/life1/life1_seg%02d.mkv" % seg_idx, + aspect_ratio=12 / 5, + name="life1_seg-%d" % seg_idx, + ) + ) # 410 volumes each, last run is 392 volumes long diff --git a/src/sessions/ses-liris.py b/src/sessions/ses-liris.py index 4f275344..03031b7f 100644 --- a/src/sessions/ses-liris.py +++ b/src/sessions/ses-liris.py @@ -1,16 +1,15 @@ from ..tasks import video, task_base import numpy as np + def get_videos(subject, session): video_idx = np.loadtxt( - 'data/liris/order_fmri_neuromod.csv', - delimiter=',', - skiprows=1, - dtype=np.int + "data/liris/order_fmri_neuromod.csv", delimiter=",", skiprows=1, dtype=np.int ) - selected_idx = video_idx[video_idx[:,0]==session, subject+1] + selected_idx = video_idx[video_idx[:, 0] == session, subject + 1] return selected_idx + def get_tasks(parsed): tasks = [] @@ -20,13 +19,12 @@ def get_tasks(parsed): for idx in video_indices: tasks.append( video.SingleVideo( - f"data/liris/videos/{idx:03d}.mp4", - name=f"task-liris{idx:03d}" + f"data/liris/videos/{idx:03d}.mp4", name=f"task-liris{idx:03d}" ) ) tasks.append( task_base.Pause( - """The video is finished. + """The video is finished. The scanner might run for a few seconds to acquire more images. Please remain still.""" ) diff --git a/src/sessions/ses-megmotion.py b/src/sessions/ses-megmotion.py index 32acfdbd..d26a73f6 100644 --- a/src/sessions/ses-megmotion.py +++ b/src/sessions/ses-megmotion.py @@ -1,26 +1,28 @@ from ..tasks import images, video, memory, task_base TASKS = [ - - task_base.Fixation(duration=7*60, name='resting_state'), + task_base.Fixation(duration=7 * 60, name="resting_state"), task_base.Pause(), video.SingleVideo( - 'data/videos/movies_for_montreal/03_Inscaped_NoScannerSound_h264.mov', - name='Inscapes'), + "data/videos/movies_for_montreal/03_Inscaped_NoScannerSound_h264.mov", + name="Inscapes", + ), task_base.Pause(), video.SingleVideo( - 'data/videos/movies_for_montreal/Oceans_10m_fs.mp4', - name='Oceans'), + "data/videos/movies_for_montreal/Oceans_10m_fs.mp4", name="Oceans" + ), task_base.Pause(), - task_base.Fixation(duration=7*60, name='resting_state'), + task_base.Fixation(duration=7 * 60, name="resting_state"), task_base.Pause(), video.SingleVideo( - 'data/videos/movies_for_montreal/03_Inscaped_NoScannerSound_h264.mov', - scaling=.5, - name='Inscapes_scaled'), + "data/videos/movies_for_montreal/03_Inscaped_NoScannerSound_h264.mov", + scaling=0.5, + name="Inscapes_scaled", + ), task_base.Pause(), video.SingleVideo( - 'data/videos/movies_for_montreal/Oceans_10m_fs.mp4', - scaling=.5, - name='Oceans_scaled') + "data/videos/movies_for_montreal/Oceans_10m_fs.mp4", + scaling=0.5, + name="Oceans_scaled", + ), ] diff --git a/src/sessions/ses-motion.py b/src/sessions/ses-motion.py index a4e501a5..b47c9090 100644 --- a/src/sessions/ses-motion.py +++ b/src/sessions/ses-motion.py @@ -1,18 +1,19 @@ from ..tasks import images, video, memory, task_base TASKS = [ - task_base.Pause("""Hi! We are about to start the MRI session. + task_base.Pause( + """Hi! We are about to start the MRI session. Make yourself comfortable. Ensure that you can see the full screen and that the image is sharp. -Please keep your eyes open."""), - speech.Speech('data/speech/motion_study_speech_words.csv', name='Speech'), - video.SingleVideo( - 'data/videos/Inscapes-67962604.mp4', name='Inscapes'), +Please keep your eyes open.""" + ), + speech.Speech("data/speech/motion_study_speech_words.csv", name="Speech"), + video.SingleVideo("data/videos/Inscapes-67962604.mp4", name="Inscapes"), # source: https://drive.google.com/file/d/1prOM1QuPEAcqe_D-3rLYNu937A8VzbsB/view - video.SingleVideo( - 'data/videos/tammy/Oceans_1.mp4', - name='Oceans_chunk1'), - videogame.VideoGame(state_name='Level1',name='ShinobiIIIReturnOfTheNinjaMaster-test'), - ] + video.SingleVideo("data/videos/tammy/Oceans_1.mp4", name="Oceans_chunk1"), + videogame.VideoGame( + state_name="Level1", name="ShinobiIIIReturnOfTheNinjaMaster-test" + ), +] - # TODO: randomize the order of the tasks? +# TODO: randomize the order of the tasks? diff --git a/src/sessions/ses-shinobi.py b/src/sessions/ses-shinobi.py index ce5722e9..a97d9a65 100644 --- a/src/sessions/ses-shinobi.py +++ b/src/sessions/ses-shinobi.py @@ -1,13 +1,12 @@ - from ..tasks import images, videogame, memory, task_base TASKS = [ - videogame.VideoGame( - state_name='Level1', - scenario='scenario_repeat1', # this scenario repeats the same level - max_duration=10*60, # if when level completed or dead we exceed that time in secs, stop the task - name='ShinobiIIIReturnOfTheNinjaMaster-level1'), - #videogame.VideoGameReplay('output/sub-test/ses-test_refactor/sub-test_ses-test_refactor_20190109_154007_ShinobiIIIReturnOfTheNinjaMaster-Genesis_Level4_000.bk2',name='ShinobiIIIReturnOfTheNinjaMaster-replay',), - -] * 3 ###!!!!!!!!!!! REPEAT !!!!!!!!!!!### + state_name="Level1", + scenario="scenario_repeat1", # this scenario repeats the same level + max_duration=10 + * 60, # if when level completed or dead we exceed that time in secs, stop the task + name="ShinobiIIIReturnOfTheNinjaMaster-level1", + ), + # videogame.VideoGameReplay('output/sub-test/ses-test_refactor/sub-test_ses-test_refactor_20190109_154007_ShinobiIIIReturnOfTheNinjaMaster-Genesis_Level4_000.bk2',name='ShinobiIIIReturnOfTheNinjaMaster-replay',), +] * 3 ###!!!!!!!!!!! REPEAT !!!!!!!!!!!### diff --git a/src/sessions/ses-shinobi3levels.py b/src/sessions/ses-shinobi3levels.py index 708bcf88..6d1dd0c5 100644 --- a/src/sessions/ses-shinobi3levels.py +++ b/src/sessions/ses-shinobi3levels.py @@ -12,25 +12,29 @@ "The right thoughts/movements occur of their own accord.", "I know what I have to do each step of the way.", "I feel that I have everything under control.", - "I am completely lost in thought." + "I am completely lost in thought.", ] -levels = ['scenario_repeat1', 'scenario_Level4-1', 'scenario_Level5-0'] -random.shuffle(levels) # randomize order - -TASKS = sum([ +levels = ["scenario_repeat1", "scenario_Level4-1", "scenario_Level5-0"] +random.shuffle(levels) # randomize order +TASKS = sum( [ - videogame.VideoGameMultiLevel( - state_names=['Level1-0','Level4-1','Level5-0'], - scenarii=['data/videogames/%s.json'%sc for sc in levels], # this scenario repeats the same level - repeat_scenario=True, - max_duration=10*60, # if when level completed or dead we exceed that time in secs, stop the task - name=f"task-shinobi_run-{run+1:02d}", - #post_level_ratings = [(q, 7) for q in flow_ratings] - ), - task_base.Pause() - ] - for run in range(5) - -],[]) + [ + videogame.VideoGameMultiLevel( + state_names=["Level1-0", "Level4-1", "Level5-0"], + scenarii=[ + "data/videogames/%s.json" % sc for sc in levels + ], # this scenario repeats the same level + repeat_scenario=True, + max_duration=10 + * 60, # if when level completed or dead we exceed that time in secs, stop the task + name=f"task-shinobi_run-{run+1:02d}", + # post_level_ratings = [(q, 7) for q in flow_ratings] + ), + task_base.Pause(), + ] + for run in range(5) + ], + [], +) diff --git a/src/sessions/ses-shinobi_3levels.py b/src/sessions/ses-shinobi_3levels.py index d6960b22..b674f3b6 100644 --- a/src/sessions/ses-shinobi_3levels.py +++ b/src/sessions/ses-shinobi_3levels.py @@ -1,25 +1,26 @@ - from ..tasks import images, videogame, memory, task_base TASKS = [ - videogame.VideoGame( - state_name='Level1', - scenario='scenario_repeat1', # this scenario repeats the same level - max_duration=10*60, # if when level completed or dead we exceed that time in secs, stop the task - name='ShinobiIIIReturnOfTheNinjaMaster-level1'), - #videogame.VideoGameReplay('output/sub-test/ses-test_refactor/sub-test_ses-test_refactor_20190109_154007_ShinobiIIIReturnOfTheNinjaMaster-Genesis_Level4_000.bk2',name='ShinobiIIIReturnOfTheNinjaMaster-replay',), - + state_name="Level1", + scenario="scenario_repeat1", # this scenario repeats the same level + max_duration=10 + * 60, # if when level completed or dead we exceed that time in secs, stop the task + name="ShinobiIIIReturnOfTheNinjaMaster-level1", + ), + # videogame.VideoGameReplay('output/sub-test/ses-test_refactor/sub-test_ses-test_refactor_20190109_154007_ShinobiIIIReturnOfTheNinjaMaster-Genesis_Level4_000.bk2',name='ShinobiIIIReturnOfTheNinjaMaster-replay',), videogame.VideoGame( - state_name='Level4-1', - scenario='scenario_Level4-1', # this scenario repeats the same level - max_duration=10*60, # if when level completed or dead we exceed that time in secs, stop the task - name='ShinobiIIIReturnOfTheNinjaMaster-level4-1'), - + state_name="Level4-1", + scenario="scenario_Level4-1", # this scenario repeats the same level + max_duration=10 + * 60, # if when level completed or dead we exceed that time in secs, stop the task + name="ShinobiIIIReturnOfTheNinjaMaster-level4-1", + ), videogame.VideoGame( - state_name='Level5-0', - scenario='scenario_Level5-0', # this scenario repeats the same level - max_duration=10*60, # if when level completed or dead we exceed that time in secs, stop the task - name='ShinobiIIIReturnOfTheNinjaMaster-level5-0'), - -] * 3 ###!!!!!!!!!!! REPEAT !!!!!!!!!!!### + state_name="Level5-0", + scenario="scenario_Level5-0", # this scenario repeats the same level + max_duration=10 + * 60, # if when level completed or dead we exceed that time in secs, stop the task + name="ShinobiIIIReturnOfTheNinjaMaster-level5-0", + ), +] * 3 ###!!!!!!!!!!! REPEAT !!!!!!!!!!!### diff --git a/src/sessions/ses-synctest.py b/src/sessions/ses-synctest.py index 34cb53de..644fa0c1 100644 --- a/src/sessions/ses-synctest.py +++ b/src/sessions/ses-synctest.py @@ -1,7 +1,5 @@ from ..tasks import video TASKS = [ -video.SingleVideo( - 'data/videos/Sync-Footage-V1-H264.mp4', - name='task-synctest') + video.SingleVideo("data/videos/Sync-Footage-V1-H264.mp4", name="task-synctest") ] * 10 diff --git a/src/sessions/ses-test64.py b/src/sessions/ses-test64.py index e8d0344f..a1b0ce3a 100644 --- a/src/sessions/ses-test64.py +++ b/src/sessions/ses-test64.py @@ -1,12 +1,11 @@ from ..tasks import images, video, memory, task_base TASKS = [ - video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), - + "data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4", name="Inscapes" + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_1_filt.mp4', - name='Oceans_fs_10m_1'), - + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_1_filt.mp4", + name="Oceans_fs_10m_1", + ), ] diff --git a/src/sessions/ses-things.py b/src/sessions/ses-things.py index 0bbad4c9..eb65cdc3 100644 --- a/src/sessions/ses-things.py +++ b/src/sessions/ses-things.py @@ -1,67 +1,70 @@ import os -THINGS_DATA_PATH = os.path.join('data','things') -IMAGE_PATH = os.path.join(THINGS_DATA_PATH,'images') +THINGS_DATA_PATH = os.path.join("data", "things") +IMAGE_PATH = os.path.join(THINGS_DATA_PATH, "images") + def get_tasks(parsed): from ..tasks.things import Things session_design_filename = os.path.join( - THINGS_DATA_PATH, 'designs', - f"sub-{parsed.subject}_ses-{parsed.session}_design.tsv") + THINGS_DATA_PATH, + "designs", + f"sub-{parsed.subject}_ses-{parsed.session}_design.tsv", + ) tasks = [ - Things( - session_design_filename, - IMAGE_PATH, - run, - name=f"task-things_run-{run}") \ - for run in range(1,n_runs+1) + Things(session_design_filename, IMAGE_PATH, run, name=f"task-things_run-{run}") + for run in range(1, n_runs + 1) ] return tasks - - - - # experiment -n_sessions = 12 # number of sessions -n_runs = 10 # number of runs -n_trials_exp = 72 # number of trials for each run -n_trials_catch = 10 # catch trials where response is required -n_trials_test = 10 # for test data, separate images -n_trials_total = n_trials_exp + n_trials_test + n_trials_catch -final_wait = 9 # time to wait after last trial -initial_wait = 3 # time until first trial starts +n_sessions = 12 # number of sessions +n_runs = 10 # number of runs +n_trials_exp = 72 # number of trials for each run +n_trials_catch = 10 # catch trials where response is required +n_trials_test = 10 # for test data, separate images +n_trials_total = n_trials_exp + n_trials_test + n_trials_catch +final_wait = 9 # time to wait after last trial +initial_wait = 3 # time until first trial starts # trial -trial_duration = 4.5 # mean trial duration -jitters = 0 # chosen to be a range that minimizes phase synchrony and that can be presented exactly on most screens -image_duration = 0.5 # duration of image presentation -rm_duration = 4.0 # duration of response mapping screen -max_rt = 4.0 # from stimulus onset +trial_duration = 4.5 # mean trial duration +jitters = 0 # chosen to be a range that minimizes phase synchrony and that can be presented exactly on most screens +image_duration = 0.5 # duration of image presentation +rm_duration = 4.0 # duration of response mapping screen +max_rt = 4.0 # from stimulus onset # constraints min_catch_spacing = 3 max_catch_spacing = 20 + def generate_design_file(subject, session): import pandas import numpy as np import random import hashlib - images_list = pandas.read_csv(os.path.join(THINGS_DATA_PATH, 'image_paths_fmri.csv')) - images_exp = images_list[images_list.condition.eq('exp') & images_list.exemplar_nr.eq(int(session))].sample(frac=1) - images_catch = images_list[images_list.condition.eq('catch')].sample(frac=1) - images_test = images_list[images_list.condition.eq('test')].sample(frac=1) + images_list = pandas.read_csv( + os.path.join(THINGS_DATA_PATH, "image_paths_fmri.csv") + ) + + images_exp = images_list[ + images_list.condition.eq("exp") & images_list.exemplar_nr.eq(int(session)) + ].sample(frac=1) + images_catch = images_list[images_list.condition.eq("catch")].sample(frac=1) + images_test = images_list[images_list.condition.eq("test")].sample(frac=1) design = pandas.DataFrame() - print("%s-%s"%(subject, session)) - seed = int(hashlib.sha1(("%s-%s"%(subject, session)).encode('utf-8')).hexdigest(), 16) % (2**32 - 1) - print('seed', seed) + print("%s-%s" % (subject, session)) + seed = int( + hashlib.sha1(("%s-%s" % (subject, session)).encode("utf-8")).hexdigest(), 16 + ) % (2 ** 32 - 1) + print("seed", seed) np.random.seed(seed) all_run_trials = pandas.DataFrame() @@ -69,42 +72,46 @@ def generate_design_file(subject, session): for run in range(n_runs): niter = 0 while True: - niter+=1 + niter += 1 randorder = np.random.permutation(n_trials_total) n_noncatch_trial = n_trials_exp + n_trials_test catch_indices = np.where(randorder >= n_noncatch_trial)[0] catch_indices_bounds = np.hstack([[0], catch_indices, [n_trials_total]]) catch_spacings = np.diff(catch_indices_bounds) - if np.all(catch_spacings>min_catch_spacing) and \ - np.all(catch_spacings min_catch_spacing) and np.all( + catch_spacings < max_catch_spacing + ): break print(subject, session, run, niter) - run_trials = pandas.concat([ - images_exp[run*n_trials_exp:(run+1)*n_trials_exp], - images_test[run*n_trials_test:(run+1)*n_trials_test], - images_catch[run*n_trials_catch:(run+1)*n_trials_catch] - ]) + run_trials = pandas.concat( + [ + images_exp[run * n_trials_exp : (run + 1) * n_trials_exp], + images_test[run * n_trials_test : (run + 1) * n_trials_test], + images_catch[run * n_trials_catch : (run + 1) * n_trials_catch], + ] + ) run_trials = run_trials.iloc[randorder] - run_trials['run'] = run + 1 - run_trials['onset'] = initial_wait + np.arange(n_trials_total) * trial_duration - run_trials['duration'] = image_duration + run_trials["run"] = run + 1 + run_trials["onset"] = initial_wait + np.arange(n_trials_total) * trial_duration + run_trials["duration"] = image_duration all_run_trials = pandas.concat([all_run_trials, run_trials]) - out_fname = os.path.join(THINGS_DATA_PATH, 'designs', f"sub-{parsed.subject}_ses-{parsed.session}_design.tsv") - all_run_trials.to_csv(out_fname, sep='\t') + out_fname = os.path.join( + THINGS_DATA_PATH, + "designs", + f"sub-{parsed.subject}_ses-{parsed.session}_design.tsv", + ) + all_run_trials.to_csv(out_fname, sep="\t") + if __name__ == "__main__": import argparse import sys + parser = argparse.ArgumentParser( formatter_class=argparse.RawTextHelpFormatter, - description='generate design files for participant / session') - parser.add_argument( - 'subject', - type=str, - help='participant id') - parser.add_argument( - 'session', - type=str, - help='session id') + description="generate design files for participant / session", + ) + parser.add_argument("subject", type=str, help="participant id") + parser.add_argument("session", type=str, help="session id") parsed = parser.parse_args() generate_design_file(parsed.subject, parsed.session) diff --git a/src/sessions/ses-triplet_test.py b/src/sessions/ses-triplet_test.py index 7a437f86..05132333 100644 --- a/src/sessions/ses-triplet_test.py +++ b/src/sessions/ses-triplet_test.py @@ -2,21 +2,11 @@ TASKS = [ language.Triplet( - 'data/language/triplet/first1000triples.csv', - wait_key=True, - name='triplet_test'), - - task_base.Pause( - text="Take a break. Press any key to continue...", - wait_key=None), - + "data/language/triplet/first1000triples.csv", wait_key=True, name="triplet_test" + ), + task_base.Pause(text="Take a break. Press any key to continue...", wait_key=None), language.Triplet( - 'data/language/triplet/first1000triples.csv', - wait_key=True, - name='triplet_test'), - - task_base.Pause( - text="Take a break. Press to continue...", - wait_key=['a']), - - ] + "data/language/triplet/first1000triples.csv", wait_key=True, name="triplet_test" + ), + task_base.Pause(text="Take a break. Press to continue...", wait_key=["a"]), +] diff --git a/src/sessions/ses-video1.py b/src/sessions/ses-video1.py index f34e62f8..d465aaf6 100644 --- a/src/sessions/ses-video1.py +++ b/src/sessions/ses-video1.py @@ -1,23 +1,23 @@ from ..tasks import images, video, memory, task_base TASKS = [ - video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), + "data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4", name="Inscapes" + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), - + "data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4", name="Inscapes" + ), task_base.Pause(), - video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_1_filt.mp4', - name='Oceans_fs_10m_1'), + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_1_filt.mp4", + name="Oceans_fs_10m_1", + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_2_filt.mp4', - name='Oceans_fs_10m_2'), + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_2_filt.mp4", + name="Oceans_fs_10m_2", + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_3_filt.mp4', - name='Oceans_fs_10m_3'), - - + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_3_filt.mp4", + name="Oceans_fs_10m_3", + ), ] diff --git a/src/sessions/ses-video2.py b/src/sessions/ses-video2.py index 7f1ad4fa..556c7a72 100644 --- a/src/sessions/ses-video2.py +++ b/src/sessions/ses-video2.py @@ -1,25 +1,27 @@ from ..tasks import images, video, memory, task_base TASKS = [ - video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), + "data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4", name="Inscapes" + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), - + "data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4", name="Inscapes" + ), task_base.Pause(), - video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_4_filt.mp4', - name='Oceans_fs_10m_4'), + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_4_filt.mp4", + name="Oceans_fs_10m_4", + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_5_filt.mp4', - name='Oceans_fs_10m_5'), + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_5_filt.mp4", + name="Oceans_fs_10m_5", + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_6_filt.mp4', - name='Oceans_fs_10m_6'), + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_6_filt.mp4", + name="Oceans_fs_10m_6", + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_7_filt.mp4', - name='Oceans_fs_10m_7'), - + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_7_filt.mp4", + name="Oceans_fs_10m_7", + ), ] diff --git a/src/sessions/ses-video3.py b/src/sessions/ses-video3.py index 9bfd84fd..c88b8a40 100644 --- a/src/sessions/ses-video3.py +++ b/src/sessions/ses-video3.py @@ -1,25 +1,27 @@ from ..tasks import images, video, memory, task_base TASKS = [ - video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), + "data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4", name="Inscapes" + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), - + "data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4", name="Inscapes" + ), task_base.Pause(), - video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_8_filt.mp4', - name='Oceans_fs_10m_8'), + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_8_filt.mp4", + name="Oceans_fs_10m_8", + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_9_filt.mp4', - name='Oceans_fs_10m_9'), + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_9_filt.mp4", + name="Oceans_fs_10m_9", + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_10_filt.mp4', - name='Oceans_fs_10m_10'), + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_10_filt.mp4", + name="Oceans_fs_10m_10", + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_11_filt.mp4', - name='Oceans_fs_10m_11'), - + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_11_filt.mp4", + name="Oceans_fs_10m_11", + ), ] diff --git a/src/sessions/ses-video3b.py b/src/sessions/ses-video3b.py index 0a75028a..8052865f 100644 --- a/src/sessions/ses-video3b.py +++ b/src/sessions/ses-video3b.py @@ -1,25 +1,27 @@ from ..tasks import images, video, memory, task_base TASKS = [ - video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), + "data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4", name="Inscapes" + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4', name='Inscapes'), - + "data/videos/Oceans_fs_10m_filt/Inscapes_sound_normed_filt.mp4", name="Inscapes" + ), task_base.Pause(), - video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_7_filt.mp4', - name='Oceans_fs_10m_7'), + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_7_filt.mp4", + name="Oceans_fs_10m_7", + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_8_filt.mp4', - name='Oceans_fs_10m_8'), + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_8_filt.mp4", + name="Oceans_fs_10m_8", + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_9_filt.mp4', - name='Oceans_fs_10m_9'), + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_9_filt.mp4", + name="Oceans_fs_10m_9", + ), video.SingleVideo( - 'data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_10_filt.mp4', - name='Oceans_fs_10m_10'), - + "data/videos/Oceans_fs_10m_filt/Oceans_fs_10m_10_filt.mp4", + name="Oceans_fs_10m_10", + ), ] diff --git a/src/sessions/ses-video_images_pilot.py b/src/sessions/ses-video_images_pilot.py index 5c288b1c..80ae4065 100644 --- a/src/sessions/ses-video_images_pilot.py +++ b/src/sessions/ses-video_images_pilot.py @@ -1,18 +1,25 @@ from ..tasks import images, video, memory, task_base TASKS = [ - #memory.ImagePosition('data/memory/stimuli.csv', use_fmri=parsed.fmri, use_eyetracking=True), + # memory.ImagePosition('data/memory/stimuli.csv', use_fmri=parsed.fmri, use_eyetracking=True), video.SingleVideo( - 'data/videos/Inscapes-67962604.mp4', name='Inscapes', - use_fmri=parsed.fmri, use_eyetracking=True), + "data/videos/Inscapes-67962604.mp4", + name="Inscapes", + use_fmri=parsed.fmri, + use_eyetracking=True, + ), task_base.Pause(), video.SingleVideo( - 'data/videos/skateboard_fails.mp4', - name='skateboard_fails', - use_fmri=parsed.fmri, use_eyetracking=True), + "data/videos/skateboard_fails.mp4", + name="skateboard_fails", + use_fmri=parsed.fmri, + use_eyetracking=True, + ), images.Images( - 'data/images/test_conditions.csv', - '/home/basile/data/projects/task_stimuli/data/images/bold5000/Scene_Stimuli/Presented_Stimuli/ImageNet', - name='bold5000', - use_fmri=parsed.fmri, use_eyetracking=True) - ] + "data/images/test_conditions.csv", + "/home/basile/data/projects/task_stimuli/data/images/bold5000/Scene_Stimuli/Presented_Stimuli/ImageNet", + name="bold5000", + use_fmri=parsed.fmri, + use_eyetracking=True, + ), +] diff --git a/src/sessions/ses-video_pilot.py b/src/sessions/ses-video_pilot.py index b1e51cb3..3267f2bb 100644 --- a/src/sessions/ses-video_pilot.py +++ b/src/sessions/ses-video_pilot.py @@ -1,10 +1,9 @@ from ..tasks import images, video, memory, task_base TASKS = [ - - video.SingleVideo( - 'data/videos/movies_for_montreal/03_Inscaped_NoScannerSound_h264.mov', name='Inscapes'), video.SingleVideo( - 'data/videos/tammy/Oceans_1.mp4', - name='Oceans_chunk1'), - ] + "data/videos/movies_for_montreal/03_Inscaped_NoScannerSound_h264.mov", + name="Inscapes", + ), + video.SingleVideo("data/videos/tammy/Oceans_1.mp4", name="Oceans_chunk1"), +] diff --git a/src/sessions/ses-videogame_test.py b/src/sessions/ses-videogame_test.py index b0fa8b9d..8528b4d6 100644 --- a/src/sessions/ses-videogame_test.py +++ b/src/sessions/ses-videogame_test.py @@ -1,13 +1,12 @@ - from ..tasks import images, videogame, memory, task_base TASKS = [ - videogame.VideoGame( - state_name='Level1', - scenario='scenario_repeat1', # this scenario repeats the same level - max_duration=10*60, # if when level completed or dead we exceed that time in secs, stop the task - name='ShinobiIIIReturnOfTheNinjaMaster-test'), - #videogame.VideoGameReplay('output/sub-test/ses-test_refactor/sub-test_ses-test_refactor_20190109_154007_ShinobiIIIReturnOfTheNinjaMaster-Genesis_Level4_000.bk2',name='ShinobiIIIReturnOfTheNinjaMaster-replay',), - -] * 3 ###!!!!!!!!!!! REPEAT !!!!!!!!!!!### + state_name="Level1", + scenario="scenario_repeat1", # this scenario repeats the same level + max_duration=10 + * 60, # if when level completed or dead we exceed that time in secs, stop the task + name="ShinobiIIIReturnOfTheNinjaMaster-test", + ), + # videogame.VideoGameReplay('output/sub-test/ses-test_refactor/sub-test_ses-test_refactor_20190109_154007_ShinobiIIIReturnOfTheNinjaMaster-Genesis_Level4_000.bk2',name='ShinobiIIIReturnOfTheNinjaMaster-replay',), +] * 3 ###!!!!!!!!!!! REPEAT !!!!!!!!!!!### diff --git a/src/sessions/ses-videoshorttest.py b/src/sessions/ses-videoshorttest.py index 63f8614c..25cf7e13 100644 --- a/src/sessions/ses-videoshorttest.py +++ b/src/sessions/ses-videoshorttest.py @@ -1,7 +1,5 @@ -from ..tasks import video, task_base +from ..tasks import video, task_base TASKS = [ - - video.SingleVideo( - 'data/videos/bourne_test.mkv', name='testshort'), + video.SingleVideo("data/videos/bourne_test.mkv", name="testshort"), ] diff --git a/src/sessions/ses-wolfwallstreet1.py b/src/sessions/ses-wolfwallstreet1.py index 58bf28c8..df37dfeb 100644 --- a/src/sessions/ses-wolfwallstreet1.py +++ b/src/sessions/ses-wolfwallstreet1.py @@ -1,9 +1,12 @@ from ..tasks import images, video, memory, task_base TASKS = [] -for seg_idx in range(1,7): +for seg_idx in range(1, 7): TASKS.append( video.SingleVideo( - 'data/videos/the_wolf_of_wall_street/the_wolf_of_wall_street_seg%02d.mkv'%seg_idx, - aspect_ratio = 12/5, - name='the_wolf_of_wall_street_seg-%d'%seg_idx)) + "data/videos/the_wolf_of_wall_street/the_wolf_of_wall_street_seg%02d.mkv" + % seg_idx, + aspect_ratio=12 / 5, + name="the_wolf_of_wall_street_seg-%d" % seg_idx, + ) + ) diff --git a/src/sessions/ses-wolfwallstreet2.py b/src/sessions/ses-wolfwallstreet2.py index 5e77d7fc..9ff5c2eb 100644 --- a/src/sessions/ses-wolfwallstreet2.py +++ b/src/sessions/ses-wolfwallstreet2.py @@ -2,9 +2,12 @@ TASKS = [] -for seg_idx in range(7,13): +for seg_idx in range(7, 13): TASKS.append( video.SingleVideo( - 'data/videos/the_wolf_of_wall_street/the_wolf_of_wall_street_seg%02d.mkv'%seg_idx, - aspect_ratio = 12/5, - name='the_wolf_of_wall_street_seg-%d'%seg_idx)) + "data/videos/the_wolf_of_wall_street/the_wolf_of_wall_street_seg%02d.mkv" + % seg_idx, + aspect_ratio=12 / 5, + name="the_wolf_of_wall_street_seg-%d" % seg_idx, + ) + ) diff --git a/src/sessions/ses-wolfwallstreet3.py b/src/sessions/ses-wolfwallstreet3.py index d8fe9b09..be12b191 100644 --- a/src/sessions/ses-wolfwallstreet3.py +++ b/src/sessions/ses-wolfwallstreet3.py @@ -1,9 +1,12 @@ from ..tasks import images, video, memory, task_base TASKS = [] -for seg_idx in range(13,18): +for seg_idx in range(13, 18): TASKS.append( video.SingleVideo( - 'data/videos/the_wolf_of_wall_street/the_wolf_of_wall_street_seg%02d.mkv'%seg_idx, - aspect_ratio = 12/5, - name='the_wolf_of_wall_street_seg-%d'%seg_idx)) + "data/videos/the_wolf_of_wall_street/the_wolf_of_wall_street_seg%02d.mkv" + % seg_idx, + aspect_ratio=12 / 5, + name="the_wolf_of_wall_street_seg-%d" % seg_idx, + ) + ) diff --git a/src/shared/__init__.py b/src/shared/__init__.py index 97cc74af..0b47e1ef 100644 --- a/src/shared/__init__.py +++ b/src/shared/__init__.py @@ -1,2 +1,3 @@ import sys, os -#sys.path.append(os.environ['PUPIL_MODULES_PATH']) + +# sys.path.append(os.environ['PUPIL_MODULES_PATH']) diff --git a/src/shared/cli.py b/src/shared/cli.py index f8d1a446..7ef48db2 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -11,66 +11,72 @@ globalClock = core.MonotonicClock(0) logging.setDefaultClock(globalClock) -from . import config #import first separately +from . import config # import first separately from . import fmri, eyetracking, utils, meg, config from ..tasks import task_base, video + def listen_shortcuts(): - if any([k[1]&event.MOD_CTRL for k in event._keyBuffer]): - allKeys = event.getKeys(['n','c','q'], modifiers=True) - ctrl_pressed = any([k[1]['ctrl'] for k in allKeys]) + if any([k[1] & event.MOD_CTRL for k in event._keyBuffer]): + allKeys = event.getKeys(["n", "c", "q"], modifiers=True) + ctrl_pressed = any([k[1]["ctrl"] for k in allKeys]) all_keys_only = [k[0] for k in allKeys] if len(allKeys) and ctrl_pressed: return all_keys_only[0] return False + def run_task_loop(loop, eyetracker=None, gaze_drawer=None, record_movie=False): for frameN, _ in enumerate(loop): if gaze_drawer: gaze = eyetracker.get_gaze() if not gaze is None: gaze_drawer.draw_gazepoint(gaze) - if record_movie and frameN%6==0: - record_movie.getMovieFrame(buffer='back') + if record_movie and frameN % 6 == 0: + record_movie.getMovieFrame(buffer="back") # check for global event keys shortcut_evt = listen_shortcuts() if shortcut_evt: return shortcut_evt -def run_task(task, exp_win, ctl_win=None, eyetracker=None, gaze_drawer=None, record_movie=False): - print('Next task: %s'%str(task)) + +def run_task( + task, exp_win, ctl_win=None, eyetracker=None, gaze_drawer=None, record_movie=False +): + print("Next task: %s" % str(task)) # show instruction shortcut_evt = run_task_loop( task.instructions(exp_win, ctl_win), eyetracker, gaze_drawer, - record_movie=exp_win if record_movie else False - ) + record_movie=exp_win if record_movie else False, + ) if task.use_fmri and not shortcut_evt: for _ in fmri.wait_for_ttl(): shortcut_evt = listen_shortcuts() - if shortcut_evt: return shortcut_evt + if shortcut_evt: + return shortcut_evt - logging.info('GO') + logging.info("GO") if eyetracker and not shortcut_evt: eyetracker.start_recording(task.name) # send start trigger/marker to MEG + Biopac (or anything else on parallel port) if task.use_meg and not shortcut_evt: - meg.send_signal(meg.MEG_settings['TASK_START_CODE']) + meg.send_signal(meg.MEG_settings["TASK_START_CODE"]) if not shortcut_evt: shortcut_evt = run_task_loop( task.run(exp_win, ctl_win), eyetracker, gaze_drawer, - record_movie=exp_win if record_movie else False - ) + record_movie=exp_win if record_movie else False, + ) # send stop trigger/marker to MEG + Biopac (or anything else on parallel port) if task.use_meg and not shortcut_evt: - meg.send_signal(meg.MEG_settings['TASK_STOP_CODE']) + meg.send_signal(meg.MEG_settings["TASK_STOP_CODE"]) if eyetracker: eyetracker.stop_recording() @@ -81,12 +87,14 @@ def run_task(task, exp_win, ctl_win=None, eyetracker=None, gaze_drawer=None, rec task.stop(exp_win, ctl_win), eyetracker, gaze_drawer, - record_movie=exp_win if record_movie else False - ) + record_movie=exp_win if record_movie else False, + ) return shortcut_evt -def main_loop(all_tasks, + +def main_loop( + all_tasks, subject, session, output_ds, @@ -97,10 +105,10 @@ def main_loop(all_tasks, allow_run_on_battery=False, enable_ptt=False, record_movie=False, - ): +): # force screen resolution to solve issues with video splitter at scanner - """ xrandr = Popen([ + """xrandr = Popen([ 'xrandr', '--output', 'eDP-1', '--mode', '%dx%d'%config.EXP_WINDOW['size'], @@ -108,93 +116,131 @@ def main_loop(all_tasks, time.sleep(5)""" if not utils.check_power_plugged(): - print('*'*25+'WARNING: the power cord is not connected'+'*'*25) + print("*" * 25 + "WARNING: the power cord is not connected" + "*" * 25) if not allow_run_on_battery: return - bids_sub_ses = ('sub-%s'%subject,'ses-%s'%session) - log_path = os.path.abspath(os.path.join(output_ds, 'sourcedata', *bids_sub_ses)) + bids_sub_ses = ("sub-%s" % subject, "ses-%s" % session) + log_path = os.path.abspath(os.path.join(output_ds, "sourcedata", *bids_sub_ses)) if not os.path.exists(log_path): os.makedirs(log_path, exist_ok=True) - log_name_prefix = 'sub-%s_ses-%s_%s'%(subject, session, datetime.datetime.now().strftime('%Y%m%d-%H%M%S')) - logfile_path = os.path.join(log_path, log_name_prefix+'.log') - log_file = logging.LogFile( - logfile_path, - level=logging.INFO, filemode='w') + log_name_prefix = "sub-%s_ses-%s_%s" % ( + subject, + session, + datetime.datetime.now().strftime("%Y%m%d-%H%M%S"), + ) + logfile_path = os.path.join(log_path, log_name_prefix + ".log") + log_file = logging.LogFile(logfile_path, level=logging.INFO, filemode="w") exp_win = visual.Window(**config.EXP_WINDOW) exp_win.mouseVisible = False if show_ctl_win: ctl_win = visual.Window(**config.CTL_WINDOW) - ctl_win.name = 'Stimuli' + ctl_win.name = "Stimuli" else: ctl_win = None ptt = None if enable_ptt: from .ptt import PushToTalk + ptt = PushToTalk() eyetracker_client = None gaze_drawer = None if enable_eyetracker: - print('creating et client') + print("creating et client") eyetracker_client = eyetracking.EyeTrackerClient( output_path=log_path, output_fname_base=log_name_prefix, profile=False, debug=False, - ) - print('starting et client') + ) + print("starting et client") eyetracker_client.start() - print('done') - all_tasks.insert(0, eyetracking.EyetrackerCalibration(eyetracker_client, name='EyeTracker-Calibration')) + print("done") + all_tasks.insert( + 0, + eyetracking.EyetrackerCalibration( + eyetracker_client, name="EyeTracker-Calibration" + ), + ) if show_ctl_win: gaze_drawer = eyetracking.GazeDrawer(ctl_win) if use_fmri: - setup_video_path = glob.glob(os.path.join('data','videos','subject_setup_videos','sub-%s_*'%subject)) + setup_video_path = glob.glob( + os.path.join("data", "videos", "subject_setup_videos", "sub-%s_*" % subject) + ) if not len(setup_video_path): - setup_video_path = [os.path.join('data','videos','subject_setup_videos','sub-default_setup_video.mp4')] - - all_tasks.insert(0, video.VideoAudioCheckLoop(setup_video_path[0], name='setup_soundcheck_video')) - all_tasks.insert(1, task_base.Pause("""We are completing the setup and initializing the scanner. + setup_video_path = [ + os.path.join( + "data", + "videos", + "subject_setup_videos", + "sub-default_setup_video.mp4", + ) + ] + + all_tasks.insert( + 0, + video.VideoAudioCheckLoop( + setup_video_path[0], name="setup_soundcheck_video" + ), + ) + all_tasks.insert( + 1, + task_base.Pause( + """We are completing the setup and initializing the scanner. We will start the tasks in a few minutes. -Please remain still.""")) - all_tasks.append(task_base.Pause("""We are done for today. +Please remain still.""" + ), + ) + all_tasks.append( + task_base.Pause( + """We are done for today. The scanner might run for a few seconds to acquire reference images. Please remain still. -We are coming to get you out of the scanner shortly.""")) +We are coming to get you out of the scanner shortly.""" + ) + ) else: - all_tasks.append(task_base.Pause("""We are done with the tasks for today. -Thanks for your participation!""")) + all_tasks.append( + task_base.Pause( + """We are done with the tasks for today. +Thanks for your participation!""" + ) + ) # list of tasks to be ran in a session - print('Here are the stimuli planned for today\n' + '_'*50) + print("Here are the stimuli planned for today\n" + "_" * 50) for task in all_tasks: - print('- ' + task.name) - print('_'*50) + print("- " + task.name) + print("_" * 50) try: for task in all_tasks: - #clear events buffer in case the user pressed a lot of buttoons + # clear events buffer in case the user pressed a lot of buttoons event.clearEvents() use_eyetracking = False if enable_eyetracker and task.use_eyetracking: use_eyetracking = True - #setup task files (eg. video) + # setup task files (eg. video) task.setup( - exp_win, log_path, log_name_prefix, + exp_win, + log_path, + log_name_prefix, use_fmri=use_fmri, use_eyetracking=use_eyetracking, - use_meg=use_meg) - print('READY') + use_meg=use_meg, + ) + print("READY") while True: - #force focus on the task window to ensure getting keys, TTL, ... + # force focus on the task window to ensure getting keys, TTL, ... exp_win.winHandle.activate() # record frame intervals for debug @@ -204,50 +250,50 @@ def main_loop(all_tasks, ctl_win, eyetracker_client, gaze_drawer, - record_movie=record_movie) + record_movie=record_movie, + ) - if shortcut_evt == 'n': + if shortcut_evt == "n": # restart the task - logging.exp(msg="task - %s: restart"%str(task)) + logging.exp(msg="task - %s: restart" % str(task)) task.restart() continue elif shortcut_evt: # abort/skip or quit - logging.exp(msg="task - %s: abort"%str(task)) + logging.exp(msg="task - %s: abort" % str(task)) break - else: # task completed - logging.exp(msg="task - %s: complete"%str(task)) + else: # task completed + logging.exp(msg="task - %s: complete" % str(task)) # send stop trigger/marker to MEG + Biopac (or anything else on parallel port) break logging.flush() if record_movie: out_fname = os.path.join( - task.output_path, - '%s_%s.mp4'%(task.output_fname_base, task.name)) + task.output_path, "%s_%s.mp4" % (task.output_fname_base, task.name) + ) print(f"saving movie as {out_fname}") exp_win.saveMovieFrames(out_fname, fps=10) task.unload() - if shortcut_evt=='q': - print('quit') + if shortcut_evt == "q": + print("quit") break elif shortcut_evt is None: # add a delay between tasks to avoid remaining TTL to start next task # do that only if the task was not aborted to save time # there is anyway the duration of the instruction before listening to TTL - for i in range(DELAY_BETWEEN_TASK*config.FRAME_RATE): + for i in range(DELAY_BETWEEN_TASK * config.FRAME_RATE): exp_win.flip(clearBuffer=False) - - exp_win.saveFrameIntervals('exp_win_frame_intervals.txt') + exp_win.saveFrameIntervals("exp_win_frame_intervals.txt") if ctl_win: - ctl_win.saveFrameIntervals('ctl_win_frame_intervals.txt') + ctl_win.saveFrameIntervals("ctl_win_frame_intervals.txt") except KeyboardInterrupt as ki: print(traceback.format_exc()) logging.exp(msg="user killing the program") - print('you killing me!') + print("you killing me!") finally: if enable_eyetracker: eyetracker_client.join(TIMEOUT) diff --git a/src/shared/config.py b/src/shared/config.py index 350a6a8a..926452b3 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -1,33 +1,33 @@ from psychopy import prefs # avoids delay in movie3 audio seek -prefs.hardware['audioLib'] = ['sounddevice'] -#prefs.hardware['general'] = ['glfw'] +prefs.hardware["audioLib"] = ["sounddevice"] +# prefs.hardware['general'] = ['glfw'] -OUTPUT_DIR = 'output' +OUTPUT_DIR = "output" -EYETRACKING_ROI = (60,30,660,450) +EYETRACKING_ROI = (60, 30, 660, 450) -EXP_SCREEN_XRANDR_NAME='eDP-1' +EXP_SCREEN_XRANDR_NAME = "eDP-1" EXP_WINDOW = dict( - size = (1280, 1024), + size=(1280, 1024), screen=1, fullscr=True, - gammaErrorPolicy='warn', + gammaErrorPolicy="warn", waitBlanking=False, ) CTL_WINDOW = dict( - size = (1280, 1024), - pos = (100,0), + size=(1280, 1024), + pos=(100, 0), screen=0, - gammaErrorPolicy='warn', -# swapInterval=0., - waitBlanking=False, # avoid ctrl window to block the script in case of differing refresh rate. + gammaErrorPolicy="warn", + # swapInterval=0., + waitBlanking=False, # avoid ctrl window to block the script in case of differing refresh rate. ) -FRAME_RATE=60 +FRAME_RATE = 60 # task parameters INSTRUCTION_DURATION = 6 @@ -35,4 +35,4 @@ WRAP_WIDTH = 1 # port for meg setup -PARALLEL_PORT_ADDRESS = '/dev/parport0' +PARALLEL_PORT_ADDRESS = "/dev/parport0" diff --git a/src/shared/ellipse.py b/src/shared/ellipse.py index 69b5649e..e6520430 100644 --- a/src/shared/ellipse.py +++ b/src/shared/ellipse.py @@ -11,13 +11,14 @@ import numpy + class Ellipse(Polygon): """Creates a Circle with a given radius as a special case of a :class:`~psychopy.visual.ShapeStim` (New in version 1.72.00) """ - def __init__(self, win, radius=.5, radius2=.5, edges=32, **kwargs): + def __init__(self, win, radius=0.5, radius2=0.5, edges=32, **kwargs): """ Circle accepts all input parameters that `~psychopy.visual.ShapeStim` accept, @@ -26,15 +27,15 @@ def __init__(self, win, radius=.5, radius2=.5, edges=32, **kwargs): # what local vars are defined (these are the init params) for use by # __repr__ self._initParams = dir() - self._initParams.remove('self') + self._initParams.remove("self") # kwargs isn't a parameter, but a list of params - self._initParams.remove('kwargs') + self._initParams.remove("kwargs") self._initParams.extend(kwargs) # initialise parent class - kwargs['edges'] = edges - kwargs['radius'] = radius - self.__dict__['radius2'] = numpy.asarray(radius2) + kwargs["edges"] = edges + kwargs["radius"] = radius + self.__dict__["radius2"] = numpy.asarray(radius2) super(Ellipse, self).__init__(win, **kwargs) @attributeSetter @@ -46,12 +47,17 @@ def radius2(self, radius2): Usually there's a setAttribute(value, log=False) method for each attribute. Use this if you want to disable logging. """ - self.__dict__['radius2'] = numpy.array(radius2) + self.__dict__["radius2"] = numpy.array(radius2) self._calcVertices() self.setVertices(self.vertices, log=False) def _calcVertices(self): d = numpy.pi * 2 / self.edges self.vertices = numpy.asarray( - [numpy.asarray((numpy.sin(e * d) * self.radius, numpy.cos(e * d) * self.radius2)) - for e in range(int(round(self.edges)))]) + [ + numpy.asarray( + (numpy.sin(e * d) * self.radius, numpy.cos(e * d) * self.radius2) + ) + for e in range(int(round(self.edges))) + ] + ) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index acd57853..46f01fe7 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -47,7 +47,7 @@ "global_gain": 1, "gev_packet_size": 2000, "uid": "Aravis-Fake-GV01", # for test purposes - #"uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", + # "uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", } diff --git a/src/shared/fmri.py b/src/shared/fmri.py index 36a9b030..e3eb997d 100644 --- a/src/shared/fmri.py +++ b/src/shared/fmri.py @@ -3,30 +3,32 @@ import time MR_settings = { - 'TR': 2.000, # duration (sec) per whole-brain volume - 'sync': '5', # character to use as the sync timing event; assumed to come at start of a volume - 'skip': 0, # number of volumes lacking a sync pulse at start of scan (for T1 stabilization) - } + "TR": 2.000, # duration (sec) per whole-brain volume + "sync": "5", # character to use as the sync timing event; assumed to come at start of a volume + "skip": 0, # number of volumes lacking a sync pulse at start of scan (for T1 stabilization) +} globalClock = core.Clock() + def get_ttl(): - allKeys = event.getKeys([MR_settings['sync']]) + allKeys = event.getKeys([MR_settings["sync"]]) for key in allKeys: - if key.lower() == MR_settings['sync']: + if key.lower() == MR_settings["sync"]: return True return False + # blocking function (iterator) def wait_for_ttl(): - get_ttl() # flush any remaining TTL keys + get_ttl() # flush any remaining TTL keys ttl_index = 0 logging.exp(msg="waiting for fMRI TTL") while True: if get_ttl(): - #TODO: log real timing of TTL? - logging.exp(msg="fMRI TTL %d"%ttl_index) + # TODO: log real timing of TTL? + logging.exp(msg="fMRI TTL %d" % ttl_index) ttl_index += 1 return - time.sleep(.0005) #just to avoid looping to fast + time.sleep(0.0005) # just to avoid looping to fast yield diff --git a/src/shared/meg.py b/src/shared/meg.py index 898f28b5..fc69dc42 100644 --- a/src/shared/meg.py +++ b/src/shared/meg.py @@ -3,12 +3,13 @@ import time MEG_settings = { - 'TASK_START_CODE': int("00000010", 2), - 'TASK_START_STOP': int("00000100", 2), + "TASK_START_CODE": int("00000010", 2), + "TASK_START_STOP": int("00000100", 2), } + def send_signal(data): port = parallel.ParallelPort(address=config.PARALLEL_PORT_ADDRESS) port.setData(data) time.sleep(0.001) - port.setData(0) #reset + port.setData(0) # reset diff --git a/src/shared/parser.py b/src/shared/parser.py index 9e045c8d..115c0264 100644 --- a/src/shared/parser.py +++ b/src/shared/parser.py @@ -1,48 +1,42 @@ import argparse + def parse_args(): parser = argparse.ArgumentParser( - prog='main.py', - description=('Run all tasks in a session'), - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument('--subject', '-s', - required=True, - help='Subject ID') - parser.add_argument('--session', '-ss', - required=True, - help='Session') - parser.add_argument('--tasks', '-t', - required=True, - help='tasks set') - parser.add_argument('--output', '-o', - required=True, - help='output dataset') - parser.add_argument('--fmri', '-f', - help='Wait for fmri TTL to start each task', - action='store_true') - parser.add_argument('--meg', '-m', - help='Send signal to parallel port to start trigger to MEG and Biopac.', - action='store_true') - parser.add_argument('--eyetracking', '-e', - help='Enable eyetracking', - action='store_true') - parser.add_argument('--skip_n_tasks', - help='skip n of the tasks', - default=0, - type=int) - parser.add_argument('--ctl_win', - help='show control window', - action='store_true') - parser.add_argument('--run_on_battery', - help='allow the script to run on battery', - action='store_true') - parser.add_argument('--ptt', - help='enable Push-To-Talk function', - action='store_true') - parser.add_argument('--profile', - help='enable profiling', - action='store_true') - parser.add_argument('--record-movie', - help='record a movie of each task', - action='store_true') + prog="main.py", + description=("Run all tasks in a session"), + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument("--subject", "-s", required=True, help="Subject ID") + parser.add_argument("--session", "-ss", required=True, help="Session") + parser.add_argument("--tasks", "-t", required=True, help="tasks set") + parser.add_argument("--output", "-o", required=True, help="output dataset") + parser.add_argument( + "--fmri", "-f", help="Wait for fmri TTL to start each task", action="store_true" + ) + parser.add_argument( + "--meg", + "-m", + help="Send signal to parallel port to start trigger to MEG and Biopac.", + action="store_true", + ) + parser.add_argument( + "--eyetracking", "-e", help="Enable eyetracking", action="store_true" + ) + parser.add_argument( + "--skip_n_tasks", help="skip n of the tasks", default=0, type=int + ) + parser.add_argument("--ctl_win", help="show control window", action="store_true") + parser.add_argument( + "--run_on_battery", + help="allow the script to run on battery", + action="store_true", + ) + parser.add_argument( + "--ptt", help="enable Push-To-Talk function", action="store_true" + ) + parser.add_argument("--profile", help="enable profiling", action="store_true") + parser.add_argument( + "--record-movie", help="record a movie of each task", action="store_true" + ) return parser.parse_args() diff --git a/src/shared/ptt.py b/src/shared/ptt.py index 772a0ed1..8551dbf2 100644 --- a/src/shared/ptt.py +++ b/src/shared/ptt.py @@ -1,23 +1,24 @@ # Push-To-Talk functions import pulsectl -MIC_SOURCE_NAME = 'alsa_input.pci-0000_00_1f.3.analog-stereo' -DSP_SINK_NAME = 'dsp' -DSP_SINK_NAME = 'alsa_output.usb-Lenovo_ThinkPad_USB-C_Dock_Audio_000000000000-00.analog-stereo' +MIC_SOURCE_NAME = "alsa_input.pci-0000_00_1f.3.analog-stereo" +DSP_SINK_NAME = "dsp" +DSP_SINK_NAME = ( + "alsa_output.usb-Lenovo_ThinkPad_USB-C_Dock_Audio_000000000000-00.analog-stereo" +) -class PushToTalk(): +class PushToTalk: def __init__(self): - self._pa_client = pulsectl.Pulse('ptt_loopback') + self._pa_client = pulsectl.Pulse("ptt_loopback") self._source_idx = self._pa_client.get_source_by_name(MIC_SOURCE_NAME) self._sink_idx = self._pa_client.get_sink_by_name(DSP_SINK_NAME) def _init_loopback(self): self._loopback_mod_idx = self._pa_client.module_load( - 'module-loopback', - 'latency_msec=5 source=%d sink=%d'%( - self._source_idx, - self._sink_idx)) + "module-loopback", + "latency_msec=5 source=%d sink=%d" % (self._source_idx, self._sink_idx), + ) def _destroy_loopback(self): self._pa_client.unload_module(self._loopback_mod_idx) diff --git a/src/shared/screen.py b/src/shared/screen.py index 69330222..4a2c8f53 100644 --- a/src/shared/screen.py +++ b/src/shared/screen.py @@ -1,17 +1,27 @@ from . import config from subprocess import Popen + def init_exp_screen(): - xrandr = Popen([ - 'xrandr', - '--output', config.EXP_SCREEN_XRANDR_NAME, - '--mode', '%dx%d'%config.EXP_WINDOW['size'], - '--rate', str(config.FRAME_RATE), - ]) + xrandr = Popen( + [ + "xrandr", + "--output", + config.EXP_SCREEN_XRANDR_NAME, + "--mode", + "%dx%d" % config.EXP_WINDOW["size"], + "--rate", + str(config.FRAME_RATE), + ] + ) + def reset_exp_screen(): - xrandr = Popen([ - 'xrandr', - '--output', config.EXP_SCREEN_XRANDR_NAME, - '--preferred', - ]) + xrandr = Popen( + [ + "xrandr", + "--output", + config.EXP_SCREEN_XRANDR_NAME, + "--preferred", + ] + ) diff --git a/src/shared/utils.py b/src/shared/utils.py index 9e2a7ea0..033d1d8b 100644 --- a/src/shared/utils.py +++ b/src/shared/utils.py @@ -1,5 +1,6 @@ import psutil + def check_power_plugged(): battery = psutil.sensors_battery() if battery: diff --git a/src/tasks/images.py b/src/tasks/images.py index 8bfce8d2..c4ea5046 100644 --- a/src/tasks/images.py +++ b/src/tasks/images.py @@ -4,64 +4,74 @@ from ..shared import config -STIMULI_DURATION=3 -BASELINE_BEGIN=5 -BASELINE_END=5 -ISI=4 +STIMULI_DURATION = 3 +BASELINE_BEGIN = 5 +BASELINE_END = 5 +ISI = 4 + class Images(Task): DEFAULT_INSTRUCTION = """Please keep your eyes open an focused on the screen all the time. You will see pictures of scenes and objects.""" - def __init__(self, images_list, images_path, *args,**kwargs): + def __init__(self, images_list, images_path, *args, **kwargs): super().__init__(**kwargs) - #TODO: image lists as params, subjects .... + # TODO: image lists as params, subjects .... self.image_names = data.importConditions(images_list) - if os.path.exists(images_path) and os.path.exists(os.path.join(images_path, self.images_names[0]['images_path'])): + if os.path.exists(images_path) and os.path.exists( + os.path.join(images_path, self.images_names[0]["images_path"]) + ): self.images_path = images_path else: - raise ValueError('Cannot find the listed images in %s '%images_path) + raise ValueError("Cannot find the listed images in %s " % images_path) def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( - exp_win, text=self.instruction, - alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + exp_win, + text=self.instruction, + alignText="center", + color="white", + wrapWidth=config.WRAP_WIDTH, + ) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): screen_text.draw(exp_win) if ctl_win: screen_text.draw(ctl_win) - yield() + yield () def _run(self, exp_win, ctl_win): - self.trials = data.TrialHandler(self.image_names, 1, method='sequential') - img = visual.ImageStim(exp_win,size=(1,1),units='height') - exp_win.logOnFlip(level=logging.EXP,msg='image: task starting at %f'%time.time()) + self.trials = data.TrialHandler(self.image_names, 1, method="sequential") + img = visual.ImageStim(exp_win, size=(1, 1), units="height") + exp_win.logOnFlip( + level=logging.EXP, msg="image: task starting at %f" % time.time() + ) for frameN in range(config.FRAME_RATE * BASELINE_BEGIN): - yield() + yield () for trial in self.trials: - image_path = os.path.join(self.images_path, trial['image_path']) + image_path = os.path.join(self.images_path, trial["image_path"]) img.image = image_path - exp_win.logOnFlip(level=logging.EXP,msg='image: display %s'%image_path) - trial['onset'] = self.task_timer.getTime() + exp_win.logOnFlip(level=logging.EXP, msg="image: display %s" % image_path) + trial["onset"] = self.task_timer.getTime() for frameN in range(config.FRAME_RATE * STIMULI_DURATION): img.draw(exp_win) if ctl_win: img.draw(ctl_win) - yield() - trial['offset'] = self.task_timer.getTime() - trial['duration'] = trial['offset']-trial['onset'] - exp_win.logOnFlip(level=logging.EXP,msg='image: rest') + yield () + trial["offset"] = self.task_timer.getTime() + trial["duration"] = trial["offset"] - trial["onset"] + exp_win.logOnFlip(level=logging.EXP, msg="image: rest") for frameN in range(config.FRAME_RATE * ISI): - yield() + yield () for frameN in range(config.FRAME_RATE * BASELINE_END): - yield() + yield () def _save(self): self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) return False + class BOLD5000Images(Images): pass diff --git a/src/tasks/language.py b/src/tasks/language.py index 88147cd0..62ec28c3 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -4,12 +4,13 @@ from ..shared import config -STIMULI_DURATION=4 -BASELINE_BEGIN=5 -BASELINE_END=5 -TRIPLET_RIGHT_KEY='l' -TRIPLET_LEFT_KEY='d' -ISI=2 +STIMULI_DURATION = 4 +BASELINE_BEGIN = 5 +BASELINE_END = 5 +TRIPLET_RIGHT_KEY = "l" +TRIPLET_LEFT_KEY = "d" +ISI = 2 + class Triplet(Task): @@ -20,10 +21,12 @@ class Triplet(Task): You have to select the response (left or right) that is closest to the target.""" - INSTRUCTION_WAIT_KEY = DEFAULT_INSTRUCTION + "\nWhen you're ready press <%s>" % TRIPLET_LEFT_KEY + INSTRUCTION_WAIT_KEY = ( + DEFAULT_INSTRUCTION + "\nWhen you're ready press <%s>" % TRIPLET_LEFT_KEY + ) - def __init__(self, words_file,*args,**kwargs): - self.wait_key = kwargs.pop('wait_key', False) + def __init__(self, words_file, *args, **kwargs): + self.wait_key = kwargs.pop("wait_key", False) super().__init__(**kwargs) if self.wait_key: self.instruction = Triplet.INSTRUCTION_WAIT_KEY @@ -31,18 +34,23 @@ def __init__(self, words_file,*args,**kwargs): self.words_file = words_file self.words_list = data.importConditions(self.words_file) else: - raise ValueError('File %s does not exists'%words_file) + raise ValueError("File %s does not exists" % words_file) def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( - exp_win, text=self.instruction, - alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + exp_win, + text=self.instruction, + alignText="center", + color="white", + wrapWidth=config.WRAP_WIDTH, + ) def _draw_instr(): screen_text.draw(exp_win) if ctl_win: screen_text.draw(ctl_win) + if self.wait_key: while True: if len(event.getKeys([TRIPLET_LEFT_KEY])): @@ -56,60 +64,61 @@ def _draw_instr(): def _run(self, exp_win, ctl_win): - self.trials = data.TrialHandler(self.words_list, 1, method='random') + self.trials = data.TrialHandler(self.words_list, 1, method="random") target_stim = visual.TextStim( - exp_win, text='', - pos = (0,.25), - alignText="center", color = 'white') + exp_win, text="", pos=(0, 0.25), alignText="center", color="white" + ) r1_stim = visual.TextStim( - exp_win, text='', - pos = (-.5,-.25), - alignText="center", color = 'white') + exp_win, text="", pos=(-0.5, -0.25), alignText="center", color="white" + ) r2_stim = visual.TextStim( - exp_win, text='', - pos = (.5,-.25), - alignText="center", color = 'white') + exp_win, text="", pos=(0.5, -0.25), alignText="center", color="white" + ) - exp_win.logOnFlip(level=logging.EXP,msg='triplet: task starting at %f'%time.time()) + exp_win.logOnFlip( + level=logging.EXP, msg="triplet: task starting at %f" % time.time() + ) for frameN in range(config.FRAME_RATE * BASELINE_BEGIN): - yield() + yield () for trial_idx, trial in enumerate(self.trials): - target_stim.text = trial['target'] - r1_stim.text = trial['response1'] - r2_stim.text = trial['response2'] + target_stim.text = trial["target"] + r1_stim.text = trial["response1"] + r2_stim.text = trial["response2"] - exp_win.logOnFlip(level=logging.EXP,msg='triplet: %d'%trial_idx) + exp_win.logOnFlip(level=logging.EXP, msg="triplet: %d" % trial_idx) onset = self.task_timer.getTime() # flush keys pressed before - event.getKeys([TRIPLET_LEFT_KEY,TRIPLET_RIGHT_KEY]) + event.getKeys([TRIPLET_LEFT_KEY, TRIPLET_RIGHT_KEY]) for frameN in range(config.FRAME_RATE * STIMULI_DURATION): - triplet_answer_keys = event.getKeys([TRIPLET_LEFT_KEY,TRIPLET_RIGHT_KEY]) - if(len(triplet_answer_keys)): - self.trials.addData('answer', triplet_answer_keys[0]) + triplet_answer_keys = event.getKeys( + [TRIPLET_LEFT_KEY, TRIPLET_RIGHT_KEY] + ) + if len(triplet_answer_keys): + self.trials.addData("answer", triplet_answer_keys[0]) for frameNN in range(frameN, config.FRAME_RATE * STIMULI_DURATION): - yield() + yield () break for stim in [target_stim, r1_stim, r2_stim]: stim.draw(exp_win) if ctl_win: stim.draw(ctl_win) - yield() + yield () else: - self.trials.addData('answer', '') # no answer, too slow or asleep + self.trials.addData("answer", "") # no answer, too slow or asleep offset = self.task_timer.getTime() - self.trials.addData('onset', onset) - self.trials.addData('offset', offset) - self.trials.addData('duration', offset-onset) # RT or max stim duration - exp_win.logOnFlip(level=logging.EXP,msg='triplet: rest') + self.trials.addData("onset", onset) + self.trials.addData("offset", offset) + self.trials.addData("duration", offset - onset) # RT or max stim duration + exp_win.logOnFlip(level=logging.EXP, msg="triplet: rest") for frameN in range(config.FRAME_RATE * ISI): - yield() + yield () for frameN in range(config.FRAME_RATE * BASELINE_END): - yield() + yield () def _save(self): - self.trials.saveAsWideText(self._generate_unique_filename("events","tsv")) + self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) return False diff --git a/src/tasks/memory.py b/src/tasks/memory.py index 1df9a790..1eff9f42 100644 --- a/src/tasks/memory.py +++ b/src/tasks/memory.py @@ -4,62 +4,67 @@ from ..shared import config -STIMULI_DURATION=4 -BASELINE_BEGIN=5 -BASELINE_END=5 -ISI=1 -IMAGES_FOLDER = '/home/basile/data/projects/task_stimuli/BOLD5000_Stimuli/Scene_Stimuli/Presented_Stimuli/ImageNet' +STIMULI_DURATION = 4 +BASELINE_BEGIN = 5 +BASELINE_END = 5 +ISI = 1 +IMAGES_FOLDER = "/home/basile/data/projects/task_stimuli/BOLD5000_Stimuli/Scene_Stimuli/Presented_Stimuli/ImageNet" -STIMULI_SIZE = (400,400) +STIMULI_SIZE = (400, 400) + +quadrant_id_to_pos = [(-200, 100), (200, 100), (-200, -100), (200, -100)] -quadrant_id_to_pos = [ - (-200,100), - (200,100), - (-200,-100), - (200,-100) -] class ImagePosition(Task): DEFAULT_INSTRUCTION = """You will be presented a set of items in different quadrant of the screen. Try to remember the items and their location on the screen.""" - def __init__(self, items_list,*args,**kwargs): + def __init__(self, items_list, *args, **kwargs): super().__init__(**kwargs) - #TODO: image lists as params, subjects .... + # TODO: image lists as params, subjects .... self.item_list = data.importConditions(items_list) def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( - exp_win, text=self.instruction, - alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + exp_win, + text=self.instruction, + alignText="center", + color="white", + wrapWidth=config.WRAP_WIDTH, + ) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): screen_text.draw(exp_win) if ctl_win: screen_text.draw(ctl_win) - yield() + yield () def _run(self, exp_win, ctl_win): - - trials = data.TrialHandler(self.item_list, 1, method='sequential') - img = visual.ImageStim(exp_win,size=STIMULI_SIZE, units='pixels') - exp_win.logOnFlip(level=logging.EXP,msg='memory: task starting at %f'%time.time()) + trials = data.TrialHandler(self.item_list, 1, method="sequential") + img = visual.ImageStim(exp_win, size=STIMULI_SIZE, units="pixels") + exp_win.logOnFlip( + level=logging.EXP, msg="memory: task starting at %f" % time.time() + ) for frameN in range(config.FRAME_RATE * BASELINE_BEGIN): - yield() + yield () for trial in trials: - image_path = trial['image_path'] + image_path = trial["image_path"] img.image = image_path - img.pos = quadrant_id_to_pos[trial['quadrant']] - exp_win.logOnFlip(level=logging.EXP,msg='memory: display %s in quadrant %d'%(image_path,trial['quadrant'])) + img.pos = quadrant_id_to_pos[trial["quadrant"]] + exp_win.logOnFlip( + level=logging.EXP, + msg="memory: display %s in quadrant %d" + % (image_path, trial["quadrant"]), + ) for frameN in range(config.FRAME_RATE * STIMULI_DURATION): img.draw(exp_win) if ctl_win: img.draw(ctl_win) - yield() - exp_win.logOnFlip(level=logging.EXP,msg='memory: rest') + yield () + exp_win.logOnFlip(level=logging.EXP, msg="memory: rest") for frameN in range(config.FRAME_RATE * ISI): - yield() + yield () for frameN in range(config.FRAME_RATE * BASELINE_END): - yield() + yield () diff --git a/src/tasks/speech.py b/src/tasks/speech.py index eaa70a2e..4f74c56f 100644 --- a/src/tasks/speech.py +++ b/src/tasks/speech.py @@ -4,62 +4,67 @@ from ..shared import config -STIMULI_DURATION=4 -BASELINE_BEGIN=5 -BASELINE_END=5 -ISI=4 +STIMULI_DURATION = 4 +BASELINE_BEGIN = 5 +BASELINE_END = 5 +ISI = 4 + class Speech(Task): DEFAULT_INSTRUCTION = """You will be presented text that you need to read out loud right when you see it.""" - def __init__(self, words_file,*args,**kwargs): + def __init__(self, words_file, *args, **kwargs): super().__init__(**kwargs) if os.path.exists(words_file): self.words_file = words_file self.words_list = data.importConditions(self.words_file) else: - raise ValueError('File %s does not exists'%words_file) + raise ValueError("File %s does not exists" % words_file) def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( - exp_win, text=self.instruction, - alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + exp_win, + text=self.instruction, + alignText="center", + color="white", + wrapWidth=config.WRAP_WIDTH, + ) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): screen_text.draw(exp_win) if ctl_win: screen_text.draw(ctl_win) - yield() + yield () def _run(self, exp_win, ctl_win): - self.trials = data.TrialHandler(self.words_list, 1, method='random') + self.trials = data.TrialHandler(self.words_list, 1, method="random") - text = visual.TextStim( - exp_win, text='', - alignText="center", color = 'white') + text = visual.TextStim(exp_win, text="", alignText="center", color="white") - exp_win.logOnFlip(level=logging.EXP,msg='speech: task starting at %f'%time.time()) + exp_win.logOnFlip( + level=logging.EXP, msg="speech: task starting at %f" % time.time() + ) for frameN in range(config.FRAME_RATE * BASELINE_BEGIN): - yield() + yield () for trial in self.trials: - text.text = trial['text'] - exp_win.logOnFlip(level=logging.EXP,msg='speech: %s'%text.text) - trial['onset'] = self.task_timer.getTime() + text.text = trial["text"] + exp_win.logOnFlip(level=logging.EXP, msg="speech: %s" % text.text) + trial["onset"] = self.task_timer.getTime() for frameN in range(config.FRAME_RATE * STIMULI_DURATION): text.draw(exp_win) if ctl_win: text.draw(ctl_win) - yield() - trial['offset'] = self.task_timer.getTime() - trial['duration'] = trial['offset']-trial['onset'] - exp_win.logOnFlip(level=logging.EXP,msg='speech: rest') + yield () + trial["offset"] = self.task_timer.getTime() + trial["duration"] = trial["offset"] - trial["onset"] + exp_win.logOnFlip(level=logging.EXP, msg="speech: rest") for frameN in range(config.FRAME_RATE * ISI): - yield() + yield () for frameN in range(config.FRAME_RATE * BASELINE_END): - yield() + yield () def _save(self): self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index bc846489..b37866c8 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -5,9 +5,10 @@ from ..shared import fmri, meg, config + class Task(object): - DEFAULT_INSTRUCTION='' + DEFAULT_INSTRUCTION = "" def __init__(self, name, instruction=None): self.name = name @@ -18,7 +19,15 @@ def __init__(self, name, instruction=None): self.instruction = instruction # setup large files for accurate start with other recordings (scanner, biopac...) - def setup(self, exp_win, output_path, output_fname_base, use_fmri=False, use_eyetracking=False, use_meg=False): + def setup( + self, + exp_win, + output_path, + output_fname_base, + use_fmri=False, + use_eyetracking=False, + use_meg=False, + ): self.output_path = output_path self.output_fname_base = output_fname_base self.use_fmri = use_fmri @@ -27,18 +36,25 @@ def setup(self, exp_win, output_path, output_fname_base, use_fmri=False, use_eye self._events = [] self._setup(exp_win) # initialize a progress bar if we know the duration of the task - self.progress_bar = tqdm.tqdm(total=self.duration) if hasattr(self, 'duration') else False - if not hasattr(self,'_progress_bar_refresh_rate'): + self.progress_bar = ( + tqdm.tqdm(total=self.duration) if hasattr(self, "duration") else False + ) + if not hasattr(self, "_progress_bar_refresh_rate"): self._progress_bar_refresh_rate = config.FRAME_RATE def _setup(self, exp_win): pass - def _generate_unique_filename(self, suffix, ext='tsv'): - fname = os.path.join(self.output_path, f"{self.output_fname_base}_{self.name}_{suffix}.{ext}") + def _generate_unique_filename(self, suffix, ext="tsv"): + fname = os.path.join( + self.output_path, f"{self.output_fname_base}_{self.name}_{suffix}.{ext}" + ) fi = 1 while os.path.exists(fname): - fname = os.path.join(self.output_path, f"{self.output_fname_base}_{self.name}_{suffix}-{fi:03d}.{ext}") + fname = os.path.join( + self.output_path, + f"{self.output_fname_base}_{self.name}_{suffix}-{fi:03d}.{ext}", + ) fi += 1 return fname @@ -46,7 +62,7 @@ def unload(self): pass def __str__(self): - return '%s : %s'%(self.__class__, self.name) + return "%s : %s" % (self.__class__, self.name) def _flip_all_windows(self, exp_win, ctl_win=None, clearBuffer=True): if not ctl_win is None: @@ -54,7 +70,7 @@ def _flip_all_windows(self, exp_win, ctl_win=None, clearBuffer=True): self._exp_win_last_flip_time = exp_win.flip(clearBuffer=clearBuffer) def instructions(self, exp_win, ctl_win): - if hasattr(self, '_instructions'): + if hasattr(self, "_instructions"): for clearBuffer in self._instructions(exp_win, ctl_win): yield self._flip_all_windows(exp_win, ctl_win, clearBuffer) @@ -71,7 +87,7 @@ def run(self, exp_win, ctl_win): # yield first to allow external draw before flip yield self._flip_all_windows(exp_win, ctl_win, clearBuffer) - if not hasattr(self, '_exp_win_first_flip_time'): + if not hasattr(self, "_exp_win_first_flip_time"): self._exp_win_first_flip_time = self._exp_win_last_flip_time # increment the progress bar every second if self.progress_bar: @@ -84,7 +100,7 @@ def run(self, exp_win, ctl_win): self.progress_bar.close() def stop(self, exp_win, ctl_win): - if hasattr(self, '_stop'): + if hasattr(self, "_stop"): for clearBuffer in self._stop(exp_win, ctl_win): yield self._flip_all_windows(exp_win, ctl_win, clearBuffer) @@ -93,11 +109,11 @@ def stop(self, exp_win, ctl_win): self._flip_all_windows(exp_win, ctl_win, True) def restart(self): - if hasattr(self, '_restart'): + if hasattr(self, "_restart"): self._restart() def _log_event(self, event): - event.update({'onset': self.task_timer.getTime()}) + event.update({"onset": self.task_timer.getTime()}) self._events.append(event) def _save(self): @@ -112,15 +128,14 @@ def save(self): if save_events is None and len(self._events): fname = self._generate_unique_filename("events", "tsv") df = pandas.DataFrame(self._events) - df.to_csv(fname, sep='\t', index=False) + df.to_csv(fname, sep="\t", index=False) class Pause(Task): - def __init__(self, text="Taking a short break, relax...", **kwargs): - self.wait_key = kwargs.pop('wait_key', False) - if not 'name' in kwargs: - kwargs['name'] = 'Pause' + self.wait_key = kwargs.pop("wait_key", False) + if not "name" in kwargs: + kwargs["name"] = "Pause" super().__init__(**kwargs) self.text = text @@ -131,8 +146,12 @@ def _setup(self, exp_win): def _run(self, exp_win, ctl_win): screen_text = visual.TextStim( - exp_win, text=self.text, - alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + exp_win, + text=self.text, + alignText="center", + color="white", + wrapWidth=config.WRAP_WIDTH, + ) while True: if not self.wait_key is False: @@ -146,23 +165,28 @@ def _run(self, exp_win, ctl_win): def _stop(self, exp_win, ctl_win): yield True + class Fixation(Task): DEFAULT_INSTRUCTION = """We are going to acquired resting-state data. Please keep your eyes open and fixate the cross. Do not think about something in particular, let your mind wander...""" - def __init__(self, duration=7*60, symbol="+", **kwargs): - if not 'name' in kwargs: - kwargs['name'] = 'Pause' + def __init__(self, duration=7 * 60, symbol="+", **kwargs): + if not "name" in kwargs: + kwargs["name"] = "Pause" super().__init__(**kwargs) self.duration = duration self.symbol = symbol def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( - exp_win, text=self.instruction, - alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + exp_win, + text=self.instruction, + alignText="center", + color="white", + wrapWidth=config.WRAP_WIDTH, + ) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): screen_text.draw(exp_win) @@ -172,9 +196,9 @@ def _instructions(self, exp_win, ctl_win): def _run(self, exp_win, ctl_win): screen_text = visual.TextStim( - exp_win, text=self.symbol, - alignText="center", color = 'white') - screen_text.height = .2 + exp_win, text=self.symbol, alignText="center", color="white" + ) + screen_text.height = 0.2 for frameN in range(config.FRAME_RATE * self.duration): screen_text.draw(exp_win) diff --git a/src/tasks/things.py b/src/tasks/things.py index 72d6ee00..5cce2701 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -4,8 +4,9 @@ from ..shared import config -RESPONSE_KEY='d' -RESPONSE_TIME=4 +RESPONSE_KEY = "d" +RESPONSE_TIME = 4 + class Things(Task): @@ -13,87 +14,115 @@ class Things(Task): Press the button when you see an unrecognizable object that was generated.""" - def __init__(self, design, images_path, run, *args,**kwargs): + def __init__(self, design, images_path, run, *args, **kwargs): super().__init__(**kwargs) - #TODO: image lists as params, subjects .... + # TODO: image lists as params, subjects .... design = data.importConditions(design) - self.design = [trial for trial in design if trial['run'] == run] - if os.path.exists(images_path) and os.path.exists(os.path.join(images_path, self.design[0]['image_path'])): + self.design = [trial for trial in design if trial["run"] == run] + if os.path.exists(images_path) and os.path.exists( + os.path.join(images_path, self.design[0]["image_path"]) + ): self.images_path = images_path else: - raise ValueError('Cannot find the listed images in %s '%images_path) + raise ValueError("Cannot find the listed images in %s " % images_path) def _setup(self, exp_win): self.fixation_cross = visual.ImageStim( exp_win, - os.path.join('data','things', 'images', 'fixation_cross.png'), - size=(.1,.1), units='height', opacity=.5) + os.path.join("data", "things", "images", "fixation_cross.png"), + size=(0.1, 0.1), + units="height", + opacity=0.5, + ) - #preload all images + # preload all images for trial in self.design: - trial['stim'] = visual.ImageStim( - exp_win, - os.path.join(self.images_path, trial['image_path'])) - self.trials = data.TrialHandler(self.design, 1, method='sequential') + trial["stim"] = visual.ImageStim( + exp_win, os.path.join(self.images_path, trial["image_path"]) + ) + self.trials = data.TrialHandler(self.design, 1, method="sequential") self.duration = len(self.design) - self._progress_bar_refresh_rate = 2 # 2 flips per trial + self._progress_bar_refresh_rate = 2 # 2 flips per trial super()._setup(exp_win) def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( - exp_win, text=self.instruction, - alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + exp_win, + text=self.instruction, + alignText="center", + color="white", + wrapWidth=config.WRAP_WIDTH, + ) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): screen_text.draw(exp_win) if ctl_win: screen_text.draw(ctl_win) - yield() + yield () def _run(self, exp_win, ctl_win): - exp_win.logOnFlip(level=logging.EXP, msg='Things: task starting at %f'%time.time()) + exp_win.logOnFlip( + level=logging.EXP, msg="Things: task starting at %f" % time.time() + ) self.fixation_cross.draw(exp_win) if ctl_win: self.fixation_cross.draw(ctl_win) yield True for trial_n, trial in enumerate(self.trials): - exp_win.logOnFlip(level=logging.EXP, msg=f"image: {trial['condition']}:{trial['image_path']}") - self.progress_bar.set_description(f"Trial {trial_n}:: {trial['condition']}:{trial['image_path']}" ) + exp_win.logOnFlip( + level=logging.EXP, + msg=f"image: {trial['condition']}:{trial['image_path']}", + ) + self.progress_bar.set_description( + f"Trial {trial_n}:: {trial['condition']}:{trial['image_path']}" + ) # draw to backbuffer - trial['stim'].draw(exp_win) + trial["stim"].draw(exp_win) self.fixation_cross.draw(exp_win) if ctl_win: - trial['stim'].draw(ctl_win) + trial["stim"].draw(ctl_win) self.fixation_cross.draw(ctl_win) # wait onset - while self.task_timer.getTime() < trial['onset'] - 1/config.FRAME_RATE: - time.sleep(.0005) #just to avoid looping to fast - yield True #flip - trial['onset_flip'] = self._exp_win_last_flip_time - self._exp_win_first_flip_time + while self.task_timer.getTime() < trial["onset"] - 1 / config.FRAME_RATE: + time.sleep(0.0005) # just to avoid looping to fast + yield True # flip + trial["onset_flip"] = ( + self._exp_win_last_flip_time - self._exp_win_first_flip_time + ) # draw to backbuffer - exp_win.logOnFlip(level=logging.EXP, msg='fixation') + exp_win.logOnFlip(level=logging.EXP, msg="fixation") self.fixation_cross.draw(exp_win) if ctl_win: self.fixation_cross.draw(ctl_win) - while self.task_timer.getTime() < trial['onset'] + trial['duration'] - 1/config.FRAME_RATE: - time.sleep(.0005) #just to avoid looping to fast - yield True #flip - trial['offset_flip'] = self._exp_win_last_flip_time - self._exp_win_first_flip_time - - while self.task_timer.getTime() < trial['onset'] + RESPONSE_TIME - 1/config.FRAME_RATE: - time.sleep(.0005) #just to avoid looping to fast + while ( + self.task_timer.getTime() + < trial["onset"] + trial["duration"] - 1 / config.FRAME_RATE + ): + time.sleep(0.0005) # just to avoid looping to fast + yield True # flip + trial["offset_flip"] = ( + self._exp_win_last_flip_time - self._exp_win_first_flip_time + ) + + while ( + self.task_timer.getTime() + < trial["onset"] + RESPONSE_TIME - 1 / config.FRAME_RATE + ): + time.sleep(0.0005) # just to avoid looping to fast keypress = event.getKeys([RESPONSE_KEY], timeStamped=self.task_timer) - trial['response'] = len(keypress) > 0 - trial['response_time'] = (keypress[0][1] - trial['onset']) if len(keypress) else None - trial['duration_flip'] = trial['offset_flip']-trial['onset_flip'] - del trial['stim'] - - while self.task_timer.getTime() < trial['onset'] + RESPONSE_TIME: - time.sleep(.0005) + trial["response"] = len(keypress) > 0 + trial["response_time"] = ( + (keypress[0][1] - trial["onset"]) if len(keypress) else None + ) + trial["duration_flip"] = trial["offset_flip"] - trial["onset_flip"] + del trial["stim"] + + while self.task_timer.getTime() < trial["onset"] + RESPONSE_TIME: + time.sleep(0.0005) def _save(self): self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) diff --git a/src/tasks/video.py b/src/tasks/video.py index 22013dea..dffa77b2 100644 --- a/src/tasks/video.py +++ b/src/tasks/video.py @@ -7,26 +7,33 @@ FADE_TO_GREY_DURATION = 2 + class SingleVideo(Task): DEFAULT_INSTRUCTION = """You are about to watch a video. Please keep your eyes open.""" - def __init__(self, filepath, *args,**kwargs): - self._aspect_ratio = kwargs.pop('aspect_ratio', None) - self._scaling = kwargs.pop('scaling', None) + def __init__(self, filepath, *args, **kwargs): + self._aspect_ratio = kwargs.pop("aspect_ratio", None) + self._scaling = kwargs.pop("scaling", None) super().__init__(**kwargs) self.filepath = filepath if not os.path.exists(self.filepath): - raise ValueError('File %s does not exists'%self.filepath) + raise ValueError("File %s does not exists" % self.filepath) def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( - exp_win, text=self.instruction, - alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + exp_win, + text=self.instruction, + alignText="center", + color="white", + wrapWidth=config.WRAP_WIDTH, + ) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): - grey = [-float(frameN)/config.FRAME_RATE/config.INSTRUCTION_DURATION]*3 + grey = [ + -float(frameN) / config.FRAME_RATE / config.INSTRUCTION_DURATION + ] * 3 exp_win.setColor(grey) screen_text.draw(exp_win) if ctl_win: @@ -36,16 +43,18 @@ def _instructions(self, exp_win, ctl_win): def _setup(self, exp_win): - self.movie_stim = visual.MovieStim2(exp_win, self.filepath, units='pixels') - #print(self.movie_stim._audioStream.__class__) - aspect_ratio = self._aspect_ratio or self.movie_stim.size[0]/self.movie_stim.size[1] - min_ratio = min( - exp_win.size[0]/ self.movie_stim.size[0], - exp_win.size[1]/ self.movie_stim.size[0]*aspect_ratio) - + self.movie_stim = visual.MovieStim2(exp_win, self.filepath, units="pixels") + # print(self.movie_stim._audioStream.__class__) + aspect_ratio = ( + self._aspect_ratio or self.movie_stim.size[0] / self.movie_stim.size[1] + ) + min_ratio = min( + exp_win.size[0] / self.movie_stim.size[0], + exp_win.size[1] / self.movie_stim.size[0] * aspect_ratio, + ) - width = min_ratio*self.movie_stim.size[0] - height = min_ratio*self.movie_stim.size[0]/aspect_ratio + width = min_ratio * self.movie_stim.size[0] + height = min_ratio * self.movie_stim.size[0] / aspect_ratio if self._scaling is not None: width *= self._scaling @@ -53,16 +62,16 @@ def _setup(self, exp_win): self.movie_stim.size = (width, height) self.duration = self.movie_stim.duration -# print(self.movie_stim.size) -# print(self.movie_stim.duration) + # print(self.movie_stim.size) + # print(self.movie_stim.duration) super()._setup(exp_win) def _run(self, exp_win, ctl_win): # give the original size of the movie in pixels: - #print(self.movie_stim.format.width, self.movie_stim.format.height) + # print(self.movie_stim.format.width, self.movie_stim.format.height) exp_win.logOnFlip( - level=logging.EXP, - msg='video: task starting at %f'%time.time()) + level=logging.EXP, msg="video: task starting at %f" % time.time() + ) self.movie_stim.play() while self.movie_stim.status != visual.FINISHED: self.movie_stim.draw(exp_win) @@ -70,11 +79,10 @@ def _run(self, exp_win, ctl_win): self.movie_stim.draw(ctl_win) yield False - def _stop(self, exp_win, ctl_win): self.movie_stim.stop() for frameN in range(config.FRAME_RATE * FADE_TO_GREY_DURATION): - grey = [float(frameN)/config.FRAME_RATE/FADE_TO_GREY_DURATION-1]*3 + grey = [float(frameN) / config.FRAME_RATE / FADE_TO_GREY_DURATION - 1] * 3 exp_win.setColor(grey) if ctl_win: ctl_win.setColor(grey) @@ -86,13 +94,13 @@ def _restart(self): def unload(self): del self.movie_stim + class VideoAudioCheckLoop(SingleVideo): DEFAULT_INSTRUCTION = """We are setting up for the MRI session. Make yourself comfortable. We will play your personalized video so that you can ensure you can see the full screen and that the image is sharp.""" - def _setup(self, exp_win): super()._setup(exp_win) # set infinite loop for setup, need to be skipped diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index a499011a..7ccfa692 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -9,36 +9,38 @@ import retro -DEFAULT_GAME_NAME = 'ShinobiIIIReturnOfTheNinjaMaster-Genesis' +DEFAULT_GAME_NAME = "ShinobiIIIReturnOfTheNinjaMaster-Genesis" -#KEY_SET = 'zx__abudlr_y' -#KEY_SET = 'zx__udlry___' -#KEY_SET = ['a','b','c','d','up','down','left','right','x','y','z','k'] -#KEY_SET = ['x','z','_','_','up','down','left','right','c','_','_','_'] -KEY_SET = ['y','a','_','_','u','d','l','r','b','_','_','_'] +# KEY_SET = 'zx__abudlr_y' +# KEY_SET = 'zx__udlry___' +# KEY_SET = ['a','b','c','d','up','down','left','right','x','y','z','k'] +# KEY_SET = ['x','z','_','_','up','down','left','right','c','_','_','_'] +KEY_SET = ["y", "a", "_", "_", "u", "d", "l", "r", "b", "_", "_", "_"] -#KEY_SET = '0123456789' +# KEY_SET = '0123456789' _keyPressBuffer = [] _keyReleaseBuffer = [] import pyglet + def _onPygletKeyPress(symbol, modifier): if modifier: event._onPygletKey(symbol, modifier) global _keyPressBuffer keyTime = core.getTime() - key = pyglet.window.key.symbol_string(symbol).lower().lstrip('_').lstrip('NUM_') + key = pyglet.window.key.symbol_string(symbol).lower().lstrip("_").lstrip("NUM_") _keyPressBuffer.append((key, keyTime)) + def _onPygletKeyRelease(symbol, modifier): global _keyReleaseBuffer keyTime = core.getTime() - key = pyglet.window.key.symbol_string(symbol).lower().lstrip('_').lstrip('NUM_') + key = pyglet.window.key.symbol_string(symbol).lower().lstrip("_").lstrip("NUM_") _keyReleaseBuffer.append((key, keyTime)) -class SoundDeviceBlockStream(sound.backend_sounddevice.SoundDeviceSound): +class SoundDeviceBlockStream(sound.backend_sounddevice.SoundDeviceSound): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.blocks = queue.Queue() @@ -56,68 +58,73 @@ def _nextBlock(self): if self.status == constants.STOPPED: return if self.blocks.empty(): - block = np.zeros((self.blockSize,2),dtype=np.float) + block = np.zeros((self.blockSize, 2), dtype=np.float) else: with self.lock: block = self.blocks.get() - self.t += self.blockSize/float(self.sampleRate) + self.t += self.blockSize / float(self.sampleRate) return block -class VideoGameBase(Task): - +class VideoGameBase(Task): def _setup(self, exp_win): self.game_sound = SoundDeviceBlockStream(stereo=True, blockSize=735) self._first_frame = self.emulator.reset() - min_ratio = min( - exp_win.size[0]/ self._first_frame.shape[1], - exp_win.size[1]/ self._first_frame.shape[0]) - width = int(min_ratio*self._first_frame.shape[1]) - height = int(min_ratio*self._first_frame.shape[0]) + min_ratio = min( + exp_win.size[0] / self._first_frame.shape[1], + exp_win.size[1] / self._first_frame.shape[0], + ) + width = int(min_ratio * self._first_frame.shape[1]) + height = int(min_ratio * self._first_frame.shape[0]) self.game_vis_stim = visual.ImageStim( exp_win, size=(width, height), - units='pixels', + units="pixels", interpolate=False, flipVert=True, - autoLog=False) + autoLog=False, + ) def _transform_soundblock(self, sound_block): - return sound_block[:735]/float(2**15) + return sound_block[:735] / float(2 ** 15) def _render_graphics_sound(self, obs, sound_block, exp_win, ctl_win): - self.game_vis_stim.image = obs/255. #np.flip(obs, 0)/255. + self.game_vis_stim.image = obs / 255.0 # np.flip(obs, 0)/255. self.game_vis_stim.draw(exp_win) if ctl_win: self.game_vis_stim.draw(ctl_win) self.game_sound.add_block(self._transform_soundblock(sound_block)) if not self.game_sound.status == constants.PLAYING: - exp_win.callOnFlip(self.game_sound.play) # start sound only at flip + exp_win.callOnFlip(self.game_sound.play) # start sound only at flip def _stop(self, exp_win, ctl_win): self.game_sound.stop() - exp_win.setColor([0]*3) + exp_win.setColor([0] * 3) if ctl_win: - ctl_win.setColor([0]*3) + ctl_win.setColor([0] * 3) yield True def unload(self): self.emulator.close() + class VideoGame(VideoGameBase): DEFAULT_INSTRUCTION = "Let's play a video game.\n%s: %s\nHave fun!" - def __init__(self, + def __init__( + self, game_name=DEFAULT_GAME_NAME, state_name=None, scenario=None, repeat_scenario=True, max_duration=0, post_level_ratings=None, - *args,**kwargs): + *args, + **kwargs + ): super().__init__(**kwargs) self.game_name = game_name @@ -130,11 +137,15 @@ def __init__(self, def _instructions(self, exp_win, ctl_win): - instruction = self.instruction%(self.game_name, self.state_name) + instruction = self.instruction % (self.game_name, self.state_name) screen_text = visual.TextStim( - exp_win, text=instruction, - alignText="center", color = 'white', wrapWidth=config.WRAP_WIDTH) + exp_win, + text=instruction, + alignText="center", + color="white", + wrapWidth=config.WRAP_WIDTH, + ) for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): screen_text.draw(exp_win) @@ -146,37 +157,36 @@ def _instructions(self, exp_win, ctl_win): def _setup(self, exp_win): self.emulator = retro.make( - self.game_name, - state=self.state_name, - scenario=self.scenario, - record=False) + self.game_name, state=self.state_name, scenario=self.scenario, record=False + ) super()._setup(exp_win) self._set_recording_file() self._set_key_handler(exp_win) - def _set_recording_file(self): nnn = 0 while True: self.movie_path = os.path.join( self.output_path, - "%s_%s_%s_%03d.bk2"%(self.output_fname_base, self.game_name, self.state_name, nnn)) + "%s_%s_%s_%03d.bk2" + % (self.output_fname_base, self.game_name, self.state_name, nnn), + ) if not os.path.exists(self.movie_path): break nnn += 1 - logging.exp('VideoGame: recording movie in %s'%self.movie_path) + logging.exp("VideoGame: recording movie in %s" % self.movie_path) self.emulator.record_movie(self.movie_path) def _handle_controller_presses(self): global _keyPressBuffer, _keyReleaseBuffer for k in _keyReleaseBuffer: - #print('release',k) + # print('release',k) self.pressed_keys.discard(k[0]) _keyReleaseBuffer.clear() for k in _keyPressBuffer: - #print('press',k) + # print('press',k) self.pressed_keys.add(k[0]) _keyPressBuffer.clear() return self.pressed_keys @@ -192,21 +202,25 @@ def _run_emulator(self, exp_win, ctl_win): total_reward = 0 _done = False level_step = 0 - keys = [False]*12 + keys = [False] * 12 # flush all keys to avoid unwanted actions self.clear_key_buffers() # render the initial frame and audio - self._render_graphics_sound(self._first_frame, self.emulator.em.get_audio(), exp_win, ctl_win) - exp_win.logOnFlip(level=logging.EXP, msg="level step: %d"%level_step) - exp_win.callOnFlip(self._log_event, + self._render_graphics_sound( + self._first_frame, self.emulator.em.get_audio(), exp_win, ctl_win + ) + exp_win.logOnFlip(level=logging.EXP, msg="level step: %d" % level_step) + exp_win.callOnFlip( + self._log_event, { - 'trial_type': 'gym-retro_game', - 'game': self.game_name, - 'level': self.state_name, - 'stim_file': self.movie_path, - }) + "trial_type": "gym-retro_game", + "game": self.game_name, + "level": self.state_name, + "stim_file": self.movie_path, + }, + ) yield True while not _done: level_step += 1 @@ -214,15 +228,19 @@ def _run_emulator(self, exp_win, ctl_win): keys = [k in self.pressed_keys for k in KEY_SET] _obs, _rew, _done, _info = self.emulator.step(keys) total_reward += _rew - if _rew > 0 : - exp_win.logOnFlip(level=logging.EXP, msg='Reward %f'%(total_reward)) - self._render_graphics_sound(_obs, self.emulator.em.get_audio(), exp_win, ctl_win) + if _rew > 0: + exp_win.logOnFlip(level=logging.EXP, msg="Reward %f" % (total_reward)) + self._render_graphics_sound( + _obs, self.emulator.em.get_audio(), exp_win, ctl_win + ) if _done: exp_win.logOnFlip( level=logging.EXP, - msg='VideoGame %s: %s stopped at %f'%(self.game_name, self.state_name, time.time())) - if not level_step%config.FRAME_RATE: - exp_win.logOnFlip(level=logging.EXP, msg="level step: %d"%level_step) + msg="VideoGame %s: %s stopped at %f" + % (self.game_name, self.state_name, time.time()), + ) + if not level_step % config.FRAME_RATE: + exp_win.logOnFlip(level=logging.EXP, msg="level step: %d" % level_step) yield True self.game_sound.stop() self.game_sound.flush() @@ -236,42 +254,44 @@ def _set_key_handler(self, exp_win): def _unset_key_handler(self, exp_win): # deactivate custom keys handling exp_win.winHandle.on_key_press = event._onPygletKey - #del exp_win.winHandle.on_key_release + # del exp_win.winHandle.on_key_release def _run(self, exp_win, ctl_win): self._set_key_handler(exp_win) self._nlevels = 0 - exp_win.setColor([-1.0]*3) + exp_win.setColor([-1.0] * 3) if ctl_win: - ctl_win.setColor([-1.0]*3) + ctl_win.setColor([-1.0] * 3) while True: self._nlevels += 1 exp_win.logOnFlip( level=logging.EXP, - msg='VideoGame %s: %s starting at %f'%(self.game_name, self.state_name, time.time())) + msg="VideoGame %s: %s starting at %f" + % (self.game_name, self.state_name, time.time()), + ) yield from self._run_emulator(exp_win, ctl_win) if self.post_level_ratings: yield from self._run_ratings(exp_win, ctl_win) - if not self.repeat_scenario or \ - (self.max_duration and - self.task_timer.getTime() > self.max_duration): # stop if we are above the planned duration + if not self.repeat_scenario or ( + self.max_duration and self.task_timer.getTime() > self.max_duration + ): # stop if we are above the planned duration break self.emulator.reset() - exp_win.setColor([0]*3) + exp_win.setColor([0] * 3) if ctl_win: - ctl_win.setColor([0]*3) + ctl_win.setColor([0] * 3) def _run_ratings(self, exp_win, ctl_win): for question, n_pts in self.post_level_ratings: yield from self._likert_scale_answer(exp_win, ctl_win, question, n_pts) - text = visual.TextStim(exp_win, 'Thanks for your answers', pos=(0, 0)) + text = visual.TextStim(exp_win, "Thanks for your answers", pos=(0, 0)) for i in range(config.FRAME_RATE): text.draw(exp_win) - yield i<3 + yield i < 3 # clear screen for i in range(2): yield True @@ -284,94 +304,125 @@ def _questionnaire(self, exp_win, ctl_win): responses = [] y_spacing = 40 win_width = exp_win.size[0] - scales_block_x = win_width*.25 - scales_block_y = -len(self.post_level_ratings)/2 * y_spacing - extent = win_width * .2 - + scales_block_x = win_width * 0.25 + scales_block_y = -len(self.post_level_ratings) / 2 * y_spacing + extent = win_width * 0.2 # create all stimuli all_questions_text = "" for q_n, q_vals in enumerate(self.post_level_ratings): question, n_pts = q_vals print(question) - default_response = n_pts//2 + default_response = n_pts // 2 responses.append(default_response) - x_spacing = extent*2/(n_pts-1) - all_questions_text += question + '\n\n' + x_spacing = extent * 2 / (n_pts - 1) + all_questions_text += question + "\n\n" lines.append( visual.Line( exp_win, - (scales_block_x-extent, scales_block_y+q_n*y_spacing), (scales_block_x+extent, scales_block_y+q_n*y_spacing), - units='pixels', lineWidth=2, autoLog=False, - lineColor=((0,-1,-1) if q_n==0 else(-1,-1,-1)) - ) + (scales_block_x - extent, scales_block_y + q_n * y_spacing), + (scales_block_x + extent, scales_block_y + q_n * y_spacing), + units="pixels", + lineWidth=2, + autoLog=False, + lineColor=((0, -1, -1) if q_n == 0 else (-1, -1, -1)), + ) ) - bullets.append([ - visual.Circle( - exp_win, - units='pixels', - radius=10, pos=(scales_block_x-extent+i*x_spacing, scales_block_y+q_n*y_spacing), - fillColor=((1,1,1) if default_response==i else (-1,-1,-1)), - lineColor=(-1,-1,-1), lineWidth=10, - autoLog=False) + bullets.append( + [ + visual.Circle( + exp_win, + units="pixels", + radius=10, + pos=( + scales_block_x - extent + i * x_spacing, + scales_block_y + q_n * y_spacing, + ), + fillColor=( + (1, 1, 1) if default_response == i else (-1, -1, -1) + ), + lineColor=(-1, -1, -1), + lineWidth=10, + autoLog=False, + ) for i in range(n_pts) - ]) + ] + ) text = visual.TextStim( - exp_win, all_questions_text, units='pixels', - pos=(.1*win_width, 0), wrapWidth=win_width*.5, - height = y_spacing/2, - anchorHoriz='right') + exp_win, + all_questions_text, + units="pixels", + pos=(0.1 * win_width, 0), + wrapWidth=win_width * 0.5, + height=y_spacing / 2, + anchorHoriz="right", + ) # questionnaire interaction loop while True: - for stim in lines + sum(bullets,[]) + [text]: + for stim in lines + sum(bullets, []) + [text]: stim.draw(exp_win) yield True - def _likert_scale_answer(self, exp_win, ctl_win, question, n_pts=7, extent=.6, autoLog=False): - extent *= config.EXP_WINDOW['size'][0] - value = n_pts//2 + def _likert_scale_answer( + self, exp_win, ctl_win, question, n_pts=7, extent=0.6, autoLog=False + ): + extent *= config.EXP_WINDOW["size"][0] + value = n_pts // 2 answered = False - text = visual.TextStim(exp_win, question, pos=(0, .5)) - line = visual.Line(exp_win, (-extent, 0), (extent, 0), units='pixels', lineWidth=2, autoLog=False) - x_spacing = extent*2/(n_pts-1) + text = visual.TextStim(exp_win, question, pos=(0, 0.5)) + line = visual.Line( + exp_win, + (-extent, 0), + (extent, 0), + units="pixels", + lineWidth=2, + autoLog=False, + ) + x_spacing = extent * 2 / (n_pts - 1) circles = [ visual.Circle( exp_win, - units='pixels', - radius=40, pos=(-extent+i*x_spacing, 0), - fillColor=(-1,-1,-1), lineColor=(-1,-1,-1), lineWidth=10, - autoLog=False) - for i in range(n_pts) - ] - circles[value].fillColor = (1,1,1) + units="pixels", + radius=40, + pos=(-extent + i * x_spacing, 0), + fillColor=(-1, -1, -1), + lineColor=(-1, -1, -1), + lineWidth=10, + autoLog=False, + ) + for i in range(n_pts) + ] + circles[value].fillColor = (1, 1, 1) frame = 0 while not answered: frame += 1 for stim in [text, line] + circles: stim.draw(exp_win) self._handle_controller_presses() - if 'a' in self.pressed_keys: + if "a" in self.pressed_keys: exp_win.logOnFlip( level=logging.EXP, - msg='nlevel: %d, question: %s, answer: %d'%(self._nlevels, question, value)) + msg="nlevel: %d, question: %s, answer: %d" + % (self._nlevels, question, value), + ) for i in range(config.FRAME_RATE): yield True self.pressed_keys.clear() break - if 'r' in self.pressed_keys and value < n_pts-1: + if "r" in self.pressed_keys and value < n_pts - 1: value += 1 - elif 'l' in self.pressed_keys and value > 0: + elif "l" in self.pressed_keys and value > 0: value -= 1 else: yield frame < 4 continue self.pressed_keys.clear() for c in circles: - c.fillColor = (-1,-1,-1) - circles[value].fillColor = (1,1,1) + c.fillColor = (-1, -1, -1) + circles[value].fillColor = (1, 1, 1) yield True yield True @@ -381,27 +432,26 @@ def _stop(self, exp_win, ctl_win): exp_win.waitBlanking = True yield from super()._stop(exp_win, ctl_win) -class VideoGameMultiLevel(VideoGame): - def __init__(self, *args,**kwargs): +class VideoGameMultiLevel(VideoGame): + def __init__(self, *args, **kwargs): - self._state_names = kwargs.pop('state_names') - self._scenarii = kwargs.pop('scenarii') - self._repeat_scenario_multilevel = kwargs.get('repeat_scenario', False) + self._state_names = kwargs.pop("state_names") + self._scenarii = kwargs.pop("scenarii") + self._repeat_scenario_multilevel = kwargs.get("repeat_scenario", False) - kwargs['repeat_scenario'] = False + kwargs["repeat_scenario"] = False super().__init__( - state_name = self._state_names[0], - scenario=self._scenarii[0], - **kwargs) + state_name=self._state_names[0], scenario=self._scenarii[0], **kwargs + ) def _run(self, exp_win, ctl_win): exp_win.waitBlanking = False - exp_win.setColor([-1.0]*3) + exp_win.setColor([-1.0] * 3) if ctl_win: - ctl_win.setColor([-1.0]*3) + ctl_win.setColor([-1.0] * 3) self._nlevels = 0 while True: @@ -410,42 +460,53 @@ def _run(self, exp_win, ctl_win): self.state_name = level self.emulator.load_state(level) self.emulator.data.load( - retro.data.get_file_path(self.game_name, 'data.json'), - scenario) + retro.data.get_file_path(self.game_name, "data.json"), scenario + ) self._first_frame = self.emulator.reset() if self._nlevels > 1: self._set_recording_file() yield from self._instructions(exp_win, ctl_win) - yield from self._questionnaire(exp_win, ctl_win) # here for tests, to move after _run_emulator + yield from self._questionnaire( + exp_win, ctl_win + ) # here for tests, to move after _run_emulator yield from super()._run_emulator(exp_win, ctl_win) self.game_sound.stop() if self.post_level_ratings: yield from self._run_ratings(exp_win, ctl_win) - time_exceeded = self.max_duration and self.task_timer.getTime() > self.max_duration - if time_exceeded: # stop if we are above the planned duration + time_exceeded = ( + self.max_duration and self.task_timer.getTime() > self.max_duration + ) + if time_exceeded: # stop if we are above the planned duration break if time_exceeded or not self._repeat_scenario_multilevel: break exp_win.waitBlanking = True -class VideoGameReplay(VideoGameBase): - def __init__(self, movie_filename, game_name=DEFAULT_GAME_NAME, scenario=None, *args, **kwargs): +class VideoGameReplay(VideoGameBase): + def __init__( + self, + movie_filename, + game_name=DEFAULT_GAME_NAME, + scenario=None, + *args, + **kwargs + ): super().__init__(**kwargs) self.game_name = game_name self.scenario = scenario self.movie_filename = movie_filename if not os.path.exists(self.movie_filename): - raise ValueError('file %s does not exists'%self.movie_filename) + raise ValueError("file %s does not exists" % self.movie_filename) def instructions(self, exp_win, ctl_win): - instruction_text = "You are going to watch someone play %s."%self.game_name + instruction_text = "You are going to watch someone play %s." % self.game_name screen_text = visual.TextStim( - exp_win, text=instruction_text, - alignText="center", color = 'white') + exp_win, text=instruction_text, alignText="center", color="white" + ) for frameN in range(config.FRAME_RATE * INSTRUCTION_DURATION): screen_text.draw(exp_win) @@ -461,20 +522,21 @@ def _setup(self, exp_win): record=False, state=retro.State.NONE, scenario=self.scenario, - #use_restricted_actions=retro.Actions.ALL, - players=self.movie.players) + # use_restricted_actions=retro.Actions.ALL, + players=self.movie.players, + ) self.emulator.initial_state = self.movie.get_state() super()._setup(exp_win) - def _run(self, exp_win, ctl_win): # give the original size of the movie in pixels: - #print(self.movie_stim.format.width, self.movie_stim.format.height) + # print(self.movie_stim.format.width, self.movie_stim.format.height) total_reward = 0 exp_win.logOnFlip( level=logging.EXP, - msg='VideoGameReplay %s starting at %f'%(self.game_name, time.time())) + msg="VideoGameReplay %s starting at %f" % (self.game_name, time.time()), + ) while self.movie.step(): keys = [] for p in range(self.movie.players): @@ -484,8 +546,10 @@ def _run(self, exp_win, ctl_win): _obs, _rew, _done, _info = self.emulator.step(keys) total_reward += _rew - if _rew > 0 : - exp_win.logOnFlip(level=logging.EXP, msg='Reward %f'%(total_reward)) + if _rew > 0: + exp_win.logOnFlip(level=logging.EXP, msg="Reward %f" % (total_reward)) - self._render_graphics_sound(_obs,self.emulator.em.get_audio(), exp_win, ctl_win) + self._render_graphics_sound( + _obs, self.emulator.em.get_audio(), exp_win, ctl_win + ) yield From bcd4403212564f12bfc332ae069de3ebba25ff27 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 11 Dec 2020 09:46:35 -0500 Subject: [PATCH 078/170] calibrate et for each task --- src/sessions/ses-liris.py | 1 + src/shared/cli.py | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sessions/ses-liris.py b/src/sessions/ses-liris.py index 03031b7f..3044398e 100644 --- a/src/sessions/ses-liris.py +++ b/src/sessions/ses-liris.py @@ -22,6 +22,7 @@ def get_tasks(parsed): f"data/liris/videos/{idx:03d}.mp4", name=f"task-liris{idx:03d}" ) ) + continue tasks.append( task_base.Pause( """The video is finished. diff --git a/src/shared/cli.py b/src/shared/cli.py index 7ef48db2..230552e2 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -160,12 +160,11 @@ def main_loop( print("starting et client") eyetracker_client.start() print("done") - all_tasks.insert( - 0, + all_tasks = sum(([ eyetracking.EyetrackerCalibration( eyetracker_client, name="EyeTracker-Calibration" - ), - ) + ), t] for t in all_tasks), []) + if show_ctl_win: gaze_drawer = eyetracking.GazeDrawer(ctl_win) if use_fmri: From c269a4949c24643dacb4834cc824ce061908240d Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 11 Dec 2020 13:30:53 -0500 Subject: [PATCH 079/170] remove waitBlanking for exp_win, to allow non-slip timing --- src/shared/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/config.py b/src/shared/config.py index 926452b3..d77f4323 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -15,7 +15,7 @@ screen=1, fullscr=True, gammaErrorPolicy="warn", - waitBlanking=False, + #waitBlanking=False, ) CTL_WINDOW = dict( From e6daccb2c1858b827ee25e1be5bea022532de875 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 11 Dec 2020 14:25:54 -0500 Subject: [PATCH 080/170] fix pupil accessor --- src/shared/eyetracking.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index 46f01fe7..4dc22f03 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -209,8 +209,8 @@ def __init__(self, output_path, output_fname_base, profile=False, debug=False): self.stoprequest = threading.Event() self.lock = threading.Lock() - self.pupils = [] - self.gazes = [] + self.pupil = None + self.gaze = None self.unset_pupil_cb() self.output_path = output_path @@ -345,11 +345,11 @@ def run(self): topic, tmp = msg with self.lock: if topic.startswith("pupil"): - self.pupils.append(tmp) + self.pupil = tmp if self._pupil_cb: self._pupil_cb(tmp) elif topic.startswith("gaze"): - self.gazes.append(tmp) + self.gaze = tmp time.sleep(1 / 120.0) logging.info("eyetracker listener: stopping") From 6ae2a96805627e0b8888ad557871ff7e5d247f22 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 12 Jan 2021 09:09:37 -0500 Subject: [PATCH 081/170] fix eyetracking issues --- src/shared/eyetracking.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index 4dc22f03..182c9185 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -45,7 +45,7 @@ "frame_rate": 250, "exposure_time": 4000, "global_gain": 1, - "gev_packet_size": 2000, + "gev_packet_size": 1400, "uid": "Aravis-Fake-GV01", # for test purposes # "uid": "MRC Systems GmbH-GVRD-MRC HighSpeed-MR_CAM_HS_0014", } @@ -276,7 +276,7 @@ def __init__(self, output_path, output_fname_base, profile=False, debug=False): ) # stop a bunch of eye plugins for performance - for plugin in ["NDSI_Manager", "Detector3DPlugin"]: + for plugin in ["NDSI_Manager", "Pye3DPlugin"]: self.send_recv_notification( { "subject": "stop_eye_plugin", @@ -350,7 +350,7 @@ def run(self): self._pupil_cb(tmp) elif topic.startswith("gaze"): self.gaze = tmp - time.sleep(1 / 120.0) + time.sleep(1e-3) logging.info("eyetracker listener: stopping") def set_pupil_cb(self, pupil_cb): From e39db9ba2b02199f0b2feeae8f79c47497ebebfc Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 12 Jan 2021 09:10:34 -0500 Subject: [PATCH 082/170] log computer monotonic time with events --- src/tasks/task_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index b37866c8..4efd6d49 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -1,5 +1,6 @@ import os import tqdm +import time import pandas from psychopy import logging, visual, core, event @@ -113,7 +114,7 @@ def restart(self): self._restart() def _log_event(self, event): - event.update({"onset": self.task_timer.getTime()}) + event.update({"onset": self.task_timer.getTime(),"sample":time.monotonic()}) self._events.append(event) def _save(self): From ffc48b10aef88aa6f5b7372f8f7d6a0664955ce5 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 12 Jan 2021 09:45:29 -0500 Subject: [PATCH 083/170] fix loading of scenarii, randomize order of levels within runs --- src/sessions/ses-shinobi3levels.py | 14 ++++++++------ src/tasks/videogame.py | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/sessions/ses-shinobi3levels.py b/src/sessions/ses-shinobi3levels.py index 6d1dd0c5..e8c44e94 100644 --- a/src/sessions/ses-shinobi3levels.py +++ b/src/sessions/ses-shinobi3levels.py @@ -15,17 +15,19 @@ "I am completely lost in thought.", ] -levels = ["scenario_repeat1", "scenario_Level4-1", "scenario_Level5-0"] -random.shuffle(levels) # randomize order +levels_scenario = [ + ("Level1-0", "scenario_repeat1"), + ("Level4-1", "scenario_Level4-1"), + ("Level5-0", "scenario_Level5-0")] +random.shuffle(levels_scenario) # randomize order TASKS = sum( [ [ videogame.VideoGameMultiLevel( - state_names=["Level1-0", "Level4-1", "Level5-0"], - scenarii=[ - "data/videogames/%s.json" % sc for sc in levels - ], # this scenario repeats the same level + state_names=[l for l,s in levels_scenario], + scenarii=[s for l,s in levels_scenario] + , # this scenario repeats the same level repeat_scenario=True, max_duration=10 * 60, # if when level completed or dead we exceed that time in secs, stop the task diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 7ccfa692..6d846447 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -460,7 +460,8 @@ def _run(self, exp_win, ctl_win): self.state_name = level self.emulator.load_state(level) self.emulator.data.load( - retro.data.get_file_path(self.game_name, "data.json"), scenario + retro.data.get_file_path(self.game_name, "data.json"), + retro.data.get_file_path(self.game_name, f"{scenario}.json") ) self._first_frame = self.emulator.reset() if self._nlevels > 1: @@ -515,7 +516,6 @@ def instructions(self, exp_win, ctl_win): yield def _setup(self, exp_win): - super().setup(exp_win, output_path, output_fname_base) self.movie = retro.Movie(self.movie_filename) self.emulator = retro.make( self.game_name, From f918b42287a9e034056e648a30e2d15cf969f0d1 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 13 Jan 2021 09:01:30 -0500 Subject: [PATCH 084/170] code to create design files --- src/sessions/ses-thingsmem.py | 125 ++++++++++++++++++++++++++++++++++ src/tasks/things.py | 71 +++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 src/sessions/ses-thingsmem.py diff --git a/src/sessions/ses-thingsmem.py b/src/sessions/ses-thingsmem.py new file mode 100644 index 00000000..8db01044 --- /dev/null +++ b/src/sessions/ses-thingsmem.py @@ -0,0 +1,125 @@ +import os + +THINGS_DATA_PATH = os.path.join("data", "things") +IMAGE_PATH = os.path.join(THINGS_DATA_PATH, "images") + + +def get_tasks(parsed): + from ..tasks.things import Things + + session_design_filename = os.path.join( + THINGS_DATA_PATH, + "designs", + f"sub-{parsed.subject}_ses-{parsed.session}_design.tsv", + ) + tasks = [ + Things(session_design_filename, IMAGE_PATH, run, name=f"task-things_run-{run}") + for run in range(1, n_runs + 1) + ] + return tasks + + +# experiment + +n_sessions = 18 # number of sessions +n_runs = 12 # number of runs +n_trials = 60 # number of trials for each run +splits = n_trials * 2 + +final_wait = 9 # time to wait after last trial +initial_wait = 3 # time until first trial starts + +# trial +tr = 1.49 +trial_duration = 3*tr # mean trial duration +jitters = 0 # chosen to be a range that minimizes phase synchrony and that can be presented exactly on most screens +image_duration = 2*tr # duration of image presentation +rm_duration = 4. # duration of response mapping screen +max_rt = 4.0 # from stimulus onset + +# constraints +min_catch_spacing = 3 +max_catch_spacing = 20 + + +def generate_design_file(subject): + import pandas + import numpy as np + import random + import hashlib + + images_list = pandas.read_csv( + os.path.join(THINGS_DATA_PATH, "image_paths_fmri.csv") + ) + + images_exp = images_list[ + images_list.condition.eq("exp") & images_list.exemplar_nr < 7 + ] + + design = pandas.DataFrame() + + seed = int( + hashlib.sha1(("%s" % (subject)).encode("utf-8")).hexdigest(), 16 + ) % (2 ** 32 - 1) + print("seed", seed) + np.random.seed(seed) + + all_run_trials = pandas.DataFrame() + + # permute categories per participant + categories = np.random.permutation(720)+1 + + for session in range(n_sessions): + exemplar = session//3+1 + cat_unseen_within = categories[splits*(session%6):splits*(session%6+1)] + cat_unseen_between = categories[splits*((session+1)%6):splits*((session+1)%6+1)] + + img_unseen_within = images_exp[ + images_exp.category_nr.isin(cat_unseen_within) & + images_exp.exemplar_nr.eq(exemplar)] + img_unseen_between = images_exp[ + images_exp.category_nr.isin(cat_unseen_between) & + images_exp.exemplar_nr.eq(exemplar)] + + n_runs_session = n_runs + if session == 0: + # show twice the within set and once the between set + all_run_trials = pandas.concat( + [img_unseen_within]*2 + + [img_unseen_between]) + n_runs_session = n_runs//2 + else: + all_run_trials = pandas.concat( + [img_unseen_within]*2 + + [img_unseen_between]+ + [img_between_within]*2 + + [img_within_between] + ) + # pass to next session + img_between_within = img_unseen_between + img_within_between = img_unseen_within + + all_run_trials = all_run_trials.sample(frac=1) + all_run_trials["run"] = np.arange(1, n_runs_session+1).repeat(n_trials) + all_run_trials["onset"] = np.tile(initial_wait + np.arange(n_trials) * trial_duration, n_runs_session) + all_run_trials["duration"] = image_duration + + out_fname = os.path.join( + THINGS_DATA_PATH, + "memory_designs", + f"sub-{parsed.subject}_ses-{session+1}_design.tsv", + ) + all_run_trials.to_csv(out_fname, sep="\t", index=False) + + +if __name__ == "__main__": + import argparse + import sys + + parser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter, + description="generate design files for participant / session", + ) + parser.add_argument("subject", type=str, help="participant id") + parsed = parser.parse_args() + generate_design_file(parsed.subject) diff --git a/src/tasks/things.py b/src/tasks/things.py index 5cce2701..72127e3c 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -127,3 +127,74 @@ def _run(self, exp_win, ctl_win): def _save(self): self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) return False + + +class ThingsMemory(Task): + + + DEFAULT_INSTRUCTION = """You will see images on the screen. + +Press the buttons to indicate you confidence in having seen that image previously.""" + + def _run(self, exp_win, ctl_win): + exp_win.logOnFlip( + level=logging.EXP, msg="ThingsMemory: task starting at %f" % time.time() + ) + self.fixation_cross.draw(exp_win) + if ctl_win: + self.fixation_cross.draw(ctl_win) + yield True + + for trial_n, trial in enumerate(self.trials): + exp_win.logOnFlip( + level=logging.EXP, + msg=f"image: {trial['condition']}:{trial['image_path']}", + ) + self.progress_bar.set_description( + f"Trial {trial_n}:: {trial['condition']}:{trial['image_path']}" + ) + + # draw to backbuffer + trial["stim"].draw(exp_win) + self.fixation_cross.draw(exp_win) + if ctl_win: + trial["stim"].draw(ctl_win) + self.fixation_cross.draw(ctl_win) + # wait onset + while self.task_timer.getTime() < trial["onset"] - 1 / config.FRAME_RATE: + time.sleep(0.0005) # just to avoid looping to fast + yield True # flip + trial["onset_flip"] = ( + self._exp_win_last_flip_time - self._exp_win_first_flip_time + ) + + # draw to backbuffer + exp_win.logOnFlip(level=logging.EXP, msg="fixation") + self.fixation_cross.draw(exp_win) + if ctl_win: + self.fixation_cross.draw(ctl_win) + while ( + self.task_timer.getTime() + < trial["onset"] + trial["duration"] - 1 / config.FRAME_RATE + ): + time.sleep(0.0005) # just to avoid looping to fast + yield True # flip + trial["offset_flip"] = ( + self._exp_win_last_flip_time - self._exp_win_first_flip_time + ) + + while ( + self.task_timer.getTime() + < trial["onset"] + RESPONSE_TIME - 1 / config.FRAME_RATE + ): + time.sleep(0.0005) # just to avoid looping to fast + keypress = event.getKeys([RESPONSE_KEY], timeStamped=self.task_timer) + trial["response"] = len(keypress) > 0 + trial["response_time"] = ( + (keypress[0][1] - trial["onset"]) if len(keypress) else None + ) + trial["duration_flip"] = trial["offset_flip"] - trial["onset_flip"] + del trial["stim"] + + while self.task_timer.getTime() < trial["onset"] + RESPONSE_TIME: + time.sleep(0.0005) From 5e06869833519d51f863bcbc22a3039de1465a51 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 15 Jan 2021 12:46:55 -0500 Subject: [PATCH 085/170] complete coding of ThingsMemory task. --- src/sessions/ses-thingsmem.py | 66 +++++++++++++++++++++++------ src/tasks/things.py | 79 ++++++++++++++++++++++++++++++----- 2 files changed, 122 insertions(+), 23 deletions(-) diff --git a/src/sessions/ses-thingsmem.py b/src/sessions/ses-thingsmem.py index 8db01044..5dde0659 100644 --- a/src/sessions/ses-thingsmem.py +++ b/src/sessions/ses-thingsmem.py @@ -5,16 +5,17 @@ def get_tasks(parsed): - from ..tasks.things import Things + from ..tasks.things import ThingsMemory session_design_filename = os.path.join( THINGS_DATA_PATH, - "designs", + "memory_designs", f"sub-{parsed.subject}_ses-{parsed.session}_design.tsv", ) + n_runs_session = n_runs if int(parsed.session) > 1 else 6 tasks = [ - Things(session_design_filename, IMAGE_PATH, run, name=f"task-things_run-{run}") - for run in range(1, n_runs + 1) + ThingsMemory(session_design_filename, IMAGE_PATH, run, name=f"task-thingsmemory_run-{run}") + for run in range(1, n_runs_session + 1) ] return tasks @@ -38,8 +39,7 @@ def get_tasks(parsed): max_rt = 4.0 # from stimulus onset # constraints -min_catch_spacing = 3 -max_catch_spacing = 20 +max_seen_spacing = 8 def generate_design_file(subject): @@ -52,8 +52,9 @@ def generate_design_file(subject): os.path.join(THINGS_DATA_PATH, "image_paths_fmri.csv") ) - images_exp = images_list[ - images_list.condition.eq("exp") & images_list.exemplar_nr < 7 + images_exp = images_list.loc[ + images_list.condition.eq("exp") & + (images_list.exemplar_nr > 6 ) # > 6 for pilot < 7 for study ] design = pandas.DataFrame() @@ -70,16 +71,26 @@ def generate_design_file(subject): categories = np.random.permutation(720)+1 for session in range(n_sessions): - exemplar = session//3+1 - cat_unseen_within = categories[splits*(session%6):splits*(session%6+1)] - cat_unseen_between = categories[splits*((session+1)%6):splits*((session+1)%6+1)] + exemplar = session//3+1 + 6 # + 6 here for pilot, remove for study!! + new_stimuli_categories = categories[splits*2*(session%3):splits*2*(session%3+1)] + #randomize categories to be used within and between to avoid bias + new_stimuli_categories = np.random.permutation(new_stimuli_categories) + cat_unseen_within = new_stimuli_categories[:splits] + cat_unseen_between = new_stimuli_categories[splits:] img_unseen_within = images_exp[ images_exp.category_nr.isin(cat_unseen_within) & images_exp.exemplar_nr.eq(exemplar)] + img_unseen_within.condition = "unseen" + img_unseen_within['subcondition'] = "unseen-within" + img_unseen_within['repetition'] = 1 + img_unseen_between = images_exp[ images_exp.category_nr.isin(cat_unseen_between) & images_exp.exemplar_nr.eq(exemplar)] + img_unseen_between.condition = "unseen" + img_unseen_between['subcondition'] = "unseen-between" + img_unseen_between['repetition'] = 1 n_runs_session = n_runs if session == 0: @@ -95,19 +106,48 @@ def generate_design_file(subject): [img_between_within]*2 + [img_within_between] ) + # pass to next session img_between_within = img_unseen_between - img_within_between = img_unseen_within + img_between_within.condition = 'seen' + img_between_within.subcondition = "seen-between-within" + img_between_within.repetition = 2 + img_within_between = img_unseen_within + img_within_between.condition = 'seen' + img_within_between.subcondition = "seen-within-between" + img_within_between.repetition = 3 + + """ + if session > 0: + while True: + all_run_trials = all_run_trials.sample(frac=1) + repeated_within = all_run_trials['image_nr'].duplicated() + unseen = ~ repeated_within & all_run_trials.condition.eq('unseen') + unseen_idx = np.hstack([[0], np.where(unseen)[0], [n_trials]]) + if np.all(np.ediff1d(unseen_idx) < max_seen_spacing): + break + print('no') + else:""" + + # randomize order all_run_trials = all_run_trials.sample(frac=1) + repeated_within = all_run_trials['image_nr'].duplicated() + + # set seen condition for second viewing of new set of images + all_run_trials.loc[repeated_within, 'condition'] = 'seen' + all_run_trials.loc[repeated_within & all_run_trials.subcondition.eq('unseen-within'), 'repetition'] = 2 + all_run_trials.loc[repeated_within & all_run_trials.subcondition.eq('seen-between-within'), 'repetition'] = 3 + all_run_trials["run"] = np.arange(1, n_runs_session+1).repeat(n_trials) all_run_trials["onset"] = np.tile(initial_wait + np.arange(n_trials) * trial_duration, n_runs_session) all_run_trials["duration"] = image_duration + all_run_trials["response_mapping_flip"] = np.hstack([np.random.permutation(np.arange(2,dtype=np.bool).repeat(n_trials/2)) for i in range(n_runs_session)]) out_fname = os.path.join( THINGS_DATA_PATH, "memory_designs", - f"sub-{parsed.subject}_ses-{session+1}_design.tsv", + f"sub-{parsed.subject}_ses-{session+1:03d}_design.tsv", ) all_run_trials.to_csv(out_fname, sep="\t", index=False) diff --git a/src/tasks/things.py b/src/tasks/things.py index 72127e3c..62aa1b3e 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -18,6 +18,7 @@ def __init__(self, design, images_path, run, *args, **kwargs): super().__init__(**kwargs) # TODO: image lists as params, subjects .... design = data.importConditions(design) + self.run_id = run self.design = [trial for trial in design if trial["run"] == run] if os.path.exists(images_path) and os.path.exists( os.path.join(images_path, self.design[0]["image_path"]) @@ -129,12 +130,58 @@ def _save(self): return False -class ThingsMemory(Task): - +class ThingsMemory(Things): DEFAULT_INSTRUCTION = """You will see images on the screen. -Press the buttons to indicate you confidence in having seen that image previously.""" +Press the buttons to indicate your confidence in having seen or not that image previously. +""" + + EXTRA_INSTRUCTION = """ The response are: + surely not seen(bold red -), + not sure not seen (small red -), + not sure seen (small green +) + and surely seen (bold green +). + + +The button mapping will change from trial to trial as indicated at the center of the screen with that image. + """ + + RESPONSE_MAPPING = ['a','b','c','d'] + + def _instructions(self, exp_win, ctl_win): + screen_text = visual.TextStim( + exp_win, + text=self.instruction, + alignText="center", + color="white", + wrapWidth=config.WRAP_WIDTH, + ) + + for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): + screen_text.draw(exp_win) + if ctl_win: + screen_text.draw(ctl_win) + yield + screen_text.text = self.EXTRA_INSTRUCTION + if self.run_id == 1: + for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION * 2): + screen_text.draw(exp_win) + self._response_mapping.draw(exp_win) + if ctl_win: + screen_text.draw(ctl_win) + self._response_mapping.draw(ctl_win) + yield frameN <2 + + def _setup(self, exp_win): + super()._setup(exp_win) + + self._response_mapping = visual.ImageStim( + exp_win, + os.path.join("data", "things", "images", "response_mapping.png"), + size=(234, 50), + units="pixels", + ) def _run(self, exp_win, ctl_win): exp_win.logOnFlip( @@ -156,10 +203,12 @@ def _run(self, exp_win, ctl_win): # draw to backbuffer trial["stim"].draw(exp_win) - self.fixation_cross.draw(exp_win) + self._response_mapping.flipHoriz = trial["response_mapping_flip"] + self._response_mapping.pos = (0,0) #force update to flip + self._response_mapping.draw(exp_win) if ctl_win: trial["stim"].draw(ctl_win) - self.fixation_cross.draw(ctl_win) + self._response_mapping.draw(ctl_win) # wait onset while self.task_timer.getTime() < trial["onset"] - 1 / config.FRAME_RATE: time.sleep(0.0005) # just to avoid looping to fast @@ -188,11 +237,21 @@ def _run(self, exp_win, ctl_win): < trial["onset"] + RESPONSE_TIME - 1 / config.FRAME_RATE ): time.sleep(0.0005) # just to avoid looping to fast - keypress = event.getKeys([RESPONSE_KEY], timeStamped=self.task_timer) - trial["response"] = len(keypress) > 0 - trial["response_time"] = ( - (keypress[0][1] - trial["onset"]) if len(keypress) else None - ) + keypresses = event.getKeys(self.RESPONSE_MAPPING, timeStamped=self.task_timer) + if len(keypresses): + keypress = keypresses[0] #only take the first keypress, TODO: log extra keypresses? + key = keypress[0] + idx = self.RESPONSE_MAPPING.index(key) + # map to -2 -1 1 2 for not-seen to seen + idx = idx - 2 + int(idx > 1) + if trial["response_mapping_flip"]: + idx = - idx + trial["response"] = idx + trial["response_txt"] = "seen" if idx > 0 else "unseen" + trial["response_confidence"] = abs(idx) > 1 + trial["response_time"] = (keypress[1] - trial["onset"]) + else: + print('Warning: no response') trial["duration_flip"] = trial["offset_flip"] - trial["onset_flip"] del trial["stim"] From 76d182ec6b38a171cb9c798ede9849ecbc875a2d Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 15 Jan 2021 13:02:18 -0500 Subject: [PATCH 086/170] comment design generation --- src/sessions/ses-thingsmem.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/sessions/ses-thingsmem.py b/src/sessions/ses-thingsmem.py index 5dde0659..b0773a2f 100644 --- a/src/sessions/ses-thingsmem.py +++ b/src/sessions/ses-thingsmem.py @@ -52,6 +52,8 @@ def generate_design_file(subject): os.path.join(THINGS_DATA_PATH, "image_paths_fmri.csv") ) + # we select only the exp trial from Martin's BIG experiment + # and only the first (or second for pilot) half of the 12 exemplar of each category images_exp = images_list.loc[ images_list.condition.eq("exp") & (images_list.exemplar_nr > 6 ) # > 6 for pilot < 7 for study @@ -59,6 +61,7 @@ def generate_design_file(subject): design = pandas.DataFrame() + # seed numpy with subject id to have reproducible design generation seed = int( hashlib.sha1(("%s" % (subject)).encode("utf-8")).hexdigest(), 16 ) % (2 ** 32 - 1) @@ -71,13 +74,20 @@ def generate_design_file(subject): categories = np.random.permutation(720)+1 for session in range(n_sessions): + # select the examplar exemplar = session//3+1 + 6 # + 6 here for pilot, remove for study!! + + # subselect 240 categories for new stimuli + # loop through the 3 sets of 240 across sessions and thus avoid + # having distractors from the same category within-session + # but there will still be between session distractors new_stimuli_categories = categories[splits*2*(session%3):splits*2*(session%3+1)] - #randomize categories to be used within and between to avoid bias + #randomize categories to be used as within and between repeated new stimuli to avoid systematic bias new_stimuli_categories = np.random.permutation(new_stimuli_categories) cat_unseen_within = new_stimuli_categories[:splits] cat_unseen_between = new_stimuli_categories[splits:] + # get all the new stimuli that will be repeated within first img_unseen_within = images_exp[ images_exp.category_nr.isin(cat_unseen_within) & images_exp.exemplar_nr.eq(exemplar)] @@ -85,6 +95,7 @@ def generate_design_file(subject): img_unseen_within['subcondition'] = "unseen-within" img_unseen_within['repetition'] = 1 + # get all the new stimuli that will be repeated between first img_unseen_between = images_exp[ images_exp.category_nr.isin(cat_unseen_between) & images_exp.exemplar_nr.eq(exemplar)] @@ -107,7 +118,7 @@ def generate_design_file(subject): [img_within_between] ) - # pass to next session + # pass new "within"/"between" stimuli to the next session img_between_within = img_unseen_between img_between_within.condition = 'seen' img_between_within.subcondition = "seen-between-within" @@ -118,19 +129,7 @@ def generate_design_file(subject): img_within_between.subcondition = "seen-within-between" img_within_between.repetition = 3 - """ - if session > 0: - while True: - all_run_trials = all_run_trials.sample(frac=1) - repeated_within = all_run_trials['image_nr'].duplicated() - unseen = ~ repeated_within & all_run_trials.condition.eq('unseen') - unseen_idx = np.hstack([[0], np.where(unseen)[0], [n_trials]]) - if np.all(np.ediff1d(unseen_idx) < max_seen_spacing): - break - print('no') - else:""" - - # randomize order + # randomize order across the whole session all_run_trials = all_run_trials.sample(frac=1) repeated_within = all_run_trials['image_nr'].duplicated() @@ -139,11 +138,15 @@ def generate_design_file(subject): all_run_trials.loc[repeated_within & all_run_trials.subcondition.eq('unseen-within'), 'repetition'] = 2 all_run_trials.loc[repeated_within & all_run_trials.subcondition.eq('seen-between-within'), 'repetition'] = 3 + # split in runs all_run_trials["run"] = np.arange(1, n_runs_session+1).repeat(n_trials) + # set timing all_run_trials["onset"] = np.tile(initial_wait + np.arange(n_trials) * trial_duration, n_runs_session) all_run_trials["duration"] = image_duration + # set equal number of flipped and unflipped response mapping all_run_trials["response_mapping_flip"] = np.hstack([np.random.permutation(np.arange(2,dtype=np.bool).repeat(n_trials/2)) for i in range(n_runs_session)]) + # save a file for the whole session (will be split in runs in the task) out_fname = os.path.join( THINGS_DATA_PATH, "memory_designs", From d404f1f843024a3a6d13ef796be6927609eb81d9 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 19 Jan 2021 14:34:09 -0500 Subject: [PATCH 087/170] change to circular mapping --- src/sessions/ses-thingsmem.py | 3 +- src/tasks/things.py | 62 +++++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/sessions/ses-thingsmem.py b/src/sessions/ses-thingsmem.py index b0773a2f..577153ff 100644 --- a/src/sessions/ses-thingsmem.py +++ b/src/sessions/ses-thingsmem.py @@ -144,7 +144,8 @@ def generate_design_file(subject): all_run_trials["onset"] = np.tile(initial_wait + np.arange(n_trials) * trial_duration, n_runs_session) all_run_trials["duration"] = image_duration # set equal number of flipped and unflipped response mapping - all_run_trials["response_mapping_flip"] = np.hstack([np.random.permutation(np.arange(2,dtype=np.bool).repeat(n_trials/2)) for i in range(n_runs_session)]) + all_run_trials["response_mapping_flip_h"] = np.hstack([np.random.permutation(np.arange(2,dtype=np.bool).repeat(n_trials/2)) for i in range(n_runs_session)]) + all_run_trials["response_mapping_flip_v"] = np.hstack([np.random.permutation(np.arange(2,dtype=np.bool).repeat(n_trials/2)) for i in range(n_runs_session)]) # save a file for the whole session (will be split in runs in the task) out_fname = os.path.join( diff --git a/src/tasks/things.py b/src/tasks/things.py index 62aa1b3e..3b492bdf 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -1,6 +1,7 @@ import os, sys, time from psychopy import visual, core, data, logging, event from .task_base import Task +import numpy as np from ..shared import config @@ -31,9 +32,9 @@ def _setup(self, exp_win): self.fixation_cross = visual.ImageStim( exp_win, os.path.join("data", "things", "images", "fixation_cross.png"), - size=(0.1, 0.1), - units="height", - opacity=0.5, + size=(128,128), + units="pixels", + #opacity=0.5, ) # preload all images @@ -134,20 +135,22 @@ class ThingsMemory(Things): DEFAULT_INSTRUCTION = """You will see images on the screen. -Press the buttons to indicate your confidence in having seen or not that image previously. +Press the buttons for each image to indicate your confidence in having seen or not that image previously. """ EXTRA_INSTRUCTION = """ The response are: - surely not seen(bold red -), - not sure not seen (small red -), - not sure seen (small green +) - and surely seen (bold green +). +-- surely not seen , +- not sure not seen, ++ not sure seen, +++ and surely seen. + The button mapping will change from trial to trial as indicated at the center of the screen with that image. """ - - RESPONSE_MAPPING = ['a','b','c','d'] + RESPONSE_KEYS = ['up','right','left','down'] + RESPONSE_MAPPING = np.asarray(RESPONSE_KEYS).reshape(2,2) + RESPONSE_VALUES = np.asarray([[2,1],[-2,-1]]) def _instructions(self, exp_win, ctl_win): screen_text = visual.TextStim( @@ -178,8 +181,8 @@ def _setup(self, exp_win): self._response_mapping = visual.ImageStim( exp_win, - os.path.join("data", "things", "images", "response_mapping.png"), - size=(234, 50), + os.path.join("data", "things", "images", "response_mapping3.png"), + size=(128, 128), units="pixels", ) @@ -203,7 +206,8 @@ def _run(self, exp_win, ctl_win): # draw to backbuffer trial["stim"].draw(exp_win) - self._response_mapping.flipHoriz = trial["response_mapping_flip"] + self._response_mapping.flipHoriz = trial["response_mapping_flip_h"] + self._response_mapping.flipVert = trial["response_mapping_flip_v"] self._response_mapping.pos = (0,0) #force update to flip self._response_mapping.draw(exp_win) if ctl_win: @@ -212,6 +216,7 @@ def _run(self, exp_win, ctl_win): # wait onset while self.task_timer.getTime() < trial["onset"] - 1 / config.FRAME_RATE: time.sleep(0.0005) # just to avoid looping to fast + keypresses = event.getKeys(self.RESPONSE_KEYS) # flush response keys yield True # flip trial["onset_flip"] = ( self._exp_win_last_flip_time - self._exp_win_first_flip_time @@ -237,20 +242,27 @@ def _run(self, exp_win, ctl_win): < trial["onset"] + RESPONSE_TIME - 1 / config.FRAME_RATE ): time.sleep(0.0005) # just to avoid looping to fast - keypresses = event.getKeys(self.RESPONSE_MAPPING, timeStamped=self.task_timer) + keypresses = event.getKeys(self.RESPONSE_KEYS, timeStamped=self.task_timer) if len(keypresses): - keypress = keypresses[0] #only take the first keypress, TODO: log extra keypresses? - key = keypress[0] - idx = self.RESPONSE_MAPPING.index(key) - # map to -2 -1 1 2 for not-seen to seen - idx = idx - 2 + int(idx > 1) - if trial["response_mapping_flip"]: - idx = - idx - trial["response"] = idx - trial["response_txt"] = "seen" if idx > 0 else "unseen" - trial["response_confidence"] = abs(idx) > 1 - trial["response_time"] = (keypress[1] - trial["onset"]) + trial['keypresses'] = keypresses # log all keypresses with timing + #keypress = keypresses[0] # only take the first keypress, TODO: log extra keypresses? + idxs = [np.where(self.RESPONSE_MAPPING == k[0]) for k in keypresses] + responses = [self.RESPONSE_VALUES[ + idx[0][0] * (1-2*trial['response_mapping_flip_v']), + idx[1][0] * (1-2*trial['response_mapping_flip_h'])] for idx in idxs] + + main_key = keypresses[0] # take the first response as main one, to be decided + main_response = responses[0] + trial["response"] = main_response + trial["response_txt"] = "seen" if main_response > 0 else "unseen" + trial["error"] = trial["response_txt"] != trial["condition"] + trial["response_confidence"] = abs(main_response) > 1 + trial["response_time"] = (main_key[1] - trial["onset"]) else: + # we need to force empty values for the first trials + # otherwise following values are not recorded!? + for k in ['keypresses', 'response', 'response_txt', 'error', 'response_confidence', 'response_time']: + trial[k] = '' print('Warning: no response') trial["duration_flip"] = trial["offset_flip"] - trial["onset_flip"] del trial["stim"] From 0b7050ad4bdc0717a3d590daef11a0de7afee9ad Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 19 Jan 2021 14:34:39 -0500 Subject: [PATCH 088/170] change wrap_width for instruction --- src/shared/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/config.py b/src/shared/config.py index d77f4323..27e06d9f 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -32,7 +32,7 @@ # task parameters INSTRUCTION_DURATION = 6 -WRAP_WIDTH = 1 +WRAP_WIDTH = 2 # port for meg setup PARALLEL_PORT_ADDRESS = "/dev/parport0" From badc9f7f8fb551a7d0d9600c8fca6cec96a72d7f Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 19 Jan 2021 14:37:35 -0500 Subject: [PATCH 089/170] add a requirement file, wip --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..9dd93058 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PsychoPy>=2020.2.4.post1 +gym-retro>=0.8.0 From dcaaf87c0a3d28b8d70e3051730ff61dd44b376d Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 21 Jan 2021 13:53:33 -0500 Subject: [PATCH 090/170] [DATALAD] Recorded changes --- .gitmodules | 4 ++++ data/things/stimuli | 1 + 2 files changed, 5 insertions(+) create mode 100644 .gitmodules create mode 160000 data/things/stimuli diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..c9e4dd95 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "data/things/stimuli"] + path = data/things/stimuli + url = git@github.com:courtois-neuromod/things.stimuli.git + datalad-id = 5504289b-0ac5-4f7c-a6cd-86af1f5c2135 diff --git a/data/things/stimuli b/data/things/stimuli new file mode 160000 index 00000000..f968c2fb --- /dev/null +++ b/data/things/stimuli @@ -0,0 +1 @@ +Subproject commit f968c2fb42b29b85068e3b0b5773185d028aae8f From 490fbc0c957055da6a8b669f172422784fdf0573 Mon Sep 17 00:00:00 2001 From: Basile Pinsard Date: Fri, 22 Jan 2021 11:54:16 -0500 Subject: [PATCH 091/170] reinstall things stimuli subdataset --- .gitmodules | 4 ---- data/things/stimuli | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.gitmodules b/.gitmodules index c9e4dd95..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +0,0 @@ -[submodule "data/things/stimuli"] - path = data/things/stimuli - url = git@github.com:courtois-neuromod/things.stimuli.git - datalad-id = 5504289b-0ac5-4f7c-a6cd-86af1f5c2135 diff --git a/data/things/stimuli b/data/things/stimuli index f968c2fb..d0150953 160000 --- a/data/things/stimuli +++ b/data/things/stimuli @@ -1 +1 @@ -Subproject commit f968c2fb42b29b85068e3b0b5773185d028aae8f +Subproject commit d0150953b62f327b8e2a782ffeb82daec740a7b5 From 26e4997210851e36af02627adc047fcfbca66e4b Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 22 Jan 2021 16:00:19 -0500 Subject: [PATCH 092/170] set images size in visual angle --- src/sessions/ses-thingsmem.py | 4 ++-- src/shared/cli.py | 2 +- src/shared/config.py | 9 +++++++++ src/tasks/things.py | 19 +++++++++++-------- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/sessions/ses-thingsmem.py b/src/sessions/ses-thingsmem.py index 577153ff..6c8f69a3 100644 --- a/src/sessions/ses-thingsmem.py +++ b/src/sessions/ses-thingsmem.py @@ -1,7 +1,7 @@ import os THINGS_DATA_PATH = os.path.join("data", "things") -IMAGE_PATH = os.path.join(THINGS_DATA_PATH, "images") +IMAGE_PATH = os.path.join(THINGS_DATA_PATH, "stimuli") def get_tasks(parsed): @@ -49,7 +49,7 @@ def generate_design_file(subject): import hashlib images_list = pandas.read_csv( - os.path.join(THINGS_DATA_PATH, "image_paths_fmri.csv") + os.path.join(THINGS_DATA_PATH, "stimuli", "image_paths_fmri.csv") ) # we select only the exp trial from Martin's BIG experiment diff --git a/src/shared/cli.py b/src/shared/cli.py index 230552e2..d4100eda 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -132,7 +132,7 @@ def main_loop( logfile_path = os.path.join(log_path, log_name_prefix + ".log") log_file = logging.LogFile(logfile_path, level=logging.INFO, filemode="w") - exp_win = visual.Window(**config.EXP_WINDOW) + exp_win = visual.Window(**config.EXP_WINDOW, monitor=config.EXP_MONITOR) exp_win.mouseVisible = False if show_ctl_win: diff --git a/src/shared/config.py b/src/shared/config.py index 27e06d9f..5f874d2c 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -1,4 +1,5 @@ from psychopy import prefs +from psychopy.monitors import Monitor # avoids delay in movie3 audio seek prefs.hardware["audioLib"] = ["sounddevice"] @@ -10,6 +11,12 @@ EXP_SCREEN_XRANDR_NAME = "eDP-1" +EXP_MONITOR = Monitor( + name='__blank__', + width=55, + distance=180, + ) + EXP_WINDOW = dict( size=(1280, 1024), screen=1, @@ -18,6 +25,8 @@ #waitBlanking=False, ) +EXP_MONITOR.setSizePix(EXP_WINDOW['size']) + CTL_WINDOW = dict( size=(1280, 1024), pos=(100, 0), diff --git a/src/tasks/things.py b/src/tasks/things.py index 3b492bdf..21cf4fa0 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -31,16 +31,17 @@ def __init__(self, design, images_path, run, *args, **kwargs): def _setup(self, exp_win): self.fixation_cross = visual.ImageStim( exp_win, - os.path.join("data", "things", "images", "fixation_cross.png"), - size=(128,128), - units="pixels", - #opacity=0.5, + os.path.join("data", "things", "pngs", "fixation_cross.png"), + size=2, + units='deg', ) # preload all images for trial in self.design: trial["stim"] = visual.ImageStim( - exp_win, os.path.join(self.images_path, trial["image_path"]) + exp_win, os.path.join(self.images_path, trial["image_path"]), + size=10, + units='deg', ) self.trials = data.TrialHandler(self.design, 1, method="sequential") self.duration = len(self.design) @@ -147,6 +148,8 @@ class ThingsMemory(Things): The button mapping will change from trial to trial as indicated at the center of the screen with that image. + + """ RESPONSE_KEYS = ['up','right','left','down'] RESPONSE_MAPPING = np.asarray(RESPONSE_KEYS).reshape(2,2) @@ -181,9 +184,9 @@ def _setup(self, exp_win): self._response_mapping = visual.ImageStim( exp_win, - os.path.join("data", "things", "images", "response_mapping3.png"), - size=(128, 128), - units="pixels", + os.path.join("data", "things", "pngs", "response_mapping3.png"), + size=2, + units='deg', ) def _run(self, exp_win, ctl_win): From 0c91f5b94a49a81874b485188820ed960512ebfe Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 26 Jan 2021 11:28:27 -0500 Subject: [PATCH 093/170] add mapping for controller for Things memory task --- .../snes_for_things_mem.gamecontroller.amgp | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 data/things/snes_for_things_mem.gamecontroller.amgp diff --git a/data/things/snes_for_things_mem.gamecontroller.amgp b/data/things/snes_for_things_mem.gamecontroller.amgp new file mode 100644 index 00000000..bff7c69a --- /dev/null +++ b/data/things/snes_for_things_mem.gamecontroller.amgp @@ -0,0 +1,101 @@ + + + + Retrolink SNES Controller + + 0300000079000000110000001001000012117 + + + + + + + + + + L Stick + R Stick + + + + + + + + 0x1000012 + keyboard + + + + + + + 0x1000014 + keyboard + + + + + + + 0x1000013 + keyboard + + + + + + + 0x1000015 + keyboard + + + + + + 0 + -32767 + 32767 + positivehalf + + + 0 + -32767 + 32767 + positivehalf + + + + + + + + From 29991e5998ae867b7c04147b6499e9690c562f63 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 26 Jan 2021 11:35:43 -0500 Subject: [PATCH 094/170] [DATALAD] Recorded changes --- .gitmodules | 4 ++++ data/things/images | 1 + 2 files changed, 5 insertions(+) create mode 160000 data/things/images diff --git a/.gitmodules b/.gitmodules index e69de29b..5a0103d3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "data/things/images"] + path = data/things/images + url = git@github.com:courtois-neuromod/things.stimuli.git + datalad-id = 5504289b-0ac5-4f7c-a6cd-86af1f5c2135 diff --git a/data/things/images b/data/things/images new file mode 160000 index 00000000..d0150953 --- /dev/null +++ b/data/things/images @@ -0,0 +1 @@ +Subproject commit d0150953b62f327b8e2a782ffeb82daec740a7b5 From c07c86718db7dfcfa021c261fe87a2c2e2d25c55 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 28 Jan 2021 14:49:47 -0500 Subject: [PATCH 095/170] [DATALAD] new dataset --- .datalad/.gitattributes | 4 ++++ .datalad/config | 2 ++ .gitattributes | 3 +++ 3 files changed, 9 insertions(+) create mode 100644 .datalad/.gitattributes create mode 100644 .datalad/config create mode 100644 .gitattributes diff --git a/.datalad/.gitattributes b/.datalad/.gitattributes new file mode 100644 index 00000000..b5408201 --- /dev/null +++ b/.datalad/.gitattributes @@ -0,0 +1,4 @@ + +config annex.largefiles=nothing +metadata/aggregate* annex.largefiles=nothing +metadata/objects/** annex.largefiles=(anything) \ No newline at end of file diff --git a/.datalad/config b/.datalad/config new file mode 100644 index 00000000..119c0751 --- /dev/null +++ b/.datalad/config @@ -0,0 +1,2 @@ +[datalad "dataset"] + id = 2b3f09cc-05d0-4364-a694-73e1af4079e6 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..c3aaefef --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ + +* annex.backend=MD5E +**/.git* annex.largefiles=nothing \ No newline at end of file From 76634ad87e1f3ef42890cd67ce4ad356f0a59b89 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 28 Jan 2021 15:27:47 -0500 Subject: [PATCH 096/170] save .gitattributes --- .gitattributes | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index c3aaefef..448d70f9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,9 @@ - * annex.backend=MD5E -**/.git* annex.largefiles=nothing \ No newline at end of file +**/.git* annex.largefiles=nothing +*.jpg annex.largefiles=anything +*.png annex.largefiles=anything +*.svg annex.largefiles=anything +*.txt annex.largefiles=nothing +*.tsv annex.largefiles=nothing +*.csv annex.largefiles=nothing +*.py annex.largefiles=nothing From 4e3ffb73b8ecf3bc00182e796b96dbded29edf46 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 28 Jan 2021 15:28:20 -0500 Subject: [PATCH 097/170] add static images and source svgs --- data/things/pngs/fixation_cross.png | 1 + data/things/pngs/fixation_cross_old.png | 1 + data/things/pngs/response_mapping.png | 1 + data/things/pngs/response_mapping.svg | 1 + data/things/pngs/response_mapping2.png | 1 + data/things/pngs/response_mapping2.svg | 1 + data/things/pngs/response_mapping3.png | 1 + 7 files changed, 7 insertions(+) create mode 120000 data/things/pngs/fixation_cross.png create mode 120000 data/things/pngs/fixation_cross_old.png create mode 120000 data/things/pngs/response_mapping.png create mode 120000 data/things/pngs/response_mapping.svg create mode 120000 data/things/pngs/response_mapping2.png create mode 120000 data/things/pngs/response_mapping2.svg create mode 120000 data/things/pngs/response_mapping3.png diff --git a/data/things/pngs/fixation_cross.png b/data/things/pngs/fixation_cross.png new file mode 120000 index 00000000..e499b9aa --- /dev/null +++ b/data/things/pngs/fixation_cross.png @@ -0,0 +1 @@ +../../../.git/annex/objects/6p/MF/MD5E-s3730--af10957c81bd6f4ec6fda46976b2f92c.png/MD5E-s3730--af10957c81bd6f4ec6fda46976b2f92c.png \ No newline at end of file diff --git a/data/things/pngs/fixation_cross_old.png b/data/things/pngs/fixation_cross_old.png new file mode 120000 index 00000000..dcc0cae0 --- /dev/null +++ b/data/things/pngs/fixation_cross_old.png @@ -0,0 +1 @@ +../../../.git/annex/objects/wq/2f/MD5E-s15446--ffafa1de6e0d71a131bcf618f812f550.png/MD5E-s15446--ffafa1de6e0d71a131bcf618f812f550.png \ No newline at end of file diff --git a/data/things/pngs/response_mapping.png b/data/things/pngs/response_mapping.png new file mode 120000 index 00000000..cd78247a --- /dev/null +++ b/data/things/pngs/response_mapping.png @@ -0,0 +1 @@ +../../../.git/annex/objects/vj/6k/MD5E-s1162--ecc582b653454b9f8efb57c8cd9ca019.png/MD5E-s1162--ecc582b653454b9f8efb57c8cd9ca019.png \ No newline at end of file diff --git a/data/things/pngs/response_mapping.svg b/data/things/pngs/response_mapping.svg new file mode 120000 index 00000000..dfa2ccc1 --- /dev/null +++ b/data/things/pngs/response_mapping.svg @@ -0,0 +1 @@ +../../../.git/annex/objects/ww/7P/MD5E-s4331--b225b41a96c216781817484b5d9d1e4f.svg/MD5E-s4331--b225b41a96c216781817484b5d9d1e4f.svg \ No newline at end of file diff --git a/data/things/pngs/response_mapping2.png b/data/things/pngs/response_mapping2.png new file mode 120000 index 00000000..54521960 --- /dev/null +++ b/data/things/pngs/response_mapping2.png @@ -0,0 +1 @@ +../../../.git/annex/objects/jF/65/MD5E-s11203--2f5f3b8566ac2e511c9d2b25ca864c21.png/MD5E-s11203--2f5f3b8566ac2e511c9d2b25ca864c21.png \ No newline at end of file diff --git a/data/things/pngs/response_mapping2.svg b/data/things/pngs/response_mapping2.svg new file mode 120000 index 00000000..211139db --- /dev/null +++ b/data/things/pngs/response_mapping2.svg @@ -0,0 +1 @@ +../../../.git/annex/objects/P5/JJ/MD5E-s9961--7bb7949c37aeb8dc26bb5c67658080fa.svg/MD5E-s9961--7bb7949c37aeb8dc26bb5c67658080fa.svg \ No newline at end of file diff --git a/data/things/pngs/response_mapping3.png b/data/things/pngs/response_mapping3.png new file mode 120000 index 00000000..3c303c33 --- /dev/null +++ b/data/things/pngs/response_mapping3.png @@ -0,0 +1 @@ +../../../.git/annex/objects/Mx/Mx/MD5E-s4934--406900a974a48fdccb5f314a200b4723.png/MD5E-s4934--406900a974a48fdccb5f314a200b4723.png \ No newline at end of file From 6b11790521ecf355527d916373ef95d211608b95 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 28 Jan 2021 15:29:05 -0500 Subject: [PATCH 098/170] save structure --- data/things/memory_designs/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/things/memory_designs/.gitkeep diff --git a/data/things/memory_designs/.gitkeep b/data/things/memory_designs/.gitkeep new file mode 100644 index 00000000..e69de29b From 4644003ebd3f197d7e4085202741731825070505 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 3 Feb 2021 13:09:26 -0500 Subject: [PATCH 099/170] adjust instructions for marker fixation --- src/tasks/things.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tasks/things.py b/src/tasks/things.py index 21cf4fa0..48828e1d 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -135,7 +135,7 @@ def _save(self): class ThingsMemory(Things): DEFAULT_INSTRUCTION = """You will see images on the screen. - +Try to fixate the central marker at all time. Press the buttons for each image to indicate your confidence in having seen or not that image previously. """ @@ -177,7 +177,7 @@ def _instructions(self, exp_win, ctl_win): if ctl_win: screen_text.draw(ctl_win) self._response_mapping.draw(ctl_win) - yield frameN <2 + yield frameN < 2 def _setup(self, exp_win): super()._setup(exp_win) From 27aa68deebb87ca68cf429a1debcd1c37fab4e02 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 3 Feb 2021 13:24:21 -0500 Subject: [PATCH 100/170] [DATALAD] Recorded changes --- .gitmodules | 4 ++++ data/videogames/shinobi | 1 + 2 files changed, 5 insertions(+) create mode 160000 data/videogames/shinobi diff --git a/.gitmodules b/.gitmodules index 5a0103d3..e6c781ad 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,7 @@ path = data/things/images url = git@github.com:courtois-neuromod/things.stimuli.git datalad-id = 5504289b-0ac5-4f7c-a6cd-86af1f5c2135 +[submodule "data/videogames/shinobi"] + path = data/videogames/shinobi + url = git@github.com:courtois-neuromod/shinobi.stimuli.git + datalad-id = 74cb8a6e-664b-11eb-a4f2-1a44c1d5432b diff --git a/data/videogames/shinobi b/data/videogames/shinobi new file mode 160000 index 00000000..ed682779 --- /dev/null +++ b/data/videogames/shinobi @@ -0,0 +1 @@ +Subproject commit ed6827793896100e9c3f303d34ec74ea347dc387 From 9e49e5083cd17040f1385119d214278360ac464d Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 3 Feb 2021 15:33:12 -0500 Subject: [PATCH 101/170] update submodule --- data/things/images | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/things/images b/data/things/images index d0150953..f5dea7c5 160000 --- a/data/things/images +++ b/data/things/images @@ -1 +1 @@ -Subproject commit d0150953b62f327b8e2a782ffeb82daec740a7b5 +Subproject commit f5dea7c5264bd81fb58c60e1a6afb0474db511fb From c1cf7b64e35e02c178a5d4efc3a327c4f3533ad5 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 3 Feb 2021 15:35:26 -0500 Subject: [PATCH 102/170] use local copy of gym-retro game for simplified and robust deployment --- src/sessions/ses-shinobi3levels.py | 7 +++++++ src/tasks/videogame.py | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/sessions/ses-shinobi3levels.py b/src/sessions/ses-shinobi3levels.py index 708bcf88..a90a8672 100644 --- a/src/sessions/ses-shinobi3levels.py +++ b/src/sessions/ses-shinobi3levels.py @@ -1,4 +1,11 @@ +import os import random +import retro + +# point to a copy of the whole gym-retro with custom states and scenarii +retro.data.Integrations.add_custom_path( + os.path.join(os.getcwd(), "data", "videogames", "shinobi") +) from ..tasks import images, videogame, memory, task_base diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 2c858a1e..46e4c821 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -150,7 +150,9 @@ def _setup(self, exp_win): self.game_name, state=self.state_name, scenario=self.scenario, - record=False) + record=False, + inttype=retro.data.Integrations.CUSTOM_ONLY + ) super()._setup(exp_win) self._events = [] From d4a57e4defaf0f5920c3a6bfae571c401cc248ac Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 9 Feb 2021 09:56:12 -0500 Subject: [PATCH 103/170] change pseudo-randomization to have constant seen/unseen ratio in each run --- src/sessions/ses-thingsmem.py | 123 ++++++++++++++++++++++++++++------ src/tasks/things.py | 17 +++-- 2 files changed, 114 insertions(+), 26 deletions(-) diff --git a/src/sessions/ses-thingsmem.py b/src/sessions/ses-thingsmem.py index 6c8f69a3..bc52dfc9 100644 --- a/src/sessions/ses-thingsmem.py +++ b/src/sessions/ses-thingsmem.py @@ -1,7 +1,7 @@ import os THINGS_DATA_PATH = os.path.join("data", "things") -IMAGE_PATH = os.path.join(THINGS_DATA_PATH, "stimuli") +IMAGE_PATH = os.path.join(THINGS_DATA_PATH, "images") def get_tasks(parsed): @@ -42,14 +42,31 @@ def get_tasks(parsed): max_seen_spacing = 8 + + def generate_design_file(subject): import pandas import numpy as np import random import hashlib + + # proportions for intra-session pseudo-randomize + props = pandas.DataFrame() + props['unseen_between'] = [5, 6, 7, 8, 9, 10, 10, 11, 12, 13, 14, 15] + props['unseen_within'] = [15, 14, 13, 12, 11, 10, 10, 9, 8, 7, 6, 5] + props['seen_within'] = props['unseen_between'] + props['seen_between_within'] = props['unseen_within'] + props['seen_between_within2'] = props['unseen_between'] + props['seen_within_between'] = props['unseen_within'] + + props_session1 = pandas.DataFrame() + props_session1['unseen_between'] = [12, 16, 18, 22, 24, 28] + props_session1['unseen_within'] = [28, 24 , 22, 18, 16, 12] + props_session1['seen_within'] = [20] * (n_runs//2) + images_list = pandas.read_csv( - os.path.join(THINGS_DATA_PATH, "stimuli", "image_paths_fmri.csv") + os.path.join(THINGS_DATA_PATH, "images", "image_paths_fmri.csv") ) # we select only the exp trial from Martin's BIG experiment @@ -59,8 +76,6 @@ def generate_design_file(subject): (images_list.exemplar_nr > 6 ) # > 6 for pilot < 7 for study ] - design = pandas.DataFrame() - # seed numpy with subject id to have reproducible design generation seed = int( hashlib.sha1(("%s" % (subject)).encode("utf-8")).hexdigest(), 16 @@ -73,6 +88,10 @@ def generate_design_file(subject): # permute categories per participant categories = np.random.permutation(720)+1 + #empty roll-over subconditions for first session + img_within_between = pandas.DataFrame() + img_between_within = pandas.DataFrame() + for session in range(n_sessions): # select the examplar exemplar = session//3+1 + 6 # + 6 here for pilot, remove for study!! @@ -105,23 +124,79 @@ def generate_design_file(subject): n_runs_session = n_runs if session == 0: - # show twice the within set and once the between set - all_run_trials = pandas.concat( - [img_unseen_within]*2 + - [img_unseen_between]) + session_props = props_session1 n_runs_session = n_runs//2 else: - all_run_trials = pandas.concat( - [img_unseen_within]*2 + - [img_unseen_between]+ - [img_between_within]*2 + - [img_within_between] - ) + session_props = props + + img_unseen_between = img_unseen_between.sample(frac=1).reset_index(drop=True) #randomize + img_unseen_between["run"] = np.hstack([r+1]*prop for r,prop in enumerate(session_props.unseen_between)) + img_unseen_within = img_unseen_within.sample(frac=1).reset_index(drop=True) #randomize + img_unseen_within["run"] = np.hstack([r+1]*prop for r,prop in enumerate(session_props.unseen_within)) + + if session > 0: + img_within_between = img_within_between.sample(frac=1).reset_index(drop=True) #randomize + img_within_between["run"] = np.hstack([r+1]*prop for r,prop in enumerate(session_props.seen_within_between)) + img_between_within = img_between_within.sample(frac=1).reset_index(drop=True) #randomize + img_between_within["run"] = np.hstack([r+1]*prop for r,prop in enumerate(session_props.seen_between_within)) + + + # here it is more complex due to temporal dependencies of within session repetitions + + all_unseen_within = pandas.DataFrame() + img_seen_within = pandas.DataFrame() + for run in range(n_runs_session): + img_unseen_within_run = img_unseen_within[img_unseen_within.run == run+1] + # aggregate all the unused repetitions + all_unseen_within = all_unseen_within.append(img_unseen_within_run) + # randomely sample the unused repetitions + img_seen_within_run = all_unseen_within.sample(n=session_props.seen_within[run]) + all_unseen_within = all_unseen_within.drop(img_seen_within_run.index) # without replacement + img_seen_within_run['run'] = run+1 + img_seen_within = img_seen_within.append(img_seen_within_run) + +# img_seen_within.set_index( +# img_seen_within.index+np.random.randint(1, n_trials/2, img_seen_within.shape[0]), +# inplace=True) + #img_seen_within['repetition'] = 2 + #img_seen_within['condition'] = 'seen' + #img_seen_within['subcondition'] = 'seen-within' + #img_seen_within = img_seen_within.reset_index(drop=True) + + img_between_within2 = pandas.DataFrame() + if session > 0: + all_between_within = pandas.DataFrame() + for run in range(n_runs_session): + img_between_within_run = img_between_within[img_between_within.run == run+1] + # aggregate all the unused repetitions + all_between_within = all_between_within.append(img_between_within_run) + # randomely sample the unused repetitions + img_between_within_run = all_between_within.sample(n=session_props.seen_between_within2[run]) + all_between_within = all_between_within.drop(img_between_within_run.index) # without replacement + img_between_within_run['run'] = run+1 + img_between_within2 = img_between_within2.append(img_between_within_run) + #img_between_within2['repetition'] = 3 + #img_between_within2 = img_between_within2.reset_index(drop=True) +# img_between_within2.set_index( +# img_between_within2.index + np.random.randint(1, n_trials/2, img_between_within2.shape[0]), +# inplace=True) + + + all_run_trials = pandas.concat([ + img_unseen_within, + img_unseen_between, + img_seen_within, + img_between_within, + img_between_within2, + img_within_between + ], ignore_index=True) + + #all_run_trials = all_run_trials.reset_index().sort_values(['run','index','image_nr','repetition']) # pass new "within"/"between" stimuli to the next session img_between_within = img_unseen_between img_between_within.condition = 'seen' - img_between_within.subcondition = "seen-between-within" + img_between_within.subcondition = "seen-between" img_between_within.repetition = 2 img_within_between = img_unseen_within @@ -129,17 +204,23 @@ def generate_design_file(subject): img_within_between.subcondition = "seen-within-between" img_within_between.repetition = 3 - # randomize order across the whole session - all_run_trials = all_run_trials.sample(frac=1) + + all_run_trials = all_run_trials.sample(frac=1).sort_values('run') repeated_within = all_run_trials['image_nr'].duplicated() # set seen condition for second viewing of new set of images all_run_trials.loc[repeated_within, 'condition'] = 'seen' - all_run_trials.loc[repeated_within & all_run_trials.subcondition.eq('unseen-within'), 'repetition'] = 2 - all_run_trials.loc[repeated_within & all_run_trials.subcondition.eq('seen-between-within'), 'repetition'] = 3 + seen_within_subset = repeated_within & \ + all_run_trials.subcondition.eq('unseen-within') & \ + all_run_trials.repetition.eq(1) + all_run_trials.loc[seen_within_subset, 'repetition'] = 2 + all_run_trials.loc[seen_within_subset, 'subcondition'] = 'seen-within' + + #all_run_trials.loc[repeated_within & all_run_trials.subcondition.eq('unseen-within'), 'repetition'] = 2 + seen_between_within = repeated_within & all_run_trials.subcondition.eq('seen-between') + all_run_trials.loc[seen_between_within, 'repetition'] = 3 + all_run_trials.loc[seen_between_within, 'subcondition'] = 'seen-between-within' - # split in runs - all_run_trials["run"] = np.arange(1, n_runs_session+1).repeat(n_trials) # set timing all_run_trials["onset"] = np.tile(initial_wait + np.arange(n_trials) * trial_duration, n_runs_session) all_run_trials["duration"] = image_duration diff --git a/src/tasks/things.py b/src/tasks/things.py index 48828e1d..c4b8ebe1 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -7,7 +7,7 @@ RESPONSE_KEY = "d" RESPONSE_TIME = 4 - +FINAL_WAIT = 9 class Things(Task): @@ -124,8 +124,11 @@ def _run(self, exp_win, ctl_win): trial["duration_flip"] = trial["offset_flip"] - trial["onset_flip"] del trial["stim"] - while self.task_timer.getTime() < trial["onset"] + RESPONSE_TIME: - time.sleep(0.0005) +# while self.task_timer.getTime() < trial["onset"] + RESPONSE_TIME: +# time.sleep(0.0005) + + for frameN in range(config.FRAME_RATE * FINAL_WAIT): + yield def _save(self): self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) @@ -270,5 +273,9 @@ def _run(self, exp_win, ctl_win): trial["duration_flip"] = trial["offset_flip"] - trial["onset_flip"] del trial["stim"] - while self.task_timer.getTime() < trial["onset"] + RESPONSE_TIME: - time.sleep(0.0005) + +# while self.task_timer.getTime() < trial["onset"] + RESPONSE_TIME: +# time.sleep(0.0005) + + for frameN in range(config.FRAME_RATE * FINAL_WAIT): + yield From 2f2e0c44c52bde749c768b9871ccc6d2722727e7 Mon Sep 17 00:00:00 2001 From: bpinsard Date: Fri, 12 Feb 2021 13:43:36 -0500 Subject: [PATCH 104/170] add pandas as requirementes --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 9dd93058..5f14c1fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ PsychoPy>=2020.2.4.post1 gym-retro>=0.8.0 +pandas>=1.1.1 From ef8957832cb8f47043e52880f6cec14e907a314d Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 16 Feb 2021 13:15:18 -0500 Subject: [PATCH 105/170] add friends season 5 --- src/sessions/ses-friends-s5.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/sessions/ses-friends-s5.py diff --git a/src/sessions/ses-friends-s5.py b/src/sessions/ses-friends-s5.py new file mode 100644 index 00000000..cc7bc8b1 --- /dev/null +++ b/src/sessions/ses-friends-s5.py @@ -0,0 +1,21 @@ +from ..tasks import video + +TASKS = [] + +for episode in range(1, 23): + for segment in "ab": + TASKS.append( + video.SingleVideo( + "data/videos/friends/s5/friends_s05e%02d%s.mkv" % (episode, segment), + aspect_ratio=4 / 3.0, + name="task-friends-s5e%d%s" % (episode, segment), + ) + ) + +for segment in 'abcd': + TASKS.append( + video.SingleVideo( + 'data/videos/friends/s5/friends_s05e%02d%s.mkv'%(23, segment), + aspect_ratio = 4/3., + name='task-friends-s5e%d%s'%(23, segment))) + From 3b0a4e2fa1b35429d20dc5223bc4ed41a4c597b9 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 16 Feb 2021 14:40:38 -0500 Subject: [PATCH 106/170] things mem to use real images not pilot ones --- src/sessions/ses-thingsmem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sessions/ses-thingsmem.py b/src/sessions/ses-thingsmem.py index bc52dfc9..799f0ed0 100644 --- a/src/sessions/ses-thingsmem.py +++ b/src/sessions/ses-thingsmem.py @@ -73,7 +73,7 @@ def generate_design_file(subject): # and only the first (or second for pilot) half of the 12 exemplar of each category images_exp = images_list.loc[ images_list.condition.eq("exp") & - (images_list.exemplar_nr > 6 ) # > 6 for pilot < 7 for study + (images_list.exemplar_nr < 7 ) # > 6 for pilot < 7 for study ] # seed numpy with subject id to have reproducible design generation @@ -94,7 +94,7 @@ def generate_design_file(subject): for session in range(n_sessions): # select the examplar - exemplar = session//3+1 + 6 # + 6 here for pilot, remove for study!! + exemplar = session//3+1 # + 6 here for pilot, remove for study!! # subselect 240 categories for new stimuli # loop through the 3 sets of 240 across sessions and thus avoid From 78ea449147a3c03096fbc8a879143b64c25366ce Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 16 Feb 2021 15:24:13 -0500 Subject: [PATCH 107/170] load env with dotenv --- requirements.txt | 1 + src/shared/config.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/requirements.txt b/requirements.txt index 5f14c1fe..e5de3452 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ PsychoPy>=2020.2.4.post1 gym-retro>=0.8.0 pandas>=1.1.1 +python-dotenv diff --git a/src/shared/config.py b/src/shared/config.py index 5f874d2c..d6326c08 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -1,3 +1,6 @@ +from dotenv import load_dotenv +load_dotenv() + from psychopy import prefs from psychopy.monitors import Monitor From 871041eac358f0e8e6f8e3db856d99a0f8f2754e Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 16 Feb 2021 15:24:29 -0500 Subject: [PATCH 108/170] fix python integer division --- src/shared/eyetracking.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index 182c9185..99e525cf 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -125,8 +125,8 @@ def _run(self, exp_win, ctl_win): radius_anim = np.hstack( [ - np.linspace(MARKER_SIZE, 0, MARKER_DURATION_FRAMES / 2), - np.linspace(0, MARKER_SIZE, MARKER_DURATION_FRAMES / 2), + np.linspace(MARKER_SIZE, 0, MARKER_DURATION_FRAMES // 2), + np.linspace(0, MARKER_SIZE, MARKER_DURATION_FRAMES // 2), ] ) From 09ce81335d917bf8b94855413868b9c8152eb844 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 16 Feb 2021 15:25:57 -0500 Subject: [PATCH 109/170] fix rendering --- src/tasks/things.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tasks/things.py b/src/tasks/things.py index c4b8ebe1..202d69ff 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -171,7 +171,7 @@ def _instructions(self, exp_win, ctl_win): screen_text.draw(exp_win) if ctl_win: screen_text.draw(ctl_win) - yield + yield frameN < 3 screen_text.text = self.EXTRA_INSTRUCTION if self.run_id == 1: for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION * 2): @@ -180,7 +180,7 @@ def _instructions(self, exp_win, ctl_win): if ctl_win: screen_text.draw(ctl_win) self._response_mapping.draw(ctl_win) - yield frameN < 2 + yield frameN < 3 def _setup(self, exp_win): super()._setup(exp_win) From c501079d5d38727a41dbf4e39ff868294217253b Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 16 Feb 2021 15:26:19 -0500 Subject: [PATCH 110/170] fix rendering --- src/tasks/things.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tasks/things.py b/src/tasks/things.py index 202d69ff..488e4ae0 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -150,6 +150,7 @@ class ThingsMemory(Things): + The button mapping will change from trial to trial as indicated at the center of the screen with that image. From a33a3f3e7f27e349e96d54623a85a342aa53b0c2 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 16 Feb 2021 15:43:22 -0500 Subject: [PATCH 111/170] add template .env --- .env.tpl | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .env.tpl diff --git a/.env.tpl b/.env.tpl new file mode 100644 index 00000000..324526cf --- /dev/null +++ b/.env.tpl @@ -0,0 +1,3 @@ +export PUPIL_PATH=../pupil +#export PUPIL_PATH=/home/basile/data/src/pupil +export GI_TYPELIB_PATH=/usr/local/lib/x86_64-linux-gnu/girepository-1.0/ From 869548c87ebee7870cbd2d030b37269c7e1e2aa6 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 1 Mar 2021 10:09:14 -0500 Subject: [PATCH 112/170] fix problem when restarting the task --- src/tasks/things.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/tasks/things.py b/src/tasks/things.py index c4b8ebe1..cd7ac816 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -37,12 +37,13 @@ def _setup(self, exp_win): ) # preload all images + self._stimuli = [] for trial in self.design: - trial["stim"] = visual.ImageStim( + self._stimuli.append(visual.ImageStim( exp_win, os.path.join(self.images_path, trial["image_path"]), size=10, units='deg', - ) + )) self.trials = data.TrialHandler(self.design, 1, method="sequential") self.duration = len(self.design) self._progress_bar_refresh_rate = 2 # 2 flips per trial @@ -73,7 +74,7 @@ def _run(self, exp_win, ctl_win): self.fixation_cross.draw(ctl_win) yield True - for trial_n, trial in enumerate(self.trials): + for trial_n, (trial, stimuli) in enumerate(zip(self.trials, self._stimuli)): exp_win.logOnFlip( level=logging.EXP, msg=f"image: {trial['condition']}:{trial['image_path']}", @@ -83,10 +84,10 @@ def _run(self, exp_win, ctl_win): ) # draw to backbuffer - trial["stim"].draw(exp_win) + stimuli.draw(exp_win) self.fixation_cross.draw(exp_win) if ctl_win: - trial["stim"].draw(ctl_win) + stimuli.draw(ctl_win) self.fixation_cross.draw(ctl_win) # wait onset while self.task_timer.getTime() < trial["onset"] - 1 / config.FRAME_RATE: @@ -122,7 +123,6 @@ def _run(self, exp_win, ctl_win): (keypress[0][1] - trial["onset"]) if len(keypress) else None ) trial["duration_flip"] = trial["offset_flip"] - trial["onset_flip"] - del trial["stim"] # while self.task_timer.getTime() < trial["onset"] + RESPONSE_TIME: # time.sleep(0.0005) @@ -130,10 +130,16 @@ def _run(self, exp_win, ctl_win): for frameN in range(config.FRAME_RATE * FINAL_WAIT): yield + def _restart(self): + self.trials = data.TrialHandler(self.design, 1, method="sequential") + def _save(self): self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) return False + def unload(self): + del self._stimuli + class ThingsMemory(Things): @@ -150,6 +156,7 @@ class ThingsMemory(Things): + The button mapping will change from trial to trial as indicated at the center of the screen with that image. @@ -180,7 +187,7 @@ def _instructions(self, exp_win, ctl_win): if ctl_win: screen_text.draw(ctl_win) self._response_mapping.draw(ctl_win) - yield frameN < 2 + yield frameN def _setup(self, exp_win): super()._setup(exp_win) @@ -201,7 +208,7 @@ def _run(self, exp_win, ctl_win): self.fixation_cross.draw(ctl_win) yield True - for trial_n, trial in enumerate(self.trials): + for trial_n, (trial, stimuli) in enumerate(zip(self.trials, self._stimuli)): exp_win.logOnFlip( level=logging.EXP, msg=f"image: {trial['condition']}:{trial['image_path']}", @@ -211,13 +218,13 @@ def _run(self, exp_win, ctl_win): ) # draw to backbuffer - trial["stim"].draw(exp_win) + stimuli.draw(exp_win) self._response_mapping.flipHoriz = trial["response_mapping_flip_h"] self._response_mapping.flipVert = trial["response_mapping_flip_v"] self._response_mapping.pos = (0,0) #force update to flip self._response_mapping.draw(exp_win) if ctl_win: - trial["stim"].draw(ctl_win) + stimuli.draw(ctl_win) self._response_mapping.draw(ctl_win) # wait onset while self.task_timer.getTime() < trial["onset"] - 1 / config.FRAME_RATE: @@ -271,7 +278,6 @@ def _run(self, exp_win, ctl_win): trial[k] = '' print('Warning: no response') trial["duration_flip"] = trial["offset_flip"] - trial["onset_flip"] - del trial["stim"] # while self.task_timer.getTime() < trial["onset"] + RESPONSE_TIME: From f95b88c035bc03e993ea9749ac30eb323d2c33f9 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 1 Mar 2021 16:35:30 -0500 Subject: [PATCH 113/170] fix framerate issue, --- src/tasks/videogame.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index da3047d5..61c60bac 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -91,7 +91,7 @@ def _transform_soundblock(self, sound_block): return sound_block[:735] / float(2 ** 15) def _render_graphics_sound(self, obs, sound_block, exp_win, ctl_win): - self.game_vis_stim.image = obs / 255.0 # np.flip(obs, 0)/255. + self.game_vis_stim.image = obs / 255.0 self.game_vis_stim.draw(exp_win) if ctl_win: self.game_vis_stim.draw(ctl_win) @@ -156,6 +156,15 @@ def _instructions(self, exp_win, ctl_win): def _setup(self, exp_win): + retraceRate = exp_win._monitorFrameRate + if retraceRate is None: + retraceRate = exp_win.getActualFrameRate() + if retraceRate is None: + logging.warning("FrameRate could not be supplied by psychopy; " + "defaulting to 60.0") + retraceRate = 60.0 + self._retraceInterval = 1.0/retraceRate + self.emulator = retro.make( self.game_name, state=self.state_name, @@ -164,6 +173,9 @@ def _setup(self, exp_win): inttype=retro.data.Integrations.CUSTOM_ONLY ) + self.game_fps = self.emulator.em.get_screen_rate() + self._frameInterval = 1.0/self.game_fps + super()._setup(exp_win) self._set_recording_file() self._set_key_handler(exp_win) @@ -226,8 +238,12 @@ def _run_emulator(self, exp_win, ctl_win): }, ) yield True + _nextFrameT = self.task_timer.getTime() while not _done: level_step += 1 + while _nextFrameT > (self.task_timer.getTime() - + self._retraceInterval/2.0): + time.sleep(.0001) self._handle_controller_presses() keys = [k in self.pressed_keys for k in KEY_SET] _obs, _rew, _done, _info = self.emulator.step(keys) @@ -246,8 +262,9 @@ def _run_emulator(self, exp_win, ctl_win): if not level_step % config.FRAME_RATE: exp_win.logOnFlip(level=logging.EXP, msg="level step: %d" % level_step) yield True - self.game_sound.stop() + _nextFrameT += self._frameInterval self.game_sound.flush() + self.game_sound.stop() def _set_key_handler(self, exp_win): # activate repeat keys From 2a07b567fa5b7548930ed71c1d1b1f3f48e59cd9 Mon Sep 17 00:00:00 2001 From: FrancoisPgm Date: Thu, 4 Mar 2021 13:36:45 -0500 Subject: [PATCH 114/170] add a reading task class for word by word reading --- src/tasks/language.py | 73 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/tasks/language.py b/src/tasks/language.py index 62ec28c3..6ec5dcf2 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -122,3 +122,76 @@ def _run(self, exp_win, ctl_win): def _save(self): self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) return False + + +class Reading(Task): + + DEFAULT_INSTRUCTION = """You will be presented a text to read word by word.""" + + def __init__(self, words_file, word_duration=0.5, cross_duration=20, + txt_color="black", txt_font="Palatino", txt_size=42, + bg_color=(125,125,125), *args, **kwargs): + super().__init__(**kwargs) + if os.path.exists(words_file): + self.words_file = words_file + self.word_duration = word_duration + self.cross_duration = cross_duration + self.txt_color = txt_color + self.txt_font = txt_font + self.txt_size = txt_size + self.bg_color = bg_color + self.words_list = data.importConditions(self.words_file) + else: + raise ValueError("File %s does not exists" % words_file) + + def _instructions(self, exp_win, ctl_win): + screen_text = visual.TextStim( + exp_win, + text=self.instruction, + alignText="center", + color="white", + wrapWidth=config.WRAP_WIDTH, + ) + + for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): + screen_text.draw(exp_win) + if ctl_win: + screen_text.draw(ctl_win) + yield () + + def _run(self, exp_win, ctl_win): + exp_win.setColor(self.bg_color) + if ctl_win: + ctl_win.setColor(self.bg_color) + + txt_stim = visual.TextStim( + exp_win, + text="+", + font=self.txt_font, + height=self.txt_size, + alignText="center", + color=self.txt_color, + wrapWidth=config.WRAP_WIDTH, + ) + + # Display a centered cross for 20s + for frameN in range(config.FRAME_RATE * self.cross_duration): + txt_stim.draw(exp_win) + if ctl_win: + txt_stim.draw(ctl_win) + yield () + + # Display each word for 0.5s + for word in self.words_list: + italic = False + if word[0] == "@": + italic = True + word = word[1:] + txt_stim.text = word + txt_stim.italic = italic + + for frameN in range(config.FRAME_RATE * WORD_DURATION): + txt_stim.draw(exp_win) + if ctl_win: + txt_stim.draw(ctl_win) + yield () From 5737a75823e9c9a53ba55e91a21e444eb9f8794c Mon Sep 17 00:00:00 2001 From: courtois-neuromod-machine Date: Tue, 9 Mar 2021 15:13:41 -0500 Subject: [PATCH 115/170] comment out height arg and add ses-harrypotter --- src/sessions/ses-harrypotter.py | 12 ++++++++++++ src/tasks/language.py | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 src/sessions/ses-harrypotter.py diff --git a/src/sessions/ses-harrypotter.py b/src/sessions/ses-harrypotter.py new file mode 100644 index 00000000..4a4e60de --- /dev/null +++ b/src/sessions/ses-harrypotter.py @@ -0,0 +1,12 @@ +from ..tasks import language + +TASKS = [] +for seg_idx in range(1, 2): + TASKS.append( + language.Reading( + "data/language/triplet/first1000triples.csv", + name="harrypotter_seg-%d" % seg_idx, + ) + ) + +# ??? volumes each, last run is ??? volumes long diff --git a/src/tasks/language.py b/src/tasks/language.py index 6ec5dcf2..9011ca28 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -160,15 +160,15 @@ def _instructions(self, exp_win, ctl_win): yield () def _run(self, exp_win, ctl_win): - exp_win.setColor(self.bg_color) + exp_win.setColor(self.bg_color, "rgb") if ctl_win: - ctl_win.setColor(self.bg_color) + ctl_win.setColor(self.bg_color, "rgb") txt_stim = visual.TextStim( exp_win, text="+", font=self.txt_font, - height=self.txt_size, + #height=self.txt_size, alignText="center", color=self.txt_color, wrapWidth=config.WRAP_WIDTH, From 4deb6415d7e65564f3f1127840191b4998f26eed Mon Sep 17 00:00:00 2001 From: Basile Date: Tue, 9 Mar 2021 15:29:37 -0500 Subject: [PATCH 116/170] add clearbuffer through yield --- src/tasks/language.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tasks/language.py b/src/tasks/language.py index 9011ca28..fac18d21 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -179,7 +179,7 @@ def _run(self, exp_win, ctl_win): txt_stim.draw(exp_win) if ctl_win: txt_stim.draw(ctl_win) - yield () + yield frameN < 2 # Display each word for 0.5s for word in self.words_list: @@ -194,4 +194,4 @@ def _run(self, exp_win, ctl_win): txt_stim.draw(exp_win) if ctl_win: txt_stim.draw(ctl_win) - yield () + yield frameN < 2 From b61e6109c48e65ca6b57cc4962bab2f46ab89149 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 9 Mar 2021 15:35:26 -0500 Subject: [PATCH 117/170] fix colorspace error --- src/tasks/video.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tasks/video.py b/src/tasks/video.py index dffa77b2..63da25fc 100644 --- a/src/tasks/video.py +++ b/src/tasks/video.py @@ -34,7 +34,7 @@ def _instructions(self, exp_win, ctl_win): grey = [ -float(frameN) / config.FRAME_RATE / config.INSTRUCTION_DURATION ] * 3 - exp_win.setColor(grey) + exp_win.setColor(grey, colorSpace='rgb') screen_text.draw(exp_win) if ctl_win: ctl_win.setColor(grey) @@ -83,7 +83,7 @@ def _stop(self, exp_win, ctl_win): self.movie_stim.stop() for frameN in range(config.FRAME_RATE * FADE_TO_GREY_DURATION): grey = [float(frameN) / config.FRAME_RATE / FADE_TO_GREY_DURATION - 1] * 3 - exp_win.setColor(grey) + exp_win.setColor(grey, colorSpace='rgb') if ctl_win: ctl_win.setColor(grey) yield True From 7accb514f3002ebf47b3cbe151db2d7490b8e40c Mon Sep 17 00:00:00 2001 From: courtois-neuromod-machine Date: Tue, 9 Mar 2021 15:37:03 -0500 Subject: [PATCH 118/170] use target in triplets as word for test --- src/tasks/language.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tasks/language.py b/src/tasks/language.py index 9011ca28..bd2db726 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -184,13 +184,14 @@ def _run(self, exp_win, ctl_win): # Display each word for 0.5s for word in self.words_list: italic = False + word = word["target"] if word[0] == "@": italic = True word = word[1:] txt_stim.text = word txt_stim.italic = italic - for frameN in range(config.FRAME_RATE * WORD_DURATION): + for frameN in range(int(config.FRAME_RATE * self.word_duration)): txt_stim.draw(exp_win) if ctl_win: txt_stim.draw(ctl_win) From 530ba99d4add1ebcaf81bb9ff4fb45f39caa0c19 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 10 Mar 2021 11:04:21 -0500 Subject: [PATCH 119/170] fixes --- src/sessions/ses-harrypotter.py | 2 ++ src/tasks/language.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sessions/ses-harrypotter.py b/src/sessions/ses-harrypotter.py index 4a4e60de..f5f6bd80 100644 --- a/src/sessions/ses-harrypotter.py +++ b/src/sessions/ses-harrypotter.py @@ -6,6 +6,8 @@ language.Reading( "data/language/triplet/first1000triples.csv", name="harrypotter_seg-%d" % seg_idx, + cross_duration=2, + txt_size=124, ) ) diff --git a/src/tasks/language.py b/src/tasks/language.py index 748e6b23..ea693cec 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -168,7 +168,8 @@ def _run(self, exp_win, ctl_win): exp_win, text="+", font=self.txt_font, - #height=self.txt_size, + height=self.txt_size, + units='pixels', alignText="center", color=self.txt_color, wrapWidth=config.WRAP_WIDTH, From da3ef6e4b4c610cb3c2d9c2c8fbd8faf9d722c7f Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 11 Mar 2021 16:27:55 -0500 Subject: [PATCH 120/170] add a helper function for non-slip timing. --- src/shared/utils.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/shared/utils.py b/src/shared/utils.py index 033d1d8b..36d7e76e 100644 --- a/src/shared/utils.py +++ b/src/shared/utils.py @@ -1,4 +1,6 @@ import psutil +import time +from psychopy import core def check_power_plugged(): @@ -7,3 +9,20 @@ def check_power_plugged(): return battery.power_plugged else: return True + +def wait_until(clock, deadline, hogCPUperiod=0.1, keyboard_accuracy=.0005): + sleep_until = deadline - hogCPUperiod + poll_windows() + current_time = clock.getTime() + while current_time < deadline: + if current_time < sleep_until: + time.sleep(keyboard_accuracy) + poll_windows() + current_time = clock.getTime() + +def poll_windows(): + for winWeakRef in core.openWindows: + win = winWeakRef() + if (win.winType == "pyglet" and + hasattr(win.winHandle, "dispatch_events")): + win.winHandle.dispatch_events() # pump events From 834235de4f6f0bd2b03d9f0897881947871ccb4a Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 11 Mar 2021 16:42:51 -0500 Subject: [PATCH 121/170] try to fix timing issue --- src/tasks/things.py | 57 ++++++++++++++------------------------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/src/tasks/things.py b/src/tasks/things.py index a353be75..9b16c71e 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -3,7 +3,7 @@ from .task_base import Task import numpy as np -from ..shared import config +from ..shared import config, utils RESPONSE_KEY = "d" RESPONSE_TIME = 4 @@ -90,8 +90,7 @@ def _run(self, exp_win, ctl_win): stimuli.draw(ctl_win) self.fixation_cross.draw(ctl_win) # wait onset - while self.task_timer.getTime() < trial["onset"] - 1 / config.FRAME_RATE: - time.sleep(0.0005) # just to avoid looping to fast + utils.wait_until(self.task_timer, trial["onset"] - 1 / config.FRAME_RATE) yield True # flip trial["onset_flip"] = ( self._exp_win_last_flip_time - self._exp_win_first_flip_time @@ -102,21 +101,14 @@ def _run(self, exp_win, ctl_win): self.fixation_cross.draw(exp_win) if ctl_win: self.fixation_cross.draw(ctl_win) - while ( - self.task_timer.getTime() - < trial["onset"] + trial["duration"] - 1 / config.FRAME_RATE - ): - time.sleep(0.0005) # just to avoid looping to fast + utils.wait_until(self.task_timer, trial["onset"] + trial["duration"] - 1 / config.FRAME_RATE) yield True # flip trial["offset_flip"] = ( self._exp_win_last_flip_time - self._exp_win_first_flip_time ) - while ( - self.task_timer.getTime() - < trial["onset"] + RESPONSE_TIME - 1 / config.FRAME_RATE - ): - time.sleep(0.0005) # just to avoid looping to fast + utils.wait_until(self.task_timer, trial["onset"] + RESPONSE_TIME - 1 / config.FRAME_RATE) + keypress = event.getKeys([RESPONSE_KEY], timeStamped=self.task_timer) trial["response"] = len(keypress) > 0 trial["response_time"] = ( @@ -124,11 +116,7 @@ def _run(self, exp_win, ctl_win): ) trial["duration_flip"] = trial["offset_flip"] - trial["onset_flip"] -# while self.task_timer.getTime() < trial["onset"] + RESPONSE_TIME: -# time.sleep(0.0005) - - for frameN in range(config.FRAME_RATE * FINAL_WAIT): - yield + utils.wait_until(self.task_timer, trial["onset"] + RESPONSE_TIME + FINAL_WAIT) def _restart(self): self.trials = data.TrialHandler(self.design, 1, method="sequential") @@ -227,8 +215,7 @@ def _run(self, exp_win, ctl_win): stimuli.draw(ctl_win) self._response_mapping.draw(ctl_win) # wait onset - while self.task_timer.getTime() < trial["onset"] - 1 / config.FRAME_RATE: - time.sleep(0.0005) # just to avoid looping to fast + utils.wait_until(self.task_timer, trial["onset"] - 1 / config.FRAME_RATE) keypresses = event.getKeys(self.RESPONSE_KEYS) # flush response keys yield True # flip trial["onset_flip"] = ( @@ -240,29 +227,25 @@ def _run(self, exp_win, ctl_win): self.fixation_cross.draw(exp_win) if ctl_win: self.fixation_cross.draw(ctl_win) - while ( - self.task_timer.getTime() - < trial["onset"] + trial["duration"] - 1 / config.FRAME_RATE - ): - time.sleep(0.0005) # just to avoid looping to fast + utils.wait_until(self.task_timer, trial["onset"] + trial["duration"] - 1 / config.FRAME_RATE) yield True # flip trial["offset_flip"] = ( self._exp_win_last_flip_time - self._exp_win_first_flip_time ) - while ( - self.task_timer.getTime() - < trial["onset"] + RESPONSE_TIME - 1 / config.FRAME_RATE - ): - time.sleep(0.0005) # just to avoid looping to fast + utils.wait_until(self.task_timer, trial["onset"] + RESPONSE_TIME - 1 / config.FRAME_RATE) + keypresses = event.getKeys(self.RESPONSE_KEYS, timeStamped=self.task_timer) if len(keypresses): trial['keypresses'] = keypresses # log all keypresses with timing #keypress = keypresses[0] # only take the first keypress, TODO: log extra keypresses? idxs = [np.where(self.RESPONSE_MAPPING == k[0]) for k in keypresses] - responses = [self.RESPONSE_VALUES[ - idx[0][0] * (1-2*trial['response_mapping_flip_v']), - idx[1][0] * (1-2*trial['response_mapping_flip_h'])] for idx in idxs] + response_mapping_flipped = self.RESPONSE_VALUES.copy() + if trial["response_mapping_flip_h"]: + response_mapping_flipped = np.rot90(np.fliplr(self.RESPONSE_VALUES)) + if trial["response_mapping_flip_v"]: + response_mapping_flipped = np.rot90(np.fliplr(self.RESPONSE_VALUES),3) + responses = [response_mapping_flipped[idx[0][0],idx[1][0]] for idx in idxs] main_key = keypresses[0] # take the first response as main one, to be decided main_response = responses[0] @@ -271,6 +254,7 @@ def _run(self, exp_win, ctl_win): trial["error"] = trial["response_txt"] != trial["condition"] trial["response_confidence"] = abs(main_response) > 1 trial["response_time"] = (main_key[1] - trial["onset"]) + if trial['error']: print("error") else: # we need to force empty values for the first trials # otherwise following values are not recorded!? @@ -279,9 +263,4 @@ def _run(self, exp_win, ctl_win): print('Warning: no response') trial["duration_flip"] = trial["offset_flip"] - trial["onset_flip"] - -# while self.task_timer.getTime() < trial["onset"] + RESPONSE_TIME: -# time.sleep(0.0005) - - for frameN in range(config.FRAME_RATE * FINAL_WAIT): - yield + utils.wait_until(self.task_timer, trial["onset"] + RESPONSE_TIME + FINAL_WAIT) From ba36e32f6415712d57f978c3a2b009e9a4d7b6df Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 12 Mar 2021 13:22:40 -0500 Subject: [PATCH 122/170] fix response mapping: seems ok now, implement operator feedback --- src/tasks/things.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/tasks/things.py b/src/tasks/things.py index 9b16c71e..034e59de 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -2,6 +2,7 @@ from psychopy import visual, core, data, logging, event from .task_base import Task import numpy as np +from colorama import Fore from ..shared import config, utils @@ -201,9 +202,6 @@ def _run(self, exp_win, ctl_win): level=logging.EXP, msg=f"image: {trial['condition']}:{trial['image_path']}", ) - self.progress_bar.set_description( - f"Trial {trial_n}:: {trial['condition']}:{trial['image_path']}" - ) # draw to backbuffer stimuli.draw(exp_win) @@ -221,6 +219,9 @@ def _run(self, exp_win, ctl_win): trial["onset_flip"] = ( self._exp_win_last_flip_time - self._exp_win_first_flip_time ) + self.progress_bar.set_description( + f"Trial {trial_n}:: {trial['condition']}:{trial['image_path']}" + ) # draw to backbuffer exp_win.logOnFlip(level=logging.EXP, msg="fixation") @@ -238,13 +239,12 @@ def _run(self, exp_win, ctl_win): keypresses = event.getKeys(self.RESPONSE_KEYS, timeStamped=self.task_timer) if len(keypresses): trial['keypresses'] = keypresses # log all keypresses with timing - #keypress = keypresses[0] # only take the first keypress, TODO: log extra keypresses? idxs = [np.where(self.RESPONSE_MAPPING == k[0]) for k in keypresses] response_mapping_flipped = self.RESPONSE_VALUES.copy() if trial["response_mapping_flip_h"]: - response_mapping_flipped = np.rot90(np.fliplr(self.RESPONSE_VALUES)) + response_mapping_flipped = np.rot90(np.fliplr(response_mapping_flipped)) if trial["response_mapping_flip_v"]: - response_mapping_flipped = np.rot90(np.fliplr(self.RESPONSE_VALUES),3) + response_mapping_flipped = np.rot90(np.fliplr(response_mapping_flipped),3) responses = [response_mapping_flipped[idx[0][0],idx[1][0]] for idx in idxs] main_key = keypresses[0] # take the first response as main one, to be decided @@ -254,13 +254,20 @@ def _run(self, exp_win, ctl_win): trial["error"] = trial["response_txt"] != trial["condition"] trial["response_confidence"] = abs(main_response) > 1 trial["response_time"] = (main_key[1] - trial["onset"]) - if trial['error']: print("error") + if trial['error']: + self.progress_bar.set_description( + f"Trial {trial_n}:: {trial['condition']}:{trial['image_path']} \u274c") + else: + self.progress_bar.set_description( + f"Trial {trial_n}:: {trial['condition']}:{trial['image_path']} \u2705") else: # we need to force empty values for the first trials # otherwise following values are not recorded!? for k in ['keypresses', 'response', 'response_txt', 'error', 'response_confidence', 'response_time']: trial[k] = '' - print('Warning: no response') + self.progress_bar.set_description( + f"{Fore.RED}Trial {trial_n}:: {trial['condition']}:{trial['image_path']}: no response{Fore.RESET}") + trial["duration_flip"] = trial["offset_flip"] - trial["onset_flip"] utils.wait_until(self.task_timer, trial["onset"] + RESPONSE_TIME + FINAL_WAIT) From 44f12270eb72e4547a2e01f94b0b958795f71a09 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 16 Mar 2021 13:33:50 -0400 Subject: [PATCH 123/170] fix colorspace errors with newer psychopy. --- src/tasks/videogame.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index da3047d5..e38467a3 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -101,9 +101,9 @@ def _render_graphics_sound(self, obs, sound_block, exp_win, ctl_win): def _stop(self, exp_win, ctl_win): self.game_sound.stop() - exp_win.setColor([0] * 3) + exp_win.setColor([0] * 3, colorSpace='rgb') if ctl_win: - ctl_win.setColor([0] * 3) + ctl_win.setColor([0] * 3, colorSpace='rgb') yield True def unload(self): @@ -264,9 +264,9 @@ def _run(self, exp_win, ctl_win): self._set_key_handler(exp_win) self._nlevels = 0 - exp_win.setColor([-1.0] * 3) + exp_win.setColor([-1.0] * 3, colorSpace='rgb') if ctl_win: - ctl_win.setColor([-1.0] * 3) + ctl_win.setColor([-1.0] * 3, colorSpace='rgb') while True: self._nlevels += 1 @@ -284,9 +284,9 @@ def _run(self, exp_win, ctl_win): break self.emulator.reset() - exp_win.setColor([0] * 3) + exp_win.setColor([0] * 3, colorSpace='rgb') if ctl_win: - ctl_win.setColor([0] * 3) + ctl_win.setColor([0] * 3, colorSpace='rgb') def _run_ratings(self, exp_win, ctl_win): for question, n_pts in self.post_level_ratings: @@ -453,9 +453,9 @@ def _run(self, exp_win, ctl_win): exp_win.waitBlanking = False - exp_win.setColor([-1.0] * 3) + exp_win.setColor([-1.0] * 3, colorSpace='rgb') if ctl_win: - ctl_win.setColor([-1.0] * 3) + ctl_win.setColor([-1.0] * 3, colorSpace='rgb') self._nlevels = 0 while True: From 195433ca48c72f71705ca6020f87af5d7d9f43fd Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 16 Mar 2021 13:37:37 -0400 Subject: [PATCH 124/170] fix wrong scenario name --- src/sessions/ses-shinobi3levels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sessions/ses-shinobi3levels.py b/src/sessions/ses-shinobi3levels.py index c31acd64..a7f0ce25 100644 --- a/src/sessions/ses-shinobi3levels.py +++ b/src/sessions/ses-shinobi3levels.py @@ -23,7 +23,7 @@ ] levels_scenario = [ - ("Level1-0", "scenario_repeat1"), + ("Level1-0", "scenario_Level1"), ("Level4-1", "scenario_Level4-1"), ("Level5-0", "scenario_Level5-0")] random.shuffle(levels_scenario) # randomize order From 993583ba9e5347005ce9bdd9713f0382653beab2 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 16 Mar 2021 13:50:36 -0400 Subject: [PATCH 125/170] fix: load state from custom only --- src/tasks/videogame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 28255537..e0fbaea6 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -479,7 +479,7 @@ def _run(self, exp_win, ctl_win): for level, scenario in zip(self._state_names, self._scenarii): self._nlevels += 1 self.state_name = level - self.emulator.load_state(level) + self.emulator.load_state(level, inttype=retro.data.Integrations.CUSTOM_ONLY) self.emulator.data.load( retro.data.get_file_path(self.game_name, "data.json"), retro.data.get_file_path(self.game_name, f"{scenario}.json") From ba6e224a6f6364539368360a03ebc74ce7b402a2 Mon Sep 17 00:00:00 2001 From: FrancoisPgm Date: Thu, 18 Mar 2021 12:12:06 -0400 Subject: [PATCH 126/170] add a suggestion if --task arg is wrong --- main.py | 5 ++++- requirements.txt | 1 + utils/didyoumean.py | 10 ++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 utils/didyoumean.py diff --git a/main.py b/main.py index f80f2ea6..854b35dc 100755 --- a/main.py +++ b/main.py @@ -4,6 +4,8 @@ import os, sys, importlib from src.shared import parser, config, screen +from utils.didyoumean import suggest_session_tasks + def run(parsed): # initializing the screen need to be done before loading any psychopy @@ -12,7 +14,8 @@ def run(parsed): ses_mod = importlib.import_module('src.sessions.ses-%s'%parsed.tasks) tasks = ses_mod.get_tasks(parsed) if hasattr(ses_mod, 'get_tasks') else ses_mod.TASKS except ImportError: - raise(ValueError('session tasks file cannot be found for %s'%parsed.session)) + suggestion = suggest_session_tasks(parsed.tasks) + raise(ValueError('session tasks file cannot be found for %s. Did you mean %s ?'%(parsed.tasks, suggestion))) from src.shared import cli try: cli.main_loop( diff --git a/requirements.txt b/requirements.txt index e5de3452..dc9d0ea9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ PsychoPy>=2020.2.4.post1 gym-retro>=0.8.0 pandas>=1.1.1 python-dotenv +textdistance diff --git a/utils/didyoumean.py b/utils/didyoumean.py new file mode 100644 index 00000000..d8367fc1 --- /dev/null +++ b/utils/didyoumean.py @@ -0,0 +1,10 @@ +import os +from importlib.util import find_spec +from textdistance import jaro + + +def suggest_session_tasks(query): + ses_dir_path = find_spec('src.sessions').submodule_search_locations._path[0] + avail_sess = [s.replace("ses-","").replace(".py","") for s in os.listdir(ses_dir_path)] + best_match = max(avail_sess, key=lambda x: jaro(x, query)) + return best_match From 6f792c0654aaba4506ba36412e1bed42031562d1 Mon Sep 17 00:00:00 2001 From: FrancoisPgm Date: Thu, 18 Mar 2021 12:16:33 -0400 Subject: [PATCH 127/170] Revert "add a reading task class for word by word reading" This reverts commit 2a07b567fa5b7548930ed71c1d1b1f3f48e59cd9. --- src/tasks/language.py | 73 ------------------------------------------- 1 file changed, 73 deletions(-) diff --git a/src/tasks/language.py b/src/tasks/language.py index 6ec5dcf2..62ec28c3 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -122,76 +122,3 @@ def _run(self, exp_win, ctl_win): def _save(self): self.trials.saveAsWideText(self._generate_unique_filename("events", "tsv")) return False - - -class Reading(Task): - - DEFAULT_INSTRUCTION = """You will be presented a text to read word by word.""" - - def __init__(self, words_file, word_duration=0.5, cross_duration=20, - txt_color="black", txt_font="Palatino", txt_size=42, - bg_color=(125,125,125), *args, **kwargs): - super().__init__(**kwargs) - if os.path.exists(words_file): - self.words_file = words_file - self.word_duration = word_duration - self.cross_duration = cross_duration - self.txt_color = txt_color - self.txt_font = txt_font - self.txt_size = txt_size - self.bg_color = bg_color - self.words_list = data.importConditions(self.words_file) - else: - raise ValueError("File %s does not exists" % words_file) - - def _instructions(self, exp_win, ctl_win): - screen_text = visual.TextStim( - exp_win, - text=self.instruction, - alignText="center", - color="white", - wrapWidth=config.WRAP_WIDTH, - ) - - for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): - screen_text.draw(exp_win) - if ctl_win: - screen_text.draw(ctl_win) - yield () - - def _run(self, exp_win, ctl_win): - exp_win.setColor(self.bg_color) - if ctl_win: - ctl_win.setColor(self.bg_color) - - txt_stim = visual.TextStim( - exp_win, - text="+", - font=self.txt_font, - height=self.txt_size, - alignText="center", - color=self.txt_color, - wrapWidth=config.WRAP_WIDTH, - ) - - # Display a centered cross for 20s - for frameN in range(config.FRAME_RATE * self.cross_duration): - txt_stim.draw(exp_win) - if ctl_win: - txt_stim.draw(ctl_win) - yield () - - # Display each word for 0.5s - for word in self.words_list: - italic = False - if word[0] == "@": - italic = True - word = word[1:] - txt_stim.text = word - txt_stim.italic = italic - - for frameN in range(config.FRAME_RATE * WORD_DURATION): - txt_stim.draw(exp_win) - if ctl_win: - txt_stim.draw(ctl_win) - yield () From f642eb223c8efe51ffe6b3075038b31b3dae6e30 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 19 Mar 2021 11:03:35 -0400 Subject: [PATCH 128/170] remove randomization, fix loading of custom integration --- src/sessions/ses-shinobi3levels.py | 2 +- src/tasks/videogame.py | 41 ++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/sessions/ses-shinobi3levels.py b/src/sessions/ses-shinobi3levels.py index a7f0ce25..8c80ad9e 100644 --- a/src/sessions/ses-shinobi3levels.py +++ b/src/sessions/ses-shinobi3levels.py @@ -26,7 +26,7 @@ ("Level1-0", "scenario_Level1"), ("Level4-1", "scenario_Level4-1"), ("Level5-0", "scenario_Level5-0")] -random.shuffle(levels_scenario) # randomize order +#random.shuffle(levels_scenario) # randomize order TASKS = sum( [ diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index e0fbaea6..f1f7bcab 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -67,6 +67,25 @@ def _nextBlock(self): class VideoGameBase(Task): + + def __init__( + self, + game_name=DEFAULT_GAME_NAME, + state_name=None, + scenario=None, + repeat_scenario=True, + inttype=retro.data.Integrations.CUSTOM_ONLY, + *args, + **kwargs + ): + + super().__init__(**kwargs) + self.game_name = game_name + self.state_name = state_name + self.scenario = scenario + self.repeat_scenario = repeat_scenario + self.inttype = inttype + def _setup(self, exp_win): self.game_sound = SoundDeviceBlockStream(stereo=True, blockSize=735) self._first_frame = self.emulator.reset() @@ -116,10 +135,6 @@ class VideoGame(VideoGameBase): def __init__( self, - game_name=DEFAULT_GAME_NAME, - state_name=None, - scenario=None, - repeat_scenario=True, max_duration=0, post_level_ratings=None, *args, @@ -127,10 +142,6 @@ def __init__( ): super().__init__(**kwargs) - self.game_name = game_name - self.state_name = state_name - self.scenario = scenario - self.repeat_scenario = repeat_scenario self.max_duration = max_duration self.duration = max_duration self.post_level_ratings = post_level_ratings @@ -170,7 +181,7 @@ def _setup(self, exp_win): state=self.state_name, scenario=self.scenario, record=False, - inttype=retro.data.Integrations.CUSTOM_ONLY + inttype=self.inttype ) self.game_fps = self.emulator.em.get_screen_rate() @@ -479,10 +490,14 @@ def _run(self, exp_win, ctl_win): for level, scenario in zip(self._state_names, self._scenarii): self._nlevels += 1 self.state_name = level - self.emulator.load_state(level, inttype=retro.data.Integrations.CUSTOM_ONLY) + self.emulator.load_state(level, inttype=self.inttype) + print( + retro.data.get_file_path(self.game_name, "data.json", inttype=self.inttype), + retro.data.get_file_path(self.game_name, f"{scenario}.json", inttype=self.inttype) + ) self.emulator.data.load( - retro.data.get_file_path(self.game_name, "data.json"), - retro.data.get_file_path(self.game_name, f"{scenario}.json") + retro.data.get_file_path(self.game_name, "data.json", inttype=self.inttype), + retro.data.get_file_path(self.game_name, f"{scenario}.json", inttype=self.inttype) ) self._first_frame = self.emulator.reset() if self._nlevels > 1: @@ -512,8 +527,6 @@ class VideoGameReplay(VideoGameBase): def __init__( self, movie_filename, - game_name=DEFAULT_GAME_NAME, - scenario=None, *args, **kwargs ): From 5f32ff95146e666f67cc856f922d4c055d0ffd1b Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 23 Mar 2021 13:27:02 -0400 Subject: [PATCH 129/170] make calibration restart if failed. add instruction for rolling eyes --- src/shared/eyetracking.py | 170 +++++++++++++++++++++----------------- 1 file changed, 93 insertions(+), 77 deletions(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index 99e525cf..391ab7f6 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -69,7 +69,9 @@ def __init__( def _instructions(self, exp_win, ctl_win): instruction_text = """We're going to calibrate the eyetracker. -Please look at the markers that appear on the screen.""" +Please look at the markers that appear on the screen. + +While awaiting for the calibration to start please roll your eyes in one direction then in the other.""" screen_text = visual.TextStim( exp_win, text=instruction_text, @@ -85,7 +87,6 @@ def _instructions(self, exp_win, ctl_win): def _setup(self, exp_win): self.use_fmri = False - self._pupils_list = [] def _pupil_cb(self, pupil): if pupil["timestamp"] > self.task_stop: @@ -95,88 +96,99 @@ def _pupil_cb(self, pupil): self._pupils_list.append(pupil) def _run(self, exp_win, ctl_win): - while True: - allKeys = event.getKeys([CALIBRATE_HOTKEY]) - start_calibration = False - for key in allKeys: - if key == CALIBRATE_HOTKEY: - start_calibration = True - if start_calibration: - break - yield False - logging.info("calibration started") - print("calibration started") - - window_size_frame = exp_win.size - MARKER_SIZE * 2 - circle_marker = visual.Circle( - exp_win, - edges=64, - units="pixels", - lineColor=None, - fillColor=self.marker_fill_color, - autoLog=False, - ) + calibration_success = False + while not calibration_success: + while True: + allKeys = event.getKeys([CALIBRATE_HOTKEY]) + start_calibration = False + for key in allKeys: + if key == CALIBRATE_HOTKEY: + start_calibration = True + if start_calibration: + break + yield False + logging.info("calibration started") + print("calibration started") + + window_size_frame = exp_win.size - MARKER_SIZE * 2 + circle_marker = visual.Circle( + exp_win, + edges=64, + units="pixels", + lineColor=None, + fillColor=self.marker_fill_color, + autoLog=False, + ) - markers_order = np.arange(len(self.markers)) - if self.markers_order == "random": - markers_order = np.random.permutation(markers_order) + markers_order = np.arange(len(self.markers)) + if self.markers_order == "random": + markers_order = np.random.permutation(markers_order) - self.all_refs_per_flip = [] + self.all_refs_per_flip = [] + self._pupils_list = [] - radius_anim = np.hstack( - [ - np.linspace(MARKER_SIZE, 0, MARKER_DURATION_FRAMES // 2), - np.linspace(0, MARKER_SIZE, MARKER_DURATION_FRAMES // 2), - ] - ) + radius_anim = np.hstack( + [ + np.linspace(MARKER_SIZE, 0, MARKER_DURATION_FRAMES // 2), + np.linspace(0, MARKER_SIZE, MARKER_DURATION_FRAMES // 2), + ] + ) - self.task_start = time.monotonic() - self.task_stop = np.inf - self.eyetracker.set_pupil_cb(self._pupil_cb) + self.task_start = time.monotonic() + self.task_stop = np.inf + self.eyetracker.set_pupil_cb(self._pupil_cb) - while not len(self._pupils_list): # wait until we get at least a pupil - yield False + while not len(self._pupils_list): # wait until we get at least a pupil + yield False - exp_win.logOnFlip( - level=logging.EXP, - msg="eyetracker_calibration: starting at %f" % time.time(), - ) - for site_id in markers_order: - marker_pos = self.markers[site_id] - pos = (marker_pos - 0.5) * window_size_frame - circle_marker.pos = pos exp_win.logOnFlip( level=logging.EXP, - msg="calibrate_position,%d,%d,%d,%d" - % (marker_pos[0], marker_pos[1], pos[0], pos[1]), + msg="eyetracker_calibration: starting at %f" % time.time(), ) - exp_win.callOnFlip( - self._log_event, {"marker_x": pos[0], "marker_y": pos[1]} + for site_id in markers_order: + marker_pos = self.markers[site_id] + pos = (marker_pos - 0.5) * window_size_frame + circle_marker.pos = pos + exp_win.logOnFlip( + level=logging.EXP, + msg="calibrate_position,%d,%d,%d,%d" + % (marker_pos[0], marker_pos[1], pos[0], pos[1]), + ) + exp_win.callOnFlip( + self._log_event, {"marker_x": pos[0], "marker_y": pos[1]} + ) + for f, r in enumerate(radius_anim): + circle_marker.radius = r + circle_marker.draw(exp_win) + circle_marker.draw(ctl_win) + + if ( + f > CALIBRATION_LEAD_IN + and f < len(radius_anim) - CALIBRATION_LEAD_OUT + ): + screen_pos = pos + exp_win.size / 2 + norm_pos = screen_pos / exp_win.size + ref = { + "norm_pos": norm_pos.tolist(), + "screen_pos": screen_pos.tolist(), + "timestamp": time.monotonic(), # =pupil frame timestamp on same computer + } + self.all_refs_per_flip.append(ref) # accumulate all refs + yield True + yield True + self.task_stop = time.monotonic() + logging.info( + f"calibrating on {len(self._pupils_list)} pupils and {len(self.all_refs_per_flip)} markers" ) - for f, r in enumerate(radius_anim): - circle_marker.radius = r - circle_marker.draw(exp_win) - circle_marker.draw(ctl_win) - - if ( - f > CALIBRATION_LEAD_IN - and f < len(radius_anim) - CALIBRATION_LEAD_OUT - ): - screen_pos = pos + exp_win.size / 2 - norm_pos = screen_pos / exp_win.size - ref = { - "norm_pos": norm_pos.tolist(), - "screen_pos": screen_pos.tolist(), - "timestamp": time.monotonic(), # =pupil frame timestamp on same computer - } - self.all_refs_per_flip.append(ref) # accumulate all refs - yield True - yield True - self.task_stop = time.monotonic() - logging.info( - f"calibrating on {len(self._pupils_list)} pupils and {len(self.all_refs_per_flip)} markers" - ) - self.eyetracker.calibrate(self._pupils_list, self.all_refs_per_flip) + self.eyetracker.calibrate(self._pupils_list, self.all_refs_per_flip) + while True: + notes = getattr(self.eyetracker, '_last_calibration_notification',None) + if notes: + calibration_success = notes['topic'].startswith("notify.calibration.successful") + if not calibration_success: + print('#### CALIBRATION FAILED: restart with ####') + break + def stop(self, exp_win, ctl_win): self.eyetracker.unset_pupil_cb() @@ -337,7 +349,8 @@ def run(self): ipc_sub_port = int(self._req_socket.recv()) logging.info(f"ipc_sub_port: {ipc_sub_port}") self.pupil_monitor = Msg_Receiver( - self._ctx, f"tcp://localhost:{ipc_sub_port}", topics=("gaze", "pupil") + self._ctx, f"tcp://localhost:{ipc_sub_port}", + topics=("gaze", "pupil", "notify.calibration.successful", "notify.calibration.failed") ) while not self.stoprequest.isSet(): msg = self.pupil_monitor.recv() @@ -350,6 +363,8 @@ def run(self): self._pupil_cb(tmp) elif topic.startswith("gaze"): self.gaze = tmp + elif topic.startswith("notify.calibration"): + self._last_calibration_notification = tmp time.sleep(1e-3) logging.info("eyetracker listener: stopping") @@ -377,16 +392,17 @@ def calibrate(self, pupil_list, ref_list): calib_data = {"ref_list": ref_list, "pupil_list": pupil_list} logging.info("sending calibration data to pupil") - self.send_recv_notification( + calib_res = self.send_recv_notification( { "subject": "start_plugin", "name": "Gazer2D", "args": {"calib_data": calib_data}, - "raise_calibration_error": True, + "raise_calibration_error": False, } ) + class GazeDrawer: def __init__(self, win): From 025a66f9f7aa4ec7bc3c148b11d9a79730a977d7 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 24 Mar 2021 09:49:42 -0400 Subject: [PATCH 130/170] add yield to clear things instructions --- src/tasks/things.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tasks/things.py b/src/tasks/things.py index 034e59de..7eabe718 100644 --- a/src/tasks/things.py +++ b/src/tasks/things.py @@ -168,6 +168,7 @@ def _instructions(self, exp_win, ctl_win): if ctl_win: screen_text.draw(ctl_win) yield frameN < 3 + yield True screen_text.text = self.EXTRA_INSTRUCTION if self.run_id == 1: for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION * 2): @@ -177,6 +178,7 @@ def _instructions(self, exp_win, ctl_win): screen_text.draw(ctl_win) self._response_mapping.draw(ctl_win) yield frameN + yield True def _setup(self, exp_win): super()._setup(exp_win) From 033e446849feb03b25dd26ed5715038792419ecd Mon Sep 17 00:00:00 2001 From: FrancoisPgm Date: Wed, 31 Mar 2021 19:15:39 -0400 Subject: [PATCH 131/170] Add events tasks, load them with pd.read_csv --- .../harrypotter/task-harry_run-1_events.tsv | 1304 +++++++++++++ .../harrypotter/task-harry_run-2_events.tsv | 1352 ++++++++++++++ .../harrypotter/task-harry_run-3_events.tsv | 1060 +++++++++++ .../harrypotter/task-harry_run-4_events.tsv | 1464 +++++++++++++++ .../harrypotter/task-harry_run-5_events.tsv | 1634 +++++++++++++++++ .../harrypotter/task-harry_run-6_events.tsv | 1298 +++++++++++++ .../harrypotter/task-harry_run-7_events.tsv | 1546 ++++++++++++++++ src/sessions/ses-harrypotter.py | 5 +- src/tasks/language.py | 22 +- 9 files changed, 9674 insertions(+), 11 deletions(-) create mode 100644 data/language/harrypotter/task-harry_run-1_events.tsv create mode 100644 data/language/harrypotter/task-harry_run-2_events.tsv create mode 100644 data/language/harrypotter/task-harry_run-3_events.tsv create mode 100644 data/language/harrypotter/task-harry_run-4_events.tsv create mode 100644 data/language/harrypotter/task-harry_run-5_events.tsv create mode 100644 data/language/harrypotter/task-harry_run-6_events.tsv create mode 100644 data/language/harrypotter/task-harry_run-7_events.tsv diff --git a/data/language/harrypotter/task-harry_run-1_events.tsv b/data/language/harrypotter/task-harry_run-1_events.tsv new file mode 100644 index 00000000..be2e3ce5 --- /dev/null +++ b/data/language/harrypotter/task-harry_run-1_events.tsv @@ -0,0 +1,1304 @@ +word format onset duration +Harry 10.0 0.5 +had 10.5 0.5 +never 11.0 0.5 +believed 11.5 0.5 +he 12.0 0.5 +would 12.5 0.5 +meet 13.0 0.5 +a 13.5 0.5 +boy 14.0 0.5 +he 14.5 0.5 +hated 15.0 0.5 +more 15.5 0.5 +than 16.0 0.5 +Dudley, 16.5 0.5 +but 17.0 0.5 +that 17.5 0.5 +was 18.0 0.5 +before 18.5 0.5 +he 19.0 0.5 +met 19.5 0.5 +Draco 20.0 0.5 +Malfoy. 20.5 0.5 +Still, 21.0 0.5 +first-year 21.5 0.5 +Gryffindors 22.0 0.5 +only 22.5 0.5 +had 23.0 0.5 +Potions 23.5 0.5 +with 24.0 0.5 +the 24.5 0.5 +Slytherins, 25.0 0.5 +so 25.5 0.5 +they 26.0 0.5 +didn't 26.5 0.5 +have 27.0 0.5 +to 27.5 0.5 +put 28.0 0.5 +up 28.5 0.5 +with 29.0 0.5 +Malfoy 29.5 0.5 +much. 30.0 0.5 +Or 30.5 0.5 +at 31.0 0.5 +least, 31.5 0.5 +they 32.0 0.5 +didn't 32.5 0.5 +until 33.0 0.5 +they 33.5 0.5 +spotted 34.0 0.5 +a 34.5 0.5 +notice 35.0 0.5 +pinned 35.5 0.5 +up 36.0 0.5 +in 36.5 0.5 +the 37.0 0.5 +Gryffindor 37.5 0.5 +common 38.0 0.5 +room 38.5 0.5 +that 39.0 0.5 +made 39.5 0.5 +them 40.0 0.5 +all 40.5 0.5 +groan. 41.0 0.5 +Flying 41.5 0.5 +lessons 42.0 0.5 +would 42.5 0.5 +be 43.0 0.5 +starting 43.5 0.5 +on 44.0 0.5 +Thursday 44.5 0.5 +-- 45.0 0.5 +and 45.5 0.5 +Gryffindor 46.0 0.5 +and 46.5 0.5 +Slytherin 47.0 0.5 +would 47.5 0.5 +be 48.0 0.5 +learning 48.5 0.5 +together. 49.0 0.5 ++ 49.5 0.5 +"""Typical,""" 50.0 0.5 +said 50.5 0.5 +Harry 51.0 0.5 +darkly. 51.5 0.5 +"""Just" 52.0 0.5 +what 52.5 0.5 +I 53.0 0.5 +always 53.5 0.5 +wanted. 54.0 0.5 +To 54.5 0.5 +make 55.0 0.5 +a 55.5 0.5 +fool 56.0 0.5 +of 56.5 0.5 +myself 57.0 0.5 +on 57.5 0.5 +a 58.0 0.5 +broomstick 58.5 0.5 +in 59.0 0.5 +front 59.5 0.5 +of 60.0 0.5 +"Malfoy.""" 60.5 0.5 ++ 61.0 0.5 +He 61.5 0.5 +had 62.0 0.5 +been 62.5 0.5 +looking 63.0 0.5 +forward 63.5 0.5 +to 64.0 0.5 +learning 64.5 0.5 +to 65.0 0.5 +fly 65.5 0.5 +more 66.0 0.5 +than 66.5 0.5 +anything 67.0 0.5 +else. 67.5 0.5 +"""You" 68.0 0.5 +don't 68.5 0.5 +know 69.0 0.5 +that 69.5 0.5 +you'll 70.0 0.5 +make 70.5 0.5 +a 71.0 0.5 +fool 71.5 0.5 +of 72.0 0.5 +"yourself,""" 72.5 0.5 +said 73.0 0.5 +Ron 73.5 0.5 +reasonably. 74.0 0.5 +"""Anyway," 74.5 0.5 +I 75.0 0.5 +know 75.5 0.5 +Malfoy's 76.0 0.5 +always 76.5 0.5 +going 77.0 0.5 +on 77.5 0.5 +about 78.0 0.5 +how 78.5 0.5 +good 79.0 0.5 +he 79.5 0.5 +is 80.0 0.5 +at 80.5 0.5 +Quidditch, 81.0 0.5 +but 81.5 0.5 +I 82.0 0.5 +bet 82.5 0.5 +that's 83.0 0.5 +all 83.5 0.5 +"talk.""" 84.0 0.5 ++ 84.5 0.5 +Malfoy 85.0 0.5 +certainly 85.5 0.5 +did 86.0 0.5 +talk 86.5 0.5 +about 87.0 0.5 +flying 87.5 0.5 +a 88.0 0.5 +lot. 88.5 0.5 +He 89.0 0.5 +complained 89.5 0.5 +loudly 90.0 0.5 +about 90.5 0.5 +first 91.0 0.5 +years 91.5 0.5 +never 92.0 0.5 +getting 92.5 0.5 +on 93.0 0.5 +the 93.5 0.5 +House 94.0 0.5 +Quidditch 94.5 0.5 +teams 95.0 0.5 +and 95.5 0.5 +told 96.0 0.5 +long, 96.5 0.5 +boastful 97.0 0.5 +stories 97.5 0.5 +that 98.0 0.5 +always 98.5 0.5 +seemed 99.0 0.5 +to 99.5 0.5 +end 100.0 0.5 +with 100.5 0.5 +him 101.0 0.5 +narrowly 101.5 0.5 +escaping 102.0 0.5 +Muggles 102.5 0.5 +in 103.0 0.5 +helicopters. 103.5 0.5 +He 104.0 0.5 +wasn't 104.5 0.5 +the 105.0 0.5 +only 105.5 0.5 +one, 106.0 0.5 +though: 106.5 0.5 +the 107.0 0.5 +way 107.5 0.5 +Seamus 108.0 0.5 +Finnigan 108.5 0.5 +told 109.0 0.5 +it, 109.5 0.5 +he'd 110.0 0.5 +spent 110.5 0.5 +most 111.0 0.5 +of 111.5 0.5 +his 112.0 0.5 +childhood 112.5 0.5 +zooming 113.0 0.5 +around 113.5 0.5 +the 114.0 0.5 +countryside 114.5 0.5 +on 115.0 0.5 +his 115.5 0.5 +broomstick. 116.0 0.5 +Even 116.5 0.5 +Ron 117.0 0.5 +would 117.5 0.5 +tell 118.0 0.5 +anyone 118.5 0.5 +who'd 119.0 0.5 +listen 119.5 0.5 +about 120.0 0.5 +the 120.5 0.5 +time 121.0 0.5 +he'd 121.5 0.5 +almost 122.0 0.5 +hit 122.5 0.5 +a 123.0 0.5 +hang 123.5 0.5 +glider 124.0 0.5 +on 124.5 0.5 +Charlie's 125.0 0.5 +old 125.5 0.5 +broom. 126.0 0.5 +Everyone 126.5 0.5 +from 127.0 0.5 +wizarding 127.5 0.5 +families 128.0 0.5 +talked 128.5 0.5 +about 129.0 0.5 +Quidditch 129.5 0.5 +constantly. 130.0 0.5 +Ron 130.5 0.5 +had 131.0 0.5 +already 131.5 0.5 +had 132.0 0.5 +a 132.5 0.5 +big 133.0 0.5 +argument 133.5 0.5 +with 134.0 0.5 +Dean 134.5 0.5 +Thomas, 135.0 0.5 +who 135.5 0.5 +shared 136.0 0.5 +their 136.5 0.5 +dormitory, 137.0 0.5 +about 137.5 0.5 +soccer. 138.0 0.5 +Ron 138.5 0.5 +couldn't 139.0 0.5 +see 139.5 0.5 +what 140.0 0.5 +was 140.5 0.5 +exciting 141.0 0.5 +about 141.5 0.5 +a 142.0 0.5 +game 142.5 0.5 +with 143.0 0.5 +only 143.5 0.5 +one 144.0 0.5 +ball 144.5 0.5 +where 145.0 0.5 +no 145.5 0.5 +one 146.0 0.5 +was 146.5 0.5 +allowed 147.0 0.5 +to 147.5 0.5 +fly. 148.0 0.5 +Harry 148.5 0.5 +had 149.0 0.5 +caught 149.5 0.5 +Ron 150.0 0.5 +prodding 150.5 0.5 +Dean's 151.0 0.5 +poster 151.5 0.5 +of 152.0 0.5 +West 152.5 0.5 +Ham 153.0 0.5 +soccer 153.5 0.5 +team, 154.0 0.5 +trying 154.5 0.5 +to 155.0 0.5 +make 155.5 0.5 +the 156.0 0.5 +players 156.5 0.5 +move. 157.0 0.5 ++ 157.5 0.5 +Neville 158.0 0.5 +had 158.5 0.5 +never 159.0 0.5 +been 159.5 0.5 +on 160.0 0.5 +a 160.5 0.5 +broomstick 161.0 0.5 +in 161.5 0.5 +his 162.0 0.5 +life, 162.5 0.5 +because 163.0 0.5 +his 163.5 0.5 +grandmother 164.0 0.5 +had 164.5 0.5 +never 165.0 0.5 +let 165.5 0.5 +him 166.0 0.5 +near 166.5 0.5 +one. 167.0 0.5 +Privately, 167.5 0.5 +Harry 168.0 0.5 +felt 168.5 0.5 +she'd 169.0 0.5 +had 169.5 0.5 +good 170.0 0.5 +reason, 170.5 0.5 +because 171.0 0.5 +Neville 171.5 0.5 +managed 172.0 0.5 +to 172.5 0.5 +have 173.0 0.5 +an 173.5 0.5 +extraordinary 174.0 0.5 +number 174.5 0.5 +of 175.0 0.5 +accidents 175.5 0.5 +even 176.0 0.5 +with 176.5 0.5 +both 177.0 0.5 +feet 177.5 0.5 +on 178.0 0.5 +the 178.5 0.5 +ground. 179.0 0.5 ++ 179.5 0.5 +Hermione 180.0 0.5 +Granger 180.5 0.5 +was 181.0 0.5 +almost 181.5 0.5 +as 182.0 0.5 +nervous 182.5 0.5 +about 183.0 0.5 +flying 183.5 0.5 +as 184.0 0.5 +Neville 184.5 0.5 +was. 185.0 0.5 +This 185.5 0.5 +was 186.0 0.5 +something 186.5 0.5 +you 187.0 0.5 +couldn't 187.5 0.5 +learn 188.0 0.5 +by 188.5 0.5 +heart 189.0 0.5 +out 189.5 0.5 +of 190.0 0.5 +a 190.5 0.5 +book 191.0 0.5 +-- 191.5 0.5 +not 192.0 0.5 +that 192.5 0.5 +she 193.0 0.5 +hadn't 193.5 0.5 +tried. 194.0 0.5 +At 194.5 0.5 +breakfast 195.0 0.5 +on 195.5 0.5 +Thursday 196.0 0.5 +she 196.5 0.5 +bored 197.0 0.5 +them 197.5 0.5 +all 198.0 0.5 +stupid 198.5 0.5 +with 199.0 0.5 +flying 199.5 0.5 +tips 200.0 0.5 +she'd 200.5 0.5 +gotten 201.0 0.5 +out 201.5 0.5 +of 202.0 0.5 +a 202.5 0.5 +library 203.0 0.5 +book 203.5 0.5 +called 204.0 0.5 +Quidditch italic 204.5 0.5 +Through italic 205.0 0.5 +the italic 205.5 0.5 +Ages. italic 206.0 0.5 +Neville 206.5 0.5 +was 207.0 0.5 +hanging 207.5 0.5 +on 208.0 0.5 +to 208.5 0.5 +her 209.0 0.5 +every 209.5 0.5 +word, 210.0 0.5 +desperate 210.5 0.5 +for 211.0 0.5 +anything 211.5 0.5 +that 212.0 0.5 +might 212.5 0.5 +help 213.0 0.5 +him 213.5 0.5 +hang 214.0 0.5 +on 214.5 0.5 +to 215.0 0.5 +his 215.5 0.5 +broomstick 216.0 0.5 +later, 216.5 0.5 +but 217.0 0.5 +everybody 217.5 0.5 +else 218.0 0.5 +was 218.5 0.5 +very 219.0 0.5 +pleased 219.5 0.5 +when 220.0 0.5 +Hermione's 220.5 0.5 +lecture 221.0 0.5 +was 221.5 0.5 +interrupted 222.0 0.5 +by 222.5 0.5 +the 223.0 0.5 +arrival 223.5 0.5 +of 224.0 0.5 +the 224.5 0.5 +mail. 225.0 0.5 ++ 225.5 0.5 +Harry 226.0 0.5 +hadn't 226.5 0.5 +had 227.0 0.5 +a 227.5 0.5 +single 228.0 0.5 +letter 228.5 0.5 +since 229.0 0.5 +Hagrid's 229.5 0.5 +note, 230.0 0.5 +something 230.5 0.5 +that 231.0 0.5 +Malfoy 231.5 0.5 +had 232.0 0.5 +been 232.5 0.5 +quick 233.0 0.5 +to 233.5 0.5 +notice, 234.0 0.5 +of 234.5 0.5 +course. 235.0 0.5 +Malfoy's 235.5 0.5 +eagle 236.0 0.5 +owl 236.5 0.5 +was 237.0 0.5 +always 237.5 0.5 +bringing 238.0 0.5 +him 238.5 0.5 +packages 239.0 0.5 +of 239.5 0.5 +sweets 240.0 0.5 +from 240.5 0.5 +home, 241.0 0.5 +which 241.5 0.5 +he 242.0 0.5 +opened 242.5 0.5 +gloatingly 243.0 0.5 +at 243.5 0.5 +the 244.0 0.5 +Slytherin 244.5 0.5 +table. 245.0 0.5 ++ 245.5 0.5 +A 246.0 0.5 +barn 246.5 0.5 +owl 247.0 0.5 +brought 247.5 0.5 +Neville 248.0 0.5 +a 248.5 0.5 +small 249.0 0.5 +package 249.5 0.5 +from 250.0 0.5 +his 250.5 0.5 +grandmother. 251.0 0.5 +He 251.5 0.5 +opened 252.0 0.5 +it 252.5 0.5 +excitedly 253.0 0.5 +and 253.5 0.5 +showed 254.0 0.5 +them 254.5 0.5 +a 255.0 0.5 +glass 255.5 0.5 +ball 256.0 0.5 +the 256.5 0.5 +size 257.0 0.5 +of 257.5 0.5 +a 258.0 0.5 +large 258.5 0.5 +marble, 259.0 0.5 +which 259.5 0.5 +seemed 260.0 0.5 +to 260.5 0.5 +be 261.0 0.5 +full 261.5 0.5 +of 262.0 0.5 +white 262.5 0.5 +smoke. 263.0 0.5 ++ 263.5 0.5 +"""It's" 264.0 0.5 +a 264.5 0.5 +"Remembrall!""" 265.0 0.5 +he 265.5 0.5 +explained. 266.0 0.5 +"""Gran" 266.5 0.5 +knows 267.0 0.5 +I 267.5 0.5 +forget 268.0 0.5 +things 268.5 0.5 +-- 269.0 0.5 +this 269.5 0.5 +tells 270.0 0.5 +you 270.5 0.5 +if 271.0 0.5 +there's 271.5 0.5 +something 272.0 0.5 +you've 272.5 0.5 +forgotten 273.0 0.5 +to 273.5 0.5 +do. 274.0 0.5 +Look, 274.5 0.5 +you 275.0 0.5 +hold 275.5 0.5 +it 276.0 0.5 +tight 276.5 0.5 +like 277.0 0.5 +this 277.5 0.5 +and 278.0 0.5 +if 278.5 0.5 +it 279.0 0.5 +turns 279.5 0.5 +red 280.0 0.5 +-- 280.5 0.5 +oh 281.0 0.5 +"...""" 281.5 0.5 +His 282.0 0.5 +face 282.5 0.5 +fell, 283.0 0.5 +because 283.5 0.5 +the 284.0 0.5 +Remembrall 284.5 0.5 +had 285.0 0.5 +suddenly 285.5 0.5 +glowed 286.0 0.5 +scarlet, 286.5 0.5 +"""..." 287.0 0.5 +you've 287.5 0.5 +forgotten 288.0 0.5 +something 288.5 0.5 +"...""" 289.0 0.5 ++ 289.5 0.5 +Neville 290.0 0.5 +was 290.5 0.5 +trying 291.0 0.5 +to 291.5 0.5 +remember 292.0 0.5 +what 292.5 0.5 +he'd 293.0 0.5 +forgotten 293.5 0.5 +when 294.0 0.5 +Draco 294.5 0.5 +Malfoy, 295.0 0.5 +who 295.5 0.5 +was 296.0 0.5 +passing 296.5 0.5 +the 297.0 0.5 +Gryffindor 297.5 0.5 +table, 298.0 0.5 +snatched 298.5 0.5 +the 299.0 0.5 +Remembrall 299.5 0.5 +out 300.0 0.5 +of 300.5 0.5 +his 301.0 0.5 +hand. 301.5 0.5 ++ 302.0 0.5 +Harry 302.5 0.5 +and 303.0 0.5 +Ron 303.5 0.5 +jumped 304.0 0.5 +to 304.5 0.5 +their 305.0 0.5 +feet. 305.5 0.5 +They 306.0 0.5 +were 306.5 0.5 +half 307.0 0.5 +hoping 307.5 0.5 +for 308.0 0.5 +a 308.5 0.5 +reason 309.0 0.5 +to 309.5 0.5 +fight 310.0 0.5 +Malfoy, 310.5 0.5 +but 311.0 0.5 +Professor 311.5 0.5 +McGonagall, 312.0 0.5 +who 312.5 0.5 +could 313.0 0.5 +spot 313.5 0.5 +trouble 314.0 0.5 +quicker 314.5 0.5 +than 315.0 0.5 +any 315.5 0.5 +teacher 316.0 0.5 +in 316.5 0.5 +the 317.0 0.5 +school, 317.5 0.5 +was 318.0 0.5 +there 318.5 0.5 +in 319.0 0.5 +a 319.5 0.5 +flash. 320.0 0.5 ++ 320.5 0.5 +"""What's" 321.0 0.5 +going 321.5 0.5 +"on?""" 322.0 0.5 ++ 322.5 0.5 +"""Malfoy's" 323.0 0.5 +got 323.5 0.5 +my 324.0 0.5 +Remembrall, 324.5 0.5 +"Professor.""" 325.0 0.5 ++ 325.5 0.5 +Scowling, 326.0 0.5 +Malfoy 326.5 0.5 +quickly 327.0 0.5 +dropped 327.5 0.5 +the 328.0 0.5 +Remembrall 328.5 0.5 +back 329.0 0.5 +on 329.5 0.5 +the 330.0 0.5 +table. 330.5 0.5 ++ 331.0 0.5 +"""Just" 331.5 0.5 +"looking,""" 332.0 0.5 +he 332.5 0.5 +said, 333.0 0.5 +and 333.5 0.5 +he 334.0 0.5 +sloped 334.5 0.5 +away 335.0 0.5 +with 335.5 0.5 +Crabbe 336.0 0.5 +and 336.5 0.5 +Goyle 337.0 0.5 +behind 337.5 0.5 +him. 338.0 0.5 ++ 338.5 0.5 ++ 339.0 0.5 +At 339.5 0.5 +three-thirty 340.0 0.5 +that 340.5 0.5 +afternoon, 341.0 0.5 +Harry, 341.5 0.5 +Ron, 342.0 0.5 +and 342.5 0.5 +the 343.0 0.5 +other 343.5 0.5 +Gryffindors 344.0 0.5 +hurried 344.5 0.5 +down 345.0 0.5 +the 345.5 0.5 +front 346.0 0.5 +steps 346.5 0.5 +onto 347.0 0.5 +the 347.5 0.5 +grounds 348.0 0.5 +for 348.5 0.5 +their 349.0 0.5 +first 349.5 0.5 +flying 350.0 0.5 +lesson. 350.5 0.5 +It 351.0 0.5 +was 351.5 0.5 +a 352.0 0.5 +clear, 352.5 0.5 +breezy 353.0 0.5 +day, 353.5 0.5 +and 354.0 0.5 +the 354.5 0.5 +grass 355.0 0.5 +rippled 355.5 0.5 +under 356.0 0.5 +their 356.5 0.5 +feet 357.0 0.5 +as 357.5 0.5 +they 358.0 0.5 +marched 358.5 0.5 +down 359.0 0.5 +the 359.5 0.5 +sloping 360.0 0.5 +lawns 360.5 0.5 +toward 361.0 0.5 +a 361.5 0.5 +smooth, 362.0 0.5 +flat 362.5 0.5 +lawn 363.0 0.5 +on 363.5 0.5 +the 364.0 0.5 +opposite 364.5 0.5 +side 365.0 0.5 +of 365.5 0.5 +the 366.0 0.5 +grounds 366.5 0.5 +to 367.0 0.5 +the 367.5 0.5 +forbidden 368.0 0.5 +forest, 368.5 0.5 +whose 369.0 0.5 +trees 369.5 0.5 +were 370.0 0.5 +swaying 370.5 0.5 +darkly 371.0 0.5 +in 371.5 0.5 +the 372.0 0.5 +distance. 372.5 0.5 ++ 373.0 0.5 +The 373.5 0.5 +Slytherins 374.0 0.5 +were 374.5 0.5 +already 375.0 0.5 +there, 375.5 0.5 +and 376.0 0.5 +so 376.5 0.5 +were 377.0 0.5 +twenty 377.5 0.5 +broomsticks 378.0 0.5 +lying 378.5 0.5 +in 379.0 0.5 +neat 379.5 0.5 +lines 380.0 0.5 +on 380.5 0.5 +the 381.0 0.5 +ground. 381.5 0.5 +Harry 382.0 0.5 +had 382.5 0.5 +heard 383.0 0.5 +Fred 383.5 0.5 +and 384.0 0.5 +George 384.5 0.5 +Weasley 385.0 0.5 +complain 385.5 0.5 +about 386.0 0.5 +the 386.5 0.5 +school 387.0 0.5 +brooms, 387.5 0.5 +saying 388.0 0.5 +that 388.5 0.5 +some 389.0 0.5 +of 389.5 0.5 +them 390.0 0.5 +started 390.5 0.5 +to 391.0 0.5 +vibrate 391.5 0.5 +if 392.0 0.5 +you 392.5 0.5 +flew 393.0 0.5 +too 393.5 0.5 +high, 394.0 0.5 +or 394.5 0.5 +always 395.0 0.5 +flew 395.5 0.5 +slightly 396.0 0.5 +to 396.5 0.5 +the 397.0 0.5 +left. 397.5 0.5 ++ 398.0 0.5 +Their 398.5 0.5 +teacher, 399.0 0.5 +Madam 399.5 0.5 +Hooch, 400.0 0.5 +arrived. 400.5 0.5 +She 401.0 0.5 +had 401.5 0.5 +short, 402.0 0.5 +gray 402.5 0.5 +hair, 403.0 0.5 +and 403.5 0.5 +yellow 404.0 0.5 +eyes 404.5 0.5 +like 405.0 0.5 +a 405.5 0.5 +hawk. 406.0 0.5 ++ 406.5 0.5 +"""Well," 407.0 0.5 +what 407.5 0.5 +are 408.0 0.5 +you 408.5 0.5 +all 409.0 0.5 +waiting 409.5 0.5 +"for?""" 410.0 0.5 +she 410.5 0.5 +barked. 411.0 0.5 +"""Everyone" 411.5 0.5 +stand 412.0 0.5 +by 412.5 0.5 +a 413.0 0.5 +broomstick. 413.5 0.5 +Come 414.0 0.5 +on, 414.5 0.5 +hurry 415.0 0.5 +"up.""" 415.5 0.5 ++ 416.0 0.5 +Harry 416.5 0.5 +glanced 417.0 0.5 +down 417.5 0.5 +at 418.0 0.5 +his 418.5 0.5 +broom. 419.0 0.5 +It 419.5 0.5 +was 420.0 0.5 +old 420.5 0.5 +and 421.0 0.5 +some 421.5 0.5 +of 422.0 0.5 +the 422.5 0.5 +twigs 423.0 0.5 +stuck 423.5 0.5 +out 424.0 0.5 +at 424.5 0.5 +odd 425.0 0.5 +angles. 425.5 0.5 ++ 426.0 0.5 +"""Stick" 426.5 0.5 +out 427.0 0.5 +your 427.5 0.5 +right 428.0 0.5 +hand 428.5 0.5 +over 429.0 0.5 +your 429.5 0.5 +"broom,""" 430.0 0.5 +called 430.5 0.5 +Madam 431.0 0.5 +Hooch 431.5 0.5 +at 432.0 0.5 +the 432.5 0.5 +front, 433.0 0.5 +"""and" 433.5 0.5 +say 434.0 0.5 +"'Up!'""" 434.5 0.5 ++ 435.0 0.5 +"""UP!""" 435.5 0.5 +everyone 436.0 0.5 +shouted. 436.5 0.5 ++ 437.0 0.5 +Harry's 437.5 0.5 +broom 438.0 0.5 +jumped 438.5 0.5 +into 439.0 0.5 +his 439.5 0.5 +hand 440.0 0.5 +at 440.5 0.5 +once, 441.0 0.5 +but 441.5 0.5 +it 442.0 0.5 +was 442.5 0.5 +one 443.0 0.5 +of 443.5 0.5 +the 444.0 0.5 +few 444.5 0.5 +that 445.0 0.5 +did. 445.5 0.5 +Hermione 446.0 0.5 +Granger's 446.5 0.5 +had 447.0 0.5 +simply 447.5 0.5 +rolled 448.0 0.5 +over 448.5 0.5 +on 449.0 0.5 +the 449.5 0.5 +ground, 450.0 0.5 +and 450.5 0.5 +Neville's 451.0 0.5 +hadn't 451.5 0.5 +moved 452.0 0.5 +at 452.5 0.5 +all. 453.0 0.5 +Perhaps 453.5 0.5 +brooms, 454.0 0.5 +like 454.5 0.5 +horses, 455.0 0.5 +could 455.5 0.5 +tell 456.0 0.5 +when 456.5 0.5 +you 457.0 0.5 +were 457.5 0.5 +afraid, 458.0 0.5 +thought 458.5 0.5 +Harry; 459.0 0.5 +there 459.5 0.5 +was 460.0 0.5 +a 460.5 0.5 +quaver 461.0 0.5 +in 461.5 0.5 +Neville's 462.0 0.5 +voice 462.5 0.5 +that 463.0 0.5 +said 463.5 0.5 +only 464.0 0.5 +too 464.5 0.5 +clearly 465.0 0.5 +that 465.5 0.5 +he 466.0 0.5 +wanted 466.5 0.5 +to 467.0 0.5 +keep 467.5 0.5 +his 468.0 0.5 +feet 468.5 0.5 +on 469.0 0.5 +the 469.5 0.5 +ground. 470.0 0.5 ++ 470.5 0.5 +Madam 471.0 0.5 +Hooch 471.5 0.5 +then 472.0 0.5 +showed 472.5 0.5 +them 473.0 0.5 +how 473.5 0.5 +to 474.0 0.5 +mount 474.5 0.5 +their 475.0 0.5 +brooms 475.5 0.5 +without 476.0 0.5 +sliding 476.5 0.5 +off 477.0 0.5 +the 477.5 0.5 +end, 478.0 0.5 +and 478.5 0.5 +walked 479.0 0.5 +up 479.5 0.5 +and 480.0 0.5 +down 480.5 0.5 +the 481.0 0.5 +rows 481.5 0.5 +correcting 482.0 0.5 +their 482.5 0.5 +grips. 483.0 0.5 +Harry 483.5 0.5 +and 484.0 0.5 +Ron 484.5 0.5 +were 485.0 0.5 +delighted 485.5 0.5 +when 486.0 0.5 +she 486.5 0.5 +told 487.0 0.5 +Malfoy 487.5 0.5 +he'd 488.0 0.5 +been 488.5 0.5 +doing 489.0 0.5 +it 489.5 0.5 +wrong 490.0 0.5 +for 490.5 0.5 +years. 491.0 0.5 ++ 491.5 0.5 +"""Now," 492.0 0.5 +when 492.5 0.5 +I 493.0 0.5 +blow 493.5 0.5 +my 494.0 0.5 +whistle, 494.5 0.5 +you 495.0 0.5 +kick 495.5 0.5 +off 496.0 0.5 +from 496.5 0.5 +the 497.0 0.5 +ground, 497.5 0.5 +"hard,""" 498.0 0.5 +said 498.5 0.5 +Madam 499.0 0.5 +Hooch. 499.5 0.5 +"""Keep" 500.0 0.5 +your 500.5 0.5 +brooms 501.0 0.5 +steady, 501.5 0.5 +rise 502.0 0.5 +a 502.5 0.5 +few 503.0 0.5 +feet, 503.5 0.5 +and 504.0 0.5 +then 504.5 0.5 +come 505.0 0.5 +straight 505.5 0.5 +back 506.0 0.5 +down 506.5 0.5 +by 507.0 0.5 +leaning 507.5 0.5 +forward 508.0 0.5 +slightly. 508.5 0.5 +On 509.0 0.5 +my 509.5 0.5 +whistle 510.0 0.5 +-- 510.5 0.5 +three 511.0 0.5 +-- 511.5 0.5 +two 512.0 0.5 +"--""" 512.5 0.5 ++ 513.0 0.5 +But 513.5 0.5 +Neville, 514.0 0.5 +nervous 514.5 0.5 +and 515.0 0.5 +jumpy 515.5 0.5 +and 516.0 0.5 +frightened 516.5 0.5 +of 517.0 0.5 +being 517.5 0.5 +left 518.0 0.5 +on 518.5 0.5 +the 519.0 0.5 +ground, 519.5 0.5 +pushed 520.0 0.5 +off 520.5 0.5 +hard 521.0 0.5 +before 521.5 0.5 +the 522.0 0.5 +whistle 522.5 0.5 +had 523.0 0.5 +touched 523.5 0.5 +Madam 524.0 0.5 +Hooch's 524.5 0.5 +lips. 525.0 0.5 ++ 525.5 0.5 +"""Come" 526.0 0.5 +back, 526.5 0.5 +"boy!""" 527.0 0.5 +she 527.5 0.5 +shouted, 528.0 0.5 +but 528.5 0.5 +Neville 529.0 0.5 +was 529.5 0.5 +rising 530.0 0.5 +straight 530.5 0.5 +up 531.0 0.5 +like 531.5 0.5 +a 532.0 0.5 +cork 532.5 0.5 +shot 533.0 0.5 +out 533.5 0.5 +of 534.0 0.5 +a 534.5 0.5 +bottle 535.0 0.5 +-- 535.5 0.5 +twelve 536.0 0.5 +feet 536.5 0.5 +-- 537.0 0.5 +twenty 537.5 0.5 +feet. 538.0 0.5 +Harry 538.5 0.5 +saw 539.0 0.5 +his 539.5 0.5 +scared 540.0 0.5 +white 540.5 0.5 +face 541.0 0.5 +look 541.5 0.5 +down 542.0 0.5 +at 542.5 0.5 +the 543.0 0.5 +ground 543.5 0.5 +falling 544.0 0.5 +away, 544.5 0.5 +saw 545.0 0.5 +him 545.5 0.5 +gasp, 546.0 0.5 +slip 546.5 0.5 +sideways 547.0 0.5 +off 547.5 0.5 +the 548.0 0.5 +broom 548.5 0.5 +and 549.0 0.5 +-- 549.5 0.5 ++ 550.0 0.5 +WHAM 550.5 0.5 +-- 551.0 0.5 +a 551.5 0.5 +thud 552.0 0.5 +and 552.5 0.5 +a 553.0 0.5 +nasty 553.5 0.5 +crack 554.0 0.5 +and 554.5 0.5 +Neville 555.0 0.5 +lay 555.5 0.5 +facedown 556.0 0.5 +on 556.5 0.5 +the 557.0 0.5 +grass 557.5 0.5 +in 558.0 0.5 +a 558.5 0.5 +heap. 559.0 0.5 +His 559.5 0.5 +broomstick 560.0 0.5 +was 560.5 0.5 +still 561.0 0.5 +rising 561.5 0.5 +higher 562.0 0.5 +and 562.5 0.5 +higher, 563.0 0.5 +and 563.5 0.5 +started 564.0 0.5 +to 564.5 0.5 +drift 565.0 0.5 +lazily 565.5 0.5 +toward 566.0 0.5 +the 566.5 0.5 +forbidden 567.0 0.5 +forest 567.5 0.5 +and 568.0 0.5 +out 568.5 0.5 +of 569.0 0.5 +sight. 569.5 0.5 ++ 570.0 0.5 +Madam 570.5 0.5 +Hooch 571.0 0.5 +was 571.5 0.5 +bending 572.0 0.5 +over 572.5 0.5 +Neville, 573.0 0.5 +her 573.5 0.5 +face 574.0 0.5 +as 574.5 0.5 +white 575.0 0.5 +as 575.5 0.5 +his. 576.0 0.5 +"""Broken" 576.5 0.5 +"wrist,""" 577.0 0.5 +Harry 577.5 0.5 +heard 578.0 0.5 +her 578.5 0.5 +mutter. 579.0 0.5 +"""Come" 579.5 0.5 +on, 580.0 0.5 +boy 580.5 0.5 +-- 581.0 0.5 +it's 581.5 0.5 +all 582.0 0.5 +right, 582.5 0.5 +up 583.0 0.5 +you 583.5 0.5 +"get.""" 584.0 0.5 ++ 584.5 0.5 +She 585.0 0.5 +turned 585.5 0.5 +to 586.0 0.5 +the 586.5 0.5 +rest 587.0 0.5 +of 587.5 0.5 +the 588.0 0.5 +class. 588.5 0.5 +"""None" 589.0 0.5 +of 589.5 0.5 +you 590.0 0.5 +is 590.5 0.5 +to 591.0 0.5 +move 591.5 0.5 +while 592.0 0.5 +I 592.5 0.5 +take 593.0 0.5 +this 593.5 0.5 +boy 594.0 0.5 +to 594.5 0.5 +the 595.0 0.5 +hospital 595.5 0.5 +wing! 596.0 0.5 +You 596.5 0.5 +leave 597.0 0.5 +those 597.5 0.5 +brooms 598.0 0.5 +where 598.5 0.5 +they 599.0 0.5 +are 599.5 0.5 +or 600.0 0.5 +you'll 600.5 0.5 +be 601.0 0.5 +out 601.5 0.5 +of 602.0 0.5 +Hogwarts 602.5 0.5 +before 603.0 0.5 +you 603.5 0.5 +can 604.0 0.5 +say 604.5 0.5 +'Quidditch.' 605.0 0.5 +Come 605.5 0.5 +on, 606.0 0.5 +"dear.""" 606.5 0.5 ++ 607.0 0.5 +Neville, 607.5 0.5 +his 608.0 0.5 +face 608.5 0.5 +tear-streaked, 609.0 0.5 +clutching 609.5 0.5 +his 610.0 0.5 +wrist, 610.5 0.5 +hobbled 611.0 0.5 +off 611.5 0.5 +with 612.0 0.5 +Madam 612.5 0.5 +Hooch, 613.0 0.5 +who 613.5 0.5 +had 614.0 0.5 +her 614.5 0.5 +arm 615.0 0.5 +around 615.5 0.5 +him. 616.0 0.5 ++ 616.5 0.5 +No 617.0 0.5 +sooner 617.5 0.5 +were 618.0 0.5 +they 618.5 0.5 +out 619.0 0.5 +of 619.5 0.5 +earshot 620.0 0.5 +than 620.5 0.5 +Malfoy 621.0 0.5 +burst 621.5 0.5 +into 622.0 0.5 +laughter. 622.5 0.5 ++ 623.0 0.5 +"""Did" 623.5 0.5 +you 624.0 0.5 +see 624.5 0.5 +his 625.0 0.5 +face, 625.5 0.5 +the 626.0 0.5 +great 626.5 0.5 +"lump?""" 627.0 0.5 ++ 627.5 0.5 +The 628.0 0.5 +other 628.5 0.5 +Slytherins 629.0 0.5 +joined 629.5 0.5 +in. 630.0 0.5 ++ 630.5 0.5 +"""Shut" 631.0 0.5 +up, 631.5 0.5 +"Malfoy,""" 632.0 0.5 +snapped 632.5 0.5 +Parvati 633.0 0.5 +Patil. 633.5 0.5 ++ 634.0 0.5 +"""Ooh," 634.5 0.5 +sticking 635.0 0.5 +up 635.5 0.5 +for 636.0 0.5 +"Longbottom?""" 636.5 0.5 +said 637.0 0.5 +Pansy 637.5 0.5 +Parkinson, 638.0 0.5 +a 638.5 0.5 +hard-faced 639.0 0.5 +Slytherin 639.5 0.5 +girl. 640.0 0.5 +"""Never" 640.5 0.5 +thought 641.0 0.5 +you'd italic 641.5 0.5 +like 642.0 0.5 +fat 642.5 0.5 +little 643.0 0.5 +crybabies, 643.5 0.5 +"Parvati.""" 644.0 0.5 ++ 644.5 0.5 +"""Look!""" 645.0 0.5 +said 645.5 0.5 +Malfoy, 646.0 0.5 +darting 646.5 0.5 +forward 647.0 0.5 +and 647.5 0.5 +snatching 648.0 0.5 +something 648.5 0.5 +out 649.0 0.5 +of 649.5 0.5 +the 650.0 0.5 +grass. 650.5 0.5 +"""It's" 651.0 0.5 +that 651.5 0.5 +stupid 652.0 0.5 +thing 652.5 0.5 +Longbottom's 653.0 0.5 +gran 653.5 0.5 +sent 654.0 0.5 +"him.""" 654.5 0.5 ++ 655.0 0.5 +The 655.5 0.5 +Remembrall 656.0 0.5 +glittered 656.5 0.5 +in 657.0 0.5 +the 657.5 0.5 +sun 658.0 0.5 +as 658.5 0.5 +he 659.0 0.5 +held 659.5 0.5 +it 660.0 0.5 +up. 660.5 0.5 ++ 661.0 0.5 diff --git a/data/language/harrypotter/task-harry_run-2_events.tsv b/data/language/harrypotter/task-harry_run-2_events.tsv new file mode 100644 index 00000000..28fae10e --- /dev/null +++ b/data/language/harrypotter/task-harry_run-2_events.tsv @@ -0,0 +1,1352 @@ +word format onset duration +"""Give" 10.0 0.5 +that 10.5 0.5 +here, 11.0 0.5 +"Malfoy,""" 11.5 0.5 +said 12.0 0.5 +Harry 12.5 0.5 +quietly. 13.0 0.5 +Everyone 13.5 0.5 +stopped 14.0 0.5 +talking 14.5 0.5 +to 15.0 0.5 +watch. 15.5 0.5 ++ 16.0 0.5 +Malfoy 16.5 0.5 +smiled 17.0 0.5 +nastily. 17.5 0.5 +"""I" 18.0 0.5 +think 18.5 0.5 +I'll 19.0 0.5 +leave 19.5 0.5 +it 20.0 0.5 +somewhere 20.5 0.5 +for 21.0 0.5 +Longbottom 21.5 0.5 +to 22.0 0.5 +find 22.5 0.5 +-- 23.0 0.5 +how 23.5 0.5 +about 24.0 0.5 +-- 24.5 0.5 +up 25.0 0.5 +a 25.5 0.5 +"tree?""" 26.0 0.5 ++ 26.5 0.5 +"""Give" 27.0 0.5 +it 27.5 0.5 +"here!""" italic 28.0 0.5 +Harry 28.5 0.5 +yelled, 29.0 0.5 +but 29.5 0.5 +Malfoy 30.0 0.5 +had 30.5 0.5 +leapt 31.0 0.5 +onto 31.5 0.5 +his 32.0 0.5 +broomstick 32.5 0.5 +and 33.0 0.5 +taken 33.5 0.5 +off. 34.0 0.5 +He 34.5 0.5 +hadn't 35.0 0.5 +been 35.5 0.5 +lying, 36.0 0.5 +he 36.5 0.5 +could italic 37.0 0.5 +fly 37.5 0.5 +well. 38.0 0.5 +Hovering 38.5 0.5 +level 39.0 0.5 +with 39.5 0.5 +the 40.0 0.5 +topmost 40.5 0.5 +branches 41.0 0.5 +of 41.5 0.5 +an 42.0 0.5 +oak 42.5 0.5 +he 43.0 0.5 +called, 43.5 0.5 +"""Come" 44.0 0.5 +and 44.5 0.5 +get 45.0 0.5 +it, 45.5 0.5 +"Potter!""" 46.0 0.5 ++ 46.5 0.5 +Harry 47.0 0.5 +grabbed 47.5 0.5 +his 48.0 0.5 +broom. 48.5 0.5 ++ 49.0 0.5 +"""No!""" italic 49.5 0.5 +shouted 50.0 0.5 +Hermione 50.5 0.5 +Granger. 51.0 0.5 +"""Madam" 51.5 0.5 +Hooch 52.0 0.5 +told 52.5 0.5 +us 53.0 0.5 +not 53.5 0.5 +to 54.0 0.5 +move 54.5 0.5 +-- 55.0 0.5 +you'll 55.5 0.5 +get 56.0 0.5 +us 56.5 0.5 +all 57.0 0.5 +into 57.5 0.5 +"trouble.""" 58.0 0.5 ++ 58.5 0.5 +Harry 59.0 0.5 +ignored 59.5 0.5 +her. 60.0 0.5 +Blood 60.5 0.5 +was 61.0 0.5 +pounding 61.5 0.5 +in 62.0 0.5 +his 62.5 0.5 +ears. 63.0 0.5 +He 63.5 0.5 +mounted 64.0 0.5 +the 64.5 0.5 +broom 65.0 0.5 +and 65.5 0.5 +kicked 66.0 0.5 +hard 66.5 0.5 +against 67.0 0.5 +the 67.5 0.5 +ground 68.0 0.5 +and 68.5 0.5 +up, 69.0 0.5 +up 69.5 0.5 +he 70.0 0.5 +soared; 70.5 0.5 +air 71.0 0.5 +rushed 71.5 0.5 +through 72.0 0.5 +his 72.5 0.5 +hair, 73.0 0.5 +and 73.5 0.5 +his 74.0 0.5 +robes 74.5 0.5 +whipped 75.0 0.5 +out 75.5 0.5 +behind 76.0 0.5 +him 76.5 0.5 +-- 77.0 0.5 +and 77.5 0.5 +in 78.0 0.5 +a 78.5 0.5 +rush 79.0 0.5 +of 79.5 0.5 +fierce 80.0 0.5 +joy 80.5 0.5 +he 81.0 0.5 +realized 81.5 0.5 +he'd 82.0 0.5 +found 82.5 0.5 +something 83.0 0.5 +he 83.5 0.5 +could 84.0 0.5 +do 84.5 0.5 +without 85.0 0.5 +being 85.5 0.5 +taught 86.0 0.5 +-- 86.5 0.5 +this 87.0 0.5 +was 87.5 0.5 +easy, 88.0 0.5 +this 88.5 0.5 +was 89.0 0.5 +wonderful. italic 89.5 0.5 +He 90.0 0.5 +pulled 90.5 0.5 +his 91.0 0.5 +broomstick 91.5 0.5 +up 92.0 0.5 +a 92.5 0.5 +little 93.0 0.5 +to 93.5 0.5 +take 94.0 0.5 +it 94.5 0.5 +even 95.0 0.5 +higher, 95.5 0.5 +and 96.0 0.5 +heard 96.5 0.5 +screams 97.0 0.5 +and 97.5 0.5 +gasps 98.0 0.5 +of 98.5 0.5 +girls 99.0 0.5 +back 99.5 0.5 +on 100.0 0.5 +the 100.5 0.5 +ground 101.0 0.5 +and 101.5 0.5 +an 102.0 0.5 +admiring 102.5 0.5 +whoop 103.0 0.5 +from 103.5 0.5 +Ron. 104.0 0.5 ++ 104.5 0.5 +He 105.0 0.5 +turned 105.5 0.5 +his 106.0 0.5 +broomstick 106.5 0.5 +sharply 107.0 0.5 +to 107.5 0.5 +face 108.0 0.5 +Malfoy 108.5 0.5 +in 109.0 0.5 +midair. 109.5 0.5 ++ 110.0 0.5 +Malfoy 110.5 0.5 +looked 111.0 0.5 +stunned. 111.5 0.5 +"""Give" 112.0 0.5 +it 112.5 0.5 +"here,""" 113.0 0.5 +Harry 113.5 0.5 +called, 114.0 0.5 +"""or" 114.5 0.5 +I'll 115.0 0.5 +knock 115.5 0.5 +you 116.0 0.5 +off 116.5 0.5 +that 117.0 0.5 +"broom!""" 117.5 0.5 ++ 118.0 0.5 +"""Oh," 118.5 0.5 +"yeah?""" 119.0 0.5 +said 119.5 0.5 +Malfoy, 120.0 0.5 +trying 120.5 0.5 +to 121.0 0.5 +sneer, 121.5 0.5 +but 122.0 0.5 +looking 122.5 0.5 +worried. 123.0 0.5 ++ 123.5 0.5 +Harry 124.0 0.5 +knew, 124.5 0.5 +somehow, 125.0 0.5 +what 125.5 0.5 +to 126.0 0.5 +do. 126.5 0.5 +He 127.0 0.5 +leaned 127.5 0.5 +forward 128.0 0.5 +and 128.5 0.5 +grasped 129.0 0.5 +the 129.5 0.5 +broom 130.0 0.5 +tightly 130.5 0.5 +in 131.0 0.5 +both 131.5 0.5 +hands, 132.0 0.5 +and 132.5 0.5 +it 133.0 0.5 +shot 133.5 0.5 +toward 134.0 0.5 +Malfoy 134.5 0.5 +like 135.0 0.5 +a 135.5 0.5 +javelin. 136.0 0.5 +Malfoy 136.5 0.5 +only 137.0 0.5 +just 137.5 0.5 +got 138.0 0.5 +out 138.5 0.5 +of 139.0 0.5 +the 139.5 0.5 +way 140.0 0.5 +in 140.5 0.5 +time; 141.0 0.5 +Harry 141.5 0.5 +made 142.0 0.5 +a 142.5 0.5 +sharp 143.0 0.5 +about-face 143.5 0.5 +and 144.0 0.5 +held 144.5 0.5 +the 145.0 0.5 +broom 145.5 0.5 +steady. 146.0 0.5 +A 146.5 0.5 +few 147.0 0.5 +people 147.5 0.5 +below 148.0 0.5 +were 148.5 0.5 +clapping. 149.0 0.5 ++ 149.5 0.5 +"""No" 150.0 0.5 +Crabbe 150.5 0.5 +and 151.0 0.5 +Goyle 151.5 0.5 +up 152.0 0.5 +here 152.5 0.5 +to 153.0 0.5 +save 153.5 0.5 +your 154.0 0.5 +neck, 154.5 0.5 +"Malfoy,""" 155.0 0.5 +Harry 155.5 0.5 +called. 156.0 0.5 ++ 156.5 0.5 +The 157.0 0.5 +same 157.5 0.5 +thought 158.0 0.5 +seemed 158.5 0.5 +to 159.0 0.5 +have 159.5 0.5 +struck 160.0 0.5 +Malfoy. 160.5 0.5 ++ 161.0 0.5 +"""Catch" 161.5 0.5 +it 162.0 0.5 +if 162.5 0.5 +you 163.0 0.5 +can, 163.5 0.5 +"then!""" 164.0 0.5 +he 164.5 0.5 +shouted, 165.0 0.5 +and 165.5 0.5 +he 166.0 0.5 +threw 166.5 0.5 +the 167.0 0.5 +glass 167.5 0.5 +ball 168.0 0.5 +high 168.5 0.5 +into 169.0 0.5 +the 169.5 0.5 +air 170.0 0.5 +and 170.5 0.5 +streaked 171.0 0.5 +back 171.5 0.5 +toward 172.0 0.5 +the 172.5 0.5 +ground. 173.0 0.5 ++ 173.5 0.5 +Harry 174.0 0.5 +saw, 174.5 0.5 +as 175.0 0.5 +though 175.5 0.5 +in 176.0 0.5 +slow 176.5 0.5 +motion, 177.0 0.5 +the 177.5 0.5 +ball 178.0 0.5 +rise 178.5 0.5 +up 179.0 0.5 +in 179.5 0.5 +the 180.0 0.5 +air 180.5 0.5 +and 181.0 0.5 +then 181.5 0.5 +start 182.0 0.5 +to 182.5 0.5 +fall. 183.0 0.5 +He 183.5 0.5 +leaned 184.0 0.5 +forward 184.5 0.5 +and 185.0 0.5 +pointed 185.5 0.5 +his 186.0 0.5 +broom 186.5 0.5 +handle 187.0 0.5 +down 187.5 0.5 +-- 188.0 0.5 +next 188.5 0.5 +second 189.0 0.5 +he 189.5 0.5 +was 190.0 0.5 +gathering 190.5 0.5 +speed 191.0 0.5 +in 191.5 0.5 +a 192.0 0.5 +steep 192.5 0.5 +dive, 193.0 0.5 +racing 193.5 0.5 +the 194.0 0.5 +ball 194.5 0.5 +-- 195.0 0.5 +wind 195.5 0.5 +whistled 196.0 0.5 +in 196.5 0.5 +his 197.0 0.5 +ears, 197.5 0.5 +mingled 198.0 0.5 +with 198.5 0.5 +the 199.0 0.5 +screams 199.5 0.5 +of 200.0 0.5 +people 200.5 0.5 +watching 201.0 0.5 +-- 201.5 0.5 +he 202.0 0.5 +stretched 202.5 0.5 +out 203.0 0.5 +his 203.5 0.5 +hand 204.0 0.5 +-- 204.5 0.5 +a 205.0 0.5 +foot 205.5 0.5 +from 206.0 0.5 +the 206.5 0.5 +ground 207.0 0.5 +he 207.5 0.5 +caught 208.0 0.5 +it, 208.5 0.5 +just 209.0 0.5 +in 209.5 0.5 +time 210.0 0.5 +to 210.5 0.5 +pull 211.0 0.5 +his 211.5 0.5 +broom 212.0 0.5 +straight, 212.5 0.5 +and 213.0 0.5 +he 213.5 0.5 +toppled 214.0 0.5 +gently 214.5 0.5 +onto 215.0 0.5 +the 215.5 0.5 +grass 216.0 0.5 +with 216.5 0.5 +the 217.0 0.5 +Remembrall 217.5 0.5 +clutched 218.0 0.5 +safely 218.5 0.5 +in 219.0 0.5 +his 219.5 0.5 +fist. 220.0 0.5 ++ 220.5 0.5 +"""HARRY" 221.0 0.5 +"POTTER!""" 221.5 0.5 ++ 222.0 0.5 +His 222.5 0.5 +heart 223.0 0.5 +sank 223.5 0.5 +faster 224.0 0.5 +than 224.5 0.5 +he'd 225.0 0.5 +just 225.5 0.5 +dived. 226.0 0.5 +Professor 226.5 0.5 +McGonagall 227.0 0.5 +was 227.5 0.5 +running 228.0 0.5 +toward 228.5 0.5 +them. 229.0 0.5 +He 229.5 0.5 +got 230.0 0.5 +to 230.5 0.5 +his 231.0 0.5 +feet, 231.5 0.5 +trembling. 232.0 0.5 ++ 232.5 0.5 +"""Never" italic 233.0 0.5 +-- 233.5 0.5 +in 234.0 0.5 +all 234.5 0.5 +my 235.0 0.5 +time 235.5 0.5 +at 236.0 0.5 +Hogwarts 236.5 0.5 +"--""" 237.0 0.5 +Professor 237.5 0.5 +McGonagall 238.0 0.5 +was 238.5 0.5 +almost 239.0 0.5 +speechless 239.5 0.5 +with 240.0 0.5 +shock, 240.5 0.5 +and 241.0 0.5 +her 241.5 0.5 +glasses 242.0 0.5 +flashed 242.5 0.5 +furiously, 243.0 0.5 +"""--" 243.5 0.5 +how 244.0 0.5 +dare italic 244.5 0.5 +you 245.0 0.5 +-- 245.5 0.5 +might 246.0 0.5 +have 246.5 0.5 +broken 247.0 0.5 +your 247.5 0.5 +neck 248.0 0.5 +"--""" 248.5 0.5 ++ 249.0 0.5 +"""It" 249.5 0.5 +wasn't 250.0 0.5 +his 250.5 0.5 +fault, 251.0 0.5 +Professor 251.5 0.5 +"--""" 252.0 0.5 ++ 252.5 0.5 +"""Be" 253.0 0.5 +quiet, 253.5 0.5 +Miss 254.0 0.5 +Patil 254.5 0.5 +"--""" 255.0 0.5 ++ 255.5 0.5 +"""But" 256.0 0.5 +Malfoy 256.5 0.5 +"--""" 257.0 0.5 +"""That's" 257.5 0.5 +enough, 258.0 0.5 +Mr. 258.5 0.5 +Weasley. 259.0 0.5 +Potter, 259.5 0.5 +follow 260.0 0.5 +me, 260.5 0.5 +"now.""" 261.0 0.5 ++ 261.5 0.5 +Harry 262.0 0.5 +caught 262.5 0.5 +sight 263.0 0.5 +of 263.5 0.5 +Malfoy, 264.0 0.5 +Crabbe, 264.5 0.5 +and 265.0 0.5 +Goyle's 265.5 0.5 +triumphant 266.0 0.5 +faces 266.5 0.5 +as 267.0 0.5 +he 267.5 0.5 +left, 268.0 0.5 +walking 268.5 0.5 +numbly 269.0 0.5 +in 269.5 0.5 +Professor 270.0 0.5 +McGonagall's 270.5 0.5 +wake 271.0 0.5 +as 271.5 0.5 +she 272.0 0.5 +strode 272.5 0.5 +toward 273.0 0.5 +the 273.5 0.5 +castle. 274.0 0.5 +He 274.5 0.5 +was 275.0 0.5 +going 275.5 0.5 +to 276.0 0.5 +be 276.5 0.5 +expelled, 277.0 0.5 +he 277.5 0.5 +just 278.0 0.5 +knew 278.5 0.5 +it. 279.0 0.5 +He 279.5 0.5 +wanted 280.0 0.5 +to 280.5 0.5 +say 281.0 0.5 +something 281.5 0.5 +to 282.0 0.5 +defend 282.5 0.5 +himself, 283.0 0.5 +but 283.5 0.5 +there 284.0 0.5 +seemed 284.5 0.5 +to 285.0 0.5 +be 285.5 0.5 +something 286.0 0.5 +wrong 286.5 0.5 +with 287.0 0.5 +his 287.5 0.5 +voice. 288.0 0.5 +Professor 288.5 0.5 +McGonagall 289.0 0.5 +was 289.5 0.5 +sweeping 290.0 0.5 +along 290.5 0.5 +without 291.0 0.5 +even 291.5 0.5 +looking 292.0 0.5 +at 292.5 0.5 +him; 293.0 0.5 +he 293.5 0.5 +had 294.0 0.5 +to 294.5 0.5 +jog 295.0 0.5 +to 295.5 0.5 +keep 296.0 0.5 +up. 296.5 0.5 +Now 297.0 0.5 +he'd 297.5 0.5 +done 298.0 0.5 +it. 298.5 0.5 +He 299.0 0.5 +hadn't 299.5 0.5 +even 300.0 0.5 +lasted 300.5 0.5 +two 301.0 0.5 +weeks. 301.5 0.5 +He'd 302.0 0.5 +be 302.5 0.5 +packing 303.0 0.5 +his 303.5 0.5 +bags 304.0 0.5 +in 304.5 0.5 +ten 305.0 0.5 +minutes. 305.5 0.5 +What 306.0 0.5 +would 306.5 0.5 +the 307.0 0.5 +Dursleys 307.5 0.5 +say 308.0 0.5 +when 308.5 0.5 +he 309.0 0.5 +turned 309.5 0.5 +up 310.0 0.5 +on 310.5 0.5 +the 311.0 0.5 +doorstep? 311.5 0.5 ++ 312.0 0.5 +Up 312.5 0.5 +the 313.0 0.5 +front 313.5 0.5 +steps, 314.0 0.5 +up 314.5 0.5 +the 315.0 0.5 +marble 315.5 0.5 +staircase 316.0 0.5 +inside, 316.5 0.5 +and 317.0 0.5 +still 317.5 0.5 +Professor 318.0 0.5 +McGonagall 318.5 0.5 +didn't 319.0 0.5 +say 319.5 0.5 +a 320.0 0.5 +word 320.5 0.5 +to 321.0 0.5 +him. 321.5 0.5 +She 322.0 0.5 +wrenched 322.5 0.5 +open 323.0 0.5 +doors 323.5 0.5 +and 324.0 0.5 +marched 324.5 0.5 +along 325.0 0.5 +corridors 325.5 0.5 +with 326.0 0.5 +Harry 326.5 0.5 +trotting 327.0 0.5 +miserably 327.5 0.5 +behind 328.0 0.5 +her. 328.5 0.5 +Maybe 329.0 0.5 +she 329.5 0.5 +was 330.0 0.5 +taking 330.5 0.5 +him 331.0 0.5 +to 331.5 0.5 +Dumbledore. 332.0 0.5 +He 332.5 0.5 +thought 333.0 0.5 +of 333.5 0.5 +Hagrid, 334.0 0.5 +expelled 334.5 0.5 +but 335.0 0.5 +allowed 335.5 0.5 +to 336.0 0.5 +stay 336.5 0.5 +on 337.0 0.5 +as 337.5 0.5 +gamekeeper. 338.0 0.5 +Perhaps 338.5 0.5 +he 339.0 0.5 +could 339.5 0.5 +be 340.0 0.5 +Hagrid's 340.5 0.5 +assistant. 341.0 0.5 +His 341.5 0.5 +stomach 342.0 0.5 +twisted 342.5 0.5 +as 343.0 0.5 +he 343.5 0.5 +imagined 344.0 0.5 +it, 344.5 0.5 +watching 345.0 0.5 +Ron 345.5 0.5 +and 346.0 0.5 +the 346.5 0.5 +others 347.0 0.5 +becoming 347.5 0.5 +wizards 348.0 0.5 +while 348.5 0.5 +he 349.0 0.5 +stumped 349.5 0.5 +around 350.0 0.5 +the 350.5 0.5 +grounds 351.0 0.5 +carrying 351.5 0.5 +Hagrid's 352.0 0.5 +bag. 352.5 0.5 ++ 353.0 0.5 +Professor 353.5 0.5 +McGonagall 354.0 0.5 +stopped 354.5 0.5 +outside 355.0 0.5 +a 355.5 0.5 +classroom. 356.0 0.5 +She 356.5 0.5 +opened 357.0 0.5 +the 357.5 0.5 +door 358.0 0.5 +and 358.5 0.5 +poked 359.0 0.5 +her 359.5 0.5 +head 360.0 0.5 +inside. 360.5 0.5 ++ 361.0 0.5 +"""Excuse" 361.5 0.5 +me, 362.0 0.5 +Professor 362.5 0.5 +Flitwick, 363.0 0.5 +could 363.5 0.5 +I 364.0 0.5 +borrow 364.5 0.5 +Wood 365.0 0.5 +for 365.5 0.5 +a 366.0 0.5 +"moment?""" 366.5 0.5 ++ 367.0 0.5 +Wood? 367.5 0.5 +thought 368.0 0.5 +Harry, 368.5 0.5 +bewildered; 369.0 0.5 +was 369.5 0.5 +Wood 370.0 0.5 +a 370.5 0.5 +cane 371.0 0.5 +she 371.5 0.5 +was 372.0 0.5 +going 372.5 0.5 +to 373.0 0.5 +use 373.5 0.5 +on 374.0 0.5 +him? 374.5 0.5 +But 375.0 0.5 +Wood 375.5 0.5 +turned 376.0 0.5 +out 376.5 0.5 +to 377.0 0.5 +be 377.5 0.5 +a 378.0 0.5 +person, 378.5 0.5 +a 379.0 0.5 +burly 379.5 0.5 +fifth-year 380.0 0.5 +boy 380.5 0.5 +who 381.0 0.5 +came 381.5 0.5 +out 382.0 0.5 +of 382.5 0.5 +Flitwick's 383.0 0.5 +class 383.5 0.5 +looking 384.0 0.5 +confused. 384.5 0.5 ++ 385.0 0.5 +"""Follow" 385.5 0.5 +me, 386.0 0.5 +you 386.5 0.5 +"two,""" 387.0 0.5 +said 387.5 0.5 +Professor 388.0 0.5 +McGonagall, 388.5 0.5 +and 389.0 0.5 +they 389.5 0.5 +marched 390.0 0.5 +on 390.5 0.5 +up 391.0 0.5 +the 391.5 0.5 +corridor, 392.0 0.5 +Wood 392.5 0.5 +looking 393.0 0.5 +curiously 393.5 0.5 +at 394.0 0.5 +Harry. 394.5 0.5 ++ 395.0 0.5 +"""In" 395.5 0.5 +"here.""" 396.0 0.5 ++ 396.5 0.5 +Professor 397.0 0.5 +McGonagall 397.5 0.5 +pointed 398.0 0.5 +them 398.5 0.5 +into 399.0 0.5 +a 399.5 0.5 +classroom 400.0 0.5 +that 400.5 0.5 +was 401.0 0.5 +empty 401.5 0.5 +except 402.0 0.5 +for 402.5 0.5 +Peeves, 403.0 0.5 +who 403.5 0.5 +was 404.0 0.5 +busy 404.5 0.5 +writing 405.0 0.5 +rude 405.5 0.5 +words 406.0 0.5 +on 406.5 0.5 +the 407.0 0.5 +blackboard. 407.5 0.5 ++ 408.0 0.5 +"""Out," 408.5 0.5 +"Peeves!""" 409.0 0.5 +she 409.5 0.5 +barked. 410.0 0.5 +Peeves 410.5 0.5 +threw 411.0 0.5 +the 411.5 0.5 +chalk 412.0 0.5 +into 412.5 0.5 +a 413.0 0.5 +bin, 413.5 0.5 +which 414.0 0.5 +clanged 414.5 0.5 +loudly, 415.0 0.5 +and 415.5 0.5 +he 416.0 0.5 +swooped 416.5 0.5 +out 417.0 0.5 +cursing. 417.5 0.5 +Professor 418.0 0.5 +McGonagall 418.5 0.5 +slammed 419.0 0.5 +the 419.5 0.5 +door 420.0 0.5 +behind 420.5 0.5 +him 421.0 0.5 +and 421.5 0.5 +turned 422.0 0.5 +to 422.5 0.5 +face 423.0 0.5 +the 423.5 0.5 +two 424.0 0.5 +boys. 424.5 0.5 ++ 425.0 0.5 +"""Potter," 425.5 0.5 +this 426.0 0.5 +is 426.5 0.5 +Oliver 427.0 0.5 +Wood. 427.5 0.5 +^Wood 428.0 0.5 +-- 428.5 0.5 +I've 429.0 0.5 +found 429.5 0.5 +you 430.0 0.5 +a 430.5 0.5 +"Seeker.""" 431.0 0.5 ++ 431.5 0.5 +Wood's 432.0 0.5 +expression 432.5 0.5 +changed 433.0 0.5 +from 433.5 0.5 +puzzlement 434.0 0.5 +to 434.5 0.5 +delight. 435.0 0.5 ++ 435.5 0.5 +"""Are" 436.0 0.5 +you 436.5 0.5 +serious, 437.0 0.5 +"Professor?""" 437.5 0.5 ++ 438.0 0.5 +"""Absolutely,""" 438.5 0.5 +said 439.0 0.5 +Professor 439.5 0.5 +McGonagall 440.0 0.5 +crisply. 440.5 0.5 +"""The" 441.0 0.5 +boy's 441.5 0.5 +a 442.0 0.5 +natural. 442.5 0.5 +I've 443.0 0.5 +never 443.5 0.5 +seen 444.0 0.5 +anything 444.5 0.5 +like 445.0 0.5 +it. 445.5 0.5 +Was 446.0 0.5 +that 446.5 0.5 +your 447.0 0.5 +first 447.5 0.5 +time 448.0 0.5 +on 448.5 0.5 +a 449.0 0.5 +broomstick, 449.5 0.5 +"Potter?""" 450.0 0.5 ++ 450.5 0.5 +Harry 451.0 0.5 +nodded 451.5 0.5 +silently. 452.0 0.5 +He 452.5 0.5 +didn't 453.0 0.5 +have 453.5 0.5 +a 454.0 0.5 +clue 454.5 0.5 +what 455.0 0.5 +was 455.5 0.5 +going 456.0 0.5 +on, 456.5 0.5 +but 457.0 0.5 +he 457.5 0.5 +didn't 458.0 0.5 +seem 458.5 0.5 +to 459.0 0.5 +be 459.5 0.5 +being 460.0 0.5 +expelled, 460.5 0.5 +and 461.0 0.5 +some 461.5 0.5 +of 462.0 0.5 +the 462.5 0.5 +feeling 463.0 0.5 +started 463.5 0.5 +coming 464.0 0.5 +back 464.5 0.5 +to 465.0 0.5 +his 465.5 0.5 +legs. 466.0 0.5 ++ 466.5 0.5 +"""He" 467.0 0.5 +caught 467.5 0.5 +that 468.0 0.5 +thing 468.5 0.5 +in 469.0 0.5 +his 469.5 0.5 +hand 470.0 0.5 +after 470.5 0.5 +a 471.0 0.5 +fifty-foot 471.5 0.5 +"dive,""" 472.0 0.5 +Professor 472.5 0.5 +McGonagall 473.0 0.5 +told 473.5 0.5 +Wood. 474.0 0.5 +"""Didn't" 474.5 0.5 +even 475.0 0.5 +scratch 475.5 0.5 +himself. 476.0 0.5 +Charlie 476.5 0.5 +Weasley 477.0 0.5 +couldn't 477.5 0.5 +have 478.0 0.5 +done 478.5 0.5 +"it.""" 479.0 0.5 ++ 479.5 0.5 +Wood 480.0 0.5 +was 480.5 0.5 +now 481.0 0.5 +looking 481.5 0.5 +as 482.0 0.5 +though 482.5 0.5 +all 483.0 0.5 +his 483.5 0.5 +dreams 484.0 0.5 +had 484.5 0.5 +come 485.0 0.5 +true 485.5 0.5 +at 486.0 0.5 +once. 486.5 0.5 ++ 487.0 0.5 +"""Ever" 487.5 0.5 +seen 488.0 0.5 +a 488.5 0.5 +game 489.0 0.5 +of 489.5 0.5 +Quidditch, 490.0 0.5 +"Potter?""" 490.5 0.5 +he 491.0 0.5 +asked 491.5 0.5 +excitedly. 492.0 0.5 ++ 492.5 0.5 +"""Wood's" 493.0 0.5 +captain 493.5 0.5 +of 494.0 0.5 +the 494.5 0.5 +Gryffindor 495.0 0.5 +"team,""" 495.5 0.5 +Professor 496.0 0.5 +McGonagall 496.5 0.5 +explained. 497.0 0.5 ++ 497.5 0.5 +"""He's" 498.0 0.5 +just 498.5 0.5 +the 499.0 0.5 +build 499.5 0.5 +for 500.0 0.5 +a 500.5 0.5 +Seeker, 501.0 0.5 +"too,""" 501.5 0.5 +said 502.0 0.5 +Wood, 502.5 0.5 +now 503.0 0.5 +walking 503.5 0.5 +around 504.0 0.5 +Harry 504.5 0.5 +and 505.0 0.5 +staring 505.5 0.5 +at 506.0 0.5 +him. 506.5 0.5 +"""Light" 507.0 0.5 +-- 507.5 0.5 +speedy 508.0 0.5 +-- 508.5 0.5 +we'll 509.0 0.5 +have 509.5 0.5 +to 510.0 0.5 +get 510.5 0.5 +him 511.0 0.5 +a 511.5 0.5 +decent 512.0 0.5 +broom, 512.5 0.5 +Professor 513.0 0.5 +-- 513.5 0.5 +a 514.0 0.5 +Nimbus 514.5 0.5 +Two 515.0 0.5 +Thousand 515.5 0.5 +or 516.0 0.5 +a 516.5 0.5 +Cleansweep 517.0 0.5 +Seven, 517.5 0.5 +I'd 518.0 0.5 +"say.""" 518.5 0.5 ++ 519.0 0.5 +"""I" 519.5 0.5 +shall 520.0 0.5 +speak 520.5 0.5 +to 521.0 0.5 +Professor 521.5 0.5 +Dumbledore 522.0 0.5 +and 522.5 0.5 +see 523.0 0.5 +if 523.5 0.5 +we 524.0 0.5 +can't 524.5 0.5 +bend 525.0 0.5 +the 525.5 0.5 +first-year 526.0 0.5 +rule. 526.5 0.5 +Heaven 527.0 0.5 +knows, 527.5 0.5 +we 528.0 0.5 +need 528.5 0.5 +a 529.0 0.5 +better 529.5 0.5 +team 530.0 0.5 +than 530.5 0.5 +last 531.0 0.5 +year. 531.5 0.5 +Flattened 532.0 0.5 +in 532.5 0.5 +that 533.0 0.5 +last 533.5 0.5 +match 534.0 0.5 +by 534.5 0.5 +Slytherin, 535.0 0.5 +I 535.5 0.5 +couldn't 536.0 0.5 +look 536.5 0.5 +Severus 537.0 0.5 +Snape 537.5 0.5 +in 538.0 0.5 +the 538.5 0.5 +face 539.0 0.5 +for 539.5 0.5 +weeks. 540.0 0.5 +"...""" 540.5 0.5 ++ 541.0 0.5 +Professor 541.5 0.5 +McGonagall 542.0 0.5 +peered 542.5 0.5 +sternly 543.0 0.5 +over 543.5 0.5 +her 544.0 0.5 +glasses 544.5 0.5 +at 545.0 0.5 +Harry. 545.5 0.5 ++ 546.0 0.5 +"""I" 546.5 0.5 +want 547.0 0.5 +to 547.5 0.5 +hear 548.0 0.5 +you're 548.5 0.5 +training 549.0 0.5 +hard, 549.5 0.5 +Potter, 550.0 0.5 +or 550.5 0.5 +I 551.0 0.5 +may 551.5 0.5 +change 552.0 0.5 +my 552.5 0.5 +mind 553.0 0.5 +about 553.5 0.5 +punishing 554.0 0.5 +"you.""" 554.5 0.5 ++ 555.0 0.5 +Then 555.5 0.5 +she 556.0 0.5 +suddenly 556.5 0.5 +smiled. 557.0 0.5 ++ 557.5 0.5 +"""Your" 558.0 0.5 +father 558.5 0.5 +would 559.0 0.5 +have 559.5 0.5 +been 560.0 0.5 +"proud,""" 560.5 0.5 +she 561.0 0.5 +said. 561.5 0.5 +"""He" 562.0 0.5 +was 562.5 0.5 +an 563.0 0.5 +excellent 563.5 0.5 +Quidditch 564.0 0.5 +player 564.5 0.5 +"himself.""" 565.0 0.5 ++ 565.5 0.5 ++ 566.0 0.5 +"""You're" 566.5 0.5 +"joking.""" italic 567.0 0.5 ++ 567.5 0.5 +It 568.0 0.5 +was 568.5 0.5 +dinnertime. 569.0 0.5 +Harry 569.5 0.5 +had 570.0 0.5 +just 570.5 0.5 +finished 571.0 0.5 +telling 571.5 0.5 +Ron 572.0 0.5 +what 572.5 0.5 +had 573.0 0.5 +happened 573.5 0.5 +when 574.0 0.5 +he'd 574.5 0.5 +left 575.0 0.5 +the 575.5 0.5 +grounds 576.0 0.5 +with 576.5 0.5 +Professor 577.0 0.5 +McGonagall. 577.5 0.5 +Ron 578.0 0.5 +had 578.5 0.5 +a 579.0 0.5 +piece 579.5 0.5 +of 580.0 0.5 +steak 580.5 0.5 +and 581.0 0.5 +kidney 581.5 0.5 +pie 582.0 0.5 +halfway 582.5 0.5 +to 583.0 0.5 +his 583.5 0.5 +mouth, 584.0 0.5 +but 584.5 0.5 +he'd 585.0 0.5 +forgotten 585.5 0.5 +all 586.0 0.5 +about 586.5 0.5 +it. 587.0 0.5 ++ 587.5 0.5 +"""Seeker?""" italic 588.0 0.5 +he 588.5 0.5 +said. 589.0 0.5 +"""But" 589.5 0.5 +first 590.0 0.5 +years 590.5 0.5 +never italic 591.0 0.5 +-- 591.5 0.5 +you 592.0 0.5 +must 592.5 0.5 +be 593.0 0.5 +the 593.5 0.5 +youngest 594.0 0.5 +House 594.5 0.5 +player 595.0 0.5 +in 595.5 0.5 +about 596.0 0.5 +"--""" 596.5 0.5 ++ 597.0 0.5 +"""--" 597.5 0.5 +a 598.0 0.5 +"century,""" 598.5 0.5 +said 599.0 0.5 +Harry, 599.5 0.5 +shoveling 600.0 0.5 +pie 600.5 0.5 +into 601.0 0.5 +his 601.5 0.5 +mouth. 602.0 0.5 +He 602.5 0.5 +felt 603.0 0.5 +particularly 603.5 0.5 +hungry 604.0 0.5 +after 604.5 0.5 +the 605.0 0.5 +excitement 605.5 0.5 +of 606.0 0.5 +the 606.5 0.5 +afternoon. 607.0 0.5 +"""Wood" 607.5 0.5 +told 608.0 0.5 +"me.""" 608.5 0.5 ++ 609.0 0.5 +Ron 609.5 0.5 +was 610.0 0.5 +so 610.5 0.5 +amazed, 611.0 0.5 +so 611.5 0.5 +impressed, 612.0 0.5 +he 612.5 0.5 +just 613.0 0.5 +sat 613.5 0.5 +and 614.0 0.5 +gaped 614.5 0.5 +at 615.0 0.5 +Harry. 615.5 0.5 ++ 616.0 0.5 +"""I" 616.5 0.5 +start 617.0 0.5 +training 617.5 0.5 +next 618.0 0.5 +"week,""" 618.5 0.5 +said 619.0 0.5 +Harry. 619.5 0.5 +"""Only" 620.0 0.5 +don't 620.5 0.5 +tell 621.0 0.5 +anyone, 621.5 0.5 +Wood 622.0 0.5 +wants 622.5 0.5 +to 623.0 0.5 +keep 623.5 0.5 +it 624.0 0.5 +a 624.5 0.5 +"secret.""" 625.0 0.5 ++ 625.5 0.5 +Fred 626.0 0.5 +and 626.5 0.5 +George 627.0 0.5 +Weasley 627.5 0.5 +now 628.0 0.5 +came 628.5 0.5 +into 629.0 0.5 +the 629.5 0.5 +hall, 630.0 0.5 +spotted 630.5 0.5 +Harry, 631.0 0.5 +and 631.5 0.5 +hurried 632.0 0.5 +over. 632.5 0.5 ++ 633.0 0.5 +"""Well" 633.5 0.5 +"done,""" 634.0 0.5 +said 634.5 0.5 +George 635.0 0.5 +in 635.5 0.5 +a 636.0 0.5 +low 636.5 0.5 +voice. 637.0 0.5 +"""Wood" 637.5 0.5 +told 638.0 0.5 +us. 638.5 0.5 +We're 639.0 0.5 +on 639.5 0.5 +the 640.0 0.5 +team 640.5 0.5 +too 641.0 0.5 +-- 641.5 0.5 +"Beaters.""" 642.0 0.5 ++ 642.5 0.5 +"""I" 643.0 0.5 +tell 643.5 0.5 +you, 644.0 0.5 +we're 644.5 0.5 +going 645.0 0.5 +to 645.5 0.5 +win 646.0 0.5 +that 646.5 0.5 +Quidditch 647.0 0.5 +Cup 647.5 0.5 +for 648.0 0.5 +sure 648.5 0.5 +this 649.0 0.5 +"year,""" 649.5 0.5 +said 650.0 0.5 +Fred. 650.5 0.5 +"""We" 651.0 0.5 +haven't 651.5 0.5 +won 652.0 0.5 +since 652.5 0.5 +Charlie 653.0 0.5 +left, 653.5 0.5 +but 654.0 0.5 +this 654.5 0.5 +year's 655.0 0.5 +team 655.5 0.5 +is 656.0 0.5 +going 656.5 0.5 +to 657.0 0.5 +be 657.5 0.5 +brilliant. 658.0 0.5 +You 658.5 0.5 +must 659.0 0.5 +be 659.5 0.5 +good, 660.0 0.5 +Harry, 660.5 0.5 +Wood 661.0 0.5 +was 661.5 0.5 +almost 662.0 0.5 +skipping 662.5 0.5 +when 663.0 0.5 +he 663.5 0.5 +told 664.0 0.5 +"us.""" 664.5 0.5 ++ 665.0 0.5 +"""Anyway," 665.5 0.5 +we've 666.0 0.5 +got 666.5 0.5 +to 667.0 0.5 +go, 667.5 0.5 +Lee 668.0 0.5 +Jordan 668.5 0.5 +reckons 669.0 0.5 +he's 669.5 0.5 +found 670.0 0.5 +a 670.5 0.5 +new 671.0 0.5 +secret 671.5 0.5 +passageway 672.0 0.5 +out 672.5 0.5 +of 673.0 0.5 +the 673.5 0.5 +"school.""" 674.0 0.5 ++ 674.5 0.5 +"""Bet" 675.0 0.5 +it's 675.5 0.5 +that 676.0 0.5 +one 676.5 0.5 +behind 677.0 0.5 +the 677.5 0.5 +statue 678.0 0.5 +of 678.5 0.5 +Gregory 679.0 0.5 +the 679.5 0.5 +Smarmy 680.0 0.5 +that 680.5 0.5 +we 681.0 0.5 +found 681.5 0.5 +in 682.0 0.5 +our 682.5 0.5 +first 683.0 0.5 +week. 683.5 0.5 +See 684.0 0.5 +"you.""" 684.5 0.5 ++ 685.0 0.5 diff --git a/data/language/harrypotter/task-harry_run-3_events.tsv b/data/language/harrypotter/task-harry_run-3_events.tsv new file mode 100644 index 00000000..d3514f6c --- /dev/null +++ b/data/language/harrypotter/task-harry_run-3_events.tsv @@ -0,0 +1,1060 @@ +word format onset duration +Fred 10.0 0.5 +and 10.5 0.5 +George 11.0 0.5 +had 11.5 0.5 +hardly 12.0 0.5 +disappeared 12.5 0.5 +when 13.0 0.5 +someone 13.5 0.5 +far 14.0 0.5 +less 14.5 0.5 +welcome 15.0 0.5 +turned 15.5 0.5 +up: 16.0 0.5 +Malfoy, 16.5 0.5 +flanked 17.0 0.5 +by 17.5 0.5 +Crabbe 18.0 0.5 +and 18.5 0.5 +Goyle. 19.0 0.5 ++ 19.5 0.5 +"""Having" 20.0 0.5 +a 20.5 0.5 +last 21.0 0.5 +meal, 21.5 0.5 +Potter? 22.0 0.5 +When 22.5 0.5 +are 23.0 0.5 +you 23.5 0.5 +getting 24.0 0.5 +the 24.5 0.5 +train 25.0 0.5 +back 25.5 0.5 +to 26.0 0.5 +the 26.5 0.5 +"Muggles?""" 27.0 0.5 ++ 27.5 0.5 +"""You're" 28.0 0.5 +a 28.5 0.5 +lot 29.0 0.5 +braver 29.5 0.5 +now 30.0 0.5 +that 30.5 0.5 +you're 31.0 0.5 +back 31.5 0.5 +on 32.0 0.5 +the 32.5 0.5 +ground 33.0 0.5 +and 33.5 0.5 +you've 34.0 0.5 +got 34.5 0.5 +your 35.0 0.5 +little 35.5 0.5 +friends 36.0 0.5 +with 36.5 0.5 +"you,""" 37.0 0.5 +said 37.5 0.5 +Harry 38.0 0.5 +coolly. 38.5 0.5 +There 39.0 0.5 +was 39.5 0.5 +of 40.0 0.5 +course 40.5 0.5 +nothing 41.0 0.5 +at 41.5 0.5 +all 42.0 0.5 +little 42.5 0.5 +about 43.0 0.5 +Crabbe 43.5 0.5 +and 44.0 0.5 +Goyle, 44.5 0.5 +but 45.0 0.5 +as 45.5 0.5 +the 46.0 0.5 +High 46.5 0.5 +Table 47.0 0.5 +was 47.5 0.5 +full 48.0 0.5 +of 48.5 0.5 +teachers, 49.0 0.5 +neither 49.5 0.5 +of 50.0 0.5 +them 50.5 0.5 +could 51.0 0.5 +do 51.5 0.5 +more 52.0 0.5 +than 52.5 0.5 +crack 53.0 0.5 +their 53.5 0.5 +knuckles 54.0 0.5 +and 54.5 0.5 +scowl. 55.0 0.5 ++ 55.5 0.5 +"""I'd" 56.0 0.5 +take 56.5 0.5 +you 57.0 0.5 +on 57.5 0.5 +anytime 58.0 0.5 +on 58.5 0.5 +my 59.0 0.5 +"own,""" 59.5 0.5 +said 60.0 0.5 +Malfoy. 60.5 0.5 +"""Tonight," 61.0 0.5 +if 61.5 0.5 +you 62.0 0.5 +want. 62.5 0.5 +Wizard's 63.0 0.5 +duel. 63.5 0.5 +Wands 64.0 0.5 +only 64.5 0.5 +-- 65.0 0.5 +no 65.5 0.5 +contact. 66.0 0.5 +What's 66.5 0.5 +the 67.0 0.5 +matter? 67.5 0.5 +Never 68.0 0.5 +heard 68.5 0.5 +of 69.0 0.5 +a 69.5 0.5 +wizard's 70.0 0.5 +duel 70.5 0.5 +before, 71.0 0.5 +I 71.5 0.5 +"suppose?""" 72.0 0.5 ++ 72.5 0.5 +"""Of" 73.0 0.5 +course 73.5 0.5 +he 74.0 0.5 +"has,""" 74.5 0.5 +said 75.0 0.5 +Ron, 75.5 0.5 +wheeling 76.0 0.5 +around. 76.5 0.5 +"""I'm" 77.0 0.5 +his 77.5 0.5 +second, 78.0 0.5 +who's 78.5 0.5 +"yours?""" 79.0 0.5 ++ 79.5 0.5 +Malfoy 80.0 0.5 +looked 80.5 0.5 +at 81.0 0.5 +Crabbe 81.5 0.5 +and 82.0 0.5 +Goyle, 82.5 0.5 +sizing 83.0 0.5 +them 83.5 0.5 +up. 84.0 0.5 ++ 84.5 0.5 +"""Crabbe,""" 85.0 0.5 +he 85.5 0.5 +said. 86.0 0.5 +"""Midnight" 86.5 0.5 +all 87.0 0.5 +right? 87.5 0.5 +We'll 88.0 0.5 +meet 88.5 0.5 +you 89.0 0.5 +in 89.5 0.5 +the 90.0 0.5 +trophy 90.5 0.5 +room; 91.0 0.5 +that's 91.5 0.5 +always 92.0 0.5 +"unlocked.""" 92.5 0.5 ++ 93.0 0.5 +When 93.5 0.5 +Malfoy 94.0 0.5 +had 94.5 0.5 +gone, 95.0 0.5 +Ron 95.5 0.5 +and 96.0 0.5 +Harry 96.5 0.5 +looked 97.0 0.5 +at 97.5 0.5 +each 98.0 0.5 +other. 98.5 0.5 ++ 99.0 0.5 +"""What" 99.5 0.5 +is italic 100.0 0.5 +a 100.5 0.5 +wizard's 101.0 0.5 +"duel?""" 101.5 0.5 +said 102.0 0.5 +Harry. 102.5 0.5 +"""And" 103.0 0.5 +what 103.5 0.5 +do 104.0 0.5 +you 104.5 0.5 +mean, 105.0 0.5 +you're 105.5 0.5 +my 106.0 0.5 +"second?""" 106.5 0.5 ++ 107.0 0.5 +"""Well," 107.5 0.5 +a 108.0 0.5 +second's 108.5 0.5 +there 109.0 0.5 +to 109.5 0.5 +take 110.0 0.5 +over 110.5 0.5 +if 111.0 0.5 +you 111.5 0.5 +"die,""" 112.0 0.5 +said 112.5 0.5 +Ron 113.0 0.5 +casually, 113.5 0.5 +getting 114.0 0.5 +started 114.5 0.5 +at 115.0 0.5 +last 115.5 0.5 +on 116.0 0.5 +his 116.5 0.5 +cold 117.0 0.5 +pie. 117.5 0.5 +Catching 118.0 0.5 +the 118.5 0.5 +look 119.0 0.5 +on 119.5 0.5 +Harry's 120.0 0.5 +face, 120.5 0.5 +he 121.0 0.5 +added 121.5 0.5 +quickly, 122.0 0.5 +"""But" 122.5 0.5 +people 123.0 0.5 +only 123.5 0.5 +die 124.0 0.5 +in 124.5 0.5 +proper 125.0 0.5 +duels, 125.5 0.5 +you 126.0 0.5 +know, 126.5 0.5 +with 127.0 0.5 +real 127.5 0.5 +wizards. 128.0 0.5 +The 128.5 0.5 +most 129.0 0.5 +you 129.5 0.5 +and 130.0 0.5 +Malfoy'll 130.5 0.5 +be 131.0 0.5 +able 131.5 0.5 +to 132.0 0.5 +do 132.5 0.5 +is 133.0 0.5 +send 133.5 0.5 +sparks 134.0 0.5 +at 134.5 0.5 +each 135.0 0.5 +other. 135.5 0.5 +Neither 136.0 0.5 +of 136.5 0.5 +you 137.0 0.5 +knows 137.5 0.5 +enough 138.0 0.5 +magic 138.5 0.5 +to 139.0 0.5 +do 139.5 0.5 +any 140.0 0.5 +real 140.5 0.5 +damage. 141.0 0.5 +I 141.5 0.5 +bet 142.0 0.5 +he 142.5 0.5 +expected 143.0 0.5 +you 143.5 0.5 +to 144.0 0.5 +refuse, 144.5 0.5 +"anyway.""" 145.0 0.5 ++ 145.5 0.5 +"""And" 146.0 0.5 +what 146.5 0.5 +if 147.0 0.5 +I 147.5 0.5 +wave 148.0 0.5 +my 148.5 0.5 +wand 149.0 0.5 +and 149.5 0.5 +nothing 150.0 0.5 +"happens?""" 150.5 0.5 ++ 151.0 0.5 +"""Throw" 151.5 0.5 +it 152.0 0.5 +away 152.5 0.5 +and 153.0 0.5 +punch 153.5 0.5 +him 154.0 0.5 +on 154.5 0.5 +the 155.0 0.5 +"nose,""" 155.5 0.5 +Ron 156.0 0.5 +suggested. 156.5 0.5 ++ 157.0 0.5 +"""Excuse" 157.5 0.5 +"me.""" 158.0 0.5 ++ 158.5 0.5 +They 159.0 0.5 +both 159.5 0.5 +looked 160.0 0.5 +up. 160.5 0.5 +It 161.0 0.5 +was 161.5 0.5 +Hermione 162.0 0.5 +Granger. 162.5 0.5 ++ 163.0 0.5 +"""Can't" 163.5 0.5 +a 164.0 0.5 +person 164.5 0.5 +eat 165.0 0.5 +in 165.5 0.5 +peace 166.0 0.5 +in 166.5 0.5 +this 167.0 0.5 +"place?""" 167.5 0.5 +said 168.0 0.5 +Ron. 168.5 0.5 ++ 169.0 0.5 +Hermione 169.5 0.5 +ignored 170.0 0.5 +him 170.5 0.5 +and 171.0 0.5 +spoke 171.5 0.5 +to 172.0 0.5 +Harry. 172.5 0.5 +"""I" 173.0 0.5 +couldn't 173.5 0.5 +help 174.0 0.5 +overhearing 174.5 0.5 +what 175.0 0.5 +you 175.5 0.5 +and 176.0 0.5 +Malfoy 176.5 0.5 +were 177.0 0.5 +saying 177.5 0.5 +"--""" 178.0 0.5 ++ 178.5 0.5 +"""Bet" 179.0 0.5 +you 179.5 0.5 +"could,""" 180.0 0.5 +Ron 180.5 0.5 +muttered. 181.0 0.5 ++ 181.5 0.5 +"""--" 182.0 0.5 +and 182.5 0.5 +you 183.0 0.5 +mustn't italic 183.5 0.5 +go 184.0 0.5 +wandering 184.5 0.5 +around 185.0 0.5 +the 185.5 0.5 +school 186.0 0.5 +at 186.5 0.5 +night, 187.0 0.5 +think 187.5 0.5 +of 188.0 0.5 +the 188.5 0.5 +points 189.0 0.5 +you'll 189.5 0.5 +lose 190.0 0.5 +Gryffindor 190.5 0.5 +if 191.0 0.5 +you're 191.5 0.5 +caught, 192.0 0.5 +and 192.5 0.5 +you're 193.0 0.5 +bound 193.5 0.5 +to 194.0 0.5 +be. 194.5 0.5 +It's 195.0 0.5 +really 195.5 0.5 +very 196.0 0.5 +selfish 196.5 0.5 +of 197.0 0.5 +"you.""" 197.5 0.5 ++ 198.0 0.5 +"""And" 198.5 0.5 +it's 199.0 0.5 +really 199.5 0.5 +none 200.0 0.5 +of 200.5 0.5 +your 201.0 0.5 +"business,""" 201.5 0.5 +said 202.0 0.5 +Harry. 202.5 0.5 ++ 203.0 0.5 +"""Good-bye,""" 203.5 0.5 +said 204.0 0.5 +Ron. 204.5 0.5 ++ 205.0 0.5 ++ 205.5 0.5 +All 206.0 0.5 +the 206.5 0.5 +same, 207.0 0.5 +it 207.5 0.5 +wasn't 208.0 0.5 +what 208.5 0.5 +you'd 209.0 0.5 +call 209.5 0.5 +the 210.0 0.5 +perfect 210.5 0.5 +end 211.0 0.5 +to 211.5 0.5 +the 212.0 0.5 +day, 212.5 0.5 +Harry 213.0 0.5 +thought, 213.5 0.5 +as 214.0 0.5 +he 214.5 0.5 +lay 215.0 0.5 +awake 215.5 0.5 +much 216.0 0.5 +later 216.5 0.5 +listening 217.0 0.5 +to 217.5 0.5 +Dean 218.0 0.5 +and 218.5 0.5 +Seamus 219.0 0.5 +falling 219.5 0.5 +asleep 220.0 0.5 +(Neville 220.5 0.5 +wasn't 221.0 0.5 +back 221.5 0.5 +from 222.0 0.5 +the 222.5 0.5 +hospital 223.0 0.5 +wing). 223.5 0.5 +Ron 224.0 0.5 +had 224.5 0.5 +spent 225.0 0.5 +all 225.5 0.5 +evening 226.0 0.5 +giving 226.5 0.5 +him 227.0 0.5 +advice 227.5 0.5 +such 228.0 0.5 +as 228.5 0.5 +"""If" 229.0 0.5 +he 229.5 0.5 +tries 230.0 0.5 +to 230.5 0.5 +curse 231.0 0.5 +you, 231.5 0.5 +you'd 232.0 0.5 +better 232.5 0.5 +dodge 233.0 0.5 +it, 233.5 0.5 +because 234.0 0.5 +I 234.5 0.5 +can't 235.0 0.5 +remember 235.5 0.5 +how 236.0 0.5 +to 236.5 0.5 +block 237.0 0.5 +"them.""" 237.5 0.5 +There 238.0 0.5 +was 238.5 0.5 +a 239.0 0.5 +very 239.5 0.5 +good 240.0 0.5 +chance 240.5 0.5 +they 241.0 0.5 +were 241.5 0.5 +going 242.0 0.5 +to 242.5 0.5 +get 243.0 0.5 +caught 243.5 0.5 +by 244.0 0.5 +Filch 244.5 0.5 +or 245.0 0.5 +Mrs. 245.5 0.5 +Norris, 246.0 0.5 +and 246.5 0.5 +Harry 247.0 0.5 +felt 247.5 0.5 +he 248.0 0.5 +was 248.5 0.5 +pushing 249.0 0.5 +his 249.5 0.5 +luck, 250.0 0.5 +breaking 250.5 0.5 +another 251.0 0.5 +school 251.5 0.5 +rule 252.0 0.5 +today. 252.5 0.5 +On 253.0 0.5 +the 253.5 0.5 +other 254.0 0.5 +hand, 254.5 0.5 +Malfoy's 255.0 0.5 +sneering 255.5 0.5 +face 256.0 0.5 +kept 256.5 0.5 +looming 257.0 0.5 +up 257.5 0.5 +out 258.0 0.5 +of 258.5 0.5 +the 259.0 0.5 +darkness 259.5 0.5 +-- 260.0 0.5 +this 260.5 0.5 +was 261.0 0.5 +his 261.5 0.5 +big 262.0 0.5 +chance 262.5 0.5 +to 263.0 0.5 +beat 263.5 0.5 +Malfoy 264.0 0.5 +face-to-face. 264.5 0.5 +He 265.0 0.5 +couldn't 265.5 0.5 +miss 266.0 0.5 +it. 266.5 0.5 ++ 267.0 0.5 +"""Half-past" 267.5 0.5 +"eleven,""" 268.0 0.5 +Ron 268.5 0.5 +muttered 269.0 0.5 +at 269.5 0.5 +last, 270.0 0.5 +"""we'd" 270.5 0.5 +better 271.0 0.5 +"go.""" 271.5 0.5 ++ 272.0 0.5 +They 272.5 0.5 +pulled 273.0 0.5 +on 273.5 0.5 +their 274.0 0.5 +bathrobes, 274.5 0.5 +picked 275.0 0.5 +up 275.5 0.5 +their 276.0 0.5 +wands, 276.5 0.5 +and 277.0 0.5 +crept 277.5 0.5 +across 278.0 0.5 +the 278.5 0.5 +tower 279.0 0.5 +room, 279.5 0.5 +down 280.0 0.5 +the 280.5 0.5 +spiral 281.0 0.5 +staircase, 281.5 0.5 +and 282.0 0.5 +into 282.5 0.5 +the 283.0 0.5 +Gryffindor 283.5 0.5 +common 284.0 0.5 +room. 284.5 0.5 +A 285.0 0.5 +few 285.5 0.5 +embers 286.0 0.5 +were 286.5 0.5 +still 287.0 0.5 +glowing 287.5 0.5 +in 288.0 0.5 +the 288.5 0.5 +fireplace, 289.0 0.5 +turning 289.5 0.5 +all 290.0 0.5 +the 290.5 0.5 +armchairs 291.0 0.5 +into 291.5 0.5 +hunched 292.0 0.5 +black 292.5 0.5 +shadows. 293.0 0.5 +They 293.5 0.5 +had 294.0 0.5 +almost 294.5 0.5 +reached 295.0 0.5 +the 295.5 0.5 +portrait 296.0 0.5 +hole 296.5 0.5 +when 297.0 0.5 +a 297.5 0.5 +voice 298.0 0.5 +spoke 298.5 0.5 +from 299.0 0.5 +the 299.5 0.5 +chair 300.0 0.5 +nearest 300.5 0.5 +them, 301.0 0.5 +"""I" 301.5 0.5 +can't 302.0 0.5 +believe 302.5 0.5 +you're 303.0 0.5 +going 303.5 0.5 +to 304.0 0.5 +do 304.5 0.5 +this, 305.0 0.5 +"Harry.""" 305.5 0.5 ++ 306.0 0.5 +A 306.5 0.5 +lamp 307.0 0.5 +flickered 307.5 0.5 +on. 308.0 0.5 +It 308.5 0.5 +was 309.0 0.5 +Hermione 309.5 0.5 +Granger, 310.0 0.5 +wearing 310.5 0.5 +a 311.0 0.5 +pink 311.5 0.5 +bathrobe 312.0 0.5 +and 312.5 0.5 +a 313.0 0.5 +frown. 313.5 0.5 ++ 314.0 0.5 +"""You!""" italic 314.5 0.5 +said 315.0 0.5 +Ron 315.5 0.5 +furiously. 316.0 0.5 +"""Go" 316.5 0.5 +back 317.0 0.5 +to 317.5 0.5 +"bed!""" 318.0 0.5 ++ 318.5 0.5 +"""I" 319.0 0.5 +almost 319.5 0.5 +told 320.0 0.5 +your 320.5 0.5 +"brother,""" 321.0 0.5 +Hermione 321.5 0.5 +snapped, 322.0 0.5 +"""Percy" 322.5 0.5 +-- 323.0 0.5 +he's 323.5 0.5 +a 324.0 0.5 +prefect, 324.5 0.5 +he'd 325.0 0.5 +put 325.5 0.5 +a 326.0 0.5 +stop 326.5 0.5 +to 327.0 0.5 +"this.""" 327.5 0.5 ++ 328.0 0.5 +Harry 328.5 0.5 +couldn't 329.0 0.5 +believe 329.5 0.5 +anyone 330.0 0.5 +could 330.5 0.5 +be 331.0 0.5 +so 331.5 0.5 +interfering. 332.0 0.5 ++ 332.5 0.5 +"""Come" 333.0 0.5 +"on,""" 333.5 0.5 +he 334.0 0.5 +said 334.5 0.5 +to 335.0 0.5 +Ron. 335.5 0.5 +He 336.0 0.5 +pushed 336.5 0.5 +open 337.0 0.5 +the 337.5 0.5 +portrait 338.0 0.5 +of 338.5 0.5 +the 339.0 0.5 +Fat 339.5 0.5 +Lady 340.0 0.5 +and 340.5 0.5 +climbed 341.0 0.5 +through 341.5 0.5 +the 342.0 0.5 +hole. 342.5 0.5 ++ 343.0 0.5 +Hermione 343.5 0.5 +wasn't 344.0 0.5 +going 344.5 0.5 +to 345.0 0.5 +give 345.5 0.5 +up 346.0 0.5 +that 346.5 0.5 +easily. 347.0 0.5 +She 347.5 0.5 +followed 348.0 0.5 +Ron 348.5 0.5 +through 349.0 0.5 +the 349.5 0.5 +portrait 350.0 0.5 +hole, 350.5 0.5 +hissing 351.0 0.5 +at 351.5 0.5 +them 352.0 0.5 +like 352.5 0.5 +an 353.0 0.5 +angry 353.5 0.5 +goose. 354.0 0.5 +"""Don't" 354.5 0.5 +you 355.0 0.5 +care italic 355.5 0.5 +about 356.0 0.5 +Gryffindor, 356.5 0.5 +do 357.0 0.5 +you 357.5 0.5 +only italic 358.0 0.5 +care 358.5 0.5 +about 359.0 0.5 +yourselves, 359.5 0.5 +I 360.0 0.5 +don't 360.5 0.5 +want 361.0 0.5 +Slytherin 361.5 0.5 +to 362.0 0.5 +win 362.5 0.5 +the 363.0 0.5 +House 363.5 0.5 +Cup, 364.0 0.5 +and 364.5 0.5 +you'll 365.0 0.5 +lose 365.5 0.5 +all 366.0 0.5 +the 366.5 0.5 +points 367.0 0.5 +I 367.5 0.5 +got 368.0 0.5 +from 368.5 0.5 +Professor 369.0 0.5 +McGonagall 369.5 0.5 +for 370.0 0.5 +knowing 370.5 0.5 +about 371.0 0.5 +Switching 371.5 0.5 +"Spells.""" 372.0 0.5 ++ 372.5 0.5 +"""Go" 373.0 0.5 +"away.""" 373.5 0.5 ++ 374.0 0.5 +"""All" 374.5 0.5 +right, 375.0 0.5 +but 375.5 0.5 +I 376.0 0.5 +warned 376.5 0.5 +you, 377.0 0.5 +you 377.5 0.5 +just 378.0 0.5 +remember 378.5 0.5 +what 379.0 0.5 +I 379.5 0.5 +said 380.0 0.5 +when 380.5 0.5 +you're 381.0 0.5 +on 381.5 0.5 +the 382.0 0.5 +train 382.5 0.5 +home 383.0 0.5 +tomorrow, 383.5 0.5 +you're 384.0 0.5 +so 384.5 0.5 +"--""" 385.0 0.5 ++ 385.5 0.5 +But 386.0 0.5 +what 386.5 0.5 +they 387.0 0.5 +were, 387.5 0.5 +they 388.0 0.5 +didn't 388.5 0.5 +find 389.0 0.5 +out. 389.5 0.5 +Hermione 390.0 0.5 +had 390.5 0.5 +turned 391.0 0.5 +to 391.5 0.5 +the 392.0 0.5 +portrait 392.5 0.5 +of 393.0 0.5 +the 393.5 0.5 +Fat 394.0 0.5 +Lady 394.5 0.5 +to 395.0 0.5 +get 395.5 0.5 +back 396.0 0.5 +inside 396.5 0.5 +and 397.0 0.5 +found 397.5 0.5 +herself 398.0 0.5 +facing 398.5 0.5 +an 399.0 0.5 +empty 399.5 0.5 +painting. 400.0 0.5 +The 400.5 0.5 +Fat 401.0 0.5 +Lady 401.5 0.5 +had 402.0 0.5 +gone 402.5 0.5 +on 403.0 0.5 +a 403.5 0.5 +nighttime 404.0 0.5 +visit 404.5 0.5 +and 405.0 0.5 +Hermione 405.5 0.5 +was 406.0 0.5 +locked 406.5 0.5 +out 407.0 0.5 +of 407.5 0.5 +Gryffindor 408.0 0.5 +Tower. 408.5 0.5 ++ 409.0 0.5 +"""Now" 409.5 0.5 +what 410.0 0.5 +am 410.5 0.5 +I 411.0 0.5 +going 411.5 0.5 +to 412.0 0.5 +"do?""" 412.5 0.5 +she 413.0 0.5 +asked 413.5 0.5 +shrilly. 414.0 0.5 ++ 414.5 0.5 +"""That's" 415.0 0.5 +your 415.5 0.5 +"problem,""" 416.0 0.5 +said 416.5 0.5 +Ron. 417.0 0.5 +"""We've" 417.5 0.5 +got 418.0 0.5 +to 418.5 0.5 +go, 419.0 0.5 +we're 419.5 0.5 +going 420.0 0.5 +to 420.5 0.5 +be 421.0 0.5 +"late.""" 421.5 0.5 ++ 422.0 0.5 +They 422.5 0.5 +hadn't 423.0 0.5 +even 423.5 0.5 +reached 424.0 0.5 +the 424.5 0.5 +end 425.0 0.5 +of 425.5 0.5 +the 426.0 0.5 +corridor 426.5 0.5 +when 427.0 0.5 +Hermione 427.5 0.5 +caught 428.0 0.5 +up 428.5 0.5 +with 429.0 0.5 +them. 429.5 0.5 ++ 430.0 0.5 +"""I'm" 430.5 0.5 +coming 431.0 0.5 +with 431.5 0.5 +"you,""" 432.0 0.5 +she 432.5 0.5 +said. 433.0 0.5 +"""You" 433.5 0.5 +are 434.0 0.5 +"not.""" italic 434.5 0.5 ++ 435.0 0.5 +"""D'you" 435.5 0.5 +think 436.0 0.5 +I'm 436.5 0.5 +going 437.0 0.5 +to 437.5 0.5 +stand 438.0 0.5 +out 438.5 0.5 +here 439.0 0.5 +and 439.5 0.5 +wait 440.0 0.5 +for 440.5 0.5 +Filch 441.0 0.5 +to 441.5 0.5 +catch 442.0 0.5 +me? 442.5 0.5 +If 443.0 0.5 +he 443.5 0.5 +finds 444.0 0.5 +all 444.5 0.5 +three 445.0 0.5 +of 445.5 0.5 +us 446.0 0.5 +I'll 446.5 0.5 +tell 447.0 0.5 +him 447.5 0.5 +the 448.0 0.5 +truth, 448.5 0.5 +that 449.0 0.5 +I 449.5 0.5 +was 450.0 0.5 +trying 450.5 0.5 +to 451.0 0.5 +stop 451.5 0.5 +you, 452.0 0.5 +and 452.5 0.5 +you 453.0 0.5 +can 453.5 0.5 +back 454.0 0.5 +me 454.5 0.5 +"up.""" 455.0 0.5 ++ 455.5 0.5 +"""You've" 456.0 0.5 +got 456.5 0.5 +some 457.0 0.5 +nerve 457.5 0.5 +"--""" 458.0 0.5 +said 458.5 0.5 +Ron 459.0 0.5 +loudly. 459.5 0.5 ++ 460.0 0.5 +"""Shut" 460.5 0.5 +up, 461.0 0.5 +both 461.5 0.5 +of 462.0 0.5 +"you!""" 462.5 0.5 +said 463.0 0.5 +Harry 463.5 0.5 +sharply. 464.0 0.5 +"""I" 464.5 0.5 +heard 465.0 0.5 +"something.""" 465.5 0.5 ++ 466.0 0.5 +It 466.5 0.5 +was 467.0 0.5 +a 467.5 0.5 +sort 468.0 0.5 +of 468.5 0.5 +snuffling. 469.0 0.5 ++ 469.5 0.5 +"""Mrs." 470.0 0.5 +"Norris?""" 470.5 0.5 +breathed 471.0 0.5 +Ron, 471.5 0.5 +squinting 472.0 0.5 +through 472.5 0.5 +the 473.0 0.5 +dark. 473.5 0.5 ++ 474.0 0.5 +It 474.5 0.5 +wasn't 475.0 0.5 +Mrs. 475.5 0.5 +Norris. 476.0 0.5 +It 476.5 0.5 +was 477.0 0.5 +Neville. 477.5 0.5 +He 478.0 0.5 +was 478.5 0.5 +curled 479.0 0.5 +up 479.5 0.5 +on 480.0 0.5 +the 480.5 0.5 +floor, 481.0 0.5 +fast 481.5 0.5 +asleep, 482.0 0.5 +but 482.5 0.5 +jerked 483.0 0.5 +suddenly 483.5 0.5 +awake 484.0 0.5 +as 484.5 0.5 +they 485.0 0.5 +crept 485.5 0.5 +nearer. 486.0 0.5 ++ 486.5 0.5 +"""Thank" 487.0 0.5 +goodness 487.5 0.5 +you 488.0 0.5 +found 488.5 0.5 +me! 489.0 0.5 +I've 489.5 0.5 +been 490.0 0.5 +out 490.5 0.5 +here 491.0 0.5 +for 491.5 0.5 +hours, 492.0 0.5 +I 492.5 0.5 +couldn't 493.0 0.5 +remember 493.5 0.5 +the 494.0 0.5 +new 494.5 0.5 +password 495.0 0.5 +to 495.5 0.5 +get 496.0 0.5 +in 496.5 0.5 +to 497.0 0.5 +"bed.""" 497.5 0.5 ++ 498.0 0.5 +"""Keep" 498.5 0.5 +your 499.0 0.5 +voice 499.5 0.5 +down, 500.0 0.5 +Neville. 500.5 0.5 +The 501.0 0.5 +password's 501.5 0.5 +'Pig 502.0 0.5 +snout' 502.5 0.5 +but 503.0 0.5 +it 503.5 0.5 +won't 504.0 0.5 +help 504.5 0.5 +you 505.0 0.5 +now, 505.5 0.5 +the 506.0 0.5 +Fat 506.5 0.5 +Lady's 507.0 0.5 +gone 507.5 0.5 +off 508.0 0.5 +"somewhere.""" 508.5 0.5 ++ 509.0 0.5 +"""How's" 509.5 0.5 +your 510.0 0.5 +"arm?""" 510.5 0.5 +said 511.0 0.5 +Harry. 511.5 0.5 ++ 512.0 0.5 +"""Fine,""" 512.5 0.5 +said 513.0 0.5 +Neville, 513.5 0.5 +showing 514.0 0.5 +them. 514.5 0.5 +"""Madam" 515.0 0.5 +Pomfrey 515.5 0.5 +mended 516.0 0.5 +it 516.5 0.5 +in 517.0 0.5 +about 517.5 0.5 +a 518.0 0.5 +"minute.""" 518.5 0.5 ++ 519.0 0.5 +"""Good" 519.5 0.5 +-- 520.0 0.5 +well, 520.5 0.5 +look, 521.0 0.5 +Neville, 521.5 0.5 +we've 522.0 0.5 +got 522.5 0.5 +to 523.0 0.5 +be 523.5 0.5 +somewhere, 524.0 0.5 +we'll 524.5 0.5 +see 525.0 0.5 +you 525.5 0.5 +later 526.0 0.5 +"--""" 526.5 0.5 ++ 527.0 0.5 +"""Don't" 527.5 0.5 +leave 528.0 0.5 +"me!""" 528.5 0.5 +said 529.0 0.5 +Neville, 529.5 0.5 +scrambling 530.0 0.5 +to 530.5 0.5 +his 531.0 0.5 +feet, 531.5 0.5 +"""I" 532.0 0.5 +don't 532.5 0.5 +want 533.0 0.5 +to 533.5 0.5 +stay 534.0 0.5 +here 534.5 0.5 +alone, 535.0 0.5 +the 535.5 0.5 +Bloody 536.0 0.5 +Baron's 536.5 0.5 +been 537.0 0.5 +past 537.5 0.5 +twice 538.0 0.5 +"already.""" 538.5 0.5 ++ 539.0 0.5 diff --git a/data/language/harrypotter/task-harry_run-4_events.tsv b/data/language/harrypotter/task-harry_run-4_events.tsv new file mode 100644 index 00000000..be7d6600 --- /dev/null +++ b/data/language/harrypotter/task-harry_run-4_events.tsv @@ -0,0 +1,1464 @@ +word format onset duration +Ron 10.0 0.5 +looked 10.5 0.5 +at 11.0 0.5 +his 11.5 0.5 +watch 12.0 0.5 +and 12.5 0.5 +then 13.0 0.5 +glared 13.5 0.5 +furiously 14.0 0.5 +at 14.5 0.5 +Hermione 15.0 0.5 +and 15.5 0.5 +Neville. 16.0 0.5 ++ 16.5 0.5 +"""If" 17.0 0.5 +either 17.5 0.5 +of 18.0 0.5 +you 18.5 0.5 +get 19.0 0.5 +us 19.5 0.5 +caught, 20.0 0.5 +I'll 20.5 0.5 +never 21.0 0.5 +rest 21.5 0.5 +until 22.0 0.5 +I've 22.5 0.5 +learned 23.0 0.5 +that 23.5 0.5 +Curse 24.0 0.5 +of 24.5 0.5 +the 25.0 0.5 +Bogies 25.5 0.5 +Quirrell 26.0 0.5 +told 26.5 0.5 +us 27.0 0.5 +about, 27.5 0.5 +and 28.0 0.5 +used 28.5 0.5 +it 29.0 0.5 +on 29.5 0.5 +"you.""" 30.0 0.5 ++ 30.5 0.5 +Hermione 31.0 0.5 +opened 31.5 0.5 +her 32.0 0.5 +mouth, 32.5 0.5 +perhaps 33.0 0.5 +to 33.5 0.5 +tell 34.0 0.5 +Ron 34.5 0.5 +exactly 35.0 0.5 +how 35.5 0.5 +to 36.0 0.5 +use 36.5 0.5 +the 37.0 0.5 +Curse 37.5 0.5 +of 38.0 0.5 +the 38.5 0.5 +Bogies, 39.0 0.5 +but 39.5 0.5 +Harry 40.0 0.5 +hissed 40.5 0.5 +at 41.0 0.5 +her 41.5 0.5 +to 42.0 0.5 +be 42.5 0.5 +quiet 43.0 0.5 +and 43.5 0.5 +beckoned 44.0 0.5 +them 44.5 0.5 +all 45.0 0.5 +forward. 45.5 0.5 ++ 46.0 0.5 +They 46.5 0.5 +flitted 47.0 0.5 +along 47.5 0.5 +corridors 48.0 0.5 +striped 48.5 0.5 +with 49.0 0.5 +bars 49.5 0.5 +of 50.0 0.5 +moonlight 50.5 0.5 +from 51.0 0.5 +the 51.5 0.5 +high 52.0 0.5 +windows. 52.5 0.5 +At 53.0 0.5 +every 53.5 0.5 +turn 54.0 0.5 +Harry 54.5 0.5 +expected 55.0 0.5 +to 55.5 0.5 +run 56.0 0.5 +into 56.5 0.5 +Filch 57.0 0.5 +or 57.5 0.5 +Mrs. 58.0 0.5 +Norris, 58.5 0.5 +but 59.0 0.5 +they 59.5 0.5 +were 60.0 0.5 +lucky. 60.5 0.5 +They 61.0 0.5 +sped 61.5 0.5 +up 62.0 0.5 +a 62.5 0.5 +staircase 63.0 0.5 +to 63.5 0.5 +the 64.0 0.5 +third 64.5 0.5 +floor 65.0 0.5 +and 65.5 0.5 +tiptoed 66.0 0.5 +toward 66.5 0.5 +the 67.0 0.5 +trophy 67.5 0.5 +room. 68.0 0.5 ++ 68.5 0.5 +Malfoy 69.0 0.5 +and 69.5 0.5 +Crabbe 70.0 0.5 +weren't 70.5 0.5 +there 71.0 0.5 +yet. 71.5 0.5 +The 72.0 0.5 +crystal 72.5 0.5 +trophy 73.0 0.5 +cases 73.5 0.5 +glimmered 74.0 0.5 +where 74.5 0.5 +the 75.0 0.5 +moonlight 75.5 0.5 +caught 76.0 0.5 +them. 76.5 0.5 +Cups, 77.0 0.5 +shields, 77.5 0.5 +plates, 78.0 0.5 +and 78.5 0.5 +statues 79.0 0.5 +winked 79.5 0.5 +silver 80.0 0.5 +and 80.5 0.5 +gold 81.0 0.5 +in 81.5 0.5 +the 82.0 0.5 +darkness. 82.5 0.5 +They 83.0 0.5 +edged 83.5 0.5 +along 84.0 0.5 +the 84.5 0.5 +walls, 85.0 0.5 +keeping 85.5 0.5 +their 86.0 0.5 +eyes 86.5 0.5 +on 87.0 0.5 +the 87.5 0.5 +doors 88.0 0.5 +at 88.5 0.5 +either 89.0 0.5 +end 89.5 0.5 +of 90.0 0.5 +the 90.5 0.5 +room. 91.0 0.5 +Harry 91.5 0.5 +took 92.0 0.5 +out 92.5 0.5 +his 93.0 0.5 +wand 93.5 0.5 +in 94.0 0.5 +case 94.5 0.5 +Malfoy 95.0 0.5 +leapt 95.5 0.5 +in 96.0 0.5 +and 96.5 0.5 +started 97.0 0.5 +at 97.5 0.5 +once. 98.0 0.5 +The 98.5 0.5 +minutes 99.0 0.5 +crept 99.5 0.5 +by. 100.0 0.5 ++ 100.5 0.5 +"""He's" 101.0 0.5 +late, 101.5 0.5 +maybe 102.0 0.5 +he's 102.5 0.5 +chickened 103.0 0.5 +"out,""" 103.5 0.5 +Ron 104.0 0.5 +whispered. 104.5 0.5 ++ 105.0 0.5 +Then 105.5 0.5 +a 106.0 0.5 +noise 106.5 0.5 +in 107.0 0.5 +the 107.5 0.5 +next 108.0 0.5 +room 108.5 0.5 +made 109.0 0.5 +them 109.5 0.5 +jump. 110.0 0.5 +Harry 110.5 0.5 +had 111.0 0.5 +only 111.5 0.5 +just 112.0 0.5 +raised 112.5 0.5 +his 113.0 0.5 +wand 113.5 0.5 +when 114.0 0.5 +they 114.5 0.5 +heard 115.0 0.5 +someone 115.5 0.5 +speak 116.0 0.5 +-- 116.5 0.5 +and 117.0 0.5 +it 117.5 0.5 +wasn't 118.0 0.5 +Malfoy. 118.5 0.5 ++ 119.0 0.5 +"""Sniff" 119.5 0.5 +around, 120.0 0.5 +my 120.5 0.5 +sweet, 121.0 0.5 +they 121.5 0.5 +might 122.0 0.5 +be 122.5 0.5 +lurking 123.0 0.5 +in 123.5 0.5 +a 124.0 0.5 +"corner.""" 124.5 0.5 ++ 125.0 0.5 +It 125.5 0.5 +was 126.0 0.5 +Filch 126.5 0.5 +speaking 127.0 0.5 +to 127.5 0.5 +Mrs. 128.0 0.5 +Norris. 128.5 0.5 +Horror-struck, 129.0 0.5 +Harry 129.5 0.5 +waved 130.0 0.5 +madly 130.5 0.5 +at 131.0 0.5 +the 131.5 0.5 +other 132.0 0.5 +three 132.5 0.5 +to 133.0 0.5 +follow 133.5 0.5 +him 134.0 0.5 +as 134.5 0.5 +quickly 135.0 0.5 +as 135.5 0.5 +possible; 136.0 0.5 +they 136.5 0.5 +scurried 137.0 0.5 +silently 137.5 0.5 +toward 138.0 0.5 +the 138.5 0.5 +door, 139.0 0.5 +away 139.5 0.5 +from 140.0 0.5 +Filch's 140.5 0.5 +voice. 141.0 0.5 +Neville's 141.5 0.5 +robes 142.0 0.5 +had 142.5 0.5 +barely 143.0 0.5 +whipped 143.5 0.5 +round 144.0 0.5 +the 144.5 0.5 +corner 145.0 0.5 +when 145.5 0.5 +they 146.0 0.5 +heard 146.5 0.5 +Filch 147.0 0.5 +enter 147.5 0.5 +the 148.0 0.5 +trophy 148.5 0.5 +room. 149.0 0.5 ++ 149.5 0.5 +"""They're" 150.0 0.5 +in 150.5 0.5 +here 151.0 0.5 +"somewhere,""" 151.5 0.5 +they 152.0 0.5 +heard 152.5 0.5 +him 153.0 0.5 +mutter, 153.5 0.5 +"""probably" 154.0 0.5 +"hiding.""" 154.5 0.5 ++ 155.0 0.5 +"""This" 155.5 0.5 +"way!""" 156.0 0.5 +Harry 156.5 0.5 +mouthed 157.0 0.5 +to 157.5 0.5 +the 158.0 0.5 +others 158.5 0.5 +and, 159.0 0.5 +petrified, 159.5 0.5 +they 160.0 0.5 +began 160.5 0.5 +to 161.0 0.5 +creep 161.5 0.5 +down 162.0 0.5 +a 162.5 0.5 +long 163.0 0.5 +gallery 163.5 0.5 +full 164.0 0.5 +of 164.5 0.5 +suits 165.0 0.5 +of 165.5 0.5 +armor. 166.0 0.5 ++ 166.5 0.5 +They 167.0 0.5 +could 167.5 0.5 +hear 168.0 0.5 +Filch 168.5 0.5 +getting 169.0 0.5 +nearer. 169.5 0.5 +Neville 170.0 0.5 +suddenly 170.5 0.5 +let 171.0 0.5 +out 171.5 0.5 +a 172.0 0.5 +frightened 172.5 0.5 +squeak 173.0 0.5 +and 173.5 0.5 +broke 174.0 0.5 +into 174.5 0.5 +a 175.0 0.5 +run 175.5 0.5 +-- 176.0 0.5 +he 176.5 0.5 +tripped, 177.0 0.5 +grabbed 177.5 0.5 +Ron 178.0 0.5 +around 178.5 0.5 +the 179.0 0.5 +waist, 179.5 0.5 +and 180.0 0.5 +the 180.5 0.5 +pair 181.0 0.5 +of 181.5 0.5 +them 182.0 0.5 +toppled 182.5 0.5 +right 183.0 0.5 +into 183.5 0.5 +a 184.0 0.5 +suit 184.5 0.5 +of 185.0 0.5 +armor. 185.5 0.5 ++ 186.0 0.5 +The 186.5 0.5 +clanging 187.0 0.5 +and 187.5 0.5 +crashing 188.0 0.5 +were 188.5 0.5 +enough 189.0 0.5 +to 189.5 0.5 +wake 190.0 0.5 +the 190.5 0.5 +whole 191.0 0.5 +castle. 191.5 0.5 ++ 192.0 0.5 +"""RUN!""" 192.5 0.5 +Harry 193.0 0.5 +yelled, 193.5 0.5 +and 194.0 0.5 +the 194.5 0.5 +four 195.0 0.5 +of 195.5 0.5 +them 196.0 0.5 +sprinted 196.5 0.5 +down 197.0 0.5 +the 197.5 0.5 +gallery, 198.0 0.5 +not 198.5 0.5 +looking 199.0 0.5 +back 199.5 0.5 +to 200.0 0.5 +see 200.5 0.5 +whether 201.0 0.5 +Filch 201.5 0.5 +was 202.0 0.5 +following 202.5 0.5 +-- 203.0 0.5 +they 203.5 0.5 +swung 204.0 0.5 +around 204.5 0.5 +the 205.0 0.5 +doorpost 205.5 0.5 +and 206.0 0.5 +galloped 206.5 0.5 +down 207.0 0.5 +one 207.5 0.5 +corridor 208.0 0.5 +then 208.5 0.5 +another, 209.0 0.5 +Harry 209.5 0.5 +in 210.0 0.5 +the 210.5 0.5 +lead, 211.0 0.5 +without 211.5 0.5 +any 212.0 0.5 +idea 212.5 0.5 +where 213.0 0.5 +they 213.5 0.5 +were 214.0 0.5 +or 214.5 0.5 +where 215.0 0.5 +they 215.5 0.5 +were 216.0 0.5 +going 216.5 0.5 +-- 217.0 0.5 +they 217.5 0.5 +ripped 218.0 0.5 +through 218.5 0.5 +a 219.0 0.5 +tapestry 219.5 0.5 +and 220.0 0.5 +found 220.5 0.5 +themselves 221.0 0.5 +in 221.5 0.5 +a 222.0 0.5 +hidden 222.5 0.5 +passageway, 223.0 0.5 +hurtled 223.5 0.5 +along 224.0 0.5 +it 224.5 0.5 +and 225.0 0.5 +came 225.5 0.5 +out 226.0 0.5 +near 226.5 0.5 +their 227.0 0.5 +Charms 227.5 0.5 +classroom, 228.0 0.5 +which 228.5 0.5 +they 229.0 0.5 +knew 229.5 0.5 +was 230.0 0.5 +miles 230.5 0.5 +from 231.0 0.5 +the 231.5 0.5 +trophy 232.0 0.5 +room. 232.5 0.5 ++ 233.0 0.5 +"""I" 233.5 0.5 +think 234.0 0.5 +we've 234.5 0.5 +lost 235.0 0.5 +"him,""" 235.5 0.5 +Harry 236.0 0.5 +panted, 236.5 0.5 +leaning 237.0 0.5 +against 237.5 0.5 +the 238.0 0.5 +cold 238.5 0.5 +wall 239.0 0.5 +and 239.5 0.5 +wiping 240.0 0.5 +his 240.5 0.5 +forehead. 241.0 0.5 +Neville 241.5 0.5 +was 242.0 0.5 +bent 242.5 0.5 +double, 243.0 0.5 +wheezing 243.5 0.5 +and 244.0 0.5 +spluttering. 244.5 0.5 ++ 245.0 0.5 +"""I" 245.5 0.5 +-- 246.0 0.5 +told italic 246.5 0.5 +-- 247.0 0.5 +"you,""" 247.5 0.5 +Hermione 248.0 0.5 +gasped, 248.5 0.5 +clutching 249.0 0.5 +at 249.5 0.5 +the 250.0 0.5 +stitch 250.5 0.5 +in 251.0 0.5 +her 251.5 0.5 +chest, 252.0 0.5 +"""I" 252.5 0.5 +-- 253.0 0.5 +told 253.5 0.5 +-- 254.0 0.5 +"you.""" 254.5 0.5 ++ 255.0 0.5 +"""We've" 255.5 0.5 +got 256.0 0.5 +to 256.5 0.5 +get 257.0 0.5 +back 257.5 0.5 +to 258.0 0.5 +Gryffindor 258.5 0.5 +"Tower,""" 259.0 0.5 +said 259.5 0.5 +Ron, 260.0 0.5 +"""quickly" 260.5 0.5 +as 261.0 0.5 +"possible.""" 261.5 0.5 ++ 262.0 0.5 +"""Malfoy" 262.5 0.5 +tricked 263.0 0.5 +"you,""" 263.5 0.5 +Hermione 264.0 0.5 +said 264.5 0.5 +to 265.0 0.5 +Harry. 265.5 0.5 +"""You" 266.0 0.5 +realize 266.5 0.5 +that, 267.0 0.5 +don't 267.5 0.5 +you? 268.0 0.5 +He 268.5 0.5 +was 269.0 0.5 +never 269.5 0.5 +going 270.0 0.5 +to 270.5 0.5 +meet 271.0 0.5 +you 271.5 0.5 +-- 272.0 0.5 +Filch 272.5 0.5 +knew 273.0 0.5 +someone 273.5 0.5 +was 274.0 0.5 +going 274.5 0.5 +to 275.0 0.5 +be 275.5 0.5 +in 276.0 0.5 +the 276.5 0.5 +trophy 277.0 0.5 +room, 277.5 0.5 +Malfoy 278.0 0.5 +must 278.5 0.5 +have 279.0 0.5 +tipped 279.5 0.5 +him 280.0 0.5 +"off.""" 280.5 0.5 ++ 281.0 0.5 +Harry 281.5 0.5 +thought 282.0 0.5 +she 282.5 0.5 +was 283.0 0.5 +probably 283.5 0.5 +right, 284.0 0.5 +but 284.5 0.5 +he 285.0 0.5 +wasn't 285.5 0.5 +going 286.0 0.5 +to 286.5 0.5 +tell 287.0 0.5 +her 287.5 0.5 +that. 288.0 0.5 ++ 288.5 0.5 +"""Let's" 289.0 0.5 +"go.""" 289.5 0.5 ++ 290.0 0.5 +It 290.5 0.5 +wasn't 291.0 0.5 +going 291.5 0.5 +to 292.0 0.5 +be 292.5 0.5 +that 293.0 0.5 +simple. 293.5 0.5 +They 294.0 0.5 +hadn't 294.5 0.5 +gone 295.0 0.5 +more 295.5 0.5 +than 296.0 0.5 +a 296.5 0.5 +dozen 297.0 0.5 +paces 297.5 0.5 +when 298.0 0.5 +a 298.5 0.5 +doorknob 299.0 0.5 +rattled 299.5 0.5 +and 300.0 0.5 +something 300.5 0.5 +came 301.0 0.5 +shooting 301.5 0.5 +out 302.0 0.5 +of 302.5 0.5 +a 303.0 0.5 +classroom 303.5 0.5 +in 304.0 0.5 +front 304.5 0.5 +of 305.0 0.5 +them. 305.5 0.5 ++ 306.0 0.5 +It 306.5 0.5 +was 307.0 0.5 +Peeves. 307.5 0.5 +He 308.0 0.5 +caught 308.5 0.5 +sight 309.0 0.5 +of 309.5 0.5 +them 310.0 0.5 +and 310.5 0.5 +gave 311.0 0.5 +a 311.5 0.5 +squeal 312.0 0.5 +of 312.5 0.5 +delight. 313.0 0.5 ++ 313.5 0.5 +"""Shut" 314.0 0.5 +up, 314.5 0.5 +Peeves 315.0 0.5 +-- 315.5 0.5 +please 316.0 0.5 +-- 316.5 0.5 +you'll 317.0 0.5 +get 317.5 0.5 +us 318.0 0.5 +thrown 318.5 0.5 +"out.""" 319.0 0.5 ++ 319.5 0.5 +Peeves 320.0 0.5 +cackled. 320.5 0.5 ++ 321.0 0.5 +"""Wandering" 321.5 0.5 +around 322.0 0.5 +at 322.5 0.5 +midnight, 323.0 0.5 +Ickle 323.5 0.5 +Firsties? 324.0 0.5 +Tut, 324.5 0.5 +tut, 325.0 0.5 +tut. 325.5 0.5 +Naughty, 326.0 0.5 +naughty, 326.5 0.5 +you'll 327.0 0.5 +get 327.5 0.5 +"caughty.""" 328.0 0.5 ++ 328.5 0.5 +"""Not" 329.0 0.5 +if 329.5 0.5 +you 330.0 0.5 +don't 330.5 0.5 +give 331.0 0.5 +us 331.5 0.5 +away, 332.0 0.5 +Peeves, 332.5 0.5 +"please.""" 333.0 0.5 ++ 333.5 0.5 +"""Should" 334.0 0.5 +tell 334.5 0.5 +Filch, 335.0 0.5 +I 335.5 0.5 +"should,""" 336.0 0.5 +said 336.5 0.5 +Peeves 337.0 0.5 +in 337.5 0.5 +a 338.0 0.5 +saintly 338.5 0.5 +voice, 339.0 0.5 +but 339.5 0.5 +his 340.0 0.5 +eyes 340.5 0.5 +glittered 341.0 0.5 +wickedly. 341.5 0.5 +"""It's" 342.0 0.5 +for 342.5 0.5 +your 343.0 0.5 +own 343.5 0.5 +good, 344.0 0.5 +you 344.5 0.5 +"know.""" 345.0 0.5 ++ 345.5 0.5 +"""Get" 346.0 0.5 +out 346.5 0.5 +of 347.0 0.5 +the 347.5 0.5 +"way,""" 348.0 0.5 +snapped 348.5 0.5 +Ron, 349.0 0.5 +taking 349.5 0.5 +a 350.0 0.5 +swipe 350.5 0.5 +at 351.0 0.5 +Peeves 351.5 0.5 +-- 352.0 0.5 +this 352.5 0.5 +was 353.0 0.5 +a 353.5 0.5 +big 354.0 0.5 +mistake. 354.5 0.5 ++ 355.0 0.5 +"""STUDENTS" 355.5 0.5 +OUT 356.0 0.5 +OF 356.5 0.5 +"BED!""" 357.0 0.5 +Peeves 357.5 0.5 +bellowed, 358.0 0.5 +"""STUDENTS" 358.5 0.5 +OUT 359.0 0.5 +OF 359.5 0.5 +BED 360.0 0.5 +DOWN 360.5 0.5 +THE 361.0 0.5 +CHARMS 361.5 0.5 +"CORRIDOR!""" 362.0 0.5 ++ 362.5 0.5 +Ducking 363.0 0.5 +under 363.5 0.5 +Peeves, 364.0 0.5 +they 364.5 0.5 +ran 365.0 0.5 +for 365.5 0.5 +their 366.0 0.5 +lives, 366.5 0.5 +right 367.0 0.5 +to 367.5 0.5 +the 368.0 0.5 +end 368.5 0.5 +of 369.0 0.5 +the 369.5 0.5 +corridor 370.0 0.5 +where 370.5 0.5 +they 371.0 0.5 +slammed 371.5 0.5 +into 372.0 0.5 +a 372.5 0.5 +door 373.0 0.5 +-- 373.5 0.5 +and 374.0 0.5 +it 374.5 0.5 +was 375.0 0.5 +locked. 375.5 0.5 ++ 376.0 0.5 +"""This" 376.5 0.5 +is 377.0 0.5 +"it!""" 377.5 0.5 +Ron 378.0 0.5 +moaned, 378.5 0.5 +as 379.0 0.5 +they 379.5 0.5 +pushed 380.0 0.5 +helplessly 380.5 0.5 +at 381.0 0.5 +the 381.5 0.5 +door, 382.0 0.5 +"""We're" 382.5 0.5 +done 383.0 0.5 +for! 383.5 0.5 +This 384.0 0.5 +is 384.5 0.5 +the 385.0 0.5 +"end!""" 385.5 0.5 ++ 386.0 0.5 +They 386.5 0.5 +could 387.0 0.5 +hear 387.5 0.5 +footsteps, 388.0 0.5 +Filch 388.5 0.5 +running 389.0 0.5 +as 389.5 0.5 +fast 390.0 0.5 +as 390.5 0.5 +he 391.0 0.5 +could 391.5 0.5 +toward 392.0 0.5 +Peeves's 392.5 0.5 +shouts. 393.0 0.5 ++ 393.5 0.5 +"""Oh," 394.0 0.5 +move 394.5 0.5 +"over,""" 395.0 0.5 +Hermione 395.5 0.5 +snarled. 396.0 0.5 +She 396.5 0.5 +grabbed 397.0 0.5 +Harry's 397.5 0.5 +wand, 398.0 0.5 +tapped 398.5 0.5 +the 399.0 0.5 +lock, 399.5 0.5 +and 400.0 0.5 +whispered, 400.5 0.5 +"""Alohomora!""" italic 401.0 0.5 ++ 401.5 0.5 +The 402.0 0.5 +lock 402.5 0.5 +clicked 403.0 0.5 +and 403.5 0.5 +the 404.0 0.5 +door 404.5 0.5 +swung 405.0 0.5 +open 405.5 0.5 +-- 406.0 0.5 +they 406.5 0.5 +piled 407.0 0.5 +through 407.5 0.5 +it, 408.0 0.5 +shut 408.5 0.5 +it 409.0 0.5 +quickly, 409.5 0.5 +and 410.0 0.5 +pressed 410.5 0.5 +their 411.0 0.5 +ears 411.5 0.5 +against 412.0 0.5 +it, 412.5 0.5 +listening. 413.0 0.5 ++ 413.5 0.5 +"""Which" 414.0 0.5 +way 414.5 0.5 +did 415.0 0.5 +they 415.5 0.5 +go, 416.0 0.5 +"Peeves?""" 416.5 0.5 +Filch 417.0 0.5 +was 417.5 0.5 +saying. 418.0 0.5 +"""Quick," 418.5 0.5 +tell 419.0 0.5 +"me.""" 419.5 0.5 +"""Say" 420.0 0.5 +"'please.'""" 420.5 0.5 ++ 421.0 0.5 +"""Don't" 421.5 0.5 +mess 422.0 0.5 +with 422.5 0.5 +me, 423.0 0.5 +Peeves, 423.5 0.5 +now 424.0 0.5 +where italic 424.5 0.5 +did italic 425.0 0.5 +they italic 425.5 0.5 +"go?""" italic 426.0 0.5 +"""Shan't" 426.5 0.5 +say 427.0 0.5 +nothing 427.5 0.5 +if 428.0 0.5 +you 428.5 0.5 +don't 429.0 0.5 +say 429.5 0.5 +"please,""" 430.0 0.5 +said 430.5 0.5 +Peeves 431.0 0.5 +in 431.5 0.5 +his 432.0 0.5 +annoying 432.5 0.5 +singsong 433.0 0.5 +voice. 433.5 0.5 +"""All" 434.0 0.5 +right 434.5 0.5 +-- 435.0 0.5 +"please.""" italic 435.5 0.5 ++ 436.0 0.5 +"""NOTHING!" 436.5 0.5 +Ha 437.0 0.5 +haaa! 437.5 0.5 +Told 438.0 0.5 +you 438.5 0.5 +I 439.0 0.5 +wouldn't 439.5 0.5 +say 440.0 0.5 +nothing 440.5 0.5 +if 441.0 0.5 +you 441.5 0.5 +didn't 442.0 0.5 +say 442.5 0.5 +please! 443.0 0.5 +Ha 443.5 0.5 +ha! 444.0 0.5 +"Haaaaaa!""" 444.5 0.5 +And 445.0 0.5 +they 445.5 0.5 +heard 446.0 0.5 +the 446.5 0.5 +sound 447.0 0.5 +of 447.5 0.5 +Peeves 448.0 0.5 +whooshing 448.5 0.5 +away 449.0 0.5 +and 449.5 0.5 +Filch 450.0 0.5 +cursing 450.5 0.5 +in 451.0 0.5 +rage. 451.5 0.5 ++ 452.0 0.5 +"""He" 452.5 0.5 +thinks 453.0 0.5 +this 453.5 0.5 +door 454.0 0.5 +is 454.5 0.5 +"locked,""" 455.0 0.5 +Harry 455.5 0.5 +whispered. 456.0 0.5 +"""I" 456.5 0.5 +think 457.0 0.5 +we'll 457.5 0.5 +be 458.0 0.5 +okay 458.5 0.5 +-- 459.0 0.5 +get 459.5 0.5 +off, italic 460.0 0.5 +"Neville!""" 460.5 0.5 +For 461.0 0.5 +Neville 461.5 0.5 +had 462.0 0.5 +been 462.5 0.5 +tugging 463.0 0.5 +on 463.5 0.5 +the 464.0 0.5 +sleeve 464.5 0.5 +of 465.0 0.5 +Harry's 465.5 0.5 +bathrobe 466.0 0.5 +for 466.5 0.5 +the 467.0 0.5 +last 467.5 0.5 +minute. 468.0 0.5 +"""What?""" italic 468.5 0.5 ++ 469.0 0.5 +Harry 469.5 0.5 +turned 470.0 0.5 +around 470.5 0.5 +-- 471.0 0.5 +and 471.5 0.5 +saw, 472.0 0.5 +quite 472.5 0.5 +clearly, 473.0 0.5 +what. 473.5 0.5 +For 474.0 0.5 +a 474.5 0.5 +moment, 475.0 0.5 +he 475.5 0.5 +was 476.0 0.5 +sure 476.5 0.5 +he'd 477.0 0.5 +walked 477.5 0.5 +into 478.0 0.5 +a 478.5 0.5 +nightmare 479.0 0.5 +-- 479.5 0.5 +this 480.0 0.5 +was 480.5 0.5 +too 481.0 0.5 +much, 481.5 0.5 +on 482.0 0.5 +top 482.5 0.5 +of 483.0 0.5 +everything 483.5 0.5 +that 484.0 0.5 +had 484.5 0.5 +happened 485.0 0.5 +so 485.5 0.5 +far. 486.0 0.5 ++ 486.5 0.5 +They 487.0 0.5 +weren't 487.5 0.5 +in 488.0 0.5 +a 488.5 0.5 +room, 489.0 0.5 +as 489.5 0.5 +he 490.0 0.5 +had 490.5 0.5 +supposed. 491.0 0.5 +They 491.5 0.5 +were 492.0 0.5 +in 492.5 0.5 +a 493.0 0.5 +corridor. 493.5 0.5 +The 494.0 0.5 +forbidden 494.5 0.5 +corridor 495.0 0.5 +on 495.5 0.5 +the 496.0 0.5 +third 496.5 0.5 +floor. 497.0 0.5 +And 497.5 0.5 +now 498.0 0.5 +they 498.5 0.5 +knew 499.0 0.5 +why 499.5 0.5 +it 500.0 0.5 +was 500.5 0.5 +forbidden. 501.0 0.5 ++ 501.5 0.5 +They 502.0 0.5 +were 502.5 0.5 +looking 503.0 0.5 +straight 503.5 0.5 +into 504.0 0.5 +the 504.5 0.5 +eyes 505.0 0.5 +of 505.5 0.5 +a 506.0 0.5 +monstrous 506.5 0.5 +dog, 507.0 0.5 +a 507.5 0.5 +dog 508.0 0.5 +that 508.5 0.5 +filled 509.0 0.5 +the 509.5 0.5 +whole 510.0 0.5 +space 510.5 0.5 +between 511.0 0.5 +ceiling 511.5 0.5 +and 512.0 0.5 +floor. 512.5 0.5 +It 513.0 0.5 +had 513.5 0.5 +three 514.0 0.5 +heads. 514.5 0.5 +Three 515.0 0.5 +pairs 515.5 0.5 +of 516.0 0.5 +rolling, 516.5 0.5 +mad 517.0 0.5 +eyes; 517.5 0.5 +three 518.0 0.5 +noses, 518.5 0.5 +twitching 519.0 0.5 +and 519.5 0.5 +quivering 520.0 0.5 +in 520.5 0.5 +their 521.0 0.5 +direction; 521.5 0.5 +three 522.0 0.5 +drooling 522.5 0.5 +mouths, 523.0 0.5 +saliva 523.5 0.5 +hanging 524.0 0.5 +in 524.5 0.5 +slippery 525.0 0.5 +ropes 525.5 0.5 +from 526.0 0.5 +yellowish 526.5 0.5 +fangs. 527.0 0.5 ++ 527.5 0.5 +It 528.0 0.5 +was 528.5 0.5 +standing 529.0 0.5 +quite 529.5 0.5 +still, 530.0 0.5 +all 530.5 0.5 +six 531.0 0.5 +eyes 531.5 0.5 +staring 532.0 0.5 +at 532.5 0.5 +them, 533.0 0.5 +and 533.5 0.5 +Harry 534.0 0.5 +knew 534.5 0.5 +that 535.0 0.5 +the 535.5 0.5 +only 536.0 0.5 +reason 536.5 0.5 +they 537.0 0.5 +weren't 537.5 0.5 +already 538.0 0.5 +dead 538.5 0.5 +was 539.0 0.5 +that 539.5 0.5 +their 540.0 0.5 +sudden 540.5 0.5 +appearance 541.0 0.5 +had 541.5 0.5 +taken 542.0 0.5 +it 542.5 0.5 +by 543.0 0.5 +surprise, 543.5 0.5 +but 544.0 0.5 +it 544.5 0.5 +was 545.0 0.5 +quickly 545.5 0.5 +getting 546.0 0.5 +over 546.5 0.5 +that, 547.0 0.5 +there 547.5 0.5 +was 548.0 0.5 +no 548.5 0.5 +mistaking 549.0 0.5 +what 549.5 0.5 +those 550.0 0.5 +thunderous 550.5 0.5 +growls 551.0 0.5 +meant. 551.5 0.5 ++ 552.0 0.5 +Harry 552.5 0.5 +groped 553.0 0.5 +for 553.5 0.5 +the 554.0 0.5 +doorknob 554.5 0.5 +-- 555.0 0.5 +between 555.5 0.5 +Filch 556.0 0.5 +and 556.5 0.5 +death, 557.0 0.5 +he'd 557.5 0.5 +take 558.0 0.5 +Filch. 558.5 0.5 ++ 559.0 0.5 +They 559.5 0.5 +fell 560.0 0.5 +backward 560.5 0.5 +-- 561.0 0.5 +Harry 561.5 0.5 +slammed 562.0 0.5 +the 562.5 0.5 +door 563.0 0.5 +shut, 563.5 0.5 +and 564.0 0.5 +they 564.5 0.5 +ran, 565.0 0.5 +they 565.5 0.5 +almost 566.0 0.5 +flew, 566.5 0.5 +back 567.0 0.5 +down 567.5 0.5 +the 568.0 0.5 +corridor. 568.5 0.5 +Filch 569.0 0.5 +must 569.5 0.5 +have 570.0 0.5 +hurried 570.5 0.5 +off 571.0 0.5 +to 571.5 0.5 +look 572.0 0.5 +for 572.5 0.5 +them 573.0 0.5 +somewhere 573.5 0.5 +else, 574.0 0.5 +because 574.5 0.5 +they 575.0 0.5 +didn't 575.5 0.5 +see 576.0 0.5 +him 576.5 0.5 +anywhere, 577.0 0.5 +but 577.5 0.5 +they 578.0 0.5 +hardly 578.5 0.5 +cared 579.0 0.5 +-- 579.5 0.5 +all 580.0 0.5 +they 580.5 0.5 +wanted 581.0 0.5 +to 581.5 0.5 +do 582.0 0.5 +was 582.5 0.5 +put 583.0 0.5 +as 583.5 0.5 +much 584.0 0.5 +space 584.5 0.5 +as 585.0 0.5 +possible 585.5 0.5 +between 586.0 0.5 +them 586.5 0.5 +and 587.0 0.5 +that 587.5 0.5 +monster. 588.0 0.5 +They 588.5 0.5 +didn't 589.0 0.5 +stop 589.5 0.5 +running 590.0 0.5 +until 590.5 0.5 +they 591.0 0.5 +reached 591.5 0.5 +the 592.0 0.5 +portrait 592.5 0.5 +of 593.0 0.5 +the 593.5 0.5 +Fat 594.0 0.5 +Lady 594.5 0.5 +on 595.0 0.5 +the 595.5 0.5 +seventh 596.0 0.5 +floor. 596.5 0.5 ++ 597.0 0.5 +"""Where" 597.5 0.5 +on 598.0 0.5 +earth 598.5 0.5 +have 599.0 0.5 +you 599.5 0.5 +all 600.0 0.5 +"been?""" 600.5 0.5 +she 601.0 0.5 +asked, 601.5 0.5 +looking 602.0 0.5 +at 602.5 0.5 +their 603.0 0.5 +bathrobes 603.5 0.5 +hanging 604.0 0.5 +off 604.5 0.5 +their 605.0 0.5 +shoulders 605.5 0.5 +and 606.0 0.5 +their 606.5 0.5 +flushed, 607.0 0.5 +sweaty 607.5 0.5 +faces. 608.0 0.5 ++ 608.5 0.5 +"""Never" 609.0 0.5 +mind 609.5 0.5 +that 610.0 0.5 +-- 610.5 0.5 +pig 611.0 0.5 +snout, 611.5 0.5 +pig 612.0 0.5 +"snout,""" 612.5 0.5 +panted 613.0 0.5 +Harry, 613.5 0.5 +and 614.0 0.5 +the 614.5 0.5 +portrait 615.0 0.5 +swung 615.5 0.5 +forward. 616.0 0.5 +They 616.5 0.5 +scrambled 617.0 0.5 +into 617.5 0.5 +the 618.0 0.5 +common 618.5 0.5 +room 619.0 0.5 +and 619.5 0.5 +collapsed, 620.0 0.5 +trembling, 620.5 0.5 +into 621.0 0.5 +armchairs. 621.5 0.5 ++ 622.0 0.5 +It 622.5 0.5 +was 623.0 0.5 +a 623.5 0.5 +while 624.0 0.5 +before 624.5 0.5 +any 625.0 0.5 +of 625.5 0.5 +them 626.0 0.5 +said 626.5 0.5 +anything. 627.0 0.5 +Neville, 627.5 0.5 +indeed, 628.0 0.5 +looked 628.5 0.5 +as 629.0 0.5 +if 629.5 0.5 +he'd 630.0 0.5 +never 630.5 0.5 +speak 631.0 0.5 +again. 631.5 0.5 ++ 632.0 0.5 +"""What" 632.5 0.5 +do 633.0 0.5 +they 633.5 0.5 +think 634.0 0.5 +they're 634.5 0.5 +doing, 635.0 0.5 +keeping 635.5 0.5 +a 636.0 0.5 +thing 636.5 0.5 +like 637.0 0.5 +that 637.5 0.5 +locked 638.0 0.5 +up 638.5 0.5 +in 639.0 0.5 +a 639.5 0.5 +"school?""" 640.0 0.5 +said 640.5 0.5 +Ron 641.0 0.5 +finally. 641.5 0.5 +"""If" 642.0 0.5 +any 642.5 0.5 +dog 643.0 0.5 +needs 643.5 0.5 +exercise, 644.0 0.5 +that 644.5 0.5 +one 645.0 0.5 +"does.""" 645.5 0.5 ++ 646.0 0.5 +Hermione 646.5 0.5 +had 647.0 0.5 +got 647.5 0.5 +both 648.0 0.5 +her 648.5 0.5 +breath 649.0 0.5 +and 649.5 0.5 +her 650.0 0.5 +bad 650.5 0.5 +temper 651.0 0.5 +back 651.5 0.5 +again. 652.0 0.5 ++ 652.5 0.5 +"""You" 653.0 0.5 +don't 653.5 0.5 +use 654.0 0.5 +your 654.5 0.5 +eyes, 655.0 0.5 +any 655.5 0.5 +of 656.0 0.5 +you, 656.5 0.5 +do 657.0 0.5 +"you?""" 657.5 0.5 +she 658.0 0.5 +snapped. 658.5 0.5 +"""Didn't" 659.0 0.5 +you 659.5 0.5 +see 660.0 0.5 +what 660.5 0.5 +it 661.0 0.5 +was 661.5 0.5 +standing 662.0 0.5 +"on?""" 662.5 0.5 ++ 663.0 0.5 +"""The" 663.5 0.5 +"floor?""" 664.0 0.5 +Harry 664.5 0.5 +suggested. 665.0 0.5 +"""I" 665.5 0.5 +wasn't 666.0 0.5 +looking 666.5 0.5 +at 667.0 0.5 +its 667.5 0.5 +feet, 668.0 0.5 +I 668.5 0.5 +was 669.0 0.5 +too 669.5 0.5 +busy 670.0 0.5 +with 670.5 0.5 +its 671.0 0.5 +"heads.""" 671.5 0.5 ++ 672.0 0.5 +"""No," 672.5 0.5 +not italic 673.0 0.5 +the 673.5 0.5 +floor. 674.0 0.5 +It 674.5 0.5 +was 675.0 0.5 +standing 675.5 0.5 +on 676.0 0.5 +a 676.5 0.5 +trapdoor. 677.0 0.5 +It's 677.5 0.5 +obviously 678.0 0.5 +guarding 678.5 0.5 +"something.""" 679.0 0.5 ++ 679.5 0.5 +She 680.0 0.5 +stood 680.5 0.5 +up, 681.0 0.5 +glaring 681.5 0.5 +at 682.0 0.5 +them. 682.5 0.5 ++ 683.0 0.5 +"""I" 683.5 0.5 +hope 684.0 0.5 +you're 684.5 0.5 +pleased 685.0 0.5 +with 685.5 0.5 +yourselves. 686.0 0.5 +We 686.5 0.5 +could 687.0 0.5 +all 687.5 0.5 +have 688.0 0.5 +been 688.5 0.5 +killed 689.0 0.5 +-- 689.5 0.5 +or 690.0 0.5 +worse, 690.5 0.5 +expelled. 691.0 0.5 +Now, 691.5 0.5 +if 692.0 0.5 +you 692.5 0.5 +don't 693.0 0.5 +mind, 693.5 0.5 +I'm 694.0 0.5 +going 694.5 0.5 +to 695.0 0.5 +"bed.""" 695.5 0.5 ++ 696.0 0.5 +Ron 696.5 0.5 +stared 697.0 0.5 +after 697.5 0.5 +her, 698.0 0.5 +his 698.5 0.5 +mouth 699.0 0.5 +open. 699.5 0.5 +"""No," 700.0 0.5 +we 700.5 0.5 +don't 701.0 0.5 +"mind,""" 701.5 0.5 +he 702.0 0.5 +said. 702.5 0.5 +"""You'd" 703.0 0.5 +think 703.5 0.5 +we 704.0 0.5 +dragged 704.5 0.5 +her 705.0 0.5 +along, 705.5 0.5 +wouldn't 706.0 0.5 +"you?""" 706.5 0.5 ++ 707.0 0.5 +But 707.5 0.5 +Hermione 708.0 0.5 +had 708.5 0.5 +given 709.0 0.5 +Harry 709.5 0.5 +something 710.0 0.5 +else 710.5 0.5 +to 711.0 0.5 +think 711.5 0.5 +about 712.0 0.5 +as 712.5 0.5 +he 713.0 0.5 +climbed 713.5 0.5 +back 714.0 0.5 +into 714.5 0.5 +bed. 715.0 0.5 +The 715.5 0.5 +dog 716.0 0.5 +was 716.5 0.5 +guarding 717.0 0.5 +something. 717.5 0.5 +. 718.0 0.5 +^. 718.5 0.5 +^^. 719.0 0.5 +What 719.5 0.5 +had 720.0 0.5 +Hagrid 720.5 0.5 +said? 721.0 0.5 +Gringotts 721.5 0.5 +was 722.0 0.5 +the 722.5 0.5 +safest 723.0 0.5 +place 723.5 0.5 +in 724.0 0.5 +the 724.5 0.5 +world 725.0 0.5 +for 725.5 0.5 +something 726.0 0.5 +you 726.5 0.5 +wanted 727.0 0.5 +to 727.5 0.5 +hide 728.0 0.5 +-- 728.5 0.5 +except 729.0 0.5 +perhaps 729.5 0.5 +Hogwarts. 730.0 0.5 ++ 730.5 0.5 +It 731.0 0.5 +looked 731.5 0.5 +as 732.0 0.5 +though 732.5 0.5 +Harry 733.0 0.5 +had 733.5 0.5 +found 734.0 0.5 +out 734.5 0.5 +where 735.0 0.5 +the 735.5 0.5 +grubby 736.0 0.5 +little 736.5 0.5 +package 737.0 0.5 +from 737.5 0.5 +vault 738.0 0.5 +seven 738.5 0.5 +hundred 739.0 0.5 +and 739.5 0.5 +thirteen 740.0 0.5 +was. 740.5 0.5 ++ 741.0 0.5 diff --git a/data/language/harrypotter/task-harry_run-5_events.tsv b/data/language/harrypotter/task-harry_run-5_events.tsv new file mode 100644 index 00000000..670ad322 --- /dev/null +++ b/data/language/harrypotter/task-harry_run-5_events.tsv @@ -0,0 +1,1634 @@ +word format onset duration +Malfoy 10.0 0.5 +couldn't 10.5 0.5 +believe 11.0 0.5 +his 11.5 0.5 +eyes 12.0 0.5 +when 12.5 0.5 +he 13.0 0.5 +saw 13.5 0.5 +that 14.0 0.5 +Harry 14.5 0.5 +and 15.0 0.5 +Ron 15.5 0.5 +were 16.0 0.5 +still 16.5 0.5 +at 17.0 0.5 +Hogwarts 17.5 0.5 +the 18.0 0.5 +next 18.5 0.5 +day, 19.0 0.5 +looking 19.5 0.5 +tired 20.0 0.5 +but 20.5 0.5 +perfectly 21.0 0.5 +cheerful. 21.5 0.5 +Indeed, 22.0 0.5 +by 22.5 0.5 +the 23.0 0.5 +next 23.5 0.5 +morning 24.0 0.5 +Harry 24.5 0.5 +and 25.0 0.5 +Ron 25.5 0.5 +thought 26.0 0.5 +that 26.5 0.5 +meeting 27.0 0.5 +the 27.5 0.5 +three-headed 28.0 0.5 +dog 28.5 0.5 +had 29.0 0.5 +been 29.5 0.5 +an 30.0 0.5 +excellent 30.5 0.5 +adventure, 31.0 0.5 +and 31.5 0.5 +they 32.0 0.5 +were 32.5 0.5 +quite 33.0 0.5 +keen 33.5 0.5 +to 34.0 0.5 +have 34.5 0.5 +another 35.0 0.5 +one. 35.5 0.5 +In 36.0 0.5 +the 36.5 0.5 +meantime, 37.0 0.5 +Harry 37.5 0.5 +filled 38.0 0.5 +Ron 38.5 0.5 +in 39.0 0.5 +about 39.5 0.5 +the 40.0 0.5 +package 40.5 0.5 +that 41.0 0.5 +seemed 41.5 0.5 +to 42.0 0.5 +have 42.5 0.5 +been 43.0 0.5 +moved 43.5 0.5 +from 44.0 0.5 +Gringotts 44.5 0.5 +to 45.0 0.5 +Hogwarts, 45.5 0.5 +and 46.0 0.5 +they 46.5 0.5 +spent 47.0 0.5 +a 47.5 0.5 +lot 48.0 0.5 +of 48.5 0.5 +time 49.0 0.5 +wondering 49.5 0.5 +what 50.0 0.5 +could 50.5 0.5 +possibly 51.0 0.5 +need 51.5 0.5 +such 52.0 0.5 +heavy 52.5 0.5 +protection. 53.0 0.5 ++ 53.5 0.5 +"""It's" 54.0 0.5 +either 54.5 0.5 +really 55.0 0.5 +valuable 55.5 0.5 +or 56.0 0.5 +really 56.5 0.5 +"dangerous,""" 57.0 0.5 +said 57.5 0.5 +Ron. 58.0 0.5 ++ 58.5 0.5 +"""Or" 59.0 0.5 +"both,""" 59.5 0.5 +said 60.0 0.5 +Harry. 60.5 0.5 ++ 61.0 0.5 +But 61.5 0.5 +as 62.0 0.5 +all 62.5 0.5 +they 63.0 0.5 +knew 63.5 0.5 +for 64.0 0.5 +sure 64.5 0.5 +about 65.0 0.5 +the 65.5 0.5 +mysterious 66.0 0.5 +object 66.5 0.5 +was 67.0 0.5 +that 67.5 0.5 +it 68.0 0.5 +was 68.5 0.5 +about 69.0 0.5 +two 69.5 0.5 +inches 70.0 0.5 +long, 70.5 0.5 +they 71.0 0.5 +didn't 71.5 0.5 +have 72.0 0.5 +much 72.5 0.5 +chance 73.0 0.5 +of 73.5 0.5 +guessing 74.0 0.5 +what 74.5 0.5 +it 75.0 0.5 +was 75.5 0.5 +without 76.0 0.5 +further 76.5 0.5 +clues. 77.0 0.5 ++ 77.5 0.5 +Neither 78.0 0.5 +Neville 78.5 0.5 +nor 79.0 0.5 +Hermione 79.5 0.5 +showed 80.0 0.5 +the 80.5 0.5 +slightest 81.0 0.5 +interest 81.5 0.5 +in 82.0 0.5 +what 82.5 0.5 +lay 83.0 0.5 +underneath 83.5 0.5 +the 84.0 0.5 +dog 84.5 0.5 +and 85.0 0.5 +the 85.5 0.5 +trapdoor. 86.0 0.5 +All 86.5 0.5 +Neville 87.0 0.5 +cared 87.5 0.5 +about 88.0 0.5 +was 88.5 0.5 +never 89.0 0.5 +going 89.5 0.5 +near 90.0 0.5 +the 90.5 0.5 +dog 91.0 0.5 +again. 91.5 0.5 ++ 92.0 0.5 +Hermione 92.5 0.5 +was 93.0 0.5 +now 93.5 0.5 +refusing 94.0 0.5 +to 94.5 0.5 +speak 95.0 0.5 +to 95.5 0.5 +Harry 96.0 0.5 +and 96.5 0.5 +Ron, 97.0 0.5 +but 97.5 0.5 +she 98.0 0.5 +was 98.5 0.5 +such 99.0 0.5 +a 99.5 0.5 +bossy 100.0 0.5 +know-it-all 100.5 0.5 +that 101.0 0.5 +they 101.5 0.5 +saw 102.0 0.5 +this 102.5 0.5 +as 103.0 0.5 +an 103.5 0.5 +added 104.0 0.5 +bonus. 104.5 0.5 +All 105.0 0.5 +they 105.5 0.5 +really 106.0 0.5 +wanted 106.5 0.5 +now 107.0 0.5 +was 107.5 0.5 +a 108.0 0.5 +way 108.5 0.5 +of 109.0 0.5 +getting 109.5 0.5 +back 110.0 0.5 +at 110.5 0.5 +Malfoy, 111.0 0.5 +and 111.5 0.5 +to 112.0 0.5 +their 112.5 0.5 +great 113.0 0.5 +delight, 113.5 0.5 +just 114.0 0.5 +such 114.5 0.5 +a 115.0 0.5 +thing 115.5 0.5 +arrived 116.0 0.5 +in 116.5 0.5 +the 117.0 0.5 +mail 117.5 0.5 +about 118.0 0.5 +a 118.5 0.5 +week 119.0 0.5 +later. 119.5 0.5 ++ 120.0 0.5 +As 120.5 0.5 +the 121.0 0.5 +owls 121.5 0.5 +flooded 122.0 0.5 +into 122.5 0.5 +the 123.0 0.5 +Great 123.5 0.5 +Hall 124.0 0.5 +as 124.5 0.5 +usual, 125.0 0.5 +everyone's 125.5 0.5 +attention 126.0 0.5 +was 126.5 0.5 +caught 127.0 0.5 +at 127.5 0.5 +once 128.0 0.5 +by 128.5 0.5 +a 129.0 0.5 +long, 129.5 0.5 +thin 130.0 0.5 +package 130.5 0.5 +carried 131.0 0.5 +by 131.5 0.5 +six 132.0 0.5 +large 132.5 0.5 +screech 133.0 0.5 +owls. 133.5 0.5 +Harry 134.0 0.5 +was 134.5 0.5 +just 135.0 0.5 +as 135.5 0.5 +interested 136.0 0.5 +as 136.5 0.5 +everyone 137.0 0.5 +else 137.5 0.5 +to 138.0 0.5 +see 138.5 0.5 +what 139.0 0.5 +was 139.5 0.5 +in 140.0 0.5 +this 140.5 0.5 +large 141.0 0.5 +parcel, 141.5 0.5 +and 142.0 0.5 +was 142.5 0.5 +amazed 143.0 0.5 +when 143.5 0.5 +the 144.0 0.5 +owls 144.5 0.5 +soared 145.0 0.5 +down 145.5 0.5 +and 146.0 0.5 +dropped 146.5 0.5 +it 147.0 0.5 +right 147.5 0.5 +in 148.0 0.5 +front 148.5 0.5 +of 149.0 0.5 +him, 149.5 0.5 +knocking 150.0 0.5 +his 150.5 0.5 +bacon 151.0 0.5 +to 151.5 0.5 +the 152.0 0.5 +floor. 152.5 0.5 +They 153.0 0.5 +had 153.5 0.5 +hardly 154.0 0.5 +fluttered 154.5 0.5 +out 155.0 0.5 +of 155.5 0.5 +the 156.0 0.5 +way 156.5 0.5 +when 157.0 0.5 +another 157.5 0.5 +owl 158.0 0.5 +dropped 158.5 0.5 +a 159.0 0.5 +letter 159.5 0.5 +on 160.0 0.5 +top 160.5 0.5 +of 161.0 0.5 +the 161.5 0.5 +parcel. 162.0 0.5 ++ 162.5 0.5 +Harry 163.0 0.5 +ripped 163.5 0.5 +open 164.0 0.5 +the 164.5 0.5 +letter 165.0 0.5 +first, 165.5 0.5 +which 166.0 0.5 +was 166.5 0.5 +lucky, 167.0 0.5 +because 167.5 0.5 +it 168.0 0.5 +said: 168.5 0.5 ++ 169.0 0.5 +DO 169.5 0.5 +NOT 170.0 0.5 +OPEN 170.5 0.5 +THE 171.0 0.5 +PARCEL 171.5 0.5 +AT 172.0 0.5 +THE 172.5 0.5 +TABLE. 173.0 0.5 +It 173.5 0.5 +contains 174.0 0.5 +your 174.5 0.5 +new 175.0 0.5 +Nimbus 175.5 0.5 +Two 176.0 0.5 +Thousand, 176.5 0.5 +but 177.0 0.5 +I 177.5 0.5 +don't 178.0 0.5 +want 178.5 0.5 +everybody 179.0 0.5 +knowing 179.5 0.5 +you've 180.0 0.5 +got 180.5 0.5 +a 181.0 0.5 +broomstick 181.5 0.5 +or 182.0 0.5 +they'll 182.5 0.5 +all 183.0 0.5 +want 183.5 0.5 +one. 184.0 0.5 +Oliver 184.5 0.5 +Wood 185.0 0.5 +will 185.5 0.5 +meet 186.0 0.5 +you 186.5 0.5 +tonight 187.0 0.5 +on 187.5 0.5 +the 188.0 0.5 +Quidditch 188.5 0.5 +field 189.0 0.5 +at 189.5 0.5 +seven 190.0 0.5 +o'clock 190.5 0.5 +for 191.0 0.5 +your 191.5 0.5 +first 192.0 0.5 +training 192.5 0.5 +session. 193.0 0.5 +Professor italic 193.5 0.5 +McGonagall italic 194.0 0.5 ++ 194.5 0.5 +Harry 195.0 0.5 +had 195.5 0.5 +difficulty 196.0 0.5 +hiding 196.5 0.5 +his 197.0 0.5 +glee 197.5 0.5 +as 198.0 0.5 +he 198.5 0.5 +handed 199.0 0.5 +the 199.5 0.5 +note 200.0 0.5 +to 200.5 0.5 +Ron 201.0 0.5 +to 201.5 0.5 +read. 202.0 0.5 ++ 202.5 0.5 +"""A" 203.0 0.5 +Nimbus 203.5 0.5 +Two 204.0 0.5 +"Thousand!""" 204.5 0.5 +Ron 205.0 0.5 +moaned 205.5 0.5 +enviously. 206.0 0.5 +"""I've" 206.5 0.5 +never 207.0 0.5 +even 207.5 0.5 +touched 208.0 0.5 +"one.""" 208.5 0.5 ++ 209.0 0.5 +They 209.5 0.5 +left 210.0 0.5 +the 210.5 0.5 +hall 211.0 0.5 +quickly, 211.5 0.5 +wanting 212.0 0.5 +to 212.5 0.5 +unwrap 213.0 0.5 +the 213.5 0.5 +broomstick 214.0 0.5 +in 214.5 0.5 +private 215.0 0.5 +before 215.5 0.5 +their 216.0 0.5 +first 216.5 0.5 +class, 217.0 0.5 +but 217.5 0.5 +halfway 218.0 0.5 +across 218.5 0.5 +the 219.0 0.5 +entrance 219.5 0.5 +hall 220.0 0.5 +they 220.5 0.5 +found 221.0 0.5 +the 221.5 0.5 +way 222.0 0.5 +upstairs 222.5 0.5 +barred 223.0 0.5 +by 223.5 0.5 +Crabbe 224.0 0.5 +and 224.5 0.5 +Goyle. 225.0 0.5 +Malfoy 225.5 0.5 +seized 226.0 0.5 +the 226.5 0.5 +package 227.0 0.5 +from 227.5 0.5 +Harry 228.0 0.5 +and 228.5 0.5 +felt 229.0 0.5 +it. 229.5 0.5 ++ 230.0 0.5 +"""That's" 230.5 0.5 +a 231.0 0.5 +"broomstick,""" 231.5 0.5 +he 232.0 0.5 +said, 232.5 0.5 +throwing 233.0 0.5 +it 233.5 0.5 +back 234.0 0.5 +to 234.5 0.5 +Harry 235.0 0.5 +with 235.5 0.5 +a 236.0 0.5 +mixture 236.5 0.5 +of 237.0 0.5 +jealousy 237.5 0.5 +and 238.0 0.5 +spite 238.5 0.5 +on 239.0 0.5 +his 239.5 0.5 +face. 240.0 0.5 +"""You'll" 240.5 0.5 +be 241.0 0.5 +in 241.5 0.5 +for 242.0 0.5 +it 242.5 0.5 +this 243.0 0.5 +time, 243.5 0.5 +Potter, 244.0 0.5 +first 244.5 0.5 +years 245.0 0.5 +aren't 245.5 0.5 +allowed 246.0 0.5 +"them.""" 246.5 0.5 ++ 247.0 0.5 +Ron 247.5 0.5 +couldn't 248.0 0.5 +resist 248.5 0.5 +it. 249.0 0.5 ++ 249.5 0.5 +"""It's" 250.0 0.5 +not 250.5 0.5 +any 251.0 0.5 +old 251.5 0.5 +"broomstick,""" 252.0 0.5 +he 252.5 0.5 +said, 253.0 0.5 +"""it's" 253.5 0.5 +a 254.0 0.5 +Nimbus 254.5 0.5 +Two 255.0 0.5 +Thousand. 255.5 0.5 +What 256.0 0.5 +did 256.5 0.5 +you 257.0 0.5 +say 257.5 0.5 +you've 258.0 0.5 +got 258.5 0.5 +at 259.0 0.5 +home, 259.5 0.5 +Malfoy, 260.0 0.5 +a 260.5 0.5 +Comet 261.0 0.5 +Two 261.5 0.5 +"Sixty?""" 262.0 0.5 +Ron 262.5 0.5 +grinned 263.0 0.5 +at 263.5 0.5 +Harry. 264.0 0.5 +"""Comets" 264.5 0.5 +look 265.0 0.5 +flashy, 265.5 0.5 +but 266.0 0.5 +they're 266.5 0.5 +not 267.0 0.5 +in 267.5 0.5 +the 268.0 0.5 +same 268.5 0.5 +league 269.0 0.5 +as 269.5 0.5 +the 270.0 0.5 +"Nimbus.""" 270.5 0.5 ++ 271.0 0.5 +"""What" 271.5 0.5 +would 272.0 0.5 +you 272.5 0.5 +know 273.0 0.5 +about 273.5 0.5 +it, 274.0 0.5 +Weasley, 274.5 0.5 +you 275.0 0.5 +couldn't 275.5 0.5 +afford 276.0 0.5 +half 276.5 0.5 +the 277.0 0.5 +"handle,""" 277.5 0.5 +Malfoy 278.0 0.5 +snapped 278.5 0.5 +back. 279.0 0.5 +"""I" 279.5 0.5 +suppose 280.0 0.5 +you 280.5 0.5 +and 281.0 0.5 +your 281.5 0.5 +brothers 282.0 0.5 +have 282.5 0.5 +to 283.0 0.5 +save 283.5 0.5 +up 284.0 0.5 +twig 284.5 0.5 +by 285.0 0.5 +"twig.""" 285.5 0.5 ++ 286.0 0.5 +Before 286.5 0.5 +Ron 287.0 0.5 +could 287.5 0.5 +answer, 288.0 0.5 +Professor 288.5 0.5 +Flitwick 289.0 0.5 +appeared 289.5 0.5 +at 290.0 0.5 +Malfoy's 290.5 0.5 +elbow. 291.0 0.5 ++ 291.5 0.5 +"""Not" 292.0 0.5 +arguing, 292.5 0.5 +I 293.0 0.5 +hope, 293.5 0.5 +"boys?""" 294.0 0.5 +he 294.5 0.5 +squeaked. 295.0 0.5 ++ 295.5 0.5 +"""Potter's" 296.0 0.5 +been 296.5 0.5 +sent 297.0 0.5 +a 297.5 0.5 +broomstick, 298.0 0.5 +"Professor,""" 298.5 0.5 +said 299.0 0.5 +Malfoy 299.5 0.5 +quickly. 300.0 0.5 ++ 300.5 0.5 +"""Yes," 301.0 0.5 +yes, 301.5 0.5 +that's 302.0 0.5 +"right,""" 302.5 0.5 +said 303.0 0.5 +Professor 303.5 0.5 +Flitwick, 304.0 0.5 +beaming 304.5 0.5 +at 305.0 0.5 +Harry. 305.5 0.5 +"""Professor" 306.0 0.5 +McGonagall 306.5 0.5 +told 307.0 0.5 +me 307.5 0.5 +all 308.0 0.5 +about 308.5 0.5 +the 309.0 0.5 +special 309.5 0.5 +circumstances, 310.0 0.5 +Potter. 310.5 0.5 +And 311.0 0.5 +what 311.5 0.5 +model 312.0 0.5 +is 312.5 0.5 +"it?""" 313.0 0.5 ++ 313.5 0.5 +"""A" 314.0 0.5 +Nimbus 314.5 0.5 +Two 315.0 0.5 +Thousand, 315.5 0.5 +"sit,""" 316.0 0.5 +said 316.5 0.5 +Harry, 317.0 0.5 +fighting 317.5 0.5 +not 318.0 0.5 +to 318.5 0.5 +laugh 319.0 0.5 +at 319.5 0.5 +the 320.0 0.5 +look 320.5 0.5 +of 321.0 0.5 +horror 321.5 0.5 +on 322.0 0.5 +Malfoy's 322.5 0.5 +face. 323.0 0.5 +"""And" 323.5 0.5 +it's 324.0 0.5 +really 324.5 0.5 +thanks 325.0 0.5 +to 325.5 0.5 +Malfoy 326.0 0.5 +here 326.5 0.5 +that 327.0 0.5 +I've 327.5 0.5 +got 328.0 0.5 +"it,""" 328.5 0.5 +he 329.0 0.5 +added. 329.5 0.5 ++ 330.0 0.5 +Harry 330.5 0.5 +and 331.0 0.5 +Ron 331.5 0.5 +headed 332.0 0.5 +upstairs, 332.5 0.5 +smothering 333.0 0.5 +their 333.5 0.5 +laughter 334.0 0.5 +at 334.5 0.5 +Malfoy's 335.0 0.5 +obvious 335.5 0.5 +rage 336.0 0.5 +and 336.5 0.5 +confusion. 337.0 0.5 ++ 337.5 0.5 +"""Well," 338.0 0.5 +it's 338.5 0.5 +"true,""" 339.0 0.5 +Harry 339.5 0.5 +chortled 340.0 0.5 +as 340.5 0.5 +they 341.0 0.5 +reached 341.5 0.5 +the 342.0 0.5 +top 342.5 0.5 +of 343.0 0.5 +the 343.5 0.5 +marble 344.0 0.5 +staircase, 344.5 0.5 +"""If" 345.0 0.5 +he 345.5 0.5 +hadn't 346.0 0.5 +stolen 346.5 0.5 +Neville's 347.0 0.5 +Remembrall 347.5 0.5 +I 348.0 0.5 +wouldn't 348.5 0.5 +be 349.0 0.5 +on 349.5 0.5 +the 350.0 0.5 +"team....""" 350.5 0.5 ++ 351.0 0.5 +"""So" 351.5 0.5 +I 352.0 0.5 +suppose 352.5 0.5 +you 353.0 0.5 +think 353.5 0.5 +that's 354.0 0.5 +a 354.5 0.5 +reward 355.0 0.5 +for 355.5 0.5 +breaking 356.0 0.5 +"rules?""" 356.5 0.5 +came 357.0 0.5 +an 357.5 0.5 +angry 358.0 0.5 +voice 358.5 0.5 +from 359.0 0.5 +just 359.5 0.5 +behind 360.0 0.5 +them. 360.5 0.5 +Hermione 361.0 0.5 +was 361.5 0.5 +stomping 362.0 0.5 +up 362.5 0.5 +the 363.0 0.5 +stairs, 363.5 0.5 +looking 364.0 0.5 +disapprovingly 364.5 0.5 +at 365.0 0.5 +the 365.5 0.5 +package 366.0 0.5 +in 366.5 0.5 +Harry's 367.0 0.5 +hand. 367.5 0.5 ++ 368.0 0.5 +"""I" 368.5 0.5 +thought 369.0 0.5 +you 369.5 0.5 +weren't 370.0 0.5 +speaking 370.5 0.5 +to 371.0 0.5 +"us?""" 371.5 0.5 +said 372.0 0.5 +Harry. 372.5 0.5 ++ 373.0 0.5 +"""Yes," 373.5 0.5 +don't 374.0 0.5 +stop 374.5 0.5 +"now,""" 375.0 0.5 +said 375.5 0.5 +Ron, 376.0 0.5 +"""it's" 376.5 0.5 +doing 377.0 0.5 +us 377.5 0.5 +so 378.0 0.5 +much 378.5 0.5 +"good.""" 379.0 0.5 ++ 379.5 0.5 +Hermione 380.0 0.5 +marched 380.5 0.5 +away 381.0 0.5 +with 381.5 0.5 +her 382.0 0.5 +nose 382.5 0.5 +in 383.0 0.5 +the 383.5 0.5 +air. 384.0 0.5 ++ 384.5 0.5 +Harry 385.0 0.5 +had 385.5 0.5 +a 386.0 0.5 +lot 386.5 0.5 +of 387.0 0.5 +trouble 387.5 0.5 +keeping 388.0 0.5 +his 388.5 0.5 +mind 389.0 0.5 +on 389.5 0.5 +his 390.0 0.5 +lessons 390.5 0.5 +that 391.0 0.5 +day. 391.5 0.5 +It 392.0 0.5 +kept 392.5 0.5 +wandering 393.0 0.5 +up 393.5 0.5 +to 394.0 0.5 +the 394.5 0.5 +dormitory 395.0 0.5 +where 395.5 0.5 +his 396.0 0.5 +new 396.5 0.5 +broomstick 397.0 0.5 +was 397.5 0.5 +lying 398.0 0.5 +under 398.5 0.5 +his 399.0 0.5 +bed, 399.5 0.5 +or 400.0 0.5 +straying 400.5 0.5 +off 401.0 0.5 +to 401.5 0.5 +the 402.0 0.5 +Quidditch 402.5 0.5 +field 403.0 0.5 +where 403.5 0.5 +he'd 404.0 0.5 +be 404.5 0.5 +learning 405.0 0.5 +to 405.5 0.5 +play 406.0 0.5 +that 406.5 0.5 +night. 407.0 0.5 +He 407.5 0.5 +bolted 408.0 0.5 +his 408.5 0.5 +dinner 409.0 0.5 +that 409.5 0.5 +evening 410.0 0.5 +without 410.5 0.5 +noticing 411.0 0.5 +what 411.5 0.5 +he 412.0 0.5 +was 412.5 0.5 +eating, 413.0 0.5 +and 413.5 0.5 +then 414.0 0.5 +rushed 414.5 0.5 +upstairs 415.0 0.5 +with 415.5 0.5 +Ron 416.0 0.5 +to 416.5 0.5 +unwrap 417.0 0.5 +the 417.5 0.5 +Nimbus 418.0 0.5 +Two 418.5 0.5 +Thousand 419.0 0.5 +at 419.5 0.5 +last. 420.0 0.5 ++ 420.5 0.5 +"""Wow,""" 421.0 0.5 +Ron 421.5 0.5 +sighed, 422.0 0.5 +as 422.5 0.5 +the 423.0 0.5 +broomstick 423.5 0.5 +rolled 424.0 0.5 +onto 424.5 0.5 +Harry's 425.0 0.5 +bedspread. 425.5 0.5 ++ 426.0 0.5 +Even 426.5 0.5 +Harry, 427.0 0.5 +who 427.5 0.5 +knew 428.0 0.5 +nothing 428.5 0.5 +about 429.0 0.5 +the 429.5 0.5 +different 430.0 0.5 +brooms, 430.5 0.5 +thought 431.0 0.5 +it 431.5 0.5 +looked 432.0 0.5 +wonderful. 432.5 0.5 +Sleek 433.0 0.5 +and 433.5 0.5 +shiny, 434.0 0.5 +with 434.5 0.5 +a 435.0 0.5 +mahogany 435.5 0.5 +handle, 436.0 0.5 +it 436.5 0.5 +had 437.0 0.5 +a 437.5 0.5 +long 438.0 0.5 +tail 438.5 0.5 +of 439.0 0.5 +neat, 439.5 0.5 +straight 440.0 0.5 +twigs 440.5 0.5 +and 441.0 0.5 +Nimbus 441.5 0.5 +Two 442.0 0.5 +Thousand 442.5 0.5 +written 443.0 0.5 +in 443.5 0.5 +gold 444.0 0.5 +near 444.5 0.5 +the 445.0 0.5 +top. 445.5 0.5 ++ 446.0 0.5 +As 446.5 0.5 +seven 447.0 0.5 +o'clock 447.5 0.5 +drew 448.0 0.5 +nearer, 448.5 0.5 +Harry 449.0 0.5 +left 449.5 0.5 +the 450.0 0.5 +castle 450.5 0.5 +and 451.0 0.5 +set 451.5 0.5 +off 452.0 0.5 +in 452.5 0.5 +the 453.0 0.5 +dusk 453.5 0.5 +toward 454.0 0.5 +the 454.5 0.5 +Quidditch 455.0 0.5 +field. 455.5 0.5 +Held 456.0 0.5 +never 456.5 0.5 +been 457.0 0.5 +inside 457.5 0.5 +the 458.0 0.5 +stadium 458.5 0.5 +before. 459.0 0.5 +Hundreds 459.5 0.5 +of 460.0 0.5 +seats 460.5 0.5 +were 461.0 0.5 +raised 461.5 0.5 +in 462.0 0.5 +stands 462.5 0.5 +around 463.0 0.5 +the 463.5 0.5 +field 464.0 0.5 +so 464.5 0.5 +that 465.0 0.5 +the 465.5 0.5 +spectators 466.0 0.5 +were 466.5 0.5 +high 467.0 0.5 +enough 467.5 0.5 +to 468.0 0.5 +see 468.5 0.5 +what 469.0 0.5 +was 469.5 0.5 +going 470.0 0.5 +on. 470.5 0.5 +At 471.0 0.5 +either 471.5 0.5 +end 472.0 0.5 +of 472.5 0.5 +the 473.0 0.5 +field 473.5 0.5 +were 474.0 0.5 +three 474.5 0.5 +golden 475.0 0.5 +poles 475.5 0.5 +with 476.0 0.5 +hoops 476.5 0.5 +on 477.0 0.5 +the 477.5 0.5 +end. 478.0 0.5 +They 478.5 0.5 +reminded 479.0 0.5 +Harry 479.5 0.5 +of 480.0 0.5 +the 480.5 0.5 +little 481.0 0.5 +plastic 481.5 0.5 +sticks 482.0 0.5 +Muggle 482.5 0.5 +children 483.0 0.5 +blew 483.5 0.5 +bubbles 484.0 0.5 +through, 484.5 0.5 +except 485.0 0.5 +that 485.5 0.5 +they 486.0 0.5 +were 486.5 0.5 +fifty 487.0 0.5 +feet 487.5 0.5 +high. 488.0 0.5 ++ 488.5 0.5 +Too 489.0 0.5 +eager 489.5 0.5 +to 490.0 0.5 +fly 490.5 0.5 +again 491.0 0.5 +to 491.5 0.5 +wait 492.0 0.5 +for 492.5 0.5 +Wood, 493.0 0.5 +Harry 493.5 0.5 +mounted 494.0 0.5 +his 494.5 0.5 +broomstick 495.0 0.5 +and 495.5 0.5 +kicked 496.0 0.5 +off 496.5 0.5 +from 497.0 0.5 +the 497.5 0.5 +ground. 498.0 0.5 +What 498.5 0.5 +a 499.0 0.5 +feeling 499.5 0.5 +-- 500.0 0.5 +he 500.5 0.5 +swooped 501.0 0.5 +in 501.5 0.5 +and 502.0 0.5 +out 502.5 0.5 +of 503.0 0.5 +the 503.5 0.5 +goal 504.0 0.5 +posts 504.5 0.5 +and 505.0 0.5 +then 505.5 0.5 +sped 506.0 0.5 +up 506.5 0.5 +and 507.0 0.5 +down 507.5 0.5 +the 508.0 0.5 +field. 508.5 0.5 +The 509.0 0.5 +Nimbus 509.5 0.5 +Two 510.0 0.5 +Thousand 510.5 0.5 +turned 511.0 0.5 +wherever 511.5 0.5 +he 512.0 0.5 +wanted 512.5 0.5 +at 513.0 0.5 +his 513.5 0.5 +lightest 514.0 0.5 +touch. 514.5 0.5 ++ 515.0 0.5 +"""Hey," 515.5 0.5 +Potter, 516.0 0.5 +come 516.5 0.5 +"down!""" 517.0 0.5 ++ 517.5 0.5 +Oliver 518.0 0.5 +Wood 518.5 0.5 +had 519.0 0.5 +arrived. 519.5 0.5 +He 520.0 0.5 +was 520.5 0.5 +carrying 521.0 0.5 +a 521.5 0.5 +large 522.0 0.5 +wooden 522.5 0.5 +crate 523.0 0.5 +under 523.5 0.5 +his 524.0 0.5 +arm. 524.5 0.5 +Harry 525.0 0.5 +landed 525.5 0.5 +next 526.0 0.5 +to 526.5 0.5 +him. 527.0 0.5 ++ 527.5 0.5 +"""Very" 528.0 0.5 +"nice,""" 528.5 0.5 +said 529.0 0.5 +Wood, 529.5 0.5 +his 530.0 0.5 +eyes 530.5 0.5 +glinting. 531.0 0.5 +"""I" 531.5 0.5 +see 532.0 0.5 +what 532.5 0.5 +McGonagall 533.0 0.5 +meant... 533.5 0.5 +you 534.0 0.5 +really 534.5 0.5 +are 535.0 0.5 +a 535.5 0.5 +natural. 536.0 0.5 +I'm 536.5 0.5 +just 537.0 0.5 +going 537.5 0.5 +to 538.0 0.5 +teach 538.5 0.5 +you 539.0 0.5 +the 539.5 0.5 +rules 540.0 0.5 +this 540.5 0.5 +evening, 541.0 0.5 +then 541.5 0.5 +you'll 542.0 0.5 +be 542.5 0.5 +joining 543.0 0.5 +team 543.5 0.5 +practice 544.0 0.5 +three 544.5 0.5 +times 545.0 0.5 +a 545.5 0.5 +"week.""" 546.0 0.5 ++ 546.5 0.5 +He 547.0 0.5 +opened 547.5 0.5 +the 548.0 0.5 +crate. 548.5 0.5 +Inside 549.0 0.5 +were 549.5 0.5 +four 550.0 0.5 +different-sized 550.5 0.5 +balls. 551.0 0.5 ++ 551.5 0.5 +"""Right,""" 552.0 0.5 +said 552.5 0.5 +Wood. 553.0 0.5 +"""Now," 553.5 0.5 +Quidditch 554.0 0.5 +is 554.5 0.5 +easy 555.0 0.5 +enough 555.5 0.5 +to 556.0 0.5 +understand, 556.5 0.5 +even 557.0 0.5 +if 557.5 0.5 +it's 558.0 0.5 +not 558.5 0.5 +too 559.0 0.5 +easy 559.5 0.5 +to 560.0 0.5 +play. 560.5 0.5 +There 561.0 0.5 +are 561.5 0.5 +seven 562.0 0.5 +players 562.5 0.5 +on 563.0 0.5 +each 563.5 0.5 +side. 564.0 0.5 +Three 564.5 0.5 +of 565.0 0.5 +them 565.5 0.5 +are 566.0 0.5 +called 566.5 0.5 +"Chasers.""" 567.0 0.5 ++ 567.5 0.5 +"""Three" 568.0 0.5 +"Chasers,""" 568.5 0.5 +Harry 569.0 0.5 +repeated, 569.5 0.5 +as 570.0 0.5 +Wood 570.5 0.5 +took 571.0 0.5 +out 571.5 0.5 +a 572.0 0.5 +bright 572.5 0.5 +red 573.0 0.5 +ball 573.5 0.5 +about 574.0 0.5 +the 574.5 0.5 +size 575.0 0.5 +of 575.5 0.5 +a 576.0 0.5 +soccer 576.5 0.5 +ball. 577.0 0.5 ++ 577.5 0.5 +"""This" 578.0 0.5 +ball's 578.5 0.5 +called 579.0 0.5 +the 579.5 0.5 +"Quaffle,""" 580.0 0.5 +said 580.5 0.5 +Wood. 581.0 0.5 +"""The" 581.5 0.5 +Chasers 582.0 0.5 +throw 582.5 0.5 +the 583.0 0.5 +Quaffle 583.5 0.5 +to 584.0 0.5 +each 584.5 0.5 +other 585.0 0.5 +and 585.5 0.5 +try 586.0 0.5 +and 586.5 0.5 +get 587.0 0.5 +it 587.5 0.5 +through 588.0 0.5 +one 588.5 0.5 +of 589.0 0.5 +the 589.5 0.5 +hoops 590.0 0.5 +to 590.5 0.5 +score 591.0 0.5 +a 591.5 0.5 +goal. 592.0 0.5 +Ten 592.5 0.5 +points 593.0 0.5 +every 593.5 0.5 +time 594.0 0.5 +the 594.5 0.5 +Quaffle 595.0 0.5 +goes 595.5 0.5 +through 596.0 0.5 +one 596.5 0.5 +of 597.0 0.5 +the 597.5 0.5 +hoops. 598.0 0.5 +Follow 598.5 0.5 +"me?""" 599.0 0.5 ++ 599.5 0.5 +"""The" 600.0 0.5 +Chasers 600.5 0.5 +throw 601.0 0.5 +the 601.5 0.5 +Quaffle 602.0 0.5 +and 602.5 0.5 +put 603.0 0.5 +it 603.5 0.5 +through 604.0 0.5 +the 604.5 0.5 +hoops 605.0 0.5 +to 605.5 0.5 +"score,""" 606.0 0.5 +Harry 606.5 0.5 +recited. 607.0 0.5 +"""So" 607.5 0.5 +-- 608.0 0.5 +that's 608.5 0.5 +sort 609.0 0.5 +of 609.5 0.5 +like 610.0 0.5 +basketball 610.5 0.5 +on 611.0 0.5 +broomsticks 611.5 0.5 +with 612.0 0.5 +six 612.5 0.5 +hoops, 613.0 0.5 +isn't 613.5 0.5 +"it?""" 614.0 0.5 ++ 614.5 0.5 +"""What's" 615.0 0.5 +"basketball?""" 615.5 0.5 +said 616.0 0.5 +Wood 616.5 0.5 +curiously. 617.0 0.5 ++ 617.5 0.5 +"""Never" 618.0 0.5 +"mind,""" 618.5 0.5 +said 619.0 0.5 +Harry 619.5 0.5 +quickly. 620.0 0.5 ++ 620.5 0.5 +"""Now," 621.0 0.5 +there's 621.5 0.5 +another 622.0 0.5 +player 622.5 0.5 +on 623.0 0.5 +each 623.5 0.5 +side 624.0 0.5 +who's 624.5 0.5 +called 625.0 0.5 +the 625.5 0.5 +Keeper 626.0 0.5 +-I'm 626.5 0.5 +Keeper 627.0 0.5 +for 627.5 0.5 +Gryffindor. 628.0 0.5 +I 628.5 0.5 +have 629.0 0.5 +to 629.5 0.5 +fly 630.0 0.5 +around 630.5 0.5 +our 631.0 0.5 +hoops 631.5 0.5 +and 632.0 0.5 +stop 632.5 0.5 +the 633.0 0.5 +other 633.5 0.5 +team 634.0 0.5 +from 634.5 0.5 +"scoring.""" 635.0 0.5 ++ 635.5 0.5 +"""Three" 636.0 0.5 +Chasers, 636.5 0.5 +one 637.0 0.5 +"Keeper,""" 637.5 0.5 +said 638.0 0.5 +Harry, 638.5 0.5 +who 639.0 0.5 +was 639.5 0.5 +determined 640.0 0.5 +to 640.5 0.5 +remember 641.0 0.5 +it 641.5 0.5 +all. 642.0 0.5 +"""And" 642.5 0.5 +they 643.0 0.5 +play 643.5 0.5 +with 644.0 0.5 +the 644.5 0.5 +Quaffle. 645.0 0.5 +Okay, 645.5 0.5 +got 646.0 0.5 +that. 646.5 0.5 +So 647.0 0.5 +what 647.5 0.5 +are 648.0 0.5 +they 648.5 0.5 +"for?""" 649.0 0.5 +He 649.5 0.5 +pointed 650.0 0.5 +at 650.5 0.5 +the 651.0 0.5 +three 651.5 0.5 +balls 652.0 0.5 +left 652.5 0.5 +inside 653.0 0.5 +the 653.5 0.5 +box. 654.0 0.5 ++ 654.5 0.5 +"""I'll" 655.0 0.5 +show 655.5 0.5 +you 656.0 0.5 +"now,""" 656.5 0.5 +said 657.0 0.5 +Wood. 657.5 0.5 +"""Take" 658.0 0.5 +"this.""" 658.5 0.5 ++ 659.0 0.5 +He 659.5 0.5 +handed 660.0 0.5 +Harry 660.5 0.5 +a 661.0 0.5 +small 661.5 0.5 +club, 662.0 0.5 +a 662.5 0.5 +bit 663.0 0.5 +like 663.5 0.5 +a 664.0 0.5 +short 664.5 0.5 +baseball 665.0 0.5 +bat. 665.5 0.5 ++ 666.0 0.5 +"""I'm" 666.5 0.5 +going 667.0 0.5 +to 667.5 0.5 +show 668.0 0.5 +you 668.5 0.5 +what 669.0 0.5 +the 669.5 0.5 +Bludgers 670.0 0.5 +"do,""" 670.5 0.5 +Wood 671.0 0.5 +said. 671.5 0.5 +"""These" 672.0 0.5 +two 672.5 0.5 +are 673.0 0.5 +the 673.5 0.5 +"Bludgers.""" 674.0 0.5 ++ 674.5 0.5 +He 675.0 0.5 +showed 675.5 0.5 +Harry 676.0 0.5 +two 676.5 0.5 +identical 677.0 0.5 +balls, 677.5 0.5 +jet 678.0 0.5 +black 678.5 0.5 +and 679.0 0.5 +slightly 679.5 0.5 +smaller 680.0 0.5 +than 680.5 0.5 +the 681.0 0.5 +red 681.5 0.5 +Quaffle. 682.0 0.5 +Harry 682.5 0.5 +noticed 683.0 0.5 +that 683.5 0.5 +they 684.0 0.5 +seemed 684.5 0.5 +to 685.0 0.5 +be 685.5 0.5 +straining 686.0 0.5 +to 686.5 0.5 +escape 687.0 0.5 +the 687.5 0.5 +straps 688.0 0.5 +holding 688.5 0.5 +them 689.0 0.5 +inside 689.5 0.5 +the 690.0 0.5 +box. 690.5 0.5 ++ 691.0 0.5 +"""Stand" 691.5 0.5 +"back,""" 692.0 0.5 +Wood 692.5 0.5 +warned 693.0 0.5 +Harry. 693.5 0.5 +He 694.0 0.5 +bent 694.5 0.5 +down 695.0 0.5 +and 695.5 0.5 +freed 696.0 0.5 +one 696.5 0.5 +of 697.0 0.5 +the 697.5 0.5 +Bludgers. 698.0 0.5 ++ 698.5 0.5 +At 699.0 0.5 +once, 699.5 0.5 +the 700.0 0.5 +black 700.5 0.5 +ball 701.0 0.5 +rose 701.5 0.5 +high 702.0 0.5 +in 702.5 0.5 +the 703.0 0.5 +air 703.5 0.5 +and 704.0 0.5 +then 704.5 0.5 +pelted 705.0 0.5 +straight 705.5 0.5 +at 706.0 0.5 +Harry's 706.5 0.5 +face. 707.0 0.5 +Harry 707.5 0.5 +swung 708.0 0.5 +at 708.5 0.5 +it 709.0 0.5 +with 709.5 0.5 +the 710.0 0.5 +bat 710.5 0.5 +to 711.0 0.5 +stop 711.5 0.5 +it 712.0 0.5 +from 712.5 0.5 +breaking 713.0 0.5 +his 713.5 0.5 +nose, 714.0 0.5 +and 714.5 0.5 +sent 715.0 0.5 +it 715.5 0.5 +zigzagging 716.0 0.5 +away 716.5 0.5 +into 717.0 0.5 +the 717.5 0.5 +air 718.0 0.5 +-- 718.5 0.5 +it 719.0 0.5 +zoomed 719.5 0.5 +around 720.0 0.5 +their 720.5 0.5 +heads 721.0 0.5 +and 721.5 0.5 +then 722.0 0.5 +shot 722.5 0.5 +at 723.0 0.5 +Wood, 723.5 0.5 +who 724.0 0.5 +dived 724.5 0.5 +on 725.0 0.5 +top 725.5 0.5 +of 726.0 0.5 +it 726.5 0.5 +and 727.0 0.5 +managed 727.5 0.5 +to 728.0 0.5 +pin 728.5 0.5 +it 729.0 0.5 +to 729.5 0.5 +the 730.0 0.5 +ground. 730.5 0.5 ++ 731.0 0.5 +"""See?""" 731.5 0.5 +Wood 732.0 0.5 +panted, 732.5 0.5 +forcing 733.0 0.5 +the 733.5 0.5 +struggling 734.0 0.5 +Bludger 734.5 0.5 +back 735.0 0.5 +into 735.5 0.5 +the 736.0 0.5 +crate 736.5 0.5 +and 737.0 0.5 +strapping 737.5 0.5 +it 738.0 0.5 +down 738.5 0.5 +safely. 739.0 0.5 +"""The" 739.5 0.5 +Bludgers 740.0 0.5 +rocket 740.5 0.5 +around, 741.0 0.5 +trying 741.5 0.5 +to 742.0 0.5 +knock 742.5 0.5 +players 743.0 0.5 +off 743.5 0.5 +their 744.0 0.5 +brooms. 744.5 0.5 +That's 745.0 0.5 +why 745.5 0.5 +you 746.0 0.5 +have 746.5 0.5 +two 747.0 0.5 +Beaters 747.5 0.5 +on 748.0 0.5 +each 748.5 0.5 +team 749.0 0.5 +-- 749.5 0.5 +the 750.0 0.5 +Weasley 750.5 0.5 +twins 751.0 0.5 +are 751.5 0.5 +ours 752.0 0.5 +-- 752.5 0.5 +it's 753.0 0.5 +their 753.5 0.5 +job 754.0 0.5 +to 754.5 0.5 +protect 755.0 0.5 +their 755.5 0.5 +side 756.0 0.5 +from 756.5 0.5 +the 757.0 0.5 +Bludgers 757.5 0.5 +and 758.0 0.5 +try 758.5 0.5 +and 759.0 0.5 +knock 759.5 0.5 +them 760.0 0.5 +toward 760.5 0.5 +the 761.0 0.5 +other 761.5 0.5 +team. 762.0 0.5 +So 762.5 0.5 +-- 763.0 0.5 +think 763.5 0.5 +you've 764.0 0.5 +got 764.5 0.5 +all 765.0 0.5 +"that?""" 765.5 0.5 ++ 766.0 0.5 +"""Three" 766.5 0.5 +Chasers 767.0 0.5 +try 767.5 0.5 +and 768.0 0.5 +score 768.5 0.5 +with 769.0 0.5 +the 769.5 0.5 +Quaffle; 770.0 0.5 +the 770.5 0.5 +Keeper 771.0 0.5 +guards 771.5 0.5 +the 772.0 0.5 +goal 772.5 0.5 +posts; 773.0 0.5 +the 773.5 0.5 +Beaters 774.0 0.5 +keep 774.5 0.5 +the 775.0 0.5 +Bludgers 775.5 0.5 +away 776.0 0.5 +from 776.5 0.5 +their 777.0 0.5 +"team,""" 777.5 0.5 +Harry 778.0 0.5 +reeled 778.5 0.5 +off. 779.0 0.5 ++ 779.5 0.5 +"""Very" 780.0 0.5 +"good,""" 780.5 0.5 +said 781.0 0.5 +Wood. 781.5 0.5 ++ 782.0 0.5 +"""Er" 782.5 0.5 +-- 783.0 0.5 +have 783.5 0.5 +the 784.0 0.5 +Bludgers 784.5 0.5 +ever 785.0 0.5 +killed 785.5 0.5 +"anyone?""" 786.0 0.5 +Harry 786.5 0.5 +asked, 787.0 0.5 +hoping 787.5 0.5 +he 788.0 0.5 +sounded 788.5 0.5 +offhand. 789.0 0.5 ++ 789.5 0.5 +"""Never" 790.0 0.5 +at 790.5 0.5 +Hogwarts. 791.0 0.5 +We've 791.5 0.5 +had 792.0 0.5 +a 792.5 0.5 +couple 793.0 0.5 +of 793.5 0.5 +broken 794.0 0.5 +jaws 794.5 0.5 +but 795.0 0.5 +nothing 795.5 0.5 +worse 796.0 0.5 +than 796.5 0.5 +that. 797.0 0.5 +Now, 797.5 0.5 +the 798.0 0.5 +last 798.5 0.5 +member 799.0 0.5 +of 799.5 0.5 +the 800.0 0.5 +team 800.5 0.5 +is 801.0 0.5 +the 801.5 0.5 +Seeker. 802.0 0.5 +That's 802.5 0.5 +you. 803.0 0.5 +And 803.5 0.5 +you 804.0 0.5 +don't 804.5 0.5 +have 805.0 0.5 +to 805.5 0.5 +worry 806.0 0.5 +about 806.5 0.5 +the 807.0 0.5 +Quaffle 807.5 0.5 +or 808.0 0.5 +the 808.5 0.5 +Bludgers 809.0 0.5 +"--""" 809.5 0.5 ++ 810.0 0.5 +"""--" 810.5 0.5 +unless 811.0 0.5 +they 811.5 0.5 +crack 812.0 0.5 +my 812.5 0.5 +head 813.0 0.5 +"open.""" 813.5 0.5 ++ 814.0 0.5 +"""Don't" 814.5 0.5 +worry, 815.0 0.5 +the 815.5 0.5 +Weasleys 816.0 0.5 +are 816.5 0.5 +more 817.0 0.5 +than 817.5 0.5 +a 818.0 0.5 +match 818.5 0.5 +for 819.0 0.5 +the 819.5 0.5 +Bludgers 820.0 0.5 +-- 820.5 0.5 +I 821.0 0.5 +mean, 821.5 0.5 +they're 822.0 0.5 +like 822.5 0.5 +a 823.0 0.5 +pair 823.5 0.5 +of 824.0 0.5 +human 824.5 0.5 +Bludgers 825.0 0.5 +"themselves.""" 825.5 0.5 ++ 826.0 0.5 diff --git a/data/language/harrypotter/task-harry_run-6_events.tsv b/data/language/harrypotter/task-harry_run-6_events.tsv new file mode 100644 index 00000000..44c259c4 --- /dev/null +++ b/data/language/harrypotter/task-harry_run-6_events.tsv @@ -0,0 +1,1298 @@ +word format onset duration +Wood 10.0 0.5 +reached 10.5 0.5 +into 11.0 0.5 +the 11.5 0.5 +crate 12.0 0.5 +and 12.5 0.5 +took 13.0 0.5 +out 13.5 0.5 +the 14.0 0.5 +fourth 14.5 0.5 +and 15.0 0.5 +last 15.5 0.5 +ball. 16.0 0.5 +Compared 16.5 0.5 +with 17.0 0.5 +the 17.5 0.5 +Quaffle 18.0 0.5 +and 18.5 0.5 +the 19.0 0.5 +Bludgers, 19.5 0.5 +it 20.0 0.5 +was 20.5 0.5 +tiny, 21.0 0.5 +about 21.5 0.5 +the 22.0 0.5 +size 22.5 0.5 +of 23.0 0.5 +a 23.5 0.5 +large 24.0 0.5 +walnut. 24.5 0.5 +It 25.0 0.5 +was 25.5 0.5 +bright 26.0 0.5 +gold 26.5 0.5 +and 27.0 0.5 +had 27.5 0.5 +little 28.0 0.5 +fluttering 28.5 0.5 +silver 29.0 0.5 +wings. 29.5 0.5 ++ 30.0 0.5 +"""This,""" italic 30.5 0.5 +said 31.0 0.5 +Wood, 31.5 0.5 +"""is" 32.0 0.5 +the 32.5 0.5 +Golden 33.0 0.5 +Snitch, 33.5 0.5 +and 34.0 0.5 +it's 34.5 0.5 +the 35.0 0.5 +most 35.5 0.5 +important 36.0 0.5 +ball 36.5 0.5 +of 37.0 0.5 +the 37.5 0.5 +lot. 38.0 0.5 +It's 38.5 0.5 +very 39.0 0.5 +hard 39.5 0.5 +to 40.0 0.5 +catch 40.5 0.5 +because 41.0 0.5 +it's 41.5 0.5 +so 42.0 0.5 +fast 42.5 0.5 +and 43.0 0.5 +difficult 43.5 0.5 +to 44.0 0.5 +see. 44.5 0.5 +It's 45.0 0.5 +the 45.5 0.5 +Seeker's 46.0 0.5 +job 46.5 0.5 +to 47.0 0.5 +catch 47.5 0.5 +it. 48.0 0.5 +You've 48.5 0.5 +got 49.0 0.5 +to 49.5 0.5 +weave 50.0 0.5 +in 50.5 0.5 +and 51.0 0.5 +out 51.5 0.5 +of 52.0 0.5 +the 52.5 0.5 +Chasers, 53.0 0.5 +Beaters, 53.5 0.5 +Bludgers, 54.0 0.5 +and 54.5 0.5 +Quaffle 55.0 0.5 +to 55.5 0.5 +get 56.0 0.5 +it 56.5 0.5 +before 57.0 0.5 +the 57.5 0.5 +other 58.0 0.5 +team's 58.5 0.5 +Seeker, 59.0 0.5 +because 59.5 0.5 +whichever 60.0 0.5 +Seeker 60.5 0.5 +catches 61.0 0.5 +the 61.5 0.5 +Snitch 62.0 0.5 +wins 62.5 0.5 +his 63.0 0.5 +team 63.5 0.5 +an 64.0 0.5 +extra 64.5 0.5 +hundred 65.0 0.5 +and 65.5 0.5 +fifty 66.0 0.5 +points, 66.5 0.5 +so 67.0 0.5 +they 67.5 0.5 +nearly 68.0 0.5 +always 68.5 0.5 +win. 69.0 0.5 +That's 69.5 0.5 +why 70.0 0.5 +Seekers 70.5 0.5 +get 71.0 0.5 +fouled 71.5 0.5 +so 72.0 0.5 +much. 72.5 0.5 +A 73.0 0.5 +game 73.5 0.5 +of 74.0 0.5 +Quidditch 74.5 0.5 +only 75.0 0.5 +ends 75.5 0.5 +when 76.0 0.5 +the 76.5 0.5 +Snitch 77.0 0.5 +is 77.5 0.5 +caught, 78.0 0.5 +so 78.5 0.5 +it 79.0 0.5 +can 79.5 0.5 +go 80.0 0.5 +on 80.5 0.5 +for 81.0 0.5 +ages 81.5 0.5 +-- 82.0 0.5 +I 82.5 0.5 +think 83.0 0.5 +the 83.5 0.5 +record 84.0 0.5 +is 84.5 0.5 +three 85.0 0.5 +months, 85.5 0.5 +they 86.0 0.5 +had 86.5 0.5 +to 87.0 0.5 +keep 87.5 0.5 +bringing 88.0 0.5 +on 88.5 0.5 +substitutes 89.0 0.5 +so 89.5 0.5 +the 90.0 0.5 +players 90.5 0.5 +could 91.0 0.5 +get 91.5 0.5 +some 92.0 0.5 +sleep. 92.5 0.5 ++ 93.0 0.5 +"""Well," 93.5 0.5 +that's 94.0 0.5 +it 94.5 0.5 +-- 95.0 0.5 +any 95.5 0.5 +"questions?""" 96.0 0.5 ++ 96.5 0.5 +Harry 97.0 0.5 +shook 97.5 0.5 +his 98.0 0.5 +head. 98.5 0.5 +He 99.0 0.5 +understood 99.5 0.5 +what 100.0 0.5 +he 100.5 0.5 +had 101.0 0.5 +to 101.5 0.5 +do 102.0 0.5 +all 102.5 0.5 +right, 103.0 0.5 +it 103.5 0.5 +was 104.0 0.5 +doing 104.5 0.5 +it 105.0 0.5 +that 105.5 0.5 +was 106.0 0.5 +going 106.5 0.5 +to 107.0 0.5 +be 107.5 0.5 +the 108.0 0.5 +problem. 108.5 0.5 ++ 109.0 0.5 +"""We" 109.5 0.5 +won't 110.0 0.5 +practice 110.5 0.5 +with 111.0 0.5 +the 111.5 0.5 +Snitch 112.0 0.5 +"yet,""" 112.5 0.5 +said 113.0 0.5 +Wood, 113.5 0.5 +carefully 114.0 0.5 +shutting 114.5 0.5 +it 115.0 0.5 +back 115.5 0.5 +inside 116.0 0.5 +the 116.5 0.5 +crate, 117.0 0.5 +"""it's" 117.5 0.5 +too 118.0 0.5 +dark, 118.5 0.5 +we 119.0 0.5 +might 119.5 0.5 +lose 120.0 0.5 +it. 120.5 0.5 +Let's 121.0 0.5 +try 121.5 0.5 +you 122.0 0.5 +out 122.5 0.5 +with 123.0 0.5 +a 123.5 0.5 +few 124.0 0.5 +of 124.5 0.5 +"these.""" 125.0 0.5 ++ 125.5 0.5 +He 126.0 0.5 +pulled 126.5 0.5 +a 127.0 0.5 +bag 127.5 0.5 +of 128.0 0.5 +ordinary 128.5 0.5 +golf 129.0 0.5 +balls 129.5 0.5 +out 130.0 0.5 +of 130.5 0.5 +his 131.0 0.5 +pocket 131.5 0.5 +and 132.0 0.5 +a 132.5 0.5 +few 133.0 0.5 +minutes 133.5 0.5 +later, 134.0 0.5 +he 134.5 0.5 +and 135.0 0.5 +Harry 135.5 0.5 +were 136.0 0.5 +up 136.5 0.5 +in 137.0 0.5 +the 137.5 0.5 +air, 138.0 0.5 +Wood 138.5 0.5 +throwing 139.0 0.5 +the 139.5 0.5 +golf 140.0 0.5 +balls 140.5 0.5 +as 141.0 0.5 +hard 141.5 0.5 +as 142.0 0.5 +he 142.5 0.5 +could 143.0 0.5 +in 143.5 0.5 +every 144.0 0.5 +direction 144.5 0.5 +for 145.0 0.5 +Harry 145.5 0.5 +to 146.0 0.5 +catch. 146.5 0.5 ++ 147.0 0.5 +Harry 147.5 0.5 +didn't 148.0 0.5 +miss 148.5 0.5 +a 149.0 0.5 +single 149.5 0.5 +one, 150.0 0.5 +and 150.5 0.5 +Wood 151.0 0.5 +was 151.5 0.5 +delighted. 152.0 0.5 +After 152.5 0.5 +half 153.0 0.5 +an 153.5 0.5 +hour, 154.0 0.5 +night 154.5 0.5 +had 155.0 0.5 +really 155.5 0.5 +fallen 156.0 0.5 +and 156.5 0.5 +they 157.0 0.5 +couldn't 157.5 0.5 +carry 158.0 0.5 +on. 158.5 0.5 ++ 159.0 0.5 +"""That" 159.5 0.5 +Quidditch 160.0 0.5 +cup'll 160.5 0.5 +have 161.0 0.5 +our 161.5 0.5 +name 162.0 0.5 +on 162.5 0.5 +it 163.0 0.5 +this 163.5 0.5 +"year,""" 164.0 0.5 +said 164.5 0.5 +Wood 165.0 0.5 +happily 165.5 0.5 +as 166.0 0.5 +they 166.5 0.5 +trudged 167.0 0.5 +back 167.5 0.5 +up 168.0 0.5 +to 168.5 0.5 +the 169.0 0.5 +castle. 169.5 0.5 +"""I" 170.0 0.5 +wouldn't 170.5 0.5 +be 171.0 0.5 +surprised 171.5 0.5 +if 172.0 0.5 +you 172.5 0.5 +turn 173.0 0.5 +out 173.5 0.5 +better 174.0 0.5 +than 174.5 0.5 +Charlie 175.0 0.5 +Weasley, 175.5 0.5 +and 176.0 0.5 +he 176.5 0.5 +could 177.0 0.5 +have 177.5 0.5 +played 178.0 0.5 +for 178.5 0.5 +England 179.0 0.5 +if 179.5 0.5 +he 180.0 0.5 +hadn't 180.5 0.5 +gone 181.0 0.5 +off 181.5 0.5 +chasing 182.0 0.5 +"dragons.""" 182.5 0.5 ++ 183.0 0.5 +Perhaps 183.5 0.5 +it 184.0 0.5 +was 184.5 0.5 +because 185.0 0.5 +he 185.5 0.5 +was 186.0 0.5 +now 186.5 0.5 +so 187.0 0.5 +busy, 187.5 0.5 +what 188.0 0.5 +with 188.5 0.5 +Quidditch 189.0 0.5 +practice 189.5 0.5 +three 190.0 0.5 +evenings 190.5 0.5 +a 191.0 0.5 +week 191.5 0.5 +on 192.0 0.5 +top 192.5 0.5 +of 193.0 0.5 +all 193.5 0.5 +his 194.0 0.5 +homework, 194.5 0.5 +but 195.0 0.5 +Harry 195.5 0.5 +could 196.0 0.5 +hardly 196.5 0.5 +believe 197.0 0.5 +it 197.5 0.5 +when 198.0 0.5 +he 198.5 0.5 +realized 199.0 0.5 +that 199.5 0.5 +he'd 200.0 0.5 +already 200.5 0.5 +been 201.0 0.5 +at 201.5 0.5 +Hogwarts 202.0 0.5 +two 202.5 0.5 +months. 203.0 0.5 +The 203.5 0.5 +castle 204.0 0.5 +felt 204.5 0.5 +more 205.0 0.5 +like 205.5 0.5 +home 206.0 0.5 +than 206.5 0.5 +Privet 207.0 0.5 +Drive 207.5 0.5 +ever 208.0 0.5 +had. 208.5 0.5 +His 209.0 0.5 +lessons, 209.5 0.5 +too, 210.0 0.5 +were 210.5 0.5 +becoming 211.0 0.5 +more 211.5 0.5 +and 212.0 0.5 +more 212.5 0.5 +interesting 213.0 0.5 +now 213.5 0.5 +that 214.0 0.5 +they 214.5 0.5 +had 215.0 0.5 +mastered 215.5 0.5 +the 216.0 0.5 +basics. 216.5 0.5 ++ 217.0 0.5 +On 217.5 0.5 +Halloween 218.0 0.5 +morning 218.5 0.5 +they 219.0 0.5 +woke 219.5 0.5 +to 220.0 0.5 +the 220.5 0.5 +delicious 221.0 0.5 +smell 221.5 0.5 +of 222.0 0.5 +baking 222.5 0.5 +pumpkin 223.0 0.5 +wafting 223.5 0.5 +through 224.0 0.5 +the 224.5 0.5 +corridors. 225.0 0.5 +Even 225.5 0.5 +better, 226.0 0.5 +Professor 226.5 0.5 +Flitwick 227.0 0.5 +announced 227.5 0.5 +in 228.0 0.5 +Charms 228.5 0.5 +that 229.0 0.5 +he 229.5 0.5 +thought 230.0 0.5 +they 230.5 0.5 +were 231.0 0.5 +ready 231.5 0.5 +to 232.0 0.5 +start 232.5 0.5 +making 233.0 0.5 +objects 233.5 0.5 +fly, 234.0 0.5 +something 234.5 0.5 +they 235.0 0.5 +had 235.5 0.5 +all 236.0 0.5 +been 236.5 0.5 +dying 237.0 0.5 +to 237.5 0.5 +try 238.0 0.5 +since 238.5 0.5 +they'd 239.0 0.5 +seen 239.5 0.5 +him 240.0 0.5 +make 240.5 0.5 +Neville's 241.0 0.5 +toad 241.5 0.5 +zoom 242.0 0.5 +around 242.5 0.5 +the 243.0 0.5 +classroom. 243.5 0.5 +Professor 244.0 0.5 +Flitwick 244.5 0.5 +put 245.0 0.5 +the 245.5 0.5 +class 246.0 0.5 +into 246.5 0.5 +pairs 247.0 0.5 +to 247.5 0.5 +practice. 248.0 0.5 +Harry's 248.5 0.5 +partner 249.0 0.5 +was 249.5 0.5 +Seamus 250.0 0.5 +Finnigan 250.5 0.5 +(which 251.0 0.5 +was 251.5 0.5 +a 252.0 0.5 +relief, 252.5 0.5 +because 253.0 0.5 +Neville 253.5 0.5 +had 254.0 0.5 +been 254.5 0.5 +trying 255.0 0.5 +to 255.5 0.5 +catch 256.0 0.5 +his 256.5 0.5 +eye). 257.0 0.5 +Ron, 257.5 0.5 +however, 258.0 0.5 +was 258.5 0.5 +to 259.0 0.5 +be 259.5 0.5 +working 260.0 0.5 +with 260.5 0.5 +Hermione 261.0 0.5 +Granger. 261.5 0.5 +It 262.0 0.5 +was 262.5 0.5 +hard 263.0 0.5 +to 263.5 0.5 +tell 264.0 0.5 +whether 264.5 0.5 +Ron 265.0 0.5 +or 265.5 0.5 +Hermione 266.0 0.5 +was 266.5 0.5 +angrier 267.0 0.5 +about 267.5 0.5 +this. 268.0 0.5 +She 268.5 0.5 +hadn't 269.0 0.5 +spoken 269.5 0.5 +to 270.0 0.5 +either 270.5 0.5 +of 271.0 0.5 +them 271.5 0.5 +since 272.0 0.5 +the 272.5 0.5 +day 273.0 0.5 +Harry's 273.5 0.5 +broomstick 274.0 0.5 +had 274.5 0.5 +arrived. 275.0 0.5 ++ 275.5 0.5 +"""Now," 276.0 0.5 +don't 276.5 0.5 +forget 277.0 0.5 +that 277.5 0.5 +nice 278.0 0.5 +wrist 278.5 0.5 +movement 279.0 0.5 +we've 279.5 0.5 +been 280.0 0.5 +"practicing!""" 280.5 0.5 +squeaked 281.0 0.5 +Professor 281.5 0.5 +Flitwick, 282.0 0.5 +perched 282.5 0.5 +on 283.0 0.5 +top 283.5 0.5 +of 284.0 0.5 +his 284.5 0.5 +pile 285.0 0.5 +of 285.5 0.5 +books 286.0 0.5 +as 286.5 0.5 +usual. 287.0 0.5 +"""Swish" 287.5 0.5 +and 288.0 0.5 +flick, 288.5 0.5 +remember, 289.0 0.5 +swish 289.5 0.5 +and 290.0 0.5 +flick. 290.5 0.5 +And 291.0 0.5 +saying 291.5 0.5 +the 292.0 0.5 +magic 292.5 0.5 +words 293.0 0.5 +properly 293.5 0.5 +is 294.0 0.5 +very 294.5 0.5 +important, 295.0 0.5 +too 295.5 0.5 +-- 296.0 0.5 +never 296.5 0.5 +forget 297.0 0.5 +Wizard 297.5 0.5 +Baruffio, 298.0 0.5 +who 298.5 0.5 +said 299.0 0.5 +'s' 299.5 0.5 +instead 300.0 0.5 +of 300.5 0.5 +'f' 301.0 0.5 +and 301.5 0.5 +found 302.0 0.5 +himself 302.5 0.5 +on 303.0 0.5 +the 303.5 0.5 +floor 304.0 0.5 +with 304.5 0.5 +a 305.0 0.5 +buffalo 305.5 0.5 +on 306.0 0.5 +his 306.5 0.5 +"chest.""" 307.0 0.5 ++ 307.5 0.5 +It 308.0 0.5 +was 308.5 0.5 +very 309.0 0.5 +difficult. 309.5 0.5 +Harry 310.0 0.5 +and 310.5 0.5 +Seamus 311.0 0.5 +swished 311.5 0.5 +and 312.0 0.5 +flicked, 312.5 0.5 +but 313.0 0.5 +the 313.5 0.5 +feather 314.0 0.5 +they 314.5 0.5 +were 315.0 0.5 +supposed 315.5 0.5 +to 316.0 0.5 +be 316.5 0.5 +sending 317.0 0.5 +skyward 317.5 0.5 +just 318.0 0.5 +lay 318.5 0.5 +on 319.0 0.5 +the 319.5 0.5 +desktop. 320.0 0.5 +Seamus 320.5 0.5 +got 321.0 0.5 +so 321.5 0.5 +impatient 322.0 0.5 +that 322.5 0.5 +he 323.0 0.5 +prodded 323.5 0.5 +it 324.0 0.5 +with 324.5 0.5 +his 325.0 0.5 +wand 325.5 0.5 +and 326.0 0.5 +set 326.5 0.5 +fire 327.0 0.5 +to 327.5 0.5 +it 328.0 0.5 +-- 328.5 0.5 +Harry 329.0 0.5 +had 329.5 0.5 +to 330.0 0.5 +put 330.5 0.5 +it 331.0 0.5 +out 331.5 0.5 +with 332.0 0.5 +his 332.5 0.5 +hat. 333.0 0.5 ++ 333.5 0.5 +Ron, 334.0 0.5 +at 334.5 0.5 +the 335.0 0.5 +next 335.5 0.5 +table, 336.0 0.5 +wasn't 336.5 0.5 +having 337.0 0.5 +much 337.5 0.5 +more 338.0 0.5 +luck. 338.5 0.5 ++ 339.0 0.5 +"""Wingardium" italic 339.5 0.5 +"Leviosa!""" italic 340.0 0.5 +he 340.5 0.5 +shouted, 341.0 0.5 +waving 341.5 0.5 +his 342.0 0.5 +long 342.5 0.5 +arms 343.0 0.5 +like 343.5 0.5 +a 344.0 0.5 +windmill. 344.5 0.5 ++ 345.0 0.5 +"""You're" 345.5 0.5 +saying 346.0 0.5 +it 346.5 0.5 +"wrong,""" 347.0 0.5 +Harry 347.5 0.5 +heard 348.0 0.5 +Hermione 348.5 0.5 +snap. 349.0 0.5 +"""It's" 349.5 0.5 +Wing-gar-dium italic 350.0 0.5 +Levi-o-sa, italic 350.5 0.5 +make 351.0 0.5 +the 351.5 0.5 +'gar' 352.0 0.5 +nice 352.5 0.5 +and 353.0 0.5 +"long.""" 353.5 0.5 ++ 354.0 0.5 +"""You" 354.5 0.5 +do 355.0 0.5 +it, 355.5 0.5 +then, 356.0 0.5 +if 356.5 0.5 +you're 357.0 0.5 +so 357.5 0.5 +"clever,""" 358.0 0.5 +Ron 358.5 0.5 +snarled. 359.0 0.5 ++ 359.5 0.5 +Hermione 360.0 0.5 +rolled 360.5 0.5 +up 361.0 0.5 +the 361.5 0.5 +sleeves 362.0 0.5 +of 362.5 0.5 +her 363.0 0.5 +gown, 363.5 0.5 +flicked 364.0 0.5 +her 364.5 0.5 +wand, 365.0 0.5 +and 365.5 0.5 +said, 366.0 0.5 +"""Wingardium" italic 366.5 0.5 +"Leviosa!""" italic 367.0 0.5 ++ 367.5 0.5 +Their 368.0 0.5 +feather 368.5 0.5 +rose 369.0 0.5 +off 369.5 0.5 +the 370.0 0.5 +desk 370.5 0.5 +and 371.0 0.5 +hovered 371.5 0.5 +about 372.0 0.5 +four 372.5 0.5 +feet 373.0 0.5 +above 373.5 0.5 +their 374.0 0.5 +heads. 374.5 0.5 ++ 375.0 0.5 +"""Oh," 375.5 0.5 +well 376.0 0.5 +"done!""" 376.5 0.5 +cried 377.0 0.5 +Professor 377.5 0.5 +Flitwick, 378.0 0.5 +clapping. 378.5 0.5 +"""Everyone" 379.0 0.5 +see 379.5 0.5 +here, 380.0 0.5 +Miss 380.5 0.5 +Granger's 381.0 0.5 +done 381.5 0.5 +"it!""" 382.0 0.5 ++ 382.5 0.5 +Ron 383.0 0.5 +was 383.5 0.5 +in 384.0 0.5 +a 384.5 0.5 +very 385.0 0.5 +bad 385.5 0.5 +mood 386.0 0.5 +by 386.5 0.5 +the 387.0 0.5 +end 387.5 0.5 +of 388.0 0.5 +the 388.5 0.5 +class. 389.0 0.5 ++ 389.5 0.5 +"""It's" 390.0 0.5 +no 390.5 0.5 +wonder 391.0 0.5 +no 391.5 0.5 +one 392.0 0.5 +can 392.5 0.5 +stand 393.0 0.5 +"her,""" 393.5 0.5 +he 394.0 0.5 +said 394.5 0.5 +to 395.0 0.5 +Harry 395.5 0.5 +as 396.0 0.5 +they 396.5 0.5 +pushed 397.0 0.5 +their 397.5 0.5 +way 398.0 0.5 +into 398.5 0.5 +the 399.0 0.5 +crowded 399.5 0.5 +corridor, 400.0 0.5 +"""she's" 400.5 0.5 +a 401.0 0.5 +nightmare, 401.5 0.5 +honestly. 402.0 0.5 +"""" 402.5 0.5 ++ 403.0 0.5 +Someone 403.5 0.5 +knocked 404.0 0.5 +into 404.5 0.5 +Harry 405.0 0.5 +as 405.5 0.5 +they 406.0 0.5 +hurried 406.5 0.5 +past 407.0 0.5 +him. 407.5 0.5 +It 408.0 0.5 +was 408.5 0.5 +Hermione. 409.0 0.5 +Harry 409.5 0.5 +caught 410.0 0.5 +a 410.5 0.5 +glimpse 411.0 0.5 +of 411.5 0.5 +her 412.0 0.5 +face 412.5 0.5 +-- 413.0 0.5 +and 413.5 0.5 +was 414.0 0.5 +startled 414.5 0.5 +to 415.0 0.5 +see 415.5 0.5 +that 416.0 0.5 +she 416.5 0.5 +was 417.0 0.5 +in 417.5 0.5 +tears. 418.0 0.5 ++ 418.5 0.5 +"""I" 419.0 0.5 +think 419.5 0.5 +she 420.0 0.5 +heard 420.5 0.5 +"you.""" 421.0 0.5 ++ 421.5 0.5 +"""So?""" 422.0 0.5 +said 422.5 0.5 +Ron, 423.0 0.5 +but 423.5 0.5 +he 424.0 0.5 +looked 424.5 0.5 +a 425.0 0.5 +bit 425.5 0.5 +uncomfortable. 426.0 0.5 +"""She" 426.5 0.5 +must've 427.0 0.5 +noticed 427.5 0.5 +she's 428.0 0.5 +got 428.5 0.5 +no 429.0 0.5 +"friends.""" 429.5 0.5 ++ 430.0 0.5 +Hermione 430.5 0.5 +didn't 431.0 0.5 +turn 431.5 0.5 +up 432.0 0.5 +for 432.5 0.5 +the 433.0 0.5 +next 433.5 0.5 +class 434.0 0.5 +and 434.5 0.5 +wasn't 435.0 0.5 +seen 435.5 0.5 +all 436.0 0.5 +afternoon. 436.5 0.5 +On 437.0 0.5 +their 437.5 0.5 +way 438.0 0.5 +down 438.5 0.5 +to 439.0 0.5 +the 439.5 0.5 +Great 440.0 0.5 +Hall 440.5 0.5 +for 441.0 0.5 +the 441.5 0.5 +Halloween 442.0 0.5 +feast, 442.5 0.5 +Harry 443.0 0.5 +and 443.5 0.5 +Ron 444.0 0.5 +overheard 444.5 0.5 +Parvati 445.0 0.5 +Patil 445.5 0.5 +telling 446.0 0.5 +her 446.5 0.5 +friend 447.0 0.5 +Lavender 447.5 0.5 +that 448.0 0.5 +Hermione 448.5 0.5 +was 449.0 0.5 +crying 449.5 0.5 +in 450.0 0.5 +the 450.5 0.5 +girls' 451.0 0.5 +bathroom 451.5 0.5 +and 452.0 0.5 +wanted 452.5 0.5 +to 453.0 0.5 +be 453.5 0.5 +left 454.0 0.5 +alone. 454.5 0.5 +Ron 455.0 0.5 +looked 455.5 0.5 +still 456.0 0.5 +more 456.5 0.5 +awkward 457.0 0.5 +at 457.5 0.5 +this, 458.0 0.5 +but 458.5 0.5 +a 459.0 0.5 +moment 459.5 0.5 +later 460.0 0.5 +they 460.5 0.5 +had 461.0 0.5 +entered 461.5 0.5 +the 462.0 0.5 +Great 462.5 0.5 +Hall, 463.0 0.5 +where 463.5 0.5 +the 464.0 0.5 +Halloween 464.5 0.5 +decorations 465.0 0.5 +put 465.5 0.5 +Hermione 466.0 0.5 +out 466.5 0.5 +of 467.0 0.5 +their 467.5 0.5 +minds. 468.0 0.5 ++ 468.5 0.5 +A 469.0 0.5 +thousand 469.5 0.5 +live 470.0 0.5 +bats 470.5 0.5 +fluttered 471.0 0.5 +from 471.5 0.5 +the 472.0 0.5 +walls 472.5 0.5 +and 473.0 0.5 +ceiling 473.5 0.5 +while 474.0 0.5 +a 474.5 0.5 +thousand 475.0 0.5 +more 475.5 0.5 +swooped 476.0 0.5 +over 476.5 0.5 +the 477.0 0.5 +tables 477.5 0.5 +in 478.0 0.5 +low 478.5 0.5 +black 479.0 0.5 +clouds, 479.5 0.5 +making 480.0 0.5 +the 480.5 0.5 +candles 481.0 0.5 +in 481.5 0.5 +the 482.0 0.5 +pumpkins 482.5 0.5 +stutter. 483.0 0.5 +The 483.5 0.5 +feast 484.0 0.5 +appeared 484.5 0.5 +suddenly 485.0 0.5 +on 485.5 0.5 +the 486.0 0.5 +golden 486.5 0.5 +plates, 487.0 0.5 +as 487.5 0.5 +it 488.0 0.5 +had 488.5 0.5 +at 489.0 0.5 +the 489.5 0.5 +start-of-term 490.0 0.5 +banquet. 490.5 0.5 ++ 491.0 0.5 +Harry 491.5 0.5 +was 492.0 0.5 +just 492.5 0.5 +helping 493.0 0.5 +himself 493.5 0.5 +to 494.0 0.5 +a 494.5 0.5 +baked 495.0 0.5 +potato 495.5 0.5 +when 496.0 0.5 +Professor 496.5 0.5 +Quirrell 497.0 0.5 +came 497.5 0.5 +sprinting 498.0 0.5 +into 498.5 0.5 +the 499.0 0.5 +hall, 499.5 0.5 +his 500.0 0.5 +turban 500.5 0.5 +askew 501.0 0.5 +and 501.5 0.5 +terror 502.0 0.5 +on 502.5 0.5 +his 503.0 0.5 +face. 503.5 0.5 +Everyone 504.0 0.5 +stared 504.5 0.5 +as 505.0 0.5 +he 505.5 0.5 +reached 506.0 0.5 +Professor 506.5 0.5 +Dumbledore's 507.0 0.5 +chair, 507.5 0.5 +slumped 508.0 0.5 +against 508.5 0.5 +the 509.0 0.5 +table, 509.5 0.5 +and 510.0 0.5 +gasped, 510.5 0.5 +"""Troll" 511.0 0.5 +-- 511.5 0.5 +in 512.0 0.5 +the 512.5 0.5 +dungeons 513.0 0.5 +-- 513.5 0.5 +thought 514.0 0.5 +you 514.5 0.5 +ought 515.0 0.5 +to 515.5 0.5 +"know.""" 516.0 0.5 ++ 516.5 0.5 +He 517.0 0.5 +then 517.5 0.5 +sank 518.0 0.5 +to 518.5 0.5 +the 519.0 0.5 +floor 519.5 0.5 +in 520.0 0.5 +a 520.5 0.5 +dead 521.0 0.5 +faint. 521.5 0.5 ++ 522.0 0.5 +There 522.5 0.5 +was 523.0 0.5 +an 523.5 0.5 +uproar. 524.0 0.5 +It 524.5 0.5 +took 525.0 0.5 +several 525.5 0.5 +purple 526.0 0.5 +firecrackers 526.5 0.5 +exploding 527.0 0.5 +from 527.5 0.5 +the 528.0 0.5 +end 528.5 0.5 +of 529.0 0.5 +Professor 529.5 0.5 +Dumbledore's 530.0 0.5 +wand 530.5 0.5 +to 531.0 0.5 +bring 531.5 0.5 +silence. 532.0 0.5 ++ 532.5 0.5 +"""Prefects,""" 533.0 0.5 +he 533.5 0.5 +rumbled, 534.0 0.5 +"""lead" 534.5 0.5 +your 535.0 0.5 +Houses 535.5 0.5 +back 536.0 0.5 +to 536.5 0.5 +the 537.0 0.5 +dormitories 537.5 0.5 +"immediately!""" 538.0 0.5 ++ 538.5 0.5 +Percy 539.0 0.5 +was 539.5 0.5 +in 540.0 0.5 +his 540.5 0.5 +element. 541.0 0.5 ++ 541.5 0.5 +"""Follow" 542.0 0.5 +me! 542.5 0.5 +Stick 543.0 0.5 +together, 543.5 0.5 +first 544.0 0.5 +years! 544.5 0.5 +No 545.0 0.5 +need 545.5 0.5 +to 546.0 0.5 +fear 546.5 0.5 +the 547.0 0.5 +troll 547.5 0.5 +if 548.0 0.5 +you 548.5 0.5 +follow 549.0 0.5 +my 549.5 0.5 +orders! 550.0 0.5 +Stay 550.5 0.5 +close 551.0 0.5 +behind 551.5 0.5 +me, 552.0 0.5 +now. 552.5 0.5 +Make 553.0 0.5 +way, 553.5 0.5 +first 554.0 0.5 +years 554.5 0.5 +coming 555.0 0.5 +through! 555.5 0.5 +Excuse 556.0 0.5 +me, 556.5 0.5 +I'm 557.0 0.5 +a 557.5 0.5 +"prefect!""" 558.0 0.5 ++ 558.5 0.5 +"""How" 559.0 0.5 +could 559.5 0.5 +a 560.0 0.5 +troll 560.5 0.5 +get 561.0 0.5 +"in?""" 561.5 0.5 +Harry 562.0 0.5 +asked 562.5 0.5 +as 563.0 0.5 +they 563.5 0.5 +climbed 564.0 0.5 +the 564.5 0.5 +stairs. 565.0 0.5 ++ 565.5 0.5 +"""Don't" 566.0 0.5 +ask 566.5 0.5 +me, 567.0 0.5 +they're 567.5 0.5 +supposed 568.0 0.5 +to 568.5 0.5 +be 569.0 0.5 +really 569.5 0.5 +"stupid,""" 570.0 0.5 +said 570.5 0.5 +Ron. 571.0 0.5 +"""Maybe" 571.5 0.5 +Peeves 572.0 0.5 +let 572.5 0.5 +it 573.0 0.5 +in 573.5 0.5 +for 574.0 0.5 +a 574.5 0.5 +Halloween 575.0 0.5 +"joke.""" 575.5 0.5 ++ 576.0 0.5 +They 576.5 0.5 +passed 577.0 0.5 +different 577.5 0.5 +groups 578.0 0.5 +of 578.5 0.5 +people 579.0 0.5 +hurrying 579.5 0.5 +in 580.0 0.5 +different 580.5 0.5 +directions. 581.0 0.5 +As 581.5 0.5 +they 582.0 0.5 +jostled 582.5 0.5 +their 583.0 0.5 +way 583.5 0.5 +through 584.0 0.5 +a 584.5 0.5 +crowd 585.0 0.5 +of 585.5 0.5 +confused 586.0 0.5 +Hufflepuffs, 586.5 0.5 +Harry 587.0 0.5 +suddenly 587.5 0.5 +grabbed 588.0 0.5 +Ron's 588.5 0.5 +arm. 589.0 0.5 ++ 589.5 0.5 +"""I've" 590.0 0.5 +just 590.5 0.5 +thought 591.0 0.5 +-- 591.5 0.5 +"Hermione.""" 592.0 0.5 ++ 592.5 0.5 +"""What" 593.0 0.5 +about 593.5 0.5 +"her?""" 594.0 0.5 ++ 594.5 0.5 +"""She" 595.0 0.5 +doesn't 595.5 0.5 +know 596.0 0.5 +about 596.5 0.5 +the 597.0 0.5 +"troll.""" 597.5 0.5 ++ 598.0 0.5 +Ron 598.5 0.5 +bit 599.0 0.5 +his 599.5 0.5 +lip. 600.0 0.5 ++ 600.5 0.5 +"""Oh," 601.0 0.5 +all 601.5 0.5 +"right,""" 602.0 0.5 +he 602.5 0.5 +snapped. 603.0 0.5 +"""But" 603.5 0.5 +Percy'd 604.0 0.5 +better 604.5 0.5 +not 605.0 0.5 +see 605.5 0.5 +"us.""" 606.0 0.5 ++ 606.5 0.5 +Ducking 607.0 0.5 +down, 607.5 0.5 +they 608.0 0.5 +joined 608.5 0.5 +the 609.0 0.5 +Hufflepuffs 609.5 0.5 +going 610.0 0.5 +the 610.5 0.5 +other 611.0 0.5 +way, 611.5 0.5 +slipped 612.0 0.5 +down 612.5 0.5 +a 613.0 0.5 +deserted 613.5 0.5 +side 614.0 0.5 +corridor, 614.5 0.5 +and 615.0 0.5 +hurried 615.5 0.5 +off 616.0 0.5 +toward 616.5 0.5 +the 617.0 0.5 +girls' 617.5 0.5 +bathroom. 618.0 0.5 +They 618.5 0.5 +had 619.0 0.5 +just 619.5 0.5 +turned 620.0 0.5 +the 620.5 0.5 +corner 621.0 0.5 +when 621.5 0.5 +they 622.0 0.5 +heard 622.5 0.5 +quick 623.0 0.5 +footsteps 623.5 0.5 +behind 624.0 0.5 +them. 624.5 0.5 ++ 625.0 0.5 +"""Percy!""" 625.5 0.5 +hissed 626.0 0.5 +Ron, 626.5 0.5 +pulling 627.0 0.5 +Harry 627.5 0.5 +behind 628.0 0.5 +a 628.5 0.5 +large 629.0 0.5 +stone 629.5 0.5 +griffin. 630.0 0.5 ++ 630.5 0.5 +Peering 631.0 0.5 +around 631.5 0.5 +it, 632.0 0.5 +however, 632.5 0.5 +they 633.0 0.5 +saw 633.5 0.5 +not 634.0 0.5 +Percy 634.5 0.5 +but 635.0 0.5 +Snape. 635.5 0.5 +He 636.0 0.5 +crossed 636.5 0.5 +the 637.0 0.5 +corridor 637.5 0.5 +and 638.0 0.5 +disappeared 638.5 0.5 +from 639.0 0.5 +view. 639.5 0.5 ++ 640.0 0.5 +"""What's" 640.5 0.5 +he 641.0 0.5 +"doing?""" 641.5 0.5 +Harry 642.0 0.5 +whispered. 642.5 0.5 +"""Why" 643.0 0.5 +isn't 643.5 0.5 +he 644.0 0.5 +down 644.5 0.5 +in 645.0 0.5 +the 645.5 0.5 +dungeons 646.0 0.5 +with 646.5 0.5 +the 647.0 0.5 +rest 647.5 0.5 +of 648.0 0.5 +the 648.5 0.5 +"teachers?""" 649.0 0.5 ++ 649.5 0.5 +"""Search" 650.0 0.5 +"me.""" 650.5 0.5 ++ 651.0 0.5 +Quietly 651.5 0.5 +as 652.0 0.5 +possible, 652.5 0.5 +they 653.0 0.5 +crept 653.5 0.5 +along 654.0 0.5 +the 654.5 0.5 +next 655.0 0.5 +corridor 655.5 0.5 +after 656.0 0.5 +Snape's 656.5 0.5 +fading 657.0 0.5 +footsteps. 657.5 0.5 ++ 658.0 0.5 diff --git a/data/language/harrypotter/task-harry_run-7_events.tsv b/data/language/harrypotter/task-harry_run-7_events.tsv new file mode 100644 index 00000000..25bb1837 --- /dev/null +++ b/data/language/harrypotter/task-harry_run-7_events.tsv @@ -0,0 +1,1546 @@ +word format onset duration +"""He's" 10.0 0.5 +heading 10.5 0.5 +for 11.0 0.5 +the 11.5 0.5 +third 12.0 0.5 +"floor,""" 12.5 0.5 +Harry 13.0 0.5 +said, 13.5 0.5 +but 14.0 0.5 +Ron 14.5 0.5 +held 15.0 0.5 +up 15.5 0.5 +his 16.0 0.5 +hand. 16.5 0.5 ++ 17.0 0.5 +"""Can" 17.5 0.5 +you 18.0 0.5 +smell 18.5 0.5 +"something?""" 19.0 0.5 ++ 19.5 0.5 +Harry 20.0 0.5 +sniffed 20.5 0.5 +and 21.0 0.5 +a 21.5 0.5 +foul 22.0 0.5 +stench 22.5 0.5 +reached 23.0 0.5 +his 23.5 0.5 +nostrils, 24.0 0.5 +a 24.5 0.5 +mixture 25.0 0.5 +of 25.5 0.5 +old 26.0 0.5 +socks 26.5 0.5 +and 27.0 0.5 +the 27.5 0.5 +kind 28.0 0.5 +of 28.5 0.5 +public 29.0 0.5 +toilet 29.5 0.5 +no 30.0 0.5 +one 30.5 0.5 +seems 31.0 0.5 +to 31.5 0.5 +clean. 32.0 0.5 ++ 32.5 0.5 +And 33.0 0.5 +then 33.5 0.5 +they 34.0 0.5 +heard 34.5 0.5 +it 35.0 0.5 +-- 35.5 0.5 +a 36.0 0.5 +low 36.5 0.5 +grunting, 37.0 0.5 +and 37.5 0.5 +the 38.0 0.5 +shuffling 38.5 0.5 +footfalls 39.0 0.5 +of 39.5 0.5 +gigantic 40.0 0.5 +feet. 40.5 0.5 +Ron 41.0 0.5 +pointed 41.5 0.5 +-- 42.0 0.5 +at 42.5 0.5 +the 43.0 0.5 +end 43.5 0.5 +of 44.0 0.5 +a 44.5 0.5 +passage 45.0 0.5 +to 45.5 0.5 +the 46.0 0.5 +left, 46.5 0.5 +something 47.0 0.5 +huge 47.5 0.5 +was 48.0 0.5 +moving 48.5 0.5 +toward 49.0 0.5 +them. 49.5 0.5 +They 50.0 0.5 +shrank 50.5 0.5 +into 51.0 0.5 +the 51.5 0.5 +shadows 52.0 0.5 +and 52.5 0.5 +watched 53.0 0.5 +as 53.5 0.5 +it 54.0 0.5 +emerged 54.5 0.5 +into 55.0 0.5 +a 55.5 0.5 +patch 56.0 0.5 +of 56.5 0.5 +moonlight. 57.0 0.5 ++ 57.5 0.5 +It 58.0 0.5 +was 58.5 0.5 +a 59.0 0.5 +horrible 59.5 0.5 +sight. 60.0 0.5 +Twelve 60.5 0.5 +feet 61.0 0.5 +tall, 61.5 0.5 +its 62.0 0.5 +skin 62.5 0.5 +was 63.0 0.5 +a 63.5 0.5 +dull, 64.0 0.5 +granite 64.5 0.5 +gray, 65.0 0.5 +its 65.5 0.5 +great 66.0 0.5 +lumpy 66.5 0.5 +body 67.0 0.5 +like 67.5 0.5 +a 68.0 0.5 +boulder 68.5 0.5 +with 69.0 0.5 +its 69.5 0.5 +small 70.0 0.5 +bald 70.5 0.5 +head 71.0 0.5 +perched 71.5 0.5 +on 72.0 0.5 +top 72.5 0.5 +like 73.0 0.5 +a 73.5 0.5 +coconut. 74.0 0.5 +It 74.5 0.5 +had 75.0 0.5 +short 75.5 0.5 +legs 76.0 0.5 +thick 76.5 0.5 +as 77.0 0.5 +tree 77.5 0.5 +trunks 78.0 0.5 +with 78.5 0.5 +flat, 79.0 0.5 +horny 79.5 0.5 +feet. 80.0 0.5 +The 80.5 0.5 +smell 81.0 0.5 +coming 81.5 0.5 +from 82.0 0.5 +it 82.5 0.5 +was 83.0 0.5 +incredible. 83.5 0.5 +It 84.0 0.5 +was 84.5 0.5 +holding 85.0 0.5 +a 85.5 0.5 +huge 86.0 0.5 +wooden 86.5 0.5 +club, 87.0 0.5 +which 87.5 0.5 +dragged 88.0 0.5 +along 88.5 0.5 +the 89.0 0.5 +floor 89.5 0.5 +because 90.0 0.5 +its 90.5 0.5 +arms 91.0 0.5 +were 91.5 0.5 +so 92.0 0.5 +long. 92.5 0.5 ++ 93.0 0.5 +The 93.5 0.5 +troll 94.0 0.5 +stopped 94.5 0.5 +next 95.0 0.5 +to 95.5 0.5 +a 96.0 0.5 +doorway 96.5 0.5 +and 97.0 0.5 +peered 97.5 0.5 +inside. 98.0 0.5 +It 98.5 0.5 +waggled 99.0 0.5 +its 99.5 0.5 +long 100.0 0.5 +ears, 100.5 0.5 +making 101.0 0.5 +up 101.5 0.5 +its 102.0 0.5 +tiny 102.5 0.5 +mind, 103.0 0.5 +then 103.5 0.5 +slouched 104.0 0.5 +slowly 104.5 0.5 +into 105.0 0.5 +the 105.5 0.5 +room. 106.0 0.5 ++ 106.5 0.5 +"""The" 107.0 0.5 +keys 107.5 0.5 +in 108.0 0.5 +the 108.5 0.5 +"lock,""" 109.0 0.5 +Harry 109.5 0.5 +muttered. 110.0 0.5 +"""We" 110.5 0.5 +could 111.0 0.5 +lock 111.5 0.5 +it 112.0 0.5 +"in.""" 112.5 0.5 ++ 113.0 0.5 +"""Good" 113.5 0.5 +"idea,""" 114.0 0.5 +said 114.5 0.5 +Ron 115.0 0.5 +nervously. 115.5 0.5 ++ 116.0 0.5 +They 116.5 0.5 +edged 117.0 0.5 +toward 117.5 0.5 +the 118.0 0.5 +open 118.5 0.5 +door, 119.0 0.5 +mouths 119.5 0.5 +dry, 120.0 0.5 +praying 120.5 0.5 +the 121.0 0.5 +troll 121.5 0.5 +wasn't 122.0 0.5 +about 122.5 0.5 +to 123.0 0.5 +come 123.5 0.5 +out 124.0 0.5 +of 124.5 0.5 +it. 125.0 0.5 +With 125.5 0.5 +one 126.0 0.5 +great 126.5 0.5 +leap, 127.0 0.5 +Harry 127.5 0.5 +managed 128.0 0.5 +to 128.5 0.5 +grab 129.0 0.5 +the 129.5 0.5 +key, 130.0 0.5 +slam 130.5 0.5 +the 131.0 0.5 +door, 131.5 0.5 +and 132.0 0.5 +lock 132.5 0.5 +it. 133.0 0.5 ++ 133.5 0.5 +"""Yes!""" italic 134.0 0.5 ++ 134.5 0.5 +Flushed 135.0 0.5 +with 135.5 0.5 +their 136.0 0.5 +victory, 136.5 0.5 +they 137.0 0.5 +started 137.5 0.5 +to 138.0 0.5 +run 138.5 0.5 +back 139.0 0.5 +up 139.5 0.5 +the 140.0 0.5 +passage, 140.5 0.5 +but 141.0 0.5 +as 141.5 0.5 +they 142.0 0.5 +reached 142.5 0.5 +the 143.0 0.5 +corner 143.5 0.5 +they 144.0 0.5 +heard 144.5 0.5 +something 145.0 0.5 +that 145.5 0.5 +made 146.0 0.5 +their 146.5 0.5 +hearts 147.0 0.5 +stop 147.5 0.5 +-- 148.0 0.5 +a 148.5 0.5 +high, 149.0 0.5 +petrified 149.5 0.5 +scream 150.0 0.5 +-- 150.5 0.5 +and 151.0 0.5 +it 151.5 0.5 +was 152.0 0.5 +coming 152.5 0.5 +from 153.0 0.5 +the 153.5 0.5 +chamber 154.0 0.5 +they'd 154.5 0.5 +just 155.0 0.5 +chained 155.5 0.5 +up. 156.0 0.5 ++ 156.5 0.5 +"""Oh," 157.0 0.5 +"no,""" 157.5 0.5 +said 158.0 0.5 +Ron, 158.5 0.5 +pale 159.0 0.5 +as 159.5 0.5 +the 160.0 0.5 +Bloody 160.5 0.5 +Baron. 161.0 0.5 ++ 161.5 0.5 +"""It's" 162.0 0.5 +the 162.5 0.5 +girls' 163.0 0.5 +"bathroom!""" 163.5 0.5 +Harry 164.0 0.5 +gasped. 164.5 0.5 ++ 165.0 0.5 +"""Hermione!""" italic 165.5 0.5 +they 166.0 0.5 +said 166.5 0.5 +together. 167.0 0.5 ++ 167.5 0.5 +It 168.0 0.5 +was 168.5 0.5 +the 169.0 0.5 +last 169.5 0.5 +thing 170.0 0.5 +they 170.5 0.5 +wanted 171.0 0.5 +to 171.5 0.5 +do, 172.0 0.5 +but 172.5 0.5 +what 173.0 0.5 +choice 173.5 0.5 +did 174.0 0.5 +they 174.5 0.5 +have? 175.0 0.5 +Wheeling 175.5 0.5 +around, 176.0 0.5 +they 176.5 0.5 +sprinted 177.0 0.5 +back 177.5 0.5 +to 178.0 0.5 +the 178.5 0.5 +door 179.0 0.5 +and 179.5 0.5 +turned 180.0 0.5 +the 180.5 0.5 +key, 181.0 0.5 +fumbling 181.5 0.5 +in 182.0 0.5 +their 182.5 0.5 +panic. 183.0 0.5 +Harry 183.5 0.5 +pulled 184.0 0.5 +the 184.5 0.5 +door 185.0 0.5 +open 185.5 0.5 +and 186.0 0.5 +they 186.5 0.5 +ran 187.0 0.5 +inside. 187.5 0.5 ++ 188.0 0.5 +Hermione 188.5 0.5 +Granger 189.0 0.5 +was 189.5 0.5 +shrinking 190.0 0.5 +against 190.5 0.5 +the 191.0 0.5 +wall 191.5 0.5 +opposite, 192.0 0.5 +looking 192.5 0.5 +as 193.0 0.5 +if 193.5 0.5 +she 194.0 0.5 +was 194.5 0.5 +about 195.0 0.5 +to 195.5 0.5 +faint. 196.0 0.5 +The 196.5 0.5 +troll 197.0 0.5 +was 197.5 0.5 +advancing 198.0 0.5 +on 198.5 0.5 +her, 199.0 0.5 +knocking 199.5 0.5 +the 200.0 0.5 +sinks 200.5 0.5 +off 201.0 0.5 +the 201.5 0.5 +walls 202.0 0.5 +as 202.5 0.5 +it 203.0 0.5 +went. 203.5 0.5 ++ 204.0 0.5 +"""Confuse" 204.5 0.5 +"it!""" 205.0 0.5 +Harry 205.5 0.5 +said 206.0 0.5 +desperately 206.5 0.5 +to 207.0 0.5 +Ron, 207.5 0.5 +and, 208.0 0.5 +seizing 208.5 0.5 +a 209.0 0.5 +tap, 209.5 0.5 +he 210.0 0.5 +threw 210.5 0.5 +it 211.0 0.5 +as 211.5 0.5 +hard 212.0 0.5 +as 212.5 0.5 +he 213.0 0.5 +could 213.5 0.5 +against 214.0 0.5 +the 214.5 0.5 +wall. 215.0 0.5 ++ 215.5 0.5 +The 216.0 0.5 +troll 216.5 0.5 +stopped 217.0 0.5 +a 217.5 0.5 +few 218.0 0.5 +feet 218.5 0.5 +from 219.0 0.5 +Hermione. 219.5 0.5 +It 220.0 0.5 +lumbered 220.5 0.5 +around, 221.0 0.5 +blinking 221.5 0.5 +stupidly, 222.0 0.5 +to 222.5 0.5 +see 223.0 0.5 +what 223.5 0.5 +had 224.0 0.5 +made 224.5 0.5 +the 225.0 0.5 +noise. 225.5 0.5 +Its 226.0 0.5 +mean 226.5 0.5 +little 227.0 0.5 +eyes 227.5 0.5 +saw 228.0 0.5 +Harry. 228.5 0.5 +It 229.0 0.5 +hesitated, 229.5 0.5 +then 230.0 0.5 +made 230.5 0.5 +for 231.0 0.5 +him 231.5 0.5 +instead, 232.0 0.5 +lifting 232.5 0.5 +its 233.0 0.5 +club 233.5 0.5 +as 234.0 0.5 +it 234.5 0.5 +went. 235.0 0.5 ++ 235.5 0.5 +"""Oy," 236.0 0.5 +"pea-brain!""" 236.5 0.5 +yelled 237.0 0.5 +Ron 237.5 0.5 +from 238.0 0.5 +the 238.5 0.5 +other 239.0 0.5 +side 239.5 0.5 +of 240.0 0.5 +the 240.5 0.5 +chamber, 241.0 0.5 +and 241.5 0.5 +he 242.0 0.5 +threw 242.5 0.5 +a 243.0 0.5 +metal 243.5 0.5 +pipe 244.0 0.5 +at 244.5 0.5 +it. 245.0 0.5 +The 245.5 0.5 +troll 246.0 0.5 +didn't 246.5 0.5 +even 247.0 0.5 +seem 247.5 0.5 +to 248.0 0.5 +notice 248.5 0.5 +the 249.0 0.5 +pipe 249.5 0.5 +hitting 250.0 0.5 +its 250.5 0.5 +shoulder, 251.0 0.5 +but 251.5 0.5 +it 252.0 0.5 +heard 252.5 0.5 +the 253.0 0.5 +yell 253.5 0.5 +and 254.0 0.5 +paused 254.5 0.5 +again, 255.0 0.5 +turning 255.5 0.5 +its 256.0 0.5 +ugly 256.5 0.5 +snout 257.0 0.5 +toward 257.5 0.5 +Ron 258.0 0.5 +instead, 258.5 0.5 +giving 259.0 0.5 +Harry 259.5 0.5 +time 260.0 0.5 +to 260.5 0.5 +run 261.0 0.5 +around 261.5 0.5 +it. 262.0 0.5 ++ 262.5 0.5 +"""Come" 263.0 0.5 +on, 263.5 0.5 +run, 264.0 0.5 +"run!""" italic 264.5 0.5 +Harry 265.0 0.5 +yelled 265.5 0.5 +at 266.0 0.5 +Hermione, 266.5 0.5 +trying 267.0 0.5 +to 267.5 0.5 +pull 268.0 0.5 +her 268.5 0.5 +toward 269.0 0.5 +the 269.5 0.5 +door, 270.0 0.5 +but 270.5 0.5 +she 271.0 0.5 +couldn't 271.5 0.5 +move, 272.0 0.5 +she 272.5 0.5 +was 273.0 0.5 +still 273.5 0.5 +flat 274.0 0.5 +against 274.5 0.5 +the 275.0 0.5 +wall, 275.5 0.5 +her 276.0 0.5 +mouth 276.5 0.5 +open 277.0 0.5 +with 277.5 0.5 +terror. 278.0 0.5 ++ 278.5 0.5 +The 279.0 0.5 +shouting 279.5 0.5 +and 280.0 0.5 +the 280.5 0.5 +echoes 281.0 0.5 +seemed 281.5 0.5 +to 282.0 0.5 +be 282.5 0.5 +driving 283.0 0.5 +the 283.5 0.5 +troll 284.0 0.5 +berserk. 284.5 0.5 +It 285.0 0.5 +roared 285.5 0.5 +again 286.0 0.5 +and 286.5 0.5 +started 287.0 0.5 +toward 287.5 0.5 +Ron, 288.0 0.5 +who 288.5 0.5 +was 289.0 0.5 +nearest 289.5 0.5 +and 290.0 0.5 +had 290.5 0.5 +no 291.0 0.5 +way 291.5 0.5 +to 292.0 0.5 +escape. 292.5 0.5 ++ 293.0 0.5 +Harry 293.5 0.5 +then 294.0 0.5 +did 294.5 0.5 +something 295.0 0.5 +that 295.5 0.5 +was 296.0 0.5 +both 296.5 0.5 +very 297.0 0.5 +brave 297.5 0.5 +and 298.0 0.5 +very 298.5 0.5 +stupid: 299.0 0.5 +He 299.5 0.5 +took 300.0 0.5 +a 300.5 0.5 +great 301.0 0.5 +running 301.5 0.5 +jump 302.0 0.5 +and 302.5 0.5 +managed 303.0 0.5 +to 303.5 0.5 +fasten 304.0 0.5 +his 304.5 0.5 +arms 305.0 0.5 +around 305.5 0.5 +the 306.0 0.5 +troll's 306.5 0.5 +neck 307.0 0.5 +from 307.5 0.5 +behind. 308.0 0.5 +The 308.5 0.5 +troll 309.0 0.5 +couldn't 309.5 0.5 +feel 310.0 0.5 +Harry 310.5 0.5 +hanging 311.0 0.5 +there, 311.5 0.5 +but 312.0 0.5 +even 312.5 0.5 +a 313.0 0.5 +troll 313.5 0.5 +will 314.0 0.5 +notice 314.5 0.5 +if 315.0 0.5 +you 315.5 0.5 +stick 316.0 0.5 +a 316.5 0.5 +long 317.0 0.5 +bit 317.5 0.5 +of 318.0 0.5 +wood 318.5 0.5 +up 319.0 0.5 +its 319.5 0.5 +nose, 320.0 0.5 +and 320.5 0.5 +Harry's 321.0 0.5 +wand 321.5 0.5 +had 322.0 0.5 +still 322.5 0.5 +been 323.0 0.5 +in 323.5 0.5 +his 324.0 0.5 +hand 324.5 0.5 +when 325.0 0.5 +he'd 325.5 0.5 +jumped 326.0 0.5 +-- 326.5 0.5 +it 327.0 0.5 +had 327.5 0.5 +gone 328.0 0.5 +straight 328.5 0.5 +up 329.0 0.5 +one 329.5 0.5 +of 330.0 0.5 +the 330.5 0.5 +troll's 331.0 0.5 +nostrils. 331.5 0.5 ++ 332.0 0.5 +Howling 332.5 0.5 +with 333.0 0.5 +pain, 333.5 0.5 +the 334.0 0.5 +troll 334.5 0.5 +twisted 335.0 0.5 +and 335.5 0.5 +flailed 336.0 0.5 +its 336.5 0.5 +club, 337.0 0.5 +with 337.5 0.5 +Harry 338.0 0.5 +clinging 338.5 0.5 +on 339.0 0.5 +for 339.5 0.5 +dear 340.0 0.5 +life; 340.5 0.5 +any 341.0 0.5 +second, 341.5 0.5 +the 342.0 0.5 +troll 342.5 0.5 +was 343.0 0.5 +going 343.5 0.5 +to 344.0 0.5 +rip 344.5 0.5 +him 345.0 0.5 +off 345.5 0.5 +or 346.0 0.5 +catch 346.5 0.5 +him 347.0 0.5 +a 347.5 0.5 +terrible 348.0 0.5 +blow 348.5 0.5 +with 349.0 0.5 +the 349.5 0.5 +club. 350.0 0.5 ++ 350.5 0.5 +Hermione 351.0 0.5 +had 351.5 0.5 +sunk 352.0 0.5 +to 352.5 0.5 +the 353.0 0.5 +floor 353.5 0.5 +in 354.0 0.5 +fright; 354.5 0.5 +Ron 355.0 0.5 +pulled 355.5 0.5 +out 356.0 0.5 +his 356.5 0.5 +own 357.0 0.5 +wand 357.5 0.5 +-- 358.0 0.5 +not 358.5 0.5 +knowing 359.0 0.5 +what 359.5 0.5 +he 360.0 0.5 +was 360.5 0.5 +going 361.0 0.5 +to 361.5 0.5 +do 362.0 0.5 +he 362.5 0.5 +heard 363.0 0.5 +himself 363.5 0.5 +cry 364.0 0.5 +the 364.5 0.5 +first 365.0 0.5 +spell 365.5 0.5 +that 366.0 0.5 +came 366.5 0.5 +into 367.0 0.5 +his 367.5 0.5 +head: 368.0 0.5 +"""Wingardium" italic 368.5 0.5 +"Leviosa!""" italic 369.0 0.5 ++ 369.5 0.5 +The 370.0 0.5 +club 370.5 0.5 +flew 371.0 0.5 +suddenly 371.5 0.5 +out 372.0 0.5 +of 372.5 0.5 +the 373.0 0.5 +troll's 373.5 0.5 +hand, 374.0 0.5 +rose 374.5 0.5 +high, 375.0 0.5 +high 375.5 0.5 +up 376.0 0.5 +into 376.5 0.5 +the 377.0 0.5 +air, 377.5 0.5 +turned 378.0 0.5 +slowly 378.5 0.5 +over 379.0 0.5 +-- 379.5 0.5 +and 380.0 0.5 +dropped, 380.5 0.5 +with 381.0 0.5 +a 381.5 0.5 +sickening 382.0 0.5 +crack, 382.5 0.5 +onto 383.0 0.5 +its 383.5 0.5 +owner's 384.0 0.5 +head. 384.5 0.5 +The 385.0 0.5 +troll 385.5 0.5 +swayed 386.0 0.5 +on 386.5 0.5 +the 387.0 0.5 +spot 387.5 0.5 +and 388.0 0.5 +then 388.5 0.5 +fell 389.0 0.5 +flat 389.5 0.5 +on 390.0 0.5 +its 390.5 0.5 +face, 391.0 0.5 +with 391.5 0.5 +a 392.0 0.5 +thud 392.5 0.5 +that 393.0 0.5 +made 393.5 0.5 +the 394.0 0.5 +whole 394.5 0.5 +room 395.0 0.5 +tremble. 395.5 0.5 ++ 396.0 0.5 +Harry 396.5 0.5 +got 397.0 0.5 +to 397.5 0.5 +his 398.0 0.5 +feet. 398.5 0.5 +He 399.0 0.5 +was 399.5 0.5 +shaking 400.0 0.5 +and 400.5 0.5 +out 401.0 0.5 +of 401.5 0.5 +breath. 402.0 0.5 +Ron 402.5 0.5 +was 403.0 0.5 +standing 403.5 0.5 +there 404.0 0.5 +with 404.5 0.5 +his 405.0 0.5 +wand 405.5 0.5 +still 406.0 0.5 +raised, 406.5 0.5 +staring 407.0 0.5 +at 407.5 0.5 +what 408.0 0.5 +he 408.5 0.5 +had 409.0 0.5 +done. 409.5 0.5 ++ 410.0 0.5 +It 410.5 0.5 +was 411.0 0.5 +Hermione 411.5 0.5 +who 412.0 0.5 +spoke 412.5 0.5 +first. 413.0 0.5 ++ 413.5 0.5 +"""Is" 414.0 0.5 +it 414.5 0.5 +-- 415.0 0.5 +"dead?""" 415.5 0.5 ++ 416.0 0.5 +I 416.5 0.5 +don't 417.0 0.5 +think 417.5 0.5 +"so,""" 418.0 0.5 +said 418.5 0.5 +Harry, 419.0 0.5 +I 419.5 0.5 +think 420.0 0.5 +it's 420.5 0.5 +just 421.0 0.5 +been 421.5 0.5 +knocked 422.0 0.5 +"out.""" 422.5 0.5 ++ 423.0 0.5 +He 423.5 0.5 +bent 424.0 0.5 +down 424.5 0.5 +and 425.0 0.5 +pulled 425.5 0.5 +his 426.0 0.5 +wand 426.5 0.5 +out 427.0 0.5 +of 427.5 0.5 +the 428.0 0.5 +troll's 428.5 0.5 +nose. 429.0 0.5 +It 429.5 0.5 +was 430.0 0.5 +covered 430.5 0.5 +in 431.0 0.5 +what 431.5 0.5 +looked 432.0 0.5 +like 432.5 0.5 +lumpy 433.0 0.5 +gray 433.5 0.5 +glue. 434.0 0.5 ++ 434.5 0.5 +"""Urgh" 435.0 0.5 +-- 435.5 0.5 +troll 436.0 0.5 +"boogers.""" 436.5 0.5 ++ 437.0 0.5 +He 437.5 0.5 +wiped 438.0 0.5 +it 438.5 0.5 +on 439.0 0.5 +the 439.5 0.5 +troll's 440.0 0.5 +trousers. 440.5 0.5 ++ 441.0 0.5 +A 441.5 0.5 +sudden 442.0 0.5 +slamming 442.5 0.5 +and 443.0 0.5 +loud 443.5 0.5 +footsteps 444.0 0.5 +made 444.5 0.5 +the 445.0 0.5 +three 445.5 0.5 +of 446.0 0.5 +them 446.5 0.5 +look 447.0 0.5 +up. 447.5 0.5 +They 448.0 0.5 +hadn't 448.5 0.5 +realized 449.0 0.5 +what 449.5 0.5 +a 450.0 0.5 +racket 450.5 0.5 +they 451.0 0.5 +had 451.5 0.5 +been 452.0 0.5 +making, 452.5 0.5 +but 453.0 0.5 +of 453.5 0.5 +course, 454.0 0.5 +someone 454.5 0.5 +downstairs 455.0 0.5 +must 455.5 0.5 +have 456.0 0.5 +heard 456.5 0.5 +the 457.0 0.5 +crashes 457.5 0.5 +and 458.0 0.5 +the 458.5 0.5 +troll's 459.0 0.5 +roars. 459.5 0.5 +A 460.0 0.5 +moment 460.5 0.5 +later, 461.0 0.5 +Professor 461.5 0.5 +McGonagall 462.0 0.5 +had 462.5 0.5 +come 463.0 0.5 +bursting 463.5 0.5 +into 464.0 0.5 +the 464.5 0.5 +room, 465.0 0.5 +closely 465.5 0.5 +followed 466.0 0.5 +by 466.5 0.5 +Snape, 467.0 0.5 +with 467.5 0.5 +Quirrell 468.0 0.5 +bringing 468.5 0.5 +up 469.0 0.5 +the 469.5 0.5 +rear. 470.0 0.5 +Quirrell 470.5 0.5 +took 471.0 0.5 +one 471.5 0.5 +look 472.0 0.5 +at 472.5 0.5 +the 473.0 0.5 +troll, 473.5 0.5 +let 474.0 0.5 +out 474.5 0.5 +a 475.0 0.5 +faint 475.5 0.5 +whimper, 476.0 0.5 +and 476.5 0.5 +sat 477.0 0.5 +quickly 477.5 0.5 +down 478.0 0.5 +on 478.5 0.5 +a 479.0 0.5 +toilet, 479.5 0.5 +clutching 480.0 0.5 +his 480.5 0.5 +heart. 481.0 0.5 ++ 481.5 0.5 +Snape 482.0 0.5 +bent 482.5 0.5 +over 483.0 0.5 +the 483.5 0.5 +troll. 484.0 0.5 +Professor 484.5 0.5 +McGonagall 485.0 0.5 +was 485.5 0.5 +looking 486.0 0.5 +at 486.5 0.5 +Ron 487.0 0.5 +and 487.5 0.5 +Harry. 488.0 0.5 +Harry 488.5 0.5 +had 489.0 0.5 +never 489.5 0.5 +seen 490.0 0.5 +her 490.5 0.5 +look 491.0 0.5 +so 491.5 0.5 +angry. 492.0 0.5 +Her 492.5 0.5 +lips 493.0 0.5 +were 493.5 0.5 +white. 494.0 0.5 +Hopes 494.5 0.5 +of 495.0 0.5 +winning 495.5 0.5 +fifty 496.0 0.5 +points 496.5 0.5 +for 497.0 0.5 +Gryffindor 497.5 0.5 +faded 498.0 0.5 +quickly 498.5 0.5 +from 499.0 0.5 +Harry's 499.5 0.5 +mind. 500.0 0.5 ++ 500.5 0.5 +"""What" 501.0 0.5 +on 501.5 0.5 +earth 502.0 0.5 +were 502.5 0.5 +you 503.0 0.5 +thinking 503.5 0.5 +"of?""" 504.0 0.5 +said 504.5 0.5 +Professor 505.0 0.5 +McGonagall, 505.5 0.5 +with 506.0 0.5 +cold 506.5 0.5 +fury 507.0 0.5 +in 507.5 0.5 +her 508.0 0.5 +voice. 508.5 0.5 +Harry 509.0 0.5 +looked 509.5 0.5 +at 510.0 0.5 +Ron, 510.5 0.5 +who 511.0 0.5 +was 511.5 0.5 +still 512.0 0.5 +standing 512.5 0.5 +with 513.0 0.5 +his 513.5 0.5 +wand 514.0 0.5 +in 514.5 0.5 +the 515.0 0.5 +air. 515.5 0.5 +"""You're" 516.0 0.5 +lucky 516.5 0.5 +you 517.0 0.5 +weren't 517.5 0.5 +killed. 518.0 0.5 +Why 518.5 0.5 +aren't 519.0 0.5 +you 519.5 0.5 +in 520.0 0.5 +your 520.5 0.5 +"dormitory?""" 521.0 0.5 ++ 521.5 0.5 +Snape 522.0 0.5 +gave 522.5 0.5 +Harry 523.0 0.5 +a 523.5 0.5 +swift, 524.0 0.5 +piercing 524.5 0.5 +look. 525.0 0.5 +Harry 525.5 0.5 +looked 526.0 0.5 +at 526.5 0.5 +the 527.0 0.5 +floor. 527.5 0.5 +He 528.0 0.5 +wished 528.5 0.5 +Ron 529.0 0.5 +would 529.5 0.5 +put 530.0 0.5 +his 530.5 0.5 +wand 531.0 0.5 +down. 531.5 0.5 ++ 532.0 0.5 +Then 532.5 0.5 +a 533.0 0.5 +small 533.5 0.5 +voice 534.0 0.5 +came 534.5 0.5 +out 535.0 0.5 +of 535.5 0.5 +the 536.0 0.5 +shadows. 536.5 0.5 ++ 537.0 0.5 +"""Please," 537.5 0.5 +Professor 538.0 0.5 +McGonagall 538.5 0.5 +-- 539.0 0.5 +they 539.5 0.5 +were 540.0 0.5 +looking 540.5 0.5 +for 541.0 0.5 +"me.""" 541.5 0.5 ++ 542.0 0.5 +"""Miss" 542.5 0.5 +"Granger!""" 543.0 0.5 ++ 543.5 0.5 +Hermione 544.0 0.5 +had 544.5 0.5 +managed 545.0 0.5 +to 545.5 0.5 +get 546.0 0.5 +to 546.5 0.5 +her 547.0 0.5 +feet 547.5 0.5 +at 548.0 0.5 +last. 548.5 0.5 ++ 549.0 0.5 +I 549.5 0.5 +went 550.0 0.5 +looking 550.5 0.5 +for 551.0 0.5 +the 551.5 0.5 +troll 552.0 0.5 +because 552.5 0.5 +I 553.0 0.5 +-- 553.5 0.5 +I 554.0 0.5 +thought 554.5 0.5 +I 555.0 0.5 +could 555.5 0.5 +deal 556.0 0.5 +with 556.5 0.5 +it 557.0 0.5 +on 557.5 0.5 +my 558.0 0.5 +own 558.5 0.5 +-- 559.0 0.5 +you 559.5 0.5 +know, 560.0 0.5 +because 560.5 0.5 +I've 561.0 0.5 +read 561.5 0.5 +all 562.0 0.5 +about 562.5 0.5 +"them.""" 563.0 0.5 ++ 563.5 0.5 +Ron 564.0 0.5 +dropped 564.5 0.5 +his 565.0 0.5 +wand. 565.5 0.5 +Hermione 566.0 0.5 +Granger, 566.5 0.5 +telling 567.0 0.5 +a 567.5 0.5 +downright 568.0 0.5 +lie 568.5 0.5 +to 569.0 0.5 +a 569.5 0.5 +teacher? 570.0 0.5 ++ 570.5 0.5 +"""If" 571.0 0.5 +they 571.5 0.5 +hadn't 572.0 0.5 +found 572.5 0.5 +me, 573.0 0.5 +I'd 573.5 0.5 +be 574.0 0.5 +dead 574.5 0.5 +now. 575.0 0.5 +Harry 575.5 0.5 +stuck 576.0 0.5 +his 576.5 0.5 +wand 577.0 0.5 +up 577.5 0.5 +its 578.0 0.5 +nose 578.5 0.5 +and 579.0 0.5 +Ron 579.5 0.5 +knocked 580.0 0.5 +it 580.5 0.5 +out 581.0 0.5 +with 581.5 0.5 +its 582.0 0.5 +own 582.5 0.5 +club. 583.0 0.5 +They 583.5 0.5 +didn't 584.0 0.5 +have 584.5 0.5 +time 585.0 0.5 +to 585.5 0.5 +come 586.0 0.5 +and 586.5 0.5 +fetch 587.0 0.5 +anyone. 587.5 0.5 +It 588.0 0.5 +was 588.5 0.5 +about 589.0 0.5 +to 589.5 0.5 +finish 590.0 0.5 +me 590.5 0.5 +off 591.0 0.5 +when 591.5 0.5 +they 592.0 0.5 +"arrived.""" 592.5 0.5 ++ 593.0 0.5 +Harry 593.5 0.5 +and 594.0 0.5 +Ron 594.5 0.5 +tried 595.0 0.5 +to 595.5 0.5 +look 596.0 0.5 +as 596.5 0.5 +though 597.0 0.5 +this 597.5 0.5 +story 598.0 0.5 +wasn't 598.5 0.5 +new 599.0 0.5 +to 599.5 0.5 +them. 600.0 0.5 ++ 600.5 0.5 +"""Well" 601.0 0.5 +-- 601.5 0.5 +in 602.0 0.5 +that 602.5 0.5 +"case...""" 603.0 0.5 +said 603.5 0.5 +Professor 604.0 0.5 +McGonagall, 604.5 0.5 +staring 605.0 0.5 +at 605.5 0.5 +the 606.0 0.5 +three 606.5 0.5 +of 607.0 0.5 +them, 607.5 0.5 +"""Miss" 608.0 0.5 +Granger, 608.5 0.5 +you 609.0 0.5 +foolish 609.5 0.5 +girl, 610.0 0.5 +how 610.5 0.5 +could 611.0 0.5 +you 611.5 0.5 +think 612.0 0.5 +of 612.5 0.5 +tackling 613.0 0.5 +a 613.5 0.5 +mountain 614.0 0.5 +troll 614.5 0.5 +on 615.0 0.5 +your 615.5 0.5 +"own?""" 616.0 0.5 ++ 616.5 0.5 +Hermione 617.0 0.5 +hung 617.5 0.5 +her 618.0 0.5 +head. 618.5 0.5 +Harry 619.0 0.5 +was 619.5 0.5 +speechless. 620.0 0.5 +Hermione 620.5 0.5 +was 621.0 0.5 +the 621.5 0.5 +last 622.0 0.5 +person 622.5 0.5 +to 623.0 0.5 +do 623.5 0.5 +anything 624.0 0.5 +against 624.5 0.5 +the 625.0 0.5 +rules, 625.5 0.5 +and 626.0 0.5 +here 626.5 0.5 +she 627.0 0.5 +was, 627.5 0.5 +pretending 628.0 0.5 +she 628.5 0.5 +had, 629.0 0.5 +to 629.5 0.5 +get 630.0 0.5 +them 630.5 0.5 +out 631.0 0.5 +of 631.5 0.5 +trouble. 632.0 0.5 +It 632.5 0.5 +was 633.0 0.5 +as 633.5 0.5 +if 634.0 0.5 +Snape 634.5 0.5 +had 635.0 0.5 +started 635.5 0.5 +handing 636.0 0.5 +out 636.5 0.5 +sweets. 637.0 0.5 ++ 637.5 0.5 +"""Miss" 638.0 0.5 +Granger, 638.5 0.5 +five 639.0 0.5 +points 639.5 0.5 +will 640.0 0.5 +be 640.5 0.5 +taken 641.0 0.5 +from 641.5 0.5 +Gryffindor 642.0 0.5 +for 642.5 0.5 +"this,""" 643.0 0.5 +said 643.5 0.5 +Professor 644.0 0.5 +McGonagall. 644.5 0.5 +"""I'm" 645.0 0.5 +very 645.5 0.5 +disappointed 646.0 0.5 +in 646.5 0.5 +you. 647.0 0.5 +If 647.5 0.5 +you're 648.0 0.5 +not 648.5 0.5 +hurt 649.0 0.5 +at 649.5 0.5 +all, 650.0 0.5 +you'd 650.5 0.5 +better 651.0 0.5 +get 651.5 0.5 +off 652.0 0.5 +to 652.5 0.5 +Gryffindor 653.0 0.5 +tower. 653.5 0.5 +Students 654.0 0.5 +are 654.5 0.5 +finishing 655.0 0.5 +the 655.5 0.5 +feast 656.0 0.5 +in 656.5 0.5 +their 657.0 0.5 +"houses.""" 657.5 0.5 ++ 658.0 0.5 +Hermione 658.5 0.5 +left. 659.0 0.5 ++ 659.5 0.5 +Professor 660.0 0.5 +McGonagall 660.5 0.5 +turned 661.0 0.5 +to 661.5 0.5 +Harry 662.0 0.5 +and 662.5 0.5 +Ron. 663.0 0.5 ++ 663.5 0.5 +"""Well," 664.0 0.5 +I 664.5 0.5 +still 665.0 0.5 +say 665.5 0.5 +you 666.0 0.5 +were 666.5 0.5 +lucky, 667.0 0.5 +but 667.5 0.5 +not 668.0 0.5 +many 668.5 0.5 +first 669.0 0.5 +years 669.5 0.5 +could 670.0 0.5 +have 670.5 0.5 +taken 671.0 0.5 +on 671.5 0.5 +a 672.0 0.5 +full-grown 672.5 0.5 +mountain 673.0 0.5 +troll. 673.5 0.5 +You 674.0 0.5 +each 674.5 0.5 +win 675.0 0.5 +Gryffindor 675.5 0.5 +five 676.0 0.5 +points. 676.5 0.5 +Professor 677.0 0.5 +Dumbledore 677.5 0.5 +will 678.0 0.5 +be 678.5 0.5 +informed 679.0 0.5 +of 679.5 0.5 +this. 680.0 0.5 +You 680.5 0.5 +may 681.0 0.5 +"go.""" 681.5 0.5 ++ 682.0 0.5 +They 682.5 0.5 +hurried 683.0 0.5 +out 683.5 0.5 +of 684.0 0.5 +the 684.5 0.5 +chamber 685.0 0.5 +and 685.5 0.5 +didn't 686.0 0.5 +speak 686.5 0.5 +at 687.0 0.5 +all 687.5 0.5 +until 688.0 0.5 +they 688.5 0.5 +had 689.0 0.5 +climbed 689.5 0.5 +two 690.0 0.5 +floors 690.5 0.5 +up. 691.0 0.5 +It 691.5 0.5 +was 692.0 0.5 +a 692.5 0.5 +relief 693.0 0.5 +to 693.5 0.5 +be 694.0 0.5 +away 694.5 0.5 +from 695.0 0.5 +the 695.5 0.5 +smell 696.0 0.5 +of 696.5 0.5 +the 697.0 0.5 +troll, 697.5 0.5 +quite 698.0 0.5 +apart 698.5 0.5 +from 699.0 0.5 +anything 699.5 0.5 +else. 700.0 0.5 ++ 700.5 0.5 +"""We" 701.0 0.5 +should 701.5 0.5 +have 702.0 0.5 +gotten 702.5 0.5 +more 703.0 0.5 +than 703.5 0.5 +ten 704.0 0.5 +"points,""" 704.5 0.5 +Ron 705.0 0.5 +grumbled. 705.5 0.5 ++ 706.0 0.5 +"""Five," 706.5 0.5 +you 707.0 0.5 +mean, 707.5 0.5 +once 708.0 0.5 +she's 708.5 0.5 +taken 709.0 0.5 +off 709.5 0.5 +"Hermione's.""" 710.0 0.5 ++ 710.5 0.5 +"""Good" 711.0 0.5 +of 711.5 0.5 +her 712.0 0.5 +to 712.5 0.5 +get 713.0 0.5 +us 713.5 0.5 +out 714.0 0.5 +of 714.5 0.5 +trouble 715.0 0.5 +like 715.5 0.5 +"that,""" 716.0 0.5 +Ron 716.5 0.5 +admitted. 717.0 0.5 +"""Mind" 717.5 0.5 +you, 718.0 0.5 +we 718.5 0.5 +did 719.0 0.5 +save 719.5 0.5 +"her.""" 720.0 0.5 ++ 720.5 0.5 +"""She" 721.0 0.5 +might 721.5 0.5 +not 722.0 0.5 +have 722.5 0.5 +needed 723.0 0.5 +saving 723.5 0.5 +if 724.0 0.5 +we 724.5 0.5 +hadn't 725.0 0.5 +locked 725.5 0.5 +the 726.0 0.5 +thing 726.5 0.5 +in 727.0 0.5 +with 727.5 0.5 +"her,""" 728.0 0.5 +Harry 728.5 0.5 +reminded 729.0 0.5 +him. 729.5 0.5 ++ 730.0 0.5 +They 730.5 0.5 +had 731.0 0.5 +reached 731.5 0.5 +the 732.0 0.5 +portrait 732.5 0.5 +of 733.0 0.5 +the 733.5 0.5 +Fat 734.0 0.5 +Lady. 734.5 0.5 ++ 735.0 0.5 +"""Pig" 735.5 0.5 +"snout,""" 736.0 0.5 +they 736.5 0.5 +said 737.0 0.5 +and 737.5 0.5 +entered. 738.0 0.5 ++ 738.5 0.5 +The 739.0 0.5 +common 739.5 0.5 +room 740.0 0.5 +was 740.5 0.5 +packed 741.0 0.5 +and 741.5 0.5 +noisy. 742.0 0.5 +Everyone 742.5 0.5 +was 743.0 0.5 +eating 743.5 0.5 +the 744.0 0.5 +food 744.5 0.5 +that 745.0 0.5 +had 745.5 0.5 +been 746.0 0.5 +sent 746.5 0.5 +up. 747.0 0.5 +Hermione, 747.5 0.5 +however, 748.0 0.5 +stood 748.5 0.5 +alone 749.0 0.5 +by 749.5 0.5 +the 750.0 0.5 +door, 750.5 0.5 +waiting 751.0 0.5 +for 751.5 0.5 +them. 752.0 0.5 +There 752.5 0.5 +was 753.0 0.5 +a 753.5 0.5 +very 754.0 0.5 +embarrassed 754.5 0.5 +pause. 755.0 0.5 +Then, 755.5 0.5 +none 756.0 0.5 +of 756.5 0.5 +them 757.0 0.5 +looking 757.5 0.5 +at 758.0 0.5 +each 758.5 0.5 +other, 759.0 0.5 +they 759.5 0.5 +all 760.0 0.5 +said 760.5 0.5 +"""Thanks,""" 761.0 0.5 +and 761.5 0.5 +hurried 762.0 0.5 +off 762.5 0.5 +to 763.0 0.5 +get 763.5 0.5 +plates. 764.0 0.5 ++ 764.5 0.5 +But 765.0 0.5 +from 765.5 0.5 +that 766.0 0.5 +moment 766.5 0.5 +on, 767.0 0.5 +Hermione 767.5 0.5 +Granger 768.0 0.5 +became 768.5 0.5 +their 769.0 0.5 +friend. 769.5 0.5 +There 770.0 0.5 +are 770.5 0.5 +some 771.0 0.5 +things 771.5 0.5 +you 772.0 0.5 +can't 772.5 0.5 +share 773.0 0.5 +without 773.5 0.5 +ending 774.0 0.5 +up 774.5 0.5 +liking 775.0 0.5 +each 775.5 0.5 +other, 776.0 0.5 +and 776.5 0.5 +knocking 777.0 0.5 +out 777.5 0.5 +a 778.0 0.5 +twelve-foot 778.5 0.5 +mountain 779.0 0.5 +troll 779.5 0.5 +is 780.0 0.5 +one 780.5 0.5 +of 781.0 0.5 +them. 781.5 0.5 ++ 782.0 0.5 diff --git a/src/sessions/ses-harrypotter.py b/src/sessions/ses-harrypotter.py index 4a4e60de..b1d2cc4e 100644 --- a/src/sessions/ses-harrypotter.py +++ b/src/sessions/ses-harrypotter.py @@ -1,12 +1,11 @@ from ..tasks import language TASKS = [] -for seg_idx in range(1, 2): +for seg_idx in range(1, 8): TASKS.append( language.Reading( - "data/language/triplet/first1000triples.csv", + "data/language/harrypotter/task-harry_run-%d_events.tsv" % seg_idx, name="harrypotter_seg-%d" % seg_idx, ) ) -# ??? volumes each, last run is ??? volumes long diff --git a/src/tasks/language.py b/src/tasks/language.py index 748e6b23..569755ce 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -1,5 +1,6 @@ import os, sys, time from psychopy import visual, core, data, logging, event +from pandas import read_csv from .task_base import Task from ..shared import config @@ -140,7 +141,7 @@ def __init__(self, words_file, word_duration=0.5, cross_duration=20, self.txt_font = txt_font self.txt_size = txt_size self.bg_color = bg_color - self.words_list = data.importConditions(self.words_file) + self.words_list = read_csv(self.words_file, sep='\t') else: raise ValueError("File %s does not exists" % words_file) @@ -182,13 +183,9 @@ def _run(self, exp_win, ctl_win): yield frameN < 2 # Display each word for 0.5s - for word in self.words_list: - italic = False - word = word["target"] - if word[0] == "@": - italic = True - word = word[1:] - txt_stim.text = word + for id_word, word in self.words_list.iterrows(): + italic = word["format"] == "italic" + txt_stim.text = word["word"] txt_stim.italic = italic for frameN in range(int(config.FRAME_RATE * self.word_duration)): @@ -196,3 +193,12 @@ def _run(self, exp_win, ctl_win): if ctl_win: txt_stim.draw(ctl_win) yield frameN < 2 + + # Display a centered cross for 20s + txt_stim.text = "+" + for frameN in range(config.FRAME_RATE * self.cross_duration): + txt_stim.draw(exp_win) + if ctl_win: + txt_stim.draw(ctl_win) + yield frameN < 2 + From 228d4a5e097db9452d32a996e32284ec84bcd5c1 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 6 Apr 2021 09:59:20 -0400 Subject: [PATCH 132/170] implement non-slip code, save flips --- src/sessions/ses-harrypotter.py | 6 +-- src/tasks/language.py | 81 +++++++++++++++++++-------------- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/src/sessions/ses-harrypotter.py b/src/sessions/ses-harrypotter.py index f5f6bd80..45284128 100644 --- a/src/sessions/ses-harrypotter.py +++ b/src/sessions/ses-harrypotter.py @@ -1,11 +1,11 @@ from ..tasks import language TASKS = [] -for seg_idx in range(1, 2): +for run in range(1, 8): TASKS.append( language.Reading( - "data/language/triplet/first1000triples.csv", - name="harrypotter_seg-%d" % seg_idx, + f"data/harrypotter/task-harry_run-{run}_events.tsv", + name=f"harrypotter_run-{run}", cross_duration=2, txt_size=124, ) diff --git a/src/tasks/language.py b/src/tasks/language.py index ea693cec..78057ae0 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -2,7 +2,7 @@ from psychopy import visual, core, data, logging, event from .task_base import Task -from ..shared import config +from ..shared import config, utils STIMULI_DURATION = 4 BASELINE_BEGIN = 5 @@ -130,7 +130,7 @@ class Reading(Task): def __init__(self, words_file, word_duration=0.5, cross_duration=20, txt_color="black", txt_font="Palatino", txt_size=42, - bg_color=(125,125,125), *args, **kwargs): + bg_color=(.5, .5, .5), *args, **kwargs): super().__init__(**kwargs) if os.path.exists(words_file): self.words_file = words_file @@ -140,7 +140,9 @@ def __init__(self, words_file, word_duration=0.5, cross_duration=20, self.txt_font = txt_font self.txt_size = txt_size self.bg_color = bg_color - self.words_list = data.importConditions(self.words_file) + import pandas + self.words_list = pandas.read_csv(words_file, sep="\t") + self.duration = len(self.words_list) else: raise ValueError("File %s does not exists" % words_file) @@ -149,9 +151,12 @@ def _instructions(self, exp_win, ctl_win): exp_win, text=self.instruction, alignText="center", - color="white", - wrapWidth=config.WRAP_WIDTH, + color="black", + wrapWidth=1.2, ) + exp_win.setColor(self.bg_color, "rgb") + if ctl_win: + ctl_win.setColor(self.bg_color, "rgb") for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): screen_text.draw(exp_win) @@ -159,12 +164,9 @@ def _instructions(self, exp_win, ctl_win): screen_text.draw(ctl_win) yield () - def _run(self, exp_win, ctl_win): - exp_win.setColor(self.bg_color, "rgb") - if ctl_win: - ctl_win.setColor(self.bg_color, "rgb") + def _setup(self, exp_win): - txt_stim = visual.TextStim( + self.txt_stim = visual.TextStim( exp_win, text="+", font=self.txt_font, @@ -172,28 +174,41 @@ def _run(self, exp_win, ctl_win): units='pixels', alignText="center", color=self.txt_color, - wrapWidth=config.WRAP_WIDTH, ) - # Display a centered cross for 20s - for frameN in range(config.FRAME_RATE * self.cross_duration): - txt_stim.draw(exp_win) - if ctl_win: - txt_stim.draw(ctl_win) - yield frameN < 2 - - # Display each word for 0.5s - for word in self.words_list: - italic = False - word = word["target"] - if word[0] == "@": - italic = True - word = word[1:] - txt_stim.text = word - txt_stim.italic = italic - - for frameN in range(int(config.FRAME_RATE * self.word_duration)): - txt_stim.draw(exp_win) - if ctl_win: - txt_stim.draw(ctl_win) - yield frameN < 2 + def _run(self, exp_win, ctl_win): + + # Display each word + for trial_n, trial in self.words_list.iterrows(): + self.txt_stim.text = trial["word"] + self.txt_stim.alignText = "center" + self.txt_stim.italic = trial["format"] == 'italic' + self.txt_stim.draw(exp_win) + self.progress_bar.set_description(f"Trial {trial_n}:: {trial['word']}") + utils.wait_until(self.task_timer, trial["onset"] - 1 / config.FRAME_RATE) + yield True # flip + self.words_list.at[trial_n, "onset_flip"] = ( + self._exp_win_last_flip_time - self._exp_win_first_flip_time + ) + if trial_n > 0: + self.words_list.at[trial_n - 1, "offset_flip"] = self.words_list.at[trial_n, "onset_flip"] + self.words_list.at[trial_n -1, "duration_flip"] = ( + self.words_list.at[trial_n - 1, "offset_flip"] + - self.words_list.at[trial_n - 1, "onset_flip"] + ) + utils.wait_until(self.task_timer, trial["onset"]+trial["duration"] - 1 / config.FRAME_RATE) + + def _stop(self, exp_win, ctl_win): + exp_win.setColor((0,0,0), "rgb") + for _ in range(2): + yield True + + def _save(self): + #self.words_list.saveAsWideText(self._generate_unique_filename("events", "tsv") + self.words_list.to_csv( + self._generate_unique_filename("events", "tsv"), + sep = '\t', + index = False, + + ) + return False From 0b7fa3ab9f25ef357d5ac56e344affa6b6463307 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 6 Apr 2021 15:23:03 -0400 Subject: [PATCH 133/170] fix the italic display due to psychopy bug --- src/tasks/language.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/tasks/language.py b/src/tasks/language.py index 9306e3f8..0f616401 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -130,7 +130,7 @@ class Reading(Task): DEFAULT_INSTRUCTION = """You will be presented a text to read word by word.""" def __init__(self, words_file, word_duration=0.5, cross_duration=20, - txt_color="black", txt_font="Palatino", txt_size=42, + txt_color="black", txt_font="Palatino Linotype", txt_size=42, bg_color=(.5, .5, .5), *args, **kwargs): super().__init__(**kwargs) if os.path.exists(words_file): @@ -182,10 +182,9 @@ def _run(self, exp_win, ctl_win): # Display each word for trial_n, trial in self.words_list.iterrows(): self.txt_stim.text = trial["word"] - self.txt_stim.alignText = "center" - self.txt_stim.italic = trial["format"] == 'italic' + self.txt_stim._pygletTextObj.set_style('italic', trial["format"] == "italic") self.txt_stim.draw(exp_win) - self.progress_bar.set_description(f"Trial {trial_n}:: {trial['word']}") + self.progress_bar.set_description(f"Trial {trial_n}:: {trial['word']} {trial['format']}") utils.wait_until(self.task_timer, trial["onset"] - 1 / config.FRAME_RATE) yield True # flip self.words_list.at[trial_n, "onset_flip"] = ( From 6b9d33f7ed320eb247a76895a2cfed99d9ea438b Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 7 Apr 2021 08:17:00 -0400 Subject: [PATCH 134/170] add updated hp tsv files --- .../harrypotter/task-harry_run-1_events.tsv | 27 ++++----- .../harrypotter/task-harry_run-2_events.tsv | 55 +++++++++--------- .../harrypotter/task-harry_run-3_events.tsv | 21 +++---- .../harrypotter/task-harry_run-4_events.tsv | 57 ++++++++++--------- .../harrypotter/task-harry_run-5_events.tsv | 23 ++++---- .../harrypotter/task-harry_run-6_events.tsv | 19 ++++--- .../harrypotter/task-harry_run-7_events.tsv | 29 +++++----- 7 files changed, 119 insertions(+), 112 deletions(-) diff --git a/data/language/harrypotter/task-harry_run-1_events.tsv b/data/language/harrypotter/task-harry_run-1_events.tsv index be2e3ce5..cbba7e5c 100644 --- a/data/language/harrypotter/task-harry_run-1_events.tsv +++ b/data/language/harrypotter/task-harry_run-1_events.tsv @@ -1,4 +1,5 @@ word format onset duration ++ 0 10 Harry 10.0 0.5 had 10.5 0.5 never 11.0 0.5 @@ -69,7 +70,7 @@ be 43.0 0.5 starting 43.5 0.5 on 44.0 0.5 Thursday 44.5 0.5 --- 45.0 0.5 +— 45.0 0.5 and 45.5 0.5 Gryffindor 46.0 0.5 and 46.5 0.5 @@ -362,7 +363,7 @@ out 189.5 0.5 of 190.0 0.5 a 190.5 0.5 book 191.0 0.5 --- 191.5 0.5 +— 191.5 0.5 not 192.0 0.5 that 192.5 0.5 she 193.0 0.5 @@ -517,7 +518,7 @@ knows 267.0 0.5 I 267.5 0.5 forget 268.0 0.5 things 268.5 0.5 --- 269.0 0.5 +— 269.0 0.5 this 269.5 0.5 tells 270.0 0.5 you 270.5 0.5 @@ -540,7 +541,7 @@ if 278.5 0.5 it 279.0 0.5 turns 279.5 0.5 red 280.0 0.5 --- 280.5 0.5 +— 280.5 0.5 oh 281.0 0.5 "...""" 281.5 0.5 His 282.0 0.5 @@ -1000,11 +1001,11 @@ slightly. 508.5 0.5 On 509.0 0.5 my 509.5 0.5 whistle 510.0 0.5 --- 510.5 0.5 +— 510.5 0.5 three 511.0 0.5 --- 511.5 0.5 +— 511.5 0.5 two 512.0 0.5 -"--""" 512.5 0.5 +"—""" 512.5 0.5 + 513.0 0.5 But 513.5 0.5 Neville, 514.0 0.5 @@ -1050,10 +1051,10 @@ out 533.5 0.5 of 534.0 0.5 a 534.5 0.5 bottle 535.0 0.5 --- 535.5 0.5 +— 535.5 0.5 twelve 536.0 0.5 feet 536.5 0.5 --- 537.0 0.5 +— 537.0 0.5 twenty 537.5 0.5 feet. 538.0 0.5 Harry 538.5 0.5 @@ -1078,10 +1079,10 @@ off 547.5 0.5 the 548.0 0.5 broom 548.5 0.5 and 549.0 0.5 --- 549.5 0.5 +— 549.5 0.5 + 550.0 0.5 WHAM 550.5 0.5 --- 551.0 0.5 +— 551.0 0.5 a 551.5 0.5 thud 552.0 0.5 and 552.5 0.5 @@ -1141,7 +1142,7 @@ mutter. 579.0 0.5 """Come" 579.5 0.5 on, 580.0 0.5 boy 580.5 0.5 --- 581.0 0.5 +— 581.0 0.5 it's 581.5 0.5 all 582.0 0.5 right, 582.5 0.5 @@ -1301,4 +1302,4 @@ he 659.0 0.5 held 659.5 0.5 it 660.0 0.5 up. 660.5 0.5 -+ 661.0 0.5 ++ 661.0 16 diff --git a/data/language/harrypotter/task-harry_run-2_events.tsv b/data/language/harrypotter/task-harry_run-2_events.tsv index 28fae10e..3f74fee1 100644 --- a/data/language/harrypotter/task-harry_run-2_events.tsv +++ b/data/language/harrypotter/task-harry_run-2_events.tsv @@ -1,4 +1,5 @@ word format onset duration ++ 0 10 """Give" 10.0 0.5 that 10.5 0.5 here, 11.0 0.5 @@ -25,10 +26,10 @@ for 21.0 0.5 Longbottom 21.5 0.5 to 22.0 0.5 find 22.5 0.5 --- 23.0 0.5 +— 23.0 0.5 how 23.5 0.5 about 24.0 0.5 --- 24.5 0.5 +— 24.5 0.5 up 25.0 0.5 a 25.5 0.5 "tree?""" 26.0 0.5 @@ -89,7 +90,7 @@ us 53.0 0.5 not 53.5 0.5 to 54.0 0.5 move 54.5 0.5 --- 55.0 0.5 +— 55.0 0.5 you'll 55.5 0.5 get 56.0 0.5 us 56.5 0.5 @@ -133,7 +134,7 @@ whipped 75.0 0.5 out 75.5 0.5 behind 76.0 0.5 him 76.5 0.5 --- 77.0 0.5 +— 77.0 0.5 and 77.5 0.5 in 78.0 0.5 a 78.5 0.5 @@ -152,7 +153,7 @@ do 84.5 0.5 without 85.0 0.5 being 85.5 0.5 taught 86.0 0.5 --- 86.5 0.5 +— 86.5 0.5 this 87.0 0.5 was 87.5 0.5 easy, 88.0 0.5 @@ -355,7 +356,7 @@ his 186.0 0.5 broom 186.5 0.5 handle 187.0 0.5 down 187.5 0.5 --- 188.0 0.5 +— 188.0 0.5 next 188.5 0.5 second 189.0 0.5 he 189.5 0.5 @@ -369,7 +370,7 @@ dive, 193.0 0.5 racing 193.5 0.5 the 194.0 0.5 ball 194.5 0.5 --- 195.0 0.5 +— 195.0 0.5 wind 195.5 0.5 whistled 196.0 0.5 in 196.5 0.5 @@ -382,13 +383,13 @@ screams 199.5 0.5 of 200.0 0.5 people 200.5 0.5 watching 201.0 0.5 --- 201.5 0.5 +— 201.5 0.5 he 202.0 0.5 stretched 202.5 0.5 out 203.0 0.5 his 203.5 0.5 hand 204.0 0.5 --- 204.5 0.5 +— 204.5 0.5 a 205.0 0.5 foot 205.5 0.5 from 206.0 0.5 @@ -446,14 +447,14 @@ feet, 231.5 0.5 trembling. 232.0 0.5 + 232.5 0.5 """Never" italic 233.0 0.5 --- 233.5 0.5 +— 233.5 0.5 in 234.0 0.5 all 234.5 0.5 my 235.0 0.5 time 235.5 0.5 at 236.0 0.5 Hogwarts 236.5 0.5 -"--""" 237.0 0.5 +"—""" 237.0 0.5 Professor 237.5 0.5 McGonagall 238.0 0.5 was 238.5 0.5 @@ -466,34 +467,34 @@ her 241.5 0.5 glasses 242.0 0.5 flashed 242.5 0.5 furiously, 243.0 0.5 -"""--" 243.5 0.5 +"""—" 243.5 0.5 how 244.0 0.5 dare italic 244.5 0.5 you 245.0 0.5 --- 245.5 0.5 +— 245.5 0.5 might 246.0 0.5 have 246.5 0.5 broken 247.0 0.5 your 247.5 0.5 neck 248.0 0.5 -"--""" 248.5 0.5 +"—""" 248.5 0.5 + 249.0 0.5 """It" 249.5 0.5 wasn't 250.0 0.5 his 250.5 0.5 fault, 251.0 0.5 Professor 251.5 0.5 -"--""" 252.0 0.5 +"—""" 252.0 0.5 + 252.5 0.5 """Be" 253.0 0.5 quiet, 253.5 0.5 Miss 254.0 0.5 Patil 254.5 0.5 -"--""" 255.0 0.5 +"—""" 255.0 0.5 + 255.5 0.5 """But" 256.0 0.5 Malfoy 256.5 0.5 -"--""" 257.0 0.5 +"—""" 257.0 0.5 """That's" 257.5 0.5 enough, 258.0 0.5 Mr. 258.5 0.5 @@ -835,8 +836,8 @@ this 426.0 0.5 is 426.5 0.5 Oliver 427.0 0.5 Wood. 427.5 0.5 -^Wood 428.0 0.5 --- 428.5 0.5 + Wood 428.0 0.5 +— 428.5 0.5 I've 429.0 0.5 found 429.5 0.5 you 430.0 0.5 @@ -994,9 +995,9 @@ staring 505.5 0.5 at 506.0 0.5 him. 506.5 0.5 """Light" 507.0 0.5 --- 507.5 0.5 +— 507.5 0.5 speedy 508.0 0.5 --- 508.5 0.5 +— 508.5 0.5 we'll 509.0 0.5 have 509.5 0.5 to 510.0 0.5 @@ -1006,7 +1007,7 @@ a 511.5 0.5 decent 512.0 0.5 broom, 512.5 0.5 Professor 513.0 0.5 --- 513.5 0.5 +— 513.5 0.5 a 514.0 0.5 Nimbus 514.5 0.5 Two 515.0 0.5 @@ -1162,7 +1163,7 @@ said. 589.0 0.5 first 590.0 0.5 years 590.5 0.5 never italic 591.0 0.5 --- 591.5 0.5 +— 591.5 0.5 you 592.0 0.5 must 592.5 0.5 be 593.0 0.5 @@ -1172,9 +1173,9 @@ House 594.5 0.5 player 595.0 0.5 in 595.5 0.5 about 596.0 0.5 -"--""" 596.5 0.5 +"—""" 596.5 0.5 + 597.0 0.5 -"""--" 597.5 0.5 +"""—" 597.5 0.5 a 598.0 0.5 "century,""" 598.5 0.5 said 599.0 0.5 @@ -1262,7 +1263,7 @@ on 639.5 0.5 the 640.0 0.5 team 640.5 0.5 too 641.0 0.5 --- 641.5 0.5 +— 641.5 0.5 "Beaters.""" 642.0 0.5 + 642.5 0.5 """I" 643.0 0.5 @@ -1349,4 +1350,4 @@ first 683.0 0.5 week. 683.5 0.5 See 684.0 0.5 "you.""" 684.5 0.5 -+ 685.0 0.5 ++ 685 16 diff --git a/data/language/harrypotter/task-harry_run-3_events.tsv b/data/language/harrypotter/task-harry_run-3_events.tsv index d3514f6c..427a1ece 100644 --- a/data/language/harrypotter/task-harry_run-3_events.tsv +++ b/data/language/harrypotter/task-harry_run-3_events.tsv @@ -1,4 +1,5 @@ word format onset duration ++ 0 10 Fred 10.0 0.5 and 10.5 0.5 George 11.0 0.5 @@ -109,7 +110,7 @@ Wizard's 63.0 0.5 duel. 63.5 0.5 Wands 64.0 0.5 only 64.5 0.5 --- 65.0 0.5 +— 65.0 0.5 no 65.5 0.5 contact. 66.0 0.5 What's 66.5 0.5 @@ -335,7 +336,7 @@ and 176.0 0.5 Malfoy 176.5 0.5 were 177.0 0.5 saying 177.5 0.5 -"--""" 178.0 0.5 +"—""" 178.0 0.5 + 178.5 0.5 """Bet" 179.0 0.5 you 179.5 0.5 @@ -343,7 +344,7 @@ you 179.5 0.5 Ron 180.5 0.5 muttered. 181.0 0.5 + 181.5 0.5 -"""--" 182.0 0.5 +"""—" 182.0 0.5 and 182.5 0.5 you 183.0 0.5 mustn't italic 183.5 0.5 @@ -499,7 +500,7 @@ out 258.0 0.5 of 258.5 0.5 the 259.0 0.5 darkness 259.5 0.5 --- 260.0 0.5 +— 260.0 0.5 this 260.5 0.5 was 261.0 0.5 his 261.5 0.5 @@ -625,7 +626,7 @@ your 320.5 0.5 Hermione 321.5 0.5 snapped, 322.0 0.5 """Percy" 322.5 0.5 --- 323.0 0.5 +— 323.0 0.5 he's 323.5 0.5 a 324.0 0.5 prefect, 324.5 0.5 @@ -749,7 +750,7 @@ home 383.0 0.5 tomorrow, 383.5 0.5 you're 384.0 0.5 so 384.5 0.5 -"--""" 385.0 0.5 +"—""" 385.0 0.5 + 385.5 0.5 But 386.0 0.5 what 386.5 0.5 @@ -895,7 +896,7 @@ me 454.5 0.5 got 456.5 0.5 some 457.0 0.5 nerve 457.5 0.5 -"--""" 458.0 0.5 +"—""" 458.0 0.5 said 458.5 0.5 Ron 459.0 0.5 loudly. 459.5 0.5 @@ -1019,7 +1020,7 @@ a 518.0 0.5 "minute.""" 518.5 0.5 + 519.0 0.5 """Good" 519.5 0.5 --- 520.0 0.5 +— 520.0 0.5 well, 520.5 0.5 look, 521.0 0.5 Neville, 521.5 0.5 @@ -1032,7 +1033,7 @@ we'll 524.5 0.5 see 525.0 0.5 you 525.5 0.5 later 526.0 0.5 -"--""" 526.5 0.5 +"—""" 526.5 0.5 + 527.0 0.5 """Don't" 527.5 0.5 leave 528.0 0.5 @@ -1057,4 +1058,4 @@ been 537.0 0.5 past 537.5 0.5 twice 538.0 0.5 "already.""" 538.5 0.5 -+ 539.0 0.5 ++ 539.0 16 diff --git a/data/language/harrypotter/task-harry_run-4_events.tsv b/data/language/harrypotter/task-harry_run-4_events.tsv index be7d6600..11b3b18b 100644 --- a/data/language/harrypotter/task-harry_run-4_events.tsv +++ b/data/language/harrypotter/task-harry_run-4_events.tsv @@ -1,4 +1,5 @@ word format onset duration ++ 0 10 Ron 10.0 0.5 looked 10.5 0.5 at 11.0 0.5 @@ -212,7 +213,7 @@ they 114.5 0.5 heard 115.0 0.5 someone 115.5 0.5 speak 116.0 0.5 --- 116.5 0.5 +— 116.5 0.5 and 117.0 0.5 it 117.5 0.5 wasn't 118.0 0.5 @@ -331,7 +332,7 @@ broke 174.0 0.5 into 174.5 0.5 a 175.0 0.5 run 175.5 0.5 --- 176.0 0.5 +— 176.0 0.5 he 176.5 0.5 tripped, 177.0 0.5 grabbed 177.5 0.5 @@ -385,7 +386,7 @@ whether 201.0 0.5 Filch 201.5 0.5 was 202.0 0.5 following 202.5 0.5 --- 203.0 0.5 +— 203.0 0.5 they 203.5 0.5 swung 204.0 0.5 around 204.5 0.5 @@ -413,7 +414,7 @@ where 215.0 0.5 they 215.5 0.5 were 216.0 0.5 going 216.5 0.5 --- 217.0 0.5 +— 217.0 0.5 they 217.5 0.5 ripped 218.0 0.5 through 218.5 0.5 @@ -471,9 +472,9 @@ and 244.0 0.5 spluttering. 244.5 0.5 + 245.0 0.5 """I" 245.5 0.5 --- 246.0 0.5 +— 246.0 0.5 told italic 246.5 0.5 --- 247.0 0.5 +— 247.0 0.5 "you,""" 247.5 0.5 Hermione 248.0 0.5 gasped, 248.5 0.5 @@ -485,9 +486,9 @@ in 251.0 0.5 her 251.5 0.5 chest, 252.0 0.5 """I" 252.5 0.5 --- 253.0 0.5 +— 253.0 0.5 told 253.5 0.5 --- 254.0 0.5 +— 254.0 0.5 "you.""" 254.5 0.5 + 255.0 0.5 """We've" 255.5 0.5 @@ -523,7 +524,7 @@ going 270.0 0.5 to 270.5 0.5 meet 271.0 0.5 you 271.5 0.5 --- 272.0 0.5 +— 272.0 0.5 Filch 272.5 0.5 knew 273.0 0.5 someone 273.5 0.5 @@ -610,9 +611,9 @@ delight. 313.0 0.5 """Shut" 314.0 0.5 up, 314.5 0.5 Peeves 315.0 0.5 --- 315.5 0.5 +— 315.5 0.5 please 316.0 0.5 --- 316.5 0.5 +— 316.5 0.5 you'll 317.0 0.5 get 317.5 0.5 us 318.0 0.5 @@ -683,7 +684,7 @@ a 350.0 0.5 swipe 350.5 0.5 at 351.0 0.5 Peeves 351.5 0.5 --- 352.0 0.5 +— 352.0 0.5 this 352.5 0.5 was 353.0 0.5 a 353.5 0.5 @@ -726,7 +727,7 @@ slammed 371.5 0.5 into 372.0 0.5 a 372.5 0.5 door 373.0 0.5 --- 373.5 0.5 +— 373.5 0.5 and 374.0 0.5 it 374.5 0.5 was 375.0 0.5 @@ -791,7 +792,7 @@ the 404.0 0.5 door 404.5 0.5 swung 405.0 0.5 open 405.5 0.5 --- 406.0 0.5 +— 406.0 0.5 they 406.5 0.5 piled 407.0 0.5 through 407.5 0.5 @@ -849,7 +850,7 @@ singsong 433.0 0.5 voice. 433.5 0.5 """All" 434.0 0.5 right 434.5 0.5 --- 435.0 0.5 +— 435.0 0.5 "please.""" italic 435.5 0.5 + 436.0 0.5 """NOTHING!" 436.5 0.5 @@ -897,7 +898,7 @@ think 457.0 0.5 we'll 457.5 0.5 be 458.0 0.5 okay 458.5 0.5 --- 459.0 0.5 +— 459.0 0.5 get 459.5 0.5 off, italic 460.0 0.5 "Neville!""" 460.5 0.5 @@ -921,7 +922,7 @@ minute. 468.0 0.5 Harry 469.5 0.5 turned 470.0 0.5 around 470.5 0.5 --- 471.0 0.5 +— 471.0 0.5 and 471.5 0.5 saw, 472.0 0.5 quite 472.5 0.5 @@ -938,7 +939,7 @@ walked 477.5 0.5 into 478.0 0.5 a 478.5 0.5 nightmare 479.0 0.5 --- 479.5 0.5 +— 479.5 0.5 this 480.0 0.5 was 480.5 0.5 too 481.0 0.5 @@ -1089,7 +1090,7 @@ groped 553.0 0.5 for 553.5 0.5 the 554.0 0.5 doorknob 554.5 0.5 --- 555.0 0.5 +— 555.0 0.5 between 555.5 0.5 Filch 556.0 0.5 and 556.5 0.5 @@ -1101,7 +1102,7 @@ Filch. 558.5 0.5 They 559.5 0.5 fell 560.0 0.5 backward 560.5 0.5 --- 561.0 0.5 +— 561.0 0.5 Harry 561.5 0.5 slammed 562.0 0.5 the 562.5 0.5 @@ -1138,7 +1139,7 @@ but 577.5 0.5 they 578.0 0.5 hardly 578.5 0.5 cared 579.0 0.5 --- 579.5 0.5 +— 579.5 0.5 all 580.0 0.5 they 580.5 0.5 wanted 581.0 0.5 @@ -1200,7 +1201,7 @@ faces. 608.0 0.5 """Never" 609.0 0.5 mind 609.5 0.5 that 610.0 0.5 --- 610.5 0.5 +— 610.5 0.5 pig 611.0 0.5 snout, 611.5 0.5 pig 612.0 0.5 @@ -1358,7 +1359,7 @@ all 687.5 0.5 have 688.0 0.5 been 688.5 0.5 killed 689.0 0.5 --- 689.5 0.5 +— 689.5 0.5 or 690.0 0.5 worse, 690.5 0.5 expelled. 691.0 0.5 @@ -1415,9 +1416,9 @@ dog 716.0 0.5 was 716.5 0.5 guarding 717.0 0.5 something. 717.5 0.5 -. 718.0 0.5 -^. 718.5 0.5 -^^. 719.0 0.5 +. 718.0 0.5 +. . 718.5 0.5 +. . . 719.0 0.5 What 719.5 0.5 had 720.0 0.5 Hagrid 720.5 0.5 @@ -1436,7 +1437,7 @@ you 726.5 0.5 wanted 727.0 0.5 to 727.5 0.5 hide 728.0 0.5 --- 728.5 0.5 +— 728.5 0.5 except 729.0 0.5 perhaps 729.5 0.5 Hogwarts. 730.0 0.5 @@ -1461,4 +1462,4 @@ hundred 739.0 0.5 and 739.5 0.5 thirteen 740.0 0.5 was. 740.5 0.5 -+ 741.0 0.5 ++ 741.0 16 diff --git a/data/language/harrypotter/task-harry_run-5_events.tsv b/data/language/harrypotter/task-harry_run-5_events.tsv index 670ad322..fbaf4386 100644 --- a/data/language/harrypotter/task-harry_run-5_events.tsv +++ b/data/language/harrypotter/task-harry_run-5_events.tsv @@ -1,4 +1,5 @@ word format onset duration ++ 0 10 Malfoy 10.0 0.5 couldn't 10.5 0.5 believe 11.0 0.5 @@ -979,7 +980,7 @@ ground. 498.0 0.5 What 498.5 0.5 a 499.0 0.5 feeling 499.5 0.5 --- 500.0 0.5 +— 500.0 0.5 he 500.5 0.5 swooped 501.0 0.5 in 501.5 0.5 @@ -1195,7 +1196,7 @@ to 605.5 0.5 Harry 606.5 0.5 recited. 607.0 0.5 """So" 607.5 0.5 --- 608.0 0.5 +— 608.0 0.5 that's 608.5 0.5 sort 609.0 0.5 of 609.5 0.5 @@ -1416,7 +1417,7 @@ away 716.5 0.5 into 717.0 0.5 the 717.5 0.5 air 718.0 0.5 --- 718.5 0.5 +— 718.5 0.5 it 719.0 0.5 zoomed 719.5 0.5 around 720.0 0.5 @@ -1478,13 +1479,13 @@ Beaters 747.5 0.5 on 748.0 0.5 each 748.5 0.5 team 749.0 0.5 --- 749.5 0.5 +— 749.5 0.5 the 750.0 0.5 Weasley 750.5 0.5 twins 751.0 0.5 are 751.5 0.5 ours 752.0 0.5 --- 752.5 0.5 +— 752.5 0.5 it's 753.0 0.5 their 753.5 0.5 job 754.0 0.5 @@ -1505,7 +1506,7 @@ the 761.0 0.5 other 761.5 0.5 team. 762.0 0.5 So 762.5 0.5 --- 763.0 0.5 +— 763.0 0.5 think 763.5 0.5 you've 764.0 0.5 got 764.5 0.5 @@ -1545,7 +1546,7 @@ said 781.0 0.5 Wood. 781.5 0.5 + 782.0 0.5 """Er" 782.5 0.5 --- 783.0 0.5 +— 783.0 0.5 have 783.5 0.5 the 784.0 0.5 Bludgers 784.5 0.5 @@ -1598,9 +1599,9 @@ Quaffle 807.5 0.5 or 808.0 0.5 the 808.5 0.5 Bludgers 809.0 0.5 -"--""" 809.5 0.5 +"—""" 809.5 0.5 + 810.0 0.5 -"""--" 810.5 0.5 +"""—" 810.5 0.5 unless 811.0 0.5 they 811.5 0.5 crack 812.0 0.5 @@ -1620,7 +1621,7 @@ match 818.5 0.5 for 819.0 0.5 the 819.5 0.5 Bludgers 820.0 0.5 --- 820.5 0.5 +— 820.5 0.5 I 821.0 0.5 mean, 821.5 0.5 they're 822.0 0.5 @@ -1631,4 +1632,4 @@ of 824.0 0.5 human 824.5 0.5 Bludgers 825.0 0.5 "themselves.""" 825.5 0.5 -+ 826.0 0.5 ++ 826.0 16 diff --git a/data/language/harrypotter/task-harry_run-6_events.tsv b/data/language/harrypotter/task-harry_run-6_events.tsv index 44c259c4..b2e10ac0 100644 --- a/data/language/harrypotter/task-harry_run-6_events.tsv +++ b/data/language/harrypotter/task-harry_run-6_events.tsv @@ -1,4 +1,5 @@ word format onset duration ++ 0 10 Wood 10.0 0.5 reached 10.5 0.5 into 11.0 0.5 @@ -143,7 +144,7 @@ go 80.0 0.5 on 80.5 0.5 for 81.0 0.5 ages 81.5 0.5 --- 82.0 0.5 +— 82.0 0.5 I 82.5 0.5 think 83.0 0.5 the 83.5 0.5 @@ -169,7 +170,7 @@ sleep. 92.5 0.5 """Well," 93.5 0.5 that's 94.0 0.5 it 94.5 0.5 --- 95.0 0.5 +— 95.0 0.5 any 95.5 0.5 "questions?""" 96.0 0.5 + 96.5 0.5 @@ -571,7 +572,7 @@ is 294.0 0.5 very 294.5 0.5 important, 295.0 0.5 too 295.5 0.5 --- 296.0 0.5 +— 296.0 0.5 never 296.5 0.5 forget 297.0 0.5 Wizard 297.5 0.5 @@ -636,7 +637,7 @@ set 326.5 0.5 fire 327.0 0.5 to 327.5 0.5 it 328.0 0.5 --- 328.5 0.5 +— 328.5 0.5 Harry 329.0 0.5 had 329.5 0.5 to 330.0 0.5 @@ -805,7 +806,7 @@ glimpse 411.0 0.5 of 411.5 0.5 her 412.0 0.5 face 412.5 0.5 --- 413.0 0.5 +— 413.0 0.5 and 413.5 0.5 was 414.0 0.5 startled 414.5 0.5 @@ -1002,11 +1003,11 @@ table, 509.5 0.5 and 510.0 0.5 gasped, 510.5 0.5 """Troll" 511.0 0.5 --- 511.5 0.5 +— 511.5 0.5 in 512.0 0.5 the 512.5 0.5 dungeons 513.0 0.5 --- 513.5 0.5 +— 513.5 0.5 thought 514.0 0.5 you 514.5 0.5 ought 515.0 0.5 @@ -1162,7 +1163,7 @@ arm. 589.0 0.5 """I've" 590.0 0.5 just 590.5 0.5 thought 591.0 0.5 --- 591.5 0.5 +— 591.5 0.5 "Hermione.""" 592.0 0.5 + 592.5 0.5 """What" 593.0 0.5 @@ -1295,4 +1296,4 @@ after 656.0 0.5 Snape's 656.5 0.5 fading 657.0 0.5 footsteps. 657.5 0.5 -+ 658.0 0.5 ++ 658.0 16 diff --git a/data/language/harrypotter/task-harry_run-7_events.tsv b/data/language/harrypotter/task-harry_run-7_events.tsv index 25bb1837..24ec5fb0 100644 --- a/data/language/harrypotter/task-harry_run-7_events.tsv +++ b/data/language/harrypotter/task-harry_run-7_events.tsv @@ -1,4 +1,5 @@ word format onset duration ++ 0 10 """He's" 10.0 0.5 heading 10.5 0.5 for 11.0 0.5 @@ -50,7 +51,7 @@ then 33.5 0.5 they 34.0 0.5 heard 34.5 0.5 it 35.0 0.5 --- 35.5 0.5 +— 35.5 0.5 a 36.0 0.5 low 36.5 0.5 grunting, 37.0 0.5 @@ -63,7 +64,7 @@ gigantic 40.0 0.5 feet. 40.5 0.5 Ron 41.0 0.5 pointed 41.5 0.5 --- 42.0 0.5 +— 42.0 0.5 at 42.5 0.5 the 43.0 0.5 end 43.5 0.5 @@ -275,12 +276,12 @@ made 146.0 0.5 their 146.5 0.5 hearts 147.0 0.5 stop 147.5 0.5 --- 148.0 0.5 +— 148.0 0.5 a 148.5 0.5 high, 149.0 0.5 petrified 149.5 0.5 scream 150.0 0.5 --- 150.5 0.5 +— 150.5 0.5 and 151.0 0.5 it 151.5 0.5 was 152.0 0.5 @@ -632,7 +633,7 @@ hand 324.5 0.5 when 325.0 0.5 he'd 325.5 0.5 jumped 326.0 0.5 --- 326.5 0.5 +— 326.5 0.5 it 327.0 0.5 had 327.5 0.5 gone 328.0 0.5 @@ -695,7 +696,7 @@ out 356.0 0.5 his 356.5 0.5 own 357.0 0.5 wand 357.5 0.5 --- 358.0 0.5 +— 358.0 0.5 not 358.5 0.5 knowing 359.0 0.5 what 359.5 0.5 @@ -738,7 +739,7 @@ air, 377.5 0.5 turned 378.0 0.5 slowly 378.5 0.5 over 379.0 0.5 --- 379.5 0.5 +— 379.5 0.5 and 380.0 0.5 dropped, 380.5 0.5 with 381.0 0.5 @@ -809,7 +810,7 @@ first. 413.0 0.5 + 413.5 0.5 """Is" 414.0 0.5 it 414.5 0.5 --- 415.0 0.5 +— 415.0 0.5 "dead?""" 415.5 0.5 + 416.0 0.5 I 416.5 0.5 @@ -850,7 +851,7 @@ gray 433.5 0.5 glue. 434.0 0.5 + 434.5 0.5 """Urgh" 435.0 0.5 --- 435.5 0.5 +— 435.5 0.5 troll 436.0 0.5 "boogers.""" 436.5 0.5 + 437.0 0.5 @@ -1057,7 +1058,7 @@ shadows. 536.5 0.5 """Please," 537.5 0.5 Professor 538.0 0.5 McGonagall 538.5 0.5 --- 539.0 0.5 +— 539.0 0.5 they 539.5 0.5 were 540.0 0.5 looking 540.5 0.5 @@ -1086,7 +1087,7 @@ the 551.5 0.5 troll 552.0 0.5 because 552.5 0.5 I 553.0 0.5 --- 553.5 0.5 +— 553.5 0.5 I 554.0 0.5 thought 554.5 0.5 I 555.0 0.5 @@ -1097,7 +1098,7 @@ it 557.0 0.5 on 557.5 0.5 my 558.0 0.5 own 558.5 0.5 --- 559.0 0.5 +— 559.0 0.5 you 559.5 0.5 know, 560.0 0.5 because 560.5 0.5 @@ -1182,7 +1183,7 @@ to 599.5 0.5 them. 600.0 0.5 + 600.5 0.5 """Well" 601.0 0.5 --- 601.5 0.5 +— 601.5 0.5 in 602.0 0.5 that 602.5 0.5 "case...""" 603.0 0.5 @@ -1543,4 +1544,4 @@ is 780.0 0.5 one 780.5 0.5 of 781.0 0.5 them. 781.5 0.5 -+ 782.0 0.5 ++ 782.0 16 From 05cca2e158ea808b5f48b0c7dfacf6ed5e7ce4e2 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 7 Apr 2021 08:41:44 -0400 Subject: [PATCH 135/170] fix tsv path in session script --- src/sessions/ses-harrypotter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sessions/ses-harrypotter.py b/src/sessions/ses-harrypotter.py index e796b384..7745e216 100644 --- a/src/sessions/ses-harrypotter.py +++ b/src/sessions/ses-harrypotter.py @@ -4,7 +4,7 @@ for run in range(1, 8): TASKS.append( language.Reading( - f"data/harrypotter/task-harry_run-{run}_events.tsv", + f"data/language/harrypotter/task-harry_run-{run}_events.tsv", name=f"harrypotter_run-{run}", cross_duration=2, txt_size=124, From 8fbe73849a1e4bcde19c3dbb69cb6e96c51275e0 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 7 Apr 2021 08:42:03 -0400 Subject: [PATCH 136/170] fix non-slip timing code --- src/tasks/language.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tasks/language.py b/src/tasks/language.py index 0f616401..d36529f7 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -185,7 +185,10 @@ def _run(self, exp_win, ctl_win): self.txt_stim._pygletTextObj.set_style('italic', trial["format"] == "italic") self.txt_stim.draw(exp_win) self.progress_bar.set_description(f"Trial {trial_n}:: {trial['word']} {trial['format']}") - utils.wait_until(self.task_timer, trial["onset"] - 1 / config.FRAME_RATE) + utils.wait_until( + self.task_timer, + trial["onset"] - 1 / config.FRAME_RATE, + hogCPUperiod=0.2) yield True # flip self.words_list.at[trial_n, "onset_flip"] = ( self._exp_win_last_flip_time - self._exp_win_first_flip_time @@ -196,7 +199,6 @@ def _run(self, exp_win, ctl_win): self.words_list.at[trial_n - 1, "offset_flip"] - self.words_list.at[trial_n - 1, "onset_flip"] ) - utils.wait_until(self.task_timer, trial["onset"]+trial["duration"] - 1 / config.FRAME_RATE) def _stop(self, exp_win, ctl_win): exp_win.setColor((0,0,0), "rgb") From 7445ad834209d370c42b8ab540d70c76d03a4b63 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 7 Apr 2021 09:58:12 -0400 Subject: [PATCH 137/170] fix hp progress bar --- src/tasks/language.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tasks/language.py b/src/tasks/language.py index d36529f7..107fc383 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -176,6 +176,7 @@ def _setup(self, exp_win): alignText="center", color=self.txt_color, ) + self._progress_bar_refresh_rate = 1 # 1 flip / trial def _run(self, exp_win, ctl_win): From aefe1fe5e72a28d7457de7d19858a476bcbfb789 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 7 Apr 2021 12:05:23 -0400 Subject: [PATCH 138/170] fix end of task --- src/tasks/language.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tasks/language.py b/src/tasks/language.py index 107fc383..ffeb0bb1 100644 --- a/src/tasks/language.py +++ b/src/tasks/language.py @@ -200,6 +200,12 @@ def _run(self, exp_win, ctl_win): self.words_list.at[trial_n - 1, "offset_flip"] - self.words_list.at[trial_n - 1, "onset_flip"] ) + # wait for last event duration + utils.wait_until( + self.task_timer, + trial["onset"]+trial["duration"] - 1 / config.FRAME_RATE + ) + yield def _stop(self, exp_win, ctl_win): exp_win.setColor((0,0,0), "rgb") From f3b5342acabef739e15a3b51e178a95d3703e0a2 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 9 Apr 2021 09:27:33 -0400 Subject: [PATCH 139/170] add separate session for shinobi MEG --- src/sessions/ses-shinobimeg.py | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/sessions/ses-shinobimeg.py diff --git a/src/sessions/ses-shinobimeg.py b/src/sessions/ses-shinobimeg.py new file mode 100644 index 00000000..61fc80b2 --- /dev/null +++ b/src/sessions/ses-shinobimeg.py @@ -0,0 +1,49 @@ +import os +import random +import retro + +# point to a copy of the whole gym-retro with custom states and scenarii +retro.data.Integrations.add_custom_path( + os.path.join(os.getcwd(), "data", "videogames", "shinobi") +) + +from ..tasks import images, videogame, memory, task_base + +flow_ratings = [ + "I feel just the right amount of challenge.", + "My thoughts/activities run fluidly and smoothly.", + "I don’t notice time passing.", + "I have no difficulty concentrating.", + "My mind is completely clear.", + "I am totally absorbed in what I am doing.", + "The right thoughts/movements occur of their own accord.", + "I know what I have to do each step of the way.", + "I feel that I have everything under control.", + "I am completely lost in thought.", +] + +levels_scenario = [ + ("Level1-0", "scenario_Level1"), + ("Level4-1", "scenario_Level4-1"), + ("Level5-0", "scenario_Level5-0")] +#random.shuffle(levels_scenario) # randomize order + +TASKS = sum( + [ + [ + videogame.VideoGameMultiLevel( + state_names=[l for l,s in levels_scenario], + scenarii=[s for l,s in levels_scenario] + , # this scenario repeats the same level + repeat_scenario=True, + max_duration=10 + * 60, # if when level completed or dead we exceed that time in secs, stop the task + name=f"task-shinobi_run-{run+1:02d}", + post_level_ratings = [(q, 7) for q in flow_ratings] + ), + task_base.Pause(), + ] + for run in range(5) + ], + [], +) From b5ef8f4060bc4d2c2d68f4ff0d336f41b855bba8 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 9 Apr 2021 15:42:20 -0400 Subject: [PATCH 140/170] set max duration to 9min --- src/sessions/ses-shinobimeg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sessions/ses-shinobimeg.py b/src/sessions/ses-shinobimeg.py index 61fc80b2..c14ef6a9 100644 --- a/src/sessions/ses-shinobimeg.py +++ b/src/sessions/ses-shinobimeg.py @@ -36,7 +36,7 @@ scenarii=[s for l,s in levels_scenario] , # this scenario repeats the same level repeat_scenario=True, - max_duration=10 + max_duration=9 * 60, # if when level completed or dead we exceed that time in secs, stop the task name=f"task-shinobi_run-{run+1:02d}", post_level_ratings = [(q, 7) for q in flow_ratings] From d5fc3ca890794ea682a979de9039f274d09143b1 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 9 Apr 2021 15:42:55 -0400 Subject: [PATCH 141/170] finish coding questionnaire for flow measures --- src/tasks/videogame.py | 105 ++++++++++++++++++++++++++++------------- 1 file changed, 72 insertions(+), 33 deletions(-) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index f1f7bcab..3323740b 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -205,7 +205,8 @@ def _set_recording_file(self): logging.exp("VideoGame: recording movie in %s" % self.movie_path) self.emulator.record_movie(self.movie_path) - def _handle_controller_presses(self): + def _handle_controller_presses(self, exp_win): + exp_win.winHandle.dispatch_events() global _keyPressBuffer, _keyReleaseBuffer for k in _keyReleaseBuffer: @@ -215,6 +216,7 @@ def _handle_controller_presses(self): for k in _keyPressBuffer: # print('press',k) self.pressed_keys.add(k[0]) + self._new_key_pressed = _keyPressBuffer[:] #copy _keyPressBuffer.clear() return self.pressed_keys @@ -255,7 +257,7 @@ def _run_emulator(self, exp_win, ctl_win): while _nextFrameT > (self.task_timer.getTime() - self._retraceInterval/2.0): time.sleep(.0001) - self._handle_controller_presses() + self._handle_controller_presses(exp_win) keys = [k in self.pressed_keys for k in KEY_SET] _obs, _rew, _done, _info = self.emulator.step(keys) total_reward += _rew @@ -329,34 +331,40 @@ def _run_ratings(self, exp_win, ctl_win): yield True def _questionnaire(self, exp_win, ctl_win): + + exp_win.setColor([0] * 3, colorSpace='rgb') if self.post_level_ratings is None: return lines = [] bullets = [] responses = [] - y_spacing = 40 + texts = [] + y_spacing = 80 win_width = exp_win.size[0] scales_block_x = win_width * 0.25 - scales_block_y = -len(self.post_level_ratings) / 2 * y_spacing + scales_block_y = len(self.post_level_ratings) // 2 * y_spacing extent = win_width * 0.2 + + active_question = 0 + # create all stimuli - all_questions_text = "" - for q_n, q_vals in enumerate(self.post_level_ratings): - question, n_pts = q_vals - print(question) + #all_questions_text = "" + for q_n, (question, n_pts) in enumerate(self.post_level_ratings): default_response = n_pts // 2 responses.append(default_response) x_spacing = extent * 2 / (n_pts - 1) - all_questions_text += question + "\n\n" + #all_questions_text += question + "\n\n" + + y_pos = scales_block_y - q_n * y_spacing lines.append( visual.Line( exp_win, - (scales_block_x - extent, scales_block_y + q_n * y_spacing), - (scales_block_x + extent, scales_block_y + q_n * y_spacing), + (scales_block_x - extent, y_pos), + (scales_block_x + extent, y_pos), units="pixels", - lineWidth=2, + lineWidth=6, autoLog=False, lineColor=((0, -1, -1) if q_n == 0 else (-1, -1, -1)), ) @@ -369,7 +377,7 @@ def _questionnaire(self, exp_win, ctl_win): radius=10, pos=( scales_block_x - extent + i * x_spacing, - scales_block_y + q_n * y_spacing, + y_pos, ), fillColor=( (1, 1, 1) if default_response == i else (-1, -1, -1) @@ -381,22 +389,55 @@ def _questionnaire(self, exp_win, ctl_win): for i in range(n_pts) ] ) + texts.append(visual.TextStim( + exp_win, + text = question, + units="pixels", + bold = q_n == active_question, + pos=(0, y_pos), + wrapWidth= win_width * 0.5, + height= y_spacing / 3, + anchorHoriz="right", + alignText="right" + )) + responses[q_n] = default_response + - text = visual.TextStim( - exp_win, - all_questions_text, - units="pixels", - pos=(0.1 * win_width, 0), - wrapWidth=win_width * 0.5, - height=y_spacing / 2, - anchorHoriz="right", - ) # questionnaire interaction loop + n_flips = 0 while True: - for stim in lines + sum(bullets, []) + [text]: + self._handle_controller_presses(exp_win) + new_key_pressed = [k[0] for k in self._new_key_pressed] + if "u" in new_key_pressed and active_question > 0: + active_question -= 1 + elif "d" in new_key_pressed and active_question < len(self.post_level_ratings)-1: + active_question += 1 + elif "r" in new_key_pressed and responses[active_question] < n_pts - 1: + responses[active_question] += 1 + elif "l" in new_key_pressed and responses[active_question] > 0: + responses[active_question] -= 1 + elif "a" in new_key_pressed: + break + elif n_flips > 1: + time.sleep(.01) + continue + + exp_win.logOnFlip( + level=logging.EXP, + msg="level ratings %s" % responses) + for q_n, (txt, line, bullet_q) in enumerate(zip(texts, lines, bullets)): + #txt.bold = q_n == active_question + txt._pygletTextObj.set_style('bold', q_n == active_question) + line.lineColor = ((0, -1, -1) if q_n == active_question else (-1, -1, -1)), + for bullet_n, bullet in enumerate(bullet_q): + bullet.fillColor = (1, 1, 1) if responses[q_n] == bullet_n else (-1, -1, -1) + + for stim in lines + sum(bullets, []) + texts: stim.draw(exp_win) yield True + n_flips += 1 + # TODO save responses def _likert_scale_answer( self, exp_win, ctl_win, question, n_pts=7, extent=0.6, autoLog=False @@ -433,7 +474,7 @@ def _likert_scale_answer( frame += 1 for stim in [text, line] + circles: stim.draw(exp_win) - self._handle_controller_presses() + self._handle_controller_presses(exp_win) if "a" in self.pressed_keys: exp_win.logOnFlip( level=logging.EXP, @@ -481,10 +522,6 @@ def _run(self, exp_win, ctl_win): exp_win.waitBlanking = False - exp_win.setColor([-1.0] * 3, colorSpace='rgb') - if ctl_win: - ctl_win.setColor([-1.0] * 3, colorSpace='rgb') - self._nlevels = 0 while True: for level, scenario in zip(self._state_names, self._scenarii): @@ -503,14 +540,16 @@ def _run(self, exp_win, ctl_win): if self._nlevels > 1: self._set_recording_file() yield from self._instructions(exp_win, ctl_win) - yield from self._questionnaire( - exp_win, ctl_win - ) # here for tests, to move after _run_emulator + exp_win.setColor([-1.0] * 3, colorSpace='rgb') + if ctl_win: + ctl_win.setColor([-1.0] * 3, colorSpace='rgb') yield from super()._run_emulator(exp_win, ctl_win) self.game_sound.stop() - if self.post_level_ratings: - yield from self._run_ratings(exp_win, ctl_win) + + yield from self._questionnaire( + exp_win, ctl_win + ) time_exceeded = ( self.max_duration and self.task_timer.getTime() > self.max_duration From 2bed8f5d1684ce3013972057d0bf47b71735fb03 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 12 Apr 2021 09:44:52 -0400 Subject: [PATCH 142/170] properly log questionnaire in events file, when changing value and final answers --- src/sessions/ses-shinobimeg.py | 22 +++++++++++----------- src/tasks/videogame.py | 24 +++++++++++++++++++----- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/sessions/ses-shinobimeg.py b/src/sessions/ses-shinobimeg.py index c14ef6a9..897b24b8 100644 --- a/src/sessions/ses-shinobimeg.py +++ b/src/sessions/ses-shinobimeg.py @@ -10,16 +10,16 @@ from ..tasks import images, videogame, memory, task_base flow_ratings = [ - "I feel just the right amount of challenge.", - "My thoughts/activities run fluidly and smoothly.", - "I don’t notice time passing.", - "I have no difficulty concentrating.", - "My mind is completely clear.", - "I am totally absorbed in what I am doing.", - "The right thoughts/movements occur of their own accord.", - "I know what I have to do each step of the way.", - "I feel that I have everything under control.", - "I am completely lost in thought.", + ("challenge", "I feel just the right amount of challenge."), + ("fluidity", "My thoughts/activities run fluidly and smoothly."), + ("time","I don’t notice time passing."), + ("focus", "I have no difficulty concentrating."), + ("insight","My mind is completely clear."), + ("absorption","I am totally absorbed in what I am doing."), + ("spontaneous","The right thoughts/movements occur of their own accord."), + ("planning", "I know what I have to do each step of the way."), + ("control", "I feel that I have everything under control."), + ("wandering", "I am completely lost in thought."), ] levels_scenario = [ @@ -39,7 +39,7 @@ max_duration=9 * 60, # if when level completed or dead we exceed that time in secs, stop the task name=f"task-shinobi_run-{run+1:02d}", - post_level_ratings = [(q, 7) for q in flow_ratings] + post_level_ratings = [(k, q, 7) for k, q in flow_ratings] ), task_base.Pause(), ] diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 3323740b..ae49fd6b 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -350,7 +350,7 @@ def _questionnaire(self, exp_win, ctl_win): # create all stimuli #all_questions_text = "" - for q_n, (question, n_pts) in enumerate(self.post_level_ratings): + for q_n, (key, question, n_pts) in enumerate(self.post_level_ratings): default_response = n_pts // 2 responses.append(default_response) x_spacing = extent * 2 / (n_pts - 1) @@ -418,11 +418,29 @@ def _questionnaire(self, exp_win, ctl_win): elif "l" in new_key_pressed and responses[active_question] > 0: responses[active_question] -= 1 elif "a" in new_key_pressed: + for (key, question, n_pts), value in zip(self.post_level_ratings, responses): + self._log_event({ + "trial_type": "questionnaire-answer", + "game": self.game_name, + "level": self.state_name, + "stim_file": self.movie_path, + "question": key, + "value": value + }) break elif n_flips > 1: time.sleep(.01) continue + self._log_event({ + "trial_type": "questionnaire-value-change", + "game": self.game_name, + "level": self.state_name, + "stim_file": self.movie_path, + "question": self.post_level_ratings[active_question][0], + "value": responses[active_question] + }) + exp_win.logOnFlip( level=logging.EXP, msg="level ratings %s" % responses) @@ -528,10 +546,6 @@ def _run(self, exp_win, ctl_win): self._nlevels += 1 self.state_name = level self.emulator.load_state(level, inttype=self.inttype) - print( - retro.data.get_file_path(self.game_name, "data.json", inttype=self.inttype), - retro.data.get_file_path(self.game_name, f"{scenario}.json", inttype=self.inttype) - ) self.emulator.data.load( retro.data.get_file_path(self.game_name, "data.json", inttype=self.inttype), retro.data.get_file_path(self.game_name, f"{scenario}.json", inttype=self.inttype) From c354d72962bade2bb5f0f2aa233a694f2846b405 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 12 Apr 2021 15:26:15 -0400 Subject: [PATCH 143/170] change design for half runs, randomize examplar orders while keeping the already shown for the end. --- src/sessions/ses-thingsmem.py | 47 +++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/src/sessions/ses-thingsmem.py b/src/sessions/ses-thingsmem.py index 799f0ed0..4bb0017a 100644 --- a/src/sessions/ses-thingsmem.py +++ b/src/sessions/ses-thingsmem.py @@ -12,7 +12,7 @@ def get_tasks(parsed): "memory_designs", f"sub-{parsed.subject}_ses-{parsed.session}_design.tsv", ) - n_runs_session = n_runs if int(parsed.session) > 1 else 6 + n_runs_session = n_runs if int(parsed.session) > 1 else n_runs//2 tasks = [ ThingsMemory(session_design_filename, IMAGE_PATH, run, name=f"task-thingsmemory_run-{run}") for run in range(1, n_runs_session + 1) @@ -22,10 +22,11 @@ def get_tasks(parsed): # experiment -n_sessions = 18 # number of sessions -n_runs = 12 # number of runs +n_sessions = 36 # number of sessions +n_runs = 6 # number of runs n_trials = 60 # number of trials for each run -splits = n_trials * 2 +splits = n_trials * 1 +new_samples_nsplits = 720//splits//2 final_wait = 9 # time to wait after last trial initial_wait = 3 # time until first trial starts @@ -60,11 +61,25 @@ def generate_design_file(subject): props['seen_between_within2'] = props['unseen_between'] props['seen_within_between'] = props['unseen_within'] + #props_session1 = pandas.DataFrame() + #props_session1['unseen_between'] = [12, 16, 18, 22, 24, 28] + #props_session1['unseen_within'] = [28, 24 , 22, 18, 16, 12] + #props_session1['seen_within'] = [20] * (n_runs//2) + + props = pandas.DataFrame() + props['unseen_between'] = [6, 8, 10, 10, 12, 14] + props['unseen_within'] = [14, 12, 10, 10, 8, 6] + props['seen_within'] = props['unseen_between'] + props['seen_between_within'] = props['unseen_within'] + props['seen_between_within2'] = props['unseen_between'] + props['seen_within_between'] = props['unseen_within'] + props_session1 = pandas.DataFrame() - props_session1['unseen_between'] = [12, 16, 18, 22, 24, 28] - props_session1['unseen_within'] = [28, 24 , 22, 18, 16, 12] + props_session1['unseen_between'] = [12, 20, 28] + props_session1['unseen_within'] = [28, 20, 12] props_session1['seen_within'] = [20] * (n_runs//2) + images_list = pandas.read_csv( os.path.join(THINGS_DATA_PATH, "images", "image_paths_fmri.csv") ) @@ -76,6 +91,21 @@ def generate_design_file(subject): (images_list.exemplar_nr < 7 ) # > 6 for pilot < 7 for study ] + # randomize examplar order across participants + exemplar_sorting = np.random.permutation(np.arange(1,7)) + if subject in ["%02d" % sn for sn in [2, 6]]: + print("already seen examplars 1 and 2") + exemplar_sorting = np.hstack([ + np.random.permutation(np.arange(3,7)), + np.random.permutation(np.arange(1,3)) + ]) + elif subject in ["%02d" % sn for sn in [1, 3 , 4]]: + print("already seen examplars 1") + exemplar_sorting = np.hstack([ + np.random.permutation(np.arange(2,7)), + [1] + ]) + # seed numpy with subject id to have reproducible design generation seed = int( hashlib.sha1(("%s" % (subject)).encode("utf-8")).hexdigest(), 16 @@ -94,13 +124,14 @@ def generate_design_file(subject): for session in range(n_sessions): # select the examplar - exemplar = session//3+1 # + 6 here for pilot, remove for study!! + exemplar = exemplar_sorting[session//6] + #exemplar = session//3+1 # + 6 here for pilot, remove for study!! # subselect 240 categories for new stimuli # loop through the 3 sets of 240 across sessions and thus avoid # having distractors from the same category within-session # but there will still be between session distractors - new_stimuli_categories = categories[splits*2*(session%3):splits*2*(session%3+1)] + new_stimuli_categories = categories[splits*2*(session%new_samples_nsplits):splits*2*(session%new_samples_nsplits+1)] #randomize categories to be used as within and between repeated new stimuli to avoid systematic bias new_stimuli_categories = np.random.permutation(new_stimuli_categories) cat_unseen_within = new_stimuli_categories[:splits] From 8a9cb26c5bce7b065ed11024275ce671f8106675 Mon Sep 17 00:00:00 2001 From: basile Date: Mon, 12 Apr 2021 16:07:57 -0400 Subject: [PATCH 144/170] fix double logging of value change when loading questionnaire --- src/tasks/videogame.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index ae49fd6b..dcfc68f6 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -432,14 +432,15 @@ def _questionnaire(self, exp_win, ctl_win): time.sleep(.01) continue - self._log_event({ - "trial_type": "questionnaire-value-change", - "game": self.game_name, - "level": self.state_name, - "stim_file": self.movie_path, - "question": self.post_level_ratings[active_question][0], - "value": responses[active_question] - }) + if n_flips > 0: #avoid double log when first loading questionnaire + self._log_event({ + "trial_type": "questionnaire-value-change", + "game": self.game_name, + "level": self.state_name, + "stim_file": self.movie_path, + "question": self.post_level_ratings[active_question][0], + "value": responses[active_question] + }) exp_win.logOnFlip( level=logging.EXP, From f83d93f6112150ee0ddea251a1b6467590d19ec2 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 20 Apr 2021 13:28:28 -0400 Subject: [PATCH 145/170] fix saving eyetracking calibration data --- src/shared/eyetracking.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index 391ab7f6..a6257a68 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -195,9 +195,9 @@ def stop(self, exp_win, ctl_win): yield def _save(self): - if hasattr(self, "all_pupils"): - fname = self._generate_unique_filename("calib-data", ".npz") - np.savez(fname, pupils=self.all_pupils, markers=self.all_refs_per_flip) + if hasattr(self, "_pupils_list"): + fname = self._generate_unique_filename("calib-data", "npz") + np.savez(fname, pupils=self._pupils_list, markers=self.all_refs_per_flip) from subprocess import Popen @@ -288,7 +288,7 @@ def __init__(self, output_path, output_fname_base, profile=False, debug=False): ) # stop a bunch of eye plugins for performance - for plugin in ["NDSI_Manager", "Pye3DPlugin"]: + for plugin in ["NDSI_Manager"]: self.send_recv_notification( { "subject": "stop_eye_plugin", From 8415df4dcd708d31233d15a53ea379fed836f419 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 23 Apr 2021 16:30:59 -0400 Subject: [PATCH 146/170] wip: implement KK/HCP/NSD retinotopy task --- src/sessions/ses-retino.py | 8 ++ src/shared/utils.py | 11 +++ src/tasks/retinotopy.py | 165 +++++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 src/sessions/ses-retino.py create mode 100644 src/tasks/retinotopy.py diff --git a/src/sessions/ses-retino.py b/src/sessions/ses-retino.py new file mode 100644 index 00000000..3e9a3dd2 --- /dev/null +++ b/src/sessions/ses-retino.py @@ -0,0 +1,8 @@ +from ..tasks import retinotopy + +TASKS = [ + retinotopy.Retinotopy( + condition = condition, + name=f"task-retinotopy{condition}", + ) for condition in ['RETEXP','RETCON','RETCCW','RETCW','RETBAR'] +] diff --git a/src/shared/utils.py b/src/shared/utils.py index 36d7e76e..49ae41d7 100644 --- a/src/shared/utils.py +++ b/src/shared/utils.py @@ -26,3 +26,14 @@ def poll_windows(): if (win.winType == "pyglet" and hasattr(win.winHandle, "dispatch_events")): win.winHandle.dispatch_events() # pump events + +def wait_until_yield(clock, deadline, hogCPUperiod=0.1, keyboard_accuracy=.0005): + sleep_until = deadline - hogCPUperiod + poll_windows() + current_time = clock.getTime() + while current_time < deadline: + if current_time < sleep_until: + time.sleep(keyboard_accuracy) + yield + poll_windows() + current_time = clock.getTime() diff --git a/src/tasks/retinotopy.py b/src/tasks/retinotopy.py new file mode 100644 index 00000000..9b99de12 --- /dev/null +++ b/src/tasks/retinotopy.py @@ -0,0 +1,165 @@ +import os, sys, time, random +from psychopy import visual, core, data, logging, event +from .task_base import Task +import numpy as np +from colorama import Fore +import pandas + +from ..shared import config, utils + +class Retinotopy(Task): + + DEFAULT_INSTRUCTION = """You will see a dot in the center of the screen. + Fixate that dot, and press the button when the color changes. + Moving patterns will be shown in the meantime, but you need to pay attention to the dot!""" + + + DOT_COLORS = [(237, 96, 31), (66, 135, 245)] + DOT_MIN_DURATION = 3 + RESPONSE_KEY = 'a' + + def __init__(self, condition, *args, **kwargs): + super().__init__(**kwargs) + if condition not in ['RETCCW', 'RETCW', 'RETEXP', 'RETCON', 'RETBAR']: + raise ValueError("Condition {condition} does not exists") + self.condition = condition + + + def _setup(self, exp_win): + self.fixation_dot = visual.Circle( + exp_win, + size=.15, + units='deg', + opacity=.5, + color=self.DOT_COLORS[0], + colorSpace='rgb255' + ) + + self.img = visual.ImageStim( + exp_win, + size=(768,768), + units='pixels', + flipVert=True) + + self._images = np.load('data/retinotopy/images.npz')['images'].astype(np.float32)/255. + if 'CW' in self.condition: + aperture_file = 'apertures_wedge.npz' + elif self.condition in ['RETEXP', 'RETCON']: + aperture_file = '/apertures_ring.npz' + elif self.condition == 'RETBAR': + aperture_file = 'apertures_bars.npz' + self._apertures = np.load(f"data/retinotopy/{aperture_file}")['apertures'].astype(np.float32)/128.-1 + + # draw random order with different successive stimuli + self._images_random = np.random.randint(0, 99, size=(8*32*15)) #max nframe in CW conditions + while any(np.ediff1d(self._images_random, to_begin=[-1])==0): + self._images_random[np.ediff1d(self._images_random, to_begin=[-1])==0] += 1 + self._images_random[self._images_random==100] = 0 + + + self.duration = 300 # seconds + self._progress_bar_refresh_rate = 15 + + + self.events = pandas.DataFrame() + super()._setup(exp_win) + + def _instructions(self, exp_win, ctl_win): + screen_text = visual.TextStim( + exp_win, + text=self.instruction, + alignText="center", + color="white", + wrapWidth=config.WRAP_WIDTH, + ) + + for frameN in range(config.FRAME_RATE * config.INSTRUCTION_DURATION): + screen_text.draw(exp_win) + if ctl_win: + screen_text.draw(ctl_win) + yield frameN < 2 + yield True + + def _run(self, exp_win, ctl_win): + + exp_win.logOnFlip( + level=logging.EXP, msg=f"Retinotopy {self.condition}: task starting at {time.time()}" + ) + + initial_wait = 16 if self.condition == 'RETBAR' else 22 + utils.wait_until(self.task_timer, initial_wait - 1 / config.FRAME_RATE) + color_state = 0 + dot_next_change = self.DOT_MIN_DURATION + random.random()*5 + + dot_changes = [dot_next_change] + self.fixation_dot.draw(exp_win) + yield True + for clearBuffer in self._run_condition(exp_win, ctl_win, initial_wait): + #TODO: log responses + keypresses = event.getKeys(self.RESPONSE_KEY, timeStamped=self.task_timer) + + if self.task_timer.getTime() > dot_next_change: + color_state = (color_state+1)%2 + self.fixation_dot.setColor( + self.DOT_COLORS[color_state], + colorSpace='rgb255') + dot_next_change += self.DOT_MIN_DURATION + random.random()*5 + dot_changes.append + self.fixation_dot.draw(exp_win) + if ctl_win: + self.fixation_dot.draw(exp_win) + yield clearBuffer + + + def _run_condition(self, exp_win, ctl_win, initial_wait): + if 'BAR' in self.condition: + middle_blank = 12 + conds = np.asarray([0,1,0,1,2,3,2,3]) + for ci, start_idx in enumerate(conds*28*15): + order = 1-(ci%4>1)*2 + for fi, frame in enumerate(range(28*15)[::order]): + flip_time = (initial_wait + (ci>3) * middle_blank + + (ci*32*15+fi) * 4/config.FRAME_RATE + - 1/config.FRAME_RATE) + + #flipVert = 1 - 2*(ci in [3,6,7]) + #flipHoriz = 1 - 2*(ci in [2]) + image_idx = self._images_random[ci*32*15+fi] + self.img.image = self._images[..., image_idx] + self.img.mask = self._apertures[..., start_idx+frame] + + utils.wait_until(self.task_timer, flip_time) + + self.img.draw(exp_win) + if ctl_win: + self.img.draw(ctl_win) + yield True + print(self.task_timer.getTime(),flip_time) + yield True + else: + order = -1 if self.condition in ['RETCW', 'RETCON'] else 1 + for ci in range(8): # 8 cycles + cycle_length = 32 if 'CW' in self.condition else 28 # shorten next loop, adds 4s blank + for fi, frame in enumerate(range(cycle_length*15)[::order]): # 32/28 sec at 15Hz + flip_time = initial_wait + (ci*32*15+fi) * 4/config.FRAME_RATE - 1/config.FRAME_RATE + image_idx = self._images_random[ci*32*15+fi] + self.img.image = self._images[..., image_idx] + self.img.mask = self._apertures[..., frame] + utils.wait_until(self.task_timer, flip_time) + + self.img.draw(exp_win) + if ctl_win: + self.img.draw(ctl_win) + yield True + flip_time = self._exp_win_last_flip_time - self._exp_win_first_flip_time + if 'CW' not in self.condition: + yield True # blank + + utils.wait_until(self.task_timer, self.duration) + + def _save(self): + return False + + def unload(self): + del self._apertures + del self._images From f734d3b1ce86531bf984d28092072699ff6dd531 Mon Sep 17 00:00:00 2001 From: Yann Harel Date: Mon, 26 Apr 2021 20:26:28 -0400 Subject: [PATCH 147/170] created session file for supermariobros --- data/videogames/supermariobros/Level1-1.state | Bin 0 -> 800 bytes data/videogames/supermariobros/Level1-2.state | Bin 0 -> 799 bytes data/videogames/supermariobros/Level1-3.state | Bin 0 -> 812 bytes data/videogames/supermariobros/Level2-1.state | Bin 0 -> 1589 bytes data/videogames/supermariobros/Level2-2.state | Bin 0 -> 1591 bytes data/videogames/supermariobros/Level2-3.state | Bin 0 -> 1595 bytes data/videogames/supermariobros/Level3-1.state | Bin 0 -> 1576 bytes data/videogames/supermariobros/Level3-2.state | Bin 0 -> 1591 bytes data/videogames/supermariobros/Level3-3.state | Bin 0 -> 1583 bytes data/videogames/supermariobros/Level4-1.state | Bin 0 -> 1586 bytes data/videogames/supermariobros/Level4-2.state | Bin 0 -> 1582 bytes data/videogames/supermariobros/Level4-3.state | Bin 0 -> 1587 bytes data/videogames/supermariobros/Level5-1.state | Bin 0 -> 1587 bytes data/videogames/supermariobros/Level5-2.state | Bin 0 -> 1588 bytes data/videogames/supermariobros/Level5-3.state | Bin 0 -> 1579 bytes data/videogames/supermariobros/Level6-1.state | Bin 0 -> 1589 bytes data/videogames/supermariobros/Level6-2.state | Bin 0 -> 1583 bytes data/videogames/supermariobros/Level6-3.state | Bin 0 -> 1587 bytes data/videogames/supermariobros/Level7-1.state | Bin 0 -> 1573 bytes data/videogames/supermariobros/Level7-2.state | Bin 0 -> 1586 bytes data/videogames/supermariobros/Level7-3.state | Bin 0 -> 1587 bytes data/videogames/supermariobros/Level8-1.state | Bin 0 -> 1943 bytes data/videogames/supermariobros/Level8-2.state | Bin 0 -> 2173 bytes data/videogames/supermariobros/data.json | 60 +++++++++++++ data/videogames/supermariobros/metadata.json | 81 ++++++++++++++++++ data/videogames/supermariobros/rom.nes | Bin 0 -> 40976 bytes data/videogames/supermariobros/rom.sha | 1 + data/videogames/supermariobros/scenario.json | 28 ++++++ data/videogames/supermariobros/test.state | Bin 0 -> 1576 bytes data/videogames/supermariobros/test2.state | Bin 0 -> 2441 bytes src/sessions/ses-supermariobros.py | 50 +++++++++++ 31 files changed, 220 insertions(+) create mode 100644 data/videogames/supermariobros/Level1-1.state create mode 100644 data/videogames/supermariobros/Level1-2.state create mode 100644 data/videogames/supermariobros/Level1-3.state create mode 100644 data/videogames/supermariobros/Level2-1.state create mode 100644 data/videogames/supermariobros/Level2-2.state create mode 100644 data/videogames/supermariobros/Level2-3.state create mode 100644 data/videogames/supermariobros/Level3-1.state create mode 100644 data/videogames/supermariobros/Level3-2.state create mode 100644 data/videogames/supermariobros/Level3-3.state create mode 100644 data/videogames/supermariobros/Level4-1.state create mode 100644 data/videogames/supermariobros/Level4-2.state create mode 100644 data/videogames/supermariobros/Level4-3.state create mode 100644 data/videogames/supermariobros/Level5-1.state create mode 100644 data/videogames/supermariobros/Level5-2.state create mode 100644 data/videogames/supermariobros/Level5-3.state create mode 100644 data/videogames/supermariobros/Level6-1.state create mode 100644 data/videogames/supermariobros/Level6-2.state create mode 100644 data/videogames/supermariobros/Level6-3.state create mode 100644 data/videogames/supermariobros/Level7-1.state create mode 100644 data/videogames/supermariobros/Level7-2.state create mode 100644 data/videogames/supermariobros/Level7-3.state create mode 100644 data/videogames/supermariobros/Level8-1.state create mode 100644 data/videogames/supermariobros/Level8-2.state create mode 100644 data/videogames/supermariobros/data.json create mode 100644 data/videogames/supermariobros/metadata.json create mode 100644 data/videogames/supermariobros/rom.nes create mode 100644 data/videogames/supermariobros/rom.sha create mode 100644 data/videogames/supermariobros/scenario.json create mode 100644 data/videogames/supermariobros/test.state create mode 100644 data/videogames/supermariobros/test2.state create mode 100644 src/sessions/ses-supermariobros.py diff --git a/data/videogames/supermariobros/Level1-1.state b/data/videogames/supermariobros/Level1-1.state new file mode 100644 index 0000000000000000000000000000000000000000..8b001a338291b8e1a8444ccb8168a83bd2968dae GIT binary patch literal 800 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 nl|S#DP%4mvPw{fyeY>t~RYaw}2{Zrz>&rNu literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/Level2-2.state b/data/videogames/supermariobros/Level2-2.state new file mode 100644 index 0000000000000000000000000000000000000000..2c9ebb801ee8f163fdfd14e50822a7469c6eb841 GIT binary patch literal 1591 zcmV-72FUpziwFP!000001MQh#Y*SSn$IoqnwPVoBlrf=b2i0u?^KMWW1l{eQ+pVMj z=N)54yInhWSEB?F?l-yp{mwa`{?56-b9&F&UC^(VZ)_)I=n29l4A=;X`w6ko!ieT& zRrJb^qSG*jj;Nwjj-xXb@PVTxyb(fZ9AZ5p96EaH80jGQvvz#^@@ej{TPDt%!ky~4 zeDBEGxcVEbb>pwri&sCVbN z_v=N-w+(K0r`s*K+xFRAtsB;^a$7nqo0|IFo4*&%lETFF<{9D2?OG0|Oqsfe)lgk1 zuOu#wdzJlyK1Ua@)mw>R?_{4HP$>{W5G)G_EQybD(_C*n%fW2@;_7S9j$I&Rk`OzK zViz##B!s)=i-NV`mO66Fwl`&Yntl#?BN5&d+8_2U!={k`@WCqoMdoYt_LHhlWxMF? zmOU$3oAo_9plH&YV0)|kzpJHjLv!nV5gy6zjm<5qp#nzac6W6WftBgg`Zn}>@2RpE z-NiBRebLl|qv`1@(?&XL*izB$`2CwQW$G@e-f^U!;t6lWMXjTAz1^|b>2OGGPDlGX zCuwPKCC;7eNQ=}?n)W(X3bND4gZ}4dJU*0Q2dgYS4(^M^_OVl#5Kz1UejJOd@i2Er zW5FPIhLinJlkf%t?EMe1C3TMs`-Xz>x~E@>@>fF39;2UwV+kJvY_`GJn5rM?!fI0G zk(QsNCOF`avX;^HD@f2EO=9=`{9u`#=(1-g&d}rCD|RQbjw(_A00SJM{Xu?iv_=z3 zKkK4xO)?RJqa=0w+)*So4oVYr@44@N?K;{!80WWt%n~RVU_Og!J z52(CD$yh=OD_&J;>&;8=o|+(}F#FwS%x=Wy+K(Q>ILrGG)q?DO0BY zHxf$aK6aHIq`)ph#_1pVgJj-P9eH2oR(o4sw5(zI7?$%`7T;ku^~3mNpKecF=shvK zV7|Q_ZRVNhYw9a1 zjKjrpAct$}*;|zOg7|z;lsk#_HnjYWi=M|Cjgv}_tK-u;_V3_4e*f5l_}8&MZ4h7Y zx8C^0nZ?IHisi2j;_Lm^8-HeI@$u{GnEChlsg@p}EQqhmg7IrOvUGl`b@?C0H&X6q zny;_S23TjQJqGJd7xKmRz=?JE)D7Sz&pv|OB;21}2eR9Exe<#lhLo1BJxrlK6efC-vZp+t~&WgzPJ{+Y#lyz1@n)5 zY3I_shQB1zSNCYS)9-Ri1~M5{*lia@N?&J{jbLS zOZYtlzVKlP+L2EmcREOhHW=|=!M>DQh4Y2+^8thV{StT)`OH)JTruD^*Gqe6|tiYa5QsfY*KjUc&gLt@vCq;Kkp7FWE8g$QOME>j%dZ zM?Sv=*Rugn@P6|)%nS080WabBMdUM2;`;X*{9a};{|z|qPRx@5pTYG|T!G_2UN+!m zJl9CbXFD*jvcdf7^R*ngc3l4kybH%uT88;YK5oG0&jFu1dY+Jj)V>Z@djsv8U<+@7 zCC9OhRz7SM_2GG#FBJ(i004C&Nd5o- literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/Level2-3.state b/data/videogames/supermariobros/Level2-3.state new file mode 100644 index 0000000000000000000000000000000000000000..829bc2869ea708e5b8075ae75b0b9cbb6f459d42 GIT binary patch literal 1595 zcmV-B2E_RviwFP!000001MQk$Y*SSn$Iorq*219IDcgji1*+Rb=G~w$2)f%pw_8X5 zTzb1d?8V|!O-N`=jASv{^#MUiNTVi_7^LcmVKJsgec`DLO?=VRCtr-25CM76ilTrf z_1yFO-EN(`y+(=PxZmXV_dDl&?(dxYJE!-a-YpzdDmS(eGW;-M;szar$U#DEv@ojr zwJLh##?Wcniw>`#Q<2d*6$*f(CjBu&XdIjUAh-X}i6dkaS;`uEch^bgIJ6J^vI;CrAH>SlcY1-NhX^h>1;yG zXv|VQ2>HCp>+SM-Iq&-2PEY&V)hoTWPWy)D0q@4Ixih3VIlXa)yK<|RgC$Fr?qJ!f zA5@hU#G^8=`n;g`(JI?i+obtT9V_T(XEhdQw=E#B#6Q4HquF>Gz#RVO%l%)p9VKLn zkQQR+>`o5iG)zIbzno|%j)ppM?O;yX9;cr}{#cBKi|mO8mLU-d9@sa}e^&EVdig0O zplDCgCx8FAg++SZNk}~9PioIr@4r{;^0h7Pb49o%d)KwJu7U~}k=NVZ1>MoR^)2Z2 z&QtYSbQ{OOo{N_59!*bQnKsfH!F3biWIzKPj-kX}Mc>jz zm6XCFH5{iVJQ$2?En{mMNjMl!VfO=UW0{-mcIGBe)9u~!PB*cSN%7#I2Dl=7!ffAY zk0+%8tqYfsN=9HSNu3~bBtenE>Cv5DaGE6{mS9&b_2^Eo?g*hCY|@=xaBQ?`1Ow_$ zFF0IOiVNChoX{>%iucU(d#e{N#kc6bGAc=syI?ZR=x&brj6ThXU<#6_M3Q@0Z$5u8 zs5Ot%k}nWpxw84_Ea*{vN@RS_AB36njf7(8U>bbNcoYMM&c4Cr{;<>LrtW0X>on&QQ9<=XRvSi7UB}^H`$^QqA%F__R*@b8rFAKe8bH zb*#@A#Mk>Z8$Um@`1r@L{Fy;~yny#~V7=)=zO)87-h@xz0A4`8xT8K_ybgTse&nX$`kXqD+sw+# zvFJfQ-wwRTB)<9^@G|nHzWV&^ufP}DfKyF)5qTbY!GzCW!~C}bH@n>=|HzkC1D7-5 z(^oM6$d|X(=XvCF_W>uH@Y(NyFW(DX!B?LzUB>()&ztZ9^63`fvRiQ;1{OarcM0>4 zd}#~jHM}@qz6iYM9-J@aa|V1K`4W#@FIdrlFYUnmBVX=8D=p3!zQg>xF#pKs4fxsf zxc*mR{slbGfG@rsfp+9GN8K)xr42^>r|?`#uf+Mn_=S+c^?m|8k9_tqyssGWD)QN8 zoUd-|$AB--0AE^x^M!mdXt0ic0Y0}J=W8>L%YavZ0$#xQg$Pw;vR4$KSkf&nk!_<7{BkKp?E8+>2RVg8$N++CO_13ruEq11@u zKwdQ9Mcmg2$mcpSucE>H>hslrTnDay1KxwEi|uIDb67^3zXWy`t>*`@X|$qv0s2Lo|6l`G t_j$0?^Nv?Wz&Bmw6r873iy5KL2K!*(w{@GF^vf{001HaO$Pt~ literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/Level3-1.state b/data/videogames/supermariobros/Level3-1.state new file mode 100644 index 0000000000000000000000000000000000000000..11b878b7946777813fa31e70a013f697a2489a4b GIT binary patch literal 1576 zcmV+@2G{u?iwFP!000001MQh#Y*SSn$Io3i*1@1<%9yNZ2i0u~csCdfg6{Uu?bgw? zrMLS7PsPNfCL}Z_MzRN2A5fG88zqq!u%+FXAFAQhA zdKJI2BXDZ>z;SADreknU1$^XW;@$zqc$_NRL23V?6NlLrb}y^>gW^fy*!2(B4e54u zJbgFG#@w9t9lBZ=?ox7=d)pC!mOskM;bcPkiCe}|%S8mmEuw=>7T`YZ1WybC> zy^Ogtth`45J)dD~#SONyyQON6{@G>}dAO>wn?)6soDU1LveI~}$vk^$!}-r%;+xkv zV-2iEs%elAb)3Rge6WS%o~n9^fA)I^21K~f?ywInEaX41ugrhJ@DrL((|6I9^w zE!{twn7A@wrZc83GrASue@m7u-68$cW5j&L;@$x_w~qETb&j1bheK|0Ia*h{SYvAw zb8TJC8s%12ztg30P(KOX?|**AVuNx06qV)2!ClelE^!JAsNR5ZVp=RLoRMfSD4d~0 zFV)1ofq*!x+7pJyhJ1rTdfn5jM#L+j>PPrz|DL!{2kiF#=pM~D#)Y+nCL+}w=O)sZIAJv+>yPmU z`5k3`=gfnvkwIazHiBrB}em6SaW7wk?x{nTRd49%H2YF?# zm3!vesUG9~gUcmVu`-nk@|^b*hPuyE{pCrr*{kh*1heg}u<56tn^Qld!YEyO4wUem zdi^a*Y+igZD9#;6y&YD(aWV2ZN26>e$K~;Po&N9OJnla{Fa9;ur%d7-{Z<;kb8_MF zkD&aeNqnQEI@VTqVeWIt1(!9Zp|AO|VCcN`k;!AbNJNTl{Wc^?~G4S~(u%1nLhUd-O zkr(i?2`^*(PVni+vHra#-_BFDF*U7RYD8niTY?4@SZDcdg zkac@eK8tqP%sG@{bC<|Y!McAWn}k)A3)C-c?xS_mqWi(Ut`9ef{oX;P0^9$nC@;CQ aqq20Hq{#R_bii7=uk;tv(%8-kGyni*w>%*L literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/Level3-2.state b/data/videogames/supermariobros/Level3-2.state new file mode 100644 index 0000000000000000000000000000000000000000..4dd9654af9c7a31c120d6f08a9d84046d2280823 GIT binary patch literal 1591 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 literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/Level4-2.state b/data/videogames/supermariobros/Level4-2.state new file mode 100644 index 0000000000000000000000000000000000000000..5ddec64c5a017ec54cb283b20857359ddd8b87ce GIT binary patch literal 1582 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 literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/Level5-1.state b/data/videogames/supermariobros/Level5-1.state new file mode 100644 index 0000000000000000000000000000000000000000..2a7969519a7d73a75ccb0e7ec6acfc6f388deb55 GIT binary patch literal 1587 zcmV-32F&>%iwFP!000001MQk$Y*SSn$Io3i){Tu;lrf=b2i0u|csCdf0FxgURD7xl2@MI6EGDZDC`z(4Y9cR4m64dmm>PZJsSHi@fuwJ|_%KI=$%EEEAP|#! z?)iOh*Dbp%DglK1P44~u&OM*|JLmq+={;u`{9d(iXFFqqFESyi*UDJf%UA_33@1I> zC4Qtv;8c&ov8!+jVL0b~-Q*-Ao&m;qoC?cv>F|j&C)pPEIBVahZ=4m5RXaKEN)M{x z!lR_m>uaSAzge#R@)aNR*Wa|CA5PxpeH{6V#rP4Aam$Z*d=HDocd>XP#A1n%JFYR0 z>%rLNYL~0k<&s=Y`)izy>(|t~s#>Itbv>?4-%A%*c6@5nv~=TsO$Sq^Og+M?D4w_q zE15GXyyAMn-=cags}^Sqt18|t)o#|#EqakvYOzWe8wn-*bHdb?>knd&em6F^;qooU zCK+>SQCt$mshp&6_wsC|vXD7nv5%koJOcwFT;M=|cO{5`_sF3V|Cr`y`Qt~`ZdF@F zuN;*`=PV!AFcyq@BHG&O`tNF}UB9|!PVAM^jTbri^sKu%@W{@%uMr%G5*BKix(wRyg7raB^#FT~}k<>#*76CWozg zt%Ef*H!{c0wX8vIW_5cVDhIWl(3Ae>XE;0<(Kc2oz8%~j3hmc+VUkbr_{4TBtcLrA zGZOOqg)`Yo3J{VWkd;jdo{ooIN%riMoTcF z^k_{qMWT@aZ6&$m6^^W1WOqnue%9JTnrc9YKWy;k5 zMpC}er5yzu%W4N>WBiZ&J~nT;j(_A9Hu;`=$#N3qlPG6Uw!g30*bifqUAmpP+Bq?_ z<7m-R0~z;1D|aolGhO=k2Zuu{{#&W0d{$oYeL`2~MY=vWO*VD2mG@x2z8N<0%4-X* z7nL2OL(74?f5CO_D@u4#e9%V<^>Hd$P{H;NJz1?#CPfRb}e_bup|2{vZ(&Mv5@pV-o{sN9IpPy3A-D7+s zrSHbY{7S5+c^2DaFyC~6&#fcQUWSj|AzlWb-Mu`Yy-j@TS#Xo|d^I|d+A7NWe89sIc z`3Ilhxjb(NpL&M4a2Y=F6Y=?{iOX~>&*$clfAID)ybM0Jnz+Ocj6=`T_oc2Q|KM}m zkyroHeEu5o&ZjV5;8O;C8hp+UuAQvgfY0ql{=w(lV3no$%#X;w4fzM3HsEJwG5=Q~ z|1$0~;Ip3us2+U$l*7gnyuyh8hSsH6J;n?1Gd_do{hD|?_{7V2uNd%2@QFH%*EY0c zz~`rl&#lCGfzNsk=FuO-r)n`?ThT8AKKU#0GUDgA;JsqN+kYoMUxU1Z&vhHjAM_^- zKJyahvjNZWycsL<0$w)YW%S<;KCv0|-(&E5xrF>zqu;H_lL4Q=e8{apKft>UcsKSn zGWb*r^6EAiUwyo)z%^t38}Lr_Ctr#DgAW_~L_KWs63VdY>tttOoj;R}!Mfd7sa@Fg7aOHz*MrAhpKTNC ly^Ts)cK8KRUh!~yW$FRRE#vpl25ahx(%(?W!|Mq&006q^JUsva literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/Level5-2.state b/data/videogames/supermariobros/Level5-2.state new file mode 100644 index 0000000000000000000000000000000000000000..55041cd811da602036475d9a57aa8919d3b2450f GIT binary patch literal 1588 zcmV-42Fv*$iwFP!000001MQh#Y*SSn$Ior|ryGM_P{xE#3skqE;O(F=2)f%pw_CUV zx$Euzz*F(5CL}Z_MzWXH2NWe)8YK}!(2C<_F{DPHcmL+| zNj>NMez#k3w>3%t;eM0b-|w9BxxaJn@0|9W-39$x`Q}bSMxP>F!k~>1)lZ0p7REDP zRz@X{)vK7weUuKBaU1%B$kJ@yrc6#)&^kl3uAKwe(7*_K{?24@sqCB$nxZNRnlTj2sJOwDec7p~o@9 zP+mox8M3^F{hnKNznOnpNn_=1VXd2PFhV!cZ)y-M%Wmd%P!b>KW_7*sVC=cC#>?t? zJ0UZKxL6dI0CAdTAl%;{#M)ddZUdNemgnf_kT(+H;X)%}AGVN?|Hz?b{>!?b(0m%( zMNbcZv!0c$@3asSO?cyMZ*~25wYP3s*RfQDpJZ3(y7o0t0i$racJx4Z^w;_h^m_lP zvKQUMG4OrS)Wf6M*=w^#x?tE+(Vh7In=)nU9;x1Oq@L8cH{zt$-m|gAzTaWDOPd|` zt_=>--qk@IyEl+_sf)DjcW4x3C!vS^&rekyjkAMQmL3O(W3gd=3JWOS0C!YP4RdEA z77TJ{EHMBzac>~N52Ch&?vXLyXb@ia3@B0lN~qxp`Z;tU?qh(>HWWLc>BqRRme6>l z##7V;2mMjjvbeE{1pUzjc0a%mmf5M!mh99tJ>GS;IEZyZiTVc_U=NK1`MJ>=jVlAJ zi(E7j55ZBAI)3gblBR;=)}3B(+7u}gV~(J>b*EQ%q<|Znbf*`bR@yX%0d=Prov;#> z*kim4zMJ_Y;P+M@T#4?~eQjJ(Ab0+Fkkg$4_c=Yy34a2Tr-kApyc3Vt?`O^XnC}bm zTs3>>Ea*`^TIk@CKL|7D84E^h_R_d>|3;p{02Zuwb{I8U$RFJCPPw48KgZjmJu-WT9bOcN7U1-zKzgSgYQDG7; zmIFClRnOj{sJF%EgQDCithb@%uU_;#R%wz{a=bh~tt*#lCh-2_x5dAK^(lk+dcXC? zFV3$#{s}C9VGv*Mx8C^E^DB>ESNHtC&(Ct{@yTuRby+Ze6-R34XSo*tVSFRyZl(D8 zN^gR7mfUBs-gF^f+z6alhfm%FUP8XGw>Dq60eto`AoYV!-f1E1djT&4~$BQGK^)!}p3G5_trE$pe2f8>knfy>t6lh-i+$d`84 z=0)VQj{>LG;nP0=UwQ<%yr(u_T*Uk%FV^8D{ZM^^2ME)*Wk*0 z=?d`9wK!kMXASrq@j$cGRy$#pD*Wmkd5%b@Sf+Z)g zj8;Bk75IAV2Ag>Stka9-Ic!Ipxrk-7xvO9o&^mtvn?x(im!V&@xsSI9HTQ!DT_5e> m`@J1X1#s(tJUGynkjmOfbk literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/Level5-3.state b/data/videogames/supermariobros/Level5-3.state new file mode 100644 index 0000000000000000000000000000000000000000..f07d290430feda5d905f58515dd1b47d07bf9997 GIT binary patch literal 1579 zcmV+`2GscsbHL z+x_Fg6dz1YNN9{kvX|8d6d_p}HIWyj=)Qqd)3*S+ZpSBiU~PH4=7 zdNB4~t<%}+bV|-m`|2D`>({PwR<~4cuJ3kk`9V6za-$PlCZ#LOwH!>DGW8#6Id19| z#XnrLE14rz8k*lli)`&}qe?Z}Gg@jX#;U5QSU{jfzDE#FYsnoipqyYB|3Drc9Z-M=G}+DW^!(6LfHEZCzJq-D|g6}iedO}v9U~#&REi;V|;rzX0fx%AtmJP(Ew{;zhCSdD??GGTN@(u zax5C4tt5B6!ck;3LQb3Rw3Ab>$icAYNJ^XTwCj%SYeSdrw3AcEy9N+YciQ3fDIrKF73`GOu3gf~XTQv=ccVi1?d>(#nHQVy-=KLO-IoE(M2nS_wsR#NH&GJeIDW1EDpv7wU>3=p5gNJ$HGyi zPw}YArjD%q{;5&Ma#P=bF*~Z^r$t+r1NLK6rc9YKWy+K(Q>ILrGG)rt|3*?_wo^N1 ztt_V_W%bz>ZgomIlbW zORd~lYNtE(_YZcvr2mpCRLIFC?iGn z#q%FR`&)zf`ncutPfjkLzpj?ae_o&Y(&MuQ@pV-oehJ5xuFrg3zQg!N%G^qb^_5&t z>ny&A`a1F1M&eRscsF<(c)1LpxrX{T5I4Q6O#Q*<*Aka5!^f|n{@@Ecm*#EY z(~l7sDZ?j!Ccf|}aaq^WeEu@(58hUWm%+!^5SQG6dFWpJbLmT{KluE1)YZQ@U${uT z;}Ogk__P6^0iU;lYbWbA;PbmtfAEDiSY>fO`xEMKMg75N4EX68tpC-hzl_fr@VSoy z)DJ#!!fs_r-eJUlL(ir7D$Ez+XMG0O`!(@4@X6+YU2Y9yu@5a7H2A^(0UEK!r ztIt<8xMr+>1Kxpr3RS2-_=o|YJx_e*=nP{AxqXXl>TTF}$mWidWhc;vbsw&j#CmHX zn|g_?!-MvD^uwm6(1y)iB0CN1_=Rj7*6qGP-z+WpJb2Lc**5XKw^A#|4m~N_ dHTO0vQ+G*j8Q+ICSW^#`{sz?!T literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/Level6-1.state b/data/videogames/supermariobros/Level6-1.state new file mode 100644 index 0000000000000000000000000000000000000000..1c5f6cdd65bcfdc40c20eec024337a4f73262e25 GIT binary patch literal 1589 zcmV-52Fm##iwFP!000001MQh#Y*SSn$Io5&r-eZ;DBBpF7N~Ah8Fw3nLD1d)x!pSY zkKXPdiKh5e6A~H|BiYO912QF98a0s@r095X8d9TAJe8q|FD8BS#fJ$ICJ$N^6o^SZ z=lp)RYnR>CKNAq{H@W@&&OM+0&hP%t>Ah!nK95?w(@V(k(}YVHG!qi>5K==MqbV0V zMUV6tIt}CKh$=e82s-DyZg5gjSC9}IhnS8DM~5+(2OOHfiABiXSkVG;};>oa_V9djM z5b{lf!_nz*2#&1>EcTWyo7Oq%JA`eG1CH(A2^UCia(4TiaDBCwgCRqP{v)l%O}(Z( zP;o7>rz%6!yJ(4R;9saI&tQEQ`|KzsRL26A=OtiEe1e+|)$*waBTrd2uid+okQqW8 zEQ&*bI88GUuKJZ#sus+-n&;`~kSiGE;rs^!ZtNj`&(XtW{%g$7)60*kZk0VnPu@IH z$NEg~TL=loTv7IHb^LoZt=Y1%Ww8mjWJlY^ruA?FGIBV&I$=0^w_b%&A3T+wMfWiW z{#-Ql=xBEK`mCNV==PMf8oz%-h78>&{oQTEQbnS!pq*N4XRF1!-)6N+TW!|%W*cd0 zZy~nb&7?_cCyo1UDh1h2=u!XkGZGn&vW-=mZU+y9!w2{-%&WM(+=-}>0C&d1J|A~R zVgqm{>hgN|&eI&zJTl@Q_QC6(0VTv=3DrJHKZnMnZU&gmL*a2%+tLNpn93v7oubA! z=n1i&h1PoF^MqnJ`~crrrYGAh>B(ujy=$}Bh^a*hc?KC^^&j-{eWNK9RR-7)pY+C} ze%MM<$HN^(QX}AWYfcY1jfxZuGe=OmHK#{&ByTr%X-*F~wX|yl18PnWI1K?MB(cjl z%Pvp}bu)h){GQTLC6ICBY%i2TB>6o$lRvhk(X_!uswzCEVjjWnT>xxG1ITv=_`HHBfDNL zSr{NIS6ZpR($4p5?;mV7q5Pktq>z&;-cM-iy9nnO=D?WRI*aeqS#O$<&$j|6R^j7!fR~WZ?OmDA-3C6r8Mzs_K8psVckuQaY}%2}v;c3f z5Rj)az>CNycjA3Thfg7&Y{dEM!f|x? z!W{7VwK!kM=R7*==qB*#H8@{8FfScGbpv<_;}@R8`-%=P{tA4-f^|ne@77sAm`?=x z?6bI@b$Ei;n>Axyke76L3G)|`Pd_Ze;wYA`4nog{>Vpk`0PdCGsiCza+umT!KO~2eG6>vZLs7d zw$aK*O#)wU9bi*0gSES`eG&W7rY>O{ZRQ%-d9?PQz{b(a@)Z~tZRWFW!ivv>hh3j` n@#no2S~+s$Dc-KXzgZc&N0KG{J#?cr^hoJX_5>rk2{Zrz3JfY? literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/Level6-2.state b/data/videogames/supermariobros/Level6-2.state new file mode 100644 index 0000000000000000000000000000000000000000..eda1294ded0687b5c8160c6eab4ea3afc065dfc5 GIT binary patch literal 1583 zcmV+~2GIE*iwFP!000001MQh#Y*SSn$Iorq*1@n9WlUDIKy{nSyxSNIg05YAyLErs z(%b!kr{YshNN7w9WIn7upePAx)I_2|3yzmXO;wBuPo-$$i%DOIFFs6&FnQ4W2L)m> z&pp5I?N-=bjZQ$g-{ki9JLi1v@0|NPr}v)T27K!5wGPIHA7esNpN+AIk1-1`jHbL= z6+hBraB6nJaj0-+BXCaod&o&ey+e%gILvxL+I#TWA=b+7X1(}f$8q6km&}~krCZhU z?46{K-mzNO{bIfN(%rL9HEwr{+by}*57fFE8dtA$S2tTW)b+YIek+}3xrwQbdFk>}EeBJkO#Me%ikrGL zKhW$7=1P@^=6BH&TXWOd7E7K#V5H_!tSZS;J}wC*$HR0&rSVjed3$2uvkiT9j7>7u zs6}a%D2`>4!rgo!He1yqahH)fX?dD|_IrnhM7ZG2!5*}*pl{!vdHxH!A5(i&?J4^C zC#UMPvh}T6#zHY~RC~6%|Gny$H?C@!E7C35y>3ilJe8hB zw{Z;OxoGO%(bUxCDI=XWY$<6ezW=67nYvA?v>mCWNYp#z;?~}>w${GGX}8Pko%W_R zPFCO4z?@swuzIT-I^-@jL>-USzqb;U;Y@}y6K(Bjxm5_KPwCoZ7*}p5=qX9Nse|VRwZ{r5lm?|Pw z9pxs_=L=~qGi$3^z!!?4`(ClJOi!q_>4|fEd$+FE$*c`Z$k(R<_TbKd*f&~3QKeVw z;>1`i8lVy7Va<`0Hr;909ogT8Cf#W#X9aH>K|tMUhcl>z zWbHEclixPaZ!cZA66(->byQI(cfM#q&{|HoFX(fE@Wm*3YB0J}bkgbd`Lt%Oy0a%J za%Jh{v!F-qRD-+c{63nw&Jq6*9HgOB-93naemH?4%6xRx?-hx4*j$W_YA@@!JFk7d-g}~~%LV(sDO09QnKEU{lqplDOqnue>VG4t zINPlqvv!u#4#vj$kNkc%XSoi%>k&5fx@W<13gts6XHj;%q1pI%r&RX;W-%w1y`Rw4b%yF^@?_IjTKEX&+M8gL&pcOFUs7(I z4lM_s!LoYoElT8u_+n6;JBoT6ta$aJ=dn!Vtd!&V@p+y0cW@TxAG#s_Rn#X8;_LlZ z8owjI@c742{>mV}-fyMxC-V!BUsrSfpXX=3^!V(C__{2JU&gV;^D|#Fe>1+3GS?Gg zeI*-doyE5stT$cY3u}pURN&*+h?l|Vwk^)*t`eVq2;3xHU#$+LH;M9c6kXsm4aB=D z#83S}d<1-VVSn-4nA?%X=h2^V8nk(&!zZE%opNk{RY?j1@R8>$)|8%G2m0+lXaM{R`g@Q7xTmy zR$#us=X?h1=r`ii%Q0V@FfIc=^)vA@;uoL9eZ_!x{7QVW7I_C>=rLG77*7Oz_6e+K z1D@e}vo_=fyllYB7{3F2@^P$xufg}_Eb?E2akn5(27D6hp-_!+fcF^i9_(vm@abmc z)nhQf`g~P^Yr^_B;9VF`aT)RtK4QRU&k&zEc%HF6+`ddU^$P5(WOJ{PWrtCQ_3X1s zV!bt!O+8E2f1XiQ@2PS8Q+ICSX1|u{sQ@W8zu=f006aXLT&&6 literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/Level6-3.state b/data/videogames/supermariobros/Level6-3.state new file mode 100644 index 0000000000000000000000000000000000000000..be2901c62caa815e828520e3fa885f6d66c7b6f8 GIT binary patch literal 1587 zcmV-32F&>%iwFP!000001MQk$Y*SSn$Io5&r-ec5lrf>h0@ZCQa61?bg6{Uu?bgwM z>+Sx)Q}L-LBs3;QvXHDkpePAx)I?s8sv{wbiS^He;i(Kwe9`pH7a!(`fIMhL(SauQ z+;hISTS;$gbOOTtCbz%eIp=eK=iJ{pz4!EPey>uxxt);FCkT@;WF{o)C8U8C#?x-K ziXNE>I4%3(*c3RWD4erC4|mcrcZd)ghnS8ChmM^-PWs9Htc7!5pJ9%A$;5t3xKkaM z?j3br->|`C>b!2c{L`m&&R=@jc6L0yK*u=oCrQvFnV^;)$>biANbVxZRD>i_5h)SdOi~N@C>}+da%`5nboX)L1ygPcgUWQ)p zJ(ZtDcX15txoGIY(ag-X89klXZ7FLxzW;^{8M;fVw;idcXv`gQP;2d3XR+?JTdm?o zyR~browRp#5c|%xq+RSHt$XbX1=XFby**qNCuV`Dk zpb}SDq^48U_=mhazm99dMN+;M46FLzpHF%)4}EW0$PS93(43r(8S%bh0LG=_kh z(+ejkheh==j&a|$#P2O%xE$WD`O3H~^W1r3enuY^n9t~WMtI{qc}gHQ!aC`5d%bFN z3oZ3|0&!IOd|K2#C2(NTAL29DH|7h$K^pp$13~RvIQ|fi7901unPW3M2pLyj)=~Q| zm3uTEiOE6Pt;ieubK+a4r|7Qy;y0g^rd0e4D{FGVzHi8oAwz}?88T$ZkRd~c3>o_0 zNGO&D)T3l2dG#P~=x>ODa<_FIK#t(A0l{*I%9GHgltgj$pC93pVxa^A+`F z(oC7=Wzb<+u|>vKB*I5 z>$l$cZL`(KKY{X>I`OrB>y1A(TYdbRx@Z4+ewIp$Pi~8^sR8jTI8r-5OLg^c#@AE! zR+6o+)Ox+snd$bUQM=62P|Kls8L&SmQGiEGF| z_~OpmybXNj5za;H@Tng-UwoKzxxU(b;VSYE-d2Yf!6(`{m)e1O7_5F?<_hu;zOWs6 z^;hSMmpSiv2=fI#qr+#x7i{2qxs`PI!fxare9;9fSLbuzBmY+9AADAapPR?}Uyb~W zI8TSqzZc-`;FBlpR+6F(di)psxs+Ii`9l1hPv?3+=e!MkY8&n=I(!;@sulCK75(V& z#aYf5R${)u=e;`X=vU5XR$#uiU|c$U`e)9Ih+ljf_Z1!9_6z5W7UUg#!K1T&FrFy* z+*4T3Iy}Mk=FG?ocu|KJF@78P)RS2MZk_MTMdZH)O$d2qk$gRSg& lkAE7>lS7ZQa`W9Cm7zN%NyPWT1#9Sm(qHt07DEX%000}qIimmo literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/Level7-1.state b/data/videogames/supermariobros/Level7-1.state new file mode 100644 index 0000000000000000000000000000000000000000..53bb25ebc09e172e93b20aaa89eb0745292ec283 GIT binary patch literal 1573 zcmV+=2HN=_iwFP!000001MQh#Y*SSn$Io41Ysa7!WK7m+f$BCDyz3|og6{Uu?bfaT z=M9~6j5 zJ@=gN?b@Yx_0I%^`%P|tzjM#0zw^7lb9(RDUBIW5Z}t;1_84IjhRuY;e1tU7#ze-W zo}x!~5>CqjI5q`NIR@v9-^-m$+!H2*#%VGg6%HReeVq8oy{ya!Gdbp%)erkE;dTvN zzMEvC>iXGq`NvP_oWJ<8?aV~x2F-Eu50a!uDoHIpQmMTpnc7WK=_pC2qf$y`?$v{k zFI$|>UZ+!VZW*;WIyyJ5b++^fTib@5+b#%aNnv_!+q`gXrIv#sLx%n%t;9{esX9=3 z4RK^@L({uxljgUuFPN$`*wC+j_EZvTRs&Y&C7@+{n3+|L@&T~ztCRhmFO3p1ONdjA z;uH{dmWOM6Wp(48X?mJ|j(Eah7B09yl`@?X&WgyL1yr|5;&Vzb&dy=5UJ zlJLaUXRGtytG%^zeaB*x-;$kO>)Y4y6Fdv2b4M>9j^3?TVAQ)$m1of%%z-@@4c$MQ zo4Yorr?a{}6|KbY-;g0gcSwJC8?jWexF_tO*4n$-V%=xATE#7PYxgEQY47eJ_FbDu zyVyY%H#Btg>`FI2w(PvR#;8_V}3-Q(__JOhy9%=8PwX_?fuJ z?`IoPb3*gTxOXhTU-t~j5%x-G)g$zCN)H zZf+(4UnGIyhuFq4JKbf;PS4QoU6;j9OdWE>H>?8I;Qj#HH<}`Gc}N}N8&@J8y+Kwhlbg3GlnNVNWQTr~HXDkto%OTmL$Xf>T z;>pv~gcQzy{c(9(#ou9VO%B-i4H+_I$dDmJh71`pWXO;qL;o8IrShP9l&z$o9)wKM zKk`S&qNO_ew#01a4XJKBgZ6Q>^Jv@NRBiIxso6oz&RiOp8Q=MQ#qt62QmqvSYwg0I z_Wr?c7b^cNRaGj8HSZ@h4V>fWug-Iuz1~YRSnThHO+WKo&H0K7Q~XeCAcbnqt8Y7{L}OG^Viff|Ih1FEiFD-5?@mj;@5Cw`TA7r>R*hnr`)X+TVLr;zRr?+b=I3E z@WsuXvo+w8H#skYFYH;KFWlgK_91Yy{Q4{!kloJOt!O&H=Q=p=Xb?a1JLhBIiv!E^ z3%_wbzln312D}8`23~By=dPpv?VMZK-Jt&9iyJwYZNMk5q5j}YyO!r|;Ij{MF4llg zf6w{S2F~T(%k#yns6TjH16~B5T+g}mPRzql{qJS3p#I>C{iti8K3}@bdB+2oFYs9% zJ_o*N1Lxva(&3AHP=D~HK3KUvpZ^Z^x1#>wb2|LOMXdjIsK1Es>F|a3g1jGm>V(}& z(zHX5|C~RUl4~(vh@bcCT<>R`w}DSTh5L#Qp8=n4!+h<)I68c3p7X^um@n`JpUyh^ zmGjwF%-440rNd`_;=G9Xr6+M;(cx{saK2X{?80Gx7j0>F^TvH6r+I59%uE%&#_ItHE_+ z{p;`!}%W>UgwsaKpR#%ViMST>)|%@EVm91 z+UL*@n>mj*Z0-uT3$TtKxJ|-J(j`7FZ0@72!m`hUdtD#yV9$FiZxzVlM_If1&Squk XHc1lkd+38TbYJOD1pH@q2{ZrzPoG5r literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/Level7-2.state b/data/videogames/supermariobros/Level7-2.state new file mode 100644 index 0000000000000000000000000000000000000000..ac85cd8f2b5f5b0cea3064cfd6e506619eb1f2d7 GIT binary patch literal 1586 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 l8 literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/Level8-1.state b/data/videogames/supermariobros/Level8-1.state new file mode 100644 index 0000000000000000000000000000000000000000..19db592f587f040cbb7774a5bbb3505340292d21 GIT binary patch literal 1943 zcmV;I2Wa>oiwFP!000001MOOCa8pGT-kY>EErp~gZFx4-VuC=uEfflb;odD^Wt zF$%RobKGzY@gZK1!G8`isjFs{iOag#RaZUQ4IzCeL0?fOEc*TNa~HiApZw_gou(tb zk?QmVeep?Rr9~W~k`^(so>&uWi8$gV))B8wWNSH9CLJ}jSjIn~a*)aosXR_)Cg*mR zNr#yhD~-=gg>|fHzpzTZ1lA?7m02C z+oMMlKaSOnPg++*_r;FJhY}-6Ad5(}k`jV3h) znaK-``iSmP`rTpg>XL(O@9u0ZLPon|+m;OdIi^SG^n-!c0K2-JzNnLf)9)=M#1pjp z*)^x`#!*?eU{3XP)m*(`?yS1HMRO`=Ljy!qS69CbhBH_NCiFP?{sZj8ckh;=Vz;>I zzw5Q{KX7c2eRvVLr@*(r^RBwxV!3s8h#qE;Zw~G(HrVFvl_Y;=g=*4TOR4SlI@ z|LSY@twf)_2COgGmC>_v_|3Oac=qqUXC<;TvQy0-I}|(g>1XPn&%aRr+-U#7!3%>7 z&JY~4P^bcBw+SRPWXNt`Fc>OET)855Bi5`{H8(9?+N@?@w|>JTH#Hs&*A6)Ee872D zx2yS>KJ%GiXB;#*)46tb#?|b(=QY=!dtPsL|EcGLm>Fc5>Z#-E=)U&zlMtfGr; zWTV%+QGN<@N_MBL_yWF8S?Tk-T(Z&=bVG~Z?sUpaV+bl5>1hqQ;B}8%^2o1*ir5dv zj!phnMlcvUyqf~bBVA`87?3>`@1w%i=I}7f(1sbr7bm&CI)diXN_d4zJ8$6T2Oh_+#Sx4m|3VSH%^-GSk+K)!g?I*rZ<@E|7qZGiz4@&ZQj$f`k5&S1H^6FQf$eVqbG5(7|J4s4xS?d&azFUP+e6y@#1 zb^}WJ)r*qHEDVsT9B2BcZS2p%aqPb<$NxCCZ{_$`#w`@TX*hrUeOMpi_*cd)6n}U) zfBXup!`GjmOep?Ij(-I@^q<8e>iNmU*j4)HA$EDIyuZQ=V4wA`=j=BH$WP7(o~Z!8 ze**X-@)PUS`H6AhN9Q5$FwC!1Aw*Zo^)jrQksqrDzPW(^$lt*CAwSuy&L8~?`0*;> zMGEk3$TuNhEWnRl!u(ePZ**;e{3Aa(7kJSE{Qe7=f8?jusPj$8kKPYFUjcsj7vQJv z175sIou3@T{3G8~fG;Aye-7}%t8pFN`TIrBWB!q!T!ndc<>#l)0pDDK>xKL%haW?J z(uBN5kTwoKxeoJ>{8R%cw&%*UW|8Xa0zNdh1LVoyRysmKg5#)!6hfi?6aRcTB z`67oeV*DoLhabZIZ|9tslbHV!jC&d8iNg=$en`&1IFN7S@NM{9BO*U)#k|@$>#M9+ zG4d?9|2cd!#*-?-{3GAT;m1z^KlaiYLbg!(GRVj)C|?Dccnu`kjdhf^ZF)i8Z&r|z zCqSC*SU-Vwl#!EIM;SX0aulWcSCIWEZML&8F3Q-)wSwyX;9ke^dij2j|HU^!o?a@~ dXWZVXH2g=fiFh6wP-^&J;a}=6!T`~%u^8YihKK@89;Z7GV=cKU-K`opw8 zI)mE`t?3Uc3T>~%jM&}xJzKJLlAC4%qc zBP8G=#6%-wF@+`35Fbawx?eX!Xk-Iu%({EQhy|4)LTEk2pAbmYNvy-HY8zPtgu)aP zLtc``uv)jFj-jwTX-_bui!^377io~TCkREVA#szCO#ZA`pXaVTx``EWu*kkE=9?Oe zsB&I$}^NtQ+|xHQTiz*sokff zXgo>dDebW9f_BJtQG3sHM7v@-t}T7t+ zNzdxYq7UA*hwoP>#B%z(rhB;4>iLt32IAP zsA+jcAT0}mxn+@Zh6b(J0{zg)=0>7Z%VylD_6(3LTZm;1^+(t(wmP-*oBy7eXew2% z*j{B=hb#?ws96tS>MDB9F=f$u6k?~bGb{#N(&1(#Y!-QWPh4Jf( zH4N|GlJRgZio{LiyAQ0XC)t(H2~%<BV+={6Oo(k=XIYjMgr7%l!dR zOcU~oAS5CYvW~!KD%@%}B%jI>=W$wVor&Fq0Pzj-)wZ67Epjly0N#9mrf zh%&o{hPrjEuH3CEpkBOoi$Nm_cSlrewFrovx2959B z1;GegwsiAgggu}8JP#jN%$_NofA-w<=|>@w7fReyrShNsJp>~>_XieY$707y z<*PI542%8tckE)Uhn0)vKlv2980%r>`l#jS=H8lPP*r>`)#N`^3Z6^>Q53`B5n`nW za;w!^+QSM#rHQ+|10tMsgKVooJBCODqt zqn3UZ<`@|chlc|qv2q9`5LAX}Nz%M^v%Ps|gS}b0!`{rkUe&ib$Y!aH)YD+!*$|>a zb}st3fBg&uMuO~M6~9d;`~3cW{1oQ)C~j^9LV-bUjQhP_Zj6TeASI}{-E3+aY+>CZ zqrD?uc-_^0ZhmWeZhmYQGyPX`VZX#xf$LH#20{g(Qm!BKWzM!X%HSwx192|h7BsE;z@JOKm zGpalty3q+ntw$R2Gehun=tie*NbU|Sr5l}Km}%B1GiqoS8l7OQAN2SnHjMY#06o5r z9J`a*e+GMp?u5{o#{)-jSJ2DVD%f0oKUcWI;Jna4aF`FMOL4haw#4k-0q!eH7o7#Y z)~?XNT`P7!%v{%~dk75}JLKzABgPuW; z67t;9osb^-`B6ermtOu&_9!d&D39xKB0p9^1r=0KK?N05P(cM1R8T<$75v{o$Ygt1 z$l6JY1wtn2m;AkCg()T;kvYeHA{ULvFn$8#35<&;8Ao25Jl&)7gO|G>9KG|z`L*}nubU)@{k}Uf;w|<(&eh*Gy`4g+^*D(EnLH&BW<@ztq z7w`W##$Ph1UvIZu|D*H8`>&&Y{_6dc3%!1_s(u|NtUpgjO8X}li)*ak2y;#jCv9EQW?GaF8aS7bPK!6_#gG@n?V;ZqmR6W{zrXg zXKB5N`uHb67bv5T{uT6@O`uD3mDZ;h(f_Cy%jhN4M>c{kdMD09U-5GBH_`v7Pwzm# zdW-8bZ-Cx;6V4av;|6*a^=T1xoxrkzKD`_LkNQjpvZuH{@fY;J9sQ4b)j+@SI(QG0=z)Jq0>3HvXiKKdzK|BAtWxrF{-hyC7)elpNUaXqA~ zupg+G4fHaeYb4ai+tIJG!Tjp;Wkp>Zu73l)6Z?~~p#M=HFwiG1f?j>#b^f#ZF)_GMtFg7J%(j~u&%ab)#P;00vo-+&{?vV0lZMOJ^^ zER_5_xY6;%cK&&9hggc-w}r>6N|&nO27xT$e(1Pd__x{$Zg~6$JEsTD2{ZrzP0m_S literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/data.json b/data/videogames/supermariobros/data.json new file mode 100644 index 00000000..c0f5fec6 --- /dev/null +++ b/data/videogames/supermariobros/data.json @@ -0,0 +1,60 @@ +{ + "info": { + "2nd_goomba_position?": { + "address": 1206, + "type": "|u1" + }, + "coins": { + "address": 1886, + "type": "|u1" + }, + "coins_disp": { + "address": 2027, + "type": ">n4" + }, + "done": { + "address": 10, + "type": ">=u4" + }, + "levelHi": { + "address": 1887, + "type": "|i1" + }, + "levelLo": { + "address": 1884, + "type": "|i1" + }, + "lives": { + "address": 1882, + "type": "|i1" + }, + "reset": { + "address": 1905, + "type": "<=d4" + }, + "score": { + "address": 2013, + "type": ">n6" + }, + "scrolling": { + "address": 1912, + "type": "|i1" + }, + "time": { + "address": 2040, + "type": ">n3" + }, + "xscrollHi": { + "address": 1818, + "type": "|u1" + }, + "xscrollLo": { + "address": 1820, + "type": "|u1" + } + } +} diff --git a/data/videogames/supermariobros/metadata.json b/data/videogames/supermariobros/metadata.json new file mode 100644 index 00000000..c757c458 --- /dev/null +++ b/data/videogames/supermariobros/metadata.json @@ -0,0 +1,81 @@ +{ + "default_state": "Level1-1", + "states": { + "Level2-1": { + "runs": [ + { + "name": "ppo-10M", + "agent": "ppo", + "hyperparameters": { + "timesteps": 10000000, + "time_limit": 4500 + }, + "rewards": { + "mean": 4751.435068054444, + "first_10_mean": 973.6, + "final_10_mean": 6239.0, + "median": 5286.0, + "max": 9375.0 + } + } + ] + }, + "Level3-1": { + "runs": [ + { + "name": "ppo-10M", + "agent": "ppo", + "hyperparameters": { + "timesteps": 10000000, + "time_limit": 4500 + }, + "rewards": { + "mean": 5895.214350826931, + "first_10_mean": 717.5, + "final_10_mean": 7570.6, + "median": 6538.0, + "max": 9674.0 + } + } + ] + }, + "Level5-1": { + "runs": [ + { + "name": "ppo-10M", + "agent": "ppo", + "hyperparameters": { + "timesteps": 10000000, + "time_limit": 4500 + }, + "rewards": { + "mean": 5860.152402856797, + "first_10_mean": 647.8, + "final_10_mean": 7665.5, + "median": 6391.0, + "max": 9543.0 + } + } + ] + }, + "Level8-1": { + "runs": [ + { + "name": "ppo-10M", + "agent": "ppo", + "hyperparameters": { + "timesteps": 10000000, + "time_limit": 4500 + }, + "rewards": { + "mean": 9448.269531849577, + "first_10_mean": 392.1, + "final_10_mean": 11432.8, + "median": 10188.0, + "max": 15554.0 + } + } + ] + } + } +} diff --git a/data/videogames/supermariobros/rom.nes b/data/videogames/supermariobros/rom.nes new file mode 100644 index 0000000000000000000000000000000000000000..4342b2eec67ed2497bd5b4392d4c40279921a48b GIT binary patch literal 40976 zcmZs@2SAfo_douOCLanW%t+jR_8laY7 z9I2qCrj#hLgvJpyRTRaMI3TU9~p z?zzvT@zZ*7EX(};dGowgWnjdbu8lTMRNeFam^*8vjpvA4ZVx!2x@GGkZi|OApA}CT z4M7^i0%Elox`<&bu__HcG*-dDilSI7_N?f5jo98KensN1724@VY_01T6_cK>9Xe{< zlLc?r9lZQ+zP!BL!1KJ}KOaVeDv9qI@bfOKnLW;(=fVerYznk@>+7=Nv?@6{TAzn$#5~8!+7ZO$8o~V>q zYA-wQ@QBA=8$Zzv+-ycKqxwyC(`KJn(bHJ%ABTMh9}vPKwch^>fthJyr)NE7Q-*xn-9tl=#p z_FJLdSKKg`chIvF(tY{oljA?DVA|(A;{{nH9OL!||xG#u5HCPmG!7~m|3y5J6 zF~ktip{^c1{316Bwg; zyhwl}9jTa0bR;HwMT=SuI&k(`QN{3Os=olCS8i1s(0z+R;B{;fw>-Uw%{a^zr5{Q^ z#2#X~B4(2+LOM`X_jpz%CM1cfA~7k6?P40ji7kS70^2&mNowPhD-026s&UK;jG`ri zMCe5G3Lw{u?n`81W|P?ernbGK8{NOO+E+BLxV2)|%7#eNRx;Eo)8E%lDm61Jhgs$y z9qJJ*C2pOazdp=WDPFhRdXb|;yZ%WC?Hn0kf{@od@jEF`SoFzVcr6rEt`0< zY(0rL!tG}t1AI0SkDr@24sB<(5i8xCaX_6%W}8O5p)OC5%k~0s6R%%clf)j^#M_lk zyhmb{=xkhwI!zrf z5IQN-0UYLyQab1-QR5B7^L=9NGCWJfthKtXu3Pv1x_A5DoqKoh-J1N@JanxwoUP(Ha8VWEtXvhxRS6v6k#B3 z)sG`y!FgPdsp=R-8W8mBYtL+%0`U8bf%fq92XDdy% zj)6e6s;W8ODyinddet(1RYV~%Fucx|LLj_=eRKdYAgEp(Tr836&KTrJStMw zt)%TGR7?5#=jek#TO+hJ0j;5;?HS5qjNus!T6??%Fw;b!CB@BqV{B82r$69o;=NK^ zA@M|swg0xV=u)(lp%Pf%8f-8VD^H=ARBDwOMiCt(f@oiB>q|VhTcU`HN1Gtf#h|+d z#FJxaQ{ zLUTInU2*t+O7P-XX_!1RI6|E2FGfW62vn>41cxca0x6?b4^E72dT(3oVfLr4=+i{4 zPE_L)J<;!B!31?oK&CqO68i7^{xIFkxH#_v5#9^@s{q1xt92QA^c~j0jKy;^QYw zdS=!uuVl&Vnk2@6CYcfB03k=0(oP7N#;K+5E-$HK`8Jp5H!Fjif^BYzJ^O?w*7htI zpLNTPq+M>_TrT4ce!dAV5A{uLc^GH|;<68@?S#j#egT~`E}P9U`kN)rUrVbbRG{kG zC35*Tbo5(kRc~~HPg8*LrgflpOo`2m1q5tiC74l)WLq3bWcAW|4UajBCRG`{O(a$6 zyme#MSdfukvaN(|{6t7>()05E{p*~v5A=dF-~&BDqFk=$KmmztHY?BP>b5Z3h3av< z3%}#}T8TyKQCQdjKwdJc&308uO!=Z>YVQP!H;b&%rj+j%J;TOL@uv$e)<0YaR1kWv-y4S zsEy&f*}UUx!S=P_7K~DG+1G+@{P;}YXL{~4&r&Lm!^fO%UT0;@yqoccni+SlSK*%GmAl-}d!_EFUe;Av&+snPDZ`o~$IO>7D_KewqD63f zK$7Z}e4O*zkTM`l(ZK zmz0FT4UJPLtB0h-sZ)m1vErj{!uP%R)O){h?%TWH+4#l&eT}*~v+`%Zm^b(DYe7L_ zVO-oxFHO`wKI!o%@SQYy%De>w2fe&-{-ReFzxrCqs?rb2%FD{%Hxw3DtXaJ_Ze83u zWBVfa@jsTf)&5&6SG`AN|zgDciRWv{=>3=e-;#!oq_H!af}WqSL{^hPDx zPQa+%zA|WzHmOVsV^D_qpE8WEFG)-iL;I8{A6{eP!<5XJPzmji>6fMI6C@80RmVg` zseNm~fd5VN$zh}&4fBucqmGD*RQHSw4i8ZD?yU~SUwE8UDG!h66Qqb#2grN(>7hW` zts!VFQr#;`(Yp^VMWz4eOuru^MFnardMm>F`1+Hp!xdt8vCo;foWPz@y@S-~rcXq; z+MkIl3&JwdThT)i&_^v-`_>&sURR)(6zHX{?Gn8}f5^)M0|SK`bE-ry%sH!C98_Ty z=?clE7pBSCh9B{ziZ-(>t1(QY zXL6)WPC@@N&&}%lLRMnZV>wWH3ldxLq${Y-Av(10+D?1HuRM#wAhC0a_W(tK+{pWJ zE+}wzmKGu63D2LYmHCXGu`>Crp0$qo8$w+12eZ-4Y-c?6FvIYtgSiDmQ|A?C<2#?5 z&%MP90l}CgJtGFKwlGvV#EO=vbPM)zSP`j&6q^{xvlvKC=N}bNW1N3hK#$S$C0xO_ zMrL*Doj)t;ws6jK1md&Jb&U7?0d)qqZEY1)H-d4;c*9^%4`m~ko+C+Nu&R4jog-x@ zRKHl68lgIcrH04l6xQnJxslo~?@VE}udl9=iXX48@i$9T8^0xY&JkE?+ep_3$K>E1NfHJxu$-@czPFrq6q*ZeB1baUS-J1JA$^%+>uvdt5bSQ8DnsAYTC zHiOhpg`I1gLJYl#p&v0M5HZzm786-0h3ayVn2XAzu_(yp2D8wTL|T$b%Mb$nE{cdT zAK6|a9u+kSpe}fb?K!BBM!w^efM&f?sHrffi}d#}_pkZ%f80zbh9{{4`z$fcqE^>y zqz*#Z<+%$L&j1}Vf$IBa9%joVxB=Lg2ER$fUyasO!;gYB%kYC>%{6>4z_#7L+GorD zW#*kHSHmKO_8@+~R@}C>12!KtuNpM27fQ$#7ywN=JGVwCYwGgc1g3QyE0L*0%!W4> z3MB09K(o}QB5nZ-5wuO{leV!wmDqX1sK2L6h)7M4F^p)R%rJJlml2^U;UhwK0+jms z)&zqbmWw_`??9J*T8t7iQ$B-WDTUrtE+GvnABw3>%O8kX;|yD)FKO((M&HudZ;jyy z4?VuV2l4i@O()(S&OvJox2YL)m*g1k5Q?qMq&^q67gpy!HH^5j(dyNotC5-+G0F)@ zJ-q0@$4c}H-P4-<1utrRUtRtJhJl(3^v2K$xZmZ`8{qFk$ox3K&asQ>l&^ZJR}s zdP7mnW)0IwjnMck$!eTMs-GnV)w4jf|BldsM%;p~9y+|#EGzFz$|^+W;!E%A zU|n5Y+fdd}c7NU9*N!+s^+lyeFvO9Lex$=J*vvv=Ul6s9Nz$!jn4sX`5QdpDb((K> z8J6St?z=T>k?YGz&lu}VTC;XTY1#W#xDN^rskK-vTQ*d}z~h-(%NE)WZHVb^0l8az z<+{^w`w{cOHhfU?1LmWTXfeLKYlhjj?W1kX2Q;N<1+#o5Q>eJ;!Gs<1IhLq?g!q9jY&i`!L|K{+~{u` zKs-Xn0MgN)lx@F(v|%5*1MKlMYk|k#8m7nK z5|v2RGwuZ*IUZzQs#G%;MjQO0uNa+GOesd6(G9;h$cGaE@o-$xI!ZZIxFir@OfS=Q z7x2=mr{jp{JKI1~J&2Ch0@a*)KCV@2HVoc4dgE_ZlQ%xI@mgj2rY|-ut$Mray{hR| z1y%E^JXPnagvvb|KdW3{rPxHj?53W+Zzz6Wt?aq+%*Nu%kE{GPDmJnkfBP_SL*Rx1 zzVG-CkA4VL0rV_PM{ofs)DRDaB8dWH$RN6cM7mT-;WGii=~yKspg^yz4a8XHVcS+2 z&I)i9SOdkd4YpV+e`1M_Dik%6U13MC_M4RO2J1q@BepP&;s&#{EQwqQV?4Lf+G&A0 zKQKJ)hBr49ZWQ-z0@}QNRY8^2Nd#;Z7)O`HFs*&V7Gezt-CDKUZ5yl+ND%8ctrUy_ zD4$b3oLGAz$r`yIwWAt`6TN`)y-@y$;2GFveS+4U+-6p4-`TWFC$(#N6&Gh-(% zG=W9Oxo3mn6UepkVJhl8YL5GgI+n-0r^m28LpfmI6)+`ryR@zlH z!b3@P+xcy!p-#~C2xXWaQGI&%h!*?$3Zj9svXxRavQpS0ZF6p}gnlKfVQQt~JDVbE z1?Q)gXxX{7k~STr>Jb_e+#^&>tFka&2^~abmQ6!0Nvf}`+JsN<+-u8iA$ovhs zcxDqI4>W~|rN;~Ewh#bBvElr#vQ{X=447Gf4QMLC00P?5)i4nbrB~W^Me4bw!{k^? z44(;5;aF=aid*@CR+5F}f^F6&G^Dc%NEm4Kdzj`=8#go7Jd{PNHJo)mt)<4K*06BU z^~3P2v(O@dO>LLy%Yox|M!(par2pD#Hw3b{iM$>PC_pCdRjaa4OKBa4S_Ru^d#qK1 z_L>hLJ`B2dc2sG{Z4`*?Xu$jC0E|Pc*a%12Vp7W-xKri4U!@f(F@p?__^7RL)~wkv zQ>IQWL*2jiq{NMAg0|IfBOL;z%L4R3Q;_w-KUw=W6>K5hgF<_A=rGHY6xRIi8>#h0 zfY|Vo1n90_BEg`l=SeWc>V*=x`i*qE;w6cW5J3vni)DCTf;Ca*Cr6KPMEDtol9r*Q zekeK6u+eGRSZAHp-4mu2jXWeo-%OfvtwT&>j2Q`D2pC{U$ zo5bOpv;#NcX}^Hmm5>-1PZ`!HDa2Tvv#l`;XwhbEv@ogdOr5S2D=dX%5LNsV=qTvq zr~tfgb}$qW99=>&G|?%(tz zxN5R*;lv7M7wU&Wo&Sn557;h3C8gpeRR^g@g?YE&SJqMI80|}E@m=8V&@mpVG>gi? zI_Gzr^bm^5h^60d(hG2_2M<>$6hnp#k;iGokdF+LNumyGWPigXY@NJ7oVz}Xn6JZ# z_kX0bO(vq>M|HShiF<-LuORCQWL^D8-|iHu^*_-mPpIxr99XTt;ha>h2Psp5{6CTx zqB)hyWEB8uilCkK=oZ^Pft~e_#D18IoiM3kEEU=*#Bw8|kkj9=v=uV?HXS3CD?-8} z#eRuRak_*=Hh#8xn3xxnm_Be;|KzyYgJTEjQjj_)X-IT_U!ga%V1xZTdEaN=-DLH! zeMe1M_O{vDa`W!JVY-TKJI_l~bPG3C6=CzDi)wAOF ztkXkAzP0++jFzso(s$2s!_F`LT(z2e^=?B$LoHSRwrttDwT^zQR_nIUKKrb`zTWl; zb>DpY#<%;lKlO<`_4M;$)yMh_uXgv5T+HsXGSdBgnMo;2(!M!pOM=nSrR1NEbVn-5 z@W^eEStg~tf8>mSHwPiZzkH&0$qPRRG+yjk{d%8wW?bm=*tb=E;;Nq=uxj9mNYS+H zxr^G!QzK73Y{$QRWSCjBztciuOhpP5wf)X9g3lpD67d zs@YR=Oqc;B+mkw@VwGYGvrlof=y#?o>M%1;_@E?jk5bADvBJbX`hUOl{S2H&OR-qd8Z+-|nTsQq6#YD+bAULYXj$rKdzX}SW{BA(9|)U@mQ~Eyvq`Z}v8kxbzety=+S@ep zVufa}H+5Nsd2iF+dyIK+(!P$2GNu0j#au;~e48SnzaqJ;>-dUue8%dxcxmmM>Mi}Q z_M5okN8G=!cBuVo^VM#(Ql{_Mb=u;nBXw^ioa&cfYgJ!U_pDVCQ^Ivz&n1Rga}zft z0%#x5&)QtVh_C{8mR76%N0|$)UxA*7b ziF@~@j@+}4Q-(w`CyVCo?^;&Wz!ZH@(I&l95?&IfpERK8vx-uEv-E@FP26YPzLJx| z8GdBNJjK?EuEdI|K`RtWev@KA#SF#CihGKC#hZ%vah(}$`zn<3&lD#WcNAqMTZD_; z?uxF@S}tcaooty=5!o_PKX<&63}BY&H^3XVmZO+Z=S0PB>xwMempXC(&m)zR0nBHq zKQm~w=(D}^0Q#nVGxpzO=Iyc+-@EZf|6X*|{ga zF@C^`PvWd`9sT^{cL66+kLa#EYT0_RXk_sRfURJ`zJ6N=#_x!8#%(I9D|W>1kGJm| zSzJ1_qu;!)D2se>Lj2&A<4QhCLk1_^P)eiJ zs`M+!ym)-%@vh8GC&}GDC7T{CeVUX-lIp?v-mXnVIz&hzLtFSuQ##1vE3e7l6+V`? z2v?ald8d5v`;+{il!vZ5A|JZyUE#Q#y~4>e<;!)&^5b$v>AT!kZYOt+`;jBXA>3%; zU9N$2$^(y_9^2w~+5g*&PVV{R4`w{BW;1hOtV{0BYayLv@bVWkUdmW1Kft)=+@2%5 zI+(o6e@qVWmoZ-65;%^I_}h%j{=O0a)1?rwJWc)_`@X!My&%8O4qma9-NPJbFUvc* zNh_Wmk|!^ezsq&-cZcw+{Nz#p+=XUDaqr4MM$>~!X38ss1M&_sdc{m1YSX7qnfm{W z-cET#$pHeiaq&YY%zA3}Ohr_2!Kl|BeP`m#igzZyTfB1i`dPKfjT3$77ltN}md^n; zmd&b7Y2Yp^MiRLN*= zCT9BM{}eKb+7$8G5cQI zYZzhiTyEuCrOAOnU0^`k;+ev+H^z4zkbj%iDLi&^!n?Md&f>J+t;;5@qWJhfq_sXs z8P};wSfAhH=is~PIUhc$sm<6E(%$pW9zjK!84E*-(tE6p?5oy16+A~{3a-_B75u#> zq-kLIBf;4rD>R!!Kh=bdGEEc18-{c zGMVYo&#`YWO1t?Br(J%2_AhK;pfdG1`8B7{p%>yFJQ=Nw+Zr?C-ePeDDdpCQ#}`X( z3dv~;pILZg@d$IKq~z^u3vVQ+)TS-8E<6(}`z=Ho&t)%s{q?p*bAPW|ctiYS(O=2o z<`gk);o^k-(zKgP7jB7e7S4#5nEMQ=35kATNI6sa*73Xxv-lbk^V*^%$u}0K-OOFI zWbye$?`_|(c=w#6`Cq^IDIc7<3f?iBZk&FpdJjCf9*YhFT1W7=l_ncutky;NVPlcUMUeg^<{2n%#Z91l4+hUY-P_d=UB3QuDC9S|Frk4@aR`lj^~}9-9GEC z?unhHc^|QR@?5ho%=$@}cC$1ZoD9HUNS%M^^SB2_A075zR~r6l=#C8e+Mb%>p_9{# z!v?HVr|%2h%N!ot91^*HZt#&F(++)=9(*VA&NCy^Y-K&&HpC< z+njD>HDr7T%0r{Cs6Gh)e8~CPZL{x>vAJ%~2{U#rezHsm*F_;08mLhDT%U!8S(PX8Z9Wj;Oji`huoQ#NOR^4H_< zCy(0x=A80W|Brg5M9fNrz1$Yx6h%`8LxsW>6~f)63NPN@rj|${BiC~O#F{& zvu-SY?&@^r<;9uiX+mDi*2VW1hkTME9UJ}JqUP9B{E~b1T-wd8i_b{f7X26#_s4#& ze0kmM@Y1pBt!%SI_S=T&PdT^$_k}kLeRGtGk=itm9e-c~vjuXlMZK4&E((-x7+N{| zQ*o!zES}=;tQojGYj&XDh`daGdUQE=bm{TrJ3(nTtwFEuI+Oon{yT>b#ywyTX_-Hs z2;{hou}@4re3^On@Z-lH7^f)XI#mAaQ$jN`mV}jz25$;WSl_}vc4%hCYVpAajG72 zIXw=2lHMHL$vzZDJ{d7C;HH+{HtfRC4pFvq#E`L)Nzv29x7iKRd&o!2Z)Ci%b3&i>sm*yXRu+~R0f!v72zc2wsqnMZBs}`D$FBVoLEc#G$V{u$l z>9ViV({8$c+wkM3u{XFM$?q{Ee*B2p&vchBiQXdYRzFwtHtHpQe@S{P{ZdieqJZV` zi?!^FuLTB;8MZrpVDWIx+gyp}&aAZW>m|q116G8km;IJ@^KIt+SsyW5^Y$}G^Di;~ z2%e`vR;?*yM~|SX8CCxE+!^)RjC1{BS0e4lp0nP3>)$B@mrt1URDOQ`FRx`5Pm|_F z&rbg^W@Gv(-nl02W?tOf_%p(NHsXg1q+JqFG9qT3x^!hdcR0N{_QJyZ?0eg3m%Hbj zV*X3kO-7gZnZ7%pWAgJ~dvV3wb#pH*))ws)-Tuw8Gml(i1{P_Va^}q7Uvus==RWy` z)fPPybUx@`Dk_$%f^I}VYMd*+p<1FVCFg_FZq_sH3j>MNxYaH^bzY? z6uTup?hJqat@qcQ#{yyv^1GR*o~u4TJ7GQwS#P#AX@oVu9ad6Qy8DO~&lflM(FWr)iBaCad^ZPTx zIdW%K!177kE4J@vuTE~qeV}~h*vdtL0m@Y0QkA7turs->%>Ko_uB6S%oxLP)L*Cxm zXUXl^J->f_YKf+FvT5qNv6kqgQ_pJ7iPwbdQ$`zR@^hyynF>vW-!gV<>}4r*lNBjX zPS9jVKP{Qbt$1?X)DOvq$p`r3v7KB%@!0X>)f3c4nQ6+Kch6y&Drf7a?wxvyrTp?S zqgWoMeo<1vbh5Iq6vC^SA1f8bgEG4byMeu|1YviBxSzex4lGWeI!Q9OUxB(%{YA!c z$z}f?rJdZs?*}fN%PwI`*$SqEy~Xi*DTgJCi{}P&Ws8oB=g8GJh&f!ydg~3ba?#T7 z%VuLmS$v7TJsX|=pkQ9%7K=+_S4h^04NRw8QJl80ZV}ZTRu93mMDk_0BrZEX@w=2x zZrrW6*$UxcW{2|eZrsV(O#Wnf3_%CvD$j(bY|$&(H2uITS_t`X2cI=gGAYwB-PGS9rs z=VwDzW>4{*B+wnjzRb=ZmN#uxdOf>yTKkl^=J=Gnv2#+s&_UVszvkDWeCNbFnW(;Z z>QCwMJ9cJ-eVvnGnX)xG{*3I0usH)IsTps~DVu$nbh3f&*dfVNDyIgzpUKFZHa9u+ z@bKi)Y2iO6&Ptj+N|rI=8O=)>RT}rKb9rfB=1y&%dnWmk@K?s&jM(jC=Qa;LKJ?2e zV|QkzY)rDw+B^Gz|2KY@m4USDY-JR5*O{cjKdShZvX8ky$zcESev_2HW@ZH^u%p#r6*I>jO1xiA-E@ z;Dqm2{BWPR-zo0hBR;iPd}5FI{reV&n^OhZHp6F+0704UJ-QY7tFeAFvG_H~>>PYu zV%ZMu+|R|0J1la2AhuZoY>Vlhb8i}n7;1ZrxWyzDF72Y>G3w~j@Q6~>*}8;?`JeEH zE&=-yo@Z=N5%*-{RGXf79%G2m|u z!A5l)_&Rpo#9KG@*eH`@D*;<=qBqjno21?u zYS))Idr=qjd`rNE;db9H(R5V#gx4`Dmb3Ln>|d9m&%0fVFZECh=R^C z1|v9HLHFi!3{#0Aj2LneQ*vKGl(@fl6m`_9Y8_>Wm~KH}8I5JTFVKDLCcT_qcuWy4 z0eb&LiC1Cic@-gKJO+U zAoZgk#5tGHhsGF&o~2$D!VChi4<*BB6Td?Y0HvO!ku_{ZEH++NVjk+B!Ok{zLp@<_ zwF>N{D(i!>>*!i}zdV?jg_YzeNeVcTB(L9&Ef=eR-G9KIOT=%tcT5HH9ljA=H3k43 zxx~z`>;rrv=3*+*hU`FmEVBd90q(N`b}Q-5DeqCbwEjCH@>}#RIw0Q6QPeBSr_%6w zH0_I|*8!K)&8(#ayXd@rdL4}}*WvJl`!<67pW5L*0*YU}!ubIimc1x)u2B&Np_>-`ToTC$4r_uwScB5l=Zp zzk^_BolQ;*eneZpvv#v8#3D~D>KEtFTnDNh0Q_NDN^JNU>~Iv1Kb6I>8nPf}{Y>Afv=P zsQ!K8iT8nvZL^ddJJ;4$hL{jbNyIzHcj`+l^WQnSlX87FuTRk#-beg~7?qF^C|R&T zXEe(F?%%(E?_OsYwmul9h&kWpO^8qhT&?slT`VJZQDk^x(YZhEOK>I1usM0)@m&=u z607V$&aRTpNok0-^255aRQW#EZRA^)6t^ zIbaupcAROu3@H-lkX`!Cv`i4rF?9J59SkPlOgm&T&)D%P(s$A50RZJkJL+jqF-B0v z@dlS!0*;RN_ycy0r>AWN4iUMoGd3PD(}K}P`E^D+(_xhxRuHFVmtnF5Q-zpVL@kZo zWq4Xr%Q!Q4892&gEU}~<9q$zs92v|K9Vfv^X&b%mJzQ>uopn)J^qyBt8GET zB*ogEmguKz#ofqF{q zGp!QS7AvVXOU|n45rl2l$=I&v^@!^b&m9z%2e-(?kvGNCo8q7!MA!GWulU4M9znct zR(lx{cP6zCxmv$p8*!wI)f2~6$g+X9wZt<9M4OUhMEHAUeu?QLU*9V%Ep%u2D!M;e zOPr7IF{~xEq!D_}or!y@2TB}Q+ZneAS#;ZURo69Hq&^izcEUKUa11W4AXklqV1_c#ppqI-dVt)=W@1b&mEL2n+z?v={asYdO~9$c7joHD2>7qXpRC0*8x zewMF+#jiW6c&fd~j1y>gpWj3*->|E$Q)uTe+n0P|fLB#^U?VS9x)IZ8sN@aZ#YJN{ zA|hCGr0D&zmTU6ZW!nW1OxzlrM$##1xS{t|%1sWXXA0=K{*;jRh0~4L)jCQBqn(g` zvUaBG9f`Kr7Zg_mtqMFK4})1K!C;n15KDkC2Vba0jw#COSG`eUO)-UO2YrDFOz{K) zFNkUoDC$$bIH6jni)f=QjdnzN2KyjKOp?aTietY|VqNPelsS{XFy&g9!UX8731~OU zDz9EeSNl#!6qHL|%sVJ@2OrMl*dN4c=e0n7rCSHGkxHe4AgH|bv?VcKHphZzh&-w7$$xvwLO=*%(<(o4#k5IKU-#yx_rMuti4 z?|m8v7{%r!8Vkc{|LuJcI~_BLZ6 z4oYOIugqOqrhXc6-rpw*t8rk%d5;4}sjKeM6K>YQ`bftc`o)gb`f@9)-{&|-!&JV; zZLA)VtcJ}JOB#fsyn8WWbM$Fg+E3sd26%_c3rno`dATu`PUcuknl}hKRUo2@Mu>M# zh@&+iwPoDdIvhZ~DuBJb&W>}yNx^;mb1QE62nxT8oPD@SwvO%p7!ZowGb&1NDf z?5pb_GLW%O5}aiX!eKtD$8pwRQ=B+&6&@{I^;N#kB2-`Jw+rH}M!iqbV=ImQ%t7np ztVe2@rd+S2>3L|N`vu4be?v9t*e}?s>6(6|vbv2≷Woja?()9P3JgKzg5j00M?0 z!Md=7Ed68_euMk-jaFu$l^ci! zF-@DjzeZZ$!6R%~cc|$F-BVA~!#Xpbdw!-l@IqUo7i+<796LofZ1pd^{(+c&z}ks^ zq;wUE-w)jA@L$V$dsQN;MbFi37u<(&unEzV;^P!`;*@n(D0g zP1_K|HA+%FTH<)a^rmCAX^rC?T|_U?Y@H*{`+TA@s}AJqx2?>ZBG%F+8&BM}?pv(h z&)gJu-xR0)Qn2fQHz~{N{pNz5GhJ|;b9)PnqP;iD{Zr`6pB?8Am?$lzGc%W+1OQKQFb8p$|cq<{B9+nQS$Kkjeg8FF8u`8B>X4em-ZKUx1|)!B>=grBt>3 z-R4KYixREvp!qSJBLx$hMeX{7h^Ja|(9E`BekiB1m|?C``{hCNdnll^avq7fw-u+` z+C&>hsS{-M05(fi9OvePRFGkzr8CTC?FwtNN}Ccv%6@1HFjF05HP& z@jRqEK7;=l2|#L5hyc~b9j;F!+uPk;Fp0ws(by03V(v^9-aYC9!(=i_#adju?n zF~($IfYiT4tsSm=Ok&(&th3oV+eyLWXRGAhEP{rq4ur9aFCW%vBage#80C$e(=Wit z2h@liqwV)KOd;AC-MFAgfU(^Tds_EoftaCD^sookLFe=79M$LfN=Pt8GyO(2dk-fx z-#nbrJP&uTH^1Ke*WuFRUmnkCeymyFY(DNiY(C!o-HUtg;pdKYf1f!*pXm3y

m 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%0DEkguRi*P9DR( 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�b=$044RB=O~P^GcYz5a;=ciTpP<41&;~TYOEtR_E8;1JeC1LjYB2 zX*%|@B`#zSYFuu#Zt~qafA{ji**CsgEWhyF6QjAMrYyhV0rO)i7G4Cf1jJ%lZY;}< zWx25|HGHbWir#4<8{$Z)5Gi{_Q6BYJ)V!zv)x?Pl zKVCB>?`+tV7@s&XbMC^G??{Dlv>Gyub#M^yP{V5~S;=&IU*`fC$C0Dg;|uJuwDm76 zutc^_hl;@`DS*s%Mk6j-LsA`F%0kA%$bd?;D{2)oya*PbG6TPF`Tn!o{Qzifui#Mi zH5|@r1U@RpAIp{C-!S=_DoZhz+{Prk3tc;Ge!qB=-fmyJEb!W2h$;91%xBPy3y0c6 z`0@7-+}7ck>fJJcK-d<D>6) zkBT23e^UHds(xzf`VT*g3v8Q1VJMq-+Q<}$ephlXw99k z8Q7LPU+*VQo|Z>bew>R_E8J)%o;Fbw0gP zt~t<>*-#{6uyTB*nJY&Fr}@A;peYnz9i+t{JI|Je; zA@zE^yXye(rEOBT~ zyB=z|wV;YR19lItVDNUk#C@YG5Olh66PGuJgI=VDGEuXnGnwB4Dh%cH*g1?fhGlS#{4J3TjYlj>UBEF zUfmvaiM5w8A{T4Vi8btD^6*05V7GYttv0(IhabVb-77vTwiUevIcsakyF1N2E~4sj z_$4AehStz-pOnxc5tpAl`CzZZCVt5(*P_|fi$B&Gfr5g9f`WpAf`WpAf`WpAf`WpA af`WpAf`WpAf`WqLA;7=UMeA`1GynkXI}*JB literal 0 HcmV?d00001 diff --git a/data/videogames/supermariobros/test2.state b/data/videogames/supermariobros/test2.state new file mode 100644 index 0000000000000000000000000000000000000000..6223b17597e985e45742d3a44186a815b64ddc9f GIT binary patch literal 2441 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 Date: Mon, 26 Apr 2021 20:27:27 -0400 Subject: [PATCH 148/170] removed supermariobros files added by mistake --- data/videogames/supermariobros/Level1-1.state | Bin 800 -> 0 bytes data/videogames/supermariobros/Level1-2.state | Bin 799 -> 0 bytes data/videogames/supermariobros/Level1-3.state | Bin 812 -> 0 bytes data/videogames/supermariobros/Level2-1.state | Bin 1589 -> 0 bytes data/videogames/supermariobros/Level2-2.state | Bin 1591 -> 0 bytes data/videogames/supermariobros/Level2-3.state | Bin 1595 -> 0 bytes data/videogames/supermariobros/Level3-1.state | Bin 1576 -> 0 bytes data/videogames/supermariobros/Level3-2.state | Bin 1591 -> 0 bytes data/videogames/supermariobros/Level3-3.state | Bin 1583 -> 0 bytes data/videogames/supermariobros/Level4-1.state | Bin 1586 -> 0 bytes data/videogames/supermariobros/Level4-2.state | Bin 1582 -> 0 bytes data/videogames/supermariobros/Level4-3.state | Bin 1587 -> 0 bytes data/videogames/supermariobros/Level5-1.state | Bin 1587 -> 0 bytes data/videogames/supermariobros/Level5-2.state | Bin 1588 -> 0 bytes data/videogames/supermariobros/Level5-3.state | Bin 1579 -> 0 bytes data/videogames/supermariobros/Level6-1.state | Bin 1589 -> 0 bytes data/videogames/supermariobros/Level6-2.state | Bin 1583 -> 0 bytes data/videogames/supermariobros/Level6-3.state | Bin 1587 -> 0 bytes data/videogames/supermariobros/Level7-1.state | Bin 1573 -> 0 bytes data/videogames/supermariobros/Level7-2.state | Bin 1586 -> 0 bytes data/videogames/supermariobros/Level7-3.state | Bin 1587 -> 0 bytes data/videogames/supermariobros/Level8-1.state | Bin 1943 -> 0 bytes data/videogames/supermariobros/Level8-2.state | Bin 2173 -> 0 bytes data/videogames/supermariobros/data.json | 60 ------------- data/videogames/supermariobros/metadata.json | 81 ------------------ data/videogames/supermariobros/rom.nes | Bin 40976 -> 0 bytes data/videogames/supermariobros/rom.sha | 1 - data/videogames/supermariobros/scenario.json | 28 ------ data/videogames/supermariobros/test.state | Bin 1576 -> 0 bytes data/videogames/supermariobros/test2.state | Bin 2441 -> 0 bytes 30 files changed, 170 deletions(-) delete mode 100644 data/videogames/supermariobros/Level1-1.state delete mode 100644 data/videogames/supermariobros/Level1-2.state delete mode 100644 data/videogames/supermariobros/Level1-3.state delete mode 100644 data/videogames/supermariobros/Level2-1.state delete mode 100644 data/videogames/supermariobros/Level2-2.state delete mode 100644 data/videogames/supermariobros/Level2-3.state delete mode 100644 data/videogames/supermariobros/Level3-1.state delete mode 100644 data/videogames/supermariobros/Level3-2.state delete mode 100644 data/videogames/supermariobros/Level3-3.state delete mode 100644 data/videogames/supermariobros/Level4-1.state delete mode 100644 data/videogames/supermariobros/Level4-2.state delete mode 100644 data/videogames/supermariobros/Level4-3.state delete mode 100644 data/videogames/supermariobros/Level5-1.state delete mode 100644 data/videogames/supermariobros/Level5-2.state delete mode 100644 data/videogames/supermariobros/Level5-3.state delete mode 100644 data/videogames/supermariobros/Level6-1.state delete mode 100644 data/videogames/supermariobros/Level6-2.state delete mode 100644 data/videogames/supermariobros/Level6-3.state delete mode 100644 data/videogames/supermariobros/Level7-1.state delete mode 100644 data/videogames/supermariobros/Level7-2.state delete mode 100644 data/videogames/supermariobros/Level7-3.state delete mode 100644 data/videogames/supermariobros/Level8-1.state delete mode 100644 data/videogames/supermariobros/Level8-2.state delete mode 100644 data/videogames/supermariobros/data.json delete mode 100644 data/videogames/supermariobros/metadata.json delete mode 100644 data/videogames/supermariobros/rom.nes delete mode 100644 data/videogames/supermariobros/rom.sha delete mode 100644 data/videogames/supermariobros/scenario.json delete mode 100644 data/videogames/supermariobros/test.state delete mode 100644 data/videogames/supermariobros/test2.state diff --git a/data/videogames/supermariobros/Level1-1.state b/data/videogames/supermariobros/Level1-1.state deleted file mode 100644 index 8b001a338291b8e1a8444ccb8168a83bd2968dae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 800 zcmV+*1K<1~iwFP!000001MQg4PZL2D$KQ5=wt%!Du@ER)U?XrLZAmpzOx*5|w$LBT z?$Q>#)QjoCL@q{e)EE=I^`;3iUZ{s2yy%I<#CY=Jf8f~z7ZXlun$-ERTN)BVj6?{$ zPck##nfLb1bhq7k9bK{7zZL*iuK`0y`v43Dz(YEvnoNgCsh1t$U2{axazxv3L_XoE9NfM;VWq)LJ{PHD9m=&f5 zQ_}P|VHZAaHm5%ednfgEKoH~%L-$m0cZc=>L~77ILf_{dU+=*c4nl9EpVYH%mz%pc z8^P%Xudrt5xb=vMDuoTxiI+;MRZ)5DvF=QgRA{TQ=$5D*Zd5{_)|_gD!t1OB2JPAwIxY zBuVpgd^+;BK7m=!9-Y1D6xYD^MdaepZ07{iov5z+?c z#{7ZBa3Bz!3&~+w?c8ZUfvZ}Jo(`UO1ipf=-ANQoRJUbHVxk~D#H8V*ae7>c^n&qZ+ zS(!7>G3}RW!D{1Nw$gJorZxr*U zmZDR<7_iL@PbG;^n9!n@!9~m!aV`e6Xs$qopv7DfcSTf+aYtme ze6;fcwS0`~TU^Jx#ur>|l{AgtJH^zQ_q@O~^KJ>$L$UeIvohuin0{a!_l@w2Ex)x+A_7M?#h(UBi(rHN&N4%0&PY z$K!n@+K4*0U_~?_QSxFM4$t8a*GInK15@tNt2D8%C zXJH$5Hab%ugx%x%Iv@ygiV=D{LiZBHTFioOJ9&=w`uYxx_P`DuKGToo@$|#-I(`(I z_%rTdT;#=G{ptLz$`rsRK$=EL3r^Hc9`4{szyEJ>2Y9jNxlQ(qauL z{k!}80reL7__md?=qmcI_LVKB-WOz|k}aEbZI%9=aA0gW(m&*nWNCaje3@79Gm@km zvwS-8v_6JePanOt=mgJ!t&7O{q0`yz9LDycEj^Co_m3dR32{_9aukLs7h|Lh&W`$n z3(;UOJ{b*8k3=Cn9f4?i1j6xY7+i>2q$ACQ&img_!>E`vv1*faaH(8gVp*7~$tn}W zG73!UWnE`dwX?j&lvS0!{{hi<4XT-n&R_RrwGw+JG;o#d7gx*-?I4Pa>CXeA{_I|TE~I?EfqB~#1N zDI#gx%=1)|h{8mRTLzbeD@iVcTD(}MB4`O$lCH$ngwy0olFJZjsyYE(Njg%{O7Y$Y z)Jh5J74FG>^>eOUHBIB!PBHa_TT7hvUK3~!#il(xqLnw7*(51hQE2l7IZ9>n>{5A3)J^?U00t_MN1t=>3E^<<9%JdRxtvOrt%-bSp*`ian#hRMo zqG`%S0OAM4N5cK5FIM0hoMyhh9(>6Joiap!2>&Z^=j0IE@%u$Fwj+MoeoNN;;p5<1 zt@(}I-^>@0Q!c?}e?S`Z^#8a)NPn4}?xH#G6cY_Y3kI?b`PIK18*L?$N^7t}oH` zhlPC>yt2o|0eE;f@@l)iH1q*r6(9m`!5tB}pWapO?ynCDg6|-|BJb8*cgS%{E*6=) z{7fO^wk`6?gL`}Q64f8c^ea}zqPytl8&57XPtQmoqavGhZzQqau{V( zF2+e87$5KjCZmBsVkjCI?T2d7J= zXp7Yr*qFqr{@%dj;suLKv#^49X`tn%hs~xYb|e*@-F6!fbiX}E0#0@ZB_csCdfM0ao7+pVMj z*4zDo7mH6dA)zrbl7(dT0Rc%!gC_EV6df^H45`s4p32a~7h@kxeDPt92$Kh?e^4MM z^_=tj-L74BSN{;falgy$?|07q+}}CpcTVp;yIa7gmG5jLWaw$aCG^_~iTMbz(85SY zVO8|Vj-u1J4;@iMryN7)g5L{HCa#1Dp>ZtM!@|L%XOEN3WGU;b4+qb2$Id=ncZL6| z;qrr|ix;id^*>v$-Tai!`Ri|p7e+F-X&*=bAW3?plGM^8mD){`shuR1j*?_LDyJB; zR1ZSFZ1i|KJRZTbVXwp8x^DGKkELDM*wp9Q^sR7-6vihvO$j&d)p9Up%G3ia_Dm)E z)1kbAxHF`>hTYFAY$LzGT6MKxZ)czFdXfHEsxHPVCh-YwmeuP|12*;2;LXtK6@*L> z(oAfEtyzFLwh0JVe`K|?fw^FLfqo7s;V=&u91MB!3<>%U9jNkOWBxQ9enRtV>?wNU z>tlTA)^{C*L=sAzJzG8hUMVa3c%~0J~hH$3E7^cp9A~iUIy6h1JQk&zNHIk35`c; zI73aK-xpzLX4ftw0beA6&G+$*Wp@0CBRf7rw|DCuE@Ev}BffqHID>-$zHhWf;%Xmj z;`!D@JP2D!>iD>$N?Ht@PTlDOr%9E#Dc$MP9mzk80d=Pf97{-z zNNgBq*#Ol@XO-Vo8MqqRru*86szUC3@c^ff2;ArN3@3aENS+pq5AxQ!6`zlt?q|L? z$a7`urn8_&?bd?(=lp({x$a?q7#&PQx3)io0sZI%!jSp+h+kn&gJ^dXGQwWgQG1?B z8A?RsYDiTybwf{HdgtsoA%!d7d{Q1~*PY{KU2e1=m@;L`lqplDOqnue%9JTnrv5h) zO66X5l%1r&4noH0FZlyx&QcwIPv$mrT3)c6!SZn|=dmol&1~|!v58*Yp1Rs|YIysr z6$=gIC0Iz6N(HIr{e-Tb%TPZ%1vY!DgZ5zVd>h*Ii?7twS5z2- zgXKUD)zq`MD6x6*d8a6M2J7u;`KuQ_k2M-2l^j>cr*-Vl!Fjy?_`LYHu|8!GUvIZw z|HY|=`#*~1FAU=A?bhpmdTQbR>uR6+=lQ9Y9-qvMugik*YdEraeyTP57vmc#cQ?h? zS9%?+v*d1r^`;B?;#%OuI(+gD@DlQcU5oRD+rVcZM{WYf=g@)d7G7SCMK|)fR^Z)r z;%9ybK8Ad;XK{Y|H{kPYfXme3W#mQVr8<1>7UsVNxapmB@{fFRHE`KFeDVh7ANkUb z#d#6=>|?;i>hS3wfG<4?T)um8zBr5dM_#PMOUNgiflF`4dFWgCy6kn#Kk~(GnAgC< zeCZnS?niLGkk1 z8VUJqJLXk3m|uOq8jx$l^>4ttu|Fjn<{$Z(0iVANeD3HBAqS{^6Kv)f+7n<4Z-FHz zv5ZzeWEJ>&YX_To8LV5u@?|`aHgg5bXmi)WPNQ}I2sVjUmajs)XmcNL6c&9REOmXf nl|S#DP%4mvPw{fyeY>t~RYaw}2{Zrz>&rNu diff --git a/data/videogames/supermariobros/Level2-2.state b/data/videogames/supermariobros/Level2-2.state deleted file mode 100644 index 2c9ebb801ee8f163fdfd14e50822a7469c6eb841..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1591 zcmV-72FUpziwFP!000001MQh#Y*SSn$IoqnwPVoBlrf=b2i0u?^KMWW1l{eQ+pVMj z=N)54yInhWSEB?F?l-yp{mwa`{?56-b9&F&UC^(VZ)_)I=n29l4A=;X`w6ko!ieT& zRrJb^qSG*jj;Nwjj-xXb@PVTxyb(fZ9AZ5p96EaH80jGQvvz#^@@ej{TPDt%!ky~4 zeDBEGxcVEbb>pwri&sCVbN z_v=N-w+(K0r`s*K+xFRAtsB;^a$7nqo0|IFo4*&%lETFF<{9D2?OG0|Oqsfe)lgk1 zuOu#wdzJlyK1Ua@)mw>R?_{4HP$>{W5G)G_EQybD(_C*n%fW2@;_7S9j$I&Rk`OzK zViz##B!s)=i-NV`mO66Fwl`&Yntl#?BN5&d+8_2U!={k`@WCqoMdoYt_LHhlWxMF? zmOU$3oAo_9plH&YV0)|kzpJHjLv!nV5gy6zjm<5qp#nzac6W6WftBgg`Zn}>@2RpE z-NiBRebLl|qv`1@(?&XL*izB$`2CwQW$G@e-f^U!;t6lWMXjTAz1^|b>2OGGPDlGX zCuwPKCC;7eNQ=}?n)W(X3bND4gZ}4dJU*0Q2dgYS4(^M^_OVl#5Kz1UejJOd@i2Er zW5FPIhLinJlkf%t?EMe1C3TMs`-Xz>x~E@>@>fF39;2UwV+kJvY_`GJn5rM?!fI0G zk(QsNCOF`avX;^HD@f2EO=9=`{9u`#=(1-g&d}rCD|RQbjw(_A00SJM{Xu?iv_=z3 zKkK4xO)?RJqa=0w+)*So4oVYr@44@N?K;{!80WWt%n~RVU_Og!J z52(CD$yh=OD_&J;>&;8=o|+(}F#FwS%x=Wy+K(Q>ILrGG)q?DO0BY zHxf$aK6aHIq`)ph#_1pVgJj-P9eH2oR(o4sw5(zI7?$%`7T;ku^~3mNpKecF=shvK zV7|Q_ZRVNhYw9a1 zjKjrpAct$}*;|zOg7|z;lsk#_HnjYWi=M|Cjgv}_tK-u;_V3_4e*f5l_}8&MZ4h7Y zx8C^0nZ?IHisi2j;_Lm^8-HeI@$u{GnEChlsg@p}EQqhmg7IrOvUGl`b@?C0H&X6q zny;_S23TjQJqGJd7xKmRz=?JE)D7Sz&pv|OB;21}2eR9Exe<#lhLo1BJxrlK6efC-vZp+t~&WgzPJ{+Y#lyz1@n)5 zY3I_shQB1zSNCYS)9-Ri1~M5{*lia@N?&J{jbLS zOZYtlzVKlP+L2EmcREOhHW=|=!M>DQh4Y2+^8thV{StT)`OH)JTruD^*Gqe6|tiYa5QsfY*KjUc&gLt@vCq;Kkp7FWE8g$QOME>j%dZ zM?Sv=*Rugn@P6|)%nS080WabBMdUM2;`;X*{9a};{|z|qPRx@5pTYG|T!G_2UN+!m zJl9CbXFD*jvcdf7^R*ngc3l4kybH%uT88;YK5oG0&jFu1dY+Jj)V>Z@djsv8U<+@7 zCC9OhRz7SM_2GG#FBJ(i004C&Nd5o- diff --git a/data/videogames/supermariobros/Level2-3.state b/data/videogames/supermariobros/Level2-3.state deleted file mode 100644 index 829bc2869ea708e5b8075ae75b0b9cbb6f459d42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1595 zcmV-B2E_RviwFP!000001MQk$Y*SSn$Iorq*219IDcgji1*+Rb=G~w$2)f%pw_8X5 zTzb1d?8V|!O-N`=jASv{^#MUiNTVi_7^LcmVKJsgec`DLO?=VRCtr-25CM76ilTrf z_1yFO-EN(`y+(=PxZmXV_dDl&?(dxYJE!-a-YpzdDmS(eGW;-M;szar$U#DEv@ojr zwJLh##?Wcniw>`#Q<2d*6$*f(CjBu&XdIjUAh-X}i6dkaS;`uEch^bgIJ6J^vI;CrAH>SlcY1-NhX^h>1;yG zXv|VQ2>HCp>+SM-Iq&-2PEY&V)hoTWPWy)D0q@4Ixih3VIlXa)yK<|RgC$Fr?qJ!f zA5@hU#G^8=`n;g`(JI?i+obtT9V_T(XEhdQw=E#B#6Q4HquF>Gz#RVO%l%)p9VKLn zkQQR+>`o5iG)zIbzno|%j)ppM?O;yX9;cr}{#cBKi|mO8mLU-d9@sa}e^&EVdig0O zplDCgCx8FAg++SZNk}~9PioIr@4r{;^0h7Pb49o%d)KwJu7U~}k=NVZ1>MoR^)2Z2 z&QtYSbQ{OOo{N_59!*bQnKsfH!F3biWIzKPj-kX}Mc>jz zm6XCFH5{iVJQ$2?En{mMNjMl!VfO=UW0{-mcIGBe)9u~!PB*cSN%7#I2Dl=7!ffAY zk0+%8tqYfsN=9HSNu3~bBtenE>Cv5DaGE6{mS9&b_2^Eo?g*hCY|@=xaBQ?`1Ow_$ zFF0IOiVNChoX{>%iucU(d#e{N#kc6bGAc=syI?ZR=x&brj6ThXU<#6_M3Q@0Z$5u8 zs5Ot%k}nWpxw84_Ea*{vN@RS_AB36njf7(8U>bbNcoYMM&c4Cr{;<>LrtW0X>on&QQ9<=XRvSi7UB}^H`$^QqA%F__R*@b8rFAKe8bH zb*#@A#Mk>Z8$Um@`1r@L{Fy;~yny#~V7=)=zO)87-h@xz0A4`8xT8K_ybgTse&nX$`kXqD+sw+# zvFJfQ-wwRTB)<9^@G|nHzWV&^ufP}DfKyF)5qTbY!GzCW!~C}bH@n>=|HzkC1D7-5 z(^oM6$d|X(=XvCF_W>uH@Y(NyFW(DX!B?LzUB>()&ztZ9^63`fvRiQ;1{OarcM0>4 zd}#~jHM}@qz6iYM9-J@aa|V1K`4W#@FIdrlFYUnmBVX=8D=p3!zQg>xF#pKs4fxsf zxc*mR{slbGfG@rsfp+9GN8K)xr42^>r|?`#uf+Mn_=S+c^?m|8k9_tqyssGWD)QN8 zoUd-|$AB--0AE^x^M!mdXt0ic0Y0}J=W8>L%YavZ0$#xQg$Pw;vR4$KSkf&nk!_<7{BkKp?E8+>2RVg8$N++CO_13ruEq11@u zKwdQ9Mcmg2$mcpSucE>H>hslrTnDay1KxwEi|uIDb67^3zXWy`t>*`@X|$qv0s2Lo|6l`G t_j$0?^Nv?Wz&Bmw6r873iy5KL2K!*(w{@GF^vf{001HaO$Pt~ diff --git a/data/videogames/supermariobros/Level3-1.state b/data/videogames/supermariobros/Level3-1.state deleted file mode 100644 index 11b878b7946777813fa31e70a013f697a2489a4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1576 zcmV+@2G{u?iwFP!000001MQh#Y*SSn$Io3i*1@1<%9yNZ2i0u~csCdfg6{Uu?bgw? zrMLS7PsPNfCL}Z_MzRN2A5fG88zqq!u%+FXAFAQhA zdKJI2BXDZ>z;SADreknU1$^XW;@$zqc$_NRL23V?6NlLrb}y^>gW^fy*!2(B4e54u zJbgFG#@w9t9lBZ=?ox7=d)pC!mOskM;bcPkiCe}|%S8mmEuw=>7T`YZ1WybC> zy^Ogtth`45J)dD~#SONyyQON6{@G>}dAO>wn?)6soDU1LveI~}$vk^$!}-r%;+xkv zV-2iEs%elAb)3Rge6WS%o~n9^fA)I^21K~f?ywInEaX41ugrhJ@DrL((|6I9^w zE!{twn7A@wrZc83GrASue@m7u-68$cW5j&L;@$x_w~qETb&j1bheK|0Ia*h{SYvAw zb8TJC8s%12ztg30P(KOX?|**AVuNx06qV)2!ClelE^!JAsNR5ZVp=RLoRMfSD4d~0 zFV)1ofq*!x+7pJyhJ1rTdfn5jM#L+j>PPrz|DL!{2kiF#=pM~D#)Y+nCL+}w=O)sZIAJv+>yPmU z`5k3`=gfnvkwIazHiBrB}em6SaW7wk?x{nTRd49%H2YF?# zm3!vesUG9~gUcmVu`-nk@|^b*hPuyE{pCrr*{kh*1heg}u<56tn^Qld!YEyO4wUem zdi^a*Y+igZD9#;6y&YD(aWV2ZN26>e$K~;Po&N9OJnla{Fa9;ur%d7-{Z<;kb8_MF zkD&aeNqnQEI@VTqVeWIt1(!9Zp|AO|VCcN`k;!AbNJNTl{Wc^?~G4S~(u%1nLhUd-O zkr(i?2`^*(PVni+vHra#-_BFDF*U7RYD8niTY?4@SZDcdg zkac@eK8tqP%sG@{bC<|Y!McAWn}k)A3)C-c?xS_mqWi(Ut`9ef{oX;P0^9$nC@;CQ aqq20Hq{#R_bii7=uk;tv(%8-kGyni*w>%*L diff --git a/data/videogames/supermariobros/Level3-2.state b/data/videogames/supermariobros/Level3-2.state deleted file mode 100644 index 4dd9654af9c7a31c120d6f08a9d84046d2280823..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1591 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 diff --git a/data/videogames/supermariobros/Level4-2.state b/data/videogames/supermariobros/Level4-2.state deleted file mode 100644 index 5ddec64c5a017ec54cb283b20857359ddd8b87ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1582 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 diff --git a/data/videogames/supermariobros/Level5-1.state b/data/videogames/supermariobros/Level5-1.state deleted file mode 100644 index 2a7969519a7d73a75ccb0e7ec6acfc6f388deb55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1587 zcmV-32F&>%iwFP!000001MQk$Y*SSn$Io3i){Tu;lrf=b2i0u|csCdf0FxgURD7xl2@MI6EGDZDC`z(4Y9cR4m64dmm>PZJsSHi@fuwJ|_%KI=$%EEEAP|#! z?)iOh*Dbp%DglK1P44~u&OM*|JLmq+={;u`{9d(iXFFqqFESyi*UDJf%UA_33@1I> zC4Qtv;8c&ov8!+jVL0b~-Q*-Ao&m;qoC?cv>F|j&C)pPEIBVahZ=4m5RXaKEN)M{x z!lR_m>uaSAzge#R@)aNR*Wa|CA5PxpeH{6V#rP4Aam$Z*d=HDocd>XP#A1n%JFYR0 z>%rLNYL~0k<&s=Y`)izy>(|t~s#>Itbv>?4-%A%*c6@5nv~=TsO$Sq^Og+M?D4w_q zE15GXyyAMn-=cags}^Sqt18|t)o#|#EqakvYOzWe8wn-*bHdb?>knd&em6F^;qooU zCK+>SQCt$mshp&6_wsC|vXD7nv5%koJOcwFT;M=|cO{5`_sF3V|Cr`y`Qt~`ZdF@F zuN;*`=PV!AFcyq@BHG&O`tNF}UB9|!PVAM^jTbri^sKu%@W{@%uMr%G5*BKix(wRyg7raB^#FT~}k<>#*76CWozg zt%Ef*H!{c0wX8vIW_5cVDhIWl(3Ae>XE;0<(Kc2oz8%~j3hmc+VUkbr_{4TBtcLrA zGZOOqg)`Yo3J{VWkd;jdo{ooIN%riMoTcF z^k_{qMWT@aZ6&$m6^^W1WOqnue%9JTnrc9YKWy;k5 zMpC}er5yzu%W4N>WBiZ&J~nT;j(_A9Hu;`=$#N3qlPG6Uw!g30*bifqUAmpP+Bq?_ z<7m-R0~z;1D|aolGhO=k2Zuu{{#&W0d{$oYeL`2~MY=vWO*VD2mG@x2z8N<0%4-X* z7nL2OL(74?f5CO_D@u4#e9%V<^>Hd$P{H;NJz1?#CPfRb}e_bup|2{vZ(&Mv5@pV-o{sN9IpPy3A-D7+s zrSHbY{7S5+c^2DaFyC~6&#fcQUWSj|AzlWb-Mu`Yy-j@TS#Xo|d^I|d+A7NWe89sIc z`3Ilhxjb(NpL&M4a2Y=F6Y=?{iOX~>&*$clfAID)ybM0Jnz+Ocj6=`T_oc2Q|KM}m zkyroHeEu5o&ZjV5;8O;C8hp+UuAQvgfY0ql{=w(lV3no$%#X;w4fzM3HsEJwG5=Q~ z|1$0~;Ip3us2+U$l*7gnyuyh8hSsH6J;n?1Gd_do{hD|?_{7V2uNd%2@QFH%*EY0c zz~`rl&#lCGfzNsk=FuO-r)n`?ThT8AKKU#0GUDgA;JsqN+kYoMUxU1Z&vhHjAM_^- zKJyahvjNZWycsL<0$w)YW%S<;KCv0|-(&E5xrF>zqu;H_lL4Q=e8{apKft>UcsKSn zGWb*r^6EAiUwyo)z%^t38}Lr_Ctr#DgAW_~L_KWs63VdY>tttOoj;R}!Mfd7sa@Fg7aOHz*MrAhpKTNC ly^Ts)cK8KRUh!~yW$FRRE#vpl25ahx(%(?W!|Mq&006q^JUsva diff --git a/data/videogames/supermariobros/Level5-2.state b/data/videogames/supermariobros/Level5-2.state deleted file mode 100644 index 55041cd811da602036475d9a57aa8919d3b2450f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv*$iwFP!000001MQh#Y*SSn$Ior|ryGM_P{xE#3skqE;O(F=2)f%pw_CUV zx$Euzz*F(5CL}Z_MzWXH2NWe)8YK}!(2C<_F{DPHcmL+| zNj>NMez#k3w>3%t;eM0b-|w9BxxaJn@0|9W-39$x`Q}bSMxP>F!k~>1)lZ0p7REDP zRz@X{)vK7weUuKBaU1%B$kJ@yrc6#)&^kl3uAKwe(7*_K{?24@sqCB$nxZNRnlTj2sJOwDec7p~o@9 zP+mox8M3^F{hnKNznOnpNn_=1VXd2PFhV!cZ)y-M%Wmd%P!b>KW_7*sVC=cC#>?t? zJ0UZKxL6dI0CAdTAl%;{#M)ddZUdNemgnf_kT(+H;X)%}AGVN?|Hz?b{>!?b(0m%( zMNbcZv!0c$@3asSO?cyMZ*~25wYP3s*RfQDpJZ3(y7o0t0i$racJx4Z^w;_h^m_lP zvKQUMG4OrS)Wf6M*=w^#x?tE+(Vh7In=)nU9;x1Oq@L8cH{zt$-m|gAzTaWDOPd|` zt_=>--qk@IyEl+_sf)DjcW4x3C!vS^&rekyjkAMQmL3O(W3gd=3JWOS0C!YP4RdEA z77TJ{EHMBzac>~N52Ch&?vXLyXb@ia3@B0lN~qxp`Z;tU?qh(>HWWLc>BqRRme6>l z##7V;2mMjjvbeE{1pUzjc0a%mmf5M!mh99tJ>GS;IEZyZiTVc_U=NK1`MJ>=jVlAJ zi(E7j55ZBAI)3gblBR;=)}3B(+7u}gV~(J>b*EQ%q<|Znbf*`bR@yX%0d=Prov;#> z*kim4zMJ_Y;P+M@T#4?~eQjJ(Ab0+Fkkg$4_c=Yy34a2Tr-kApyc3Vt?`O^XnC}bm zTs3>>Ea*`^TIk@CKL|7D84E^h_R_d>|3;p{02Zuwb{I8U$RFJCPPw48KgZjmJu-WT9bOcN7U1-zKzgSgYQDG7; zmIFClRnOj{sJF%EgQDCithb@%uU_;#R%wz{a=bh~tt*#lCh-2_x5dAK^(lk+dcXC? zFV3$#{s}C9VGv*Mx8C^E^DB>ESNHtC&(Ct{@yTuRby+Ze6-R34XSo*tVSFRyZl(D8 zN^gR7mfUBs-gF^f+z6alhfm%FUP8XGw>Dq60eto`AoYV!-f1E1djT&4~$BQGK^)!}p3G5_trE$pe2f8>knfy>t6lh-i+$d`84 z=0)VQj{>LG;nP0=UwQ<%yr(u_T*Uk%FV^8D{ZM^^2ME)*Wk*0 z=?d`9wK!kMXASrq@j$cGRy$#pD*Wmkd5%b@Sf+Z)g zj8;Bk75IAV2Ag>Stka9-Ic!Ipxrk-7xvO9o&^mtvn?x(im!V&@xsSI9HTQ!DT_5e> m`@J1X1#s(tJUGynkjmOfbk diff --git a/data/videogames/supermariobros/Level5-3.state b/data/videogames/supermariobros/Level5-3.state deleted file mode 100644 index f07d290430feda5d905f58515dd1b47d07bf9997..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1579 zcmV+`2GscsbHL z+x_Fg6dz1YNN9{kvX|8d6d_p}HIWyj=)Qqd)3*S+ZpSBiU~PH4=7 zdNB4~t<%}+bV|-m`|2D`>({PwR<~4cuJ3kk`9V6za-$PlCZ#LOwH!>DGW8#6Id19| z#XnrLE14rz8k*lli)`&}qe?Z}Gg@jX#;U5QSU{jfzDE#FYsnoipqyYB|3Drc9Z-M=G}+DW^!(6LfHEZCzJq-D|g6}iedO}v9U~#&REi;V|;rzX0fx%AtmJP(Ew{;zhCSdD??GGTN@(u zax5C4tt5B6!ck;3LQb3Rw3Ab>$icAYNJ^XTwCj%SYeSdrw3AcEy9N+YciQ3fDIrKF73`GOu3gf~XTQv=ccVi1?d>(#nHQVy-=KLO-IoE(M2nS_wsR#NH&GJeIDW1EDpv7wU>3=p5gNJ$HGyi zPw}YArjD%q{;5&Ma#P=bF*~Z^r$t+r1NLK6rc9YKWy+K(Q>ILrGG)rt|3*?_wo^N1 ztt_V_W%bz>ZgomIlbW zORd~lYNtE(_YZcvr2mpCRLIFC?iGn z#q%FR`&)zf`ncutPfjkLzpj?ae_o&Y(&MuQ@pV-oehJ5xuFrg3zQg!N%G^qb^_5&t z>ny&A`a1F1M&eRscsF<(c)1LpxrX{T5I4Q6O#Q*<*Aka5!^f|n{@@Ecm*#EY z(~l7sDZ?j!Ccf|}aaq^WeEu@(58hUWm%+!^5SQG6dFWpJbLmT{KluE1)YZQ@U${uT z;}Ogk__P6^0iU;lYbWbA;PbmtfAEDiSY>fO`xEMKMg75N4EX68tpC-hzl_fr@VSoy z)DJ#!!fs_r-eJUlL(ir7D$Ez+XMG0O`!(@4@X6+YU2Y9yu@5a7H2A^(0UEK!r ztIt<8xMr+>1Kxpr3RS2-_=o|YJx_e*=nP{AxqXXl>TTF}$mWidWhc;vbsw&j#CmHX zn|g_?!-MvD^uwm6(1y)iB0CN1_=Rj7*6qGP-z+WpJb2Lc**5XKw^A#|4m~N_ dHTO0vQ+G*j8Q+ICSW^#`{sz?!T diff --git a/data/videogames/supermariobros/Level6-1.state b/data/videogames/supermariobros/Level6-1.state deleted file mode 100644 index 1c5f6cdd65bcfdc40c20eec024337a4f73262e25..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1589 zcmV-52Fm##iwFP!000001MQh#Y*SSn$Io5&r-eZ;DBBpF7N~Ah8Fw3nLD1d)x!pSY zkKXPdiKh5e6A~H|BiYO912QF98a0s@r095X8d9TAJe8q|FD8BS#fJ$ICJ$N^6o^SZ z=lp)RYnR>CKNAq{H@W@&&OM+0&hP%t>Ah!nK95?w(@V(k(}YVHG!qi>5K==MqbV0V zMUV6tIt}CKh$=e82s-DyZg5gjSC9}IhnS8DM~5+(2OOHfiABiXSkVG;};>oa_V9djM z5b{lf!_nz*2#&1>EcTWyo7Oq%JA`eG1CH(A2^UCia(4TiaDBCwgCRqP{v)l%O}(Z( zP;o7>rz%6!yJ(4R;9saI&tQEQ`|KzsRL26A=OtiEe1e+|)$*waBTrd2uid+okQqW8 zEQ&*bI88GUuKJZ#sus+-n&;`~kSiGE;rs^!ZtNj`&(XtW{%g$7)60*kZk0VnPu@IH z$NEg~TL=loTv7IHb^LoZt=Y1%Ww8mjWJlY^ruA?FGIBV&I$=0^w_b%&A3T+wMfWiW z{#-Ql=xBEK`mCNV==PMf8oz%-h78>&{oQTEQbnS!pq*N4XRF1!-)6N+TW!|%W*cd0 zZy~nb&7?_cCyo1UDh1h2=u!XkGZGn&vW-=mZU+y9!w2{-%&WM(+=-}>0C&d1J|A~R zVgqm{>hgN|&eI&zJTl@Q_QC6(0VTv=3DrJHKZnMnZU&gmL*a2%+tLNpn93v7oubA! z=n1i&h1PoF^MqnJ`~crrrYGAh>B(ujy=$}Bh^a*hc?KC^^&j-{eWNK9RR-7)pY+C} ze%MM<$HN^(QX}AWYfcY1jfxZuGe=OmHK#{&ByTr%X-*F~wX|yl18PnWI1K?MB(cjl z%Pvp}bu)h){GQTLC6ICBY%i2TB>6o$lRvhk(X_!uswzCEVjjWnT>xxG1ITv=_`HHBfDNL zSr{NIS6ZpR($4p5?;mV7q5Pktq>z&;-cM-iy9nnO=D?WRI*aeqS#O$<&$j|6R^j7!fR~WZ?OmDA-3C6r8Mzs_K8psVckuQaY}%2}v;c3f z5Rj)az>CNycjA3Thfg7&Y{dEM!f|x? z!W{7VwK!kM=R7*==qB*#H8@{8FfScGbpv<_;}@R8`-%=P{tA4-f^|ne@77sAm`?=x z?6bI@b$Ei;n>Axyke76L3G)|`Pd_Ze;wYA`4nog{>Vpk`0PdCGsiCza+umT!KO~2eG6>vZLs7d zw$aK*O#)wU9bi*0gSES`eG&W7rY>O{ZRQ%-d9?PQz{b(a@)Z~tZRWFW!ivv>hh3j` n@#no2S~+s$Dc-KXzgZc&N0KG{J#?cr^hoJX_5>rk2{Zrz3JfY? diff --git a/data/videogames/supermariobros/Level6-2.state b/data/videogames/supermariobros/Level6-2.state deleted file mode 100644 index eda1294ded0687b5c8160c6eab4ea3afc065dfc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1583 zcmV+~2GIE*iwFP!000001MQh#Y*SSn$Iorq*1@n9WlUDIKy{nSyxSNIg05YAyLErs z(%b!kr{YshNN7w9WIn7upePAx)I_2|3yzmXO;wBuPo-$$i%DOIFFs6&FnQ4W2L)m> z&pp5I?N-=bjZQ$g-{ki9JLi1v@0|NPr}v)T27K!5wGPIHA7esNpN+AIk1-1`jHbL= z6+hBraB6nJaj0-+BXCaod&o&ey+e%gILvxL+I#TWA=b+7X1(}f$8q6km&}~krCZhU z?46{K-mzNO{bIfN(%rL9HEwr{+by}*57fFE8dtA$S2tTW)b+YIek+}3xrwQbdFk>}EeBJkO#Me%ikrGL zKhW$7=1P@^=6BH&TXWOd7E7K#V5H_!tSZS;J}wC*$HR0&rSVjed3$2uvkiT9j7>7u zs6}a%D2`>4!rgo!He1yqahH)fX?dD|_IrnhM7ZG2!5*}*pl{!vdHxH!A5(i&?J4^C zC#UMPvh}T6#zHY~RC~6%|Gny$H?C@!E7C35y>3ilJe8hB zw{Z;OxoGO%(bUxCDI=XWY$<6ezW=67nYvA?v>mCWNYp#z;?~}>w${GGX}8Pko%W_R zPFCO4z?@swuzIT-I^-@jL>-USzqb;U;Y@}y6K(Bjxm5_KPwCoZ7*}p5=qX9Nse|VRwZ{r5lm?|Pw z9pxs_=L=~qGi$3^z!!?4`(ClJOi!q_>4|fEd$+FE$*c`Z$k(R<_TbKd*f&~3QKeVw z;>1`i8lVy7Va<`0Hr;909ogT8Cf#W#X9aH>K|tMUhcl>z zWbHEclixPaZ!cZA66(->byQI(cfM#q&{|HoFX(fE@Wm*3YB0J}bkgbd`Lt%Oy0a%J za%Jh{v!F-qRD-+c{63nw&Jq6*9HgOB-93naemH?4%6xRx?-hx4*j$W_YA@@!JFk7d-g}~~%LV(sDO09QnKEU{lqplDOqnue>VG4t zINPlqvv!u#4#vj$kNkc%XSoi%>k&5fx@W<13gts6XHj;%q1pI%r&RX;W-%w1y`Rw4b%yF^@?_IjTKEX&+M8gL&pcOFUs7(I z4lM_s!LoYoElT8u_+n6;JBoT6ta$aJ=dn!Vtd!&V@p+y0cW@TxAG#s_Rn#X8;_LlZ z8owjI@c742{>mV}-fyMxC-V!BUsrSfpXX=3^!V(C__{2JU&gV;^D|#Fe>1+3GS?Gg zeI*-doyE5stT$cY3u}pURN&*+h?l|Vwk^)*t`eVq2;3xHU#$+LH;M9c6kXsm4aB=D z#83S}d<1-VVSn-4nA?%X=h2^V8nk(&!zZE%opNk{RY?j1@R8>$)|8%G2m0+lXaM{R`g@Q7xTmy zR$#us=X?h1=r`ii%Q0V@FfIc=^)vA@;uoL9eZ_!x{7QVW7I_C>=rLG77*7Oz_6e+K z1D@e}vo_=fyllYB7{3F2@^P$xufg}_Eb?E2akn5(27D6hp-_!+fcF^i9_(vm@abmc z)nhQf`g~P^Yr^_B;9VF`aT)RtK4QRU&k&zEc%HF6+`ddU^$P5(WOJ{PWrtCQ_3X1s zV!bt!O+8E2f1XiQ@2PS8Q+ICSX1|u{sQ@W8zu=f006aXLT&&6 diff --git a/data/videogames/supermariobros/Level6-3.state b/data/videogames/supermariobros/Level6-3.state deleted file mode 100644 index be2901c62caa815e828520e3fa885f6d66c7b6f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1587 zcmV-32F&>%iwFP!000001MQk$Y*SSn$Io5&r-ec5lrf>h0@ZCQa61?bg6{Uu?bgwM z>+Sx)Q}L-LBs3;QvXHDkpePAx)I?s8sv{wbiS^He;i(Kwe9`pH7a!(`fIMhL(SauQ z+;hISTS;$gbOOTtCbz%eIp=eK=iJ{pz4!EPey>uxxt);FCkT@;WF{o)C8U8C#?x-K ziXNE>I4%3(*c3RWD4erC4|mcrcZd)ghnS8ChmM^-PWs9Htc7!5pJ9%A$;5t3xKkaM z?j3br->|`C>b!2c{L`m&&R=@jc6L0yK*u=oCrQvFnV^;)$>biANbVxZRD>i_5h)SdOi~N@C>}+da%`5nboX)L1ygPcgUWQ)p zJ(ZtDcX15txoGIY(ag-X89klXZ7FLxzW;^{8M;fVw;idcXv`gQP;2d3XR+?JTdm?o zyR~browRp#5c|%xq+RSHt$XbX1=XFby**qNCuV`Dk zpb}SDq^48U_=mhazm99dMN+;M46FLzpHF%)4}EW0$PS93(43r(8S%bh0LG=_kh z(+ejkheh==j&a|$#P2O%xE$WD`O3H~^W1r3enuY^n9t~WMtI{qc}gHQ!aC`5d%bFN z3oZ3|0&!IOd|K2#C2(NTAL29DH|7h$K^pp$13~RvIQ|fi7901unPW3M2pLyj)=~Q| zm3uTEiOE6Pt;ieubK+a4r|7Qy;y0g^rd0e4D{FGVzHi8oAwz}?88T$ZkRd~c3>o_0 zNGO&D)T3l2dG#P~=x>ODa<_FIK#t(A0l{*I%9GHgltgj$pC93pVxa^A+`F z(oC7=Wzb<+u|>vKB*I5 z>$l$cZL`(KKY{X>I`OrB>y1A(TYdbRx@Z4+ewIp$Pi~8^sR8jTI8r-5OLg^c#@AE! zR+6o+)Ox+snd$bUQM=62P|Kls8L&SmQGiEGF| z_~OpmybXNj5za;H@Tng-UwoKzxxU(b;VSYE-d2Yf!6(`{m)e1O7_5F?<_hu;zOWs6 z^;hSMmpSiv2=fI#qr+#x7i{2qxs`PI!fxare9;9fSLbuzBmY+9AADAapPR?}Uyb~W zI8TSqzZc-`;FBlpR+6F(di)psxs+Ii`9l1hPv?3+=e!MkY8&n=I(!;@sulCK75(V& z#aYf5R${)u=e;`X=vU5XR$#uiU|c$U`e)9Ih+ljf_Z1!9_6z5W7UUg#!K1T&FrFy* z+*4T3Iy}Mk=FG?ocu|KJF@78P)RS2MZk_MTMdZH)O$d2qk$gRSg& lkAE7>lS7ZQa`W9Cm7zN%NyPWT1#9Sm(qHt07DEX%000}qIimmo diff --git a/data/videogames/supermariobros/Level7-1.state b/data/videogames/supermariobros/Level7-1.state deleted file mode 100644 index 53bb25ebc09e172e93b20aaa89eb0745292ec283..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1573 zcmV+=2HN=_iwFP!000001MQh#Y*SSn$Io41Ysa7!WK7m+f$BCDyz3|og6{Uu?bfaT z=M9~6j5 zJ@=gN?b@Yx_0I%^`%P|tzjM#0zw^7lb9(RDUBIW5Z}t;1_84IjhRuY;e1tU7#ze-W zo}x!~5>CqjI5q`NIR@v9-^-m$+!H2*#%VGg6%HReeVq8oy{ya!Gdbp%)erkE;dTvN zzMEvC>iXGq`NvP_oWJ<8?aV~x2F-Eu50a!uDoHIpQmMTpnc7WK=_pC2qf$y`?$v{k zFI$|>UZ+!VZW*;WIyyJ5b++^fTib@5+b#%aNnv_!+q`gXrIv#sLx%n%t;9{esX9=3 z4RK^@L({uxljgUuFPN$`*wC+j_EZvTRs&Y&C7@+{n3+|L@&T~ztCRhmFO3p1ONdjA z;uH{dmWOM6Wp(48X?mJ|j(Eah7B09yl`@?X&WgyL1yr|5;&Vzb&dy=5UJ zlJLaUXRGtytG%^zeaB*x-;$kO>)Y4y6Fdv2b4M>9j^3?TVAQ)$m1of%%z-@@4c$MQ zo4Yorr?a{}6|KbY-;g0gcSwJC8?jWexF_tO*4n$-V%=xATE#7PYxgEQY47eJ_FbDu zyVyY%H#Btg>`FI2w(PvR#;8_V}3-Q(__JOhy9%=8PwX_?fuJ z?`IoPb3*gTxOXhTU-t~j5%x-G)g$zCN)H zZf+(4UnGIyhuFq4JKbf;PS4QoU6;j9OdWE>H>?8I;Qj#HH<}`Gc}N}N8&@J8y+Kwhlbg3GlnNVNWQTr~HXDkto%OTmL$Xf>T z;>pv~gcQzy{c(9(#ou9VO%B-i4H+_I$dDmJh71`pWXO;qL;o8IrShP9l&z$o9)wKM zKk`S&qNO_ew#01a4XJKBgZ6Q>^Jv@NRBiIxso6oz&RiOp8Q=MQ#qt62QmqvSYwg0I z_Wr?c7b^cNRaGj8HSZ@h4V>fWug-Iuz1~YRSnThHO+WKo&H0K7Q~XeCAcbnqt8Y7{L}OG^Viff|Ih1FEiFD-5?@mj;@5Cw`TA7r>R*hnr`)X+TVLr;zRr?+b=I3E z@WsuXvo+w8H#skYFYH;KFWlgK_91Yy{Q4{!kloJOt!O&H=Q=p=Xb?a1JLhBIiv!E^ z3%_wbzln312D}8`23~By=dPpv?VMZK-Jt&9iyJwYZNMk5q5j}YyO!r|;Ij{MF4llg zf6w{S2F~T(%k#yns6TjH16~B5T+g}mPRzql{qJS3p#I>C{iti8K3}@bdB+2oFYs9% zJ_o*N1Lxva(&3AHP=D~HK3KUvpZ^Z^x1#>wb2|LOMXdjIsK1Es>F|a3g1jGm>V(}& z(zHX5|C~RUl4~(vh@bcCT<>R`w}DSTh5L#Qp8=n4!+h<)I68c3p7X^um@n`JpUyh^ zmGjwF%-440rNd`_;=G9Xr6+M;(cx{saK2X{?80Gx7j0>F^TvH6r+I59%uE%&#_ItHE_+ z{p;`!}%W>UgwsaKpR#%ViMST>)|%@EVm91 z+UL*@n>mj*Z0-uT3$TtKxJ|-J(j`7FZ0@72!m`hUdtD#yV9$FiZxzVlM_If1&Squk XHc1lkd+38TbYJOD1pH@q2{ZrzPoG5r diff --git a/data/videogames/supermariobros/Level7-2.state b/data/videogames/supermariobros/Level7-2.state deleted file mode 100644 index ac85cd8f2b5f5b0cea3064cfd6e506619eb1f2d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1586 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 l8 diff --git a/data/videogames/supermariobros/Level8-1.state b/data/videogames/supermariobros/Level8-1.state deleted file mode 100644 index 19db592f587f040cbb7774a5bbb3505340292d21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1943 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 diff --git a/data/videogames/supermariobros/data.json b/data/videogames/supermariobros/data.json deleted file mode 100644 index c0f5fec6..00000000 --- a/data/videogames/supermariobros/data.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "info": { - "2nd_goomba_position?": { - "address": 1206, - "type": "|u1" - }, - "coins": { - "address": 1886, - "type": "|u1" - }, - "coins_disp": { - "address": 2027, - "type": ">n4" - }, - "done": { - "address": 10, - "type": ">=u4" - }, - "levelHi": { - "address": 1887, - "type": "|i1" - }, - "levelLo": { - "address": 1884, - "type": "|i1" - }, - "lives": { - "address": 1882, - "type": "|i1" - }, - "reset": { - "address": 1905, - "type": "<=d4" - }, - "score": { - "address": 2013, - "type": ">n6" - }, - "scrolling": { - "address": 1912, - "type": "|i1" - }, - "time": { - "address": 2040, - "type": ">n3" - }, - "xscrollHi": { - "address": 1818, - "type": "|u1" - }, - "xscrollLo": { - "address": 1820, - "type": "|u1" - } - } -} diff --git a/data/videogames/supermariobros/metadata.json b/data/videogames/supermariobros/metadata.json deleted file mode 100644 index c757c458..00000000 --- a/data/videogames/supermariobros/metadata.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "default_state": "Level1-1", - "states": { - "Level2-1": { - "runs": [ - { - "name": "ppo-10M", - "agent": "ppo", - "hyperparameters": { - "timesteps": 10000000, - "time_limit": 4500 - }, - "rewards": { - "mean": 4751.435068054444, - "first_10_mean": 973.6, - "final_10_mean": 6239.0, - "median": 5286.0, - "max": 9375.0 - } - } - ] - }, - "Level3-1": { - "runs": [ - { - "name": "ppo-10M", - "agent": "ppo", - "hyperparameters": { - "timesteps": 10000000, - "time_limit": 4500 - }, - "rewards": { - "mean": 5895.214350826931, - "first_10_mean": 717.5, - "final_10_mean": 7570.6, - "median": 6538.0, - "max": 9674.0 - } - } - ] - }, - "Level5-1": { - "runs": [ - { - "name": "ppo-10M", - "agent": "ppo", - "hyperparameters": { - "timesteps": 10000000, - "time_limit": 4500 - }, - "rewards": { - "mean": 5860.152402856797, - "first_10_mean": 647.8, - "final_10_mean": 7665.5, - "median": 6391.0, - "max": 9543.0 - } - } - ] - }, - "Level8-1": { - "runs": [ - { - "name": "ppo-10M", - "agent": "ppo", - "hyperparameters": { - "timesteps": 10000000, - "time_limit": 4500 - }, - "rewards": { - "mean": 9448.269531849577, - "first_10_mean": 392.1, - "final_10_mean": 11432.8, - "median": 10188.0, - "max": 15554.0 - } - } - ] - } - } -} diff --git a/data/videogames/supermariobros/rom.nes b/data/videogames/supermariobros/rom.nes deleted file mode 100644 index 4342b2eec67ed2497bd5b4392d4c40279921a48b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40976 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≷Woja?()9P3JgKzg5j00M?0 z!Md=7Ed68_euMk-jaFu$l^ci! zF-@DjzeZZ$!6R%~cc|$F-BVA~!#Xpbdw!-l@IqUo7i+<796LofZ1pd^{(+c&z}ks^ zq;wUE-w)jA@L$V$dsQN;MbFi37u<(&unEzV;^P!`;*@n(D0g zP1_K|HA+%FTH<)a^rmCAX^rC?T|_U?Y@H*{`+TA@s}AJqx2?>ZBG%F+8&BM}?pv(h z&)gJu-xR0)Qn2fQHz~{N{pNz5GhJ|;b9)PnqP;iD{Zr`6pB?8Am?$lzGc%W+1OQKQFb8p$|cq<{B9+nQS$Kkjeg8FF8u`8B>X4em-ZKUx1|)!B>=grBt>3 z-R4KYixREvp!qSJBLx$hMeX{7h^Ja|(9E`BekiB1m|?C``{hCNdnll^avq7fw-u+` z+C&>hsS{-M05(fi9OvePRFGkzr8CTC?FwtNN}Ccv%6@1HFjF05HP& z@jRqEK7;=l2|#L5hyc~b9j;F!+uPk;Fp0ws(by03V(v^9-aYC9!(=i_#adju?n zF~($IfYiT4tsSm=Ok&(&th3oV+eyLWXRGAhEP{rq4ur9aFCW%vBage#80C$e(=Wit z2h@liqwV)KOd;AC-MFAgfU(^Tds_EoftaCD^sookLFe=79M$LfN=Pt8GyO(2dk-fx z-#nbrJP&uTH^1Ke*WuFRUmnkCeymyFY(DNiY(C!o-HUtg;pdKYf1f!*pXm3y

m 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%0DEkguRi*P9DR( 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�b=$044RB=O~P^GcYz5a;=ciTpP<41&;~TYOEtR_E8;1JeC1LjYB2 zX*%|@B`#zSYFuu#Zt~qafA{ji**CsgEWhyF6QjAMrYyhV0rO)i7G4Cf1jJ%lZY;}< zWx25|HGHbWir#4<8{$Z)5Gi{_Q6BYJ)V!zv)x?Pl zKVCB>?`+tV7@s&XbMC^G??{Dlv>Gyub#M^yP{V5~S;=&IU*`fC$C0Dg;|uJuwDm76 zutc^_hl;@`DS*s%Mk6j-LsA`F%0kA%$bd?;D{2)oya*PbG6TPF`Tn!o{Qzifui#Mi zH5|@r1U@RpAIp{C-!S=_DoZhz+{Prk3tc;Ge!qB=-fmyJEb!W2h$;91%xBPy3y0c6 z`0@7-+}7ck>fJJcK-d<D>6) zkBT23e^UHds(xzf`VT*g3v8Q1VJMq-+Q<}$ephlXw99k z8Q7LPU+*VQo|Z>bew>R_E8J)%o;Fbw0gP zt~t<>*-#{6uyTB*nJY&Fr}@A;peYnz9i+t{JI|Je; zA@zE^yXye(rEOBT~ zyB=z|wV;YR19lItVDNUk#C@YG5Olh66PGuJgI=VDGEuXnGnwB4Dh%cH*g1?fhGlS#{4J3TjYlj>UBEF zUfmvaiM5w8A{T4Vi8btD^6*05V7GYttv0(IhabVb-77vTwiUevIcsakyF1N2E~4sj z_$4AehStz-pOnxc5tpAl`CzZZCVt5(*P_|fi$B&Gfr5g9f`WpAf`WpAf`WpAf`WpA af`WpAf`WpAf`WqLA;7=UMeA`1GynkXI}*JB diff --git a/data/videogames/supermariobros/test2.state b/data/videogames/supermariobros/test2.state deleted file mode 100644 index 6223b17597e985e45742d3a44186a815b64ddc9f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2441 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 Date: Wed, 28 Apr 2021 10:31:49 -0400 Subject: [PATCH 149/170] wip retino --- bug_game.txt | 123 ---------------------------------------- src/shared/utils.py | 3 +- src/tasks/retinotopy.py | 45 ++++++++++----- src/tasks/task_base.py | 8 +-- 4 files changed, 38 insertions(+), 141 deletions(-) delete mode 100644 bug_game.txt diff --git a/bug_game.txt b/bug_game.txt deleted file mode 100644 index 54673b17..00000000 --- a/bug_game.txt +++ /dev/null @@ -1,123 +0,0 @@ -labopb@criugm0279:~/git/task_stimuli$ python3 main.py --subject 01 --ses videogame_test --fmri -pygame 1.9.4 -Hello from the pygame community. https://www.pygame.org/contribute.html -Namespace(eyetracking=False, fmri=True, meg=False, session='videogame_test', subject='01') -[983.46666667 768. ] -435.61 -READY -Next task: : setup_video -16333.3571 WARNING Monitor specification not found. Creating a temporary one... -16333.6624 WARNING The size of the gamma ramp was reported as 0. This can mean that gamma settings have no effect. Proceeding with a default gamma ramp size. -16333.6629 WARNING The size of the gamma ramp was reported as 0. This can mean that gamma settings have no effect. Proceeding with a default gamma ramp size. -skip -READY -Next task: : Pause -skip -READY -Next task: : ShinobiIIIReturnOfTheNinjaMaster-test -Next task: : ShinobiIIIReturnOfTheNinjaMaster-test -Traceback (most recent call last): - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/event.py", line 367, in dispatch_event - if getattr(self, event_type)(*args): -TypeError: 'NoneType' object is not callable - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/usr/lib/python3.6/inspect.py", line 1124, in getfullargspec - sigcls=Signature) - File "/usr/lib/python3.6/inspect.py", line 2191, in _signature_from_callable - raise TypeError('{!r} is not a callable object'.format(obj)) -TypeError: None is not a callable object - -The above exception was the direct cause of the following exception: - -Traceback (most recent call last): - File "main.py", line 24, in - cli.main_loop(tasks, parsed.subject, parsed.session, parsed.eyetracking, parsed.fmri, parsed.meg) - File "/home/labopb/git/task_stimuli/src/shared/cli.py", line 135, in main_loop - exp_win.flip() - File "/home/labopb/.local/lib/python3.6/site-packages/psychopy/visual/window.py", line 779, in flip - self.backend.swapBuffers(flipThisFrame) - File "/home/labopb/.local/lib/python3.6/site-packages/psychopy/visual/backends/pygletbackend.py", line 259, in swapBuffers - self.winHandle.dispatch_events() - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/window/xlib/__init__.py", line 881, in dispatch_events - self.dispatch_platform_event(e) - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/window/xlib/__init__.py", line 917, in dispatch_platform_event - event_handler(e) - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/window/xlib/__init__.py", line 1108, in _event_key - return self._event_key_view(ev) - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/window/xlib/__init__.py", line 1101, in _event_key_view - self.dispatch_event('on_key_release', symbol, modifiers) - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/window/__init__.py", line 1232, in dispatch_event - if EventDispatcher.dispatch_event(self, *args) != False: - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/event.py", line 371, in dispatch_event - event_type, args, getattr(self, event_type)) - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/event.py", line 392, in _raise_dispatch_exception - inspect.getargspec(handler) - File "/usr/lib/python3.6/inspect.py", line 1078, in getargspec - getfullargspec(func) - File "/usr/lib/python3.6/inspect.py", line 1130, in getfullargspec - raise TypeError('unsupported callable') from ex -TypeError: unsupported callable -labopb@criugm0279:~/git/task_stimuli$ python3 main.py --subject 01 --ses videogame_test --fmri -pygame 1.9.4 -Hello from the pygame community. https://www.pygame.org/contribute.html -Namespace(eyetracking=False, fmri=True, meg=False, session='videogame_test', subject='01') -[983.46666667 768. ] -435.61 -READY -Next task: : setup_video -17292.8077 WARNING Monitor specification not found. Creating a temporary one... -17293.1032 WARNING The size of the gamma ramp was reported as 0. This can mean that gamma settings have no effect. Proceeding with a default gamma ramp size. -17293.1036 WARNING The size of the gamma ramp was reported as 0. This can mean that gamma settings have no effect. Proceeding with a default gamma ramp size. -skip -READY -Next task: : Pause -skip -READY -Next task: : ShinobiIIIReturnOfTheNinjaMaster-test -Traceback (most recent call last): - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/event.py", line 367, in dispatch_event - if getattr(self, event_type)(*args): -TypeError: 'NoneType' object is not callable - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/usr/lib/python3.6/inspect.py", line 1124, in getfullargspec - sigcls=Signature) - File "/usr/lib/python3.6/inspect.py", line 2191, in _signature_from_callable - raise TypeError('{!r} is not a callable object'.format(obj)) -TypeError: None is not a callable object - -The above exception was the direct cause of the following exception: - -Traceback (most recent call last): - File "main.py", line 24, in - cli.main_loop(tasks, parsed.subject, parsed.session, parsed.eyetracking, parsed.fmri, parsed.meg) - File "/home/labopb/git/task_stimuli/src/shared/cli.py", line 135, in main_loop - exp_win.flip() - File "/home/labopb/.local/lib/python3.6/site-packages/psychopy/visual/window.py", line 779, in flip - self.backend.swapBuffers(flipThisFrame) - File "/home/labopb/.local/lib/python3.6/site-packages/psychopy/visual/backends/pygletbackend.py", line 259, in swapBuffers - self.winHandle.dispatch_events() - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/window/xlib/__init__.py", line 881, in dispatch_events - self.dispatch_platform_event(e) - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/window/xlib/__init__.py", line 917, in dispatch_platform_event - event_handler(e) - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/window/xlib/__init__.py", line 1108, in _event_key - return self._event_key_view(ev) - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/window/xlib/__init__.py", line 1101, in _event_key_view - self.dispatch_event('on_key_release', symbol, modifiers) - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/window/__init__.py", line 1232, in dispatch_event - if EventDispatcher.dispatch_event(self, *args) != False: - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/event.py", line 371, in dispatch_event - event_type, args, getattr(self, event_type)) - File "/home/labopb/.local/lib/python3.6/site-packages/pyglet/event.py", line 392, in _raise_dispatch_exception - inspect.getargspec(handler) - File "/usr/lib/python3.6/inspect.py", line 1078, in getargspec - getfullargspec(func) - File "/usr/lib/python3.6/inspect.py", line 1130, in getfullargspec - raise TypeError('unsupported callable') from ex -TypeError: unsupported callable diff --git a/src/shared/utils.py b/src/shared/utils.py index 49ae41d7..18ae899a 100644 --- a/src/shared/utils.py +++ b/src/shared/utils.py @@ -34,6 +34,7 @@ def wait_until_yield(clock, deadline, hogCPUperiod=0.1, keyboard_accuracy=.0005) while current_time < deadline: if current_time < sleep_until: time.sleep(keyboard_accuracy) - yield + yield False + poll_windows() current_time = clock.getTime() diff --git a/src/tasks/retinotopy.py b/src/tasks/retinotopy.py index 9b99de12..d3c79eaf 100644 --- a/src/tasks/retinotopy.py +++ b/src/tasks/retinotopy.py @@ -30,7 +30,7 @@ def _setup(self, exp_win): exp_win, size=.15, units='deg', - opacity=.5, + opacity=.8, color=self.DOT_COLORS[0], colorSpace='rgb255' ) @@ -86,17 +86,17 @@ def _run(self, exp_win, ctl_win): level=logging.EXP, msg=f"Retinotopy {self.condition}: task starting at {time.time()}" ) - initial_wait = 16 if self.condition == 'RETBAR' else 22 - utils.wait_until(self.task_timer, initial_wait - 1 / config.FRAME_RATE) color_state = 0 - dot_next_change = self.DOT_MIN_DURATION + random.random()*5 + dot_next_change = 0 + dot_changes = [] - dot_changes = [dot_next_change] - self.fixation_dot.draw(exp_win) - yield True - for clearBuffer in self._run_condition(exp_win, ctl_win, initial_wait): + for do_yield in self._run_condition(exp_win, ctl_win): #TODO: log responses keypresses = event.getKeys(self.RESPONSE_KEY, timeStamped=self.task_timer) + for k in keypresses: + rt = k[2] - dot_changes[-1] + print(k, rt) + if self.task_timer.getTime() > dot_next_change: color_state = (color_state+1)%2 @@ -104,14 +104,24 @@ def _run(self, exp_win, ctl_win): self.DOT_COLORS[color_state], colorSpace='rgb255') dot_next_change += self.DOT_MIN_DURATION + random.random()*5 - dot_changes.append + dot_changes.append((color_state, self.task_timer.getTime())) + do_yield = True self.fixation_dot.draw(exp_win) if ctl_win: self.fixation_dot.draw(exp_win) - yield clearBuffer + if do_yield: + yield True - def _run_condition(self, exp_win, ctl_win, initial_wait): + def _run_condition(self, exp_win, ctl_win): + + initial_wait = 16 if self.condition == 'RETBAR' else 22 + yield True + yield from utils.wait_until_yield( + self.task_timer, + initial_wait - 1, + keyboard_accuracy=.001, + hogCPUperiod=2/config.FRAME_RATE) if 'BAR' in self.condition: middle_blank = 12 conds = np.asarray([0,1,0,1,2,3,2,3]) @@ -128,7 +138,11 @@ def _run_condition(self, exp_win, ctl_win, initial_wait): self.img.image = self._images[..., image_idx] self.img.mask = self._apertures[..., start_idx+frame] - utils.wait_until(self.task_timer, flip_time) + yield from utils.wait_until_yield + self.task_timer, + flip_time, + keyboard_accuracy=.001, + hogCPUperiod=2/config.FRAME_RATE) self.img.draw(exp_win) if ctl_win: @@ -145,7 +159,12 @@ def _run_condition(self, exp_win, ctl_win, initial_wait): image_idx = self._images_random[ci*32*15+fi] self.img.image = self._images[..., image_idx] self.img.mask = self._apertures[..., frame] - utils.wait_until(self.task_timer, flip_time) + + yield from utils.wait_until_yield + self.task_timer, + flip_time, + keyboard_accuracy=.001, + hogCPUperiod=2/config.FRAME_RATE) self.img.draw(exp_win) if ctl_win: diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index 4efd6d49..c5a3a2f3 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -82,7 +82,7 @@ def instructions(self, exp_win, ctl_win): def run(self, exp_win, ctl_win): self.task_timer = core.Clock() - frame_idx = 0 + flip_idx = 0 for clearBuffer in self._run(exp_win, ctl_win): # yield first to allow external draw before flip @@ -90,11 +90,11 @@ def run(self, exp_win, ctl_win): self._flip_all_windows(exp_win, ctl_win, clearBuffer) if not hasattr(self, "_exp_win_first_flip_time"): self._exp_win_first_flip_time = self._exp_win_last_flip_time - # increment the progress bar every second + # increment the progress bar depending on task flip rate if self.progress_bar: - frame_idx += 1 - if not frame_idx % self._progress_bar_refresh_rate: + if flip_idx % self._progress_bar_refresh_rate == 0: self.progress_bar.update(1) + flip_idx += 1 if self.progress_bar: self.progress_bar.clear() From 2a718691187990125dc3fa6a1b3eb955b16dc448 Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 28 Apr 2021 14:11:50 -0400 Subject: [PATCH 150/170] set 2d detector intensity range to 4 at startup --- src/shared/eyetracking.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/shared/eyetracking.py b/src/shared/eyetracking.py index a6257a68..0a88f358 100644 --- a/src/shared/eyetracking.py +++ b/src/shared/eyetracking.py @@ -216,6 +216,9 @@ def nonblocking(lock): class EyeTrackerClient(threading.Thread): + + EYE = "eye0" + def __init__(self, output_path, output_fname_base, profile=False, debug=False): super(EyeTrackerClient, self).__init__() self.stoprequest = threading.Event() @@ -287,12 +290,26 @@ def __init__(self, output_path, output_fname_base, profile=False, debug=False): } ) + # restart 2d detector plugin with custom output settings + self.send_recv_notification( + { + "subject": "start_eye_plugin", + "name": "Detector2DPlugin", + "target": self.EYE, + "args": { + "properties": { + "intensity_range": 4, + } + }, + } + ) + # stop a bunch of eye plugins for performance for plugin in ["NDSI_Manager"]: self.send_recv_notification( { "subject": "stop_eye_plugin", - "target": "eye0", + "target": self.EYE, "name": plugin, } ) @@ -301,7 +318,7 @@ def __init__(self, output_path, output_fname_base, profile=False, debug=False): { "subject": "start_eye_plugin", "name": "Aravis_Source", - "target": "eye0", + "target": self.EYE, "args": CAPTURE_SETTINGS, } ) From b7aa1cbc90e7f285ab63fdf6a2a3ac7cb438d1fd Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 29 Apr 2021 09:57:17 -0400 Subject: [PATCH 151/170] wip: timing adjustment for TR in retino --- .gitignore | 1 + src/shared/config.py | 2 ++ src/tasks/retinotopy.py | 37 ++++++++++++++++++++++++++----------- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 894a44cc..71513654 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,4 @@ venv.bak/ # mypy .mypy_cache/ +data/things/memory_designs/* diff --git a/src/shared/config.py b/src/shared/config.py index d6326c08..d2deb510 100644 --- a/src/shared/config.py +++ b/src/shared/config.py @@ -8,6 +8,8 @@ prefs.hardware["audioLib"] = ["sounddevice"] # prefs.hardware['general'] = ['glfw'] +TR = 1.49 #seconds + OUTPUT_DIR = "output" EYETRACKING_ROI = (60, 30, 660, 450) diff --git a/src/tasks/retinotopy.py b/src/tasks/retinotopy.py index d3c79eaf..df6a360d 100644 --- a/src/tasks/retinotopy.py +++ b/src/tasks/retinotopy.py @@ -115,13 +115,18 @@ def _run(self, exp_win, ctl_win): def _run_condition(self, exp_win, ctl_win): + frame_duration = 1/15. + initial_wait = 16 if self.condition == 'RETBAR' else 22 + initial_wait = 2 if self.condition == 'RETBAR' else 4 yield True + # wait until it's almost time to render first frame yield from utils.wait_until_yield( self.task_timer, - initial_wait - 1, + initial_wait - .2, keyboard_accuracy=.001, hogCPUperiod=2/config.FRAME_RATE) + if 'BAR' in self.condition: middle_blank = 12 conds = np.asarray([0,1,0,1,2,3,2,3]) @@ -129,7 +134,7 @@ def _run_condition(self, exp_win, ctl_win): order = 1-(ci%4>1)*2 for fi, frame in enumerate(range(28*15)[::order]): flip_time = (initial_wait + (ci>3) * middle_blank + - (ci*32*15+fi) * 4/config.FRAME_RATE + (ci*32*15+fi) * frame_duration - 1/config.FRAME_RATE) #flipVert = 1 - 2*(ci in [3,6,7]) @@ -138,7 +143,7 @@ def _run_condition(self, exp_win, ctl_win): self.img.image = self._images[..., image_idx] self.img.mask = self._apertures[..., start_idx+frame] - yield from utils.wait_until_yield + yield from utils.wait_until_yield( self.task_timer, flip_time, keyboard_accuracy=.001, @@ -152,15 +157,19 @@ def _run_condition(self, exp_win, ctl_win): yield True else: order = -1 if self.condition in ['RETCW', 'RETCON'] else 1 - for ci in range(8): # 8 cycles + if 'CW' in self.condition: + # change the frame duration to sync to the TR + TR = 1.49 + frame_duration = (21*TR)/(15*32) + for ci in range(2): # 8 cycles cycle_length = 32 if 'CW' in self.condition else 28 # shorten next loop, adds 4s blank for fi, frame in enumerate(range(cycle_length*15)[::order]): # 32/28 sec at 15Hz - flip_time = initial_wait + (ci*32*15+fi) * 4/config.FRAME_RATE - 1/config.FRAME_RATE + flip_time = initial_wait + (ci*32*15+fi) * frame_duration - 1/config.FRAME_RATE image_idx = self._images_random[ci*32*15+fi] self.img.image = self._images[..., image_idx] self.img.mask = self._apertures[..., frame] - yield from utils.wait_until_yield + yield from utils.wait_until_yield( self.task_timer, flip_time, keyboard_accuracy=.001, @@ -170,14 +179,20 @@ def _run_condition(self, exp_win, ctl_win): if ctl_win: self.img.draw(ctl_win) yield True - flip_time = self._exp_win_last_flip_time - self._exp_win_first_flip_time + real_flip_time = self._exp_win_last_flip_time - self._exp_win_first_flip_time + + exp_win.callOnFlip( + self._log_event, + {'image_idx': image_idx, 'aperture': frame} + ) if 'CW' not in self.condition: yield True # blank + yield True - utils.wait_until(self.task_timer, self.duration) - - def _save(self): - return False + yield from utils.wait_until_yield( + self.task_timer, + 32,#self.duration, + keyboard_accuracy=.001) def unload(self): del self._apertures From df0ebf0ffca571a4cdcd949791ea0866cf9a4e98 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 29 Apr 2021 11:44:45 -0400 Subject: [PATCH 152/170] add clock type to log_events, update retinotopy for our TR --- src/sessions/ses-retino.py | 2 +- src/tasks/retinotopy.py | 55 ++++++++++++++++++++++++++------------ src/tasks/task_base.py | 9 ++++--- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/sessions/ses-retino.py b/src/sessions/ses-retino.py index 3e9a3dd2..43edb6c8 100644 --- a/src/sessions/ses-retino.py +++ b/src/sessions/ses-retino.py @@ -4,5 +4,5 @@ retinotopy.Retinotopy( condition = condition, name=f"task-retinotopy{condition}", - ) for condition in ['RETEXP','RETCON','RETCCW','RETCW','RETBAR'] + ) for condition in ['RETCCW','RETCW','RETEXP','RETCON','RETBAR'] ] diff --git a/src/tasks/retinotopy.py b/src/tasks/retinotopy.py index df6a360d..2e1fd894 100644 --- a/src/tasks/retinotopy.py +++ b/src/tasks/retinotopy.py @@ -7,6 +7,10 @@ from ..shared import config, utils + +def generate_wedge(): + pass + class Retinotopy(Task): DEFAULT_INSTRUCTION = """You will see a dot in the center of the screen. @@ -43,7 +47,7 @@ def _setup(self, exp_win): self._images = np.load('data/retinotopy/images.npz')['images'].astype(np.float32)/255. if 'CW' in self.condition: - aperture_file = 'apertures_wedge.npz' + aperture_file = 'apertures_wedge_newtr.npz' elif self.condition in ['RETEXP', 'RETCON']: aperture_file = '/apertures_ring.npz' elif self.condition == 'RETBAR': @@ -81,21 +85,25 @@ def _instructions(self, exp_win, ctl_win): yield True def _run(self, exp_win, ctl_win): - + event.getKeys() # flush all keypresses exp_win.logOnFlip( level=logging.EXP, msg=f"Retinotopy {self.condition}: task starting at {time.time()}" ) color_state = 0 dot_next_change = 0 - dot_changes = [] + dot_change_idx = 0 for do_yield in self._run_condition(exp_win, ctl_win): #TODO: log responses keypresses = event.getKeys(self.RESPONSE_KEY, timeStamped=self.task_timer) for k in keypresses: - rt = k[2] - dot_changes[-1] - print(k, rt) + rt = k[1] - dot_last_change + self._log_event({ + 'trial_type':'response', + 'trial_number': dot_change_idx, + 'response_time': rt + }) if self.task_timer.getTime() > dot_next_change: @@ -103,8 +111,16 @@ def _run(self, exp_win, ctl_win): self.fixation_dot.setColor( self.DOT_COLORS[color_state], colorSpace='rgb255') + dot_last_change = dot_next_change dot_next_change += self.DOT_MIN_DURATION + random.random()*5 - dot_changes.append((color_state, self.task_timer.getTime())) + exp_win.callOnFlip( + self._log_event, + {'trial_type':'dot_color', + 'trial_number': dot_change_idx, + 'color': color_state}, + clock='flip' + ) + dot_change_idx += 1 do_yield = True self.fixation_dot.draw(exp_win) if ctl_win: @@ -119,6 +135,8 @@ def _run_condition(self, exp_win, ctl_win): initial_wait = 16 if self.condition == 'RETBAR' else 22 initial_wait = 2 if self.condition == 'RETBAR' else 4 + + cycle_length = 21*config.TR # 31.29, a bit shorter than 32s yield True # wait until it's almost time to render first frame yield from utils.wait_until_yield( @@ -134,7 +152,7 @@ def _run_condition(self, exp_win, ctl_win): order = 1-(ci%4>1)*2 for fi, frame in enumerate(range(28*15)[::order]): flip_time = (initial_wait + (ci>3) * middle_blank + - (ci*32*15+fi) * frame_duration + (ci*cycle_length*15+fi) * frame_duration - 1/config.FRAME_RATE) #flipVert = 1 - 2*(ci in [3,6,7]) @@ -152,19 +170,21 @@ def _run_condition(self, exp_win, ctl_win): self.img.draw(exp_win) if ctl_win: self.img.draw(ctl_win) + + exp_win.callOnFlip( + self._log_event, + {'condition': self.condition, 'image_idx': image_idx, 'aperture': frame}, + clock='flip' + ) yield True print(self.task_timer.getTime(),flip_time) yield True else: order = -1 if self.condition in ['RETCW', 'RETCON'] else 1 - if 'CW' in self.condition: - # change the frame duration to sync to the TR - TR = 1.49 - frame_duration = (21*TR)/(15*32) for ci in range(2): # 8 cycles - cycle_length = 32 if 'CW' in self.condition else 28 # shorten next loop, adds 4s blank - for fi, frame in enumerate(range(cycle_length*15)[::order]): # 32/28 sec at 15Hz - flip_time = initial_wait + (ci*32*15+fi) * frame_duration - 1/config.FRAME_RATE + display_length = cycle_length if 'CW' in self.condition else 28 # shorten next loop, adds 4s blank + for fi, frame in enumerate(range(int(display_length*15))[::order]): # 32/28 sec at 15Hz + flip_time = initial_wait + (ci*cycle_length*15+fi) * frame_duration - 1/config.FRAME_RATE image_idx = self._images_random[ci*32*15+fi] self.img.image = self._images[..., image_idx] self.img.mask = self._apertures[..., frame] @@ -178,13 +198,14 @@ def _run_condition(self, exp_win, ctl_win): self.img.draw(exp_win) if ctl_win: self.img.draw(ctl_win) - yield True - real_flip_time = self._exp_win_last_flip_time - self._exp_win_first_flip_time exp_win.callOnFlip( self._log_event, - {'image_idx': image_idx, 'aperture': frame} + {'condition': self.condition, 'image_idx': image_idx, 'aperture': frame}, + clock='flip' ) + yield True + if 'CW' not in self.condition: yield True # blank yield True diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index c5a3a2f3..5f5b6a11 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -80,7 +80,6 @@ def instructions(self, exp_win, ctl_win): self._flip_all_windows(exp_win, ctl_win, True) def run(self, exp_win, ctl_win): - self.task_timer = core.Clock() flip_idx = 0 @@ -113,8 +112,12 @@ def restart(self): if hasattr(self, "_restart"): self._restart() - def _log_event(self, event): - event.update({"onset": self.task_timer.getTime(),"sample":time.monotonic()}) + def _log_event(self, event, clock='task'): + if clock == 'task': + onset = self.task_timer.getTime() + elif clock == 'flip': + onset = self._exp_win_last_flip_time - self._exp_win_first_flip_time + event.update({"onset": onset, "sample": time.monotonic()}) self._events.append(event) def _save(self): From 874560eeac322fbbaee87c86992c28ac6f9740f6 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 29 Apr 2021 14:14:07 -0400 Subject: [PATCH 153/170] fix timing --- src/tasks/retinotopy.py | 8 ++++---- src/tasks/task_base.py | 14 ++++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/tasks/retinotopy.py b/src/tasks/retinotopy.py index 2e1fd894..d426f45e 100644 --- a/src/tasks/retinotopy.py +++ b/src/tasks/retinotopy.py @@ -135,7 +135,7 @@ def _run_condition(self, exp_win, ctl_win): initial_wait = 16 if self.condition == 'RETBAR' else 22 initial_wait = 2 if self.condition == 'RETBAR' else 4 - + middle_blank = 0 cycle_length = 21*config.TR # 31.29, a bit shorter than 32s yield True # wait until it's almost time to render first frame @@ -212,9 +212,9 @@ def _run_condition(self, exp_win, ctl_win): yield from utils.wait_until_yield( self.task_timer, - 32,#self.duration, + initial_wait * 2 + ((ci+1)*cycle_length*15+fi) * frame_duration + middle_blank, keyboard_accuracy=.001) def unload(self): - del self._apertures - del self._images + del self._apertures, self._images + del self.img, self._images_random, self.fixation_dot diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index 5f5b6a11..d08f7a28 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -44,6 +44,9 @@ def setup( self._progress_bar_refresh_rate = config.FRAME_RATE def _setup(self, exp_win): + self._exp_win_first_flip_time = None + self._exp_win_last_flip_time = None + self._ctl_win_last_flip_time = None pass def _generate_unique_filename(self, suffix, ext="tsv"): @@ -67,8 +70,13 @@ def __str__(self): def _flip_all_windows(self, exp_win, ctl_win=None, clearBuffer=True): if not ctl_win is None: - self._ctl_win_last_flip_time = ctl_win.flip(clearBuffer=clearBuffer) - self._exp_win_last_flip_time = exp_win.flip(clearBuffer=clearBuffer) + ctl_win.timeOnFlip(self, '_ctl_win_last_flip_time') + ctl_win.flip(clearBuffer=clearBuffer) + + if not self._exp_win_first_flip_time: + exp_win.timeOnFlip(self, '_exp_win_first_flip_time') + exp_win.timeOnFlip(self, '_exp_win_last_flip_time') + exp_win.flip(clearBuffer=clearBuffer) def instructions(self, exp_win, ctl_win): if hasattr(self, "_instructions"): @@ -87,8 +95,6 @@ def run(self, exp_win, ctl_win): # yield first to allow external draw before flip yield self._flip_all_windows(exp_win, ctl_win, clearBuffer) - if not hasattr(self, "_exp_win_first_flip_time"): - self._exp_win_first_flip_time = self._exp_win_last_flip_time # increment the progress bar depending on task flip rate if self.progress_bar: if flip_idx % self._progress_bar_refresh_rate == 0: From 54a1196f159aa17ddcc63b2d924c99569e5dc66a Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 29 Apr 2021 16:28:47 -0400 Subject: [PATCH 154/170] add an option to skip screen resolution forcing --- main.py | 6 ++++-- src/shared/parser.py | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index f80f2ea6..e8441280 100755 --- a/main.py +++ b/main.py @@ -7,7 +7,8 @@ def run(parsed): # initializing the screen need to be done before loading any psychopy - screen.init_exp_screen() + if not parsed.no_force_resolution: + screen.init_exp_screen() try: ses_mod = importlib.import_module('src.sessions.ses-%s'%parsed.tasks) tasks = ses_mod.get_tasks(parsed) if hasattr(ses_mod, 'get_tasks') else ses_mod.TASKS @@ -29,7 +30,8 @@ def run(parsed): parsed.record_movie, ) finally: - screen.reset_exp_screen() + if not parsed.no_force_resolution: + screen.reset_exp_screen() def run_profiled(parsed): import cProfile diff --git a/src/shared/parser.py b/src/shared/parser.py index 115c0264..00a0e5be 100644 --- a/src/shared/parser.py +++ b/src/shared/parser.py @@ -27,6 +27,11 @@ def parse_args(): "--skip_n_tasks", help="skip n of the tasks", default=0, type=int ) parser.add_argument("--ctl_win", help="show control window", action="store_true") + parser.add_argument( + "--no-force-resolution", + help="do not run xrandr to force screen resolution", + action="store_true") + parser.add_argument( "--run_on_battery", help="allow the script to run on battery", From b138140e631753472d4378cd1a5107ac4d4c5d34 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 29 Apr 2021 16:34:45 -0400 Subject: [PATCH 155/170] fix file saving: should occur after task is fully stopped in particular for calibration where other threads are using task callbacks --- src/shared/cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/shared/cli.py b/src/shared/cli.py index d4100eda..22eb96c1 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -80,8 +80,6 @@ def run_task( if eyetracker: eyetracker.stop_recording() - # now that time is less sensitive: save files - task.save() run_task_loop( task.stop(exp_win, ctl_win), @@ -90,6 +88,9 @@ def run_task( record_movie=exp_win if record_movie else False, ) + # now that time is less sensitive: save files + task.save() + return shortcut_evt From 41daedbc1e05580f8556706018484eb18563ba73 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 30 Apr 2021 16:34:33 -0400 Subject: [PATCH 156/170] [DATALAD] Recorded changes --- .gitmodules | 4 ++++ data/videogames/mario | 1 + 2 files changed, 5 insertions(+) create mode 160000 data/videogames/mario diff --git a/.gitmodules b/.gitmodules index e6c781ad..04be9982 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,3 +6,7 @@ path = data/videogames/shinobi url = git@github.com:courtois-neuromod/shinobi.stimuli.git datalad-id = 74cb8a6e-664b-11eb-a4f2-1a44c1d5432b +[submodule "data/videogames/mario"] + path = data/videogames/mario + url = git@github.com:courtois-neuromod/mario.stimuli.git + datalad-id = 23782599-795f-46e5-aefe-5ecd578e968d diff --git a/data/videogames/mario b/data/videogames/mario new file mode 160000 index 00000000..2979c6a1 --- /dev/null +++ b/data/videogames/mario @@ -0,0 +1 @@ +Subproject commit 2979c6a1e388459a3910db8bd7e658734b88b50d From 184666241ac2cfc1a2939003cfb59f410c06bb6c Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 5 May 2021 10:16:34 -0400 Subject: [PATCH 157/170] cleaner sound handling across emulators --- data/videogames/mario | 2 +- src/sessions/ses-supermariobros.py | 4 ++-- src/tasks/videogame.py | 9 +++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/data/videogames/mario b/data/videogames/mario index 2979c6a1..ca56a23d 160000 --- a/data/videogames/mario +++ b/data/videogames/mario @@ -1 +1 @@ -Subproject commit 2979c6a1e388459a3910db8bd7e658734b88b50d +Subproject commit ca56a23da47ce47a62eb2065c1b21acfeeb233ef diff --git a/src/sessions/ses-supermariobros.py b/src/sessions/ses-supermariobros.py index 93005377..fd50f1ae 100644 --- a/src/sessions/ses-supermariobros.py +++ b/src/sessions/ses-supermariobros.py @@ -4,7 +4,7 @@ # point to a copy of the whole gym-retro with custom states and scenarii retro.data.Integrations.add_custom_path( - os.path.join(os.getcwd(), "data", "videogames", "supermariobros") + os.path.join(os.getcwd(), "data", "videogames", "mario") ) from ..tasks import images, videogame, memory, task_base @@ -26,7 +26,7 @@ ("Level1-1", "scenario"), ("Level1-2", "scenario"), ("Level1-3", "scenario")] -random.shuffle(levels_scenario) # randomize order +#random.shuffle(levels_scenario) # randomize order TASKS = sum( [ diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index dcfc68f6..03417e9e 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -87,8 +87,13 @@ def __init__( self.inttype = inttype def _setup(self, exp_win): - self.game_sound = SoundDeviceBlockStream(stereo=True, blockSize=735) self._first_frame = self.emulator.reset() + first_sound_chunk = self.emulator.em.get_audio() + blockSize = first_sound_chunk.shape[0] + self.game_sound = SoundDeviceBlockStream( + sampleRate = self.emulator.em.get_audio_rate(), + stereo=(first_sound_chunk.ndim==2 & first_sound_chunk.shape[1]==2), + blockSize=blockSize) min_ratio = min( exp_win.size[0] / self._first_frame.shape[1], @@ -107,7 +112,7 @@ def _setup(self, exp_win): ) def _transform_soundblock(self, sound_block): - return sound_block[:735] / float(2 ** 15) + return sound_block[:self.game_sound.blockSize] / float(2 ** 15) def _render_graphics_sound(self, obs, sound_block, exp_win, ctl_win): self.game_vis_stim.image = obs / 255.0 From cf0fccb7e8ad7f995a45023f801bea417438ac7b Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 5 May 2021 13:46:10 -0400 Subject: [PATCH 158/170] make nicer instructions --- src/sessions/ses-shinobi3levels.py | 1 + src/tasks/videogame.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/sessions/ses-shinobi3levels.py b/src/sessions/ses-shinobi3levels.py index 8c80ad9e..e6aa3095 100644 --- a/src/sessions/ses-shinobi3levels.py +++ b/src/sessions/ses-shinobi3levels.py @@ -39,6 +39,7 @@ max_duration=10 * 60, # if when level completed or dead we exceed that time in secs, stop the task name=f"task-shinobi_run-{run+1:02d}", + instruction="Let's play Shinobi III: {state_name}\nHave fun!", # post_level_ratings = [(q, 7) for q in flow_ratings] ), task_base.Pause(), diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index 03417e9e..f3ea99cb 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -136,7 +136,7 @@ def unload(self): class VideoGame(VideoGameBase): - DEFAULT_INSTRUCTION = "Let's play a video game.\n%s: %s\nHave fun!" + DEFAULT_INSTRUCTION = "Let's play {game_name}: {state_name}\nHave fun!" def __init__( self, @@ -153,7 +153,9 @@ def __init__( def _instructions(self, exp_win, ctl_win): - instruction = self.instruction % (self.game_name, self.state_name) + instruction = self.instruction.format( + **{'game_name':self.game_name, + 'state_name':self.state_name}) screen_text = visual.TextStim( exp_win, From d6e6c982415b526247595fb334d562d335c01d3d Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 7 May 2021 09:24:21 -0400 Subject: [PATCH 159/170] renaming to mario --- src/sessions/{ses-supermariobros.py => ses-mario.py} | 6 ++++-- src/tasks/videogame.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) rename src/sessions/{ses-supermariobros.py => ses-mario.py} (91%) diff --git a/src/sessions/ses-supermariobros.py b/src/sessions/ses-mario.py similarity index 91% rename from src/sessions/ses-supermariobros.py rename to src/sessions/ses-mario.py index fd50f1ae..0a640e26 100644 --- a/src/sessions/ses-supermariobros.py +++ b/src/sessions/ses-mario.py @@ -28,6 +28,7 @@ ("Level1-3", "scenario")] #random.shuffle(levels_scenario) # randomize order + TASKS = sum( [ [ @@ -39,8 +40,9 @@ repeat_scenario=True, max_duration=10 * 60, # if when level completed or dead we exceed that time in secs, stop the task - name=f"task-supermariobros_run-{run+1:02d}", - # post_level_ratings = [(q, 7) for q in flow_ratings] + name=f"task-mario_run-{run+1:02d}", + instruction="playing Super Mario Bros {state_name} \n\n Let's-a go!", + # post_level_ratings = [(q, 7) for q in flow_ratings], ), task_base.Pause(), ] diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index f3ea99cb..d68ba382 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -258,7 +258,7 @@ def _run_emulator(self, exp_win, ctl_win): }, ) yield True - _nextFrameT = self.task_timer.getTime() + _nextFrameT = self._retraceInterval while not _done: level_step += 1 while _nextFrameT > (self.task_timer.getTime() - @@ -546,7 +546,7 @@ def __init__(self, *args, **kwargs): def _run(self, exp_win, ctl_win): - exp_win.waitBlanking = False + #exp_win.waitBlanking = False self._nlevels = 0 while True: @@ -581,7 +581,7 @@ def _run(self, exp_win, ctl_win): if time_exceeded or not self._repeat_scenario_multilevel: break - exp_win.waitBlanking = True + #exp_win.waitBlanking = True class VideoGameReplay(VideoGameBase): From d3d3eca3bab60f19de121707ca81d7cd65043765 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 11 May 2021 13:35:27 -0400 Subject: [PATCH 160/170] update mario.stimuli --- data/videogames/mario | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/videogames/mario b/data/videogames/mario index ca56a23d..cf2701d0 160000 --- a/data/videogames/mario +++ b/data/videogames/mario @@ -1 +1 @@ -Subproject commit ca56a23da47ce47a62eb2065c1b21acfeeb233ef +Subproject commit cf2701d004d3fcf9525cd9f1a56d558c729a4595 From bbcb5f973ded92f718b9757acde035d1d20b25cc Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 13 May 2021 11:03:19 -0400 Subject: [PATCH 161/170] retinotopy for mixed wedges and rings, global fixes to progress bar --- requirements.txt | 1 + src/sessions/ses-retino.py | 3 +- src/shared/cli.py | 2 +- src/tasks/retinotopy.py | 84 ++++++++++++++++++++++++-------------- src/tasks/task_base.py | 9 +++- 5 files changed, 65 insertions(+), 34 deletions(-) diff --git a/requirements.txt b/requirements.txt index e5de3452..eb8d13a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ PsychoPy>=2020.2.4.post1 gym-retro>=0.8.0 pandas>=1.1.1 python-dotenv +tqdm>=4.60.0 diff --git a/src/sessions/ses-retino.py b/src/sessions/ses-retino.py index 43edb6c8..72ad467b 100644 --- a/src/sessions/ses-retino.py +++ b/src/sessions/ses-retino.py @@ -3,6 +3,7 @@ TASKS = [ retinotopy.Retinotopy( condition = condition, + ncycles=4, name=f"task-retinotopy{condition}", - ) for condition in ['RETCCW','RETCW','RETEXP','RETCON','RETBAR'] + ) for condition in ['RETRINGS','RETWEDGES','RETBAR',]#'RETCCW','RETCW','RETEXP','RETCON'] ] diff --git a/src/shared/cli.py b/src/shared/cli.py index d4100eda..fd294dd0 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -214,7 +214,7 @@ def main_loop( print("Here are the stimuli planned for today\n" + "_" * 50) for task in all_tasks: - print("- " + task.name) + print(f"- {task.name} {getattr(task,'duration','')}" ) print("_" * 50) try: diff --git a/src/tasks/retinotopy.py b/src/tasks/retinotopy.py index d426f45e..b2b509f2 100644 --- a/src/tasks/retinotopy.py +++ b/src/tasks/retinotopy.py @@ -21,12 +21,15 @@ class Retinotopy(Task): DOT_COLORS = [(237, 96, 31), (66, 135, 245)] DOT_MIN_DURATION = 3 RESPONSE_KEY = 'a' + PROGRESS_BAR_FORMAT = "{l_bar}{bar}| {n:.02f}/{total:.02f} [{elapsed}<{remaining}, {rate_fmt}{postfix}]" - def __init__(self, condition, *args, **kwargs): + def __init__(self, condition, ncycles=8, *args, **kwargs): super().__init__(**kwargs) - if condition not in ['RETCCW', 'RETCW', 'RETEXP', 'RETCON', 'RETBAR']: + if condition not in ['RETCCW', 'RETCW', 'RETWEDGES', 'RETRINGS', 'RETEXP', 'RETCON', 'RETBAR']: raise ValueError("Condition {condition} does not exists") self.condition = condition + self.ncycles = ncycles + def _setup(self, exp_win): @@ -39,30 +42,48 @@ def _setup(self, exp_win): colorSpace='rgb255' ) + + grid = np.load("data/retinotopy/grid.npz")['grid'] + self.grid = visual.ImageStim( + exp_win, + image=np.ones((1,1,3)), + mask=grid/128.-1, + size=10, + units='deg' + ) + self.img = visual.ImageStim( exp_win, - size=(768,768), - units='pixels', + size=10, + units='deg', flipVert=True) self._images = np.load('data/retinotopy/images.npz')['images'].astype(np.float32)/255. - if 'CW' in self.condition: + + if self.condition in ['RETCW', 'RETCCW', 'RETWEDGES']: aperture_file = 'apertures_wedge_newtr.npz' - elif self.condition in ['RETEXP', 'RETCON']: + elif self.condition in ['RETEXP', 'RETCON', 'RETRINGS']: aperture_file = '/apertures_ring.npz' elif self.condition == 'RETBAR': aperture_file = 'apertures_bars.npz' + self.duration = 8*32 + 16*2 + 12 # cycles + initial_final_waits + middle_blank self._apertures = np.load(f"data/retinotopy/{aperture_file}")['apertures'].astype(np.float32)/128.-1 + self.cycle_length = 21*config.TR # a bit less than 32s for TR=1.49 + self.initial_wait = 16 if self.condition == 'RETBAR' else 22 + self.middle_blank = 12 if self.condition in ['RETRINGS', 'RETWEDGES', 'RETBAR'] else 0 + self.duration = ( + 32 * self.ncycles * (1 + (self.condition in ['RETRINGS', 'RETWEDGES'])) + + self.initial_wait * 2 + + self.middle_blank) + # draw random order with different successive stimuli self._images_random = np.random.randint(0, 99, size=(8*32*15)) #max nframe in CW conditions while any(np.ediff1d(self._images_random, to_begin=[-1])==0): self._images_random[np.ediff1d(self._images_random, to_begin=[-1])==0] += 1 self._images_random[self._images_random==100] = 0 - - self.duration = 300 # seconds - self._progress_bar_refresh_rate = 15 + self._progress_bar_refresh_rate = False self.events = pandas.DataFrame() @@ -86,9 +107,6 @@ def _instructions(self, exp_win, ctl_win): def _run(self, exp_win, ctl_win): event.getKeys() # flush all keypresses - exp_win.logOnFlip( - level=logging.EXP, msg=f"Retinotopy {self.condition}: task starting at {time.time()}" - ) color_state = 0 dot_next_change = 0 @@ -101,11 +119,10 @@ def _run(self, exp_win, ctl_win): rt = k[1] - dot_last_change self._log_event({ 'trial_type':'response', - 'trial_number': dot_change_idx, + 'trial_number': dot_change_idx-1, 'response_time': rt }) - if self.task_timer.getTime() > dot_next_change: color_state = (color_state+1)%2 self.fixation_dot.setColor( @@ -122,37 +139,37 @@ def _run(self, exp_win, ctl_win): ) dot_change_idx += 1 do_yield = True - self.fixation_dot.draw(exp_win) - if ctl_win: - self.fixation_dot.draw(exp_win) + if do_yield: + self.grid.draw(exp_win) + self.fixation_dot.draw(exp_win) + if ctl_win: + self.grid.draw(ctl_win) + self.fixation_dot.draw(ctl_win) + previous_flip_time = self._exp_win_last_flip_time yield True + self.progress_bar.update(self._exp_win_last_flip_time - previous_flip_time) def _run_condition(self, exp_win, ctl_win): frame_duration = 1/15. - initial_wait = 16 if self.condition == 'RETBAR' else 22 - initial_wait = 2 if self.condition == 'RETBAR' else 4 - middle_blank = 0 - cycle_length = 21*config.TR # 31.29, a bit shorter than 32s yield True # wait until it's almost time to render first frame yield from utils.wait_until_yield( self.task_timer, - initial_wait - .2, + self.initial_wait - .2, keyboard_accuracy=.001, hogCPUperiod=2/config.FRAME_RATE) if 'BAR' in self.condition: - middle_blank = 12 conds = np.asarray([0,1,0,1,2,3,2,3]) for ci, start_idx in enumerate(conds*28*15): order = 1-(ci%4>1)*2 for fi, frame in enumerate(range(28*15)[::order]): - flip_time = (initial_wait + (ci>3) * middle_blank + - (ci*cycle_length*15+fi) * frame_duration + flip_time = (self.initial_wait + (ci>3) * self.middle_blank + + (ci*self.cycle_length*15+fi) * frame_duration - 1/config.FRAME_RATE) #flipVert = 1 - 2*(ci in [3,6,7]) @@ -180,11 +197,18 @@ def _run_condition(self, exp_win, ctl_win): print(self.task_timer.getTime(),flip_time) yield True else: - order = -1 if self.condition in ['RETCW', 'RETCON'] else 1 - for ci in range(2): # 8 cycles - display_length = cycle_length if 'CW' in self.condition else 28 # shorten next loop, adds 4s blank + orders = [-1 if self.condition in ['RETCW', 'RETCON'] else 1] * self.ncycles + if self.condition in ['RETWEDGES', 'RETRINGS']: + orders = [1] * self.ncycles + [-1] * self.ncycles + middle_blank = 12 + for ci, order in enumerate(orders): + display_length = (self.cycle_length + if self.condition in ['RETCW', 'RETCCW', 'RETWEDGES'] + else 28) # shorten next loop, adds 4s blank for fi, frame in enumerate(range(int(display_length*15))[::order]): # 32/28 sec at 15Hz - flip_time = initial_wait + (ci*cycle_length*15+fi) * frame_duration - 1/config.FRAME_RATE + flip_time = (self.initial_wait + + (ci*self.cycle_length*15+fi) * frame_duration + + self.middle_blank - 1/config.FRAME_RATE) image_idx = self._images_random[ci*32*15+fi] self.img.image = self._images[..., image_idx] self.img.mask = self._apertures[..., frame] @@ -212,7 +236,7 @@ def _run_condition(self, exp_win, ctl_win): yield from utils.wait_until_yield( self.task_timer, - initial_wait * 2 + ((ci+1)*cycle_length*15+fi) * frame_duration + middle_blank, + self.initial_wait * 2 + ((ci+1)*self.cycle_length*15+fi) * frame_duration + self.middle_blank, keyboard_accuracy=.001) def unload(self): diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index d08f7a28..1631a1e5 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -10,6 +10,7 @@ class Task(object): DEFAULT_INSTRUCTION = "" + PROGRESS_BAR_FORMAT = '{l_bar}{bar}{r_bar}' def __init__(self, name, instruction=None): self.name = name @@ -38,7 +39,9 @@ def setup( self._setup(exp_win) # initialize a progress bar if we know the duration of the task self.progress_bar = ( - tqdm.tqdm(total=self.duration) if hasattr(self, "duration") else False + tqdm.tqdm(total=self.duration, + bar_format=self.PROGRESS_BAR_FORMAT, + ) if hasattr(self, "duration") else False ) if not hasattr(self, "_progress_bar_refresh_rate"): self._progress_bar_refresh_rate = config.FRAME_RATE @@ -97,7 +100,7 @@ def run(self, exp_win, ctl_win): self._flip_all_windows(exp_win, ctl_win, clearBuffer) # increment the progress bar depending on task flip rate if self.progress_bar: - if flip_idx % self._progress_bar_refresh_rate == 0: + if self._progress_bar_refresh_rate and flip_idx % self._progress_bar_refresh_rate == 0: self.progress_bar.update(1) flip_idx += 1 @@ -110,6 +113,8 @@ def stop(self, exp_win, ctl_win): for clearBuffer in self._stop(exp_win, ctl_win): yield self._flip_all_windows(exp_win, ctl_win, clearBuffer) + if self.progress_bar: + self.progress_bar.close() # 2 flips to clear screen and backbuffer for i in range(2): self._flip_all_windows(exp_win, ctl_win, True) From df9536f44f7b4e86c16f9d8a860e422bfc276e8b Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 13 May 2021 16:38:52 -0400 Subject: [PATCH 162/170] fix flip timing/logging issues --- src/sessions/ses-retino.py | 6 +++++- src/tasks/retinotopy.py | 20 ++++++++++++-------- src/tasks/task_base.py | 13 ++++++------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/sessions/ses-retino.py b/src/sessions/ses-retino.py index 72ad467b..b305e037 100644 --- a/src/sessions/ses-retino.py +++ b/src/sessions/ses-retino.py @@ -1,9 +1,13 @@ from ..tasks import retinotopy +import random + +conditions = ['RETBAR','RETRINGS','RETWEDGES',] +#conditions = random.shuffle(conditions) TASKS = [ retinotopy.Retinotopy( condition = condition, ncycles=4, name=f"task-retinotopy{condition}", - ) for condition in ['RETRINGS','RETWEDGES','RETBAR',]#'RETCCW','RETCW','RETEXP','RETCON'] + ) for condition in conditions #'RETCCW','RETCW','RETEXP','RETCON'] ] diff --git a/src/tasks/retinotopy.py b/src/tasks/retinotopy.py index b2b509f2..03dde9c3 100644 --- a/src/tasks/retinotopy.py +++ b/src/tasks/retinotopy.py @@ -37,7 +37,6 @@ def _setup(self, exp_win): exp_win, size=.15, units='deg', - opacity=.8, color=self.DOT_COLORS[0], colorSpace='rgb255' ) @@ -65,8 +64,8 @@ def _setup(self, exp_win): elif self.condition in ['RETEXP', 'RETCON', 'RETRINGS']: aperture_file = '/apertures_ring.npz' elif self.condition == 'RETBAR': + self.ncycles = 8 aperture_file = 'apertures_bars.npz' - self.duration = 8*32 + 16*2 + 12 # cycles + initial_final_waits + middle_blank self._apertures = np.load(f"data/retinotopy/{aperture_file}")['apertures'].astype(np.float32)/128.-1 self.cycle_length = 21*config.TR # a bit less than 32s for TR=1.49 @@ -85,7 +84,6 @@ def _setup(self, exp_win): self._progress_bar_refresh_rate = False - self.events = pandas.DataFrame() super()._setup(exp_win) @@ -111,17 +109,24 @@ def _run(self, exp_win, ctl_win): color_state = 0 dot_next_change = 0 dot_change_idx = 0 - + rt_sum = 0 + n_keypresses = 0 for do_yield in self._run_condition(exp_win, ctl_win): #TODO: log responses keypresses = event.getKeys(self.RESPONSE_KEY, timeStamped=self.task_timer) for k in keypresses: rt = k[1] - dot_last_change + rt_sum += rt + n_keypresses += 1 + mean_rt = rt_sum / n_keypresses self._log_event({ 'trial_type':'response', 'trial_number': dot_change_idx-1, 'response_time': rt }) + self.progress_bar.set_description( + f"({dot_change_idx-1}/{n_keypresses}), mean RT: {mean_rt}" + ) if self.task_timer.getTime() > dot_next_change: color_state = (color_state+1)%2 @@ -148,9 +153,9 @@ def _run(self, exp_win, ctl_win): self.fixation_dot.draw(ctl_win) previous_flip_time = self._exp_win_last_flip_time yield True + #print(self._exp_win_last_flip_time, previous_flip_time) self.progress_bar.update(self._exp_win_last_flip_time - previous_flip_time) - def _run_condition(self, exp_win, ctl_win): frame_duration = 1/15. @@ -194,7 +199,6 @@ def _run_condition(self, exp_win, ctl_win): clock='flip' ) yield True - print(self.task_timer.getTime(),flip_time) yield True else: orders = [-1 if self.condition in ['RETCW', 'RETCON'] else 1] * self.ncycles @@ -208,7 +212,7 @@ def _run_condition(self, exp_win, ctl_win): for fi, frame in enumerate(range(int(display_length*15))[::order]): # 32/28 sec at 15Hz flip_time = (self.initial_wait + (ci*self.cycle_length*15+fi) * frame_duration + - self.middle_blank - 1/config.FRAME_RATE) + (ci>self.ncycles//2) * self.middle_blank - 1/config.FRAME_RATE) image_idx = self._images_random[ci*32*15+fi] self.img.image = self._images[..., image_idx] self.img.mask = self._apertures[..., frame] @@ -236,7 +240,7 @@ def _run_condition(self, exp_win, ctl_win): yield from utils.wait_until_yield( self.task_timer, - self.initial_wait * 2 + ((ci+1)*self.cycle_length*15+fi) * frame_duration + self.middle_blank, + self.duration, keyboard_accuracy=.001) def unload(self): diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index 1631a1e5..6005a68b 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -47,6 +47,9 @@ def setup( self._progress_bar_refresh_rate = config.FRAME_RATE def _setup(self, exp_win): + # needs to be the 1rst callbacks + exp_win.timeOnFlip(self, '_exp_win_last_flip_time') + exp_win.timeOnFlip(self, '_exp_win_first_flip_time') self._exp_win_first_flip_time = None self._exp_win_last_flip_time = None self._ctl_win_last_flip_time = None @@ -76,10 +79,9 @@ def _flip_all_windows(self, exp_win, ctl_win=None, clearBuffer=True): ctl_win.timeOnFlip(self, '_ctl_win_last_flip_time') ctl_win.flip(clearBuffer=clearBuffer) - if not self._exp_win_first_flip_time: - exp_win.timeOnFlip(self, '_exp_win_first_flip_time') - exp_win.timeOnFlip(self, '_exp_win_last_flip_time') exp_win.flip(clearBuffer=clearBuffer) + # set callback for next flip, to be the first callback for other callbacks to use + exp_win.timeOnFlip(self, '_exp_win_last_flip_time') def instructions(self, exp_win, ctl_win): if hasattr(self, "_instructions"): @@ -104,16 +106,13 @@ def run(self, exp_win, ctl_win): self.progress_bar.update(1) flip_idx += 1 - if self.progress_bar: - self.progress_bar.clear() - self.progress_bar.close() - def stop(self, exp_win, ctl_win): if hasattr(self, "_stop"): for clearBuffer in self._stop(exp_win, ctl_win): yield self._flip_all_windows(exp_win, ctl_win, clearBuffer) if self.progress_bar: + self.progress_bar.clear() self.progress_bar.close() # 2 flips to clear screen and backbuffer for i in range(2): From a5f9034a91a1c3dd2cab65cab19ee858ea4d8d8f Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 20 May 2021 09:19:27 -0400 Subject: [PATCH 163/170] fix flip timing log, add retino block in events file --- src/tasks/retinotopy.py | 18 +++++++++++++++++- src/tasks/task_base.py | 10 +++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/tasks/retinotopy.py b/src/tasks/retinotopy.py index 03dde9c3..e484642c 100644 --- a/src/tasks/retinotopy.py +++ b/src/tasks/retinotopy.py @@ -177,6 +177,8 @@ def _run_condition(self, exp_win, ctl_win): (ci*self.cycle_length*15+fi) * frame_duration - 1/config.FRAME_RATE) + exp_win.timeOnFlip(self, '_cycle_start') + #flipVert = 1 - 2*(ci in [3,6,7]) #flipHoriz = 1 - 2*(ci in [2]) image_idx = self._images_random[ci*32*15+fi] @@ -199,6 +201,13 @@ def _run_condition(self, exp_win, ctl_win): clock='flip' ) yield True + self._events.append({ + 'condition': self.condition, + 'trial_type': 'cycle', + 'onset': self._cycle_start, + 'duration': self._exp_win_last_flip_time - self._cycle_start + }) + yield True else: orders = [-1 if self.condition in ['RETCW', 'RETCON'] else 1] * self.ncycles @@ -217,6 +226,8 @@ def _run_condition(self, exp_win, ctl_win): self.img.image = self._images[..., image_idx] self.img.mask = self._apertures[..., frame] + exp_win.timeOnFlip(self, '_cycle_start') + yield from utils.wait_until_yield( self.task_timer, flip_time, @@ -233,7 +244,12 @@ def _run_condition(self, exp_win, ctl_win): clock='flip' ) yield True - + self._events.append({ + 'condition': self.condition, + 'trial_type': 'cycle', + 'onset': self._cycle_start, + 'duration': self._exp_win_last_flip_time - self._cycle_start + }) if 'CW' not in self.condition: yield True # blank yield True diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index 6005a68b..6df3dd3e 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -47,9 +47,6 @@ def setup( self._progress_bar_refresh_rate = config.FRAME_RATE def _setup(self, exp_win): - # needs to be the 1rst callbacks - exp_win.timeOnFlip(self, '_exp_win_last_flip_time') - exp_win.timeOnFlip(self, '_exp_win_first_flip_time') self._exp_win_first_flip_time = None self._exp_win_last_flip_time = None self._ctl_win_last_flip_time = None @@ -93,7 +90,14 @@ def instructions(self, exp_win, ctl_win): self._flip_all_windows(exp_win, ctl_win, True) def run(self, exp_win, ctl_win): + # needs to be the 1rst callbacks + for attr in ['_exp_win_last_flip_time', '_exp_win_first_flip_time']: + exp_win.timeOnFlip(self, attr) + self.task_timer = core.Clock() + + if self.progress_bar: + self.progress_bar.reset() flip_idx = 0 for clearBuffer in self._run(exp_win, ctl_win): From bfc27f2103596996951b406240804090be1cbf19 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 20 May 2021 10:25:36 -0400 Subject: [PATCH 164/170] refix flip timing log, only show first response for RT --- src/tasks/retinotopy.py | 12 +++++++++--- src/tasks/task_base.py | 3 +-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/tasks/retinotopy.py b/src/tasks/retinotopy.py index e484642c..df9ee973 100644 --- a/src/tasks/retinotopy.py +++ b/src/tasks/retinotopy.py @@ -108,6 +108,7 @@ def _run(self, exp_win, ctl_win): color_state = 0 dot_next_change = 0 + responded = False dot_change_idx = 0 rt_sum = 0 n_keypresses = 0 @@ -124,11 +125,14 @@ def _run(self, exp_win, ctl_win): 'trial_number': dot_change_idx-1, 'response_time': rt }) - self.progress_bar.set_description( - f"({dot_change_idx-1}/{n_keypresses}), mean RT: {mean_rt}" - ) + if not responded: + self.progress_bar.set_description( + f"({dot_change_idx-1}/{n_keypresses}), mean RT: {mean_rt}" + ) + responded = True if self.task_timer.getTime() > dot_next_change: + responded = False color_state = (color_state+1)%2 self.fixation_dot.setColor( self.DOT_COLORS[color_state], @@ -160,6 +164,8 @@ def _run_condition(self, exp_win, ctl_win): frame_duration = 1/15. + self._cycle_start = None + yield True # wait until it's almost time to render first frame yield from utils.wait_until_yield( diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index 6df3dd3e..6dd7960c 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -91,8 +91,7 @@ def instructions(self, exp_win, ctl_win): def run(self, exp_win, ctl_win): # needs to be the 1rst callbacks - for attr in ['_exp_win_last_flip_time', '_exp_win_first_flip_time']: - exp_win.timeOnFlip(self, attr) + exp_win.timeOnFlip(self, '_exp_win_first_flip_time') self.task_timer = core.Clock() From a54335be246de6a57e344aa04f5456644d7a6839 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 21 May 2021 16:25:34 -0400 Subject: [PATCH 165/170] code adaptive design for mario, required important changes to the code-base, should be tested --- main.py | 9 ++++++- src/sessions/ses-mario.py | 50 ++++++++++++++++++++++++++++++++++++++- src/shared/cli.py | 42 ++++++++++++++++---------------- src/tasks/task_base.py | 8 ++++--- src/tasks/videogame.py | 8 ++++++- 5 files changed, 89 insertions(+), 28 deletions(-) diff --git a/main.py b/main.py index 28030f93..bada94e2 100755 --- a/main.py +++ b/main.py @@ -2,6 +2,8 @@ from subprocess import Popen import os, sys, importlib +import itertools +from collections.abc import Iterable, Iterator from src.shared import parser, config, screen from src.shared.didyoumean import suggest_session_tasks @@ -18,9 +20,14 @@ def run(parsed): suggestion = suggest_session_tasks(parsed.tasks) raise(ValueError('session tasks file cannot be found for %s. Did you mean %s ?'%(parsed.tasks, suggestion))) from src.shared import cli + if parsed.skip_n_tasks: + if isinstance(tasks, Iterator): + tasks = itertools.islice(tasks, parsed.skip_n_tasks, None) + else: + tasks[parsed.skip_n_tasks:] try: cli.main_loop( - tasks[parsed.skip_n_tasks:], + tasks, parsed.subject, parsed.session, parsed.output, diff --git a/src/sessions/ses-mario.py b/src/sessions/ses-mario.py index 0a640e26..60ab6abd 100644 --- a/src/sessions/ses-mario.py +++ b/src/sessions/ses-mario.py @@ -1,12 +1,14 @@ import os import random import retro +import json # point to a copy of the whole gym-retro with custom states and scenarii retro.data.Integrations.add_custom_path( os.path.join(os.getcwd(), "data", "videogames", "mario") ) +from psychopy import logging from ..tasks import images, videogame, memory, task_base flow_ratings = [ @@ -28,7 +30,53 @@ ("Level1-3", "scenario")] #random.shuffle(levels_scenario) # randomize order +scenario = "scenario" + +# code adaptive design for learning phase + +def get_tasks(parsed): + bids_sub = "sub-%s" % parsed.subject + savestate_path = os.path.abspath(os.path.join(parsed.output, "sourcedata", bids_sub, f"{bids_sub}_task-mario_savestate.json")) + + # check for a "savestate" + if os.path.exists(savestate_path): + with open(savestate_path) as f: + savestate = json.load(f) + else: + savestate = {"world": 1, "level":1} #TODO: determine format + + for run in range(5): + current_level = f"Level{savestate['world']}-{savestate['level']}" + task = videogame.VideoGameMultiLevel( + game_name='SuperMarioBros-Nes', + state_names=[current_level], + scenarii=[scenario], + repeat_scenario=True, + max_duration=1 * 60, # if when level completed or dead we exceed that time in secs, stop the task + name=f"task-mario_run-{run+1:02d}", + instruction="playing Super Mario Bros {state_name} \n\n Let's-a go!", + # post_level_ratings = [(q, 7) for q in flow_ratings], + ) + yield task + + if task._completed: + logging.exp(f"{current_level} successfuly completed at least once.") + savestate['level'] += 1 + if savestate['level'] > 3: + savestate['world'] +=1 + savestate['level'] = 1 + with open(savestate_path, 'w') as f: + json.dump(savestate, f) + else: + logging.exp(f"{current_level} not completed.") + + #yield task_base.Pause() + + + return tasks + +""" TASKS = sum( [ [ @@ -49,4 +97,4 @@ for run in range(5) ], [], -) +)""" diff --git a/src/shared/cli.py b/src/shared/cli.py index 2e5f31c5..b04f2ee0 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -1,7 +1,9 @@ # CLI: command line interface options and main loop import os, datetime, traceback, glob, time +from collections.abc import Iterable, Iterator from psychopy import core, visual, logging, event +import itertools visual.window.reportNDroppedFrames = 10e10 @@ -182,41 +184,37 @@ def main_loop( ) ] - all_tasks.insert( - 0, - video.VideoAudioCheckLoop( - setup_video_path[0], name="setup_soundcheck_video" - ), - ) - all_tasks.insert( - 1, - task_base.Pause( + all_tasks = itertools.chain( + [task_base.Pause( """We are completing the setup and initializing the scanner. We will start the tasks in a few minutes. Please remain still.""" - ), - ) - all_tasks.append( - task_base.Pause( + )], + all_tasks, + [task_base.Pause( """We are done for today. The scanner might run for a few seconds to acquire reference images. Please remain still. We are coming to get you out of the scanner shortly.""" - ) + )], ) else: - all_tasks.append( - task_base.Pause( + all_tasks = itertools.chain( + all_tasks, + [task_base.Pause( """We are done with the tasks for today. Thanks for your participation!""" - ) + )], ) - # list of tasks to be ran in a session - print("Here are the stimuli planned for today\n" + "_" * 50) - for task in all_tasks: - print(f"- {task.name} {getattr(task,'duration','')}" ) - print("_" * 50) + if not isinstance(all_tasks, Iterator): + + # list of tasks to be ran in a session + + print("Here are the stimuli planned for today\n" + "_" * 50) + for task in all_tasks: + print(f"- {task.name} {getattr(task,'duration','')}" ) + print("_" * 50) try: for task in all_tasks: diff --git a/src/tasks/task_base.py b/src/tasks/task_base.py index 6dd7960c..ba6c92be 100644 --- a/src/tasks/task_base.py +++ b/src/tasks/task_base.py @@ -36,6 +36,11 @@ def setup( self.use_meg = use_meg self.use_eyetracking = use_eyetracking self._events = [] + + self._exp_win_first_flip_time = None + self._exp_win_last_flip_time = None + self._ctl_win_last_flip_time = None + self._setup(exp_win) # initialize a progress bar if we know the duration of the task self.progress_bar = ( @@ -47,9 +52,6 @@ def setup( self._progress_bar_refresh_rate = config.FRAME_RATE def _setup(self, exp_win): - self._exp_win_first_flip_time = None - self._exp_win_last_flip_time = None - self._ctl_win_last_flip_time = None pass def _generate_unique_filename(self, suffix, ext="tsv"): diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index d68ba382..a2ae6537 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -87,6 +87,9 @@ def __init__( self.inttype = inttype def _setup(self, exp_win): + + super()._setup(exp_win) + self._first_frame = self.emulator.reset() first_sound_chunk = self.emulator.em.get_audio() blockSize = first_sound_chunk.shape[0] @@ -150,6 +153,7 @@ def __init__( self.max_duration = max_duration self.duration = max_duration self.post_level_ratings = post_level_ratings + self._completed = False def _instructions(self, exp_win, ctl_win): @@ -266,7 +270,7 @@ def _run_emulator(self, exp_win, ctl_win): time.sleep(.0001) self._handle_controller_presses(exp_win) keys = [k in self.pressed_keys for k in KEY_SET] - _obs, _rew, _done, _info = self.emulator.step(keys) + _obs, _rew, _done, self._game_info = self.emulator.step(keys) total_reward += _rew if _rew > 0: exp_win.logOnFlip(level=logging.EXP, msg="Reward %f" % (total_reward)) @@ -282,7 +286,9 @@ def _run_emulator(self, exp_win, ctl_win): if not level_step % config.FRAME_RATE: exp_win.logOnFlip(level=logging.EXP, msg="level step: %d" % level_step) yield True + _nextFrameT += self._frameInterval + self._completed = self._completed or self._game_info['lives'] > -1 self.game_sound.flush() self.game_sound.stop() From 42de8df6ff202fa78b6a5a4b5ce4e2ecbb30eb78 Mon Sep 17 00:00:00 2001 From: basile Date: Fri, 21 May 2021 16:27:17 -0400 Subject: [PATCH 166/170] restore previous run duration --- src/sessions/ses-mario.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sessions/ses-mario.py b/src/sessions/ses-mario.py index 60ab6abd..bbeb7ae6 100644 --- a/src/sessions/ses-mario.py +++ b/src/sessions/ses-mario.py @@ -53,7 +53,7 @@ def get_tasks(parsed): state_names=[current_level], scenarii=[scenario], repeat_scenario=True, - max_duration=1 * 60, # if when level completed or dead we exceed that time in secs, stop the task + max_duration=10 * 60, # if when level completed or dead we exceed that time in secs, stop the task name=f"task-mario_run-{run+1:02d}", instruction="playing Super Mario Bros {state_name} \n\n Let's-a go!", # post_level_ratings = [(q, 7) for q in flow_ratings], From dcaf3f29e45d6b4fe65773b73501a73f30470132 Mon Sep 17 00:00:00 2001 From: Hyruuk Date: Tue, 1 Jun 2021 14:37:05 -0400 Subject: [PATCH 167/170] uncommented flow questionnaire after each repetition --- src/sessions/ses-mario.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sessions/ses-mario.py b/src/sessions/ses-mario.py index bbeb7ae6..02b7053b 100644 --- a/src/sessions/ses-mario.py +++ b/src/sessions/ses-mario.py @@ -56,7 +56,7 @@ def get_tasks(parsed): max_duration=10 * 60, # if when level completed or dead we exceed that time in secs, stop the task name=f"task-mario_run-{run+1:02d}", instruction="playing Super Mario Bros {state_name} \n\n Let's-a go!", - # post_level_ratings = [(q, 7) for q in flow_ratings], + post_level_ratings = [(k, q, 7) for k, q in enumerate(flow_ratings)] ) yield task From 080bee79e947a2586407d2d3f8c895fd8cede29e Mon Sep 17 00:00:00 2001 From: basile Date: Wed, 2 Jun 2021 13:49:08 -0400 Subject: [PATCH 168/170] fix bk2 saving, log key releases --- src/tasks/videogame.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tasks/videogame.py b/src/tasks/videogame.py index a2ae6537..77fa398e 100644 --- a/src/tasks/videogame.py +++ b/src/tasks/videogame.py @@ -128,6 +128,7 @@ def _render_graphics_sound(self, obs, sound_block, exp_win, ctl_win): def _stop(self, exp_win, ctl_win): self.game_sound.stop() + self.emulator.stop_record() # to be sure to save the bk2 exp_win.setColor([0] * 3, colorSpace='rgb') if ctl_win: ctl_win.setColor([0] * 3, colorSpace='rgb') @@ -221,11 +222,10 @@ def _handle_controller_presses(self, exp_win): global _keyPressBuffer, _keyReleaseBuffer for k in _keyReleaseBuffer: - # print('release',k) self.pressed_keys.discard(k[0]) + logging.data(f"Keyrelease: {k[0]}") _keyReleaseBuffer.clear() for k in _keyPressBuffer: - # print('press',k) self.pressed_keys.add(k[0]) self._new_key_pressed = _keyPressBuffer[:] #copy _keyPressBuffer.clear() @@ -291,6 +291,7 @@ def _run_emulator(self, exp_win, ctl_win): self._completed = self._completed or self._game_info['lives'] > -1 self.game_sound.flush() self.game_sound.stop() + self.emulator.stop_record() def _set_key_handler(self, exp_win): # activate repeat keys @@ -469,7 +470,6 @@ def _questionnaire(self, exp_win, ctl_win): stim.draw(exp_win) yield True n_flips += 1 - # TODO save responses def _likert_scale_answer( self, exp_win, ctl_win, question, n_pts=7, extent=0.6, autoLog=False From 6b1dae01ac4cfc5ca819dc3ad693d22810e8e303 Mon Sep 17 00:00:00 2001 From: basile Date: Thu, 17 Jun 2021 08:48:54 -0400 Subject: [PATCH 169/170] reorder soundcheck and pauses. --- src/shared/cli.py | 22 +++++++++------------- src/shared/utils.py | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/shared/cli.py b/src/shared/cli.py index b04f2ee0..019df831 100644 --- a/src/shared/cli.py +++ b/src/shared/cli.py @@ -171,19 +171,6 @@ def main_loop( if show_ctl_win: gaze_drawer = eyetracking.GazeDrawer(ctl_win) if use_fmri: - setup_video_path = glob.glob( - os.path.join("data", "videos", "subject_setup_videos", "sub-%s_*" % subject) - ) - if not len(setup_video_path): - setup_video_path = [ - os.path.join( - "data", - "videos", - "subject_setup_videos", - "sub-default_setup_video.mp4", - ) - ] - all_tasks = itertools.chain( [task_base.Pause( """We are completing the setup and initializing the scanner. @@ -198,6 +185,15 @@ def main_loop( We are coming to get you out of the scanner shortly.""" )], ) + + if not skip_soundcheck: + setup_video_path = utils.get_subject_soundcheck_video(subject) + all_tasks = itertools.chain([ + video.VideoAudioCheckLoop(setup_video_path, name="setup_soundcheck_video",)], + all_tasks, + ) + + else: all_tasks = itertools.chain( all_tasks, diff --git a/src/shared/utils.py b/src/shared/utils.py index 18ae899a..170b486a 100644 --- a/src/shared/utils.py +++ b/src/shared/utils.py @@ -1,7 +1,7 @@ import psutil import time from psychopy import core - +import os, glob def check_power_plugged(): battery = psutil.sensors_battery() @@ -38,3 +38,16 @@ def wait_until_yield(clock, deadline, hogCPUperiod=0.1, keyboard_accuracy=.0005) poll_windows() current_time = clock.getTime() + +def get_subject_soundcheck_video(subject): + setup_video_path = glob.glob( + os.path.join("data", "videos", "subject_setup_videos", "sub-%s_*" % subject) + ) + if not len(setup_video_path): + return os.path.join( + "data", + "videos", + "subject_setup_videos", + "sub-default_setup_video.mp4", + ) + return setup_video_path[0] From 5c171a7ef4d218fa27ac47ac7269a4a4733f68e4 Mon Sep 17 00:00:00 2001 From: basile Date: Tue, 29 Jun 2021 11:16:21 -0400 Subject: [PATCH 170/170] fix skip_n_tasks --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index bada94e2..2b33ab7a 100755 --- a/main.py +++ b/main.py @@ -24,7 +24,7 @@ def run(parsed): if isinstance(tasks, Iterator): tasks = itertools.islice(tasks, parsed.skip_n_tasks, None) else: - tasks[parsed.skip_n_tasks:] + tasks = tasks[parsed.skip_n_tasks:] try: cli.main_loop( tasks,