psychopy 2024.1.3__py3-none-any.whl → 2024.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of psychopy might be problematic. Click here for more details.

Files changed (331) hide show
  1. psychopy/.DS_Store +0 -0
  2. psychopy/CHANGELOG.txt +206 -0
  3. psychopy/GIT_SHA +1 -0
  4. psychopy/VERSION +1 -0
  5. psychopy/__init__.py +77 -15
  6. psychopy/app/Resources/classic/plugin16.png +0 -0
  7. psychopy/app/Resources/classic/plugin16@2x.png +0 -0
  8. psychopy/app/Resources/dark/plugin16.png +0 -0
  9. psychopy/app/Resources/dark/plugin16@2x.png +0 -0
  10. psychopy/app/Resources/light/plugin16.png +0 -0
  11. psychopy/app/Resources/light/plugin16@2x.png +0 -0
  12. psychopy/app/__init__.py +76 -2
  13. psychopy/app/_psychopyApp.py +126 -101
  14. psychopy/app/builder/builder.py +14 -10
  15. psychopy/app/builder/dialogs/__init__.py +8 -8
  16. psychopy/app/builder/dialogs/dlgsConditions.py +12 -13
  17. psychopy/app/builder/dialogs/paramCtrls.py +24 -57
  18. psychopy/app/builder/localizedStrings.py +11 -9
  19. psychopy/app/builder/validators.py +2 -2
  20. psychopy/app/coder/codeEditorBase.py +8 -8
  21. psychopy/app/coder/coder.py +4 -4
  22. psychopy/app/connections/sendusage.py +2 -2
  23. psychopy/app/connections/updates.py +9 -9
  24. psychopy/app/dialogs.py +34 -2
  25. psychopy/app/idle.py +31 -0
  26. psychopy/app/jobs.py +21 -3
  27. psychopy/app/linuxconfig/__init__.py +9 -0
  28. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  29. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +4602 -2540
  30. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  31. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +56 -54
  32. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +53 -43
  33. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  34. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +56 -54
  35. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  36. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1258 -1176
  37. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +9415 -5
  38. psychopy/app/pavlovia_ui/_base.py +33 -3
  39. psychopy/app/pavlovia_ui/search.py +0 -1
  40. psychopy/app/plugin_manager/dialog.py +104 -51
  41. psychopy/app/plugin_manager/packages.py +5 -0
  42. psychopy/app/plugin_manager/plugins.py +152 -67
  43. psychopy/app/preferencesDlg.py +8 -8
  44. psychopy/app/psychopyApp.py +11 -5
  45. psychopy/app/ribbon.py +124 -14
  46. psychopy/app/runner/runner.py +6 -1
  47. psychopy/app/stdout/stdOutRich.py +27 -11
  48. psychopy/app/themes/icons.py +52 -2
  49. psychopy/assets/__init__.py +0 -0
  50. psychopy/assets/click.png +0 -0
  51. psychopy/assets/clicknext.png +0 -0
  52. psychopy/assets/next.png +0 -0
  53. psychopy/assets/psychopy.ico +0 -0
  54. psychopy/assets/psychopy.png +0 -0
  55. psychopy/assets/templates/__init__.py +0 -0
  56. psychopy/assets/touch.png +0 -0
  57. psychopy/assets/touchnext.png +0 -0
  58. psychopy/assets/window.ico +0 -0
  59. psychopy/changes/2023.1.0.md +9 -0
  60. psychopy/changes/2024.1.0.md +16 -0
  61. psychopy/changes/__init__.py +0 -0
  62. psychopy/clock.py +2 -2
  63. psychopy/colors.py +2 -1
  64. psychopy/compatibility.py +53 -1
  65. psychopy/contrib/.DS_Store +0 -0
  66. psychopy/contrib/configobj/__init__.py +10 -8
  67. psychopy/data/__init__.py +3 -2
  68. psychopy/data/base.py +5 -5
  69. psychopy/data/experiment.py +130 -4
  70. psychopy/data/routine.py +56 -0
  71. psychopy/data/staircase.py +2 -2
  72. psychopy/data/trial.py +559 -97
  73. psychopy/data/utils.py +56 -21
  74. psychopy/demos/.DS_Store +0 -0
  75. psychopy/demos/builder/.DS_Store +0 -0
  76. psychopy/demos/builder/Design Templates/.DS_Store +0 -0
  77. psychopy/demos/builder/Experiments/.DS_Store +0 -0
  78. psychopy/demos/builder/Feature Demos/.DS_Store +0 -0
  79. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +375 -0
  80. psychopy/demos/builder/Feature Demos/buttonBox/readme.md +5 -0
  81. psychopy/demos/builder/Feature Demos/pilotMode/pilotMode.psyexp +433 -0
  82. psychopy/demos/builder/Feature Demos/pilotMode/readme.md +7 -0
  83. psychopy/demos/builder/Feature Demos/progress/progressBar.psyexp +4 -4
  84. psychopy/demos/builder/Hardware/.DS_Store +0 -0
  85. psychopy/demos/builder/Helper Tools/.DS_Store +0 -0
  86. psychopy/demos/coder/.DS_Store +0 -0
  87. psychopy/demos/coder/hardware/testSoundLatency.py +2 -2
  88. psychopy/demos/coder/iohub/.DS_Store +0 -0
  89. psychopy/demos/coder/misc/hdf5_2_csv +33 -0
  90. psychopy/event.py +30 -29
  91. psychopy/experiment/.DS_Store +0 -0
  92. psychopy/experiment/_experiment.py +6 -6
  93. psychopy/experiment/components/.DS_Store +0 -0
  94. psychopy/experiment/components/__init__.py +6 -3
  95. psychopy/experiment/components/_base.py +286 -131
  96. psychopy/experiment/components/aperture/.DS_Store +0 -0
  97. psychopy/experiment/components/brush/.DS_Store +0 -0
  98. psychopy/experiment/components/button/.DS_Store +0 -0
  99. psychopy/experiment/components/button/__init__.py +5 -1
  100. psychopy/experiment/components/buttonBox/.DS_Store +0 -0
  101. psychopy/experiment/components/buttonBox/__init__.py +21 -12
  102. psychopy/experiment/components/camera/.DS_Store +0 -0
  103. psychopy/experiment/components/code/.DS_Store +0 -0
  104. psychopy/experiment/components/dots/.DS_Store +0 -0
  105. psychopy/experiment/components/eyetracker_record/.DS_Store +0 -0
  106. psychopy/experiment/components/eyetracker_record/__init__.py +92 -30
  107. psychopy/experiment/components/form/.DS_Store +0 -0
  108. psychopy/experiment/components/form/__init__.py +6 -2
  109. psychopy/experiment/components/grating/.DS_Store +0 -0
  110. psychopy/experiment/components/grating/__init__.py +14 -3
  111. psychopy/experiment/components/image/.DS_Store +0 -0
  112. psychopy/experiment/components/image/__init__.py +14 -3
  113. psychopy/experiment/components/joyButtons/.DS_Store +0 -0
  114. psychopy/experiment/components/joystick/.DS_Store +0 -0
  115. psychopy/experiment/components/keyboard/.DS_Store +0 -0
  116. psychopy/experiment/components/keyboard/__init__.py +22 -10
  117. psychopy/experiment/components/microphone/.DS_Store +0 -0
  118. psychopy/experiment/components/microphone/__init__.py +59 -39
  119. psychopy/experiment/components/mouse/.DS_Store +0 -0
  120. psychopy/experiment/components/mouse/__init__.py +44 -29
  121. psychopy/experiment/components/movie/.DS_Store +0 -0
  122. psychopy/experiment/components/movie/__init__.py +1 -1
  123. psychopy/experiment/components/panorama/.DS_Store +0 -0
  124. psychopy/experiment/components/parallelOut/.DS_Store +0 -0
  125. psychopy/experiment/components/patch/.DS_Store +0 -0
  126. psychopy/experiment/components/polygon/.DS_Store +0 -0
  127. psychopy/experiment/components/polygon/__init__.py +26 -6
  128. psychopy/experiment/components/progress/.DS_Store +0 -0
  129. psychopy/experiment/components/progress/__init__.py +1 -1
  130. psychopy/experiment/components/ratingScale/.DS_Store +0 -0
  131. psychopy/experiment/components/resourceManager/.DS_Store +0 -0
  132. psychopy/experiment/components/roi/.DS_Store +0 -0
  133. psychopy/experiment/components/roi/__init__.py +5 -0
  134. psychopy/experiment/components/routineSettings/.DS_Store +0 -0
  135. psychopy/experiment/components/routineSettings/__init__.py +57 -10
  136. psychopy/experiment/components/serialOut/.DS_Store +0 -0
  137. psychopy/experiment/components/settings/.DS_Store +0 -0
  138. psychopy/experiment/components/settings/__init__.py +117 -42
  139. psychopy/experiment/components/slider/.DS_Store +0 -0
  140. psychopy/experiment/components/sound/.DS_Store +0 -0
  141. psychopy/experiment/components/sound/__init__.py +54 -19
  142. psychopy/experiment/components/static/.DS_Store +0 -0
  143. psychopy/experiment/components/static/__init__.py +1 -1
  144. psychopy/experiment/components/text/.DS_Store +0 -0
  145. psychopy/experiment/components/text/__init__.py +28 -3
  146. psychopy/experiment/components/textbox/.DS_Store +0 -0
  147. psychopy/experiment/components/textbox/__init__.py +12 -2
  148. psychopy/experiment/components/unknown/.DS_Store +0 -0
  149. psychopy/experiment/components/unknown/__init__.py +1 -2
  150. psychopy/experiment/components/unknownPlugin/.DS_Store +0 -0
  151. psychopy/experiment/components/unknownPlugin/__init__.py +2 -2
  152. psychopy/experiment/components/variable/.DS_Store +0 -0
  153. psychopy/experiment/flow.py +11 -4
  154. psychopy/experiment/loops.py +85 -37
  155. psychopy/experiment/params.py +74 -32
  156. psychopy/experiment/py2js_transpiler.py +8 -1
  157. psychopy/experiment/routines/.DS_Store +0 -0
  158. psychopy/experiment/routines/_base.py +102 -22
  159. psychopy/experiment/routines/counterbalance/.DS_Store +0 -0
  160. psychopy/experiment/routines/counterbalance/__init__.py +5 -1
  161. psychopy/experiment/routines/eyetracker_calibrate/.DS_Store +0 -0
  162. psychopy/experiment/routines/eyetracker_validate/.DS_Store +0 -0
  163. psychopy/experiment/routines/pavlovia_survey/.DS_Store +0 -0
  164. psychopy/experiment/routines/photodiodeValidator/.DS_Store +0 -0
  165. psychopy/experiment/routines/photodiodeValidator/__init__.py +7 -6
  166. psychopy/experiment/routines/unknown/.DS_Store +0 -0
  167. psychopy/gui/wxgui.py +4 -4
  168. psychopy/hardware/.DS_Store +0 -0
  169. psychopy/hardware/__init__.py +1 -1
  170. psychopy/hardware/base.py +12 -0
  171. psychopy/hardware/camera/__init__.py +1 -15
  172. psychopy/hardware/cedrus.py +10 -11
  173. psychopy/hardware/crs/colorcal.py +13 -22
  174. psychopy/hardware/crs/optical.py +10 -20
  175. psychopy/hardware/emulator.py +17 -14
  176. psychopy/hardware/eyetracker.py +42 -118
  177. psychopy/hardware/gammasci.py +4 -15
  178. psychopy/hardware/keyboard.py +102 -11
  179. psychopy/hardware/listener.py +3 -0
  180. psychopy/hardware/microphone.py +148 -18
  181. psychopy/hardware/minolta.py +8 -15
  182. psychopy/hardware/photodiode.py +191 -16
  183. psychopy/hardware/photometer/__init__.py +11 -19
  184. psychopy/hardware/pr.py +8 -15
  185. psychopy/hardware/speaker.py +39 -4
  186. psychopy/info.py +0 -71
  187. psychopy/iohub/.DS_Store +0 -0
  188. psychopy/iohub/__init__.py +1 -1
  189. psychopy/iohub/client/__init__.py +30 -20
  190. psychopy/iohub/client/keyboard.py +24 -24
  191. psychopy/iohub/datastore/__init__.py +2 -2
  192. psychopy/iohub/datastore/util.py +2 -2
  193. psychopy/iohub/default_config.yaml +1 -1
  194. psychopy/iohub/devices/.DS_Store +0 -0
  195. psychopy/iohub/devices/__init__.py +112 -25
  196. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  197. psychopy/iohub/devices/experiment/default_experiment.yaml +12 -1
  198. psychopy/iohub/devices/experiment/supported_config_settings.yaml +5 -1
  199. psychopy/iohub/devices/eyetracker/.DS_Store +0 -0
  200. psychopy/iohub/devices/eyetracker/__init__.py +46 -0
  201. psychopy/iohub/devices/eyetracker/calibration/procedure.py +2 -2
  202. psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +14 -2
  203. psychopy/iohub/devices/eyetracker/hw/mouse/eyetracker.py +3 -4
  204. psychopy/iohub/server.py +2 -2
  205. psychopy/iohub/start_iohub_process.py +3 -0
  206. psychopy/iohub/util/__init__.py +62 -70
  207. psychopy/layout.py +5 -5
  208. psychopy/logging.py +8 -1
  209. psychopy/microphone.py +10 -37
  210. psychopy/platform_specific/__init__.py +0 -2
  211. psychopy/platform_specific/darwin.py +1 -3
  212. psychopy/platform_specific/linux.py +31 -33
  213. psychopy/platform_specific/win32.py +38 -13
  214. psychopy/plugins/__init__.py +148 -116
  215. psychopy/plugins/util.py +39 -0
  216. psychopy/preferences/Darwin.spec +4 -2
  217. psychopy/preferences/FreeBSD.spec +4 -2
  218. psychopy/preferences/Linux.spec +4 -2
  219. psychopy/preferences/Windows.spec +4 -2
  220. psychopy/preferences/baseNoArch.spec +4 -2
  221. psychopy/preferences/preferences.py +47 -24
  222. psychopy/projects/pavlovia.py +47 -4
  223. psychopy/scripts/psyexpCompile.py +0 -4
  224. psychopy/session.py +153 -21
  225. psychopy/sound/__init__.py +31 -21
  226. psychopy/sound/_base.py +20 -3
  227. psychopy/sound/audioclip.py +320 -33
  228. psychopy/sound/backend_ptb.py +47 -58
  229. psychopy/sound/backend_pygame.py +1 -1
  230. psychopy/sound/backend_pysound.py +6 -15
  231. psychopy/sound/transcribe.py +53 -0
  232. psychopy/tests/.DS_Store +0 -0
  233. psychopy/tests/data/.DS_Store +0 -0
  234. psychopy/tests/data/TestUnknownPluginComponent_load_resave.psyexp +135 -0
  235. psychopy/tests/data/Test_textbox/test_ori_0_bottom right.png +0 -0
  236. psychopy/tests/data/Test_textbox/test_ori_0_center.png +0 -0
  237. psychopy/tests/data/Test_textbox/test_ori_0_top left.png +0 -0
  238. psychopy/tests/data/Test_textbox/test_ori_120_bottom right.png +0 -0
  239. psychopy/tests/data/Test_textbox/test_ori_120_center.png +0 -0
  240. psychopy/tests/data/Test_textbox/test_ori_120_top left.png +0 -0
  241. psychopy/tests/data/Test_textbox/test_ori_180_bottom right.png +0 -0
  242. psychopy/tests/data/Test_textbox/test_ori_180_center.png +0 -0
  243. psychopy/tests/data/Test_textbox/test_ori_180_top left.png +0 -0
  244. psychopy/tests/data/Test_textbox/test_ori_240_bottom right.png +0 -0
  245. psychopy/tests/data/Test_textbox/test_ori_240_center.png +0 -0
  246. psychopy/tests/data/Test_textbox/test_ori_240_top left.png +0 -0
  247. psychopy/tests/data/correctScript/.DS_Store +0 -0
  248. psychopy/tests/data/test_components/testClearKeyboard/testClearKeyboard.psyexp +200 -0
  249. psychopy/tests/data/test_session/.DS_Store +0 -0
  250. psychopy/tests/data/test_session/root/testFutureTrials/testFutureTrials.psyexp +155 -0
  251. psychopy/tests/data/test_session/root/testTrialNav/trialNav.psyexp +158 -0
  252. psychopy/tests/test_app/.DS_Store +0 -0
  253. psychopy/tests/test_app/conftest.py +2 -2
  254. psychopy/tests/test_app/test_speed.py +4 -1
  255. psychopy/tests/test_data/test_TrialHandler2.py +146 -1
  256. psychopy/tests/test_experiment/.DS_Store +0 -0
  257. psychopy/tests/test_experiment/needs_wx/genComponsTemplate.py +3 -3
  258. psychopy/tests/test_experiment/needs_wx/test_components.py +2 -2
  259. psychopy/tests/test_experiment/test_components/test_KeyboardComponent.py +28 -0
  260. psychopy/tests/test_experiment/test_components/test_UnknownPluginComponent.py +27 -0
  261. psychopy/tests/test_experiment/test_components/test_base_components.py +58 -0
  262. psychopy/tests/test_experiment/test_py2js.py +1 -1
  263. psychopy/tests/test_hardware/test_keyboard.py +184 -16
  264. psychopy/tests/test_hardware/test_ports.py +1 -11
  265. psychopy/tests/test_liaison/test_Liaison.py +47 -0
  266. psychopy/tests/test_misc/test_core.py +5 -0
  267. psychopy/tests/test_session/test_Session.py +5 -1
  268. psychopy/tests/test_tools/test_versionchooser.py +39 -8
  269. psychopy/tests/test_visual/test_all_stimuli.py +0 -97
  270. psychopy/tests/test_visual/test_image.py +6 -5
  271. psychopy/tests/test_visual/test_textbox.py +36 -0
  272. psychopy/tests/utils.py +4 -0
  273. psychopy/tools/filetools.py +1 -1
  274. psychopy/tools/pkgtools.py +160 -137
  275. psychopy/tools/versionchooser.py +10 -10
  276. psychopy/tools/wizard.py +3 -3
  277. psychopy/visual/.DS_Store +0 -0
  278. psychopy/visual/backends/pygletbackend.py +24 -13
  279. psychopy/visual/basevisual.py +5 -11
  280. psychopy/visual/button.py +2 -14
  281. psychopy/visual/helpers.py +5 -5
  282. psychopy/visual/line.py +1 -2
  283. psychopy/visual/movie2.py +7 -816
  284. psychopy/visual/movie3.py +7 -589
  285. psychopy/visual/movies/__init__.py +8 -11
  286. psychopy/visual/movies/frame.py +5 -2
  287. psychopy/visual/movies/players/ffpyplayer_player.py +5 -2
  288. psychopy/visual/noise.py +8 -7
  289. psychopy/visual/patch.py +7 -16
  290. psychopy/visual/progress.py +1 -1
  291. psychopy/visual/radial.py +9 -7
  292. psychopy/visual/ratingscale.py +8 -1415
  293. psychopy/visual/secondorder.py +10 -9
  294. psychopy/visual/shape.py +7 -2
  295. psychopy/visual/text.py +1 -1
  296. psychopy/visual/textbox2/textbox2.py +28 -5
  297. psychopy/web.py +5 -2
  298. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/METADATA +8 -13
  299. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/RECORD +313 -219
  300. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/WHEEL +1 -1
  301. psychopy/app/Resources/click.png +0 -0
  302. psychopy/app/Resources/next.png +0 -0
  303. psychopy/experiment/components/patch/__init__.py +0 -121
  304. psychopy/experiment/components/patch/classic/patch.png +0 -0
  305. psychopy/experiment/components/patch/dark/patch.png +0 -0
  306. psychopy/experiment/components/patch/dark/patch@2x.png +0 -0
  307. psychopy/experiment/components/patch/light/patch.png +0 -0
  308. psychopy/experiment/components/patch/light/patch@2x.png +0 -0
  309. psychopy/experiment/components/ratingScale/__init__.py +0 -337
  310. psychopy/experiment/components/ratingScale/classic/ratingscale.png +0 -0
  311. psychopy/experiment/components/ratingScale/classic/ratingscale@2x.png +0 -0
  312. psychopy/experiment/components/ratingScale/dark/ratingScale@2x.png +0 -0
  313. psychopy/experiment/components/ratingScale/dark/ratingscale.png +0 -0
  314. psychopy/experiment/components/ratingScale/light/ratingScale@2x.png +0 -0
  315. psychopy/experiment/components/ratingScale/light/ratingscale.png +0 -0
  316. psychopy/platform_specific/posix.py +0 -16
  317. psychopy/tests/test_sound/test_microphone.py +0 -217
  318. psychopy/tests/test_visual/test_ratingScale.py +0 -299
  319. /psychopy/{app/Resources → assets}/Psychopy Window Favicon@16w.png +0 -0
  320. /psychopy/{app/Resources → assets}/Psychopy Window Favicon@32w.png +0 -0
  321. /psychopy/{app/Resources → assets}/USB-C.png +0 -0
  322. /psychopy/{app/Resources → assets}/USB.png +0 -0
  323. /psychopy/{app/Resources → assets}/creditCard.png +0 -0
  324. /psychopy/{app/Resources → assets}/default.mp3 +0 -0
  325. /psychopy/{app/Resources → assets}/default.mp4 +0 -0
  326. /psychopy/{app/Resources → assets}/default.png +0 -0
  327. /psychopy/{app/Resources → assets/templates}/instruct1.png +0 -0
  328. /psychopy/{app/Resources → assets/templates}/instruct2.png +0 -0
  329. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/entry_points.txt +0 -0
  330. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/licenses/AUTHORS.md +0 -0
  331. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/licenses/LICENSE +0 -0
