Skip to content

Commit 198659f

Browse files
committed
refactor(ratings): extract screen-flow helpers out of __main__
1 parent 49e3ea5 commit 198659f

2 files changed

Lines changed: 94 additions & 75 deletions

File tree

src/mid_det/ratings/__main__.py

Lines changed: 13 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
core.checkPygletDuringWait = False
2020

2121
from psychopy import visual
22-
from psychopy.hardware import keyboard
2322
from psyexp_core import rundir, screen
2423
from psyexp_core.keyboard import (
2524
KEYBOARD_BACKEND,
@@ -33,74 +32,13 @@
3332
from mid_det.io import recording
3433
from mid_det.ratings import core as rcore
3534
from mid_det.ratings import display as rdisplay
35+
from mid_det.ratings import flow
3636
from mid_det.ratings.setup_wizard import run_ratings_wizard
3737

3838
_PACKAGE_DIR = Path(__file__).resolve().parent # src/mid_det/ratings/
3939
_PROJECT_ROOT = _PACKAGE_DIR.parent.parent.parent # project root
4040
_TEXT_DIR = _PROJECT_ROOT / "text"
4141

42-
# Slider control keys (MATLAB parity): 1 = left, 2 = right, 3 = select/advance.
43-
_KEY_LEFT = "1"
44-
_KEY_RIGHT = "2"
45-
_KEY_SELECT = "3"
46-
_ADVANCE_KEYS = [_KEY_LEFT, _KEY_RIGHT, _KEY_SELECT]
47-
# Instruction/text pages advance on button 1 only (forward-only, no going back).
48-
_PAGE_ADVANCE_KEYS = [_KEY_LEFT]
49-
50-
51-
def _load_instruction_pages() -> list[str]:
52-
path = _TEXT_DIR / "instructions_ratings.txt"
53-
pages: list[str] = []
54-
with open(path) as f:
55-
for line in f:
56-
stripped = line.rstrip()
57-
if stripped:
58-
pages.append(stripped)
59-
return pages
60-
61-
62-
def _show_text_page(
63-
win: visual.Window,
64-
kb: keyboard.Keyboard | None,
65-
text_stim: visual.TextStim,
66-
hint_stim: visual.TextStim,
67-
text: str,
68-
) -> None:
69-
text_stim.text = text
70-
text_stim.draw()
71-
hint_stim.draw()
72-
win.flip()
73-
wait_for_key(kb, _PAGE_ADVANCE_KEYS, quit_keys=config.QUIT_KEYS)
74-
75-
76-
def _run_slider(
77-
win: visual.Window,
78-
kb: keyboard.Keyboard | None,
79-
stim: rdisplay.RatingStimuli,
80-
scale: str,
81-
cue: rcore.RatingCue | None,
82-
) -> int:
83-
"""Run one slider interaction; return the selected position (1..N_ELS)."""
84-
pos = rcore.START_SLIDEPOS[scale]
85-
rdisplay.draw_scale(stim, scale, pos, cue)
86-
win.flip()
87-
while True:
88-
key = wait_for_key(kb, _ADVANCE_KEYS, quit_keys=config.QUIT_KEYS)
89-
if key == _KEY_LEFT:
90-
pos = rcore.clamp_slider(pos, -1)
91-
elif key == _KEY_RIGHT:
92-
pos = rcore.clamp_slider(pos, +1)
93-
elif key == _KEY_SELECT:
94-
return pos
95-
rdisplay.draw_scale(stim, scale, pos, cue)
96-
win.flip()
97-
98-
99-
def _show_fixation(win: visual.Window, stim: rdisplay.RatingStimuli) -> None:
100-
rdisplay.draw_fixation(stim)
101-
win.flip()
102-
core.wait(0.5)
103-
10442

10543
def run() -> None:
10644
# Select the keyboard backend before any Keyboard is built.
@@ -159,32 +97,32 @@ def run() -> None:
15997
pos=(0, -0.38), height=1.0 / 28, color="white", autoLog=False,
16098
)
16199

162-
pages = _load_instruction_pages()
100+
pages = flow.load_instruction_pages(_TEXT_DIR)
163101
# pages: 0=intro, 1=valence, 2=arousal, 3=independence, 4=final
164102

165103
# ── INSTRUCTIONS + PRACTICE DEMOS ────────────────────────────────────────
166104
if show_instructions:
167-
_show_text_page(win, kb, instr_text, instr_hint, pages[0])
168-
_show_text_page(win, kb, instr_text, instr_hint, pages[1])
169-
_run_slider(win, kb, stim, "valence", cue=None) # valence practice demo
170-
_show_text_page(win, kb, instr_text, instr_hint, pages[2])
171-
_run_slider(win, kb, stim, "arousal", cue=None) # arousal practice demo
172-
_show_text_page(win, kb, instr_text, instr_hint, pages[3])
105+
flow.show_text_page(win, kb, instr_text, instr_hint, pages[0])
106+
flow.show_text_page(win, kb, instr_text, instr_hint, pages[1])
107+
flow.run_slider(win, kb, stim, "valence", cue=None) # valence practice demo
108+
flow.show_text_page(win, kb, instr_text, instr_hint, pages[2])
109+
flow.run_slider(win, kb, stim, "arousal", cue=None) # arousal practice demo
110+
flow.show_text_page(win, kb, instr_text, instr_hint, pages[3])
173111

