Building General Dynamical Systems#

@Xiaoyu Chen @Chaoming Wang

The previous sections have shown how to build neuron models, synapse models, and network models. In fact, these brain objects all inherit the base class brainpy.dyn.DynamicalSystem, brainpy.dyn.DynamicalSystem is the universal language to define dynamical models in BrainPy.

To begin with, let’s make a rief summary of previous dynamic models and give the definition of a dynamical system.

import brainpy as bp
import brainpy.math as bm

bm.set_platform('cpu')

What is a dynamical system?#

Looking back to the neuron and synapse models defined in the previous sections, they share a common feature that they all contain some variables that change over time. Because of these variables, the models become ‘dynamic’ and behave differently at different times.

Actually, a dynamical system is defined as a system with time-dependent states. These time-dependent states are displayed as variables in the previous models.

Mathematically, the change of a state \(X\) can be expressed as

\[ \dot{X} = f(X, t) \]

where \(X\) is the state of the system, \(t\) is the time, and \(f\) is a function describing the time dependence of the state.

Alternatively, the evolution of the system over time can be given by

\[ X(t+dt) = F\left(X(t), t, dt\right) \]

where \(dt\) is the time step and \(F\) is the evolution rule to update the system’s state.

Customizing your dynamical systems#

According to the mathematical expression of a dynamical system, any subclass of brainpy.dyn.DynamicalSystem must implement an updating rule in the update(self, t, dt) function.

To define a dynamical system, the following requirements should be satisfied:

  • Inherit from brainpy.dyn.DynamicalSystem.

  • Implement the update(self, t, dt) function.

  • When defining variables, they should be declared as brainpy.math.Variable.

  • When updating the variables, it should be realized by in-place operations.

Below is a simple example of a dynamical system.

class FitzHughNagumoModel(bp.dyn.DynamicalSystem):
    def __init__(self, a=0.8, b=0.7, tau=12.5, **kwargs):
        super(FitzHughNagumoModel, self).__init__(**kwargs)
        
        # parameters
        self.a = a
        self.b = b
        self.tau = tau
        
        # variables should be packed by brainpy.math.Variable
        self.v = bm.Variable([0.])
        self.w = bm.Variable([0.])
        self.I = bm.Variable([0.])
        
    def update(self, _t, _dt):
        # _t : the current time, the system keyword 
        # _dt : the time step, the system keyword 
        
        # in-place update
        self.w += (self.v + self.a - self.b * self.w) / self.tau * _dt
        self.v += (self.v - self.v ** 3 / 3 - self.w + self.I) * _dt
        self.I[:] = 0.

Here, we have defined a dynamical system called FitzHugh–Nagumo neuron model, whose dynamics is given by:

\[\begin{split} {\dot {v}}=v-{\frac {v^{3}}{3}}-w+I, \\ \tau {\dot {w}}=v+a-bw. \end{split}\]

By using the Euler method, this system can be updated by the following rule:

\[\begin{split} \begin{aligned} v(t+dt) &= v(t) + [v(t)-{v(t)^{3}/3}-w(t)+RI] * dt, \\ w(t + dt) &= w(t) + [v(t) + a - b w(t)] * dt. \end{aligned} \end{split}\]

Advantages of using brainpy.dyn.DynamicalSystem#

There are several advantages of defining a dynamical system as brainpy.dyn.DynamicalSystem.

1. A systematic naming system.#

First, every instance of DynamicalSystem has its unique name.

fhn = FitzHughNagumoModel()
fhn.name  # name for "fhn" instance
'FitzHughNagumoModel1'

Every instance has its unique name:

for _ in range(3):
    print(FitzHughNagumoModel().name)
FitzHughNagumoModel2
FitzHughNagumoModel3
FitzHughNagumoModel4

Users can also specify the name of a dynamic system:

fhn2 = FitzHughNagumoModel(name='X')

fhn2.name
'X'
# same name will cause error

try:
    FitzHughNagumoModel(name='X')
except bp.errors.UniqueNameError as e:
    print(e)
In BrainPy, each object should have a unique name. However, we detect that <__main__.FitzHughNagumoModel object at 0x000001F75163C250> has a used name "X". 
If you try to run multiple trials, you may need 

>>> brainpy.base.clear_name_cache() 

to clear all cached names. 

Second, variables, children nodes, etc. inside an instance can be easily accessed by their absolute or relative path.

# All variables can be acessed by 
# 1). the absolute path