psychopy/data/trial.py CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
+ import json
4
5
  import os
5
6
  import sys
6
7
  import copy
@@ -419,10 +420,12 @@ class TrialHandler(_BaseTrialHandler):
419
420
  for thisDataOut in dataOut:
420
421
  # make a string version of the data and then format it
421
422
  tmpData = dataAnal[thisDataOut][stimN]
423
+ replaceNone = False
422
424
  if hasattr(tmpData, 'tolist'): # is a numpy array
423
425
  strVersion = str(tmpData.tolist())
424
426
  # for numeric data replace None with a blank cell
425
- if tmpData.dtype.kind not in ['SaUV']:
427
+ if tmpData.dtype.kind not in 'SaUV':
428
+ replaceNone = True
426
429
  strVersion = strVersion.replace('None', '')
427
430
  elif tmpData in [None, 'None']:
428
431
  strVersion = ''
@@ -434,18 +437,25 @@ class TrialHandler(_BaseTrialHandler):
434
437
  strVersion = "--"
435
438
  # handle list of values (e.g. rt_raw )
436
439
  if (len(strVersion) and
437
- strVersion[0] in '[(' and
438
- strVersion[-1] in '])'):
440
+ strVersion[0] in '[(' and
441
+ strVersion[-1] in '])'):
439
442
  strVersion = strVersion[1:-1] # skip first and last chars
