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 |
|---|---|---|---|
13 |
|
stage-2 |
|
14 |
|
stage-12 or 13 transform, the anatomy it refers to, stage-9 |
|
15 |
|
stage-9 |
|
16 |
|
stage-15 estimates and ROI time courses |
|
17 |
|
stage-9 evoked (sensor level), stage-15/16 estimates (source level) |
|
18 |
|
stage-15/16 estimates or stage-9 evoked (RSA), stage-9 epochs (decoding) |
|
19 |
|
stage-15 estimates (individual), stage-16 averages and contrasts (group) |
|
20 |
|
stage-9 evoked, stage-15 covariance (or stage-9 epochs), stage-12/13 transform, stage-14 BEM |
|
21 |
|
stage-7 filtered FIF (or stage-2) of |
|
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 reconstructionsub-<label>, not scaled. The scalp is the stage-11bem/sub-<label>-head-dense.fif; the MRI fiducials arebem/sub-<label>-fiducials.fifwhen stage 12 saved them, otherwise estimated from fsaverage’s through the reconstruction’s talairach transform (MNEfiducials="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):
fit_fiducials: the digitised nasion, LPA and RPA are matched to the template fiducials (bem/fsaverage-fiducials.fif).fit_icpforicp_iterations: iterative closest point on the digitised head shape (the KIT laser scan, about 4000 points), withscale_modenone,uniformor3-axisscaling of the template. The lab’s manual practice for children was 3-axis scaling, which is the default.Points farther than
omit_distance_mmfrom the scalp after that fit are omitted (omit_head_shape_points) and the ICP repeated.With scaling,
mne.scale_mri()writes the scaled template assub-<label>next to fsaverage (coregistration.automatic.subjects_dir, defaultderivatives/13-kit_auto_coreg/subjects; fsaverage is fetched there withmne.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.coregistrationpicks the stage-12 transform when its row incoreg_summary.csviscompleteand the file is readable, otherwise the stage 13 transform (manual_then_auto);manualandautorestrict the choice (the pre-renumbering spellingsstage8_then_auto/stage8are still accepted). The anatomy is themri_subjectthat transform refers to, in its SUBJECTS_DIR.mne.setup_source_space()(source.forward.spacing, defaultico4= 2562 sources per hemisphere,add_dist='patch'), saved assub-<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 assub-<label>_bem-sol.fif. fsaverage and its scaled copies ship that surface; an individual FreeSurfer reconstruction needs the watershed surfaces (mne watershed_bemin the FreeSurfer environment), otherwise the stage reportsmissing_anatomywith that instruction.mne.make_forward_solution()withmeg=True, eeg=False,mindistandignore_ref=True.ignore_refis 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:
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.windowpins one window instead of each condition’s baseline). The estimators inmethod(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).Inverse operator:
mne.minimum_norm.make_inverse_operator()withloose(0.2),depth(0.8) andfixed(false), the lab’s settings.fixed: trueandloose: 0describe the same (strictly normal) orientation; MNE requires both to agree, so the stage sets one from the other.depthabsent means 0.8;depth: nullmeans no depth weighting.Source estimates:
mne.minimum_norm.apply_inverse()on each condition’s evoked response,lambda2 = 1 / snr**2(snr3),methoddSPM (or MNE, sLORETA, eLORETA),pick_ori. The evoked is first resampled toresample_hz(100 Hz) so the estimates stay small (an ico-4 estimate is 5124 vertices x samples, float32). Conditions with fewer thanmin_navetrials (1 by default; 5 in the epilepsy project) get no estimate: run markers such asrun_starthave a single trial and would only add noise to the group and movie stages.Morph:
mne.compute_source_morph()tomorph.subject_to(fsaverage) atmorph.spacing(ico 4), applied to every estimate, saved as<recording>_cond-<name>_desc-<method>_space-fsaverage-{lh,rh}.stc.ROI time courses:
mne.extract_label_time_course()on the fsaverage parcellationlabels.parc(aparc),labels.names(all by default),labels.mode, one TSV per condition. The mode must fit the estimate: withpick_ori: null(the default) a loose-orientation estimate is the vector norm at every source, a non-negative magnitude, and the label value is itsmean(ormax).mean_flipandpca_flipmultiply the vertices on one side of the label’s dominant normal by -1 before averaging, which only makes sense for signed estimates (fixed: trueorpick_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_VERSION2) usedmean_flipon magnitudes and are recomputed by the next run.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_gfpin the summary, defined as inmne.viz.plot_evoked_white: the evoked scaled by the square root ofnave, 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 ismne.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 aparcunknownvertices (medial wall) excluded (exclude_medial_wall);hemirestricts 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):
The stage-9 evoked responses of the codes are pooled over the recordings of the task (nave weights); MEG channels only, bad channels dropped.
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).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.mne.fit_dipolefits a free dipole (position and orientation) at every sample of the window.n_dipoles: 1uses all sensors;n_dipoles: 2fits 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.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_dipoleis divided by the number of averaged trials, so the confidence volume is that of the fitted average (MNE’sfit_dipoleitself does not scale it; position, orientation and GOF do not depend on the scale). A GOF belowdipoles.min_gofis flagged (gof_below_<x>);ica_not_projectedinwarningsays that an ICA solution could not be applied to the fit,no_mni_coordinatesthat 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):
Whole recording: Welch spectrum per sensor between
fminandfmax(segments ofwelch_segment_s;fminis 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).Over time: windows of
window_severystep_s(the stage’s own events andmne.Epochs, sostep_smay exceedwindow_s), one spectrum per window, the band powers of the sensor-mean spectrum (absolute, and relative to its power infmin..``fmax``), the alpha peak and theratio(alpha over delta by default: it falls with sleep depth and rises on arousal).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 |
No complete stage-12 row and no stage 13 transform for that subject.
Run |
Stage 14 reports |
The subject uses an individual reconstruction without BEM surfaces.
Run |
Stage 15 |
The stage-9 windows have no baseline ( |
Stage 16 |
Fewer subjects than |
Stage 17 |
Stage 15 ran with different |
Stage 18 |
|
A figure is missing in the report |
The stage’s JSON record names the error ( |