> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nimbusbci.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Encoder contract

> The encode(X)→Z product boundary: wrap, FrozenEncoder, Personalizer fit/partial_fit/save/load, and optional trunk shortcuts.

The encode hook is not a gap — it **is** the product boundary. Nimbus does not host DL architectures. Integrators (or thin shortcuts) supply embeddings.

| Layer | Owner | Responsibility |
| - | - | - |
| Frozen trunk | You / partner / bench | `encode(X) → (n, d)` pre-classifier features |
| Wrap | Nimbus | `wrap` / `FrozenEncoder` (+ optional named shortcuts) |
| Head | Nimbus | `Personalizer` — LDA / QDA / Softmax, `fit` / `adapt` / `partial_fit` |
| App | You | `BrainState` + presets (`strict` / `permissive`) |

**Rules:**

* Freeze the trunk before personalizing
* Feed **embeddings**, never class logits
* `Personalizer.load` / `from_profile` require **re-attaching** the encoder
* Optional `embedding_dim=` validates embedding width
* Prefer factories (`for_deployment`, `for_features`, …) over raw constructors

## Canonical recipe

```python theme={null}
from nimbus_bci import Personalizer, wrap

enc = wrap(model.encode, model_id="partner-encoder", embedding_dim=64,
           training_sfreq=250.0)  # declare trunk rate when known
adapter = Personalizer.for_deployment(
    enc, ["left", "right"],
    user="subject_001",
    paradigm="motor_imagery",
)
# Pin accept rate for your trunk (optional):
# adapter.fit_calibrated(X_cal, y_cal, X_val, y_val, target_accept=0.7)
adapter.fit(X_cal, y_cal, input_sfreq=250.0)
states = adapter.predict(X_test, input_sfreq=250.0)  # list[BrainState] / BrainStateBatch
adapter.save("subject_001_profile")
loaded = Personalizer.load("subject_001_profile", encoder=enc)

# Portable \<50 KB profile (head + transform + thresholds; no raw EEG):
profile = adapter.export_profile()
warm = Personalizer.from_profile(profile, encoder=enc)
```

Already have features / CSP / Riemann vectors? Skip the trunk:

```python theme={null}
from nimbus_bci import Personalizer

adapter = Personalizer.for_features(["left", "right"])
adapter.fit(X_cal, y_cal)
states = adapter.predict(X_test)
```

Riemann / P300 paradigms (`pip install nimbus-bci[riemann]`):

```python theme={null}
p = Personalizer.for_riemann(["left", "right"])  # geodesic tracking α=0.05 default
# p = Personalizer.for_p300(["NonTarget", "Target"], tracking_alpha=0.05)
```

<Info>
  Online head updates default to cumulative (`λ=1`, byte-identical to 0.5). Optional forgetting on LDA/QDA uses `λ\<1` (recency factor). Prefer session calibration and short stream budgets unless you intentionally enable forgetting.
</Info>

**Foundation pooling:** REVE / EEGPT adapters mean-pool (or flatten) token / patch features to flat `(n, d)` so LDA/QDA heads can consume them. Spatial/temporal structure is collapsed at the encoder boundary, not inside the Bayesian head.

## Ladder levels under one API

**Shipped Personalizer path** (same trunk, no architecture change):

| Level | What it is | When to think about it |
| - | - | - |
| **L0 — cal-only** | Bayesian head fit on calibration embeddings only; no stream labels | Default when clean already transfers (cal-only-when-clean) |
| **L1 — stream head** | L0 + stream `partial_fit` | Mid / severe shift when head capacity exists |

**Opt-in / priced (not deployment defaults):**

| Level | What it is | Where it lives |
| - | - | - |
| **L2 — affine-Z + head** | Supervised affine map on embeddings, then the same head | `transform="affine"` / `fit_affine()` (research / offline); also Paper 1 / NimbusBench protocol |
| **L3 — full fine-tune** | Warm-start fine-tune of the encoder | Escape hatch (outside Personalizer; priced); `[train]` PEFT / BaLoRA opt-in |

Linear probe (**LP**) in Evidence is a matched sklearn control on the same frozen `Z`, not a ladder level. Plain-language legend: [Overview](/personalizer/overview#ladder-legend-plain-language). Evidence for when L1 helps and how L3 is priced: [Evidence](/personalizer/evidence).

**Heads:** product default is LDA (`head="lda"`). QDA is a documented severe-shift option behind the same API; Softmax stays optional (`nimbus-bci[softmax]`). The multi-trunk map in Paper 1 locks LDA for comparability — numbers for LDA vs QDA vs Softmax (EEGNet): [Evidence → Which head?](/personalizer/evidence#which-head-lda-vs-qda-vs-softmax).

## Optional shortcuts

Named helpers only reduce boilerplate. Architectures stay outside core:

| Shortcut | Trunk lives in | Notes |
| - | - | - |
| `wrap(...)` | Your code | Canonical — any callable / sklearn / torch |
| `wrap_eegnet(clf)` | `nimbusbench.models.eegnet` | Convenience over `.encode` / `.freeze` |
| `wrap_braindecode(..., kind="eegnetv4")` | `braindecode` (optional) | Or pass any `encode_fn` |
| `wrap_braindecode(..., kind="reve")` | `braindecode` REVE (optional) | Foundation EEG; electrode positions + pooled embeddings |
| `wrap_braindecode(..., kind="eegpt")` | `braindecode` EEGPT (optional) | Patch-pooled foundation embeddings |

```python theme={null}
from nimbus_bci import wrap_braindecode, wrap_eegnet

enc = wrap_eegnet(clf, freeze=True)
enc = wrap_braindecode(module, kind="eegnetv4")
enc = wrap_braindecode(reve_module, kind="reve", channel_names=chans)
enc = wrap_braindecode(eegpt_module, kind="eegpt")
enc = wrap_braindecode(my_encode_fn)  # any other BrainDecode / custom net
```

Claim for second trunks: **API interchangeability** under the same `Personalizer` / `BrainState` contract — not a confirmatory accuracy bake-off.

## Active learning → personalization

Prefer Personalizer wrappers (encode for you), or `CalibrationSession` on the head:

```python theme={null}
ranked = adapter.suggest_calibration_trials(X_pool, n=4, strategy="bald")
adapter.partial_fit(X_pool[ranked.indices], y_new)
status = adapter.calibration_status(X_probe, criterion="posterior_stability")
```

See [Active Learning](/python-sdk/active-learning).

## Next Read

<CardGroup cols={2}>
  <Card title="BrainState" icon="brain" href="/personalizer/brain-state" />

  <Card title="Evidence" icon="chart-line" href="/personalizer/evidence" />

  <Card title="Overview" icon="layers" href="/personalizer/overview" />

  <Card title="API Reference" icon="book" href="/python-sdk/api-reference#middleware" />
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.