Release History¶
The changelog is maintained in CHANGES.rst at the repo root
and inlined below.
Unreleased¶
0.2.0 (2026-09-29)¶
CLI¶
New flag
--split-by-trial-type(BooleanOptionalAction, defaultTrue) onffrprep: emit per-trial-type epoched and evoked files alongside the combined output. Pass--no-split-by-trial-typefor the previous single-combined behaviour. The legacy--by_event_typeflag is kept as a SUPPRESS’d alias for one release.New flag
--trial-types A B …to restrict per-trial-type outputs to a subset.New flag
--difference-pairs A:B [C:D …]to compute difference evokeds across explicit pairs. For 2-type datasets the difference is auto-emitted; for 3+ types this flag is required to opt in.New flag
--filter-method {fir,iir}(defaultfir):iirapplies zero-phase (forward-backward) first-order Butterworth high-pass and low-pass filters (12 dB/octave overall), matching thebutter/filtfiltband-pass used in some published FFR pipelines.New flag
--reject-mode {ptp,abs}(defaultptp):absdrops epochs whose absolute amplitude reaches--reject-eegat any sample (max|x| >= threshold, drop reasonABS_AMP) instead of MNE’s peak-to-peak criterion. The preprocessing sidecar recordsRejectionMode(peak-to-peak/absolute-amplitude) next toRejectionThresholds.Fixed:
--reject-eeg 0now disables automatic rejection as its help text documents (parse_reject). It previously built a{"eeg": 0.0}threshold, which rejects every epoch. Negative thresholds are now an argument error.New flag
--no-report: skip HTML report generation (preprocessing and analysis) while still writing all derivatives. Intended for bulk runs over many subjects, where report figures dominate runtime.New flags for resumable, disk-friendly bulk runs:
--skip-existing(skip (task, run) iterations whose sidecar outputs already exist),--clean-work-dir(delete each iteration’s Nipype working files after it succeeds; logs are kept) and--keep-epochs/--no-keep-epochs(default keep;--no-keep-epochsdeletes the*_epo.fiffiles after analysis and keeps the JSON sidecars).New flag
--with-stimulionffrprep-download example(BooleanOptionalAction, defaultFalse): additionally fetches the BIDS/stimuli/directory needed by stimulus-aware analyses (e.g.corr_stim_to_resp) and augments everyevents.tsvwith the matchingstim_filecolumn.
Group-level analysis¶
ffrprep <bids_dir> <output_dir> groupnow runs, replacing the previous “Currently only participant-level analysis is supported.” stub. New moduleffrprep.groupaggregates already-computed participant-level derivatives fromoutput_dir— it does not readbids_diror re-run preprocessing/analysis:discover_group_inputs: globsffrprep-analysis/sub-*/andffrprep-preprocessing/sub-*/eeg/for combined evoked, diff evoked, and preprocessing epochs files, grouped by(task, session, run).compute_grand_average:mne.grand_averageacross subjects for each (task, session, run) with >= 2 contributing subjects.compute_subject_metrics: recomputes RMS SNR, band power, and response consistency per subject directly from saved derivatives (nothing is recomputed from raw data).Outputs land under
output_dir/ffrprep-group/: grand-average_desc-grandAverage_ave.fif(+ JSON sidecar with contributing subjects), a per-(task, run)_metrics.tsv, and a singlegroup_report.html.Deliberately aggregation-only, not inferential: no group-level statistics are computed, matching the scope other BIDS Apps (e.g. MRIQC) use for their own “group” level.
Group-level FFR metrics (opt-in):
--f0addsrms_snr_polarity_sum,f0_uvandupper_harmonics_uv(band-averaged FFT amplitude at the harmonics of F0, computed on the sum of the two per-trial-type averages; see--n-harmonics,--harmonic-bin-hz,--harmonic-window);--stimulusaddsstim2resp_r/z/lag_ms(andstim2resp_lim_*with--xcorr-lag-range);--n-trials-presentedaddsusable_pct.discover_group_inputsnow also returns the per-trial-type evoked files ("by_type").QC flags on the group metrics table:
--min-usable-pct(needs--n-trials-presented) and--min-snraddqc_usable_pct_ok/qc_snr_ok,qc_includeandqc_reasoncolumns (add_qc_flags). Subjects are flagged, never dropped: the table and the grand averages keep every subject, and the report summary shows how many were flagged.Docs: the pipeline details page now covers the group-level metrics, covariates, QC flags and output files, the
--skip-existing/--clean-work-dir/--no-keep-epochsbehaviour, and theRejectionModeandSourcessidecar fields.The group metrics TSV now has a BIDS-style data dictionary,
task-<task>[_run-<run>]_metrics.json, beside it (build_metrics_dictionary): a description and units for every column, with the actual windows and thresholds of the run embedded. Covariate columns take their description, levels and units from the JSON sidecar next to the covariate TSV (participants.json,phenotype/*.json) when one exists.merge_covariatesgainsreturn_sourcesandsave_group_metricsadictionaryargument.compute_grand_averagerenames single-channel evokeds that carry different channel names across sites (e.g.A32vsCz) to a common name before averaging; mismatched multi-channel sets raise a clearValueError.The group metrics TSV joins subject-level covariates from
<bids_dir>/participants.tsvand--covariates(merge_covariates); the report section switches to per-metric histograms for cohorts above 40 subjects (the per-subject values stay in the TSV).ffrprep.reportsgainsbuild_group_reportandbuild_metrics_table_section;build_subject_report/build_analysis_reportpass newentity_label/meta_labeltemplate variables so the sharedsubject_report.html.j2template can render a report with no single subject (existing rendered output for per-subject reports is unchanged).setup_derivatives_directoriesgains acreate_group=Falseflag to materializeffrprep-group/.
Preprocessing¶
Per-trial-type filenames now use BIDS-valid alphanumeric labels:
_bids_labeldrops non-alphanumeric separators and capitalizes each token, so ada_pol-1trial type is written as_desc-preprocDaPol1_epo.fif/_desc-evokedDaPol1.fif/_desc-evokedDiffDaPol1VsDaPol2.fifinstead of leaking_and-into thedescentity. Alphanumeric labels (positive,10) are unchanged, and sidecars still record the raw trial-type name inCondition/DifferenceOf.Fixed: the preprocessing sidecar’s
Sources/RawSourcesnow name the raw recording that actually exists (EDF, BDF, BrainVision, EEGLAB or FIF, including the session directory and every run of a concatenated output). They were hard-coded to_eeg.bdf; when no raw file is found the fields are omitted instead of recording a path that does not exist.Fixed: the preprocessing sidecar’s
EpochCountTotal/EpochCountRejectedare now per trial type. They were derived fromlen(epochs.drop_log), which spans every event (the other condition’s trials and non-analysed markers included), so a two-polarity recording reported ~2x the trials and charged each rejected trial to every file.epoch_datanow records per-condition counts while the events array is still aligned with the drop log; epochs built elsewhere fall back to the previous behaviour.epoch_dataacceptstrial_types=to narrow the discovered event_id mapping to a subset; raisesValueErrorif a requested name is absent so typos surface immediately.save_preprocessing_outputsaccepts adict[str, mne.Epochs]in addition to a scalar Epochs; dict input writes_desc-preproc{Cond}_epo.fifper trial type and the per-file sidecar carries aConditionfield. Scalar input keeps today’s_desc-preproc_epo.fiffilename + singlePathreturn.save_preprocessing_nodewraps the split:split_by_trial_type=Truefans the input Epochs out into a{cond: epochs[cond]}dict before forwarding tosave_preprocessing_outputs._save_one_preproc_epochsnow passesevents+event_idthrough to theEpochsArrayreconstruction so trial-type metadata survives the save / load round-trip (previously stripped silently; broke the per-condition flow).
Analysis¶
Fixed:
plot_pitch_and_confno longer callsplt.show(). On any local (non-Docker) install where matplotlib defaults to an interactive backend (e.g.macosxon a Mac,TkAgg/QtAggon many Linux desktops), this call blocked--stage analysis/--stage bothruns indefinitely waiting for a GUI window to close, sinceevoked_qainvokes it during every analysis report build. CI/Docker never hit this because tests forceAggand the container images run headless. The figure was already captured viaplt.gcf()immediately after the call (seereports.evoked_qa), so nothing depended on the interactive display.save_analysis_outputsaccepts a structured payload{"by_type": dict, "combined": Evoked, "diff": dict}(any subset). Filenames:per-type:
_desc-evoked{Cond}.fifcombined:
_desc-evoked.fifdifference:
_desc-evokedDiff{A}Vs{B}.fif
Sidecars carry
Condition(per-type) orDifferenceOf: [A, B](diff); combined omits both. Scalar Evoked input is treated as the combined output.create_analysis_workflowwires a newbuild_analysis_payloadhelper that produces the structured payload from a single Epochs object; the workflow inputnode gainsdifference_pairs.New helpers in
ffrprep.analysis(from PR #35 + post-merge refactor):compute_phase_consistency/plot_phase_consistency/plot_phase_consistency_masked: phase-consistency across two polarities + sum / difference.corr_stim_to_resp/corr_resp_to_resp: cross-correlation peak r + lag. Now delegate to a shared_xcorr_normalizedhelper that also returns the full correlation curve for plotting.response_consistency: mean pairwise Pearson correlation across epochs. Computed with a singlenp.corrcoefmatrix instead of a Python loop ofscipy.stats.pearsonrcalls (same values and pair ordering; ~0.3 s vs ~9 min for 3000 epochs x 4147 samples).compute_fft: amplitude spectrum helper.harmonic_amplitudes: FFT amplitude (zero-padded, single-sided, microvolts) of a response window, averaged in abin_hz-wide band around each harmonic off0; returns the per-harmonic values, the fundamental, and the summed upper harmonics. Defaults follow the /da/ measure of Whiteford et al. (2025) (60-180 ms, 100 Hz, 10 harmonics, 60 Hz bins).stim_to_resp_xcorr: maximum stimulus-to-response correlation in MATLABxcorr(..., 'coeff')form (no mean removal) with an optional lag window, returning(r, Fisher z, lag_ms).load_wav_mono/resample_signal: WAV reader and polyphase resampler used to bring a stimulus to the EEG sampling rate.
Analysis worker granularity changed from per-file to per-(task, run) group.
_collect_analysis_groupsstitches per-condition preproc files viamne.concatenate_epochs(withevent_idpreserved) and the workflow runs once per group.
Reporting¶
Per-(task, run) groups: each (task, run) gets one Raw section, one Epoched section per trial type, and per-type / combined / diff Evoked sections under one heading.
New summary rows on per-condition Evoked sections:
Stim correlation (peak r)+Stim correlation (lag, ms), derived from the BIDSstim_filecolumn inevents.tsv(silent no-op when the column or file is missing).
New figure on per-condition Evoked sections: stim ↔ response cross-correlation curve with the peak marked.
Combined Evoked sections add a second pair of rows / figure for the envelope correlation (combined ≈ ENV proxy in FFR, so
|hilbert(stim)|is the natural reference).Difference Evoked sections add a single raw-waveform stim correlation row + figure (diff ≈ TFS proxy).
Epoched sections (≥ 10 trials) gain a
Mean trial-to-trial rrow viaresponse_consistency.New Phase Consistency section per (task, run) when exactly two per-condition preproc files exist. Uses seaborn’s
flare_rcolormap; subplot titles surface the trial-type names. Default in single-subject reports is unmasked; group-level callers can passmask=Truefor the significance-masked variant.
Datasets¶
New
download_stimuli(dataset_path=None)fetches the OSF stimulus files into<dataset>/ffrprep_raw_data/stimuli/per the BIDS spec, then augments everysub-*/eeg/*_events.tsvwith astim_filecolumn based on atrial_type -> filenamelookup (STIM_FILE_MAP).Existing
download_example_data/download_raw_dataaccept awith_stimuli=Falsekwarg; True triggers the stimulus download after the EEG data.
Dependencies¶
New runtime dependency:
seaborn>=0.13(used solely for itsflare_rcolormap registration).
Internal¶
Build / run-provenance helpers updated for the per-condition filenames:
_check_preproc_output_or_raiseglob widened to*_desc-preproc*_epo.fif;_propagate_run_provenancederives the analysis base-stem viastr.partition("_desc-preproc").build_analysis_payloadimports its dependencies viaimportlib.import_moduleso the Nipype Function-node subprocess resolves them without inheriting the parent’s globals.test_system_pipelinehelper now points_derivatives_rootat the explicitoutput_dirargument (the BIDS-App--output-dirpositional is now honoured end-to-end).Production Docker image installs runtime deps only (
uv sync --frozen --no-dev); the in-imagetestsubcommand needs a separate--with-testsrebuild.