DocsPipeline Recipes
BCILattice Documentation

Pipeline Recipes

Build channel selection, dimensionality reduction, cross-validation, LOSO and domain adaptation from blocks — starting from nothing but Filtered recordings.

v1.0BCINexus Platform · 2026-05-20[email protected]

Before you start

Every recipe on this page starts from the same place and adds blocks. They are written for the case where you have nothing but the recordings — no finished Analysis stage to inherit — so they work identically on every plan.

The starting point

Drop a Select Files block and set:

SettingSet toWhy
modalityEEG / fNIRS / EMGWhichever you recorded
data_sourceFilteredThe band-passed recording — the one pre-analysis stage, available on every plan
sessionAll SessionsOr one session, if you are deliberately scoping
selected_subjects(pick in the dialog)More than one subject is required for anything cross-subject

That block now emits X (the signal table) and Y (labels), plus Channels for a recording-stage source. Everything below hangs off those two ports.

Why start from Filtered rather than a finished stage

Reusing a finished Analysis stage is a Researcher feature, and it is also less portable: the processing lives outside the pipeline, so the graph does not describe what was actually done. Built from blocks, the processing travels with the pipeline, recompiles with it, and is readable by anyone who opens it.

MLFlow, Workflow, or both

Any of these recipes can be built entirely in the Workflow canvas — that is the simplest thing to do, and for a one-off analysis it is the right answer. Build in MLFlow instead when you want the same chain reused across several workflows or studies. The combined section below shows the split.

Channel selection

Keeps the channels that actually separate your classes and drops the rest. On high-density montages this is usually the single biggest win: fewer, better channels means fewer parameters, less overfitting, and a model that trains in seconds.

Wiring it

[Select Files]                    [Channel Selection]
  data_source: Filtered
      X  ──────────────────────────▶ Data
      Y  ──────────────────────────▶ Labels
                                       Selected Data ────▶ (next stage)
                                       Selected Channels ─▶ (optional: Data Graph)
                                       Scores ────────────▶ (optional: Data Graph)
FromTo
Select Files · XChannel Selection · Data
Select Files · YChannel Selection · Labels
Channel Selection · Selected Datathe next block in the chain
SettingSet toWhy
method_namet-Value MethodA solid default for two-class contrasts
percentile5Keeps the top 5% of channels by score. Raise it if you are dropping too much
rest_label0The integer label of your rest / baseline class
ignore_classes(blank)Comma-separated labels to leave out of the ranking

See what it did

Wire Selected Channels and Scores into a Data Graph. It is a pass-through tap, so it costs you nothing in the graph and shows exactly which channels survived and why.

Choosing a method

Twenty methods are available. In practice:

UseWhen
t-Value MethodTwo classes, roughly normal — the default, and hard to beat
ANOVA F-valueMore than two classes
Common Spatial Pattern (CSP)Motor imagery. The classic choice for left/right hand EEG
Regularized CSP / Sparse CSPCSP on few trials or noisy data, where plain CSP overfits
Mutual Information Feature SelectionYou suspect a non-linear relationship
Wilcoxon Signed-Rank TestNon-normal distributions, paired design
PSD-based SelectionThe discriminating signal is a band-power difference

Dimensionality reduction

Projects many correlated channels onto a handful of components. Useful after channel selection, when what remains is still wider than your trial count.

Wiring PCA

PCA is a class block, so it has a selected_method — set it to fit_transform and its ports become X, y and one output.

[Channel Selection]                [PCA]  selected_method: fit_transform
  Selected Data ────────────────────▶ X            n_components: 20
  (labels from Select Files) ───────▶ y
                                       output ─────▶ (next stage)
SettingSet toWhy
selected_methodfit_transformFits and projects in one step, which is what a pipeline stage needs
n_components10–30Start well below your trial count. 20 is a reasonable first guess
whitenfalseTurn on only if the downstream model assumes unit variance

FastICA, KernelPCA and TruncatedSVD wire identically. Use FastICA when you want statistically independent sources rather than maximum-variance ones, KernelPCA when the structure is non-linear.

Where it belongs

Fit it inside the folds, not before them

A reduction fitted on the whole dataset has already seen your test rows — the score that comes back will be optimistic. If you are cross-validating, put the reduction inside the model you hand to Cross-Validation rather than upstream of it, so it is refitted per fold. Upstream is fine when you are training a single split and reporting held-out performance from the trainer.

Feature selection

Where dimensionality reduction builds new combined components, feature selection keeps a subset of the original columns — which stays interpretable, because every surviving feature is still a real channel or band.