440
443
  # handle lists of lists (e.g. raw of multiple key presses)
441
444
  if (len(strVersion) and
442
- strVersion[0] in '[(' and
443
- strVersion[-1] in '])'):
445
+ strVersion[0] in '[(' and
446
+ strVersion[-1] in '])'):
447
+ if replaceNone:
448
+ # Add None back so that the str is valid for eval
449
+ strVersion = strVersion.replace('[,', '[None,')
450
+ strVersion = strVersion.replace(', ,', ', None,')
444
451
  tup = eval(strVersion) # convert back to a tuple
445
452
  for entry in tup:
446
453
  # contents of each entry is a list or tuple so keep in
447
454
  # quotes to avoid probs with delim
448
- thisLine.append(str(entry))
455
+ currentEntry = str(entry)
456
+ if replaceNone:
457
+ currentEntry = currentEntry.replace('None', '')
458
+ thisLine.append(currentEntry)
449
459
  else:
450
460
  thisLine.extend(strVersion.split(','))
451
461
 
@@ -612,7 +622,7 @@ class TrialHandler(_BaseTrialHandler):
612
622
  Defaults to `utf-8-sig`.
613
623
 
614
624
  """
615
- if self.thisTrialN < 1 and self.thisRepN < 1:
625
+ if self.thisTrialN < 0 and self.thisRepN < 0:
616
626
  # if both are < 1 we haven't started
617
627
  logging.info('TrialHandler.saveAsWideText called but no '
618
628
  'trials completed. Nothing saved')
@@ -736,6 +746,74 @@ class TrialHandler(_BaseTrialHandler):
736
746
  self.getExp().addData(thisType, value)
737
747
 
738
748
 
749
+ class Trial(dict):
750
+ def __init__(self, parent, thisN, thisRepN, thisTrialN, thisIndex, data=None):
751
+ dict.__init__(self)
752
+ # TrialHandler containing this trial
753
+ self.parent = parent
754
+ # state information about this trial
755
+ self.thisN = thisN
756
+ self.thisRepN = thisRepN
757
+ self.thisTrialN = thisTrialN
758
+ self.thisIndex = thisIndex
759
+ # data for this trial
760
+ if data is None:
761
+ data = {}
762
+ else:
763
+ data = data.copy()
764
+ self.data = data
765
+
766
+ def __repr__(self):
767
+ return (
768
+ f"<Trial {self.thisN} ({self.thisTrialN} in rep {self.thisRepN}) "
769
+ f"data={ {key: val for key,val in self.items()} }>"
770
+ )
771
+
772
+ @property
773
+ def data(self):
774
+ # return self when getting data (so it's modified by modifying data)
775
+ return self
776
+
777
+ @data.setter
778
+ def data(self, value: dict):
779
+ # when setting data, clear self...
780
+ self.clear()
781
+ # ... and set each value from the given dict
782
+ for key, val in value.items():
783
+ self[key] = val
784
+
785
+ def getDict(self):
786
+ """
787
+ Get this Trial as a dict.
788
+
789
+ Returns
790
+ -------
791
+ dict
792
+ Dict containing information for this Trial.
793
+ """
794
+ return {
795
+ 'type': "trial_data",
796
+ 'thisN': self.thisN,
797
+ 'thisRepN': self.thisRepN,
798
+ 'thisTrialN': self.thisTrialN,
799
+ 'thisIndex': self.thisIndex,
800
+ 'data': {key: val for key, val in self.items()},
801
+ }
802
+
803
+ def getJSON(self):
804
+ """
805
+ Serialize this Trial to a JSON format.
806
+
807
+ Returns
808
+ -------
809
+ str
810
+ The results of Trial.getDict expressed as a JSON string
811
+ """
812
+ return json.dumps(
813
+ self.getDict()
814
+ )
815
+
816
+
739
817
  class TrialHandler2(_BaseTrialHandler):
740
818
  """Class to handle trial sequencing and data storage.
