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 Filesdata_source: FilteredXYChannelsFilesFile PathsChannel Selectionmethod_name: t-Value Methodpercentile: 5DataLabelsSelected DataSelected ChannelsScoresnext blockDataData GraphDataGraphDataGraph FileGraph ErrorData GraphDataGraphDataGraph FileGraph Error
Dashed wires are optional: the two Data Graphs only show which channels survived and their scores.
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 SelectionDataLabelsSelected DataSelected ChannelsScoresSelect FilesXYChannelsFilesFile PathsPCAselected_method: fit_transformn_components: 20Xyoutputnext blockData
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 the trainer 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 / y → output. 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

There is one way to do it: wire a Validation Scheme into the trainer's Val/Eval input. The trainer then trains once per fold and reports per-fold and aggregate metrics with a confidence interval, instead of holding out one split. Left empty, the port reads one hold-out split. Select the trainer and the canvas suggests the scheme; one click adds it already wired.

Wiring it

Channel SelectionDataLabelsSelected DataSelected ChannelsScoresEEGNet (any model block)model_objSelect FilesXYChannelsFilesFile PathsValidation Schemescheme: stratified_k_foldn_splits: 5ValidationBCILattice TrainerDatasetModelLabelsVal/EvalAdditional MetricsCallbacksTrained ModelMetricsTraining HistoryProvenanceLogsTest LabelsTest PredictionsTest ProbabilitiesFold ScoresCV ReportEvaluation ReportOutput BlockDataData GraphDataGraphDataGraph FileGraph Error
Every port is drawn; unfilled dots are ports this recipe leaves unwired. Fold Scores and CV Report are filled only when a Validation Scheme is wired.
FromTo
Channel Selection · Selected DataBCILattice Trainer · Dataset
Select Files · YBCILattice Trainer · Labels
your model block · model_objBCILattice Trainer · Model
Validation Scheme · ValidationBCILattice Trainer · Val/Eval
BCILattice Trainer · MetricsOutput Block
BCILattice Trainer · Fold ScoresData Graph (optional)

After the folds the trainer refits one final model on every row (switch off with refit_final_model), so Trained Model still feeds whatever comes next. The metrics are the fold estimate. The input takes only a Validation Scheme or an Evaluation Protocol: a scikit-learn splitter such as KFold or a bare number of folds is refused, so the card and the run record always say how the run was validated.

Picking a strategy

SettingSet toWhy
stratified_k_fold(default)One subject, balanced classes. The everyday choice
k_fold—Regression, or where stratification is meaningless
group_k_fold—Multiple subjects, but you only need folds not to split a subject
stratified_group_k_fold—Both: grouped AND class-balanced. Classification only
leave_one_group_out—LOSO / leave-one-session-out — see below
time_series—Ordered 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

Training several models

A trainer's Model port takes one wire, so comparing architectures used to mean one workflow — and one reload of every subject's recordings — per model. Model Collection gathers several models into one wire: each subject's data loads once, every model trains on it, and each gets its own row of results.

Wiring it

EEGNetmodel_objShallowConvNetmodel_objMy Pipeline (compiled)ModelModel Collectionmodel_names: CNN, Shallow, MineModel 1Model 2Model 3ModelsSelect FilesXYChannelsFilesFile PathsBCILattice TrainerDatasetModelLabelsVal/EvalAdditional MetricsCallbacksTrained ModelMetricsTraining HistoryProvenanceLogsTest LabelsTest PredictionsTest ProbabilitiesFold ScoresCV ReportEvaluation ReportOutput BlockData
Each model trains on the same loaded data and gets its own results row.
FromTo
your model block · model_objModel Collection · Model 1
another model block · model_objModel Collection · Model 2
Model Collection · ModelsBCILattice Trainer · Model
Select Files · X / YBCILattice Trainer · Dataset / Labels
BCILattice Trainer · MetricsOutput Block

Drop a second model straight on the trainer

You don't have to place the collection by hand. Wire one model into a trainer's Model port, then drag a second model onto the same port — the canvas offers to insert a Model Collection and rewire both models into it, in one undo step. Works with Nested CV too, and with a trainer that has a Validation Scheme wired.

Naming them

Results rows are named "<workflow> · <model>", where <model> is the block wired into that port — its display name, or its label with the canvas's _1 counter dropped when that stays unique. Set model_names on the collection (“CNN, LSTM, GRU”) when several models end in a layer with the same label and would otherwise be indistinguishable on the card; a blank entry keeps the wired block's own name.

