NimbusNimbus
TechnologyStudioTeamDocs
Sign InSalesContact SalesGet Started
← Back to Blog

From Training to Streaming: Deploying a Live EEG Decoder with the Nimbus Python SDK

July 2, 2026

Hero: live EEG streaming waveform

Training a BCI decoder is satisfying. You load your calibration recordings, fit a NimbusLDA model, inspect the cross-validation metrics, and hit 87% accuracy on held-out data. Then comes the real question: how do you get that model responding to live EEG at 30 ms latency, trial after trial, in a noisy session two weeks from now?

This post answers that question end-to-end. We'll cover the full path from offline training to live streaming inference using the Nimbus Python SDK — the chunk loop architecture, model persistence, entropy-based confidence gating (see Confidence-Gated BCI: How Entropy and Rejection Policies Keep Decoders Reliable in the Real World), and online adaptation via partial_fit. Along the way, we'll connect the SDK to the Nimbus Studio deployment pipeline so the same logic runs in a production-grade visual pipeline.

The Batch-to-Stream Gap in BCI Pipelines

Most BCI engineers spend the majority of their time in batch mode: offline recordings, fixed numpy arrays, deterministic pipelines. Batch inference is forgiving — you can re-run it, inspect intermediate steps, and tune hyperparameters. Streaming is a different discipline.

In a live session, data arrives continuously from a hardware device, usually in small windows (250–1000 ms). Your decoder must:

  • Preprocess each window in real time (filter, epoch, extract features)
  • Produce a probability distribution over classes — not just a label
  • Decide whether that distribution is confident enough to act on
  • Optionally update the model if the user's neural signature has drifted

The Nimbus Python SDK is designed around this exact contract. Every classifier — NimbusLDA, NimbusQDA, NimbusSoftmax, and NimbusSTS — exposes a predict_proba() method that returns calibrated class probabilities, not point estimates. That distribution is the foundation of everything that follows.

The Nimbus SDK Streaming Architecture

The full streaming path has two clearly separated phases: train and deploy.

Training phase

During offline training, you work with calibration data loaded into BCIData containers and run your preprocessing chain (bandpass → epoching → CSP spatial filters) on fixed recordings. Once you have features, training is standard:

from nimbus_bci import NimbusLDA, nimbus_save

model = NimbusLDA()
model.fit(X_train, y_train)          # Bayesian LDA with calibrated posteriors
nimbus_save(model, 'decoder.nimbus') # persist model + normalization state

The nimbus_save function serialises not just the model weights but also any normalization statistics fitted during preprocessing. This is critical: normalization must be fixed after calibration and applied identically during streaming. The SDK enforces this by bundling the normalization transform alongside the classifier checkpoint.

Deployment phase

At runtime, you restore the checkpoint and enter the chunk loop:

from nimbus_bci import nimbus_load

model = nimbus_load('decoder.nimbus')

for window in eeg_stream:            # iterate over incoming EEG windows
    features = preprocess(window)    # same pipeline as training — fixed normalization
    proba = model.predict_proba(features)   # shape: (1, n_classes)
    entropy = -np.sum(proba * np.log(proba + 1e-9), axis=1)
    
    if entropy < threshold:
        act(proba.argmax())          # confident prediction → send command
    else:
        abstain()                    # uncertain → withhold

This loop is intentionally minimal. The SDK handles the statistical heavy lifting; your loop handles timing and gating. For motor imagery with a 4 Hz trial rate, this loop runs synchronously with each epoch boundary. For continuous decoding (SSVEP, neurofeedback), it runs on every incoming window.

Confidence Gating with Entropy

Confidence gating: entropy per trial and accuracy vs. acceptance rate

The most important shift from batch to streaming is treating entropy as a first-class control signal. In batch evaluation, you measure accuracy on all trials. In live deployment, you have the option to not predict when the signal is ambiguous.

The Nimbus Python SDK's predict_batch() method returns a BatchResult object that includes per-trial entropy alongside the class probabilities. Entropy spikes reliably during:

  • Electrode contact loss or movement artifacts
  • Drowsiness or attention lapses between trials
  • Class confusion in genuinely ambiguous windows

