Skip to content

refactor: consume psyexp-core for shared experiment infrastructure#4

Open
ericwang401 wants to merge 16 commits into
mainfrom
refactor/consume-psyexp-core
Open

refactor: consume psyexp-core for shared experiment infrastructure#4
ericwang401 wants to merge 16 commits into
mainfrom
refactor/consume-psyexp-core

Conversation

@ericwang401

@ericwang401 ericwang401 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

What

Refactors mid_det to consume the shared psyexp-core harness (pinned by git tag v0.5.0) instead of carrying its own copies of generic experiment plumbing. mid_det now keeps only MID-specific logic; the common infrastructure lives in one versioned place, and every run records psyexp_core_version for reproducibility.

Generic infra → psyexp-core

  • Screen setup + ScreenDiagnosticspsyexp_core.screen / psyexp_core.diagnostics
  • Timestamped run dirs → psyexp_core.rundir.make_run_dir
  • CsvWriter base → psyexp_core.recording.CsvWriter
  • Manifest writers delegate to psyexp_core.manifest.write_manifest (runs now record psyexp_core_version)
  • Setup-wizard primitives → psyexp_core.wizard (the MID-specific frame-stepping RT prompt stays here)
  • Instruction pager → psyexp_core.instructions.page_through

Full keyboard adoption

  • configure_psychopy_backend + build_keyboard for setup (replacing manual prefs/backend asserts)
  • get_keys / wait_for_keys / clear_events for name reads
  • New timed get_presses / reset_clock_on_flip / clock_time in the timing-critical response window — no more direct psychopy.hardware.keyboard reads, while preserving the exact frame-counting RT logic

io/bootstrap.py keeps only the task-specific SessionInfo.

Why

The same plumbing was duplicated across this task and heat-task. Centralizing it in psyexp-core removes the duplication, pins a reproducible shared version per run, and lets the timing-critical MID response window read frame-accurate RT through the shared abstraction (enabled by the new timed-keyboard API added in psyexp-core v0.5.0).

Notes

  • Net −288 lines of task code.
  • All 97 tests pass against the git-pinned psyexp-core v0.5.0 (uv run pytest).
  • Manifest output verified structurally identical plus the new top-level psyexp_core_version.
  • AGENTS.md documents the git-tag pin + editable-overlay co-dev workflow.
  • Depends on psyexp-core#2 (merged; tagged v0.5.0).

Update (71ff9fb): this PR now also standardizes the keymap schema, resolves the ratings todos, and extracts the shared wait_for_key / check_quit into psyexp-core. It therefore also depends on psyexp-core#4 (v0.9.0)ratings/__main__.py imports those helpers from core. Finalize after that release: bump psyexp-core>=0.9.0 and uv lock --upgrade-package psyexp-core. See docs/STANDARDIZATION.md and the comment below.

Replace mid_det's own copies of generic plumbing with the shared
psyexp-core harness (pinned by git tag v0.5.0):

- screen setup + ScreenDiagnostics -> psyexp_core.screen / .diagnostics
- timestamped run dirs -> psyexp_core.rundir.make_run_dir
- CsvWriter base -> psyexp_core.recording.CsvWriter
- manifest writers delegate to psyexp_core.manifest.write_manifest
  (runs now record psyexp_core_version)
- setup-wizard primitives -> psyexp_core.wizard
- instruction pager -> psyexp_core.instructions.page_through
- full keyboard adoption: configure_psychopy_backend / build_keyboard
  for setup, and get_keys / wait_for_keys / clear_events plus the new
  timed get_presses / reset_clock_on_flip / clock_time in the
  timing-critical response window

io/bootstrap.py keeps only the task-specific SessionInfo. All 97 tests
pass against the editable psyexp-core overlay.

uv.lock regen is deferred until psyexp-core v0.5.0 is tagged.
…from lock

Two CI fixes:

- phases.py: the per-frame hot-loop reads (_poll_hotkeys, run_fixation
  early-press) used the backend-switching get_keys, whose event-backend
  branch imports PsychoPy. CI runs without PsychoPy/psychtoolbox (event
  backend), so the trial-boundary tests' fake keyboard tripped that import.
  mid_det is PTB-only and these loops always hold a real Keyboard, so read
  it directly via the PsychoPy-free get_presses instead.

- tests.yml: install psyexp-core --no-deps (its light modules are
  PsychoPy-free) with the pinned source read from uv.lock via 'uv export'
  rather than a hardcoded git ref in the workflow.
