psychopy 2025.1.0__py3-none-any.whl → 2025.2.1__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.
- psychopy/VERSION +1 -1
- psychopy/alerts/alertsCatalogue/4810.yaml +19 -0
- psychopy/alerts/alertsCatalogue/alertCategories.yaml +4 -0
- psychopy/alerts/alertsCatalogue/alertmsg.py +15 -1
- psychopy/alerts/alertsCatalogue/generateAlertmsg.py +2 -2
- psychopy/app/Resources/classic/add_many.png +0 -0
- psychopy/app/Resources/classic/add_many@2x.png +0 -0
- psychopy/app/Resources/classic/devices.png +0 -0
- psychopy/app/Resources/classic/devices@2x.png +0 -0
- psychopy/app/Resources/classic/photometer.png +0 -0
- psychopy/app/Resources/classic/photometer@2x.png +0 -0
- psychopy/app/Resources/dark/add_many.png +0 -0
- psychopy/app/Resources/dark/add_many@2x.png +0 -0
- psychopy/app/Resources/dark/devices.png +0 -0
- psychopy/app/Resources/dark/devices@2x.png +0 -0
- psychopy/app/Resources/dark/photometer.png +0 -0
- psychopy/app/Resources/dark/photometer@2x.png +0 -0
- psychopy/app/Resources/light/add_many.png +0 -0
- psychopy/app/Resources/light/add_many@2x.png +0 -0
- psychopy/app/Resources/light/devices.png +0 -0
- psychopy/app/Resources/light/devices@2x.png +0 -0
- psychopy/app/Resources/light/photometer.png +0 -0
- psychopy/app/Resources/light/photometer@2x.png +0 -0
- psychopy/app/_psychopyApp.py +35 -13
- psychopy/app/builder/builder.py +88 -35
- psychopy/app/builder/dialogs/__init__.py +69 -220
- psychopy/app/builder/dialogs/dlgsCode.py +29 -8
- psychopy/app/builder/dialogs/paramCtrls.py +1468 -904
- psychopy/app/builder/validators.py +25 -17
- psychopy/app/coder/coder.py +12 -1
- psychopy/app/coder/repl.py +5 -2
- psychopy/app/colorpicker/__init__.py +1 -1
- psychopy/app/deviceManager/__init__.py +1 -0
- psychopy/app/deviceManager/addDialog.py +218 -0
- psychopy/app/deviceManager/dialog.py +185 -0
- psychopy/app/deviceManager/panel.py +191 -0
- psychopy/app/deviceManager/utils.py +60 -0
- psychopy/app/idle.py +7 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +12695 -10592
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +11221 -9712
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_US/LC_MESSAGE/messages.po +10195 -18
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +11917 -9101
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +11924 -9103
- psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_US/LC_MESSAGE/messages.po +11917 -9101
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +11084 -9569
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +11590 -5806
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +11091 -9577
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +11072 -9549
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +11071 -9559
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +11072 -9560
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1485 -1137
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +11463 -8757
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +11288 -9434
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +11441 -8747
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +11069 -9545
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +12085 -8268
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +11929 -8022
- psychopy/app/plugin_manager/dialog.py +12 -3
- psychopy/app/plugin_manager/packageIndex.py +303 -0
- psychopy/app/plugin_manager/packages.py +203 -63
- psychopy/app/plugin_manager/plugins.py +120 -240
- psychopy/app/preferencesDlg.py +6 -1
- psychopy/app/psychopyApp.py +16 -4
- psychopy/app/runner/runner.py +10 -2
- psychopy/app/runner/scriptProcess.py +8 -3
- psychopy/app/stdout/stdOutRich.py +11 -4
- psychopy/app/themes/icons.py +3 -0
- psychopy/app/utils.py +61 -0
- psychopy/colors.py +10 -5
- psychopy/data/experiment.py +133 -23
- psychopy/data/routine.py +12 -0
- psychopy/data/staircase.py +42 -20
- psychopy/data/trial.py +20 -12
- psychopy/data/utils.py +43 -3
- psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
- psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
- psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
- psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
- psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
- psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
- psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
- psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
- psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
- psychopy/event.py +20 -15
- psychopy/experiment/_experiment.py +86 -10
- psychopy/experiment/components/__init__.py +3 -10
- psychopy/experiment/components/_base.py +9 -20
- psychopy/experiment/components/button/__init__.py +1 -1
- psychopy/experiment/components/buttonBox/__init__.py +50 -54
- psychopy/experiment/components/camera/__init__.py +137 -359
- psychopy/experiment/components/keyboard/__init__.py +17 -24
- psychopy/experiment/components/microphone/__init__.py +61 -110
- psychopy/experiment/components/movie/__init__.py +2 -3
- psychopy/experiment/components/serialOut/__init__.py +192 -93
- psychopy/experiment/components/settings/__init__.py +45 -27
- psychopy/experiment/components/sound/__init__.py +82 -73
- psychopy/experiment/components/soundsensor/__init__.py +43 -80
- psychopy/experiment/devices.py +303 -0
- psychopy/experiment/exports.py +20 -18
- psychopy/experiment/flow.py +7 -0
- psychopy/experiment/loops.py +47 -29
- psychopy/experiment/monitor.py +74 -0
- psychopy/experiment/params.py +48 -10
- psychopy/experiment/plugins.py +28 -108
- psychopy/experiment/py2js_transpiler.py +1 -1
- psychopy/experiment/routines/__init__.py +1 -1
- psychopy/experiment/routines/_base.py +59 -24
- psychopy/experiment/routines/audioValidator/__init__.py +19 -155
- psychopy/experiment/routines/visualValidator/__init__.py +25 -25
- psychopy/hardware/__init__.py +20 -57
- psychopy/hardware/button.py +15 -2
- psychopy/hardware/camera/__init__.py +2237 -1394
- psychopy/hardware/joystick/__init__.py +1 -1
- psychopy/hardware/keyboard.py +5 -8
- psychopy/hardware/listener.py +4 -1
- psychopy/hardware/manager.py +75 -35
- psychopy/hardware/microphone.py +53 -7
- psychopy/hardware/monitor.py +144 -0
- psychopy/hardware/photometer/__init__.py +156 -117
- psychopy/hardware/serialdevice.py +16 -2
- psychopy/hardware/soundsensor.py +4 -1
- psychopy/iohub/devices/deviceConfigValidation.py +2 -1
- psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +2 -2
- psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/__init__.py +1 -0
- psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/eyetracker.py +10 -0
- psychopy/iohub/devices/keyboard/darwin.py +8 -5
- psychopy/iohub/util/__init__.py +7 -8
- psychopy/localization/generateTranslationTemplate.py +208 -116
- psychopy/localization/messages.pot +4305 -3502
- psychopy/monitors/MonitorCenter.py +174 -74
- psychopy/plugins/__init__.py +6 -4
- psychopy/preferences/devices.py +80 -0
- psychopy/preferences/generateHints.py +2 -1
- psychopy/preferences/preferences.py +35 -11
- psychopy/scripts/psychopy-pkgutil.py +969 -0
- psychopy/scripts/psyexpCompile.py +1 -1
- psychopy/session.py +34 -38
- psychopy/sound/__init__.py +6 -260
- psychopy/sound/audioclip.py +164 -0
- psychopy/sound/backend_ptb.py +8 -0
- psychopy/sound/backend_pygame.py +10 -0
- psychopy/sound/backend_pysound.py +9 -0
- psychopy/sound/backends/__init__.py +0 -0
- psychopy/sound/microphone.py +3 -0
- psychopy/sound/sound.py +58 -0
- psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
- psychopy/tests/data/duplicateHeaders.csv +2 -0
- psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
- psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
- psychopy/tests/test_data/test_utils.py +5 -1
- psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
- psychopy/tests/test_hardware/test_ports.py +0 -12
- psychopy/tests/test_tools/test_stringtools.py +1 -1
- psychopy/tools/attributetools.py +12 -5
- psychopy/tools/fontmanager.py +17 -14
- psychopy/tools/gltools.py +4 -2
- psychopy/tools/movietools.py +43 -2
- psychopy/tools/stringtools.py +33 -8
- psychopy/tools/versionchooser.py +1 -1
- psychopy/validation/audio.py +5 -1
- psychopy/validation/visual.py +5 -1
- psychopy/visual/basevisual.py +8 -7
- psychopy/visual/circle.py +2 -2
- psychopy/visual/helpers.py +3 -1
- psychopy/visual/image.py +29 -109
- psychopy/visual/movies/__init__.py +1800 -313
- psychopy/visual/polygon.py +4 -0
- psychopy/visual/shape.py +2 -2
- psychopy/visual/window.py +35 -12
- psychopy/voicekey/__init__.py +41 -669
- psychopy/voicekey/labjack_vks.py +7 -48
- psychopy/voicekey/parallel_vks.py +7 -42
- psychopy/voicekey/vk_tools.py +114 -263
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/METADATA +20 -13
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/RECORD +222 -190
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
- psychopy/visual/movies/players/__init__.py +0 -62
- psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
- psychopy/voicekey/demo_vks.py +0 -12
- psychopy/voicekey/signal.py +0 -42
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<?xml version="1.0" ?>
|
|
2
|
-
<PsychoPy2experiment encoding="utf-8" version="2025.
|
|
2
|
+
<PsychoPy2experiment encoding="utf-8" version="2025.2.0">
|
|
3
3
|
<Settings>
|
|
4
|
-
<Param val="use prefs" valType="str" updates="None" name="Audio latency priority"/>
|
|
5
4
|
<Param val="use prefs" valType="str" updates="None" name="Audio lib"/>
|
|
6
5
|
<Param val="" valType="str" updates="None" name="Completed URL"/>
|
|
7
6
|
<Param val="auto" valType="str" updates="None" name="Data file delimiter"/>
|
|
@@ -168,17 +167,12 @@
|
|
|
168
167
|
<Param val="[[0, 0.5],[0, -0.5],[1, -0.5],[1, 0.5]]" valType="list" updates="constant" name="vertices"/>
|
|
169
168
|
</PolygonComponent>
|
|
170
169
|
<MicrophoneComponent name="mic" plugin="None">
|
|
171
|
-
<Param val="
|
|
172
|
-
<Param val="default" valType="str" updates="None" name="device"/>
|
|
173
|
-
<Param val="" valType="str" updates="None" name="deviceLabel"/>
|
|
170
|
+
<Param val="demoMicrophone" valType="device" updates="None" name="deviceLabel"/>
|
|
174
171
|
<Param val="False" valType="bool" updates="None" name="disabled"/>
|
|
175
172
|
<Param val="" valType="code" updates="None" name="durationEstim"/>
|
|
176
|
-
<Param val="False" valType="code" updates="None" name="exclusive"/>
|
|
177
|
-
<Param val="24000" valType="num" updates="None" name="maxSize"/>
|
|
178
173
|
<Param val="mic" valType="code" updates="None" name="name"/>
|
|
179
174
|
<Param val="default" valType="code" updates="None" name="outputType"/>
|
|
180
175
|
<Param val="warn" valType="str" updates="set every repeat" name="policyWhenFull"/>
|
|
181
|
-
<Param val="DVD Audio (48kHz)" valType="num" updates="None" name="sampleRate"/>
|
|
182
176
|
<Param val="True" valType="bool" updates="None" name="saveStartStop"/>
|
|
183
177
|
<Param val="True" valType="bool" updates="None" name="speakTimes"/>
|
|
184
178
|
<Param val="" valType="code" updates="None" name="startEstim"/>
|
|
@@ -258,7 +252,6 @@
|
|
|
258
252
|
<KeyboardComponent name="key_resp" plugin="None">
|
|
259
253
|
<Param val="'space'" valType="list" updates="constant" name="allowedKeys"/>
|
|
260
254
|
<Param val="" valType="str" updates="constant" name="correctAns"/>
|
|
261
|
-
<Param val="" valType="str" updates="None" name="deviceLabel"/>
|
|
262
255
|
<Param val="False" valType="bool" updates="None" name="disabled"/>
|
|
263
256
|
<Param val="True" valType="bool" updates="constant" name="discard previous"/>
|
|
264
257
|
<Param val="" valType="code" updates="None" name="durationEstim"/>
|
|
@@ -336,17 +329,12 @@
|
|
|
336
329
|
<Param val="" valType="code" updates="None" name="validator"/>
|
|
337
330
|
</TextboxComponent>
|
|
338
331
|
<MicrophoneComponent name="mic_2" plugin="None">
|
|
339
|
-
<Param val="
|
|
340
|
-
<Param val="None" valType="str" updates="None" name="device"/>
|
|
341
|
-
<Param val="" valType="str" updates="None" name="deviceLabel"/>
|
|
332
|
+
<Param val="demoMicrophone" valType="device" updates="None" name="deviceLabel"/>
|
|
342
333
|
<Param val="False" valType="bool" updates="None" name="disabled"/>
|
|
343
334
|
<Param val="" valType="code" updates="None" name="durationEstim"/>
|
|
344
|
-
<Param val="False" valType="code" updates="None" name="exclusive"/>
|
|
345
|
-
<Param val="24000" valType="num" updates="None" name="maxSize"/>
|
|
346
335
|
<Param val="mic_2" valType="code" updates="None" name="name"/>
|
|
347
336
|
<Param val="default" valType="code" updates="None" name="outputType"/>
|
|
348
337
|
<Param val="warn" valType="str" updates="set every repeat" name="policyWhenFull"/>
|
|
349
|
-
<Param val="DVD Audio (48kHz)" valType="num" updates="None" name="sampleRate"/>
|
|
350
338
|
<Param val="True" valType="bool" updates="None" name="saveStartStop"/>
|
|
351
339
|
<Param val="True" valType="bool" updates="None" name="speakTimes"/>
|
|
352
340
|
<Param val="" valType="code" updates="None" name="startEstim"/>
|
|
@@ -456,7 +444,6 @@
|
|
|
456
444
|
<KeyboardComponent name="key_resp_2" plugin="None">
|
|
457
445
|
<Param val="'space'" valType="list" updates="constant" name="allowedKeys"/>
|
|
458
446
|
<Param val="" valType="str" updates="constant" name="correctAns"/>
|
|
459
|
-
<Param val="" valType="str" updates="None" name="deviceLabel"/>
|
|
460
447
|
<Param val="False" valType="bool" updates="None" name="disabled"/>
|
|
461
448
|
<Param val="True" valType="bool" updates="constant" name="discard previous"/>
|
|
462
449
|
<Param val="" valType="code" updates="None" name="durationEstim"/>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This script reads an HDF5 eye-tracking data file, identifies trials based on specified
|
|
3
|
+
start and end markers in the experiment messages, extracts gaze data for each trial,
|
|
4
|
+
and animates gaze movement over time in a 2D space.
|
|
5
|
+
|
|
6
|
+
The user can customize:
|
|
7
|
+
- The path to the HDF5 file
|
|
8
|
+
- The marker text used to indicate the start and end of trials
|
|
9
|
+
|
|
10
|
+
⚠️ If you get an error saying a package (like h5py, numpy, matplotlib) is missing:
|
|
11
|
+
In PsychoPy, go to:
|
|
12
|
+
Tools > Plugins/Packages Manager > Packages
|
|
13
|
+
Then search for and install the missing package (e.g., "h5py", "matplotlib", "numpy").
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
This script reads an HDF5 eye-tracking data file, identifies trials based on specified
|
|
18
|
+
start and end markers in the experiment messages, extracts gaze data for each trial,
|
|
19
|
+
and animates gaze movement over time in a 2D space.
|
|
20
|
+
|
|
21
|
+
The user can customize:
|
|
22
|
+
- The path to the HDF5 file
|
|
23
|
+
- The marker text used to indicate the start and end of trials
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# ==== USER INPUTS ====
|
|
27
|
+
HDF5_FILE = '823238_eyetracking_youtube_2025-05-01_10h34.25.036.hdf5' # Path to HDF5 file
|
|
28
|
+
TRIAL_START_MARKER = 'BEGIN_SEQUENCE 3'#'trial_start' # Marker text for start of trial
|
|
29
|
+
TRIAL_END_MARKER = 'DONE_SEQUENCE 3'#'trial_end' # Marker text for end of trial
|
|
30
|
+
# ======================
|
|
31
|
+
|
|
32
|
+
import h5py
|
|
33
|
+
import numpy as np
|
|
34
|
+
import matplotlib.pyplot as plt
|
|
35
|
+
import matplotlib.animation as animation
|
|
36
|
+
|
|
37
|
+
def extract_eye_data_in_trials(hdf5_path, start_marker, end_marker):
|
|
38
|
+
"""
|
|
39
|
+
Extract gaze data for each trial based on custom trial start/end markers.
|
|
40
|
+
|
|
41
|
+
Parameters:
|
|
42
|
+
hdf5_path (str): Path to HDF5 file.
|
|
43
|
+
start_marker (str): String identifying trial start in the message events.
|
|
44
|
+
end_marker (str): String identifying trial end in the message events.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
List of dicts: Each dict contains time, gaze_x, and gaze_y arrays for one trial.
|
|
48
|
+
"""
|
|
49
|
+
with h5py.File(hdf5_path, 'r') as f:
|
|
50
|
+
# Dataset paths
|
|
51
|
+
msg_path = 'data_collection/events/experiment/MessageEvent'
|
|
52
|
+
eye_path = 'data_collection/events/eyetracker/MonocularEyeSampleEvent'
|
|
53
|
+
|
|
54
|
+
# Load message times and text
|
|
55
|
+
messages = f[msg_path]
|
|
56
|
+
msg_times = messages['time'][:]
|
|
57
|
+
msg_texts = messages['text'][:].astype(str)
|
|
58
|
+
|
|
59
|
+
# Find trial start/end times using the provided marker strings
|
|
60
|
+
trial_starts = msg_times[np.char.find(msg_texts, start_marker) != -1]
|
|
61
|
+
trial_ends = msg_times[np.char.find(msg_texts, end_marker) != -1]
|
|
62
|
+
|
|
63
|
+
if len(trial_starts) != len(trial_ends):
|
|
64
|
+
raise ValueError("Mismatched number of trial start and end events")
|
|
65
|
+
|
|
66
|
+
# Load eye tracking data
|
|
67
|
+
eye_data = f[eye_path]
|
|
68
|
+
eye_times = eye_data['time'][:]
|
|
69
|
+
gaze_x = eye_data['gaze_x'][:]
|
|
70
|
+
gaze_y = eye_data['gaze_y'][:]
|
|
71
|
+
|
|
72
|
+
# Extract data between trial start and end times
|
|
73
|
+
trials_gaze = []
|
|
74
|
+
for start, end in zip(trial_starts, trial_ends):
|
|
75
|
+
mask = (eye_times >= start) & (eye_times <= end)
|
|
76
|
+
trial_gaze = {
|
|
77
|
+
'start_time': start,
|
|
78
|
+
'end_time': end,
|
|
79
|
+
'time': eye_times[mask],
|
|
80
|
+
'gaze_x': gaze_x[mask],
|
|
81
|
+
'gaze_y': gaze_y[mask]
|
|
82
|
+
}
|
|
83
|
+
trials_gaze.append(trial_gaze)
|
|
84
|
+
|
|
85
|
+
return trials_gaze
|
|
86
|
+
|
|
87
|
+
def animate_gaze(trial_data, trial_num, save_to_file=False):
|
|
88
|
+
"""
|
|
89
|
+
Animate gaze positions over time for a single trial.
|
|
90
|
+
|
|
91
|
+
Parameters:
|
|
92
|
+
trial_data (dict): Dictionary with 'time', 'gaze_x', and 'gaze_y' keys.
|
|
93
|
+
trial_num (int): Index of the trial (for labeling).
|
|
94
|
+
save_to_file (bool): Whether to save the animation as an .mp4 video file.
|
|
95
|
+
"""
|
|
96
|
+
x = trial_data['gaze_x']
|
|
97
|
+
y = trial_data['gaze_y']
|
|
98
|
+
t = trial_data['time']
|
|
99
|
+
|
|
100
|
+
fig, ax = plt.subplots(figsize=(8, 6))
|
|
101
|
+
ax.set_xlim(-1, 1)
|
|
102
|
+
ax.set_ylim(-0.5, 0.5)
|
|
103
|
+
ax.set_title(f"Gaze Animation - Trial {trial_num}")
|
|
104
|
+
ax.set_xlabel("Gaze X")
|
|
105
|
+
ax.set_ylabel("Gaze Y")
|
|
106
|
+
|
|
107
|
+
point, = ax.plot([], [], 'ro', markersize=5) # Red dot to show gaze
|
|
108
|
+
|
|
109
|
+
def init():
|
|
110
|
+
point.set_data([], [])
|
|
111
|
+
return point,
|
|
112
|
+
|
|
113
|
+
def update(frame):
|
|
114
|
+
point.set_data(x[frame], y[frame])
|
|
115
|
+
return point,
|
|
116
|
+
|
|
117
|
+
ani = animation.FuncAnimation(
|
|
118
|
+
fig, update, frames=len(x),
|
|
119
|
+
init_func=init, blit=True, interval=10
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if save_to_file:
|
|
123
|
+
ani.save(f'gaze_trial_{trial_num}.mp4', fps=60, extra_args=['-vcodec', 'libx264'])
|
|
124
|
+
|
|
125
|
+
plt.show()
|
|
126
|
+
|
|
127
|
+
# ====== MAIN EXECUTION ======
|
|
128
|
+
if __name__ == '__main__':
|
|
129
|
+
result = extract_eye_data_in_trials(HDF5_FILE, TRIAL_START_MARKER, TRIAL_END_MARKER)
|
|
130
|
+
if not result:
|
|
131
|
+
print("No valid trials found.")
|
|
132
|
+
else:
|
|
133
|
+
animate_gaze(result[0], trial_num=1, save_to_file=False)
|
psychopy/event.py
CHANGED
|
@@ -297,7 +297,7 @@ def _onPygletMouseRelease(x, y, button, modifiers, emulated=False):
|
|
|
297
297
|
|
|
298
298
|
def _onPygletMouseWheel(x, y, scroll_x, scroll_y):
|
|
299
299
|
global mouseWheelRel
|
|
300
|
-
mouseWheelRel
|
|
300
|
+
mouseWheelRel += numpy.array([scroll_x, scroll_y])
|
|
301
301
|
msg = "Mouse: wheel shift=(%i,%i), pos=(%i,%i)"
|
|
302
302
|
logging.data(msg % (scroll_x, scroll_y, x, y))
|
|
303
303
|
|
|
@@ -874,23 +874,28 @@ class Mouse:
|
|
|
874
874
|
|
|
875
875
|
"""
|
|
876
876
|
global mouseButtons, mouseTimes
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
877
|
+
|
|
878
|
+
if self.win is None: # no backend specified
|
|
879
|
+
return None
|
|
880
|
+
|
|
881
|
+
if havePyglet and self.win.winType == 'pyglet':
|
|
880
882
|
# for each (pyglet) window, dispatch its events before checking
|
|
881
883
|
# event buffer
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
884
|
+
for win in pyglet.app.windows:
|
|
885
|
+
win.dispatch_events() # pump events on pyglet windows
|
|
886
|
+
elif haveGLFW and self.win.winType == 'glfw':
|
|
887
|
+
glfw.poll_events()
|
|
888
|
+
elif havePygame and self.win.winType == 'pygame':
|
|
889
|
+
return mouse.get_pressed()
|
|
890
|
+
else:
|
|
891
|
+
raise RuntimeError(
|
|
892
|
+
"Mouse.getPressed() is only supported for the pyglet, "
|
|
893
|
+
"pygame and glfw backends.")
|
|
888
894
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
return copy.copy(mouseButtons), copy.copy(mouseTimes)
|
|
895
|
+
if not getTime:
|
|
896
|
+
return copy.copy(mouseButtons)
|
|
897
|
+
else:
|
|
898
|
+
return copy.copy(mouseButtons), copy.copy(mouseTimes)
|
|
894
899
|
|
|
895
900
|
def isPressedIn(self, shape, buttons=(0, 1, 2)):
|
|
896
901
|
"""Returns `True` if the mouse is currently inside the shape and
|
|
@@ -117,6 +117,7 @@ class Experiment:
|
|
|
117
117
|
Routine. The Flow controls how Routines are organised
|
|
118
118
|
e.g. the nature of repeats and branching of an experiment.
|
|
119
119
|
"""
|
|
120
|
+
|
|
120
121
|
|
|
121
122
|
def __init__(self, prefs=None):
|
|
122
123
|
super(Experiment, self).__init__()
|
|
@@ -165,6 +166,11 @@ class Experiment:
|
|
|
165
166
|
self._expHandler = TrialHandler(exp=self, name='thisExp')
|
|
166
167
|
self._expHandler.type = 'ExperimentHandler' # true at run-time
|
|
167
168
|
|
|
169
|
+
# get a local reference of all Components and Routines (refreshed on loading a new file)
|
|
170
|
+
self.allCompons = getAllComponents(
|
|
171
|
+
self.prefsBuilder['componentsFolders'], fetchIcons=False)
|
|
172
|
+
self.allRoutines = getAllStandaloneRoutines(fetchIcons=False)
|
|
173
|
+
|
|
168
174
|
def __eq__(self, other):
|
|
169
175
|
if isinstance(other, Experiment):
|
|
170
176
|
# if another experiment, compare filenames
|
|
@@ -577,6 +583,14 @@ class Experiment:
|
|
|
577
583
|
name = paramNode.get('name')
|
|
578
584
|
valType = paramNode.get('valType')
|
|
579
585
|
val = paramNode.get('val')
|
|
586
|
+
#
|
|
587
|
+
# get knowwn legacy params for the current Component
|
|
588
|
+
componentLegacyParams = []
|
|
589
|
+
if componentNode is not None:
|
|
590
|
+
if componentNode.tag in self.allCompons:
|
|
591
|
+
componentLegacyParams = self.allCompons[componentNode.tag].legacyParams
|
|
592
|
+
if componentNode.tag in self.allRoutines:
|
|
593
|
+
componentLegacyParams = self.allRoutines[componentNode.tag].legacyParams
|
|
580
594
|
# many components need web char newline replacement
|
|
581
595
|
if not name == 'advancedParams':
|
|
582
596
|
val = val.replace(" ", "\n")
|
|
@@ -727,6 +741,10 @@ class Experiment:
|
|
|
727
741
|
else:
|
|
728
742
|
if name in params:
|
|
729
743
|
params[name].val = val
|
|
744
|
+
elif name in legacyParams + componentLegacyParams:
|
|
745
|
+
# don't warn people if we know it's OK (e.g. for params
|
|
746
|
+
# that have been removed
|
|
747
|
+
return recognised
|
|
730
748
|
else:
|
|
731
749
|
# we found an unknown parameter (probably from the future)
|
|
732
750
|
params[name] = Param(
|
|
@@ -740,11 +758,7 @@ class Experiment:
|
|
|
740
758
|
params[name].allowedTypes = paramNode.get('allowedTypes')
|
|
741
759
|
if params[name].allowedTypes is None:
|
|
742
760
|
params[name].allowedTypes = []
|
|
743
|
-
if
|
|
744
|
-
# don't warn people if we know it's OK (e.g. for params
|
|
745
|
-
# that have been removed
|
|
746
|
-
pass
|
|
747
|
-
elif componentNode is not None and componentNode.get("plugin", False) not in (False, "", "None", None):
|
|
761
|
+
if componentNode is not None and componentNode.get("plugin", False) not in (False, "", "None", None):
|
|
748
762
|
# is param unrecognised because it's from a plugin?
|
|
749
763
|
params[name].categ = "Plugin"
|
|
750
764
|
params[name].plugin = componentNode.get("plugin", False)
|
|
@@ -757,17 +771,24 @@ class Experiment:
|
|
|
757
771
|
|
|
758
772
|
# get the value type and update rate
|
|
759
773
|
if 'valType' in list(paramNode.keys()):
|
|
760
|
-
|
|
774
|
+
valType = paramNode.get('valType')
|
|
775
|
+
setValType = True
|
|
761
776
|
# compatibility checks:
|
|
762
777
|
if name in ['allowedKeys'] and paramNode.get('valType') == 'str':
|
|
763
778
|
# these components were changed in v1.70.00
|
|
764
|
-
|
|
779
|
+
valType = 'code'
|
|
765
780
|
elif name == 'Selected rows':
|
|
766
781
|
# changed in 1.81.00 from 'code' to 'str': allow string or var
|
|
767
|
-
|
|
782
|
+
valType = 'str'
|
|
768
783
|
# conversions based on valType
|
|
769
784
|
if params[name].valType == 'bool':
|
|
770
785
|
params[name].val = eval("%s" % params[name].val)
|
|
786
|
+
# "device" valType was introduced in 2025.2.0 and should always override saved valType
|
|
787
|
+
if params[name].valType == "device":
|
|
788
|
+
setValType = False
|
|
789
|
+
# do actual setting
|
|
790
|
+
if setValType:
|
|
791
|
+
params[name].valType = valType
|
|
771
792
|
if 'updates' in list(paramNode.keys()):
|
|
772
793
|
params[name].updates = paramNode.get('updates')
|
|
773
794
|
|
|
@@ -873,9 +894,9 @@ class Experiment:
|
|
|
873
894
|
self.setExpName(shortName)
|
|
874
895
|
# fetch routines
|
|
875
896
|
routinesNode = root.find('Routines')
|
|
876
|
-
allCompons = getAllComponents(
|
|
897
|
+
self.allCompons = allCompons = getAllComponents(
|
|
877
898
|
self.prefsBuilder['componentsFolders'], fetchIcons=False)
|
|
878
|
-
allRoutines = getAllStandaloneRoutines(fetchIcons=False)
|
|
899
|
+
self.allRoutines = allRoutines = getAllStandaloneRoutines(fetchIcons=False)
|
|
879
900
|
# get each routine node from the list of routines
|
|
880
901
|
for routineNode in routinesNode:
|
|
881
902
|
if routineNode.tag == "Routine":
|
|
@@ -959,6 +980,9 @@ class Experiment:
|
|
|
959
980
|
if paramNode.tag == "Param":
|
|
960
981
|
for key, val in paramNode.items():
|
|
961
982
|
name = paramNode.get("name")
|
|
983
|
+
# "device" valType was introduced in 2025.2.0 and should always override saved valType
|
|
984
|
+
if key == "valType" and routine.params[name].valType == "device":
|
|
985
|
+
continue
|
|
962
986
|
if name in routine.params:
|
|
963
987
|
setattr(routine.params[name], key, val)
|
|
964
988
|
# Add routine to experiment
|
|
@@ -1134,6 +1158,58 @@ class Experiment:
|
|
|
1134
1158
|
def htmlFolder(self):
|
|
1135
1159
|
return self.settings.params['HTML path'].val
|
|
1136
1160
|
|
|
1161
|
+
def getRequiredDeviceNames(self):
|
|
1162
|
+
"""
|
|
1163
|
+
Get the device names which need to be defined for this experiment to run, along with a list
|
|
1164
|
+
of possible types for each one.
|
|
1165
|
+
|
|
1166
|
+
Returns
|
|
1167
|
+
-------
|
|
1168
|
+
dict[str: list[str]]
|
|
1169
|
+
Device names and a list of possible types for each one
|
|
1170
|
+
"""
|
|
1171
|
+
# dict in which to store usages
|
|
1172
|
+
usages = {}
|
|
1173
|
+
|
|
1174
|
+
def _process(emt):
|
|
1175
|
+
"""
|
|
1176
|
+
Process an element (Component or Routine) for device names and append them to the
|
|
1177
|
+
usages dict.
|
|
1178
|
+
|
|
1179
|
+
Parameters
|
|
1180
|
+
----------
|
|
1181
|
+
emt : Component or Routine
|
|
1182
|
+
Element to process
|
|
1183
|
+
"""
|
|
1184
|
+
# iterate through param's inita values
|
|
1185
|
+
for param in getInitVals(emt.params).values():
|
|
1186
|
+
# if it's a device...
|
|
1187
|
+
if param.valType == "device":
|
|
1188
|
+
# get value
|
|
1189
|
+
deviceName = param.val
|
|
1190
|
+
# make sure device name is in usages dict
|
|
1191
|
+
if deviceName not in usages:
|
|
1192
|
+
usages[deviceName] = []
|
|
1193
|
+
# add any new usages
|
|
1194
|
+
for cls in getattr(emt, "deviceClasses", []):
|
|
1195
|
+
if cls not in usages[deviceName]:
|
|
1196
|
+
usages[deviceName].append(cls)
|
|
1197
|
+
|
|
1198
|
+
# iterate through routines
|
|
1199
|
+
for rt in self.routines.values():
|
|
1200
|
+
if isinstance(rt, BaseStandaloneRoutine):
|
|
1201
|
+
# for standalone routines, get device names from params
|
|
1202
|
+
_process(rt)
|
|
1203
|
+
else:
|
|
1204
|
+
# for regular routines, get device names from each component
|
|
1205
|
+
for comp in rt:
|
|
1206
|
+
_process(comp)
|
|
1207
|
+
# process settings
|
|
1208
|
+
_process(self.settings)
|
|
1209
|
+
|
|
1210
|
+
return usages
|
|
1211
|
+
|
|
1212
|
+
|
|
1137
1213
|
def getComponentFromName(self, name):
|
|
1138
1214
|
"""Searches all the Routines in the Experiment for a matching Comp name
|
|
1139
1215
|
|
|
@@ -25,7 +25,6 @@ excludeComponents = [
|
|
|
25
25
|
'BaseComponent',
|
|
26
26
|
'BaseVisualComponent',
|
|
27
27
|
'BaseDeviceComponent',
|
|
28
|
-
'BaseStandaloneRoutine' # templates only
|
|
29
28
|
] # this one isn't ready yet
|
|
30
29
|
|
|
31
30
|
# Plugin components are added dynamically at runtime, usually from plugin
|
|
@@ -288,14 +287,6 @@ def getInitVals(params, target="PsychoPy"):
|
|
|
288
287
|
.format(inits[name]))
|
|
289
288
|
inits[name].valType = 'code'
|
|
290
289
|
|
|
291
|
-
if name == "deviceLabel":
|
|
292
|
-
if "name" in inits and not params[name]:
|
|
293
|
-
# if deviceName exists but is blank, use component name
|
|
294
|
-
inits[name].val = inits['name'].val
|
|
295
|
-
# make a code version of device name
|
|
296
|
-
inits['deviceLabelCode'] = copy.copy(inits[name])
|
|
297
|
-
inits['deviceLabelCode'].valType = "code"
|
|
298
|
-
|
|
299
290
|
if not hasattr(inits[name], 'updates'): # might be settings parameter instead
|
|
300
291
|
continue
|
|
301
292
|
|
|
@@ -305,7 +296,7 @@ def getInitVals(params, target="PsychoPy"):
|
|
|
305
296
|
inits[name].val = None
|
|
306
297
|
inits[name].valType = 'extendedStr'
|
|
307
298
|
else:
|
|
308
|
-
inits[name].val =
|
|
299
|
+
inits[name].val = None
|
|
309
300
|
inits[name].valType = 'code'
|
|
310
301
|
|
|
311
302
|
# is constant so don't touch the parameter value
|
|
@@ -393,6 +384,8 @@ def getInitVals(params, target="PsychoPy"):
|
|
|
393
384
|
elif name == 'allowedKeys':
|
|
394
385
|
inits[name].val = "[]"
|
|
395
386
|
inits[name].valType = 'code'
|
|
387
|
+
elif name == "deviceLabel":
|
|
388
|
+
inits[name].valType = "device"
|
|
396
389
|
else:
|
|
397
390
|
# if not explicitly handled, default to None
|
|
398
391
|
inits[name].val = "None"
|
|
@@ -13,6 +13,7 @@ from xml.etree.ElementTree import Element
|
|
|
13
13
|
|
|
14
14
|
from psychopy import prefs
|
|
15
15
|
from psychopy.constants import FOREVER
|
|
16
|
+
from psychopy.experiment.devices import DeviceMixin
|
|
16
17
|
from ..params import Param
|
|
17
18
|
from psychopy.experiment.utils import canBeNumeric
|
|
18
19
|
from psychopy.experiment.utils import CodeGenerationException
|
|
@@ -42,6 +43,9 @@ class BaseComponent:
|
|
|
42
43
|
validatorClasses = []
|
|
43
44
|
# hide this Component in Builder view?
|
|
44
45
|
hidden = False
|
|
46
|
+
# are there any known legacy params for this Component?
|
|
47
|
+
# these will be removed & warnings ignored on experiment load
|
|
48
|
+
legacyParams = []
|
|
45
49
|
|
|
46
50
|
def __init__(self, exp, parentName, name='',
|
|
47
51
|
startType='time (s)', startVal='',
|
|
@@ -67,7 +71,7 @@ class BaseComponent:
|
|
|
67
71
|
msg = _translate(
|
|
68
72
|
"Name of this Component (alphanumeric or _, no spaces)")
|
|
69
73
|
self.params['name'] = Param(name,
|
|
70
|
-
valType='code', inputType="
|
|
74
|
+
valType='code', inputType="name", categ='Basic',
|
|
71
75
|
hint=msg,
|
|
72
76
|
label=_translate("Name"))
|
|
73
77
|
|
|
@@ -1373,12 +1377,10 @@ class BaseComponent:
|
|
|
1373
1377
|
return "thisExp"
|
|
1374
1378
|
|
|
1375
1379
|
|
|
1376
|
-
class BaseDeviceComponent(BaseComponent):
|
|
1380
|
+
class BaseDeviceComponent(BaseComponent, DeviceMixin):
|
|
1377
1381
|
"""
|
|
1378
1382
|
Base class for most components which interface with a hardware device.
|
|
1379
1383
|
"""
|
|
1380
|
-
# list of class strings (readable by DeviceManager) which this component's device could be
|
|
1381
|
-
deviceClasses = []
|
|
1382
1384
|
|
|
1383
1385
|
def __init__(
|
|
1384
1386
|
self, exp, parentName,
|
|
@@ -1404,22 +1406,9 @@ class BaseDeviceComponent(BaseComponent):
|
|
|
1404
1406
|
saveStartStop=saveStartStop, syncScreenRefresh=syncScreenRefresh,
|
|
1405
1407
|
disabled=disabled
|
|
1406
1408
|
)
|
|
1407
|
-
#
|
|
1408
|
-
self.
|
|
1409
|
-
|
|
1410
|
-
)
|
|
1411
|
-
# --- Device params ---
|
|
1412
|
-
self.order += [
|
|
1413
|
-
"deviceLabel"
|
|
1414
|
-
]
|
|
1415
|
-
# label to refer to device by
|
|
1416
|
-
self.params['deviceLabel'] = Param(
|
|
1417
|
-
deviceLabel, valType="str", inputType="single", categ="Device",
|
|
1418
|
-
label=_translate("Device label"),
|
|
1419
|
-
hint=_translate(
|
|
1420
|
-
"A label to refer to this Component's associated hardware device by. If using the "
|
|
1421
|
-
"same device for multiple components, be sure to use the same label here."
|
|
1422
|
-
)
|
|
1409
|
+
# add device stuff
|
|
1410
|
+
self.addDeviceParams(
|
|
1411
|
+
defaultLabel=deviceLabel
|
|
1423
1412
|
)
|
|
1424
1413
|
|
|
1425
1414
|
|
|
@@ -87,7 +87,7 @@ class ButtonComponent(BaseVisualComponent):
|
|
|
87
87
|
label=_translate("Run once per click")
|
|
88
88
|
)
|
|
89
89
|
self.params['callback'] = Param(
|
|
90
|
-
callback, valType='extendedCode', inputType="
|
|
90
|
+
callback, valType='extendedCode', inputType="code", allowedTypes=[], categ='Basic',
|
|
91
91
|
updates='constant',
|
|
92
92
|
hint=_translate("Code to run when button is clicked"),
|
|
93
93
|
label=_translate("Callback function"))
|