741
819
 
@@ -860,19 +938,15 @@ class TrialHandler2(_BaseTrialHandler):
860
938
  self.remainingIndices = []
861
939
  self.prevIndices = []
862
940
  self.method = method
863
- self.thisRepN = 0 # records which repetition or pass we are on
864
- self.thisTrialN = -1 # records trial number within this repetition
865
- self.thisN = -1
866
- self.thisIndex = None # index of current trial in the conditions list
867
- self.thisTrial = {}
868
- self.finished = False
869
941
  self.extraInfo = extraInfo
870
942
  self.seed = seed
871
943
  self._rng = np.random.default_rng(seed=seed)
872
944
  self._trialAborted = False
873
945
 
874
946
  # store a list of dicts, convert to pandas DataFrame on access
875
- self._data = []
947
+ self.elapsedTrials = []
948
+ self.upcomingTrials = None
949
+ self.thisTrial = None
876
950
 
877
951
  self.originPath, self.origin = self.getOriginPathAndFile(originPath)
878
952
  self._exp = None # the experiment handler that owns me!
@@ -941,7 +1015,7 @@ class TrialHandler2(_BaseTrialHandler):
941
1015
  Note that data are stored internally as a list of dictionaries,
942
1016
  one per trial. These are converted to a DataFrame on access.