The parity test parametrized into ~17 cases, each spawning a fresh
octave/matlab subprocess — slow startup per case and, on macOS, incessant
dock churn as the engine app opened/closed. Batch every case into one
program (results delimited by '===') computed in a single session-scoped
launch. Same parity coverage; engine starts once (verified) and the file
runs in ~0.5s.
- pin psyexp-core>=0.7.0 from PyPI; regenerate uv.lock (was git@v0.5.0)
- CI: match the new uv export format (== not @) and install pydantic,
  which core 0.7.0 now imports for its manifest models
- tests: add monitor field to the ScreenDiagnostics fake (core 0.7.0)
- docs: describe PyPI consumption + uv lock --upgrade-package flow
Both entry points now call screen.prompt_screen() before opening the
window and pass the chosen index to setup_screen(), so on multi-monitor
rigs the operator picks the display (auto-returns 0 when there's only
one). The selected monitor's OS-level detail (name/refresh/size/HiDPI)
flows into screen_diag.monitor and the run manifest via psyexp-core 0.7.0.
@ericwang401 ericwang401 self-assigned this Jun 29, 2026
uv.lock is authoritative, so any uv sync (including --inexact) reverts an
editable psyexp-core overlay to the locked PyPI version; only skipping the
sync (uv run --no-sync / UV_NO_SYNC=1) preserves it. Drop the false
'--inexact keeps the overlay' claim from AGENTS.md, docs/development.md and
the pyproject comment, and document the --no-sync flow plus the durable
[tool.uv.sources] + git skip-worktree alternative.

Add a justfile with sync/run/ratings/test and core-dev/core-run/core-test/
core-release/core-upgrade recipes wrapping the --no-sync flow.
Comment thread src/mid_det/ratings/__main__.py Outdated
…rd helpers

Keymaps (config.py): replace the str-valued KEYS_FMRI/KEYS_BEHAVIORAL + EXP_KEYS
with the heat-task-aligned list[str] schema — INSTRUCTION_KEYS / START_KEYS /
END_KEYS / QUIT_KEYS / OVERLAY_TOGGLE_KEYS / RESPONSE_KEYS — incl. numpad
variants. Route instructions, the start/end gates, and _poll_hotkeys through it;
drop the dead update_instr_keys and the vestigial fMRI/behavioral split.

Ratings: dedup _wait_keys onto psyexp_core.keyboard.wait_for_key, annotate kb as
Keyboard|None, and replace core.wait(1.5) with an end-screen "press <key> to exit"
wait (mirrors heat-task). Resolves the three todos.

Lock: pin psyexp-core 0.8.0 — fixes test_ratings_manifest_written, which failed
because HEAD's lock kept 0.7.0 after the session_started_at rename bumped the
pyproject floor to >=0.8.0 without re-locking.

Docs: add docs/STANDARDIZATION.md (keymap schema, core-extraction migration plan,
run() flexibility a/b via contextlib.ExitStack, hand-rolled-vs-library appendix).

The ratings entry point now imports the keyboard helpers from psyexp-core 0.9.0;
finalize the pin (>=0.9.0 + relock) after that release.
@ericwang401

Copy link
Copy Markdown
Contributor Author

Pushed 71ff9fb, extending this PR:

  • Keymaps standardized to the heat-task list[str] schema — INSTRUCTION_KEYS / START_KEYS / END_KEYS / QUIT_KEYS / OVERLAY_TOGGLE_KEYS / RESPONSE_KEYS, incl. numpad variants. Dead update_instr_keys and the vestigial fMRI/behavioral split removed.
  • Ratings todos resolved: the wait_for_key/heat-wait_for_key duplication (review comment) is deduped onto psyexp_core.keyboard.wait_for_key; kb typed Keyboard | None; end-screen "press <key> to exit" wait added (mirrors heat-task).
  • Failing test fixed: uv.lock pinned to psyexp-core 0.8.0 — test_ratings_manifest_written failed because HEAD's lock kept 0.7.0 after the session_started_at rename bumped the floor to >=0.8.0 without re-locking.
  • docs/STANDARDIZATION.md added: keymap schema, the core-extraction migration plan, run() flexibility via contextlib.ExitStack (§6), and a hand-rolled-vs-library appendix.

The keyboard helpers were extracted into psyexp-core (HAPNlab/psyexp-core#4, v0.9.0), so ratings/__main__.py now imports them from core. Finalize after 0.9.0 is published: bump psyexp-core>=0.9.0 and uv lock --upgrade-package psyexp-core.

Wrap the run body in a contextlib.ExitStack, registering the CSV writers,
logging, and window close as they are acquired. Previously the cleanup block ran
only on the happy path, so a quit key (core.quit -> SystemExit from _poll_hotkeys)
or a mid-run error skipped logging.flush()/win.close(). core.quit() is called
after the block so genuine errors still propagate with a traceback.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant