- Restored
TaskSettings.add_subinfo()to fall back to./outputs/humanwhensave_pathis unset or empty, while still creating explicit nested output directories. - Added pure-Python regression tests for
psyflow.sim.loader,psyflow.sim.logging,psyflow.utils.trials, andTaskSettings.add_subinfo().
python -m unittest discover -s tests -vpassed.
- Made
psyflow.utilslazy-load PsychoPy-dependent helpers so importingpsyflow.utilsno longer eagerly importsdisplayorexperiment. - Removed the duplicate sim-side deadline helper and reused
psyflow.utils.trials.resolve_deadline()frompsyflow.sim.context_helpers. - Added smoke coverage for the import boundary and sim deadline resolution.
- Addresses issue #21:
utils/__init__.py eager psychopy imports block cross-module reuse.
python -m unittest discover -s tests -p "test_smoke.py" -vpassed.python -m unittest discover -s tests -p "test_sim_golden.py" -vpassed.
- Removed in-repo
skills/task-buildfrompsyflow. - Extracted
task-buildas a standalone canonical repository ate:/Taskbeacon/skills/task-build. - No backward-compatibility shim is kept in
psyflow; skill ownership now lives outside the framework repo.
git statusinpsyflowconfirms onlyskills/task-build/*removals plus this changelog update.
- Moved condition-weight resolution into
TaskSettings:- added
TaskSettings.resolve_condition_weights()inpsyflow/TaskSettings.py.
- added
- Removed
psyflow.utils.trials.resolve_condition_weightsand all related exports. - Updated runtime/template standards to use
settings.resolve_condition_weights():- cookiecutter
main.pyand config comments now point to theTaskSettingsmethod.
- cookiecutter
- Updated contracts and task-build skill assets/checklists to standardize this policy.
- Updated condition-weight unit tests to exercise the
TaskSettingsmethod directly.
python -m unittest tests.test_condition_weightspassed.python -m unittest tests.test_validatepassed.python -m psyflow.validate "psyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}"passed withFAIL=0.
- Added framework-level condition-weight resolver:
psyflow.utils.trials.resolve_condition_weights(...)- exported via
psyflow.resolve_condition_weightsandpsyflow.utils.
- Standardized config/contract semantics for weighted condition generation:
task.condition_weightsis optional and validated when provided.- mapping/list formats must align with
task.conditions; positive numeric weights required. - omitted/null
task.condition_weightsmeans even/default generation unless task code documents custom generation.
- Updated cookiecutter template to include optional
task.condition_weightsand pass resolved weights into default condition scheduler. - Updated task-build skill docs/checklists/templates to enforce explicit
task.condition_weightswhen weighted generation is used. - Added unit coverage for weight resolver behavior in
tests/test_condition_weights.py.
python -m unittest tests.test_condition_weightspassed.python -m psyflow.validate "psyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}"passed withFAIL=0.
- Fixed CI contract validation failure on the cookiecutter template by adding required reference artifacts under:
psyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/references/references.yamlreferences.mdparameter_mapping.mdstimulus_mapping.mdtask_logic_audit.md
- All added template files follow the
reference_artifactscontract headings/table-column requirements and avoid forbidden unresolved markers.
python -m psyflow.validate 'psyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}'now passes withFAIL=0.
- Added enforceable localization-safe runtime policy so participant-facing text is config-driven, not hardcoded in
src/run_trial.py.- Extended responder-context contract with hardcoded-text guards (
TextStim/TextBoxliteral text,.setText('...'), and.text='...'assignment checks). - Implemented AST-based checks in
psyflow/validate.py.
- Extended responder-context contract with hardcoded-text guards (
- Added a new reference-artifact contract:
psyflow/contracts/v0.1.0/reference_artifacts.yaml- wired into contract
manifest.yaml - validator now checks required reference files, required headings/table columns, and
references.yamlrequired keys/paper fields.
- Updated contract docs/patterns to reflect localization + reference-artifact standards:
psyflow/contracts/v0.1.0/README.mdpsyflow/contracts/v0.1.0/run_trial_pattern.md
- Upgraded
skills/task-buildto align with these standards:- updated
SKILL.mdwith config-first localization and reference-artifact schema requirements; - added
references/reference_artifact_contract.md; - added mapping templates:
assets/templates/parameter_mapping_template.mdassets/templates/stimulus_mapping_template.md
- strengthened
scripts/check_task_standard.pyandscripts/build_reference_bundle.pyto emit/check standardized reference artifact formats.
- updated
- Expanded validator regression coverage in
tests/test_validate.py:- minimal compliant fixture now includes full reference artifacts;
- added failure test for hardcoded participant-facing runtime text.
python -m py_compile psyflow/validate.py skills/task-build/scripts/check_task_standard.py skills/task-build/scripts/build_reference_bundle.pypassed.python -m unittest -q tests.test_validatepassed.
- Improved
skills/task-buildguidance with generic anti-overdesign and PsyFlow-first decision rules (docs/templates only; no new checker enforcement). - Updated
skills/task-build/SKILL.mdto clarify:- start from the simplest PsyFlow-native path;
- prefer built-in block condition generation before custom generators;
- treat
utils.pyas optional and scoped to real framework gaps; - prefer config-driven participant-facing text and response mappings when appropriate;
- prefer auditable
main.py/run_trial.pypatterns with minimal fallback logic.
- Expanded
skills/task-build/assets/templates/task_logic_audit_template.mdwith architecture-decision fields for:- condition generation method and justification,
- runtime determinism/reproducibility,
- response key source,
utils.py/controller usage,- fallback/compatibility scope.
- Updated reusable reference docs to reinforce generic design heuristics:
skills/task-build/references/psyflow_task_standard_checklist.mdskills/task-build/references/task_development_experience.mdso future task builds remain simpler, more PsyFlow-aligned, and easier to audit.
- Decoupled
psyflow.sim.context_helpers.set_trial_contextfrom task-specific controller logic.- Automatically resolves sequence durations via
max()internally, eliminating the need for boilerplate_deadline_sfunctions in task code. - Removed controller dependency for
trial_idresolution fromcontext_helpers.
- Automatically resolves sequence durations via
- Added generic trial increment tracking natively to
psyflowvianext_trial_id()to standardize sequential trial IDs across tasks.
- Hardened
skills/task-buildto enforce logic-first development and non-placeholder stimulus implementation. - Updated
skills/task-build/SKILL.mdworkflow:- added mandatory Phase 0: Paradigm Logic Audit before coding;
- now requires
references/task_logic_audit.md(seeded from template); - clarified hard bans on placeholder/template participant text and raw condition-token displays;
- added explicit multi-option/multi-stimulus layout requirements and QA readability checks.
- Added reusable audit scaffold:
skills/task-build/assets/templates/task_logic_audit_template.md.
- Strengthened task standard checker (
skills/task-build/scripts/check_task_standard.py):- now requires
references/task_logic_audit.md; - fails placeholder cue/target patterns (e.g.,
CUE: ...,TARGET: ...); - fails common template stock text in participant stimuli (e.g., generic response/continue/quit text);
- fails participant-facing text that is only raw condition labels.
- now requires
- Updated policy/checklist references:
skills/task-build/references/psyflow_task_standard_checklist.mdskills/task-build/references/stimulus_fidelity_policy.mdto align with the stricter stimulus and layout standards.
python C:/Users/frued/.codex/skills/.system/skill-creator/scripts/quick_validate.py C:/Users/frued/.codex/skills/task-buildpassed.python -m py_compile skills/task-build/scripts/check_task_standard.pypassed.- Spot-check on a placeholder-heavy task now fails as expected with explicit stimulus-fidelity violations.
- Updated responder context contract/validator to support truly generic, task-specific phase labels via phase-token checks instead of fixed phase values.
- Extended template compliance so CI validation passes on the cookiecutter task scaffold:
- README now includes required section heading
## 4. Methods (for academic publication)andLanguagemetadata row. - Template
.gitignorenow includes recommended housekeeping ignores (.pytest_cache,.mypy_cache, virtualenv dirs). - Template
CHANGELOG.mdnow includes recommendedChangedandFixedsections. - Template run-trial phase labels updated to generic task-ready names (
pre_response_fixation,response_window). - Added template
outputs/.gitkeepto satisfy recommended path checks.
- README now includes required section heading
python -m psyflow.validate 'psyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}'now passes withFAIL=0.
- Generalized task contracts to remove MID-specific phase coupling in responder checks.
responder_contextnow validates task-agnostic response phases viarequired_context_phase_values_any.- Removed fixed
cue -> anticipation -> target -> feedbackstage-order requirement from contract docs.
- Enhanced validator logic in
psyflow/validate.py:- added support for
required_context_phase_values_any,forbidden_context_phase_values,required_stage_tokens_any, andrecommended_stage_tokens_any; - improved README contract validation with required section-heading checks and subsection recommendations.
- added support for
- Updated contract documentation and patterns to clarify that phase/stage labels are task-defined, not MID-only.
- Improved
StimBank.get_and_format(...)reliability:- now rebuilds formatted text stimuli from template definitions to preserve layout properties,
- surfaces clearer errors for missing format keys and rebuild failures.
- Updated scaffolding quality:
- template sampler responder now includes lifecycle hooks and RNG fallback compatibility,
- task README-generation prompt refreshed for standardized metadata/flow/config/method sections.
python -m py_compile psyflow/validate.pypassed.python -m psyflow.validate e:\\Taskbeacon\\T000021-drifting-double-banditpassed (13/13).
- Added a new build-automation skill package at
skills/task-buildfor end-to-end PsyFlow/TAPS task construction from literature evidence. - Implemented a decision-complete workflow in
skills/task-build/SKILL.mdcovering:- literature discovery and filtering,
- reference bundle generation,
- task standard alignment,
- placeholder asset generation,
- validate/qa/sim gate execution with retry loop,
- git commit/push publishing.
- Added curated high-impact journal reference list for selection policy enforcement:
skills/task-build/references/high_impact_psyneuro_journals.yaml.
- Added task-build reference guides:
literature_search_playbook.mdtask_param_inference_rules.mdpsyflow_task_standard_checklist.mdpublish_checklist.md
- Added task-build automation scripts:
scripts/select_papers.pyscripts/build_reference_bundle.pyscripts/check_task_standard.pyscripts/run_gates.pyscripts/publish_task.pyscripts/create_placeholder_assets.py
- Added reusable task templates under:
skills/task-build/assets/templates/*(README, CHANGELOG, taskbeacon, and split config templates).
- Added/updated skill agent metadata:
skills/task-build/agents/openai.yaml
quick_validate.pypasses forskills/task-build.- Script syntax checks pass (
py_compile) for all task-build scripts. - Dry-run completed on sandbox copy of
T000010-rest:- paper selection succeeded,
- reference bundle generated,
- standard check passed,
- gate runner passed,
- publish script verified with
--no-push.
- Split simulation config standards into two explicit profiles:
config/config_scripted_sim.yamlfor built-in scripted responder runsconfig/config_sampler_sim.yamlfor task-specific sampler responders
- Updated validator contract manifest and checks to validate both sim profiles independently.
- Strengthened config contract rules:
- base
config.yamlnow forbids embeddedqa/simsections config_qa.yamlnow requires full base task sections and forbidssim- added smoke-profile checks (shorter-than-base, condition coverage, basic trial/block consistency)
- base
- Extended validator value-spec engine with:
disallowedlist support- string
patternmatching - generic
forbidden_nested_keysenforcement
- Updated responder plugin contract to target sampler config (
config_sampler_sim.yaml) and explicit custom responder path checks. - Updated template required files contract to require:
config/config.yamlconfig/config_qa.yamlconfig/config_scripted_sim.yamlconfig/config_sampler_sim.yaml
- Reworked cookiecutter config files for readability and auditability:
- section headers/comments, cleaner spacing, explicit mode scope per file
- base config now human-only
- qa config now qa-only and short smoke profile
- removed legacy monolithic
config/config_sim.yaml - added new
config/config_scripted_sim.yamlandconfig/config_sampler_sim.yaml
- Added
responders/folder in template with starter sampler responder scaffold:responders/task_sampler.pyresponders/__init__.pyresponders/README.md
- Updated template
main.pydefault sim config mapping toconfig/config_scripted_sim.yaml.
- Updated task launcher
simshortcut default config toconfig/config_scripted_sim.yaml.
- Expanded validator tests to cover:
- split sim config files
- responder folder/class checks
- stricter smoke-profile expectations for qa/sim config profiles.
- Added versioned task contracts at
psyflow/contracts/v0.1.0for standardized psyflow/TAPS task development and audit. - Added new validator CLI:
psyflow-validate <task_path>with per-contract PASS/WARN/FAIL feedback and actionable suggestions. - Added contract adoption tracking in task metadata via
taskbeacon.yaml -> contracts.psyflow_taps. - Updated cookiecutter template to include
taskbeacon.yaml,CHANGELOG.md, andassets/README.mdso new tasks match required structure by default. - Expanded contract coverage with explicit checks for:
.gitignoretask artifact rules (outputs/*/data/*migration-safe),- stimulus schema and asset-backed path conventions (
image/movie/soundfromassets/), - responder/sampler plugin standards (
sim.responder.type, localresponders/class checks).
- Added stricter config validation semantics across contracts (
mandatory/optional/recommendedkeys and value specs). - Updated CI to install minimal validator dependency (
pyyaml) and run contract validation smoke checks against the task template. - Added validator regression tests for asset-backed stimuli and responder contract failures.
psyflow/validate.pypsyflow/contracts/v0.1.0/*tests/test_validate.py
- New console script:
psyflow-validate - Included
contracts/**/*in package data.
- Standardized responder configuration on
responder.type(built-ins or import path); removedresponder.kind/responder.classbehavior. - Strengthened
psyflow-qainto a strict gate:qa.acceptance_criteriais now required inconfig/config_qa.yaml.- static + runtime + trace/events validation remains bundled in one command.
- Added automatic post-QA metadata refresh on pass:
- promotes
taskbeacon.yamlmaturity(no downgrade), - updates README maturity badge to a cleaner flat-square style.
- promotes
- Hardened README badge update for non-UTF8 files via encoding fallback.
- Updated templates and MID example configs to use
typeconsistently (triggers.driver.type,sim.responder.type). - Updated docs/tests for the new QA/sim contract and launcher behavior.
psyflow/task_launcher.pypsyflow/sim/loader.pypsyflow/sim/context.pytests/test_task_launcher.pytests/test_responder_contract.pytests/test_sim_golden.pydocs/tutorials/cli_usage.mddocs/tutorials/qa_runner.mdpsyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/config/config.yamlpsyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/config/config_qa.yamlpsyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/config/config_sim.yamlpyproject.toml
- Removed
qaandsimsubcommands from root CLI (psyflownow exposesinitonly). - Standardized task execution on explicit task entrypoint arguments (
python main.py [human|qa|sim] --config ...). - Removed framework emphasis on env-driven QA/sim wrappers from docs; documentation now points to task-level mode/config flow.
- Removed
psyflow qa ...command. - Removed
psyflow sim ...command. - QA/sim runs should now be started from the task script directly.
psyflow/cli.pytests/test_cli_root.pydocs/tutorials/cli_usage.mddocs/tutorials/qa_runner.md
- Added a sampler-ready simulation architecture with a stable responder plugin contract (
SessionInfo,Observation,Action,Feedback, lifecycle hooks). - Added a centralized responder adapter and policy layer (
strict|warn|coerce) used byStimUnit.capture_response()andStimUnit.wait_and_continue()so injected responses flow through one validation seam. - Added deterministic simulation plumbing (seed/session/rng), structured JSONL simulation audit logs, and replay helpers.
- Added plugin loader/config support (built-ins + external import-path responders) and a demo external responder.
- Moved runtime context/session plumbing under
psyflow.sim.contextso responder/runtime logic stays insim. - Updated template MID task and T000006 MID task to standardize trial context fields (
trial_id,phase,deadline_s,valid_keys,condition_id,task_factors) for simulation readiness. - Added contract and determinism tests for responder plugins and sim runs.
- CLI redesign: moved to one root command
psyflowwith subcommandsinit,qa,simfor compact, terminal-friendly usage. - Breaking packaging change: removed separate script entrypoints (
psyflow-init,psyflow-qa,psyflow-sim) in favor ofpsyflow. - Added clearer CLI terminal summaries for QA/sim runs (status + artifact paths).
- Neutralized runtime env names (
PSYFLOW_RESPONDER_*,PSYFLOW_OUTPUT_DIR,PSYFLOW_*timing knobs) to remove QA-specific naming in simulation paths. - Added shared runtime command executor helpers used by both
qaandsimcommands.
- CLI entrypoints changed:
- old:
psyflow-init,psyflow-qa,psyflow-sim - new:
psyflow init,psyflow qa,psyflow sim
- old:
psyflow.__init__CLI export changed fromclimaintocli_main.
psyflow/sim/contracts.pypsyflow/sim/adapter.pypsyflow/sim/loader.pypsyflow/sim/logging.pypsyflow/sim/rng.pypsyflow/sim/context_helpers.pypsyflow/sim/context.pypsyflow/sim/__init__.pypsyflow/sim_command.pyexamples/sim/demo_responder.pytests/test_responder_contract.pytests/test_sim_golden.pytests/test_sim_command.pytests/test_cli_root.py
psyflow/StimUnit.pypsyflow/sim/context.pypsyflow/qa/__init__.pypsyflow/qa_command.pypsyflow/sim_command.pypsyflow/cli.pypsyflow/commands/runtime.pypsyflow/utils/config.pypsyflow/__init__.pypyproject.tomlsetup.py
psyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/main.pypsyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/src/run_trial.pypsyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/config/config.yamlpsyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/acceptance_criteria.yamlREADME.mddocs/tutorials/cli_usage.mddocs/tutorials/qa_runner.mddocs/tutorials/getting_started.mddocs/tutorials/getting_started_cn.md
StimUnit.add_stim(...)now recognizes PsychoPy runtime audio backends (via_SoundBaseand resolved backend classes), fixing false rejections of valid sound stimuli.- Unsupported stimulus objects in
StimUnit.add_stim(...)now emit a warning and raise a clearerTypeErrorthat lists supported classes. TaskSettings.from_dict(...)was hardened:- validates that input config is a dict,
- passes only
init=Truedataclass fields into constructor, - validates
trial_per_block/trials_per_blockaliases for consistency, - enforces declared trials-per-block matches
ceil(total_trials / total_blocks)when provided.
initialize_exp(...)now defaults tosettings.screenwhenscreen_idis not explicitly passed.
psyflow/StimUnit.pypsyflow/TaskSettings.pypsyflow/utils/experiment.py
- Breaking cleanup: removed
TriggerSendercompatibility layer; trigger flow is nowTriggerRuntimeonly. - Added
initialize_triggers(...)bootstrap underpsyflow.io, returning an openedTriggerRuntime. - Added
TriggerRuntime.send(code, wait=True)for simple code-based trigger sends. StimUnitnow consumesruntime=directly and no longer supports legacytriggersender=fallback.- Utility cleanup: trigger bootstrap moved out of
utils; display helper module renamed toutils/display.py; serial-port alias cleanup. - Cookiecutter template updated to the runtime-first trigger pattern (including QA mock setup).
Files:
psyflow/TriggerSender.py(removed)psyflow/io/runtime.pypsyflow/io/trigger.pypsyflow/io/__init__.pypsyflow/StimUnit.pypsyflow/__init__.py
- Removed
TriggerSenderand its exports from the public package API. initialize_triggers(...)now builds driver +TriggerRuntime, callsruntime.open(), and returns the runtime directly.- Added
TriggerRuntime.send(...)as a convenience for immediate integer trigger sends. StimUnittrigger path is now runtime-only:- constructor uses
runtime=... - internal trigger emit path uses
runtime.emit(...) - legacy
triggersenderfallback path was removed.
- constructor uses
# before
from psyflow import TriggerSender
trigger_sender = TriggerSender(...)
trigger_sender.send(code)
StimUnit("trial", win, kb, triggersender=trigger_sender)
# after
from psyflow import initialize_triggers
trigger_runtime = initialize_triggers(cfg)
trigger_runtime.send(code)
StimUnit("trial", win, kb, runtime=trigger_runtime)Files:
psyflow/utils/ports.pypsyflow/utils/display.py(renamed fromhandy_display.py)psyflow/utils/__init__.pypsyflow/io/trigger.py(trigger bootstrap moved fromutils)
- Removed
list_serial_ports()alias; standardized onshow_ports(). - Renamed timing/display helper module to
utils/display.py(exported APIcount_downremains available). - Moved trigger bootstrap ownership into
psyflow.iofor clearer package boundaries.
Files:
psyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/main.pypsyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/src/run_trial.py
- Template now initializes triggers via
initialize_triggers(cfg). - QA mode uses
initialize_triggers(mock=True)(no hardware dependency). - Task code uses
trigger_runtime.send(...)and passesruntime=trigger_runtimeintoStimUnit. - Runtime is closed via
trigger_runtime.close()during teardown.
- Timing/response-stage fixes in
StimUnit(flip-synced stamps, RT consistency, close semantics, flip-locked offsets). - Trigger sending made safer for flip callbacks (
TriggerSender.send(..., wait=False)path). - New trigger architecture (EEG/fMRI-ready):
TriggerRuntime+ pluggable drivers (MockDriver,SerialDriver,FanoutDriver), withTriggerSenderkept as a thin compatibility wrapper. - Added QA tooling:
psyflow-qaCLI +psyflow.qahelpers for static contract checks and runtime trace/event validation with standardized artifacts underoutputs/qa/. - Added QA-mode scripted response injection in
StimUnit.capture_response()andStimUnit.wait_and_continue()(no PsychoPyKeyboardemulation), plus optional QA timing scaling and trigger planned/executed event logging. - Correctness fixes:
BlockUnit.summarize()no longer crashes;taps()locates templates correctly. - Packaging/import cleanup: Python >= 3.10 declared; LLM utilities removed (deps/docs/exports cleaned);
import psyflowis now lazy. - Docs hygiene: docs deployment now does a clean build; trigger/CLI tutorials updated to be copy-paste safe.
- Basic CI smoke checks added.
Files:
psyflow/qa/*psyflow/qa_cli.pypsyflow/StimUnit.pypyproject.tomlsetup.pydocs/tutorials/qa_runner.mddocs/index.rst
- Added a lightweight QA runner CLI:
psyflow-qa <task_dir> [--runtime-cmd \"python main.py\"].- Always runs static checks (config/acceptance contract lint).
- Writes machine-readable static artifacts:
outputs/qa/static_report.jsonoutputs/qa/contract_report.json
- If
--runtime-cmdis provided, runs the task in a subprocess withPSYFLOW_MODE=qaand validates:outputs/qa/qa_trace.csvagainstacceptance_criteria.yaml(required columns) + generic invariants.outputs/qa/qa_events.jsonlfor trigger planned vs executed mismatches (when present).
- Introduced a pure-Python
psyflow.qasubpackage (no PsychoPy imports) for:- acceptance criteria lint (
contract_lint) - static checks (
static_qa) - trace CSV validation (
validate_trace_csv) - QA event validation (
validate_events) - standardized failure taxonomy in
outputs/qa/qa_report.json. - optional static validation of
task.key_list(non-empty, subset ofallowed_keyswhen provided) and trigger-code sanity (int-or-null, uniqueness when present).
- acceptance criteria lint (
pyyamlis required only when loading YAML files from disk (load_yaml()); the QA modules remain importable in minimal environments (e.g., CI) without installing dependencies.
StimUnit.capture_response()andStimUnit.wait_and_continue()can now inject scripted responses when a QA context is active (mode=qa):- responder acts on an
Observationdict (stim descriptor fields are optional and pulled fromStimUnitstate when present). - no attempt is made to emulate PsychoPy's
KeyboardAPI globally.
- responder acts on an
- QA mode supports opt-in duration scaling with guardrails:
- default: scaling disabled
- never below one refresh interval
min_framesdefault 2 in QA mode- logs both nominal and scaled durations into state (
*_duration_nominal,*_duration_scaled) when scaling is enabled.
- In QA mode, flip-scheduled triggers are logged as:
trigger_planned(when scheduled)trigger_executed(when the send path runs)- JSONL stream:
outputs/qa/qa_events.jsonl(when QA context is active).
- The bundled task template now includes:
acceptance_criteria.yamlwith required columns and basic expectations.config/config.yamlso the scaffold runs out of the box.PSYFLOW_MODE=qasupport to skip GUI subject info and avoid hardware triggers.
Files:
psyflow/io/*psyflow/TriggerSender.pypsyflow/StimUnit.pydocs/tutorials/send_trigger.md
- Added
TriggerRuntime(timing semantics + audit logging) and driver abstractions:MockDriver: development/QA without hardwareSerialDriver: pyserial-backed byte writes (encoder lives in the driver)FanoutDriver: broadcast to multiple driversCallableDriver: wrap a custom send function
TriggerSenderis now a thin compatibility wrapper aroundTriggerRuntime. Existing code callingTriggerSender.send(code)continues to work.StimUnit.show()andStimUnit.capture_response()now emit triggers throughTriggerRuntimewhen available (automatically picked up viaTriggerSender.runtime), preserving flip-locked semantics via the runtime.
File: psyflow/StimUnit.py
StimUnit.run(terminate_on_response=True)now truly ends the stage immediately after the first valid response (or timeout) is registered.- Previously,
run()kept flipping until the timeout-derived window ended and only stopped drawing stimuli after a response, which could silently add blank time and made "trial end" ambiguous. run()now supports an explicit fixed-length window mode that keeps flipping for the full window duration even after a response/timeout is registered.
fixed_response_window: bool = False: IfTrue, the stage does not end early; it continues flipping until the window ends.post_response_display: str = "stimuli": Only used whenfixed_response_window=True. Values:"stimuli"(keep drawing) or"blank"(stop drawing) after a response/timeout is registered, until the window ends.max_duration: float | None = None: If provided, explicitly sets the stage window length (seconds) and overrides any timeout-derived duration.
- Priority 1:
max_duration(explicit window length). - Priority 2: maximum registered
on_timeout()duration. - Priority 3: if
fixed_response_window=Trueand no duration source exists, raiseValueError. - Priority 4: otherwise fall back to
5.0seconds (backward-compatible default).
- Response hooks fire only for the first valid response; subsequent keypresses do not trigger more hooks in the same
run()call. - Key events are still drained every frame to reduce spillover into later stages.
- Old "keep flipping but stop drawing after response" behavior can be replicated with
fixed_response_window=True,post_response_display="blank", and a defined window length (viamax_durationoron_timeout()hooks).
# End immediately on response (now matches terminate_on_response semantics)
StimUnit("trial", win, kb).add_stim(stim).run(terminate_on_response=True)
# Fixed 1.0s window; keep stimuli visible after response
StimUnit("trial", win, kb).add_stim(stim).run(
fixed_response_window=True,
post_response_display="stimuli",
max_duration=1.0,
)
# Fixed 1.0s window; blank after response (closest to the old implicit behavior)
StimUnit("trial", win, kb).add_stim(stim).run(
fixed_response_window=True,
post_response_display="blank",
max_duration=1.0,
)File: psyflow/StimUnit.py
- Onset timestamps are now evaluated at flip-time by scheduling a callback that reads clocks inside the
win.flip()callback, instead of passing pre-evaluated float values intowin.callOnFlip(...). - Added internal helper
StimUnit._stamp_onset(...)and updated these methods to use it:StimUnit.run()StimUnit.show()StimUnit.capture_response()StimUnit.wait_and_continue()
- In response-related methods,
kb.clearEventsandclock.resetare now scheduled on the flip (before stamping onset) to reduce pre-onset spillover and make "time zero" align with the onset flip more reliably.
Files:
psyflow/StimUnit.py
- RTs are now based on PsychoPy's asynchronous keyboard timestamps (
KeyPress.rt) rather than poll-time (self.clock.getTime()) where applicable.StimUnit.capture_response()now useskp.rtforrt/response_time.StimUnit.wait_and_continue()now useskp.rtforresponse_time.
- The keyboard clock is now explicitly reset on the onset flip (via
win.callOnFlip) so thatKeyPress.rtis unambiguously onset-relative:StimUnit.run()StimUnit.capture_response()StimUnit.wait_and_continue()
- Stage-close fields are now more consistent:
- Added internal helper
StimUnit._stamp_close(...)and schedule it on the final flip for fixed-duration stages (flip-synced close). - In
capture_response(),close_timeis only set to the response RT whenterminate_on_response=True; otherwise it is stamped at the end of the response window.
- Added internal helper
StimUnit.show()offset triggers are now flip-locked:- Offset trigger +
close_timestamping are scheduled viawin.callOnFlip(...)on the final displayed frame. - Added
offset_flip_timeto record the final flip's timestamp (fromwin.flip()).
- Offset trigger +
onset_timeandclose_timeare stage-local seconds onStimUnit.clock, which is reset on the onset flip.onset_time_globalandclose_time_globalare epoch seconds for those events.close_time_globalis computed consistently fromonset_time_global + close_timewhen possible (including in_stamp_close).
flip_time(andoffset_flip_timeinshow()) are the return value of PsychoPywin.flip()(PsychoPy's monotonic timebase; not epoch).rt/response_timecome from PsychoPyKeyboardRTs (asynchronous, onset-relative).response_time_globalis derived asonset_time_global + rt.
Files:
psyflow/TriggerSender.pypsyflow/StimUnit.py
TriggerSender.send(code, wait=True)now acceptswait: booland no longer prints on every send.- When
wait=False,TriggerSender.send(...)skipscore.wait(post_delay)and does not callon_trigger_end.- This is intended for use inside flip callbacks (e.g., via
win.callOnFlip) to reduce the risk of dropped frames.
- This is intended for use inside flip callbacks (e.g., via
StimUnit.send_trigger(code, wait=True)now forwardswait, and all flip-scheduled trigger sends inStimUnit.show()/StimUnit.capture_response()usewait=False.
File: psyflow/StimUnit.py
- Replaced
from psyflow import TriggerSenderwith a relative import (from .TriggerSender import TriggerSender) so thatOptional[TriggerSender]type annotations refer to the class (not the submodule).
Files:
psyflow/BlockUnit.py
BlockUnit.summarize()no longer crashes (it now summarizes fromself.resultsinstead of mistakenly iterating the return value ofto_dict()).BlockUnit.generate_conditions()no longer prints the full condition list to stdout; it logs via PsychoPy logging instead.
Files:
psyflow/utils.py
utils.taps()now locates the bundled cookiecutter template viaimportlib.resources.files("psyflow.templates") / template(instead of looking under the package root).
Files:
psyflow/utils.py
- Added
validate_config(...)for lightweight top-level section/type checks. load_config(..., validate=True, required_sections=...)can now opt into validation at load time.
Files:
psyflow/__init__.pypyproject.tomlsetup.pydocs/index.rstdocs/api/psyflow.rstdocs/tutorials/getting_started.mddocs/tutorials/getting_started_cn.mddocs/tutorials/get_subinfo.mddocs/tutorials/get_subinfo_cn.md
psyflow/__init__.pynow uses lazy exports (__getattr__) soimport psyflowis lightweight and does not import PsychoPy unless needed.- Declared Python requirement
>=3.10(PEP 604 unions are used in the codebase). - Removed LLM-related dependencies from packaging metadata after removing the LLM utilities.
- Removed LLM-related docs entry points from the docs tree.
Files:
.github/workflows/ci.ymltests/test_smoke.py
- Added a basic CI workflow to compile sources and run a small
unittestsmoke suite.
Files:
psyflow/StimBank.pypsyflow/StimUnit.pypsyflow/SubInfo.pypsyflow/cli.py
- Replaced non-essential emoji/fancy punctuation with ASCII to avoid console encoding issues on Windows setups.