KIT general pipeline: source analysis, statistics and multivariate

Author: Hadi Zaatiti <hadi.zaatiti@nyu.edu>

This page covers the stages after the epochs and evoked responses of stage 9: the automatic template coregistration, the forward and inverse models (stages 13, 14 and 15), the group averages (stage 16), the cluster permutation tests (stage 17), the representational similarity and decoding analyses (stage 18), the source estimates browsed over time with a slider (stage 19), the dipole fits (stage 20) and the spectra of the long recordings (stage 21). Like every other stage they read one YAML config (-c) and write under <bids_root>/derivatives/<stage>/ with a timestamped copy of the config in configs/, one merged summary table, a JSON record per output, PNG figures, and a refresh of the Quarto report (individual and group sections). Everything is computed with MNE-Python; the statistics with mne.stats; the RSA with mne-rsa; the classifiers with scikit-learn inside MNE’s decoding module. Nothing is hand-coded that a library provides.

Overview

#

Script

Reads

Writes (under <bids_root>/derivatives/)

13

13-kit_auto_coreg.py

stage-2 .fif (digitisation), fsaverage

13-kit_auto_coreg/sub-*/sub-*_trans.fif + .json, scaled template subjects/sub-*, figures/*_desc-autocoregqa.png, auto_coreg_summary.csv

14

14-kit_forward_model.py

stage-12 or 13 transform, the anatomy it refers to, stage-9 *_ave.fif (sensor info of every recording)

14-kit_forward_model/sub-*/sub-*_<spacing>_src.fif, sub-*_bem-sol.fif, *_desc-forward_fwd.fif + .json, figures/*_desc-forwardqa.png, forward_summary.csv

15

15-kit_inverse_solution.py

stage-9 *_epo.fif (noise covariance) and *_ave.fif, stage-14 forward

15-kit_inverse_solution/sub-*/*_desc-noise_cov.fif, *_inv.fif, *_cond-<name>_desc-<method>_space-fsaverage-{lh,rh}.stc, *_labels.tsv, *_inverse.json, figures/*.png, inverse_summary.csv

16

16-kit_group_source.py

stage-15 estimates and ROI time courses

16-kit_group_source/subjects_manifest.json, group/cond-*, group/contrast-* (STCs, label TSVs), group/figures/*.png, group_source_summary.csv

17

17-kit_cluster_stats.py

stage-9 evoked (sensor level), stage-15/16 estimates (source level)

17-kit_cluster_stats/group/<level>/contrast-*/clusters.json, tobs.npz, clusters_desc-summary_*.stc, figures/*.png, cluster_stats_summary.csv

18

18-kit_rsa_decoding.py

stage-15/16 estimates or stage-9 evoked (RSA), stage-9 epochs (decoding)

18-kit_rsa_decoding/sub-*/*_desc-rdm.npz, *_desc-rsa.tsv, *_contrast-*_desc-decoding.tsv, group/*, figures/*.png, rsa_decoding_summary.csv

19

19-kit_source_movie.py

stage-15 estimates (individual), stage-16 averages and contrasts (group)

19-kit_source_movie/sub-*/*_frames/frame-NNN.jpg, *_movie.json, *_slider.html, *_desc-peakframe.png, group/cond-* and group/contrast-* likewise, source_movie_summary.csv

20

20-kit_dipole_fit.py

stage-9 evoked, stage-15 covariance (or stage-9 epochs), stage-12/13 transform, stage-14 BEM