943
1017
  """
944
- return pd.DataFrame(self._data)
1018
+ return pd.DataFrame(self.elapsedTrials)
945
1019
 
946
1020
  def __next__(self):
947
1021
  """Advances to next trial and returns it.
@@ -963,51 +1037,22 @@ class TrialHandler2(_BaseTrialHandler):
963
1037
  break # break out of the forever loop
964
1038
  # do stuff here for the trial
965
1039
  """
966
- # update pointer for next trials
967
- self.thisTrialN += 1 # number of trial this pass
968
- self.thisN += 1 # number of trial in total
969
- self.nRemaining -= 1
970
- if self.thisIndex is not None:
971
- self.prevIndices.append(self.thisIndex)
972
-
973
- # thisRepN has exceeded nReps
974
- if self.remainingIndices == []:
975
- # we've just started, or just starting a new repeat
976
- sequence = list(range(len(self.trialList)))
977
- if (self.method == 'fullRandom' and
978
- self.thisN < (self.nReps * len(self.trialList))):
979
- # we've only just started on a fullRandom sequence
980
- sequence *= self.nReps
981
- # NB permutation *returns* a shuffled array
982
- self.remainingIndices = list(self._rng.permutation(sequence))
983
- elif (self.method in ('sequential', 'random') and
984
- self.thisRepN < self.nReps):
985
- # start a new repetition
986
- self.thisTrialN = 0
987
- self.thisRepN += 1
988
- if self.method == 'random':
989
- self._rng.shuffle(sequence) # shuffle (is in-place)
990
- self.remainingIndices = list(sequence)
991
- else:
992
- # we've finished
993
- self.finished = True
994
- self._terminate() # raises Stop (code won't go beyond here)
995
-
996
- # fetch the trial info
997
- if len(self.trialList) == 0:
998
- self.thisIndex = 0
999
- self.thisTrial = {}
1000
- else:
1001
- self.thisIndex = self.remainingIndices.pop(0)
1002
- # if None then use empty dict
1003
- thisTrial = self.trialList[self.thisIndex] or {}
1004
- self.thisTrial = copy.copy(thisTrial)
1005
- # for fullRandom check how many times this has come up before
1006
- if self.method == 'fullRandom':
1007
- self.thisRepN = self.prevIndices.count(self.thisIndex)
1040
+ # mark previous trial as elapsed
1041
+ if self.thisTrial is not None:
1042
+ self.elapsedTrials.append(self.thisTrial)
1043
+ # if upcoming is None, recaculate
1044
+ if self.upcomingTrials is None:
1045
+ self.calculateUpcoming()
1046
+ # if upcoming is empty, finish
1047
+ if not self.upcomingTrials:
1048
+ self.finished = True
1049
+ self.thisTrial = None
1050
+ self._terminate()
1051
+ raise StopIteration
1052
+ # get first upcoming trial
1053
+ self.thisTrial = self.upcomingTrials.pop(0)
1008
1054
 
1009
1055
  # update data structure with new info
1010
- self._data.append(self.thisTrial) # update the data list of dicts
1011
1056
  self.addData('thisN', self.thisN)
1012
1057
  self.addData('thisTrialN', self.thisTrialN)
1013
1058
  self.addData('thisRepN', self.thisRepN)
@@ -1016,21 +1061,114 @@ class TrialHandler2(_BaseTrialHandler):
1016
1061
  vals = (self.thisRepN, self.thisTrialN, self.thisTrial)
1017
1062
  logging.exp(msg % vals, obj=self.thisTrial)
1018
1063
 
1019
- # if the trial was aborted, reset the flag
1020
- self._trialAborted = False
1064
+ # update experiment handler entry
1065
+ exp = self.getExp()
1066
+ if exp is not None:
1067
+ exp.updateEntryFromLoop(self)
1021
1068
 
1022
1069
  return self.thisTrial
1023
1070
 
1024
1071
  next = __next__ # allows user to call without a loop `val = trials.next()`
1025
1072
 
1026
1073
  @property
1027
- def trialAborted(self):
1028
- """`True` if the trial has been aborted an should end.
1029
-
1030
- This flag is reset to `False` on the next call to `next()`.
1031
-
1074
+ def thisN(self):
1075
+ if self.thisTrial is None:
1076
+ if len(self.elapsedTrials):
1077
+ return self.elapsedTrials[-1].thisN
1078
+ else:
1079
+ return -1
1080
+ return self.thisTrial.thisN
1081
+
1082
+ @property
1083
+ def thisTrialN(self):
1084
+ if self.thisTrial is None:
1085
+ if len(self.elapsedTrials):
1086
+ return self.elapsedTrials[-1].thisTrialN
1087
+ else:
1088
+ return -1
1089
+ return self.thisTrial.thisTrialN
1090
+
1091
+ @property
1092
+ def thisRepN(self):
1093
+ if self.thisTrial is None:
1094
+ if len(self.elapsedTrials):
1095
+ return self.elapsedTrials[-1].thisRepN
1096
+ else:
1097
+ return -1
1098
+ return self.thisTrial.thisRepN
1099
+
1100
+ def calculateUpcoming(self, fromIndex=-1):
1101
+ """Rebuild the sequence of trial/state info as if running the trials
1102
+
1103
+ Args:
1104
+ fromIndex (int, optional): the point in the sequnce from where to rebuild. Defaults to -1.
1032
1105
  """
