Physical units#

What you’ll learn / who it’s for (simulation and training). How brainpy.state uses brainunit to attach physical units to every quantity, how to construct models unit-safely, how unit-aware initializers work, and how the unit system turns silent modeling mistakes into errors raised at construction time.

Why units belong in the model#

A membrane time constant of 10 is meaningless: 10 milliseconds and 10 seconds differ by three orders of magnitude and produce completely different dynamics. Brain models are dense with such quantities — millivolts, milliseconds, nanosiemens, milliamps — and mixing their scales is one of the most common, most silent sources of wrong results.

brainpy.state carries units through the entire model with brainunit. A unitful value is a Quantity = magnitude × unit. Arithmetic checks dimensions, conversions are explicit, and an incompatible combination raises before a single time step runs.

import brainpy
import brainstate
import braintools
import brainunit as u
import jax.numpy as jnp
An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.

Quantities: magnitude × unit#

Build a quantity by multiplying a number (or array) by a unit. The units you meet constantly in point-neuron modeling:

  • Voltageu.mV (millivolt)

  • Timeu.ms (millisecond)

  • Conductanceu.nS (nanosiemens), u.mS (millisiemens)

  • Currentu.mA (milliamp), u.nA (nanoamp)

  • Frequencyu.Hz

tau = 10. * u.ms
V_th = -50. * u.mV
weight = 0.6 * u.mS
current = 20. * u.mA

print(tau)
print(V_th)
print('weight has dimension:', weight.dim)
10. ms
-50. mV
weight has dimension: m^-2 kg^-1 s^3 A^2

Arithmetic is dimensionally checked#

Adding compatible quantities works (and rescales as needed); combining incompatible ones is an error — which is exactly what you want.

# Compatible: different time units add correctly.
total = 100. * u.ms + 0.5 * u.second
print('100 ms + 0.5 s =', total.to_decimal(u.ms), 'ms')

# Derived units fall out of the algebra: V / I -> resistance.
R = (-65. * u.mV) / (2. * u.nA)
print('V / I =', R)
100 ms + 0.5 s = 600.0 ms
V / I = -32.5 Mohm
# Incompatible: adding a voltage to a time is caught immediately.
try:
    bad = (-65. * u.mV) + (10. * u.ms)
except Exception as err:
    print(type(err).__name__, '->', err)
UnitMismatchError -> Cannot calculate 
-65. mV + 10. ms, because units do not match: mV != ms

Converting to plain numbers, on purpose#

When you need a raw array (for plotting, or interfacing with code that does not understand units), convert explicitly with .to_decimal(unit). Being explicit documents the unit you are assuming.

v = -65. * u.mV
print(v.to_decimal(u.mV))   # -> -65.0
print(v.to_decimal(u.volt)) # -> -0.065
-65.0
-0.065

Unit-safe construction#

Every built-in neuron, synapse, and output takes unitful parameters. Supplying them with units is self-documenting and it lets the constructor reject a quantity with the wrong dimension.

neuron = brainpy.state.LIF(
    100,
    V_rest=-65. * u.mV,
    V_th=-50. * u.mV,
    V_reset=-65. * u.mV,
    tau=10. * u.ms,
    R=1. * u.ohm,
)
brainstate.nn.init_all_states(neuron)
print('V is stored as a unitful quantity:', neuron.V.value[:3])
V is stored as a unitful quantity: [0. 0. 0.] mV

Unit-aware initializers#

State is rarely a single constant — membrane potentials might be drawn from a distribution, weights from a Kaiming scheme. braintools.init initializers take a unit= argument so the array they produce carries the right dimension. Pass the initializer object to the model; it is invoked with the correct shape during init_all_states.

# Heterogeneous initial voltages, in millivolts.
neuron = brainpy.state.LIF(
    1000,
    V_rest=-65. * u.mV, V_th=-50. * u.mV, V_reset=-65. * u.mV, tau=10. * u.ms,
    V_initializer=braintools.init.Normal(-55., 2., unit=u.mV),
)
brainstate.nn.init_all_states(neuron)
print('mean initial V:', u.math.mean(neuron.V.value))

# Synaptic conductances initialized to zero, with units.
syn = brainpy.state.Expon(
    1000, tau=5. * u.ms,
    g_initializer=braintools.init.Constant(0. * u.mS),
)
brainstate.nn.init_all_states(syn)
print('initial g unit matches mS:', syn.g.value[:3])
mean initial V: -55.00637 mV
initial g unit matches mS: [0. 0. 0.] mS

Common initializers you’ll reach for:

  • braintools.init.Constant(value) — a fixed value (often 0. * u.mS).

  • braintools.init.Normal(mean, std, unit=...) — Gaussian, e.g. heterogeneous V.

  • braintools.init.Uniform(low, high, unit=...) — uniform over a range.

  • braintools.init.KaimingNormal(unit=...) — weight initialization for trainable layers (see Differentiability).

  • braintools.init.ZeroInit(unit=...) — zeros with a unit, e.g. a bias in u.mA.

Pitfalls the unit system catches at construction#

These mistakes would be invisible in a unitless framework and produce subtly (or catastrophically) wrong dynamics. Here they fail loudly and early.

# Pitfall 1: a time constant given in the wrong dimension.
try:
    brainpy.state.LIF(10, tau=10. * u.mV)   # mV where ms is required
except Exception as err:
    print('caught wrong-dimension tau:', type(err).__name__)
# Pitfall 2: mixing conductance scales. mS and nS differ by 1e6;
# the algebra keeps them straight instead of silently adding magnitudes.
g_total = 0.6 * u.mS + 50. * u.nS
print('0.6 mS + 50 nS =', g_total.to_decimal(u.mS), 'mS (not 50.6)')
0.6 mS + 50 nS = 0.60005 mS (not 50.6)

Recap#

  • Quantities are magnitude × unit; arithmetic is dimensionally checked and derived units fall out automatically.

  • Construct models with unitful parameters; convert to plain numbers only explicitly, via .to_decimal(unit).

  • Unit-aware initializers (braintools.init.*(..., unit=...)) attach the right dimension to initial state.

  • The unit system turns whole categories of modeling error into exceptions raised at construction time.

See also#