SettingSet toWhy
SelectKBest · k20–50Number of features to keep
SelectKBest · score_funcf_classifANOVA F between each feature and the label
SelectPercentilepercentile: 10Use instead of SelectKBest when feature count varies between subjects
VarianceThresholdthreshold: 0.0Cheap first pass — drops dead / constant channels
RFEn_features_to_selectRecursive elimination. Strong but slow: it refits the model repeatedly

Class blocks need a selected_method

VarianceThreshold and RFE are class blocks, like PCA — set selected_method to fit_transform and their ports become X / youtput. SelectKBest and SelectPercentile are plain function blocks and need no method chosen.

The same fold-leakage warning applies: selection is fitting, so inside the folds it goes.

Cross-validation

Trains nothing itself. It splits, hands each fold to the estimator you wire in, and reports per-fold and aggregate metrics with a confidence interval.

Wiring it

[Channel Selection]              [Cross-Validation]
  Selected Data ──────────────────▶ Dataset          cv_strategy: stratified_k_fold
[Select Files]                                       n_splits: 5
  Y ──────────────────────────────▶ Labels
[a model block]
  model_obj ──────────────────────▶ Model
                                     Metrics ──────────▶ [Output Block]
                                     Confusion Matrix ─▶ [Data Graph]
                                     Fold Scores ──────▶ [Data Graph]
FromTo
Channel Selection · Selected DataCross-Validation · Dataset
Select Files · YCross-Validation · Labels
your model block · model_objCross-Validation · Model
Cross-Validation · MetricsOutput Block
Cross-Validation · Confusion MatrixData Graph (optional)

Picking a strategy

SettingSet toWhy
stratified_k_fold(default)One subject, balanced classes. The everyday choice
k_foldRegression, or where stratification is meaningless
group_k_foldMultiple subjects, but you only need folds not to split a subject
stratified_group_k_foldBoth: grouped AND class-balanced. Classification only
leave_one_group_outLOSO / leave-one-session-out — see below
time_seriesOrdered data where a fold must not train on the future
repeated_stratified_k_foldn_repeats: 10Small datasets, where a single 5-fold estimate is noisy

LOSO (cross-subject)

Leave-One-Subject-Out holds out an entire subject per fold. It is the honest way to answer “will this work on someone the model has never seen” — and the number it returns is almost always lower than a within-subject score, which is the point.

The pooling step nobody expects

A normal run sees one subject at a time

Training fans out one job per subject by default. Inside such a run there is only one subject, so leave-one-subject-out has nothing to hold out and the run fails with “needs at least 2 distinct groups”. You have to pool first.

Set the Input Block's subject_scope to all. That collapses every selected subject into a single run, which is what gives the CV block real subject groups to hold out.

Wiring it

SettingSet toWhy
Input Block · subject_scopeallPools every selected subject into one run — the step that makes LOSO possible
Cross-Validation · cv_strategyleave_one_group_outOne fold per group
Cross-Validation · group_bysubjectGroups are subjects. Use session for leave-one-session-out
Select Files · selected_subjects3 or moreTwo subjects gives you two folds — technically valid, statistically meaningless

You do not need to wire the Groups port. Leave it empty and the block derives per-sample subject and session ids from the run's own provenance, selected by group_by. Wire it only when you have group ids of your own that the provenance does not know about.

group_by: auto is smarter than it looks

On auto the block prefers whichever of subject or session actually has more than one distinct value — so a single-subject multi-session recording automatically becomes cross-session rather than failing.

The leakage guard

strict_leakage_check is on by default, and it will stop a run rather than let it report a number you would have to retract.

It fires when:

  • you use a non-grouped strategy on data spanning more than one subject or session — trials from the same person land in both train and test, and the score measures memorisation;
  • group ids were present but had to be dropped, e.g. a block changed the row count and the groups no longer align with X.

The fix is almost always to switch to group_k_fold or leave_one_group_out, not to turn the check off. Turn it off only when you have a specific reason and know what the resulting number does and does not mean.

Domain adaptation

Cross-subject and cross-session models fail mostly because each recording sits in a slightly different feature space — different impedances, cap placement, day. Domain adaptation pulls those spaces together before the model sees them.

Euclidean Alignment

Start here. It is cheap, needs no target labels, and reliably lifts cross-subject scores — the standard first thing to try for transfer EEG.

[Channel Selection]                [Euclidean Alignment]
  Selected Data ────────────────────▶ Data          reference_mode: mean
                                    ▶ Reference (optional)
                                       Aligned Data ────▶ [Cross-Validation] Dataset
FromTo
Channel Selection · Selected DataEuclidean Alignment · Data
Euclidean Alignment · Aligned DataCross-Validation · Dataset