1033
- return self._trialAborted
1106
+ # clear upcoming
1107
+ self.upcomingTrials = []
1108
+ # start off at 0 trial
1109
+ thisTrialN = 0
1110
+ thisN = 0
1111
+ thisRepN = 0
1112
+ # empty array to store indices once taken
1113
+ prevIndices = []
1114
+ # empty array to store remaining indices
1115
+ remainingIndices = []
1116
+ # iterate a while loop until we run out of trials
1117
+ while thisN < (self.nReps * len(self.trialList)):
1118
+ if not remainingIndices:
1119
+ # we've just started, or just starting a new repeat
1120
+ sequence = list(range(len(self.trialList)))
1121
+ if (self.method == 'fullRandom' and
1122
+ thisN < (self.nReps * len(self.trialList))):
1123
+ # we've only just started on a fullRandom sequence
1124
+ sequence *= self.nReps
1125
+ # NB permutation *returns* a shuffled array
1126
+ remainingIndices = list(self._rng.permutation(sequence))
1127
+ elif (self.method in ('sequential', 'random') and
1128
+ thisRepN < self.nReps):
1129
+ thisTrialN = 0
1130
+ thisRepN += 1
1131
+ if self.method == 'random':
1132
+ self._rng.shuffle(sequence) # shuffle (is in-place)
1133
+ remainingIndices = list(sequence)
1134
+ else:
1135
+ # we've finished
1136
+ break
1137
+
1138
+ if thisN < len(self.elapsedTrials):
1139
+ # trial has already happened - get its value
1140
+ thisTrial = self.elapsedTrials[thisN]
1141
+ # remove from remaining
1142
+ remainingIndices.pop(remainingIndices.index(thisTrial.thisIndex))
1143
+ else:
1144
+ # fetch the trial info
1145
+ if len(self.trialList) == 0:
1146
+ thisIndex = 0
1147
+ thisTrial = {}
1148
+ else:
1149
+ thisIndex = remainingIndices.pop(0)
1150
+ # if None then use empty dict
1151
+ thisTrial = self.trialList[thisIndex] or {}
1152
+ thisTrial = copy.copy(thisTrial)
1153
+ # make Trial object
1154
+ thisTrial = Trial(
1155
+ self,
1156
+ thisN=thisN,
1157
+ thisRepN=thisRepN,
1158
+ thisTrialN=thisTrialN,
1159
+ thisIndex=thisIndex,
1160
+ data=thisTrial
1161
+ )
1162
+ # otherwise, append trial
1163
+ self.upcomingTrials.append(thisTrial)
1164
+ # for fullRandom check how many times this has come up before
1165
+ if self.method == 'fullRandom':
1166
+ thisTrial.thisRepN = prevIndices.count(thisTrial.thisIndex)
1167
+ # update prev indices
1168
+ prevIndices.append(thisTrial.thisIndex)
1169
+ # update pointer for next trials
1170
+ thisTrialN += 1 # number of trial this pass
1171
+ thisN += 1 # number of trial in total
1034
1172
 
1035
1173
  def abortCurrentTrial(self, action='random'):
1036
1174
  """Abort the current trial.
@@ -1051,39 +1189,169 @@ class TrialHandler2(_BaseTrialHandler):
1051
1189
  not used.
1052
1190
 
1053
1191
  """
1054
- # check if value for parameter `action` is valid
1055
- if not isinstance(action, str): # type checks for params
1056
- raise TypeError(
1057
- "Parameter `action` specified incorrect type, must be `str`.")
1058
-
1059
- if action not in ('random', 'append'):
1060
- raise ValueError(
1061
- "Value for parameter `action` must be either 'random' or "
1062
- "'append'.")
1063
-
1064
- # use the appropriate action for the current sampling method
1065
- if action == 'random': # insert trial into random index
1066
- # use numpy RNG to sample a new index
1067
- newIndex = np.random.randint(0, len(self.remainingIndices))
1068
- self.remainingIndices.insert(newIndex, self.thisIndex)
1069
- elif action == 'append': # insert at end of trial block
1070
- self.remainingIndices.append(self.thisIndex)
1071
-
1072
- # flag that the trial has been aborted, user can take approriate action
1073
- self._trialAborted = True
1192
+ # clear this trial so it's not appended to elapsed
1193
+ self.thisTrial = None
1194
+ # clear upcoming trials so they're recalculated on next iteration
1195
+ self.upcomingTrials = None
1196
+
1197
+ @property
1198
+ def finished(self):
1199
+ """
1200
+ Whether this loop has finished or not. Will be True if there are no upcoming trials and
1201
+ False if there are any. Set `.finished = True` to skip all remaining trials (equivalent to
1202
+ calling `.skipTrials()` with a value larger than the number of trials remaining)
1203
+
1204
+ Returns
1205
+ -------
1206
+ bool
1207
+ True if there are no upcoming trials, False otherwise.
1208
+ """
1209
+ return not bool(self.upcomingTrials)
1210
+
1211
+ @finished.setter
1212
+ def finished(self, value):
1213
+ # when setting finished to True, skip all remaining trials
1214
+ if value:
1215
+ self.upcomingTrials = []
1216
+ else:
1217
+ self.calculateUpcoming()
1218
+
1219
+ def skipTrials(self, n=1):
1220
+ """
1221
+ Skip ahead n trials - the trials inbetween will be marked as "skipped". If you try to
1222
+ skip past the last trial, will log a warning and skip *to* the last trial.
1223
+
1224
+ Parameters
1225
+ ----------
1226
+ n : int
1227
+ Number of trials to skip ahead
1228
+ """
1229
+ # if skipping past last trial, print warning and skip to last trial
1230
+ if n > len(self.upcomingTrials):
1231
+ logging.warn(
1232
+ f"Requested skip of {n} trials when only {len(self.elapsedTrials)} trials are upcoming. "
1233
+ f"Skipping to the last upcoming trial."
1234
+ )
1235
+ n = len(self.upcomingTrials)
1236
+ # iterate n times
1237
+ for i in range(n):
1238
+ # before iterating, add "skipped" to data
1239
+ self.addData("skipped", True)
1240
+ # advance row in data file
1241
+ if self.getExp() is not None:
1242
+ self.getExp().nextEntry()
1243
+ # iterate
1244
+ self.__next__()
1245
+
1246
+ def rewindTrials(self, n=1):
1247
+ """
1248
+ Rewind back n trials - previously elapsed trials will return to being upcoming. If you
1249
+ try to rewind before the first trial, will log a warning and rewind *to* the first trial.
1250
+
1251
+ Parameters
1252
+ ----------
1253
+ n : int
1254
+ Number of trials to rewind back
1255
+ """
1256
+ # treat -n as n
1257
+ n = abs(n)
1258
+ # if rewinding past first trial, print warning and rewind to first trial
1259
+ if n > len(self.elapsedTrials):
1260
+ logging.warn(
1261
+ f"Requested rewind of {n} trials when only {len(self.elapsedTrials)} trials have "
1262
+ f"elapsed. Rewinding to the first trial."
1263
+ )
1264
+ n = len(self.elapsedTrials)
1265
+ # start with no trials
1266
+ rewound = [self.thisTrial]
1267
+ # pop the last n values from elapsed trials
1268
+ for i in range(n):
1269
+ rewound = [self.elapsedTrials.pop(-1)] + rewound
1270
+ # set thisTrial from first rewound value
1271
+ self.thisTrial = rewound.pop(0)
1272
+ # prepend rewound trials to upcoming array
1273
+ self.upcomingTrials = rewound + self.upcomingTrials
1274
+
1275
+ def getCurrentTrial(self):
1276
+ """
1277
+ Returns the current trial (`.thisTrial`)
1278
+
1279
+ Returns
1280
+ -------
1281
+ Trial
1282
+ The current trial
1283
+ """
1284
+ return self.thisTrial
1285
+
1286
+ def getAllTrials(self):
1287
+ """
1288
+ Returns all trials (elapsed, current and upcoming) with an index indicating which trial is
1289
+ the current trial.
1290
+
1291
+ Returns
1292
+ -------
1293
+ list[Trial]
1294
+ List of trials, in order (oldest to newest)
1295
+ int
1296
+ Index of the current trial in this list
1297
+ """
1298
+ return (self.elapsedTrials or []) + [self.thisTrial] + (self.upcomingTrials or []), len(self.elapsedTrials)
1074
1299
 
