StimUnit: Modular Stimulus & Response Handler¶
Overview¶
StimUnit
is a versatile, stimulus-level controller for PsychoPy experiments. It bundles everything you need for one trial into a single, chainable object:
Stimulus presentation: Draw multiple visual or audio stimuli together with sub-frame accuracy.
Response collection: Detect keyboard events and record reaction times effortlessly.
Timing control: Opt for frame-based (refresh-locked) or clock-based timing based on your needs.
State management: Store all trial-related data in a centralized internal dictionary.
Event hooks: Plug in custom callbacks at start, response, timeout, and end stages.
By using StimUnit
, your trial logic (typically defined in src/run_trial.py
) becomes more modular, readable, and maintainable.
Key Features¶
Feature |
Description |
---|---|
Chainable API |
Build trials fluently by chaining methods. |
Frame-based Timing |
High precision |
Event hooks |
Call custom functions on start, response, timeout, or end. |
Flexible responses |
Specify valid keys and automatic RT handling. |
State tracking |
Centralized storage of timestamps, responses, and custom data. |
Response highlight |
Visually emphasize selections (static or dynamic). |
Jittered timing |
Support randomized (min–max) durations for unpredictability. |
Quick Reference¶
Purpose |
Method |
Example |
---|---|---|
Initialize |
|
|
Add stimuli |
|
|
Start hook |
|
|
Response hook |
|
|
Timeout hook |
|
|
End hook |
|
|
| Simple display | .show(duration)
| unit.show(1.0)
|
| Response capture | .capture_response(keys, duration)
| unit.capture_response(['left','right'], 2.0)
|
| Full trial control | .run()
| unit.run()
|
| Pause & continue | .wait_and_continue(keys)
| unit.wait_and_continue(['space'])
|
| Update state | .set_state(**kwargs)
| unit.set_state(correct=True)
|
| Retrieve state | .get_state()
or access .state
| data = unit.get_state(key,default)
|
| Export state | .to_dict()
| data = unit.to_dict()
|
Detailed Usage Guide¶
0. Create a StimBank¶
from psyflow.stim_bank import StimBank
# 1. Define a dictionary of stimulus configurations
stim_config = {
'fixation': {
'type': 'text',
'text': '+',
'color': 'white',
'height': 1.0
},
'target': {
'type': 'circle',
'radius': 0.5,
'fillColor': 'red'
},
'feedback': {
'type': 'text',
'text': 'Correct!',
'pos': [0, -2],
'color': 'green'
}
}
# 2. Create your StimBank with window and definitions
stim_bank = StimBank(win, stim_config)
stim_bank.preload_all() # optional
1. Initialization¶
Start by creating your PsychoPy window and keyboard, then instantiate StimUnit
with a descriptive label.
win = visual.Window([1024,768], color='black')
kb = Keyboard()
# Instantiate StimUnit
fix = StimUnit('fix', win, kb).add_stim(stim_bank.get('fixation'))
tar = StimUnit('tar', win, kb).add_stim(stim_bank.get('target'))
fb = StimUnit('fb', win, kb).add_stim(stim_bank.get('feedback'))
Choose descriptive unit_label
s (e.g., "fix"
, "tar"
, "fb"
). These labels are automatically prefixed to your state keys when you call set_state()
and accessed by get_state()
, ensuring your trial data is neatly namespaced and easy to query.
Pro tip: When you need to create many similar trial units in the same context, use Python’s functools.partial
to simplify instantiation:
from functools import partial
# Create a shortcut with common arguments
make_unit = partial(StimUnit, win=win, kb=kb)
# --- Cue phase ---
fix=make_unit('fix').add_stim(stim_bank.get('fixation'))
tar=make_unit('tar').add_stim(stim_bank.get('target'))
fb=make_unit('fb').add_stim(stim_bank.get('feedback'))
2. Adding Stimuli¶
Before presenting a trial, attach the stimuli you want to display or play—this defines what participants will see or hear during the unit.
StimUnit accepts instances of visual.BaseVisualStim
(e.g., TextStim
, Circle
) and sound.Sound
. You can add a single stimulus, multiple stimuli at once, or pass a list of stimuli:
# Add a single stimulus
unit.add_stim(stim_bank.get('fixation'))
# Add multiple stimuli at once
unit.add_stim(
stim_bank.get('target'),
stim_bank.get('feedback')
)
# Add a list of stimuli
unit.add_stim([
stim_bank.get('fixation'),
stim_bank.get('target'),
stim_bank.get('feedback')
])
# Chain additions
unit.add_stim(stim_bank.get('fixation')) \
.add_stim(stim_bank.get('target')) \
.add_stim(stim_bank.get('feedback'))
# Clear all stimuli
unit.clear_stimuli()
All added stimuli will be drawn or played together when you present the unit.
Note that add_stim
also support PsychoPy style definitions of the stimulus. For exmaple:
from psychopy.visual import TextStim, Circle
# Example 1
stim_list = {'fix': TextStim(win, text='+', pos=(0,0)),
'tar': Circle(win, radius=0.5, fillColor='red'),
'fb': TextStim(win, text='Correct!', pos=(0,0))}
unit.add_stim(stim_list['fix']).add_stim(stim_list['tar']).add_stim(stim_list['fb'])
# Example 2
fixation = visual.TextStim(win, text='+', height=0.8)
unit.add_stim(fixation)
unit.show(duration=(0.5, 1.5))
In the psyflow
framework, stimuli are normally retrieved from a stim_bank
object.
This lets you quickly retrieve named stimuli from your stim_bank
and pass them into a StimUnit
, streamlining stimunit setup. For example:
condition = 'loss'
make_unit(unit_label='cue').add_stim(stim_bank.get(f"{condition}_cue"))
make_unit(unit_label=f"pop")\
.add_stim(stim_bank.get(f"{condition}_pop"))\
.add_stim(stim_bank.get("pop_sound"))
3. Display stimulus with show()
¶
The show()
method is the core display function in StimUnit
. It handles precise timing, drawing, optional audio playback, and state logging—all in one call. Use it when you want to present stimuli without requiring responses.
Key Features of show()
Frame-based timing: Locks presentation to monitor refreshes for sub-frame precision.
Automatic audio support: Starts any sound stimuli on the first flip.
Flexible duration: Accepts fixed times, jittered ranges, or automatically uses sound length.
State logging: Records onset/offset timestamps and duration into
unit.state
.
Use this method when you need standalone stimulus presentation without response handling:
Feedback Displays: Show feedback messages or sounds (e.g., “Correct!”, error tones) for a short duration.
Passive Stimulus Presentation: Present visual or auditory stimuli (e.g., flicker, tones) in paradigms where responses are recorded separately, such as resting-state EEG or fMRI.
Baseline/Rest Periods: Present fixation crosses or blank screens for jittered durations to serve as inter-trial intervals or baselines.
Method Signature¶
.show(
duration: float | tuple[float, float] | None = None,
onset_trigger: int = None,
offset_trigger: int = None
) -> StimUnit
duration
:None: Uses the longest
getDuration()
of your sound stimuli (0.0 if none).float: A fixed presentation time in seconds.
(min, max): Uniformly randomize between
min
andmax
.
onset_trigger
/offset_trigger
: (optional) placeholder parameters for sending triggers—these are ignored if you manage triggers separately.
Behavior Table¶
Input Duration |
Behavior |
---|---|
|
Auto-select from audio (max duration or 0.0). |
|
Present for exactly 0.5 s. |
|
Sample in [0.8, 1.2] s. |
|
Screen stops at 1.0 s; audio may be cut off. |
|
Both screen & audio last full 2.5 s. |
The following examples demonstrate how to use .show()
for fixed, jittered, and audio-driven durations. Assume unit
is an initialized StimUnit
and stim_bank
contains your stimuli.
# 1. Fixed duration – shows text for exactly 1.0 second
unit.add_stim(stim_bank.get('fixation'))\
.show(duration=1.0)
# 2. Jittered duration – shows text for a random time between 1 and 2s
unit.add_stim(stim_bank.get('fixation'))\
.show(duration=(1,2)) #tuple
unit.add_stim(stim_bank.get('fixation'))\
.show(duration=[1,2]) #list
# 3. Auto from audio – shows visual and plays audio for the full sound duration
unit.add_stim(stim_bank['pop_sound']).show() # duration=None is default
# 4. Multiple stimuli
unit.add_stim(stim_bank.get('pop_sound'))\
.add_stim(stim_bank.get('fixation'))\
.show(duration=None)
4. Pause & Continue with wait_and_continue()
¶
Use .wait_and_continue()
to display stimuli and wait for participant input before proceeding, ideal for instruction screens, inter-block breaks, and end-of-experiment acknowledgments. It enforces a minimum display time and can optionally terminate the session after the key press.
Use this method when you need participants to confirm or acknowledge before moving on, without capturing a trial response. Common scenarios include:
Instruction screens: e.g., “Press SPACE to start” before an experiment block.
Inter-block breaks: Giving participants a chance to rest and proceed when ready.
End-of-experiment messages: Display a thank‑you note or debrief until key press.
Any standalone confirmation: When you don’t need to record response data but require an explicit continuation.
Method Signature¶
def wait_and_continue(
keys: list[str] = ["space"],
min_wait: Optional[float] = None,
log_message: Optional[str] = None,
terminate: bool = False
) -> StimUnit
keys
: List of keys that allow continuation (default['space']
).min_wait
: Minimum seconds to wait before accepting input. IfNone
, and audio stimuli are present, it defaults to the longest sound duration. If only visual stimuli are present, it defaults to infinite until the button preseed.log_message
: Custom message logged to PsychoPy’s log (defaults to:“Continuing after key ‘X’” or
“Experiment ended by key press.” if
terminate=True
).
terminate
: IfTrue
, closes the PsychoPy window after input.
Example
# Instruction screen: wait at least 2s for SPACE
StimUnit('instruction_text', win, kb)\
.add_stim(stim_bank.get('instruction_text'))\
.add_stim(stim_bank.get('instruction_text_voice'))\
.wait_and_continue()
# End screen: immediate termination on key
final_score = sum(trial.get("feedback_delta", 0) for trial in all_data)
StimUnit('goodbye',win,kb)\
.add_stim(stim_bank.get_and_format('good_bye', total_score=final_score))\
.wait_and_continue(terminate=True)
6. Get responses with capture_response()
¶
The capture_response()
method in StimUnit
integrates stimulus presentation, timing, triggers, and response handling into a single, chainable call. It is ideal for:
Detecting and logging subject responses within a predefined response window
Determining correct versus incorrect responses for performance analysis
Delivering immediate visual feedback by highlighting participant selections
Features¶
Capturing and logging participant choices within precise time windows
Sending and receiving hardware triggers (e.g., EEG/fMRI) in sync with stimuli
Differentiating correct vs. incorrect responses for performance metrics
Providing both static and dynamic visual feedback based on key presses
Method Signature¶
def capture_response(
keys: list[str],
duration: float | tuple[float, float],
onset_trigger: int = None,
response_trigger: int | dict[str, int] = None,
timeout_trigger: int = None,
terminate_on_response: bool = True,
correct_keys: list[str] | None = None,
highlight_stim: visual.BaseVisualStim | dict[str, visual.BaseVisualStim] = None,
dynamic_highlight: bool = False
) -> StimUnit
key
: Valid response keys.duration
: Response window (fixed or jittered range).terminate_on_response
: IfTrue
, stops redrawing after a valid response.correct_keys
: Subset ofkeys
to be treated as correct responses (for state logging).highligt_stim
: A stim (or dict mapping keys→stim) to draw around the selected choice.dynamic_highlight
: IfTrue
, highlights update on every keypress instead of just the first.
Below is the signature and four practical scenarios illustrating its use.
Scenario 1: Simple Anticipation Phase (detect early responses without termination)¶
In an anticipation phase of MID task, we want to monitor for any key presses but continue presenting the fixation cross even if the participant responds early. After the window, we record whether there was an early response.
# --- Anticipation phase ---
anti = make_unit(unit_label='anticipation') \
.add_stim(stim_bank.get("fixation"))
anti.capture_response(
keys=settings.key_list,
duration=settings.anticipation_duration,
onset_trigger=settings.triggers[f"{condition}_anti_onset"],
terminate_on_response=False
)
# After capture_response returns, check and store early response
early_response = anti.get_state("response", False)
anti.set_state(early_response=early_response)
anti.to_dict(trial_data)
terminate_on_response=False
ensures the fixation stays on screen for the full anticipation duration, regardless of key presses.After running,
anti.get_state("response")
tells whether any key was pressed.
Scenario 2: Target Phase with Required Response¶
In the target phase of the MID task, we require a response. Here we use default settings where keys in the keys=settings.key_list
count as potential responses, and no separate correct_keys
are defined. By default, correct_keys=None
, so any key in keys=settings.key_list
is counted as a correct response.
# --- Target phase ---
duration = controller.get_duration(condition)
target = make_unit(unit_label="target") \
.add_stim(stim_bank.get(f"{condition}_target"))
target.capture_response(
keys=settings.key_list,
duration=duration,
onset_trigger=settings.triggers[f"{condition}_target_onset"],
response_trigger=settings.triggers[f"{condition}_key_press"],
timeout_trigger=settings.triggers[f"{condition}_no_response"]
)
target.to_dict(trial_data)
By default,
correct_keys=None
, so any key inkeys
is logged as a response.response_trigger
andtimeout_trigger
send triggers for EEG/behavior marking.
Scenario 3: Detecting Correct vs. Incorrect Responses¶
In tasks where only one key is correct (e.g., left vs. right dot detection), specify correct_keys
to log hits vs. misses.
# Determine which key is correct based on target position
target_stim = stim_bank.get(f"{trial_info['target_position']}_target")
correct_key = (
settings.left_key
if trial_info['target_position'] == 'left'
else settings.right_key
)
target_unit = make_unit(unit_label="target") \
.add_stim(target_stim)
target_unit.capture_response(
keys=settings.key_list,
correct_keys=[correct_key],
duration=settings.target_duration,
onset_trigger=settings.triggers[f"{condition}_target_onset"],
response_trigger=settings.triggers.get("key_press"),
timeout_trigger=settings.triggers.get("no_response"),
terminate_on_response=True
)
target_unit.to_dict(trial_data)
correct_keys
filters which responses count as hits. The booleanhit
state is set accordingly.terminate_on_response=True
ends the trial on the first valid key press.
Scenario 4: Highlighting Participant Choices¶
To provide visual feedback, pass a dictionary of highlight stimuli to draw around the chosen option. Use dynamic_highlight=True
to allow participants to change their selection during the window.
cue = make_unit(unit_label="cue") \
.add_stim(stim_bank.get('stimA')) \
.add_stim(stim_bank.get('stimB'))
correct_key = settings.left_key # for example
cue.capture_response(
keys=settings.key_list,
correct_keys=[correct_key],
duration=settings.cue_duration,
onset_trigger=settings.triggers['cue_onset'] + marker_pad,
response_trigger=settings.triggers['key_press'] + marker_pad,
timeout_trigger=settings.triggers['no_response'] + marker_pad,
terminate_on_response=False,
highlight_stim={
'f': stim_bank.get('highlight_left'),
'j': stim_bank.get('highlight_right')
},
dynamic_highlight=False
)
cue.to_dict(trial_data)
highlight_stim
maps each key to a visual cue (e.g., a frame or dot).If
dynamic_highlight=True
, each new key press updates the highlight until the window ends.
Tip
You can also pass a dict
to response_trigger
to send different trigger codes per key.
7. Lifecycle Hooks¶
Lifecycle hooks give you maximum flexibility to define custom behaviors at key trial events—complementing built‑in methods like .capture_response()
or .show()
. You pick exactly what code runs during start, response, timeout, and end phases.
Below is a complete example demonstrating how each hook operates in practice. Watch how state keys are automatically prefixed with your unit_label
(“demo”):
from psychopy import core, visual
from psychopy.hardware.keyboard import Keyboard
from psyflow import StimUnit
# 1. Setup
win = visual.Window([800,600], color='black', units='deg')
kb = Keyboard()
unit = StimUnit('demo', win, kb)
# 2. Add a stimulus
fix = visual.TextStim(win, text='+', height=1.0)
unit.add_stim(fix)
# 3. Define hooks
@unit.on_start()
def start_hook(u):
# record the moment the trial begins
u.set_state(start_time=u.clock.getTime())
print(f"[start_hook] prefix key 'demo_start_time' = {u.state['demo_start_time']}")
@unit.on_response(['space'])
def response_hook(u, key, rt):
# capture spacebar presses
u.set_state(response=key, rt=rt)
print(f"[response_hook] key={key}, rt={rt:.3f}")
@unit.on_timeout(1.0)
def timeout_hook(u):
# handle no-response after 1 second
u.set_state(timeout=True)
print("[timeout_hook] no response within 1.0s")
@unit.on_end()
def end_hook(u):
# finalize and inspect full state
data = u.to_dict()
print("[end_hook] final state:", data)
# 4. Run the trial
unit.run()
Explanation:
start_hook: Fires immediately after the global start time is recorded and before the first screen flip.
response_hook: Executes on
'space'
press, providing(unit, key, rt)
.timeout_hook: Triggers if no response occurs within 1.0 second.
end_hook: Runs after the trial finishes but before logging, giving you a final chance to inspect or modify data.
All calls to u.set_state()
automatically add entries like demo_start_time
, demo_response
, and demo_timeout
into unit.state
, which you can retrieve with unit.to_dict()
or view in your PsychoPy logs via unit.log_unit()
.
Instead of decorators, you can register hooks fluently via chaining for concise code:
unit = StimUnit('chain_demo', win, kb)
# Chain registrations and run in one statement
unit.add_stim(fix)
.on_start(lambda u: u.set_state(start=time.time()))
.on_response(['space'], lambda u, k, rt: u.set_state(response=k, rt=rt))
.on_timeout(1.0, lambda u: u.set_state(timeout=True))
.on_end(lambda u: print('Chained final state:', u.to_dict()))
.run()
While .show()
and .capture_response()
bundle common patterns, you can achieve the same behavior using lifecycle hooks for greater customization.
Replicating show()
with Hooks¶
unit = StimUnit('show_demo', win, kb)
unit.add_stim(my_stim)
# Start: send onset trigger, record time
@unit.on_start()
def start_show(u):
u.win.callOnFlip(u.send_trigger, trigger_onset)
u.set_state(onset_time=u.clock.getTime())
# Timeout after desired duration: send offset trigger, record close time
@unit.on_timeout(show_duration)
def end_show(u):
u.send_trigger(trigger_offset)
u.set_state(close_time=u.clock.getTime())
# End hook for any cleanup or logging
@unit.on_end()
def finalize_show(u):
print('Show ended, state:', u.get_dict())
# Run without terminating on response (no responses listened for)
unit.run(terminate_on_response=False)
on_start sets up the flip‑synchronized onset trigger and timestamps.
on_timeout fires after show_duration, mirroring offset_trigger.
on_end finalizes the trial.
For a more fluent style, you can register hooks and configure trials in a chainable manner:
make_unit('show_chain', win, kb) \
.add_stim(my_stim) \
.on_start(lambda u: (
u.win.callOnFlip(u.send_trigger, trigger_onset),
u.set_state(onset_time=u.clock.getTime())
)) \
.on_timeout(show_duration, lambda u: (
u.send_trigger(trigger_offset),
u.set_state(close_time=u.clock.getTime())
)) \
.on_end(lambda u: print('Show ended, state:', u.get_dict())) \
.run(terminate_on_response=False)
Replicating .capture_response()
with Hooks¶
unit = StimUnit('resp_demo', win, kb)
unit.add_stim(response_stim)
# Start: draw stimuli, flip, send onset trigger, reset clock
@unit.on_start()
def start_resp(u):
for s in u.stimuli:
s.draw()
u.win.callOnFlip(u.send_trigger, onset_trigger)
u.win.callOnFlip(u.clock.reset)
# Response: for each valid key, send trigger and set state
@unit.on_response(['f','j'])
def on_resp(u, key, rt):
code = response_triggers[key]
u.send_trigger(code)
u.set_state(response=key, rt=rt, hit=(key in correct_keys))
# Timeout: if no response within window
@unit.on_timeout(response_duration)
def on_timeout(u):
u.send_trigger(timeout_trigger)
u.set_state(response=None, timeout=True)
# End: log and clean up
@unit.on_end()
def end_resp(u):
u.log_unit()
# Run the trial (hooks manage drawing and events)
unit.run()
Make it in a chainable manner:
make_unit('resp_chain', win, kb) \
.add_stim(response_stim) \
.on_start(lambda u: (
[s.draw() for s in u.stimuli],
u.win.callOnFlip(u.send_trigger, onset_trigger),
u.win.callOnFlip(u.clock.reset)
)) \
.on_response(['f','j'], lambda u, key, rt: (
u.send_trigger(response_triggers[key]),
u.set_state(response=key, rt=rt, hit=(key in correct_keys))
)) \
.on_timeout(response_duration, lambda u: (
u.send_trigger(timeout_trigger),
u.set_state(response=None, timeout=True)
)) \
.on_end(lambda u: u.log_unit()) \
.run()
Warning
In most cases, using .show()
, .capture_response()
, and .wait_and_continue()
covers the vast majority of task requirements and has been extensively tested. Lifecycle hooks offer maximum flexibility, but they are less commonly used and have seen less practical validation. We did not test their usage extensively. Only opt for manual hooks when you need custom behavior beyond the built-in methods—and proceed with caution.
8. State and Data Management¶
StimUnit
keeps all unit-related values in its internal unit.state
dictionary. Use the following methods to keep your StimUnit data organized, easily retrievable, and ready for analysis or export.
Recording or Updating Values with set_state()
¶
Use set_state()
to record key–value data into the unit’s state
Example
# --- Feedback ---
if early_response:
delta = settings.delta * -1
hit=False
else:
hit = target.get_state("hit", False)
if condition == "win":
delta = settings.delta if hit else 0
elif condition == "lose":
delta = 0 if hit else settings.delta * -1
else:
delta = 0
hit_type = "hit" if hit else "miss"
fb_stim = stim_bank.get(f"{condition}_{hit_type}_feedback")
fb = make_unit(unit_label="feedback") \
.add_stim(fb_stim) \
.show(duration=settings.feedback_duration, onset_trigger=settings.triggers.get(f"{condition}_{hit_type}_fb_onset"))
fb.set_state(hit=hit, delta=delta).to_dict(trial_data)
In this snippet, we:
Create and display a feedback
StimUnit
labeled “feedback” with the correct stimulus and triggers.Compute whether the trial was a hit (
True
/False
) and determine thedelta
value (+Δ, –Δ, or 0).Record these values into the unit’s internal state using
set_state()
(which prefixes keys by default).Export all stored state entries into your
trial_data
dict for logging or further analysis.
Note: set_sate()
uses prefixing to control how keys are stored
Default (unit label): keys stored as <unit_label>_<key>
fb.set_state(hit=True, delta=0.5)
# stores 'feedback_hit' and 'feedback_delta'
Raw (prefix=''
): keys stored as-is
fb.set_state(prefix='', hit=True, delta=0.5)
# stores 'hit' and 'delta'
Custom (prefix='special'
): keys prefixed with ‘special_’
fb.set_state(prefix='special', hit=True)
# stores 'special_hit'
Summary of the prefix behavior
Mode |
Prefix |
Stored keys |
---|---|---|
Default |
(unit label) |
feedback_hit, feedback_delta |
Raw |
‘’ |
hit, delta |
Custom |
‘special’ |
special_hit |
Tip
set_state
is Chaining: it returns the same StimUnit
, so you can chain calls:unit.set_state(block=2, trial=5).set_state(condition='win')
Summary of Automatic State Entries
Several StimUnit
methods populate internal state without explicit set_state()
calls. Here’s a quick reference:
Method |
Keys Set in |
Notes |
---|---|---|
|
|
Experiment-wide timestamp before start |
|
Recorded at first |
|
|
At end of trial (response or timeout) |
|
|
If a timeout occurs |
|
|
|
Final chosen duration |
|
At stimulus onset |
|
|
Time of flip after onset |
|
|
At presentation end |
|
|
|
Response window setup |
|
On response |
|
|
Response trigger and end time |
|
|
On timeout |
|
|
|
At display onset |
|
When continuation key pressed |
Use this table to quickly see what state values are filled in automatically, so you know which additional set_state()
calls you might still need.
Note
For capture_response()
, the hit
means the response was a correct key press.
Retrieving Values with get_state()
¶
When you call get_state()
, it first looks for the exact key, then for the prefixed form (using your unit_label or a supplied prefix), and if neither is found it returns the default value.
# Reads either 'response' or 'cue_response' if the unit_label is 'cue'
resp = unit.get_state('response', default=None)
# Force a different prefix lookup, it reads 'custom_response'
resp = unit.get_state('response', default=0, prefix='custom')
Exporting All State with .to_dict()
¶
to_dict(target=None)
If no
target
is given, returns theStimUnit
instance (for chaining) and leaves you to inspectunit.state
.If you pass in a dict, it merges
unit.state
into that dict and returns theStimUnit
.
# After an anticipation phase:
anti = make_unit('anticipation')\
.add_stim(stim_bank.get('fixation'))\
.capture_response(keys=settings.keys, duration=settings.anticipation_duration)
# Check if participant responded early
early = anti.get_state('response', default=None)
# Record this custom flag and merge into main trial_data
anti.set_state(early_response=early)\
.to_dict(trial_data)
# In the target phase:
target = make_unit('target')\
.add_stim(stim_bank.get(f"{condition}_target"))\
.capture_response(keys=settings.keys, duration=duration)
# Export results
target.to_dict(trial_data)
Logging State Internally with .log_unit()
¶
log_unit()
writes every key–value pair in unit.state
to the log score in data/*.log
. It uses PsychoPy’s logging.data()
, which by default appends timestamped entries to the experiment log file or console.
It’s automatically invoked within the StimUnit
class, so you usually don’t need to call it manually.
What gets logged? All entries currently in unit.state
, including:
State key examples |
Description |
---|---|
|
Pre-trial identifiers |
|
Timestamps from |
|
Response metrics from |
|
Custom values from your |
If you need a separate log for debugging at other points, you can call unit.log_unit()
manually to snapshot current state.
Next Steps¶
Now that you understand how to use StimUnit
, you can:
Organize Blocks: Explore the BlockUnit tutorial to organize trials into blocks
Manage Stimuli: Learn about StimBank for flexible stimulus management
Send Triggers: Check out trigger sending for EEG/MEG experiments