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:
Voltage —
u.mV(millivolt)Time —
u.ms(millisecond)Conductance —
u.nS(nanosiemens),u.mS(millisiemens)Current —
u.mA(milliamp),u.nA(nanoamp)Frequency —
u.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 (often0. * u.mS).braintools.init.Normal(mean, std, unit=...)— Gaussian, e.g. heterogeneousV.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 inu.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#
The state paradigm — the states these units live in.
Model anatomy — where unitful parameters enter neurons and synapses.
Differentiability — unitful weight initializers for trainable layers.