1075
1300
  def getFutureTrial(self, n=1):
1076
- """Returns the condition for n trials into the future, without
1301
+ """
1302
+ Returns the condition for n trials into the future, without
1077
1303
  advancing the trials. Returns 'None' if attempting to go beyond
1078
1304
  the last trial.
1305
+
1306
+ Returns
1307
+ -------
1308
+ Trial or None
1309
+ Trial object for n trials into the future.
1079
1310
  """
1080
- # check that we don't go out of bounds for either positive or negative
1081
- # offsets:
1082
- if n > self.nRemaining or self.thisN + n < 0:
1311
+ # make sure n is an integer
1312
+ if isinstance(n, str) and n.isnumeric():
1313
+ n = int(n)
1314
+ # return None if requesting beyond last trial
1315
+ if self.upcomingTrials is None or n > len(self.upcomingTrials):
1083
1316
  return None
1084
- seqs = np.array(self.sequenceIndices).transpose().flat
1085
- condIndex = seqs[self.thisN + n]
1086
- return self.trialList[condIndex]
1317
+ # return the corresponding trial from upcoming trials array
1318
+ return self.upcomingTrials[n-1]
1319
+
1320
+ def getFutureTrials(self, n=None, start=0):
1321
+ """
1322
+ Returns Trial objects for a given range in the future. Will start looking at `start` trials
1323
+ in the future and will return n trials from then, so e.g. to get all trials from 2 in the
1324
+ future to 5 in the future you would use `start=2` and `n=3`.
1325
+
1326
+ Parameters
1327
+ ----------
1328
+ n : int, optional
1329
+ How many trials into the future to look, by default None. Leave as None to show all
1330
+ future trials
1331
+ start : int, optional
1332
+ How many trials into the future to start looking at, by default 0
1333
+
1334
+ Returns
1335
+ -------
1336
+ list[Trial or None]
1337
+ List of Trial objects n long. Any trials beyond the last trial are None.
1338
+ """
1339
+ # if there are no future trials, return a blank list
1340
+ if self.upcomingTrials is None:
1341
+ return []
1342
+ # if None, get all future trials
1343
+ if n is None:
1344
+ n = len(self.upcomingTrials) - start
1345
+ # blank list to store trials in
1346
+ trials = []
1347
+ # iterate through n trials
1348
+ for i in range(n):
1349
+ # add each to the list
1350
+ trials.append(
1351
+ self.getFutureTrial(start + i + 1)
1352
+ )
1353
+
1354
+ return trials
1087
1355
 
1088
1356
  def getEarlierTrial(self, n=-1):
1089
1357
  """Returns the condition information from n trials previously.
@@ -1093,7 +1361,188 @@ class TrialHandler2(_BaseTrialHandler):
1093
1361
  # treat positive offset values as equivalent to negative ones:
1094
1362
  if n > 0:
1095
1363
  n = n * -1
