Decoding Under Drift: How NimbusSTS Tracks Brain State Across Sessions

Introduction
If you have trained a motor-imagery classifier on a Monday calibration session and deployed it on Friday, you already know the problem. The model that scored 87% in cross-validation is suddenly borderline usable. Nothing obvious changed — same participant, same hardware, same feature pipeline. What changed is the brain.
EEG is non-stationary by nature. Fatigue, attention shifts, subtle electrode impedance drift, and the natural day-to-day variability of cortical dynamics all conspire to move the signal distribution away from whatever the classifier memorized at calibration time. Static decoders — linear or quadratic discriminant analysis, fixed logistic regression — have no mechanism for tracking this drift. They freeze their decision boundaries at fit() time and silently degrade as the world moves on (see Neural drift and why it breaks your BCI classifier).
This post walks through how NimbusSTS, the experimental latent-state classifier in the Nimbus Python SDK (nimbus-bci), approaches the problem differently, what it borrows from dynamical systems theory, and how to put it to work in a Nimbus Studio pipeline.
What Non-Stationarity Actually Means for a BCI Decoder
Non-stationarity in EEG shows up at several timescales:
- Within-session drift — alpha power suppression as the participant warms up; motor cortex habituation during repeated imagery.
- Between-run drift — electrode impedance creeping up or down between blocks; momentary changes in arousal.
- Cross-session shift — genuine day-to-day variability in cortical oscillation amplitude and spatial topography; different cap placements.
For a static classifier like NimbusLDA, each of these translates directly into a mismatch between the class-conditional Gaussians estimated at training time and the distribution observed at inference time. The decision boundary sits in the wrong place, posterior probabilities are miscalibrated, and the entropy reported by predict_batch climbs even on trials the user considers unambiguous.
The standard fix — periodic re-calibration — works, but it costs time and disrupts the user experience. The more interesting fix is to let the decoder track the drift continuously, using ideas from online Bayesian updating.
Active Inference and the Latent State Idea
Active Inference frames perception as approximate Bayesian inference over the hidden causes of sensory data. Rather than mapping observations directly to decisions, the system maintains a generative model — a joint distribution over latent states and observations — and continuously updates its beliefs as new data arrives.
NimbusSTS applies this intuition at the decoder layer. Instead of treating each trial's feature vector as an independent observation, the classifier maintains a latent state that evolves across trials and captures the slowly-varying component of the EEG distribution. Logits that drive the softmax output are a function of both the feature vector and this latent state. When calibration data arrives, the state updates. Between labeled events, the state can be propagated forward under the transition model, spreading uncertainty to reflect that time has passed.
This is structurally close to an Extended Kalman Filter (EKF) operating over the classifier's internal belief. The transition model is linear by default (a random walk if transition_matrix is omitted), and the observation model is the softmax likelihood. The hyperparameters transition_cov and observation_cov control the trade-off between responsiveness to new data and stability against noise.
The result is a decoder that stays calibrated longer — and that can be explicitly warm-started at the beginning of a new session by loading a saved state and inflating its covariance to reflect the passage of time.
NimbusSTS in Practice: Key API Patterns
NimbusSTS is designed to be used in time order, not as a “shuffle-and-score” batch model.
At a high level, the usage pattern is:
- Train on an initial calibration set.
- Run inference sequentially so the model can propagate its latent state forward and update its beliefs as drift accumulates.
- Optionally adapt when labeled feedback arrives (immediate or delayed), instead of stopping for full recalibration.
If you evaluate NimbusSTS offline, make sure your evaluation loop preserves trial order and reflects how your real session will deliver feedback. Otherwise you can accidentally benchmark it like a static classifier and miss the main benefit: stateful adaptation under drift.
For cross-session use, the core idea is the same: warm-start from a previous belief state, then explicitly increase uncertainty to reflect the time gap before updating with new session data.
Wiring NimbusSTS into a Nimbus Studio Pipeline
Nimbus Studio handles the data acquisition and preprocessing side so the SDK can focus on decoding. A typical motor-imagery workflow looks like this:
Batch training phase (Nimbus Studio pipeline):
custom_data → highpass_filter → bandpass_filter → epoching → csp → (export features) → (train NimbusSTS in the SDK)
Live deploy phase:
hardware_device → highpass_filter → bandpass_filter → epoching → csp → (run NimbusSTS sequentially, tracking state across trials) → decision_policy
Studio's hardware_device node manages the BrainFlow or LSL connection and feeds calibrated, timestamped epochs downstream. The preprocessing chain — highpass, bandpass, epoching, csp — is identical between training and deploy, ensuring the feature space the decoder was trained on matches what it sees at runtime. This pipeline consistency is one of the key guarantees Studio provides and it matters especially for NimbusSTS, where any distributional mismatch between training and inference will be misattributed to session drift and cause the latent state to compensate in the wrong direction.
The calibration_recorder node can capture labeled live data during a short recalibration block; that data can be used to anchor the model at the start of a new session.
When to Use NimbusSTS — and When Not To
NimbusSTS is the right tool when:
- You are deploying across multiple sessions and cannot afford full re-calibration each time.
- Your paradigm involves delayed feedback (P300, SSVEP confirmation intervals) or asynchronous labeling.
- Pilot data shows classifier accuracy degrading monotonically within a single long session — a signature of within-session drift.
For a deeper technical perspective on within-session drift and EKF-style adaptation, see Within-session non-stationarity in EEG BCIs.
It is likely overkill when:
- Sessions are short (under 20 minutes) and the participant is well-practiced.
- You have abundant labeled calibration data per session — in that case
NimbusLDAorNimbusQDAwith a freshfit()will be faster and easier to tune. - Compute budget is tight: NimbusSTS carries the JAX/NumPyro stack as a dependency and is heavier than the Gaussian classifiers.
Conclusion
The non-stationarity problem is not going away — it is structural to EEG-based decoding. Static classifiers are a reasonable starting point, but they assume the world froze at calibration time. NimbusSTS takes a different stance: treat the slowly-varying component of the brain signal as a latent variable, model its dynamics explicitly, and update continuously as data arrives.
Combined with Nimbus Studio's consistent train-deploy pipeline and the SDK's streaming session helpers, the result is a decoder that degrades more gracefully over time and can be warm-started across sessions without a full recalibration protocol.
If you want to experiment, install nimbus-bci from PyPI, start from an existing end-to-end example pipeline, and swap in NimbusSTS for your static classifier. The API surface is intentionally familiar — the differences are almost entirely in what happens between trials, not within them.