fhn2.vars()
{'X.I': Variable([0.], dtype=float32),
 'X.v': Variable([0.], dtype=float32),
 'X.w': Variable([0.], dtype=float32)}
# 2). or, the relative path

fhn2.vars(method='relative')
{'I': Variable([0.], dtype=float32),
 'v': Variable([0.], dtype=float32),
 'w': Variable([0.], dtype=float32)}

2. Convenient operations for simulation and analysis.#

Brainpy provides different runners for dynamics simulation and analyzers for dynamics analysis, both of which require the dynamic model to be Brainpy.dyn.DynamicalSystem. For example, dynamic models can be packed by a runner for simulation:

runner = bp.dyn.DSRunner(fhn2, monitors=['v', 'w'], inputs=('I', 1.5))
runner(duration=100)

bp.visualize.line_plot(runner.mon.ts, runner.mon.v, legend='v', show=False)
bp.visualize.line_plot(runner.mon.ts, runner.mon.w, legend='w', show=True)
../_images/1d6c842b89995512f4131dde31055b898f1792fda5733429a944f69838e70c3d.png

Please see Runners to know more about the operations in runners.

3. Efficient computation.#

brainpy.dyn.DynamicalSystem is a subclass of brainpy.Base, and therefore, any instance of brainpy.dyn.DynamicalSystem can be complied just-in-time into efficient machine codes targeting on CPUs, GPUs, and TPUs.

runner = bp.dyn.DSRunner(fhn2, monitors=['v', 'w'], inputs=('I', 1.5), jit=True)
runner(duration=100)

bp.visualize.line_plot(runner.mon.ts, runner.mon.v, legend='v', show=False)
bp.visualize.line_plot(runner.mon.ts, runner.mon.w, legend='w', show=True)
../_images/2163b84aaaa513520c713fa2d6a9557d2931bd0d332090ac33c26daf4cdf5a5a.png

4. Support composable programming.#

Instances of brainpy.dyn.DynamicalSystem can be combined at will. The combined system is also a brainpy.dyn.DynamicalSystem and enjoys all the properties, methods, and interfaces provided by brainpy.dyn.DynamicalSystem.

For example, if the instances are wrapped into a container, i.e. brainpy.dyn.Network, variables and nodes can also be accessed by their absolute or relative path.

fhn_net = bp.dyn.Network(f1=fhn, f2=fhn2)
# absolute access of variables

fhn_net.vars()
{'FitzHughNagumoModel1.I': Variable([0.], dtype=float32),
 'FitzHughNagumoModel1.v': Variable([0.], dtype=float32),
 'FitzHughNagumoModel1.w': Variable([0.], dtype=float32),
 'X.I': Variable([0.], dtype=float32),
 'X.v': Variable([1.492591], dtype=float32),
 'X.w': Variable([1.9365357], dtype=float32)}
# relative access of variables

fhn_net.vars(method='relative')
{'f1.I': Variable([0.], dtype=float32),
 'f1.v': Variable([0.], dtype=float32),
 'f1.w': Variable([0.], dtype=float32),
 'f2.I': Variable([0.], dtype=float32),
 'f2.v': Variable([1.492591], dtype=float32),
 'f2.w': Variable([1.9365357], dtype=float32)}
# absolute access of nodes

fhn_net.nodes()
{'FitzHughNagumoModel1': <__main__.FitzHughNagumoModel at 0x1f7515a74c0>,
 'X': <__main__.FitzHughNagumoModel at 0x1f75164bd90>,
 'Network0': <brainpy.dyn.base.Network at 0x1f7529e70d0>}
# relative access of nodes

fhn_net.nodes(method='relative')
{'': <brainpy.dyn.base.Network at 0x1f7529e70d0>,
 'f1': <__main__.FitzHughNagumoModel at 0x1f7515a74c0>,
 'f2': <__main__.FitzHughNagumoModel at 0x1f75164bd90>}
runner = bp.dyn.DSRunner(fhn_net,
                         monitors=['f1.v', 'X.v'], 
                         inputs=[('f1.I', 1.5),   # relative access to variable "I" in 'fhn1'
                                 ('X.I', 1.0),])  # absolute access to variable "I" in 'fhn2'
runner(duration=100)

bp.visualize.line_plot(runner.mon.ts, runner.mon['f1.v'], legend='fhn1.v', show=False)
bp.visualize.line_plot(runner.mon.ts, runner.mon['X.v'], legend='fhn2.v', show=True)
../_images/437ee545760015a04d75ba129c3c6328e8d0e3da53c853687d18050f02fe8057.png