Overview: How Integration Works

Overview: How Integration Works#

Every dynamical model in braincell — a single ion channel, a point neuron, a branching dendrite — is ultimately a system of differential equations

\[\frac{dy}{dt} = f(t, y),\]

where \(y\) stacks together every time-varying quantity (membrane potential, gating variables, ion concentrations). Turning that system into a simulation means repeatedly answering one question: given the state now, what is it one small step dt later?

braincell separates that question into two responsibilities:

Responsibility

Who owns it

Where it lives

What to integrate — the variables and their right-hand side \(f\)

your model

DiffEqState + DiffEqModule

How to advance one step

a numerical solver

braincell.quad

This separation is the whole design. You describe the equations once; you can then swap solvers freely without rewriting the model.

The two protocol pieces#

  • DiffEqState is a state variable that participates in integration. It is an ordinary brainstate state with two extra slots the solver fills in: derivative (the right-hand side \(f(t, y)\)) and, for stochastic systems, diffusion. You meet it again in Defining Differential Equations.

  • DiffEqModule is a mixin that marks a class as integrable. It exposes a three-method lifecycle that every solver calls in the same order on each step.

The per-step lifecycle#

A solver never reaches inside your model. It only calls three hooks, always in this order, once per step:

        ┌─────────────────────────────────────────────┐
        │  one integration step (advances t by dt)     │
        └─────────────────────────────────────────────┘

   pre_integral(...)        # refresh rate constants, gather inputs
        │
        ▼
   compute_derivative(...)  # REQUIRED: write state.derivative for each DiffEqState
        │
        ▼
   << solver combines values + derivatives to produce y(t + dt) >>
        │
        ▼
   post_integral(...)       # clamp / project / fire post-step events

Only compute_derivative is mandatory; pre_integral and post_integral default to no-ops. The current time t and step size dt are not passed as arguments — solvers read them from the active brainstate.environ context, which is why you will see every simulation wrapped in with brainstate.environ.context(dt=...).

The solver registry#

Solvers are looked up by name through a registry. You rarely import a step function directly; instead you name it — either by passing solver="..." to a cell, or by asking the registry for it. Let’s see what is available.

import braincell.quad as quad

integrators = sorted(quad.all_integrators)
print(f"{len(integrators)} integrators registered\n")
print(integrators)
25 integrators registered

['backward_euler', 'cn_exp_euler', 'cn_rk4', 'dhs_voltage', 'euler', 'exp_euler', 'exp_exp_euler', 'explicit', 'heun2', 'heun3', 'implicit_euler', 'implicit_exp_euler', 'implicit_rk4', 'ind_exp_euler', 'midpoint', 'ralston2', 'ralston3', 'ralston4', 'rk2', 'rk3', 'rk4', 'splitting', 'ssprk3', 'stagger', 'staggered']

get_integrator resolves a name (or alias) to its step function:

rk4 = quad.get_integrator("rk4")
rk4
<function braincell.quad._runge_kutta.rk4_step(target: braincell.DiffEqModule, *args)>

That is the entire mental model:

  1. Describe equations with DiffEqState / DiffEqModule.

  2. Pick a solver by name from braincell.quad.

  3. Step inside a brainstate.environ context that supplies dt (and t).

The next page makes this concrete by building a model from scratch.