174112
# Final "press 3 to select" page is always shown (MATLAB inst5).
175-
_show_text_page(win, kb, instr_text, instr_hint, pages[4])
113+
flow.show_text_page(win, kb, instr_text, instr_hint, pages[4])
176114

177115
# ── RATING TRIALS ────────────────────────────────────────────────────────
178-
_show_fixation(win, stim)
116+
flow.show_fixation(win, stim)
179117
results: list[dict] = []
180118
for cue in rcore.RATING_CUES:
181-
valence = _run_slider(win, kb, stim, "valence", cue)
182-
arousal = _run_slider(win, kb, stim, "arousal", cue)
119+
valence = flow.run_slider(win, kb, stim, "valence", cue)
120+
arousal = flow.run_slider(win, kb, stim, "arousal", cue)
183121
results.append({
184122
"polarity": cue.polarity, "magnitude": cue.magnitude,
185123
"valence": valence, "arousal": arousal,
186124
})
187-
_show_fixation(win, stim)
125+
flow.show_fixation(win, stim)
188126

189127
# ── WRITE CSV ────────────────────────────────────────────────────────────
190128
# (manifest.json was already written to run_dir at startup)

src/mid_det/ratings/flow.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
2+
Screen-flow helpers for the cue-ratings survey: text pages, the slider
3+
interaction loop, and the inter-trial fixation. These own the draw + key-poll
4+
loops (I/O + response logic), keeping mid_det.ratings.display a pure draw layer
5+
and mid_det.ratings.__main__ a thin orchestrator.
6+
7+
Ported from MATLAB RunRatings.m.
8+
"""
9+
from __future__ import annotations
10+
11+
from pathlib import Path
12+
13+
from psychopy import core, visual
14+
from psychopy.hardware import keyboard
15+
from psyexp_core.keyboard import wait_for_key
16+
17+
from mid_det import config
18+
from mid_det.ratings import core as rcore
19+
from mid_det.ratings import display as rdisplay
20+
21+
# Slider control keys (MATLAB parity): 1 = left, 2 = right, 3 = select/advance.
22+
KEY_LEFT = "1"
23+
KEY_RIGHT = "2"
24+
KEY_SELECT = "3"
25+
ADVANCE_KEYS = [KEY_LEFT, KEY_RIGHT, KEY_SELECT]
26+
# Instruction/text pages advance on button 1 only (forward-only, no going back).
27+
PAGE_ADVANCE_KEYS = [KEY_LEFT]
28+
29+
30+
def load_instruction_pages(text_dir: Path) -> list[str]:
31+
path = text_dir / "instructions_ratings.txt"
32+
pages: list[str] = []
33+
with open(path) as f:
34+
for line in f:
35+
stripped = line.rstrip()
36+
if stripped:
37+
pages.append(stripped)
38+
return pages
39+
40+
41+
def show_text_page(
42+
win: visual.Window,
43+
kb: keyboard.Keyboard | None,
44+
text_stim: visual.TextStim,
45+
hint_stim: visual.TextStim,
46+
text: str,
47+
) -> None:
48+
text_stim.text = text
49+
text_stim.draw()
50+
hint_stim.draw()
51+
win.flip()
52+
wait_for_key(kb, PAGE_ADVANCE_KEYS, quit_keys=config.QUIT_KEYS)
53+
54+
55+
def run_slider(
56+
win: visual.Window,
57+
kb: keyboard.Keyboard | None,
58+
stim: rdisplay.RatingStimuli,
59+
scale: str,
60+
cue: rcore.RatingCue | None,
61+
) -> int:
62+
"""Run one slider interaction; return the selected position (1..N_ELS)."""
63+
pos = rcore.START_SLIDEPOS[scale]
64+
rdisplay.draw_scale(stim, scale, pos, cue)
65+
win.flip()
66+
while True:
67+
key = wait_for_key(kb, ADVANCE_KEYS, quit_keys=config.QUIT_KEYS)
68+
if key == KEY_LEFT:
69+
pos = rcore.clamp_slider(pos, -1)
70+
elif key == KEY_RIGHT:
71+
pos = rcore.clamp_slider(pos, +1)
72+
elif key == KEY_SELECT:
73+
return pos
74+
rdisplay.draw_scale(stim, scale, pos, cue)
75+
win.flip()
76+
77+
78+
def show_fixation(win: visual.Window, stim: rdisplay.RatingStimuli) -> None:
79+
rdisplay.draw_fixation(stim)
80+
win.flip()
81+
core.wait(0.5)

0 commit comments

Comments
 (0)