NimbusNimbus
PersonalizerStudioTeamDocs
Sign InSalesContact SalesGet Started
← Back to Blog

Nimbus SDK 0.6.0: Personalization that keeps up with the session

September 13, 2026

Published: 2026-09-13 · Live on PyPI as nimbus-bci 0.6.0

1785891898821.jpeg

Anyone who has run a real BCI session knows the pattern. Day one looks fine. Day two — same person, same headset, same task — and performance quietly falls apart. People drift. Labels are scarce. A good offline score does not survive a live stream.

0.6.0 is the personalization release of the Nimbus Python SDK. Keep your neural encoder frozen. Put a Bayesian head on top. Adapt cheaply as the session unfolds. Carry a subject to the next day without shipping raw EEG.

pip install nimbus-bci

Docs: docs.nimbusbci.com · Start here: Personalizer overview


The problem in one sentence

Lab stacks optimize for notebooks. Product stacks need a session that can adapt, refuse uncertain trials, and remember a person — without retraining a giant model every time the headset moves.

That layer is Personalizer. Your encoder turns EEG into features; Personalizer turns those features into app-ready decisions (BrainState) your product can act on.

from nimbus_bci import Personalizer

adapter = Personalizer.for_deployment(
    enc, ["left_hand", "right_hand"], paradigm="motor_imagery"
)
adapter.fit(X_cal, y_cal)
states = adapter.predict(X_test)
adapter.partial_fit(X_new, y_new)  # update the head — not the big model

Adaptation as a ladder (not a hammer)

Drift is not one problem, so 0.6 does not ship one fix. It gives the session a sequence of increasingly expensive interventions—and keeps the neural trunk frozen until the evidence says the representation itself is the problem.

Twenty minutes into a session

Imagine the headset shifts slightly. The next few embeddings move, confidence falls, and the app starts rejecting more trials. A conventional pipeline gives you two bad choices: keep guessing with a stale model, or stop everything and recalibrate.

Personalizer can respond in stages.

First: decide whether adaptation is worth spending. recommend_adapt(...) is a deliberately thin mean-shift check. You provide the threshold; Nimbus reports whether the stream has moved enough to justify an update.

decision = adapter.recommend_adapt(X_stream, tau=your_tau)
if decision.should_adapt:
    adapter.partial_fit(X_verified, y_verified)

This is separate from the BrainState gate. Trial rejection asks, “Should the app act on this intent?” The adaptation check asks, “Has the session moved enough to update the model?” A rejected trial does not automatically rewrite the model, and drift does not force the app to accept an uncertain prediction.

Rung 1: update the head

When a few verified labels arrive, partial_fit(...) updates the small Bayesian classifier on top of the encoder. The trunk stays frozen. This is the everyday path: cheap enough to use as a session unfolds, roughly 8–10× faster than a full batch refit and about 22–131× cheaper than retraining the neural trunk.

The practical story is simple: do not relearn EEGNet, REVE, or another foundation encoder because five new trials arrived. Update the part of the model that can absorb five trials responsibly.

state = adapter.predict(trial)[0]
if verified_label is not None:
    adapter.partial_fit(trial, [verified_label])

Rung 2: move the geometry, not the classes

Sometimes labels are the expensive resource. If the feature cloud moved but the class structure probably did not, realign the space before touching the decision rule.

On Riemannian paths, Personalizer can track the reference geometry without labels. for_riemann(...) enables conservative geodesic tracking by default; P300 keeps it opt-in. This gives the session a zero-label response to mild covariate drift:

adapter = Personalizer.for_riemann(
    ["left_hand", "right_hand"],
    tracking_alpha=0.05,
).fit(X_cal, y_cal)

adapter.adapt(epoch)  # update the reference, no label required

Think of this rung as correcting the coordinate system: “the cloud moved,” not “left and right swapped.” It spends geometry instead of demanding another calibration block.

Rung 3: repair the trunk

Only after the cheap fixes fail should the system change the representation itself. The optional [train] extra exposes parameter-efficient trunk adaptation through LoRA, FiLM, and BaLoRA.

This is where BaLoRA belongs in the story: not as the default personalization mechanism, but as an escalation path for the harder case. Head updates assume the frozen embedding is still useful. Geometry alignment assumes its structure survived the shift. A trunk adapter is for the moment when those assumptions stop holding—when the encoder needs a small, targeted repair for this person or session.

pip install "nimbus-bci[train]"

BaLoRA and the other PEFT paths remain an explicit research surface. They are heavier, require training dependencies, and should not be read as a certified accuracy claim. The product principle is restraint: earn the right to touch the trunk by first exhausting the safer, cheaper rungs.

The next day

Adaptation should survive the session without turning raw EEG into portable baggage. A saved profile carries the fitted personalization state forward in under ~50 KB, with no raw EEG. On day two, load the profile, run a short check, and update the head only if the new session needs it.

adapter.save("subject_001")
adapter = Personalizer.load("subject_001", encoder=enc)

The result is continuity without centralizing a person’s recordings: carry the adapter, not the EEG.

Other evidence channels

The ladder is not limited to hard labels. adapt_errp(...) can consume an external error-related signal as soft negative evidence, and asynchronous trunk jobs can train away from the live loop before committing safely. The session can keep predicting while the heavier repair is prepared.

What happened?First responseWhat it spends
A few trusted labels arrivedBayesian head updateLabels; milliseconds, not a trunk retrain
The feature cloud drifted, labels did not arriveTransform / Riemann trackingGeometry, not labels
The same person returned tomorrowProfile + short head updateA small portable state, not raw EEG
An external ErrP says the prediction was wrongadapt_errp(...)Soft negative evidence
The representation is still wrongLoRA / FiLM / BaLoRA ([train])Training time and an explicit opt-in

Thesis: adaptation should be a ladder. Measure before updating. Change the head before the geometry, and the geometry before the trunk. Defaults stay cheap; expensive tools remain explicit.


What else ships in 0.6

  • Paradigm recipes for motor imagery, P300, and Riemannian geometry — first-class, not bolted on
  • Portable profiles — day-2 warm-start beat frozen day-1 in our reported eval (+1.03 pp)
  • Honest uncertainty — reject trials the model should not answer, instead of guessing intent
  • Install extras when you need them: [riemann], [softmax], [train], or [all]

Supported Python: 3.11–3.13 (Linux, macOS, Windows wheels).

Coming from 0.5? Top-level nimbus_bci stays stable. Only deep middleware import paths and two preset names changed (research → strict, consumer → permissive). Details in the CHANGELOG.


The short version

0.6 is not “more classifiers.” It is a session-shaped personalization contract: wrap a frozen trunk, calibrate under a label budget, adapt when the stream shifts, reject when uncertain, and carry a subject forward without shipping EEG.

If you build BCI apps on foundation or classical encoders, this is the head you bolt on.


Links

  • PyPI — nimbus-bci 0.6.0
  • Documentation
  • Personalizer overview
  • API reference
  • Contact: hello@nimbusbci.com
Nimbus BCI

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

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