Data-Driven Modeling#

This is the headline of brainmass. Other whole-brain toolkits simulate forward and fit parameters by black-box search. brainmass is built on JAX, so the entire pipeline — parameters → ODE solve → coupling → forward model → signal — is differentiable. You can backpropagate through the simulation and obtain the exact gradient of a data-fit loss with respect to every parameter at once.

That single property reshapes how you do science with neural-mass models. It turns parameter fitting into gradient descent instead of grid or evolutionary search; it makes high-dimensional parameter fields (a value per region) tractable where grid search is hopeless; and it lets you train neural-mass networks on cognitive tasks the way you train any other differentiable model. Data-driven modeling — constructing, fitting, and training models against measured data — is the center of gravity of the whole library, and this page is the curated path through it.

The three verbs of the data-driven workflow:

Verb

What it means

In-package home

Construct

Build a whole-brain model from a connectome (structure, delays, coupling).

brainmass.Network

Fit

Recover parameters from data by gradient or gradient-free search.

brainmass.Fitter

Train

Drive a network’s parameters from a task / dataset of (inputs, targets).

hand-written loop today; a deferred Trainer tomorrow (see the Data-Driven Modeling Roadmap)

Throughout, models map activity to observable signals (BOLD / EEG / MEG) through in-package forward models, so the loss is computed against the same modality as the data you are fitting.

Why brainmass for data-driven modeling#

brainmass shares the neural-mass / whole-brain space with The Virtual Brain (TVB) and neurolib. Both are mature, widely-used, and excellent — but both are NumPy / Numba based: they run forward simulations and fit with grid or evolutionary search. Neither has an autodiff / JAX core, so neither can backpropagate through the solve or run natively on GPU/TPU. The table below mirrors the landing page and is deliberately conservative (“Partial” = exists in some narrower form); consult each project for its current state.

Capability

brainmass

The Virtual Brain

neurolib

Differentiable / gradient-based fitting (backprop through the solve)

Yes

No

No

JAX backend with GPU / TPU acceleration

Yes

No

No

In-package orchestration & fitting (Simulator / Network / Fitter)

Yes

Partial

Partial

Unit-safe quantities (dimensional analysis)

Yes

No

No

Next-generation / exact mean-field models (e.g. Montbrió-Pazó-Roxin, Coombes-Byrne)

Yes

Partial

Partial

In-package BOLD + EEG/MEG forward models

Yes

Yes

Partial

The differentiable row is the one that defines the pillar. The deeper rationale — the maths of backprop-through-the-solve, when not to use gradients, and the application scenarios it unlocks — lives in Why Differentiable?.

A 30-second teaser#

Here is the whole idea in one fit. We take a brainmass.HopfStep node whose settled limit-cycle amplitude depends on its bifurcation parameter a, mark a trainable with Param(..., fit=True), and let brainmass.Fitter recover the a that reproduces a target amplitude — by gradient descent through the simulation. No finite differences, no grid: the optimiser is handed the exact gradient.

(For oscillatory models, fit a scalar summary like amplitude, FC, or a spectral peak — never the raw phase-sensitive time series; see Fitting with Gradients.)

A_TRUE = 1.5  # the (unknown to the fitter) parameter we want to recover

def settled_amplitude(model):
    """RMS limit-cycle amplitude after the transient (a smooth scalar summary)."""
    sim = brainmass.Simulator(model, dt=0.1 * u.ms)
    res = sim.run(400 * u.ms, monitors=['x'], transient=200 * u.ms)
    x = u.get_magnitude(res['x'])
    return (2.0 ** 0.5) * (x ** 2).mean() ** 0.5

def make_node(a):
    # kick off the unstable fixed point so a > 0 actually oscillates
    return brainmass.HopfStep(in_size=1, a=a, w=0.3,
                              init_x=braintools.init.Constant(0.5))

# The "data": the target amplitude produced by the true model.
target_amp = float(settled_amplitude(make_node(A_TRUE)))

# Fit a fresh node (a far from the truth) back to that target by gradient descent.
node = make_node(Param(0.1, fit=True))

def loss_fn(model):
    amp = settled_amplitude(model)
    return (amp - target_amp) ** 2, amp

fitter = brainmass.Fitter(node, braintools.optim.Adam(lr=0.1), loss_fn=loss_fn)
result = fitter.fit(n_steps=120)

print(f"true a       = {A_TRUE}")
print(f"recovered a  = {result.best_params['a']:.3f}")
print(f"final loss   = {result.best_loss:.2e}")
true a       = 1.5
recovered a  = 1.500
final loss   = 2.18e-11

The optimiser walks a straight to the value that matches the target amplitude in a few dozen steps. Swap the optimiser line for backend='nevergrad' or backend='scipy' and the same objective is minimised by gradient-free search — the guided path below walks through all three.

A guided path#

The data-driven workflow is spread across the Diátaxis quadrants — this hub curates a reading order through them rather than duplicating their content. Pick the entry point that matches where you are.

1 · Understand the idea#

  • Why Differentiable? — the conceptual core: backprop-through-the-solve maths, the scaling argument vs grid / evolutionary search, when not to reach for gradients, and the full brainmass-vs-TVB-vs-neurolib comparison.

  • What Is a Neural Mass Model? — what these models are, if you are new to the mean-field idea.

2 · Learn it hands-on (tutorials)#

Work these in order — each is a runnable notebook that builds on the last:

  • Fitting with Gradientsthe centerpiece. Fit a parameter by backprop through the Simulator; see autodiff match a finite-difference check, then fit a network coupling against functional connectivity.

  • Gradient-Free Fitting — the same objective with Nevergrad and SciPy backends, head-to-head against gradients. When the loss is non-differentiable or noisy, this is your tool.

  • Training on Taskstraining, not fitting: drive a HORNSeqNetwork’s parameters from a (inputs, targets) task with a minibatched, epoched loop. This is the loop the deferred Trainer will wrap.

3 · Apply it to a recipe (how-to)#

Task-focused recipes for when you already know the shape of your problem:

4 · See it end-to-end (case studies)#

Complete application stories, from connectome to fitted result:

5 · Extend it (developer)#

  • Building a Data-Driven Workflowthe extension contract. Expose trainable parameters on a custom model, write a composable objective, fit it both ways (high-level Fitter and a hand-written grad loop), and batch the search. This is the stable contract the deferred Trainer and model-discovery tooling build against — read it before you write new data-driven machinery.

Where this pillar grows next#

The data-driven pillar has reserved homes for its growth areas — model discovery / system identification, a task-shaped Trainer (distinct from the target-fitting Fitter), and simulation-based / amortized inference. They are named and given a home now, not built in goal-13, each with a documented contract pointing back to Building a Data-Driven Workflow. See the roadmap: