This repository runs a two-armed reversal-learning bandit task for StarStim 8 / NIC-2 studies and now supports a fast no-stimulation localizer for participant-specific feedback-locked theta.
The main scientific target is task-evoked, feedback-locked theta during reward learning, not a generic resting “intrinsic theta” estimate. The intended workflow is:
- Run
LOCALIZER_FAST_THETAfor 5-10 minutes with no stimulation. - Record StarStim EEG over LSL during the localizer or import an offline EEG export later.
- Preprocess the EEG automatically.
- Estimate a participant-specific feedback theta peak.
- Save that estimate in a subject-level JSON file.
- Run
ITHETA_TACSusing the selected frequency. - Fall back to fixed 6.0 Hz when the individualized estimate is unreliable.
LOCALIZER_FAST_THETA: Fast no-stimulation bandit localizer with event-rich feedback logging.ITHETA_TACS: Theta-tACS using the most recent reliable participant-specific feedback theta estimate.FIXED_THETA_TACS: Fixed 6.0 Hz theta-tACS for controls and fallback.SHAM: Sham run with the same task timing and logging structure.THETA_NIC,DC_NIC,BEHAVIORAL: Legacy aliases preserved for backward compatibility.
LOCALIZER_FAST_THETA is a faster version of the same two-choice reversal-learning bandit task:
- Two choices, probabilistic reward, reversals, and win/loss/miss feedback.
- Default duration: 6 minutes.
- Configurable duration: 5, 6, 8, or 10 minutes.
- Default target: about 120 trials and at least 100 feedback events before artifact rejection.
- Localizer timing defaults are shorter than the legacy stimulation blocks so the operator can estimate theta before the next run.
The localizer sends the standard feedback markers (31, 32, 33), stores detailed feedback timing in the behavioral CSV, and can subscribe to a live StarStim EEG LSL stream during the run.
cd code
python bandit_main.py --mode LOCALIZER_FAST_THETA --subject 001 --session 001python run_theta_estimation.py --subject 001 --session 001 --auto-findThis writes:
- A subject/session theta estimate JSON
- A one-row CSV summary
- QC plots for channel quality, epoch retention, ROI time-frequency power, theta spectrum, split-half peaks, and bootstrap peak stability
- An HTML report when enabled in
config.json
python bandit_main.py --mode ITHETA_TACS --subject 001 --session 001 --run 1python bandit_main.py --mode FIXED_THETA_TACS --subject 001 --session 001 --run 1 --frequency 6.0- Reliable feedback-locked theta estimate: use the rounded participant-specific frequency.
- Unreliable feedback-locked theta estimate: use fixed 6.0 Hz when fallback is enabled.
- No theta file found: follow
stimulation_frequency_selection.stop_if_no_theta_file.
The task never silently substitutes frequencies. Operator-facing summaries, logs, and trial CSVs store:
- Intended stimulation frequency
- Operator-confirmed protocol
- Operator-confirmed frequency
- Theta source (
reliable_itheta,fallback_fixed_6hz,fixed_6hz,sham, ornone) - Theta estimate file path
- Reliability decision and reason
During LOCALIZER_FAST_THETA, the code can:
- Resolve an incoming EEG LSL stream
- Record samples and timestamps continuously
- Save raw EEG as
.npz - Optionally save a
.csvexport and metadata JSON
run_theta_estimation.py supports .npz, .csv, .edf, .bdf, .xdf, and .set inputs. edf, bdf, and set loading use MNE when available; xdf uses pyxdf when available.
- High-pass: 0.5 Hz
- Low-pass: 40 Hz
- Notch: 60 Hz
- Resample: 250 Hz
- Reference: average of available good channels
- ROI:
Fz,FCz,Cz,F3,F4with frontocentral fallback logic - Epoch window:
-1.0to+1.5s around feedback - Baseline window:
-0.5to-0.1s - Theta window:
+0.2to+0.8s
The estimator rejects or downgrades estimates when any of the following fail:
- Too few usable feedback epochs
- Too few usable ROI channels
- Weak peak prominence
- Peak at the edge of the theta search band
- Split-half disagreement
- Excessively wide bootstrap confidence interval
- Poor usable epoch fraction
When the estimate is unreliable, the JSON explicitly records the fallback to fixed 6.0 Hz.
- Python does not pretend to program NIC-2 directly unless your lab has separately validated that path.
- For stimulation runs, the task determines the intended frequency and shows the protocol label and frequency to the operator.
- The operator still confirms what was actually loaded in NIC-2.
- The task can wait for NIC-2 marker
203before starting the bandit run.
- StarStim 8 EEG can be useful for a quick feedback-theta localizer, but the estimate is QC-gated.
- The main theta estimate comes from the no-stimulation localizer.
- Simultaneous EEG during tACS should be treated as exploratory.
- Baseline correction is used for feedback-locked spectral estimates, but it does not by itself remove the aperiodic component.
Behavioral localizer and stimulation CSVs now include legacy fields plus additional columns such as:
subject_idsession_idrunmodephasestim_conditionprotocol_label_to_showoperator_confirmed_protocoloperator_confirmed_frequency_hzintended_stimulation_frequency_hzactual_or_confirmed_stimulation_frequency_hztheta_sourcetheta_estimate_filetheta_reliabletheta_reliability_reasonfeedback_markerfeedback_onset_task_timefeedback_onset_lsl_timelsl_marker_send_timerun_start_task_timerun_start_lsl_timerun_end_task_timerun_end_lsl_time
Existing marker codes are preserved:
10: Trial start20: Choice31: Feedback win32: Feedback loss33: Feedback miss100: Run start200: Run end203: NIC-2 stimulation start marker used to begin stimulation runs
Use the debug utility to inspect marker and EEG streams:
python lsl_debug_tool.pyIt reports available streams, tests a marker inlet, and samples from the first EEG stream it finds.
Synthetic verification lives in tests/test_theta_workflow.py and covers:
- Reliable 6.5 Hz task-evoked theta
- Noisy/no-peak fallback
- Edge-peak rejection
- Split-half disagreement fallback
- Too-few-epochs fallback
- Localizer smoke test when
pygameis available - ITHETA JSON selection and fallback behavior
Run the tests with:
python -m unittest tests/test_theta_workflow.pytacs_bandit/
├── code/
│ ├── bandit_main.py
│ ├── eeg_lsl_recorder.py
│ ├── local_starstim_module.py
│ ├── lsl_debug_tool.py
│ ├── run_theta_estimation.py
│ ├── select_theta_frequency.py
│ ├── theta_estimator.py
│ └── config.json
├── data/
├── tests/
└── readme.md