This page contains a variety of supplemental information which supports TBFM’s publications
We compare TBFMs to two existing model types. Details are as follows.
While there are a wide variety of LSSMs, some simple versions were previously demonstrated for modeling neural stimulation. These are commonly learned using the Kalman Filter, though they can also be trained using other methods such as backpropagation. In discrete time these simple LSSMs are specified as:
Here x is a latent state, u is the control input (e.g. stimulation parameters), and y is the prediction. Forward prediction can be performed by specifying an initial latent state x0 and autoregressing forward in time.
We leverage this simple formulation by providing our stimulation descriptor as input. We estimate the initial state x0 using the Moore-Penrose pseudoinverse of C and the last value of the runway. We train the model explicitly using backpropagation to perform multisstep forecasting. We use the L2 prediction loss to train the three matrices of parameters A, B, C.
We base this model on the more complex long short-term memory (LSTM) network, a nonlinear neural network for representing neural dynamics as a dynamical system with external inputs. The LSTM-based model uses an autoencoder architecture to lift the LFP data into a latent space, and predicts the effect of stimulation using an estimated dynamical system defined in the latent space. The model predicts the change in neural activity between time steps, and performs forward prediction through a simple first-order integration.
The latent space may be higher or lower dimensionality than the number of LFP channels. In this demo case it is 96 dimensions - equal to the number of electrodes in the ECog array. We chose this dimensionality to ensure we were not losing critical information when transforming into the latent space, but note that a lower dimensionality may provide similar results without penalty due to the inherently low dimensional nature of neural data. As in TBFM, we leverage a stimulation descriptor, which we concatenate to the estimated latent state of the system z. The LSTM estimates a single step change in the system, which is summed into the latent state to make a single step prediction z+. To make multi-step predictions the single step prediction is passed back into the dynamics model repeatedly. Thus: multi-step predictions are made using the first-order Euler integration method.
We train the model on the same data sets as the TBFM. We crop random sub-windows of size 60ms which we split into a 20ms runway and a 40ms prediction horizon. Like TBFM we leverage a multi-step MSE loss function. Finally, we validate the model on the test set by performing the full 164ms multistep prediction.
Training uses a tripartite loss function:
The LSTM model takes inspiration from dynamical systems and control methods which learn latent state representations for optimal control. Since it leverages an autoencoder architecture, it can be compared to methods such as deep Koopman for control, but without the key linearity constraint. We chose the LSTM model for comparison since it is a more expressive model than LSSMs, and has previously been demonstrated for control. While LSSMs were previously demonstrated for modeling neural stimulation, our ODE-LSTM model goes further by allowing for nonlinearities.
We may optionally use an orthonormality penalty applied to the basis tensor. It is calculated as:
where $B$ is the basis matrix and $\mathcal{L}_{\text{ortho}}$ is the orthonormality penalty.
The following derives the compiled TBFM from a test-time adapted multi-session TBFM.
The compiled form is implemented in TBFMMultisessionCompiled
(py-tbfm/tbfm/_multisession_module.py).
| Symbol | Meaning |
|---|---|
| $C$ | Number of channels in the session |
| $l$ | AE latent dimension |
| $r$ | Runway length (time steps) |
| $T$ | Forecast horizon (time steps) |
| $b$ | Number of basis vectors |
| $\mathbf{x} \in \mathbb{R}^{r \times C}$ | Raw input runway, one trial |
| $\mathbf{Z} \in \mathbb{R}^{r \times l}$ | Latent runway (after normalisation + encoding) |
| $B \in \mathbb{R}^{b \times T}$ | Basis matrix (row = basis, col = time) |
| $W(\mathbf{Z}) \in \mathbb{R}^{l \times b}$ | Basis weight matrix (latent channels × bases) |
| $W_{enc} \in \mathbb{R}^{l \times C}$ | AE encoder weight |
| $b_{enc} \in \mathbb{R}^{l}$ | AE encoder bias |
| $\boldsymbol{\alpha}, \boldsymbol{\beta} \in \mathbb{R}^C$ | Per-channel normaliser scale and shift |
| $\tilde{W}_{enc} \in \mathbb{R}^{l \times C}$ | Normaliser-folded encoder weight (IQR/Z-score absorbed) |
| $\tilde{b}_{enc} \in \mathbb{R}^{l}$ | Normaliser-folded encoder bias |
| $c^{rest}_s \in \mathbb{R}^{3}$ | Per-session resting-state context (A-ACF percentiles) |
| $c^{stim}_s \in \mathbb{R}^{15}$ | Per-session stimulation context (optimised by TTA) |
| $\hat{\mathbf{y}} \in \mathbb{R}^{T \times C}$ | Forecast in channel space |
| $Enc(\cdot)$, $Dec(\cdot)$ | AE encoder and decoder |
| $\phi(\cdot)$ | Activation: $\phi(P) = \text{rowNorm}(\tanh(P))$ |
For a fixed session $s$ after TTA, the full pipeline is:
Steps (1)–(3) are affine in $\mathbf{x}$ and can be fused into a single precomputed matrix. Step (4) uses a fixed $B$ (determined by the frozen basis generator at $c^{rest}_s$, $c^{stim}_s$). Step (5) is a linear decode. The only genuine nonlinearity is $\phi$ inside step (3).
Both normaliser types (Z-score and quantile) are per-channel affine maps:
Z-score: $\;\alpha_c = 1/\sigma_c,\quad \beta_c = -\mu_c/\sigma_c$
Quantile: $\;\alpha_c = 2/(q_{0.9,c} - q_{0.1,c}),\quad \beta_c = \alpha_c\cdot(-(q_{0.9,c}+q_{0.1,c})/2)$
The AE encoder (LinearChannelAE) is a per-session affine map:
Substituting Step 1:
where the normalisation-folded encoder (absorbing IQR or Z-score) is:
($\boldsymbol{\alpha}$ is broadcast column-wise over $W_{enc}$, scaling column $c$ by $\alpha_c$.)
The full latent runway is then $\mathbf{Z} = \mathbf{x}\,\tilde{W}_{enc}^\top + \mathbf{1}_r \tilde{b}_{enc}^\top \in \mathbb{R}^{r \times l}$, a single affine map of the raw runway.
The basis_weighting layer is a linear map from the flattened latent runway
to a weight matrix:
where $\text{vec}(\mathbf{Z}) \in \mathbb{R}^{rl}$ stacks all $r$ rows.
Substituting the latent encoding into the basis-weighting layer:
Hence,
The Kronecker block structure is computed efficiently as:
The raw weights are passed through $\phi$ (tanh + row-L2-normalise over the basis dimension):
After TTA, $c^{stim}_s$ is fixed. The basis generator (conditioned on $c^{rest}_s$ and $c^{stim}_s$) produces a single constant matrix:
The latent forecast is a weighted sum of basis vectors plus the $x_0$ skip:
where the $x_0$ skip encodes the last runway timestep through the same normalisation-folded encoder:
(This is row $r$ of $\mathbf{Z}$, so no extra computation is required.)
The LinearChannelAE uses tied weights: the decoder is the transpose of the encoder with no bias.
After TTA with session $s$, stimulus condition with descriptor $s_s$, and learnt contexts $c^{rest}_s$, $c^{stim}_s$, the full pipeline reduces to five stored constant tensors $\{A_\text{pre},\, v_\text{pre},\, B,\, \tilde{W}_{enc},\, \tilde{b}_{enc},\, W_{enc}\}$ and a single hidden layer with activation $\phi$:
with $\mathbf{z}_0 = \mathbf{x}_r\,\tilde{W}_{enc}^\top + \tilde{b}_{enc}$, where the normalisation (IQR or Z-score) is absorbed into the folded encoder:
The model is not affine (because $\phi$ contains $\tanh$), but it is a
single-hidden-layer network. All of: the normaliser, AE encoder,
basis_weighting layer, fixed bases, and AE decoder have been absorbed into
constant matrices. At inference only two matrix multiplies plus the $\phi$
activation are performed at runtime.
| Tensor | Shape | Formed from |
|---|---|---|
| $A_\text{pre}$ | $lb \times rC$ | $W_{bw}$, $\tilde{W}_{enc}$ (Kronecker product) |
| $v_\text{pre}$ | $lb$ | $W_{bw}$, $\tilde{b}_{enc}$, $b_{bw}$ |
| $B$ | $b \times T$ | Frozen basis generator at $c^{rest}_s$, $c^{stim}_s$, $s_s$ |
| $\tilde{W}_{enc}$ | $l \times C$ | IQR/Z-score normaliser folded into $W_{enc}$ |
| $\tilde{b}_{enc}$ | $l$ | IQR/Z-score normaliser folded into $b_{enc}$ |
| $W_{enc}$ | $l \times C$ | AE encoder weight (= decoder weight, tied) |
See TBFMMultisessionCompiled and TBFMMultisession.compile() in
py-tbfm/tbfm/_multisession_module.py for the implementation.