20-kit_dipole_fit/sub-*/*_desc-dipole.dip, *_timecourse.tsv, *_desc-dipole.json, figures/*, dipole_fit_summary.csv

21

21-kit_sleep_spectra.py

stage-7 filtered FIF (or stage-2) of spectra.tasks, scans.tsv

21-kit_sleep_spectra/sub-*/[ses-*/]*_desc-psd.tsv, *_desc-bandpower_*.tsv, *_desc-spectra.json, figures/*, spectra_summary.csv

Run them in order after stage 9:

python 13-kit_auto_coreg.py      -c pipeline_config_files/kidlang_config.yml
python 14-kit_forward_model.py     -c pipeline_config_files/kidlang_config.yml
python 15-kit_inverse_solution.py -c pipeline_config_files/kidlang_config.yml
python 16-kit_group_source.py     -c pipeline_config_files/kidlang_config.yml
python 17-kit_cluster_stats.py    -c pipeline_config_files/kidlang_config.yml
python 18-kit_rsa_decoding.py     -c pipeline_config_files/kidlang_config.yml
python 19-kit_source_movie.py     -c pipeline_config_files/kidlang_config.yml
python 20-kit_dipole_fit.py       -c pipeline_config_files/epilepsy_project_config.yml   # needs 14 (and 15 for the covariance)
python 21-kit_sleep_spectra.py    -c pipeline_config_files/epilepsy_project_config.yml   # needs 7 only

Stage 13 is optional when every subject has a reviewed stage-12 transform. Stages 14 and 15 run per recording and reuse their outputs while the inputs and settings are unchanged (--overwrite recomputes); stages 16 to 18 are group stages and always recompute from what stages 9 and 15 currently list. The launch entries (Kidlang) ... in .vscode/launch.json run each stage in the standalone MNE environment.

Conditions: events.conditions

Stages 15 to 18 and the report name the conditions through events.conditions, a mapping from the event code (as written in the stage-8 .eve files) to a name and factor levels:

events:
  conditions:
    "18": {name: dialogue_phrase, task: dialogue, condition: phrase}
    "34": {name: dialogue_object, task: dialogue, condition: object}
    "66": {name: dialogue_baseline, task: dialogue, condition: baseline}
    ...

Every key other than name is a factor. Factors give selectors that expand to every condition with that level, usable wherever a list of conditions is expected ({condition: phrase} selects the four phrase conditions of KidLang), and they define the model RDMs of the RSA. Stage 9 is unaffected by the catalogue (it keeps writing one evoked per code).

Tasks that reuse the codes. When a dataset has several tasks and each task gives the same codes a different meaning (the epilepsy project: 224 is tone_low in twotones, reversal_to_a in flippedsquares, cue_left in buttonlr), one flat catalogue cannot name them, and pooling by code across tasks (as the group and movie stages do) would mix unrelated responses. The catalogue can therefore be written per task:

events:
  conditions:
    twotones:       {"224": tone_low, "225": tone_high, "226": tone_offset}
    flippedsquares: {"224": {name: reversal_to_a, kind: reversal}, "225": reversal_to_b}
    buttonlr:       {"224": cue_left, "225": cue_right, "226": press_left, "227": press_right}
  name_conditions_by_task: auto

The name of a condition of a recording is then resolved in this order: the per-task entry, else the flat entry, else the TrialType of the run’s *_events.json (TriggerChannels.KIT<code>.TrialType, written by the stimulus scripts), else code-<code>. Names that do not come from the flat catalogue are prefixed with the task (twotones_tone_low, buttonlr_press_left), so two tasks sharing a code never pool in stages 16 to 19 and the report lists them apart. name_conditions_by_task: false turns the prefix off (single-task datasets, or when the per-task names are unique by construction), true prefixes every name, including the flat catalogue’s. Per-task entries carry a task factor automatically, so {task: twotones} selects the conditions of that task. Renaming a condition changes the stage-15 output names; stage 15 notices (the names are part of its inputs), recomputes the recording and removes the estimates written under the old names.

Stage 13: automatic coregistration

13-kit_auto_coreg.py gives a head-to-MRI transform to every subject without opening a GUI, fitted either to the subject’s own FreeSurfer reconstruction or to the fsaverage template (coregistration.automatic.mri):

  • template (default): fsaverage, scaled to the head shape (below).

  • individual: the stage-10 reconstruction sub-<label>, not scaled. The scalp is the stage-11 bem/sub-<label>-head-dense.fif; the MRI fiducials are bem/sub-<label>-fiducials.fif when stage 12 saved them, otherwise estimated from fsaverage’s through the reconstruction’s talairach transform (MNE fiducials="auto"). A subject without a reconstruction is reported and not fitted.

  • individual_then_template: the reconstruction when there is one, otherwise the template.

With the individual anatomy, require_qc_verdict: true (default) uses a reconstruction only once stage 11 accepted it (pass or pass_with_note). qc_pending_allowed: [<labels>] lifts that for the listed subjects only (a first look at a participant before the review), and require_qc_verdict: false for everyone; a pending verdict used this way is recorded as qc_status in the summary and the provenance, and a fail verdict is never used. For a cohort where some participants have no MRI, individual_then_template gives each one the best anatomy available. Stage 14 then builds the forward model on the reconstruction (the watershed BEM of stage 10), and the dipole positions of stage 20 are given on the subject’s own MRI.

Stage 13 exists for the subjects stage 12 cannot serve yet: participants without an MRI (children, typically), reconstructions whose stage 11 verdict is pending, or simply a first run of the source stages before the coregistration session. The transform is marked automatic in every downstream table, and a complete stage-12 transform replaces it (source.coregistration: manual_then_auto).

The fit is MNE’s mne.coreg.Coregistration (steps 1 to 3 also on the individual anatomy, without scaling; step 4 only for the template):

  1. fit_fiducials: the digitised nasion, LPA and RPA are matched to the template fiducials (bem/fsaverage-fiducials.fif).

  2. fit_icp for icp_iterations: iterative closest point on the digitised head shape (the KIT laser scan, about 4000 points), with scale_mode none, uniform or 3-axis scaling of the template. The lab’s manual practice for children was 3-axis scaling, which is the default.

  3. Points farther than omit_distance_mm from the scalp after that fit are omitted (omit_head_shape_points) and the ICP repeated.

  4. With scaling, mne.scale_mri() writes the scaled template as sub-<label> next to fsaverage (coregistration.automatic.subjects_dir, default derivatives/13-kit_auto_coreg/subjects; fsaverage is fetched there with mne.datasets.fetch_fsaverage() when missing). The scaled copy keeps fsaverage’s vertex numbering, so the morph of stage 15 back to fsaverage is exact. A scaled copy takes roughly 200 MB; keep the folder outside a synced drive if that matters (subjects_dir).

Quality numbers (auto_coreg_summary.csv, the JSON sidecar, the report): the three scale factors, the distance from every digitised point to the scalp (mne.dig_mri_distances: median, 95th percentile, maximum, points over 5 mm), the fiducial errors, and the number of points omitted. A median around 2 to 3 mm is what MNE quotes for a good fit. A large 95th percentile or a scale far from 1 is the signal to coregister that subject in stage 12.

Stage 14: forward model

14-kit_forward_model.py builds, per subject, the source space and the BEM, and, per recording, the forward solution. The recording matters because the KIT marker coils give each recording its own device-to-head transform (info['dev_head_t']), so two splits of one session get two forwards.

  • Transform: source.coregistration picks the stage-12 transform when its row in coreg_summary.csv is complete and the file is readable, otherwise the stage 13 transform (manual_then_auto); manual and auto restrict the choice (the pre-renumbering spellings stage8_then_auto / stage8 are still accepted). The anatomy is the mri_subject that transform refers to, in its SUBJECTS_DIR.

  • mne.setup_source_space() (source.forward.spacing, default ico4 = 2562 sources per hemisphere, add_dist='patch'), saved as sub-<label>_<spacing>_src.fif.

  • mne.make_bem_model() + mne.make_bem_solution() on the inner skull surface (bem/inner_skull.surf; one layer, conductivity [0.3]), saved as sub-<label>_bem-sol.fif. fsaverage and its scaled copies ship that surface; an individual FreeSurfer reconstruction needs the watershed surfaces (mne watershed_bem in the FreeSurfer environment), otherwise the stage reports missing_anatomy with that instruction.

  • mne.make_forward_solution() with meg=True, eeg=False, mindist and ignore_ref=True. ignore_ref is mandatory for KIT: the reference magnetometers are not compensation channels.

The per-recording JSON records the identities of the evoked file, transform, source space and BEM it used, the counts, and the median of MNE’s sensitivity map; the figure shows sources, sensors and head-shape points in head coordinates. forward_summary.csv keeps one row per recording.

Stage 15: noise covariance, inverse operator, source estimates

15-kit_inverse_solution.py works per recording:

  1. Noise covariance: mne.compute_covariance() on the baseline window of every stage-9 epoch file of the recording, all conditions pooled (source.inverse.noise_cov.window pins one window instead of each condition’s baseline). The estimators in method (shrunk, diagonal_fixed, empirical) are all fitted and the one with the best cross-validated log-likelihood is kept; the record lists them. The rank is estimated from the data (rank: null) with a relative tolerance of 1e-6 and used for the covariance and the inverse: the ICA of stage 6 lowers it by the number of removed components (stored in single precision, those dimensions are about 1e-7 of the data, not zero, and MNE’s default estimate would see full rank and inflate them when whitening).

  2. Inverse operator: mne.minimum_norm.make_inverse_operator() with loose (0.2), depth (0.8) and fixed (false), the lab’s settings. fixed: true and loose: 0 describe the same (strictly normal) orientation; MNE requires both to agree, so the stage sets one from the other. depth absent means 0.8; depth: null means no depth weighting.

  3. Source estimates: mne.minimum_norm.apply_inverse() on each condition’s evoked response, lambda2 = 1 / snr**2 (snr 3), method dSPM (or MNE, sLORETA, eLORETA), pick_ori. The evoked is first resampled to resample_hz (100 Hz) so the estimates stay small (an ico-4 estimate is 5124 vertices x samples, float32). Conditions with fewer than min_nave trials (1 by default; 5 in the epilepsy project) get no estimate: run markers such as run_start have a single trial and would only add noise to the group and movie stages.

  4. Morph: mne.compute_source_morph() to morph.subject_to (fsaverage) at morph.spacing (ico 4), applied to every estimate, saved as <recording>_cond-<name>_desc-<method>_space-fsaverage-{lh,rh}.stc.

  5. ROI time courses: mne.extract_label_time_course() on the fsaverage parcellation labels.parc (aparc), labels.names (all by default), labels.mode, one TSV per condition. The mode must fit the estimate: with pick_ori: null (the default) a loose-orientation estimate is the vector norm at every source, a non-negative magnitude, and the label value is its mean (or max). mean_flip and pca_flip multiply the vertices on one side of the label’s dominant normal by -1 before averaging, which only makes sense for signed estimates (fixed: true or pick_ori: normal); applied to a magnitude it cancels one part of the label against the other (on a uniform map of 1 the aparc labels come out between 0 and 1, two of them negative). The default follows the estimate and a mismatch is refused; records written before 2026-10-23 (INVERSE_VERSION 2) used mean_flip on magnitudes and are recomputed by the next run.

  6. Whitened-evoked check: evoked.plot_white(noise_cov) per condition. The evoked divided by the noise covariance has, with the right covariance and rank, a standard-normal baseline and a global field power of about 1 there (whitened_baseline_gfp in the summary, defined as in mne.viz.plot_evoked_white: the evoked scaled by the square root of nave, GFP = sum of the squared whitened signals over the rank, averaged over the window the covariance was computed from); a value far from 1 means the covariance or the rank is wrong and the dSPM scaling with it. The stage reports the value and does not judge it; compare it between recordings and conditions of the same dataset, and look at the figure (the whitened traces and their GFP over time) for the baseline window.

Figures per recording: the covariance matrix and its eigenvalue spectrum, each condition’s estimate at its peak (lateral and medial views of both hemispheres), its whitened evoked, and the ROI time courses. inverse_summary.csv has one row per recording and condition (nave, covariance method and rank, the whitened baseline GFP, peak). peak_value is the (signed) value at peak_time_s of the vertex with the largest absolute value in peak_window; the same holds for the stage-16 peaks. A re-saved stage-12 transform (same file name) makes stage 15 report blocked_stale_forward until stage 14 is rerun: the forward’s sidecar records the transform’s identity (size and content digest), not only its path.

Stage 16: group averages and contrasts

16-kit_group_source.py reads inverse_summary.csv and writes subjects_manifest.json: per subject and condition the recordings that carry it, their nave and STC files. A subject with a condition in two recordings contributes the nave-weighted average, so every subject counts once. Then, for every condition with at least min_subjects subjects, the grand average (group/cond-<name>_...stc), its peak in peak_window, and the ROI time courses (mean and SEM over subjects, group/cond-<name>_desc-labels_mean.tsv and _sem.tsv). source.group.contrasts are {name, a, b} entries; per subject the mean of the a conditions minus the mean of the b conditions, then the mean over subjects (group/contrast-<name>_...stc), with the same ROI treatment. The figures (brain views at the peak, ROI grids) are the group section of the report.

Individual level next to the group level: group/subjects_desc-peaks.tsv lists every subject’s own peak (time, hemisphere, value) for each condition and contrast, and for each contrast group/figures/contrast-<name>_desc-labels_subjects.png draws every subject’s ROI time course (thin lines) under the group mean (black) in the six labels where the group effect is largest, with peak_window shaded. The report shows both after the group figures, so a group peak can be read against the subjects that produced it; the complete per-recording estimates remain stage 15’s.

Stage 17: spatio-temporal cluster permutation tests

17-kit_cluster_stats.py tests every contrast with mne.stats.spatio_temporal_cluster_1samp_test() at the levels in stats.cluster.levels:

  • sensor: per subject the conditions’ evoked responses are combined over recordings (nave weights) with bad channels interpolated (sensor.interpolate_bads), so all subjects share the 207 gradiometers; the adjacency is mne.channels.find_ch_adjacency() (the KIT-208 neighbour template, restricted to the 207 channels).

  • source: the stage-16 subject estimates on fsaverage; the adjacency is mne.spatial_src_adjacency() of the ico-4 source space, with the aparc unknown vertices (medial wall) excluded (exclude_medial_wall); hemi restricts the test to one hemisphere.

Per contrast the array is subjects x times x locations of a - b (resampled to resample_hz, cropped to tmin/tmax or the common window). The cluster-forming threshold is the t value of p_threshold with n_subjects - 1 degrees of freedom (negative for tail: -1, as MNE requires); n_permutations sign flips; tail 0 is two-sided; clusters with corrected p below cluster_alpha are reported as significant. With few subjects MNE enumerates every sign flip (2^n, half for two-sided), so the smallest attainable p is 1 / n_permutations_effective (in the summary with min_attainable_p); a test that cannot reach cluster_alpha is marked underpowered in message. With interpolate_bads: false the union of the subjects’ bad channels is left out of the sensor-level test. Outputs per (level, contrast): clusters.json (every cluster: p, time extent, locations, peak t and where, the peak vertex named by its aparc label), tobs.npz (the t map), for the source level the mne.stats.summarize_clusters_stc() estimate of the significant clusters, and three figures: the cluster map (sensor: t topography at the cluster’s peak with the cluster channels marked; source: signed duration of membership per vertex, red where a > b and blue where a < b, or the t map when no cluster is significant), the time course of a, b and their difference averaged over the largest cluster’s locations (group mean and SEM), and the individual view of that cluster (figures/cluster_subjects.png): every subject’s own a - b on the cluster’s locations (thin lines, group mean in black) and each subject’s mean difference inside the cluster (dots beside the group mean and SEM). The same numbers are in cluster_subjects.tsv and in clusters.json (subject_effects). cluster_stats_summary.csv has one row per (level, contrast); its n_subjects_same_sign column (5/6) counts the subjects whose difference has the sign of the group effect, the quickest check that a significant cluster is not carried by one participant.

Permutation p-values cannot be smaller than 1 / n_permutations; 1024 is a screening value, use 5000 or more for a publication figure.

Stage 18: RDMs, RSA and decoding

18-kit_rsa_decoding.py has two parts, each with per-subject and group outputs.

RSA (rsa, with mne-rsa, installed in the MNE environment). Per subject the condition patterns form an array conditions x locations x times (rsa.space: the stage-16 subject estimates on fsaverage, or the evoked fields). mne_rsa.searchlight with a temporal radius of temporal_radius_s and mne_rsa.rdm_array give the neural RDM at every time point (metric, correlation by default); each factor of events.conditions in rsa.models (all by default) gives a model RDM (0 for a shared level, 1 otherwise); mne_rsa.rsa_array gives the rsa_metric correlation between the neural and the model RDM at every time point: spearman, or partial-spearman (each model’s Spearman correlation with the other models partialled out). Use partial-spearman when the factors are crossed, as in KidLang (every task has a phrase and an object condition): the task and condition model RDMs are then correlated with each other, and a plain Spearman value for one factor partly reflects the other, to the point of turning negative when the other factor dominates the patterns. With a single model the stage falls back to spearman. Per subject: sub-*_desc-rdm.npz (the RDM time series), sub-*_desc-rsa.tsv, figures of the RDM in rdm_windows and of the RSA time courses. Group: group/rsa_group.tsv (mean and SEM), group/rdm_group.npz, the spans where the RSA value exceeds zero across subjects (mne.stats.permutation_cluster_1samp_test(), one-sided, rsa.cluster, one test per model), the model RDMs, the group RDM, and an MDS embedding of the group RDM (scikit-learn MDS on the precomputed dissimilarities). The group RSA figure has one panel per model, with every subject’s curve under the group mean and that model’s significant spans shaded.

Decoding (decoding, MNE’s mne.decoding.SlidingEstimator). For each two-class contrast (decoding.contrasts, default the group contrasts) the stage-9 single trials of the a and b conditions are collected over the subject’s recordings (bad channels interpolated, resampled, cropped to window), a pipeline of StandardScaler and LogisticRegression (or a linear SVM, classifier) is trained at every time point and scored by cv-fold stratified cross-validation with scoring (ROC AUC: 0.5 chance). generalization: true adds the mne.decoding.GeneralizingEstimator matrix. Group: mean and SEM drawn over every subject’s curve, the spans above chance (cluster test, one-sided), figures. A subject whose two classes come from different recordings is flagged cross_recording in the summary: the head position differs between recordings and a sensor-level classifier can pick it up, so within-recording contrasts are the ones to trust at the sensor level.

rsa_decoding_summary.csv has one row per analysis, model or contrast and subject, plus group rows.

KidLang settings

kidlang_config.yml carries the catalogue of the 12 trigger IDs (4 tasks x phrase/object/baseline), the epoch windows of the KidLang guide (-0.7 to 2 s with the fixation baseline for dialogue, listening and reading; -0.1 to 5 s for production), decim: 4 so the stage-9 epoch files stay small, the lab’s inverse settings (ico-4, single-layer BEM, loose 0.2, depth 0.8, snr 3, dSPM, morph to fsaverage), 16 language-network and sensory ROIs (source.inverse.labels.names: bilateral superior and middle temporal, pars opercularis and triangularis, supramarginal, fusiform, lateral occipital, precentral) and contrasts for phrase vs object (all tasks and per task), tones 1 vs 2 and phrase vs baseline (undefined for the recordings converted so far: the baseline trigger IDs 65/66/68/72 of the guide are not in the data, split 1 holds the 8 phrase/object IDs and split 2 the two-tone recording with codes 1 and 2). The RSA and decoding windows are [-0.2, 1.5] so the production epochs are compared on the same range as the others. First run (6 ICA-reviewed subjects, 2026-10-12): template scale factors 0.89 to 1.10, head-shape medians 2.4 to 3.2 mm; the RSA task model is significant from about 50 ms to 1.3 s (the tasks differ in modality), the tone contrast decodes above chance at 140 to 180 ms, and single clusters reach p = 0.03 for tones (sensor), dialogue phrase vs object (sensor) and listening phrase vs object (source) with 1024 permutations.

Stage 19: source estimates over time (slider)

19-kit_source_movie.py shows every source estimate the way the MNE-BIDS-Pipeline report does (mne.Report.add_stc): a series of frames, one per time point, browsed with a slider in the HTML report (move the slider, or click on the image and use the arrow keys; it starts at the frame closest to 0 s). Two levels:

  • individual: one estimate per subject and condition, from the stage-15 estimates on fsaverage (a subject’s recordings of the same condition are averaged with nave weights, as in stage 16);

  • group: one per grand average and per contrast of stage 16.

Every frame shows the inflated cortex of both hemispheres, lateral views on top and medial views below (MNE’s hemi='split' layout), drawn like MNE’s 3-D Brain: the binary curvature shading of the classic cortex, the estimate smoothed onto the full surface (smoothing_steps, as SourceEstimate.plot), MNE’s transparent colour table (hot for one-sided values such as dSPM, mne blue-red for contrasts), a colour bar and the time of the frame. MNE’s own Brain needs a visible Qt window and returns a black image when rendered hidden on the lab workstation, so the frames are drawn off-screen with PyVista directly (brain_render.py), reusing MNE’s colour logic; without a working PyVista the stage falls back to MNE’s flat matplotlib backend and says so (renderer in the summary).

By default there are 51 equidistant time points over the whole estimate (as mne.Report); step_ms gives one frame every step_ms instead. The colour scale is computed once over the window and kept fixed (clim: auto: MNE’s 96 / 97.5 / 99.95 percentiles of the absolute values, or an explicit {kind: value, lims: [low, mid, high]}), so a brighter frame means more activity at that instant; values below the lowest limit are transparent.

Configuration (source.movies):

source:
  movies:
    enabled: true
    levels: [individual, group]
    conditions: []            # names from events.conditions; [] = all
    contrasts: true           # group level: also the stage-16 contrasts
    tmin: null                # seconds; null = whole estimate
    tmax: null
    n_frames: 51              # equidistant time points (mne.Report uses 51)
    step_ms: null             # or one frame every step_ms (at most max_frames)
    max_frames: 120
    renderer: auto            # auto | pyvista | matplotlib
    hemis: [lh, rh]
    views: [lateral, medial]  # also dorsal, ventral, rostral, caudal
    surface: inflated
    smoothing_steps: 10
    panel_size: [450, 340]    # pixels per view
    colormap: auto            # hot (one-sided) / mne (contrasts)
    clim: auto
    clim_percentiles: [96, 97.5, 99.95]
    formats: [html]           # besides the frames: html (slider page), mp4, gif
    jpeg_quality: 88
    fps: 8                    # mp4 / gif only
    timeline: false           # true adds a strip with peak |value| and a cursor

Outputs under derivatives/19-kit_source_movie/ (sub-<id>/ or group/), per estimate: *_frames/frame-NNN.jpg (the frames the report’s slider shows), *_movie.json (frame list, times, start frame, colour scale and what the frames were made from), *_slider.html (a self-contained slider page that opens in any browser), optionally *_movie.mp4 / *_movie.gif (videos of the same frames; formats), and *_desc-peakframe.png, the frame at the peak. source_movie_summary.csv has one row per estimate: level, subject, kind, name, renderer, n_frames, tmin, tmax, step_ms, clim, peak_time_s, the files, status (created | skipped_existing_output | failed) and message.

Frames are re-rendered when the stage-15 or stage-16 estimate changed (a new coregistration, ICA, new epochs) or when the settings changed; otherwise they are reused. --overwrite re-renders anyway; --levels individual or --levels group restricts a run. Rendering takes about a second per frame (four views), so 51 frames take under a minute per estimate. The HTML report embeds every frame (about 80 kB each as JPEG).

Stage 20: equivalent current dipole fits

20-kit_dipole_fit.py fits equivalent current dipoles (ECD) to the evoked components named in dipoles.fits. Where stage 15 spreads the activity over the cortex, a dipole fit asks the classical localiser question: where is the one generator of this component, how strong is it, and how much of the measured field does one point source explain. It is the quantity clinicians read and the right model for the primary visual (pattern reversal), auditory (N100m) and motor responses.

Per fit (a name, a task, the stage-8 event codes to pool, a time window and n_dipoles):

  1. The stage-9 evoked responses of the codes are pooled over the recordings of the task (nave weights); MEG channels only, bad channels dropped.

  2. The noise covariance is the stage-15 one of the recording (dipoles.noise_cov: stage15) or is computed from the baselines of the stage-9 epochs (baseline, cov_method). When stage 7 removed ICA components, their sensor topographies are applied as SSP projectors to the evoked field and to the dipole’s forward field (the data no longer contain them, the model would), also for the explained-variance numbers and the field figure; the record says how many (ica_components_projected).

  3. The head model is the transform of source.coregistration (the reviewed stage-12 transform or the automatic stage-13 one) and the stage-14 BEM solution of the subject.

  4. mne.fit_dipole fits a free dipole (position and orientation) at every sample of the window. n_dipoles: 1 uses all sensors; n_dipoles: 2 fits the left and the right half of the array separately (sensors split at the head midline), the standard way to obtain one dipole per hemisphere for a bilateral response.

  5. The instant with the best goodness of fit is the reported dipole: position in the head frame, in the subject’s MRI and in MNI space, orientation, amplitude (nAm), GOF and MNE’s confidence volume. The covariance handed to fit_dipole is divided by the number of averaged trials, so the confidence volume is that of the fitted average (MNE’s fit_dipole itself does not scale it; position, orientation and GOF do not depend on the scale). A GOF below dipoles.min_gof is flagged (gof_below_<x>); ica_not_projected in warning says that an ICA solution could not be applied to the fit, no_mni_coordinates that the MNI position could not be computed (the record names the reason).

How much the dipole explains. gof_percent is the goodness of fit mne.fit_dipole reports at the best instant over the fitted sensors: the explained variance of the noise-whitened field (every sensor weighted by the noise covariance; 100 % = the dipole reproduces the whitened field exactly); gof_window_mean_percent its mean over the window. The field of each reported dipole is then recomputed on all sensors (mne.make_forward_dipole) and compared with the measured field in raw units: explained_all_sensors_percent per dipole at its own best instant and explained_all_dipoles_percent for the sum of the dipoles at the common instant. The raw-field number is usually the higher of the two (the whitening emphasises the sensors whose residual is large relative to their noise); the GOF is the number to compare with the literature and with dipoles.min_gof. Figures per fit: the measured field across the window (a sequence of topomaps), the measured field, the field of each dipole, their sum and the residual at the best instant on one colour scale, the GOF and amplitude at every sample, and the dipole on the MRI (Dipole.plot_locations, the scaled template when the automatic coregistration is used).

Configuration (dipoles), with the epilepsy project as the example:

dipoles:
  enabled: true
  noise_cov: stage15      # or baseline
  cov_method: shrunk
  min_gof: 70
  fits:
    - {name: visual_reversal, task: flippedsquares, codes: ["224", "225"], window: [0.06, 0.16], n_dipoles: 1}
    - {name: auditory_tones,  task: twotones,       codes: ["224", "225"], window: [0.07, 0.14], n_dipoles: 2}
    - {name: motor_press_left, task: buttonlr,      codes: ["226"],        window: [-0.09, 0.0],  n_dipoles: 1,
       sensors: right, fit_at: field_peak}    # left hand: motor field of the right motor cortex

Outputs under derivatives/20-kit_dipole_fit/sub-<id>/: *_fit-<name>_hemi-<all|left|right>_desc-dipole.dip (mne.read_dipole), *_desc-dipole_timecourse.tsv (time, GOF, amplitude, position, orientation at every sample), *_desc-dipole.json (provenance, the reported dipoles, the explained-variance numbers), the figures, and dipole_fit_summary.csv with one row per fit and dipole. --fits <name> ... restricts a run.

A poor GOF has three usual causes: the component is not focal (several simultaneous generators), the window misses its peak, or the head model is poor (template anatomy, head movement between the markers, see stage 4).

Two options shape a fit further. sensors: left | right (with n_dipoles: 1) fits one dipole on one half of the array only, for a response of one hemisphere such as the motor field of the hand opposite it. fit_at: field_peak reports the dipole at the instant where the field over the fitted sensors is strongest in the window (the component’s peak) instead of the instant with the best GOF (best_gof, default); the GOF figure shows the field RMS, the GOF and the amplitude across the window with the reported instant marked. When the strongest field is at the first or the last sample, there is no peak inside the window (the field is still rising or decaying at its edge); the fit is flagged field_peak_at_window_edge.

Motor responses. Relative to a button press the motor cortex is seen best in the motor field (MF), which builds up over the last 100 ms and peaks just before the press (window about -100 to -10 ms, one dipole over the hemisphere opposite the hand, at the field peak). After the press the movement-evoked field (about +20 to +60 ms) is the somatosensory feedback of the moving finger (postcentral cortex), not the motor command. The baseline of the press epochs must precede the cue that triggers the movement (epoching.tasks.<task>.windows.<code>). Check the trigger delay of the response device: a press trigger written on the next video frame lags the press by about one frame.

When the two dipoles of a two-dipole fit land within dipoles.min_separation_mm (30 mm) of each other, both halves of the array fitted the same source, usually an ocular or other midline artefact (eye movements and blinks give a strong frontal field that both halves see); the fit is flagged dipoles_converged_<d>mm and the sum of the two dipoles counts that source twice. Exclude the ocular ICA components (stage 6, with the review window) before fitting motor or late responses.

Stage 21: spectra and band powers of long recordings

21-kit_sleep_spectra.py describes recordings without events (sleep, rest) by their power spectrum and the band powers over time. Per recording of spectra.tasks (the stage-7 filtered FIF, or the stage-2 FIF with spectra.input: raw; the 207 gradiometers, bad channels dropped):

  1. Whole recording: Welch spectrum per sensor between fmin and fmax (segments of welch_segment_s; fmin is raised to the high-pass of the input when the data were filtered above it, so the filter roll-off is not counted), the absolute and relative power of every band per sensor, the mean spectrum and its alpha peak. Band powers integrate the closed interval [low, high] with the trapezoid rule, so adjacent bands share their edge bin and tile the total exactly. The relative band power of a recording is the band power of the sensor-mean spectrum over its total (the per-sensor values of the topomaps are per sensor). The alpha peak is a local maximum strictly inside the alpha band after removing the 1/f trend (a line in log-log), at least 26 % above it; without one it is NaN (most of sleep has no alpha peak; the argmax of a 1/f spectrum is the band edge and was reported as a peak before 2026-10-23).

  2. Over time: windows of window_s every step_s (the stage’s own events and mne.Epochs, so step_s may exceed window_s), one spectrum per window, the band powers of the sensor-mean spectrum (absolute, and relative to its power in fmin..``fmax``), the alpha peak and the ratio (alpha over delta by default: it falls with sleep depth and rises on arousal).

  3. Over the session: when a task spans several runs, the windows of all runs are placed on one clock from scans.tsv (acq_time), with the run starts marked.

Configuration (spectra):

spectra:
  tasks: [sleep]
  input: filtered         # the stage-7 band-pass bounds the bands (1 to 40 Hz in the epilepsy project)
  fmin: 0.5
  fmax: 40
  bands: {delta: [1, 4], theta: [4, 8], alpha: [8, 12], sigma: [12, 16], beta: [16, 30]}
  window_s: 30
  step_s: 15
  welch_segment_s: 4
  ratio: [alpha, delta]

Outputs under derivatives/21-kit_sleep_spectra/sub-<id>/[ses-<id>/]: *_desc-psd.tsv (mean spectrum), *_desc-psd_sensors.npz (per sensor), *_desc-bandpower_sensors.tsv, *_desc-bandpower_timecourse.tsv (one row per window), *_desc-spectra.json, the figures (spectrum with the sensor spread and band shading, relative band power per sensor as topomaps, spectrogram, band powers and ratio over time, and the session curve), and spectra_summary.csv (one row per recording, with the mean relative power of every band and the alpha peak). Units are fT²/Hz. Existing recordings are skipped unless the settings or the input FIF changed (a stage-7 rerun) or --overwrite is given; the data are read only when a recording is analysed. Windows start every step_s (which may exceed window_s); windows over a BAD annotation are dropped; a recording shorter than one window keeps its whole-recording spectrum and is flagged. Run start times for the session clock are taken on the UTC clock from scans.tsv (matched on the BIDS entities, acquisition included), else from the FIF. Stage 4 and the stage-2 scans.tsv draft use the same UTC clock (a value without a time zone is read as UTC).

Troubleshooting

Symptom

What to do

Stage 14 reports missing_coregistration

No complete stage-12 row and no stage 13 transform for that subject. Run 13-kit_auto_coreg.py (or stage 12) first.

Stage 14 reports missing_anatomy naming bem/inner_skull.surf

The subject uses an individual reconstruction without BEM surfaces. Run mne watershed_bem -s sub-<label> -d <subjects_dir> in the FreeSurfer environment (WSL on Windows), then rerun stage 14.

Stage 15 no stage-9 epochs with a baseline window

The stage-9 windows have no baseline (baseline: null) and source.inverse.noise_cov.window is null. Set a window.

Stage 16 too_few_subjects

Fewer subjects than source.group.min_subjects have that condition in stage 15; lower the threshold for a pilot or process more subjects.

Stage 17 different sampling rates

Stage 15 ran with different resample_hz values across subjects. Rerun stage 15 with --overwrite on one setting.

Stage 18 mne-rsa is not installed

python -m pip install mne-rsa in the MNE environment.

A figure is missing in the report

The stage’s JSON record names the error (*_error keys); the stage continues without the figure so the numbers are still produced.