CORAL / MMD / TCA

These take an explicit source and target, so they suit the setup where you train on a group of subjects and adapt to one held-out person. Use Data Splitter or two Select Files blocks to produce the two streams.

BlockInputs → OutputsUse when
CORAL AdapterSource Data, Target Data → Aligned Source, Alignment MatrixFast, linear second-order alignment. The usual first choice after EA
MMD AlignerSource Data, Target Data → Aligned Data, MMD ScoreDistribution-level alignment; MMD Score tells you how far apart they were
Transfer Component AnalysisSource Data, Target Data → Source Projected, Target ProjectedLearns a shared subspace. Stronger, slower, needs tuning of n_components and mu
Feature AlignmentSource Data, Target Data → Aligned DataSimple z-score / moment matching. Good baseline to beat
Session TransferSource Features, Target Features, Source Labels → Transferred Features, Sample WeightsSame subject, different day

Subject Adaptation

Different shape from the rest: it takes a trained model and a small amount of labelled data from the new person, and returns a model tuned to them. This is the calibration-session workflow — a few minutes of data from a new user instead of a full training session.

FromTo
BCILattice Trainer · Trained ModelSubject Adaptation · Base Model
(new subject) Select Files · XSubject Adaptation · Adaptation Data
(new subject) Select Files · YSubject Adaptation · Adaptation Labels
Subject Adaptation · Adapted ModelOutput Block
SettingSet toWhy
methodfine-tuneOr a lighter scheme if you have very few shots
n_shots20Labelled trials from the new subject. This is your calibration budget
lr0.0001Low on purpose — you are nudging a trained model, not retraining it

The combined pipeline

Everything above, in one graph: filtered recordings in, an honest cross-subject number out.

[Select Files]                data_source: Filtered, 8 subjects
   X ──┐
   Y ──┼──────────────────────────────────┐
       │                                  │
       ▼                                  │
[Channel Selection]  t-Value, top 5%      │
   Selected Data                          │
       │                                  │
       ▼                                  │
[Euclidean Alignment]  reference: mean    │
   Aligned Data                           │
       │                                  │
       ▼                                  ▼
[Cross-Validation]  Dataset            Labels
   cv_strategy: leave_one_group_out
   group_by: subject
   strict_leakage_check: on
       │            ▲
       │            └──── Model ──── [a model block]
       ├── Metrics ─────────────▶ [Output Block]
       ├── Confusion Matrix ────▶ [Data Graph]
       └── Fold Scores ─────────▶ [Data Graph]

  + Input Block subject_scope: all      ← the pooling step LOSO needs

Splitting it across MLFlow + Workflow

Once that chain works, the reusable half is everything between the data and the validation. Move it into an MLFlow pipeline:

MLFlow — "EEG Preprocess"          Workflow
┌──────────────────────────┐       ┌────────────────────────────────┐
│ [Input Block] Signal ─┐  │       │ [Select Files]                 │
│ [Input Block] Labels ─┼─▶│       │   X ──▶ Signal ┌─────────────┐ │
│      [Channel Selection] │  ═══▶ │   Y ──▶ Labels │EEG Preprocess│ │
│      [Euclidean Align.]  │       │                │      Clean  ─┼─┼─▶ [Cross-Validation]
│ [Output Block] Clean     │       │                └─────────────┘ │
└──────────────────────────┘       └────────────────────────────────┘

Two Input Blocks and one Output Block become two input ports and one output port on the compiled block, named from each block's port_name. Point a second Select Files at fNIRS instead and the same compiled block runs on that. See the port contract.

Sanity checklist

Before you believe a number, check these:

  • Is the split grouped? Multi-subject data with a plain k-fold measures memorisation. The leakage guard catches this — do not switch it off to make the error go away.
  • Did anything fit outside the folds? A reduction or feature selection fitted upstream of Cross-Validation has seen the test rows.
  • Is the score above chance? The CV report carries theoretical and empirical chance levels. Compare against the empirical one — with imbalanced classes it is higher than 1/n.
  • Is the confidence interval narrow enough to mean anything? A 5-fold run on 40 trials produces a wide one. Use repeated_stratified_k_fold.
  • Did you pool for LOSO? Without subject_scope: all, a cross-subject strategy has one subject to work with.
  • Two models to compare? Use Statistical Comparison — McNemar or a paired test — rather than eyeballing two accuracies.

Block-by-block detail: Custom Blocks and Advanced Blocks. How the canvases fit together: Using the ML Suite.

Pipeline Recipes v1.0 · BCINexus Platform · 2026-05-20