If you're seeing entropy spikes that correlate with blinks, eye movements, or muscle contamination, it's often worth cleaning the signal upstream before you tune thresholds — see Cleaning EEG Before It Reaches Your Decoder: ICA, Artifact Rejection, and EOG Removal in Nimbus Studio.

A simple entropy threshold (e.g., H < 0.5 nats) already captures most of these cases. For production deployments, the SDK's evaluate_rejection_policy() helper sweeps the threshold across your calibration data and plots the accuracy-vs-acceptance tradeoff, letting you choose the operating point that fits your application — maximum accuracy at 80% acceptance, or 100% acceptance with a lower accuracy floor.

For motor imagery BCIs where each missed trial costs time but each false command costs an error, a threshold around 0.65–0.8 bits (≈ 0.45–0.55 nats) is a reasonable starting point. For assistive communication applications with zero tolerance for errors, you may operate at 60–70% acceptance and supplement with dwell-time confirmation.

Temperature scaling (temperature_scale_proba in nimbus_bci.metrics) further improves calibration before gating: a model that outputs [0.95, 0.05] too frequently will over-accept; temperature scaling adjusts confidence to match the true empirical accuracy.

Online Adaptation with partial_fit

Even with a well-calibrated decoder, neural signals drift. Fatigue, minor electrode shifts, and day-to-day variability all push the signal distribution away from the training data. The Nimbus Python SDK addresses this directly with partial_fit, available on all four classifiers:

# After a confirmed correct trial (e.g., from user feedback or a known stimulus):
model.partial_fit(new_features, new_label)

partial_fit performs a Bayesian posterior update — it doesn't retrain from scratch, it updates the existing posterior given the new evidence. This means adaptation is:

  • Fast: a single update takes microseconds
  • Principled: the prior from initial training is preserved and weighted against new data
  • Session-length stable: precision parameters prevent catastrophic forgetting

If you want a deeper conceptual and engineering treatment of this problem (and why periodic full recalibration is often the wrong default), see Continual Learning in BCI: Handling Neural Drift with Online Bayesian Updates.

For NimbusSTS (the latent-state classifier), adaptation happens automatically via the state propagation loop. You call propagate_state() between trials to advance the temporal model, and partial_fit when a ground-truth label is available. The Extended Kalman Filter-style update in NimbusSTS is particularly effective for within-session drift, where the signal shifts continuously rather than in discrete jumps.

Connecting to Nimbus Studio's Live Pipeline

If you're deploying through Nimbus Studio rather than a custom Python host, the SDK's streaming path maps directly onto the Studio live graph. The hardware_device node handles device connection and data acquisition (BrainFlow, LSL, or synthetic sources). The preprocessing chain — highpass filter, bandpass, epoching — runs identically to the batch pipeline, with the same normalization state loaded from the checkpoint. The decoder node wraps nimbus_load and the predict_proba call, while the decision_policy node implements the entropy threshold and any dwell-time or output gating logic.

If you want the structured end-to-end view of the Studio workflow from live capture to a trained (and deployable) decoder artifact, see BCI Calibration with Nimbus Studio: From Hardware to Trained Decoder.

This architecture means there is exactly one code path between offline training and live deployment — the model file. No manual translation, no hand-coded inference loop, no silent normalization mismatch. The Studio graph is the inference runtime, and the SDK checkpoint is the artifact that carries the trained state from your calibration session into production.

Conclusion

The gap between a trained BCI model and a deployed streaming decoder is real — but it is an engineering gap, not a research one. The Nimbus Python SDK closes it with a consistent API: fit() and nimbus_save() on the training side; nimbus_load(), predict_proba(), and partial_fit() on the streaming side; evaluate_rejection_policy() and temperature_scale_proba() for calibration and gating.

The result is a decoder that knows not just what it thinks, but how confident it is — and one that can keep pace with the brain's natural variability across a session. That combination is what separates a lab prototype from a system someone can actually rely on.

Nimbus BCI

Stop writing boilerplate. Start publishing papers. Built by researchers, for researchers.

LinkedInX
Navigation
ProductTechnologyStudioTeamDocsResources
Nimbus Studio
Download for WindowsDownload for macOSComparisonFeaturesPricingFAQ
© 2026 Nimbus BCI Inc. All rights reserved.
Get startedSign inPrivacyTermsCookies