SettingSet toWhy
n_inputs2 (default)Grows to whatever you wire — up to 12 — without a second block
model_names(comma-separated, optional)One name per port; blank keeps the wired block's name

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 Select Files' subject_scope (the File grouping dropdown) to Combined subjects, combined sessions. That collapses every selected subject into a single run, which is what gives the trainer real subject groups to hold out. The trainer's own cohort_mode can say the same thing; leave it on Inherit from the data source, because the run refuses if the two disagree.

Wiring it

SettingSet toWhy
Select Files · subject_scopeCombined subjects, combined sessionsPools every selected subject into one run — the step that makes LOSO possible
Validation Scheme · schemeleave_one_group_outOne fold per group
Validation Scheme · 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

There are no group ids to wire. The trainer takes per-sample subject and session ids from the run's own provenance, selected by the scheme's group_by.

group_by: auto is smarter than it looks

On auto the scheme 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.

Train on one session, test on the others

Cross-validation mixes every session into every fold. When the question is whether a model trained in one session (or one condition, such as indoors) still works in another, the sessions have to stay whole: some train, the rest only test. Wire an Evaluation Protocol into the trainer's Val/Eval input — the same input a Validation Scheme uses.

Wiring it

EEGNet (any model block)model_objSelect FilesFile grouping: Per subject, combined sessionsXYChannelsFilesFile PathsEvaluation Protocolacross: sessiontrain_on: namedtrain_values: Session 1ValidationBCILattice TrainerDatasetModelLabelsVal/EvalAdditional MetricsCallbacksTrained ModelMetricsTraining HistoryProvenanceLogsTest LabelsTest PredictionsTest ProbabilitiesFold ScoresCV ReportEvaluation ReportOutput BlockData
Every port is drawn; unfilled dots are ports this recipe leaves unwired. Evaluation Report is filled only when an Evaluation Protocol is wired. The protocol has no input: everything, including the same-session check, is set on its card.
FromTo
Select Files · XBCILattice Trainer · Dataset
Select Files · YBCILattice Trainer · Labels
your model block · model_objBCILattice Trainer · Model
Evaluation Protocol · ValidationBCILattice Trainer · Val/Eval
BCILattice Trainer · MetricsOutput Block

Choosing the settings

SettingSet toWhy
Select Files · RunsPer subject, combined sessionsEach subject is one run holding all of their sessions, so there is something to test across. Per session would give each run one session, and the run stops and says so
acrosssessionUses the session names in your dataset as written — sessions named indoor and outdoor are trained and tested on by those names
train_on · train_valuesnamed · Session 1One model, trained on Session 1. Several names train one combined model
train_oneach in turnInstead: every session trains its own model and is tested on the others — indoor → outdoor and outdoor → indoor in one run
directionboth waysOr pick one pair from the list, e.g. indoor -> outdoor, to run that way only. The list is filled with your session names
test_onall othersOr named, to test on chosen sessions only
within_check · within_sizehold-out · 0.2Holds back 20% of the training session and scores it with the same model (k-fold: in rounds, averaged): the same-session number the others are compared with

The run reports every train → test pair (Session 1 -> Session 2 accuracy), the same-session check (within_accuracy) and, as the headline accuracy, the mean over the pairs to other sessions. On the Execute page, Detailed names what each number was tested on, Per Subject shows each subject's same-session and other-session score and the drop, Statistics → Same session vs another session tests whether that drop holds subject by subject, and Graphs has the train × test matrix (its diagonal is the same-session check) and a confusion matrix per pair.

Across subjects works the same way

Set Runs to Combined subjects, combined sessions, across to subject, and name the subjects that train (sub01, sub02, …) and, if you like, the ones that test. For leave-one-subject-out over every subject, use a Validation Scheme instead — that is cross-validation.

With the other trainers

With the plain trainer the protocol only evaluates: a model never sees a row of a session it is tested on. Wire the same block into a dedicated trainer and it does what that trainer is for, and the run says so in every table:

SettingSet toWhy
DANN Traineradapts to each test sessionSees the test session’s data WITHOUT its labels while training. The unadapted score is reported beside it (baseline_accuracy), so the gain is a number, not a guess
Fine-Tuning Trainerfine-tunes on a sliceTrains on the training session, fine-tunes on target_size (20%) of each test session with labels, and is scored on the rest — beside the same model before fine-tuning
Class-/Domain-Incremental, Continual Learninglearns a sequenceThe sessions are learned in the order named in train_values; every session is scored after every step. Sessions left out are scored at the end (transfer_accuracy)
Multi-Task, DistillationevaluatesExactly like the plain trainer; Multi-Task is scored per task
Contrastive Trainernot supportedIt learns without labels, so there is nothing to score a test session against