1096
- return self.getFutureTrial(n)
1364
+ # return None if requesting before first trial
1365
+ if self.upcomingTrials is None or abs(n) > len(self.upcomingTrials):
1366
+ return None
1367
+ # return the corresponding trial from elapsed trials array
1368
+ return self.elapsedTrials[n]
1369
+
1370
+ def _createOutputArray(self, stimOut, dataOut, delim=None,
1371
+ matrixOnly=False):
1372
+ """Does the leg-work for saveAsText and saveAsExcel.
1373
+ Combines stimOut with ._parseDataOutput()
1374
+ """
1375
+ if (stimOut == [] and
1376
+ len(self.trialList) and
1377
+ hasattr(self.trialList[0], 'keys')):
1378
+ stimOut = list(self.trialList[0].keys())
1379
+ # these get added somewhere (by DataHandler?)
1380
+ if 'n' in stimOut:
1381
+ stimOut.remove('n')
1382
+ if 'float' in stimOut:
1383
+ stimOut.remove('float')
1384
+
1385
+ lines = []
1386
+ # parse the dataout section of the output
1387
+ dataOut, dataAnal, dataHead = self._createOutputArrayData(dataOut)
1388
+ if not matrixOnly:
1389
+ thisLine = []
1390
+ lines.append(thisLine)
1391
+ # write a header line
1392
+ for heading in list(stimOut) + dataHead:
1393
+ if heading == 'ran_sum':
1394
+ heading = 'n'
1395
+ elif heading == 'order_raw':
1396
+ heading = 'order'
1397
+ thisLine.append(heading)
1398
+
1399
+ # loop through stimuli, writing data
1400
+ for stimN in range(len(self.trialList)):
1401
+ thisLine = []
1402
+ lines.append(thisLine)
1403
+ # first the params for this stim (from self.trialList)
1404
+ for heading in stimOut:
1405
+ thisLine.append(self.trialList[stimN][heading])
1406
+
1407
+ # then the data for this stim (from self.data)
1408
+ for thisDataOut in dataOut:
1409
+ # make a string version of the data and then format it
1410
+ tmpData = dataAnal[thisDataOut][stimN]
1411
+ if hasattr(tmpData, 'tolist'): # is a numpy array
1412
+ strVersion = str(tmpData.tolist())
1413
+ # for numeric data replace None with a blank cell
1414
+ if tmpData.dtype.kind not in ['SaUV']:
1415
+ strVersion = strVersion.replace('None', '')
1416
+ elif tmpData in [None, 'None']:
1417
+ strVersion = ''
1418
+ else:
1419
+ strVersion = str(tmpData)
1420
+
1421
+ if strVersion == '()':
1422
+ # 'no data' in masked array should show as "--"
1423
+ strVersion = "--"
1424
+ # handle list of values (e.g. rt_raw )
1425
+ if (len(strVersion) and
1426
+ strVersion[0] in '[(' and
1427
+ strVersion[-1] in '])'):
1428
+ strVersion = strVersion[1:-1] # skip first and last chars
1429
+ # handle lists of lists (e.g. raw of multiple key presses)
1430
+ if (len(strVersion) and
1431
+ strVersion[0] in '[(' and
1432
+ strVersion[-1] in '])'):
1433
+ tup = eval(strVersion) # convert back to a tuple
1434
+ for entry in tup:
1435
+ # contents of each entry is a list or tuple so keep in
1436
+ # quotes to avoid probs with delim
1437
+ thisLine.append(str(entry))
1438
+ else:
1439
+ thisLine.extend(strVersion.split(','))
1440
+
1441
+ # add self.extraInfo
1442
+ if (self.extraInfo != None) and not matrixOnly:
1443
+ lines.append([])
1444
+ # give a single line of space and then a heading
1445
+ lines.append(['extraInfo'])
1446
+ for key, value in list(self.extraInfo.items()):
1447
+ lines.append([key, value])
1448
+ return lines
1449
+
1450
+ def _createOutputArrayData(self, dataOut):
1451
+ """This just creates the dataOut part of the output matrix.
1452
+ It is called by _createOutputArray() which creates the header
1453
+ line and adds the stimOut columns
1454
+ """
1455
+ dataHead = [] # will store list of data headers
1456
+ dataAnal = dict([]) # will store data that has been analyzed
1457
+ if type(dataOut) == str:
1458
+ # don't do list convert or we get a list of letters
1459
+ dataOut = [dataOut]
1460
+ elif type(dataOut) != list:
1461
+ dataOut = list(dataOut)
1462
+
1463
+ # expand any 'all' dataTypes to be full list of available dataTypes
1464
+ allDataTypes = list(self.data.keys())
1465
+ # ready to go through standard data types
1466
+ dataOutNew = []
1467
+ for thisDataOut in dataOut:
1468
+ if thisDataOut == 'n':
1469
+ # n is really just the sum of the ran trials
1470
+ dataOutNew.append('ran_sum')
1471
+ continue # no need to do more with this one
1472
+ # then break into dataType and analysis
1473
+ dataType, analType = thisDataOut.rsplit('_', 1)
1474
+ if dataType == 'all':
1475
+ dataOutNew.extend(
1476
+ [key + "_" + analType for key in allDataTypes])
1477
+ if 'order_mean' in dataOutNew:
1478
+ dataOutNew.remove('order_mean')
1479
+ if 'order_std' in dataOutNew:
1480
+ dataOutNew.remove('order_std')
1481
+ else:
1482
+ dataOutNew.append(thisDataOut)
1483
+ dataOut = dataOutNew
1484
+ # sort so all datatypes come together, rather than all analtypes
1485
+ dataOut.sort()
1486
+
1487
+ # do the various analyses, keeping track of fails (e.g. mean of a
1488
+ # string)
1489
+ dataOutInvalid = []
1490
+ # add back special data types (n and order)
1491
+ if 'ran_sum' in dataOut:
1492
+ # move n to the first column
1493
+ dataOut.remove('ran_sum')
1494
+ dataOut.insert(0, 'ran_sum')
1495
+ if 'order_raw' in dataOut:
1496
+ # move order_raw to the second column
1497
+ dataOut.remove('order_raw')
1498
+ dataOut.append('order_raw')
1499
+ # do the necessary analysis on the data
1500
+ for thisDataOutN, thisDataOut in enumerate(dataOut):
1501
+ dataType, analType = thisDataOut.rsplit('_', 1)
1502
+ if not dataType in self.data:
1503
+ # that analysis can't be done
1504
+ dataOutInvalid.append(thisDataOut)
1505
+ continue
1506
+ thisData = self.data[dataType]
1507
+
1508
+ # set the header
1509
+ dataHead.append(dataType + '_' + analType)
1510
+ # analyse thisData using numpy module
1511
+ if analType in dir(np):
1512
+ try:
1513
+ # will fail if we try to take mean of a string for example
1514
+ if analType == 'std':
1515
+ thisAnal = np.std(thisData, axis=1, ddof=0)
1516
+ # normalise by N-1 instead. This should work by
1517
+ # setting ddof=1 but doesn't as of 08/2010 (because
1518
+ # of using a masked array?)
1519
+ N = thisData.shape[1]
1520
+ if N == 1:
1521
+ thisAnal *= 0 # prevent a divide-by-zero error
1522
+ else:
1523
+ sqrt = np.sqrt
1524
+ thisAnal = thisAnal * sqrt(N) / sqrt(N - 1)
1525
+ else:
1526
+ thisAnal = eval("np.%s(thisData,1)" % analType)
1527
+ except Exception:
1528
+ # that analysis doesn't work
1529
+ dataHead.remove(dataType + '_' + analType)
1530
+ dataOutInvalid.append(thisDataOut)
1531
+ continue # to next analysis
1532
+ elif analType == 'raw':
1533
+ thisAnal = thisData
1534
+ else:
1535
+ raise AttributeError('You can only use analyses from numpy')
1536
+ # add extra cols to header if necess
1537
+ if len(thisAnal.shape) > 1:
1538
+ for n in range(thisAnal.shape[1] - 1):
1539
+ dataHead.append("")
1540
+ dataAnal[thisDataOut] = thisAnal
1541
+
1542
+ # remove invalid analyses (e.g. average of a string)
1543
+ for invalidAnal in dataOutInvalid:
1544
+ dataOut.remove(invalidAnal)
1545
+ return dataOut, dataAnal, dataHead
1097
1546
 
1098
1547
  def saveAsWideText(self, fileName,
1099
1548
  delim=None,
@@ -1150,7 +1599,7 @@ class TrialHandler2(_BaseTrialHandler):
1150
1599
  Defaults to `utf-8-sig`.
1151
1600
 
1152
1601
  """
1153
- if self.thisTrialN < 1 and self.thisRepN < 1:
1602
+ if self.thisTrialN < 0 and self.thisRepN < 0:
1154
1603
  # if both are < 1 we haven't started
1155
1604
  logging.info('TrialHandler.saveAsWideText called but no '
1156
1605
  'trials completed. Nothing saved')
@@ -1226,11 +1675,24 @@ class TrialHandler2(_BaseTrialHandler):
1226
1675
  # store in the columns list to help ordering later
1227
1676
  if thisType not in self.columns:
1228
1677
  self.columns.append(thisType)
1678
+ # make sure we have a thisTrial
1679
+ if self.thisTrial is None:
1680
+ if self.upcomingTrials:
1681
+ self.thisTrial = self.upcomingTrials.pop(0)
1682
+ else:
1683
+ self.thisTrial = Trial(
1684
+ self,
1685
+ thisN=0,
1686
+ thisRepN=0,
1687
+ thisTrialN=0,
1688
+ thisIndex=0,
1689
+ data={}
1690
+ )
1229
1691
  # save the actual value in a data dict
1230
1692
  self.thisTrial[thisType] = value
1231
1693
  if self.getExp() is not None:
1232
1694
  # update the experiment handler too
1233
- self.getExp().addData(thisType, value)
1695
+ self.getExp().addData(f"{self.name}.{thisType}", value)
1234
1696
 
1235
1697
 
1236
1698
  class TrialHandlerExt(TrialHandler):
@@ -1868,7 +2330,7 @@ class TrialHandlerExt(TrialHandler):
1868
2330
  Defaults to `utf-8-sig`.
1869
2331
 
1870
2332
  """
1871
- if self.thisTrialN < 1 and self.thisRepN < 1:
2333
+ if self.thisTrialN < 0 and self.thisRepN < 0:
1872
2334
  # if both are < 1 we haven't started
1873
2335
  logging.info('TrialHandler.saveAsWideText called but no trials'
1874
2336
  ' completed. Nothing saved')