An adapted score is a different claim

A DANN or fine-tuned result used the test session while training, so it is never filed or ranked as plain cross-session transfer: benchmark boards keep it apart, and the methods text says what the model saw.

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 SelectionDataLabelsSelected DataSelected ChannelsScoresEuclidean Alignmentreference_mode: meanDataReferenceAligned DataReferenceAdaptation ReportMetricsProvenanceLogsBCILattice TrainerDatasetModelLabelsVal/EvalAdditional MetricsCallbacksTrained ModelMetricsTraining HistoryProvenanceLogsTest LabelsTest PredictionsTest ProbabilitiesFold ScoresCV ReportEvaluation Report
Reference is optional. Left empty, each subject is aligned to its own mean covariance, the standard per-subject alignment.
FromTo
Channel Selection · Selected DataEuclidean Alignment · Data
Euclidean Alignment · Aligned DataBCILattice Trainer · 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

Reusing a model trained earlier

Subject Adaptation above reuses a model within one run — the trainer's output feeds it directly. Across runs, or across studies, there is nothing to wire: the model that produced last month's number exists only inside that run's artifacts unless you bring it back. Load Trained Model does that — architecture and weights, from a published .bltm or a registry entry — and Forward Pass is how you call it once it's on the canvas.

Wiring it

Load Trained Modelsource: packagefile_path: my_autoencoder.bltmModelLoad ReportArchitectureSelect FilesXYChannelsFilesFile PathsForward Passcall: autoModelDataData 2Data 3OutputCall ReportOutput BlockData
Load Trained Model has no inputs: the file it reads is a setting, not a wire.
FromTo
Load Trained Model · ModelForward Pass · Model
Select Files · XForward Pass · Data
Forward Pass · OutputOutput Block

Evaluate Model needs labels — Forward Pass does not

Use Evaluate Model instead when you have ground truth to score against (“how does last month's classifier do on this new session?”). Reach for Forward Pass for the unsupervised case Evaluate Model can't express — an autoencoder's latent codes, an encoder's embeddings, a frozen trunk's features feeding a new head.
SettingSet toWhy
sourcepackageA .bltm you were handed. Use registry for a name + version saved in this install
use_training_knowledgetrue (default)Off gives you the published ARCHITECTURE, freshly initialised, to train yourself
collection_model(only if the graph trained several)Which model, when the package came from a Model Collection run. A package from one model's own results row needs nothing set
freezetrue (default)Off only if you mean to keep training this model — freeze protects a pretrained encoder from an accidental fine-tune

The combined pipeline

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

Select Filesdata_source: Filteredsubject_scope: Combined subjects,  combined sessionsXYChannelsFilesFile PathsChannel Selectionmethod_name: t-Value Methodpercentile: 5DataLabelsSelected DataSelected ChannelsScoresEuclidean Alignmentreference_mode: meanDataReferenceAligned DataReferenceAdaptation ReportMetricsProvenanceLogsEEGNet (any model block)model_objValidation Schemescheme: leave_one_group_outgroup_by: subjectstrict_leakage_check: onValidationBCILattice TrainerDatasetModelLabelsVal/EvalAdditional MetricsCallbacksTrained ModelMetricsTraining HistoryProvenanceLogsTest LabelsTest PredictionsTest ProbabilitiesFold ScoresCV ReportEvaluation ReportOutput BlockDataData GraphDataGraphDataGraph FileGraph Error
subject_scope Combined subjects is the pooling step LOSO needs: without it each run holds one subject and there is nothing to leave out.

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 · PIPELINE "EEG PREPROCESS"Input Blockport_name: SignalDataInput Blockport_name: LabelsDataChannel SelectionDataLabelsSelected DataSelected ChannelsScoresEuclidean AlignmentDataReferenceAligned DataReferenceAdaptation ReportMetricsProvenanceLogsOutput Blockport_name: CleanData
Compile the pipeline, and in the Workflow it becomes one block:
WORKFLOWSelect FilesXYChannelsFilesFile PathsEEG PreprocessSignalLabelsCleanBCILattice TrainerDatasetModelLabelsVal/EvalAdditional MetricsCallbacksTrained ModelMetricsTraining HistoryProvenanceLogsTest LabelsTest PredictionsTest ProbabilitiesFold ScoresCV ReportEvaluation Report
Model and Validation are wired exactly as in the combined pipeline above.

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 the trainer 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 Select Files' subject_scope set to a Combined subjects mode, 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